From cc0902b13c87fc98b1ed90b0700cc53ac4d7ee3c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 10:05:10 +0100 Subject: [PATCH] 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 Fixes #2788 PR #2802 --- .../java/com/genymobile/scrcpy/Command.java | 33 ++++++++++ .../java/com/genymobile/scrcpy/Settings.java | 65 ++++++++++++++++--- 2 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Command.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java new file mode 100644 index 00000000..0ef976a6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java index 83b63477..cb15ebb4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -3,6 +3,10 @@ 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; @@ -15,25 +19,66 @@ public class Settings { this.serviceManager = serviceManager; } - public String getValue(String table, String key) throws SettingsException { - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { - return provider.getValue(table, key); + 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: + 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 { - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { - provider.putValue(table, key, value); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + 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 { - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { - String oldValue = provider.getValue(table, key); - if (!value.equals(oldValue)) { - provider.putValue(table, key, value); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + 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); } - return oldValue; } + + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; } }