diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a53..61b27434e4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -114,6 +114,7 @@ enum { OPT_NO_VD_SYSTEM_DECORATIONS, OPT_NO_VD_DESTROY_CONTENT, OPT_DISPLAY_IME_POLICY, + OPT_ROOT, }; struct sc_option { @@ -841,6 +842,15 @@ static const struct sc_option options[] = { "fails on the device. This option makes scrcpy fail if audio " "is enabled but does not work." }, + { + .longopt_id = OPT_ROOT, + .longopt = "root", + .text = "By default, scrcpy simply uses the ADB shell user context to " + "execute the server on the device. This option instructs it to " + "use the 'su' command to run the server as the system user in " + "order to retain the ability to create a secure display. This " + "allows apps that use `FLAG_SECURE` to be mirrored to your PC." + }, { // deprecated .longopt_id = OPT_ROTATION, @@ -2724,6 +2734,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; + case OPT_ROOT: + opts->root = true; + opts->clipboard_autosync = false; + LOGI("Clipboard autosync breaks when root is used to create a" + " secure display.\n" + "The shell user has direct Binder bypasses such that it" + " is allowed access to the\n" + "clipboard unrestricted. However, the system user does not" + " have this bypass.\n" + "It can't read its contents or set up listeners, but it" + " has write permissions.\n" + "Until further notice, clipboard autosync will be forcibly" + " disabled with --root."); + break; case OPT_AUDIO_BUFFER: if (!parse_buffering_time(optarg, &opts->audio_buffer)) { return false; diff --git a/app/src/options.c b/app/src/options.c index 0fe82d291b..dfb955048d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -113,6 +113,7 @@ const struct scrcpy_options scrcpy_options_default = { .angle = NULL, .vd_destroy_content = true, .vd_system_decorations = true, + .root = false, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 03b4291344..cba76e686e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -327,6 +327,7 @@ struct scrcpy_options { const char *start_app; bool vd_destroy_content; bool vd_system_decorations; + bool root; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9cf8..4671dcee7f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -473,6 +473,7 @@ scrcpy(struct scrcpy_options *options) { .camera_high_speed = options->camera_high_speed, .vd_destroy_content = options->vd_destroy_content, .vd_system_decorations = options->vd_system_decorations, + .root = options->root, .list = options->list, }; diff --git a/app/src/server.c b/app/src/server.c index 153219c3f4..725abd7cc5 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -215,6 +215,11 @@ execute_server(struct sc_server *server, cmd[count++] = "-s"; cmd[count++] = serial; cmd[count++] = "shell"; + if (params->root) { + cmd[count++] = "su"; + cmd[count++] = "1000"; + cmd[count++] = "-c"; + } cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; cmd[count++] = "app_process"; diff --git a/app/src/server.h b/app/src/server.h index 5f4592de90..961e010dca 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -70,6 +70,7 @@ struct sc_server_params { bool camera_high_speed; bool vd_destroy_content; bool vd_system_decorations; + bool root; uint8_t list; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 5d41a8f3e8..4547474eae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -16,7 +16,7 @@ public final class FakeContext extends ContextWrapper { - public static final String PACKAGE_NAME = "com.android.shell"; + public static final String PACKAGE_NAME = Process.myUid() == 1000 ? "android" : "com.android.shell"; public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 private static final FakeContext INSTANCE = new FakeContext(); diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java index 4eb67d186b..8578432fba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -3,6 +3,8 @@ import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.os.Process; + public final class Settings { public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; @@ -16,23 +18,43 @@ private Settings() { public static String getValue(String table, String key) throws SettingsException { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); + } catch (Exception e) { + throw new SettingsException("getValue", table, key, null, e); } } public static void putValue(String table, String key, String value) throws SettingsException { - try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { - provider.putValue(table, key, value); - } + if (Process.myUid() == 1000) { + // UID_SYSTEM path: fallback to 'su -c settings put ...' + try { + String cmd = "settings put " + table + " " + key + " " + value; + String[] command = { "su", "-c", cmd }; - } + java.lang.Process proc = Runtime.getRuntime().exec(command); + int exit = proc.waitFor(); - public static String getAndPutValue(String table, String key, String value) throws SettingsException { - try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { - String oldValue = provider.getValue(table, key); - if (!value.equals(oldValue)) { + if (exit != 0) { + throw new SettingsException("putValue", table, key, value, null); + } + + } catch (Exception e) { + throw new SettingsException("putValue", table, key, value, e); + } + } else { + // AID_SHELL path: use standard provider + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); + } catch (Exception e) { + throw new SettingsException("putValue", table, key, value, e); } - return oldValue; } } + + public static String getAndPutValue(String table, String key, String value) throws SettingsException { + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 5f4e1803f9..a1c80fd715 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -21,6 +21,7 @@ import android.hardware.display.VirtualDisplay; import android.os.Build; import android.os.IBinder; +import android.os.Process; import android.view.Surface; import java.io.IOException; @@ -196,8 +197,8 @@ public boolean setMaxSize(int newMaxSize) { private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". - boolean secure = Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11 - && !"S".equals(Build.VERSION.CODENAME)); + boolean secure = (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11 + && !"S".equals(Build.VERSION.CODENAME))) || android.os.Process.myUid() == 1000; return SurfaceControl.createDisplay("scrcpy", secure); }