From 67170437f1eb2c9f2780d720b3079d7d8722b9f4 Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Tue, 16 Nov 2021 22:10:34 +0100
Subject: [PATCH 1/7] 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.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 .../java/com/genymobile/scrcpy/CleanUp.java   | 18 ++++-----
 .../java/com/genymobile/scrcpy/Device.java    |  6 +--
 .../java/com/genymobile/scrcpy/Server.java    | 35 ++++++++---------
 .../java/com/genymobile/scrcpy/Settings.java  | 39 +++++++++++++++++++
 .../scrcpy/wrappers/ContentProvider.java      |  8 ----
 5 files changed, 66 insertions(+), 40 deletions(-)
 create mode 100644 server/src/main/java/com/genymobile/scrcpy/Settings.java

diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
index ec61a1c04d..9001eef70d 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
+++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
@@ -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,15 +165,14 @@ public static void main(String... args) {
 
         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");
-                }
-                if (config.restoreStayOn != -1) {
-                    Ln.i("Restoring \"stay awake\"");
-                    settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
-                }
+            Settings settings = new Settings(serviceManager);
+            if (config.disableShowTouches) {
+                Ln.i("Disabling \"show touches\"");
+                settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
+            }
+            if (config.restoreStayOn != -1) {
+                Ln.i("Restoring \"stay awake\"");
+                settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
             }
         }
 
diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java
index 3e71fe9ce3..093646e2bb 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Device.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Device.java
@@ -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 static void rotateDevice() {
         }
     }
 
-    public static ContentProvider createSettingsProvider() {
-        return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
+    public static Settings getSettings() {
+        return SETTINGS;
     }
 }
diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index fdd9db8877..c900f872ba 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -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;
@@ -27,25 +25,24 @@ private static void scrcpy(Options options) throws IOException {
         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");
-                    // If "show touches" was disabled, it must be disabled back on clean up
-                    mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
-                }
+            Settings settings = Device.getSettings();
+            if (options.getShowTouches()) {
+                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);
+            }
 
-                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));
-                    try {
-                        restoreStayOn = Integer.parseInt(oldValue);
-                        if (restoreStayOn == stayOn) {
-                            // No need to restore
-                            restoreStayOn = -1;
-                        }
-                    } catch (NumberFormatException e) {
-                        restoreStayOn = 0;
+            if (options.getStayAwake()) {
+                int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
+                String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
+                try {
+                    restoreStayOn = Integer.parseInt(oldValue);
+                    if (restoreStayOn == stayOn) {
+                        // No need to restore
+                        restoreStayOn = -1;
                     }
+                } catch (NumberFormatException e) {
+                    restoreStayOn = 0;
                 }
             }
         }
diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java
new file mode 100644
index 0000000000..b59188d56d
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java
@@ -0,0 +1,39 @@
+package com.genymobile.scrcpy;
+
+import com.genymobile.scrcpy.wrappers.ContentProvider;
+import com.genymobile.scrcpy.wrappers.ServiceManager;
+
+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;
+    }
+
+    public String getValue(String table, String key) {
+        try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
+            return provider.getValue(table, key);
+        }
+    }
+
+    public void putValue(String table, String key, String value) {
+        try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
+            provider.putValue(table, key, value);
+        }
+    }
+
+    public String getAndPutValue(String table, String key, String value) {
+        try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
+            String oldValue = provider.getValue(table, key);
+            if (!value.equals(oldValue)) {
+                provider.putValue(table, key, value);
+            }
+            return oldValue;
+        }
+    }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
index 387c7a602b..ab95f0df52 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
@@ -160,12 +160,4 @@ public void putValue(String table, String key, String value) {
         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);
-        }
-        return oldValue;
-    }
 }

From 94feae71f2bf603b1cbd78611a2fdecdfcf40b67 Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Tue, 16 Nov 2021 22:40:53 +0100
Subject: [PATCH 2/7] 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.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 .../java/com/genymobile/scrcpy/CleanUp.java   | 12 ++++++--
 .../java/com/genymobile/scrcpy/Server.java    | 28 +++++++++++-------
 .../java/com/genymobile/scrcpy/Settings.java  |  6 ++--
 .../genymobile/scrcpy/SettingsException.java  | 11 +++++++
 .../scrcpy/wrappers/ContentProvider.java      | 29 +++++++++++++------
 5 files changed, 62 insertions(+), 24 deletions(-)
 create mode 100644 server/src/main/java/com/genymobile/scrcpy/SettingsException.java

diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
index 9001eef70d..319a957d18 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
+++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
@@ -168,11 +168,19 @@ public static void main(String... args) {
             Settings settings = new Settings(serviceManager);
             if (config.disableShowTouches) {
                 Ln.i("Disabling \"show touches\"");
-                settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
+                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(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
+                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);
+                }
             }
         }
 
diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index c900f872ba..efb295b33b 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -27,22 +27,30 @@ private static void scrcpy(Options options) throws IOException {
         if (options.getShowTouches() || options.getStayAwake()) {
             Settings settings = Device.getSettings();
             if (options.getShowTouches()) {
-                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);
+                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(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
                 try {
-                    restoreStayOn = Integer.parseInt(oldValue);
-                    if (restoreStayOn == stayOn) {
-                        // No need to restore
-                        restoreStayOn = -1;
+                    String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
+                    try {
+                        restoreStayOn = Integer.parseInt(oldValue);
+                        if (restoreStayOn == stayOn) {
+                            // No need to restore
+                            restoreStayOn = -1;
+                        }
+                    } catch (NumberFormatException e) {
+                        restoreStayOn = 0;
                     }
-                } catch (NumberFormatException e) {
-                    restoreStayOn = 0;
+                } catch (SettingsException e) {
+                    Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
                 }
             }
         }
diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java
index b59188d56d..83b6347792 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Settings.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java
@@ -15,19 +15,19 @@ public Settings(ServiceManager serviceManager) {
         this.serviceManager = serviceManager;
     }
 
-    public String getValue(String table, String key) {
+    public String getValue(String table, String key) throws SettingsException {
         try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
             return provider.getValue(table, key);
         }
     }
 
-    public void putValue(String table, String key, String value) {
+    public void putValue(String table, String key, String value) throws SettingsException {
         try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
             provider.putValue(table, key, value);
         }
     }
 
-    public String getAndPutValue(String table, String key, String 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)) {
diff --git a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java
new file mode 100644
index 0000000000..36ef63ee1a
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java
@@ -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);
+    }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
index ab95f0df52..47eae64d1e 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
@@ -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 @@ private Object getAttributionSource()
         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 @@ private Bundle call(String callMethod, String arg, Bundle extras) {
             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,22 +144,31 @@ private static String getPutMethod(String table) {
         }
     }
 
-    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);
+        try {
+            call(method, key, arg);
+        } catch (Exception e) {
+            throw new SettingsException(table, "put", key, value, e);
+        }
     }
 }

From 48b572c272901af1b32afa204ae011ebb9344b40 Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Wed, 17 Nov 2021 10:01:13 +0100
Subject: [PATCH 3/7] Add throwable parameter to Log.w()

When an exception occurs, we might want to log a warning instead of an
error.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 server/src/main/java/com/genymobile/scrcpy/Ln.java | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java
index 061cda9595..c39fc621c7 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Ln.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java
@@ -57,13 +57,20 @@ public static void i(String message) {
         }
     }
 
-    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);

From cc0902b13c87fc98b1ed90b0700cc53ac4d7ee3c Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Wed, 17 Nov 2021 10:05:10 +0100
Subject: [PATCH 4/7] 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>
PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 .../java/com/genymobile/scrcpy/Command.java   | 33 ++++++++++
 .../java/com/genymobile/scrcpy/Settings.java  | 63 ++++++++++++++++---
 2 files changed, 87 insertions(+), 9 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 0000000000..0ef976a66c
--- /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 83b6347792..cb15ebb46b 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 @@
 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 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 {
-        try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
-            return provider.getValue(table, key);
+        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 {
-        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: <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 {
-        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: <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);
             }
-            return oldValue;
         }
+
+        String oldValue = getValue(table, key);
+        if (!value.equals(oldValue)) {
+             putValue(table, key, value);
+        }
+        return oldValue;
     }
 }

From 411bb0d18e4d09f10480d2a60dcb7187b4fc62aa Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Wed, 17 Nov 2021 10:16:11 +0100
Subject: [PATCH 5/7] Move init and cleanup to a separate method

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 .../main/java/com/genymobile/scrcpy/Server.java    | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index efb295b33b..488a877f7b 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -17,11 +17,7 @@ private 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) throws IOException {
         boolean mustDisableShowTouchesOnCleanUp = false;
         int restoreStayOn = -1;
         if (options.getShowTouches() || options.getStayAwake()) {
@@ -56,6 +52,14 @@ private static void scrcpy(Options options) throws IOException {
         }
 
         CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
+    }
+
+    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());
+
+        initAndCleanUp(options);
 
         boolean tunnelForward = options.isTunnelForward();
 

From c29a0bf675dc94fd350ea696d3ffa772651ff4bb Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Wed, 17 Nov 2021 10:21:42 +0100
Subject: [PATCH 6/7] 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.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 server/src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index 488a877f7b..db5bc7eca9 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -17,7 +17,7 @@ private Server() {
         // not instantiable
     }
 
-    private static void initAndCleanUp(Options options) throws IOException {
+    private static void initAndCleanUp(Options options) {
         boolean mustDisableShowTouchesOnCleanUp = false;
         int restoreStayOn = -1;
         if (options.getShowTouches() || options.getStayAwake()) {
@@ -51,7 +51,11 @@ private static void initAndCleanUp(Options options) throws IOException {
             }
         }
 
-        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 {

From ee93d2aac17ddb996ba42e1f898e34a970ade88a Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Wed, 17 Nov 2021 10:29:41 +0100
Subject: [PATCH 7/7] Configure init and cleanup asynchronously

Accessing the settings (like --show-touches) on start should not delay
screen mirroring.

PR #2802 <https://github.com/Genymobile/scrcpy/pull/2802>
---
 .../main/java/com/genymobile/scrcpy/Server.java    | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index db5bc7eca9..5a1f4619c3 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -63,7 +63,7 @@ private static void scrcpy(Options options) throws IOException {
         final Device device = new Device(options);
         List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
 
-        initAndCleanUp(options);
+        Thread initThread = startInitThread(options);
 
         boolean tunnelForward = options.isTunnelForward();
 
@@ -95,6 +95,7 @@ public void onClipboardTextChanged(String text) {
                 // this is expected on close
                 Ln.d("Screen streaming stopped");
             } finally {
+                initThread.interrupt();
                 if (controllerThread != null) {
                     controllerThread.interrupt();
                 }
@@ -105,6 +106,17 @@ public void onClipboardTextChanged(String text) {
         }
     }
 
+    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