Compare commits

...

19 commits

Author SHA1 Message Date
Romain Vimont e512b5bd69 Simplify adb_execute_p()
Only pass the stdout pipe as parameter, we never need stdin or stderr.
2021-11-17 21:59:59 +01:00
Romain Vimont b7328a75e3 Make "adb get-serialno" interruptible
All process execution from the server must be interruptible, so that
Ctrl+c reacts immediately.
2021-11-17 21:59:48 +01:00
Romain Vimont 7516c0d1c5 Add interruptible function to read from pipe
This will avoid to block Ctrl+c if the process we read from takes too
much time.
2021-11-17 21:59:48 +01:00
Romain Vimont 6b4761820f Generalize string trunctation util function
Add an additional argument to let the client pass the possible end
chars.
2021-11-17 21:59:48 +01:00
Romain Vimont e83562a6fe Expose util function to truncate first line
Move the local implementation from adb functions to the string util
functions.
2021-11-17 21:59:48 +01:00
Romain Vimont 921044e8a4 Always retrieve device serial
This allows to execute all adb commands with the specific -s parameter,
even if it is not provided by the user.

In practice, calling adb without -s works if there is exactly one device
connected. But some adb commands could be executed after another device
is executed (for example on drag & drop), so they need the specific
device serial.
2021-11-17 21:59:48 +01:00
Romain Vimont b49f4e795e Add missing error handling
If "adb get-serialno" fails, attempting to read from the uninitialized
pipe is incorrect.
2021-11-17 21:59:19 +01:00
Romain Vimont 3eb8202adf Configure init and cleanup asynchronously
Accessing the settings (like --show-touches) on start should not delay
screen mirroring.
2021-11-17 20:09:22 +01:00
Romain Vimont 83f66ea06c Do not quit on cleanup configuration failure
Cleanup is used for some options like --show-touches to restore the
state on exit.

If the configuration fails, do not crash the whole process. Just log an
error.
2021-11-17 20:09:22 +01:00
Romain Vimont 3cc0921e61 Move init and cleanup to a separate method 2021-11-17 20:09:22 +01:00
Romain Vimont d8358ce857 Read/write settings via command on Android >= 12
Before Android 8, executing the "settings" command from a shell was
very slow (~1 second), because it spawned a new app_process to execute
Java code. Therefore, to access settings without performance issues,
scrcpy used private APIs to read from and write to settings.

However, since Android 12, this is not possible anymore, due to
permissions changes.

To make it work again, execute the "settings" command on Android 12 (or
on previous version if the other method failed). This method is faster
than before Android 8 (~100ms).

Fixes #2671 <https://github.com/Genymobile/scrcpy/issues/2671>
Fixes #2788 <https://github.com/Genymobile/scrcpy/issues/2788>
2021-11-17 20:08:54 +01:00
Romain Vimont 4bc5baeeb5 Add throwable parameter to Log.w()
When an exception occurs, we might want to log a warning instead of an
error.
2021-11-17 19:59:43 +01:00
Romain Vimont 0672f7e405 Report settings errors via Exceptions
Settings read/write errors were silently ignored. Report them via a
SettingsException so that the caller can handle them.

This allows to log a proper error message, and will also allow to
fallback to a different settings method in case of failure.
2021-11-17 19:59:27 +01:00
Romain Vimont 6d6a436563 Wrap settings management into a Settings class
Until now, the code that needed to read/write the Android settings had
to explicitly open and close a ContentProvider.

Wrap these details into a Settings class.

This paves the way to provide an alternative implementation of settings
read/write for Android >= 12.
2021-11-17 19:52:04 +01:00
Romain Vimont 9cb14b5166 Inherit only specific handles on Windows
To be able to communicate with a child process via stdin, stdout and
stderr, the CreateProcess() parameter bInheritHandles must be set to
TRUE. But this causes *all* handles to be inherited, including sockets.

As a result, the server socket was inherited by the process running adb
to execute the server on the device, so it could not be closed properly,
causing other scrcpy instances to fail.

To fix the issue, use an extended API to explicitly set the HANDLEs to
inherit:
 - <https://stackoverflow.com/a/28185363/1987178>
 - <https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873>

Fixes #2779 <https://github.com/Genymobile/scrcpy/issues/2779>
PR #2783 <https://github.com/Genymobile/scrcpy/pull/2783>
2021-11-15 10:13:30 +01:00
Romain Vimont 9cb8766220 Factorize resource release after CreateProcess()
Free the wide characters string in all cases before checking for errors.
2021-11-15 07:49:01 +01:00
Romain Vimont fd4ec784e0 Remove useless assignments on error
Leave the output parameter untouched on error.
2021-11-14 22:53:49 +01:00
Romain Vimont 52cebe1597 Fix Windows sc_pipe function names
The implementation name was incorrect (it was harmless, because they are
not used on Windows).
2021-11-14 22:41:38 +01:00
Romain Vimont 6a27062f48 Stop connection attempts if interrupted
If the interruptor is interrupted, every network call will fail, but the
retry-on-error mechanism must also be stopped.
2021-11-14 15:40:59 +01:00
18 changed files with 439 additions and 125 deletions

View file

@ -111,7 +111,7 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
sc_pid
adb_execute_p(const char *serial, const char *const adb_cmd[],
size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr) {
size_t len, sc_pipe *pout) {
int i;
sc_pid pid;
@ -132,7 +132,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
memcpy(&argv[i], adb_cmd, len * sizeof(const char *));
argv[len + i] = NULL;
enum sc_process_result r =
sc_process_execute_p(argv, &pid, pin, pout, perr);
sc_process_execute_p(argv, &pid, NULL, pout, NULL);
if (r != SC_PROCESS_SUCCESS) {
show_adb_err_msg(r, argv);
pid = SC_PROCESS_NONE;
@ -144,7 +144,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
sc_pid
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
return adb_execute_p(serial, adb_cmd, len, NULL, NULL, NULL);
return adb_execute_p(serial, adb_cmd, len, NULL);
}
sc_pid
@ -233,45 +233,8 @@ adb_install(const char *serial, const char *local) {
return pid;
}
static ssize_t
adb_execute_for_output(const char *serial, const char *const adb_cmd[],
size_t adb_cmd_len, char *buf, size_t buf_len,
const char *name) {
sc_pipe pout;
sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL);
ssize_t r = sc_pipe_read_all(pout, buf, buf_len);
sc_pipe_close(pout);
if (!sc_process_check_success(pid, name, true)) {
return -1;
}
return r;
}
static size_t
truncate_first_line(char *data, size_t len) {
data[len - 1] = '\0';
char *eol = strpbrk(data, "\r\n");
if (eol) {
*eol = '\0';
len = eol - data;
}
return len;
}
char *
adb_get_serialno(void) {
char buf[128];
sc_pid
adb_get_serialno(sc_pipe *pout) {
const char *const adb_cmd[] = {"get-serialno"};
ssize_t r = adb_execute_for_output(NULL, adb_cmd, ARRAY_LEN(adb_cmd),
buf, sizeof(buf), "get-serialno");
if (r <= 0) {
return NULL;
}
truncate_first_line(buf, r);
return strdup(buf);
return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout);
}

View file

@ -12,8 +12,8 @@ sc_pid
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
sc_pid
adb_execute_p(const char *serial, const char *const adb_cmd[],
size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr);
adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len,
sc_pipe *pout);
sc_pid
adb_forward(const char *serial, uint16_t local_port,
@ -35,8 +35,8 @@ adb_push(const char *serial, const char *local, const char *remote);
sc_pid
adb_install(const char *serial, const char *local);
// Return the result of "adb get-serialno".
char *
adb_get_serialno(void);
// Execute "adb get-serialno" and give a pipe to get the result
sc_pid
adb_get_serialno(sc_pipe *pout);
#endif

View file

@ -394,8 +394,11 @@ scrcpy(struct scrcpy_options *options) {
// It is necessarily initialized here, since the device is connected
struct sc_server_info *info = &s->server.info;
const char *serial = s->server.params.serial;
assert(serial);
if (options->display && options->control) {
if (!file_handler_init(&s->file_handler, options->serial,
if (!file_handler_init(&s->file_handler, serial,
options->push_target)) {
goto end;
}
@ -516,21 +519,7 @@ scrcpy(struct scrcpy_options *options) {
#ifdef HAVE_AOA_HID
bool aoa_hid_ok = false;
char *serialno = NULL;
const char *serial = options->serial;
if (!serial) {
serialno = adb_get_serialno();
if (!serialno) {
LOGE("Could not get device serial");
goto aoa_hid_end;
}
serial = serialno;
LOGI("Device serial: %s", serial);
}
bool ok = sc_aoa_init(&s->aoa, serial);
free(serialno);
if (!ok) {
goto aoa_hid_end;
}

View file

@ -234,6 +234,12 @@ connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) {
net_close(socket);
}
if (sc_intr_is_interrupted(&server->intr)) {
// Stop immediately
break;
}
if (attempts) {
sc_mutex_lock(&server->mutex);
sc_tick deadline = sc_tick_now() + delay;
@ -416,12 +422,59 @@ sc_server_on_terminated(void *userdata) {
LOGD("Server terminated");
}
static char *
sc_server_get_serialno(struct sc_intr *intr) {
sc_pipe pout;
sc_pid pid = adb_get_serialno(&pout);
if (pid == SC_PROCESS_NONE) {
return false;
}
char buf[128];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
sc_pipe_close(pout);
bool ok = sc_process_check_success_intr(intr, pid, "adb get-serialno");
sc_process_close(pid);
if (!ok) {
return NULL;
}
sc_str_truncate(buf, r, " \r\n");
return strdup(buf);
}
static bool
sc_server_fill_serial(struct sc_server *server) {
// Retrieve the actual device immediately if not provided, so that all
// future adb commands are executed for this specific device, even if other
// devices are connected afterwards (without "more than one
// device/emulator" error)
if (!server->params.serial) {
// The serial is owned by sc_server_params, and will be freed on destroy
server->params.serial = sc_server_get_serialno(&server->intr);
if (!server->params.serial) {
LOGE("Could not get device serial");
return false;
}
}
return true;
}
static int
run_server(void *data) {
struct sc_server *server = data;
if (!sc_server_fill_serial(server)) {
goto error_connection_failed;
}
const struct sc_server_params *params = &server->params;
LOGD("Device serial: %s", params->serial);
bool ok = push_server(&server->intr, params->serial);
if (!ok) {
goto error_connection_failed;

View file

@ -1,5 +1,10 @@
// <https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873>
#define _WIN32_WINNT 0x0600 // For extended process API
#include "util/process.h"
#include <processthreadsapi.h>
#include <assert.h>
#include "util/log.h"
@ -26,6 +31,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
HANDLE *pin, HANDLE *pout, HANDLE *perr) {
enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC;
// Add 1 per non-NULL pointer
unsigned handle_count = !!pin + !!pout + !!perr;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
@ -65,45 +73,94 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
}
}
STARTUPINFOW si;
STARTUPINFOEXW si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
if (pin || pout || perr) {
si.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.cb = sizeof(si);
HANDLE handles[3];
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL;
if (handle_count) {
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
if (pin) {
si.hStdInput = stdin_read_handle;
si.StartupInfo.hStdInput = stdin_read_handle;
}
if (pout) {
si.hStdOutput = stdout_write_handle;
si.StartupInfo.hStdOutput = stdout_write_handle;
}
if (perr) {
si.hStdError = stderr_write_handle;
si.StartupInfo.hStdError = stderr_write_handle;
}
SIZE_T size;
// Call it once to know the required buffer size
BOOL ok =
InitializeProcThreadAttributeList(NULL, 1, 0, &size)
|| GetLastError() == ERROR_INSUFFICIENT_BUFFER;
if (!ok) {
goto error_close_stderr;
}
lpAttributeList = malloc(size);
if (!lpAttributeList) {
goto error_close_stderr;
}
ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size);
if (!ok) {
free(lpAttributeList);
goto error_close_stderr;
}
// Explicitly pass the HANDLEs that must be inherited
unsigned i = 0;
if (pin) {
handles[i++] = stdin_read_handle;
}
if (pout) {
handles[i++] = stdout_write_handle;
}
if (perr) {
handles[i++] = stderr_write_handle;
}
ok = UpdateProcThreadAttribute(lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handles, handle_count * sizeof(HANDLE),
NULL, NULL);
if (!ok) {
goto error_free_attribute_list;
}
si.lpAttributeList = lpAttributeList;
}
char *cmd = malloc(CMD_MAX_LEN);
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) {
*handle = NULL;
goto error_close_stderr;
goto error_free_attribute_list;
}
wchar_t *wide = sc_str_to_wchars(cmd);
free(cmd);
if (!wide) {
LOGC("Could not allocate wide char string");
goto error_close_stderr;
goto error_free_attribute_list;
}
if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si,
&pi)) {
free(wide);
*handle = NULL;
BOOL bInheritHandles = handle_count > 0;
DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT : 0;
BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles,
dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi);
free(wide);
if (!ok) {
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
ret = SC_PROCESS_ERROR_MISSING_BINARY;
}
goto error_close_stderr;
goto error_free_attribute_list;
}
if (lpAttributeList) {
DeleteProcThreadAttributeList(lpAttributeList);
free(lpAttributeList);
}
// These handles are used by the child process, close them for this process
@ -117,11 +174,15 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
CloseHandle(stderr_write_handle);
}
free(wide);
*handle = pi.hProcess;
return SC_PROCESS_SUCCESS;
error_free_attribute_list:
if (lpAttributeList) {
DeleteProcThreadAttributeList(lpAttributeList);
free(lpAttributeList);
}
error_close_stderr:
if (perr) {
CloseHandle(*perr);
@ -168,7 +229,7 @@ sc_process_close(HANDLE handle) {
}
ssize_t
sc_read_pipe(HANDLE pipe, char *data, size_t len) {
sc_pipe_read(HANDLE pipe, char *data, size_t len) {
DWORD r;
if (!ReadFile(pipe, data, len, &r, NULL)) {
return -1;
@ -177,7 +238,7 @@ sc_read_pipe(HANDLE pipe, char *data, size_t len) {
}
void
sc_close_pipe(HANDLE pipe) {
sc_pipe_close(HANDLE pipe) {
if (!CloseHandle(pipe)) {
LOGW("Cannot close pipe");
}

View file

@ -14,3 +14,31 @@ sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
sc_intr_set_process(intr, SC_PROCESS_NONE);
return ret;
}
ssize_t
sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data,
size_t len) {
if (!sc_intr_set_process(intr, pid)) {
// Already interrupted
return false;
}
ssize_t ret = sc_pipe_read(pipe, data, len);
sc_intr_set_process(intr, SC_PROCESS_NONE);
return ret;
}
ssize_t
sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe,
char *data, size_t len) {
if (!sc_intr_set_process(intr, pid)) {
// Already interrupted
return false;
}
ssize_t ret = sc_pipe_read_all(pipe, data, len);
sc_intr_set_process(intr, SC_PROCESS_NONE);
return ret;
}

View file

@ -10,4 +10,12 @@ bool
sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
const char *name);
ssize_t
sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data,
size_t len);
ssize_t
sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe,
char *data, size_t len);
#endif

View file

@ -291,3 +291,14 @@ error:
free(buf.s);
return NULL;
}
size_t
sc_str_truncate(char *data, size_t len, const char *endchars) {
data[len - 1] = '\0';
char *eol = strpbrk(data, endchars);
if (eol) {
*eol = '\0';
len = eol - data;
}
return len;
}

View file

@ -103,4 +103,15 @@ sc_str_from_wchars(const wchar_t *s);
char *
sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent);
/**
* Truncate the data after any of the characters from `endchars`
*
* An '\0' is always written at the end of the data, even if no newline
* character is encountered.
*
* Return the size of the resulting line.
*/
size_t
sc_str_truncate(char *data, size_t len, const char *endchars);
#endif

View file

@ -337,6 +337,26 @@ static void test_wrap_lines(void) {
free(formatted);
}
static void test_truncate(void) {
char s[] = "hello\nworld\n!";
size_t line_len = sc_str_truncate(s, sizeof(s), "\n");
assert(line_len == 5);
assert(!strcmp("hello", s));
char s2[] = "hello\r\nworkd\r\n!";
line_len = sc_str_truncate(s2, sizeof(s2), "\n\r");
assert(line_len == 5);
assert(!strcmp("hello", s));
char s3[] = "hello world\n!";
line_len = sc_str_truncate(s3, sizeof(s3), " \n\r");
assert(line_len == 5);
assert(!strcmp("hello", s3));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -356,5 +376,6 @@ int main(int argc, char *argv[]) {
test_parse_integer_with_suffix();
test_strlist_contains();
test_wrap_lines();
test_truncate();
return 0;
}

View file

@ -1,6 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
@ -166,14 +165,21 @@ public final class CleanUp {
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
Settings settings = new Settings(serviceManager);
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
try {
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e);
}
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
}
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
try {
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
} catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
}
}
}

View file

@ -0,0 +1,33 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.util.Arrays;
import java.util.Scanner;
public final class Command {
private Command() {
// not instantiable
}
public static void exec(String... cmd) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
}
public static String execReadLine(String... cmd) throws IOException, InterruptedException {
String result = null;
Process process = Runtime.getRuntime().exec(cmd);
Scanner scanner = new Scanner(process.getInputStream());
if (scanner.hasNextLine()) {
result = scanner.nextLine();
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
return result;
}
}

View file

@ -1,7 +1,6 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
@ -29,6 +28,7 @@ public final class Device {
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
private static final Settings SETTINGS = new Settings(SERVICE_MANAGER);
public interface RotationListener {
void onRotationChanged(int rotation);
@ -296,7 +296,7 @@ public final class Device {
}
}
public static ContentProvider createSettingsProvider() {
return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
public static Settings getSettings() {
return SETTINGS;
}
}

View file

@ -57,13 +57,20 @@ public final class Ln {
}
}
public static void w(String message) {
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message);
Log.w(TAG, message, throwable);
System.out.println(PREFIX + "WARN: " + message);
if (throwable != null) {
throwable.printStackTrace();
}
}
}
public static void w(String message) {
w(message, null);
}
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);

View file

@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
@ -19,24 +17,25 @@ public final class Server {
// not instantiable
}
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
private static void initAndCleanUp(Options options) {
boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1;
if (options.getShowTouches() || options.getStayAwake()) {
try (ContentProvider settings = Device.createSettingsProvider()) {
if (options.getShowTouches()) {
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
Settings settings = Device.getSettings();
if (options.getShowTouches()) {
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) {
@ -46,11 +45,25 @@ public final class Server {
} catch (NumberFormatException e) {
restoreStayOn = 0;
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
}
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
try {
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
} catch (IOException e) {
Ln.e("Could not configure cleanup", e);
}
}
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
Thread initThread = startInitThread(options);
boolean tunnelForward = options.isTunnelForward();
@ -82,6 +95,7 @@ public final class Server {
// this is expected on close
Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt();
if (controllerThread != null) {
controllerThread.interrupt();
}
@ -92,6 +106,17 @@ public final class Server {
}
}
private static Thread startInitThread(final Options options) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
initAndCleanUp(options);
}
});
thread.start();
return thread;
}
private static Thread startController(final Controller controller) {
Thread thread = new Thread(new Runnable() {
@Override

View file

@ -0,0 +1,84 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import java.io.IOException;
public class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
private final ServiceManager serviceManager;
public Settings(ServiceManager serviceManager) {
this.serviceManager = serviceManager;
}
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
try {
Command.exec("settings", "put", table, key, value);
} catch (IOException | InterruptedException e) {
throw new SettingsException("put", table, key, value, e);
}
}
private static String execSettingsGet(String table, String key) throws SettingsException {
try {
return Command.execReadLine("settings", "get", table, key);
} catch (IOException | InterruptedException e) {
throw new SettingsException("get", table, key, null, e);
}
}
public String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
} catch (SettingsException e) {
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
}
}
return execSettingsGet(table, key);
}
public void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
} catch (SettingsException e) {
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
}
}
execSettingsPut(table, key, value);
}
public String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
}
return oldValue;
} catch (SettingsException e) {
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
}
}
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
}
return oldValue;
}
}

View file

@ -0,0 +1,11 @@
package com.genymobile.scrcpy;
public class SettingsException extends Exception {
private static String createMessage(String method, String table, String key, String value) {
return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
}
public SettingsException(String method, String table, String key, String value, Throwable cause) {
super(createMessage(method, table, key, value), cause);
}
}

View file

@ -1,6 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import android.annotation.SuppressLint;
import android.os.Bundle;
@ -87,7 +88,8 @@ public class ContentProvider implements Closeable {
return attributionSource;
}
private Bundle call(String callMethod, String arg, Bundle extras) {
private Bundle call(String callMethod, String arg, Bundle extras)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
try {
Method method = getCallMethod();
Object[] args;
@ -108,7 +110,7 @@ public class ContentProvider implements Closeable {
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
Ln.e("Could not invoke method", e);
return null;
throw e;
}
}
@ -142,30 +144,31 @@ public class ContentProvider implements Closeable {
}
}
public String getValue(String table, String key) {
public String getValue(String table, String key) throws SettingsException {
String method = getGetMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
try {
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
}
return bundle.getString("value");
} catch (Exception e) {
throw new SettingsException(table, "get", key, null, e);
}
return bundle.getString("value");
}
public void putValue(String table, String key, String value) {
public void putValue(String table, String key, String value) throws SettingsException {
String method = getPutMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putString(NAME_VALUE_TABLE_VALUE, value);
call(method, key, arg);
}
public String getAndPutValue(String table, String key, String value) {
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
try {
call(method, key, arg);
} catch (Exception e) {
throw new SettingsException(table, "put", key, value, e);
}
return oldValue;
}
}