Configure clean up actions dynamically

Some actions may be performed when scrcpy exits, currently:
 - disable "show touches"
 - restore "stay on while plugged in"
 - power off screen
 - restore "power mode" (to disable "turn screen off")

They are performed from a separate process so that they can be executed
even when scrcpy-server is killed (e.g. if the device is unplugged).

The clean up actions to perform were configured when scrcpy started.
Given that there is no method to read the current "power mode" in
Android, and that "turn screen off" can be applied at any time using an
scrcpy shortcut, there was no way to determine if "power mode" had to be
restored on exit. Therefore, it was always restored to "normal", even
when not necessary.

However, setting the "power mode" is quite fragile on some devices, and
may cause some issues, so it is preferable to call it only when
necessary (when "turn screen off" has actually been called).

For that purpose, make the scrcpy-server main process and the clean up
process communicate the actions to perform over a pipe (stdin/stdout),
so that they can be changed dynamically. In particular, when the power
mode is changed at runtime, notify the clean up process.

Refs 1beec99f82
Refs #4456 <https://github.com/Genymobile/scrcpy/issues/4456>
Refs #4624 <https://github.com/Genymobile/scrcpy/issues/4624>
PR #4649 <https://github.com/Genymobile/scrcpy/pull/4649>
pull/4649/head
Romain Vimont 4 months ago
parent be3f949aa5
commit 9efa162949

@ -1,11 +1,8 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
/** /**
* Handle the cleanup of scrcpy, even if the main process is killed. * Handle the cleanup of scrcpy, even if the main process is killed.
@ -14,127 +11,59 @@ import java.io.IOException;
*/ */
public final class CleanUp { public final class CleanUp {
// A simple struct to be passed from the main process to the cleanup process private static final int MSG_TYPE_MASK = 0b11;
public static class Config implements Parcelable { private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
public static final Creator<Config> CREATOR = new Creator<Config>() { private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
@Override private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
public Config createFromParcel(Parcel in) {
return new Config(in);
}
@Override
public Config[] newArray(int size) {
return new Config[size];
}
};
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
private static final int FLAG_POWER_OFF_SCREEN = 4;
private int displayId;
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
private int restoreStayOn = -1;
private boolean disableShowTouches;
private boolean restoreNormalPowerMode;
private boolean powerOffScreen;
public Config() {
// Default constructor, the fields are initialized by CleanUp.configure()
}
protected Config(Parcel in) {
displayId = in.readInt();
restoreStayOn = in.readInt();
byte options = in.readByte();
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(displayId);
dest.writeInt(restoreStayOn);
byte options = 0;
if (disableShowTouches) {
options |= FLAG_DISABLE_SHOW_TOUCHES;
}
if (restoreNormalPowerMode) {
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
}
if (powerOffScreen) {
options |= FLAG_POWER_OFF_SCREEN;
}
dest.writeByte(options);
}
private boolean hasWork() { private static final int MSG_PARAM_SHIFT = 2;
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
}
@Override private final OutputStream out;
public int describeContents() {
return 0;
}
byte[] serialize() { public CleanUp(OutputStream out) {
Parcel parcel = Parcel.obtain(); this.out = out;
writeToParcel(parcel, 0); }
byte[] bytes = parcel.marshall();
parcel.recycle();
return bytes;
}
static Config deserialize(byte[] bytes) { public static CleanUp configure(int displayId) throws IOException {
Parcel parcel = Parcel.obtain(); String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
return CREATOR.createFromParcel(parcel);
}
static Config fromBase64(String base64) { ProcessBuilder builder = new ProcessBuilder(cmd);
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); builder.environment().put("CLASSPATH", Server.SERVER_PATH);
return deserialize(bytes); Process process = builder.start();
} return new CleanUp(process.getOutputStream());
}
String toBase64() { private boolean sendMessage(int type, int param) {
byte[] bytes = serialize(); assert (type & ~MSG_TYPE_MASK) == 0;
return Base64.encodeToString(bytes, Base64.NO_WRAP); int msg = type | param << MSG_PARAM_SHIFT;
try {
out.write(msg);
out.flush();
return true;
} catch (IOException e) {
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
return false;
} }
} }
private CleanUp() { public boolean setRestoreStayOn(int restoreValue) {
// not instantiable // Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
assert restoreValue >= -1 && restoreValue <= 7;
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
} }
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) public boolean setDisableShowTouches(boolean disableOnExit) {
throws IOException { return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
Config config = new Config();
config.displayId = displayId;
config.disableShowTouches = disableShowTouches;
config.restoreStayOn = restoreStayOn;
config.restoreNormalPowerMode = restoreNormalPowerMode;
config.powerOffScreen = powerOffScreen;
if (config.hasWork()) {
startProcess(config);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
} }
private static void startProcess(Config config) throws IOException { public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
}
ProcessBuilder builder = new ProcessBuilder(cmd); public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
builder.environment().put("CLASSPATH", Server.SERVER_PATH); return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
builder.start();
} }
public static void unlinkSelf() { public static void unlinkSelf() {
@ -148,41 +77,66 @@ public final class CleanUp {
public static void main(String... args) { public static void main(String... args) {
unlinkSelf(); unlinkSelf();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = -1;
boolean disableShowTouches = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
try { try {
// Wait for the server to die // Wait for the server to die
System.in.read(); int msg;
while ((msg = System.in.read()) != -1) {
int type = msg & MSG_TYPE_MASK;
int param = msg >> MSG_PARAM_SHIFT;
switch (type) {
case MSG_TYPE_RESTORE_STAY_ON:
restoreStayOn = param > 7 ? -1 : param;
break;
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
disableShowTouches = param != 0;
break;
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
restoreNormalPowerMode = param != 0;
break;
case MSG_TYPE_POWER_OFF_SCREEN:
powerOffScreen = param != 0;
break;
default:
Ln.w("Unexpected msg type: " + type);
break;
}
}
} catch (IOException e) { } catch (IOException e) {
// Expected when the server is dead // Expected when the server is dead
} }
Ln.i("Cleaning up"); Ln.i("Cleaning up");
Config config = Config.fromBase64(args[0]); if (disableShowTouches) {
Ln.i("Disabling \"show touches\"");
if (config.disableShowTouches || config.restoreStayOn != -1) { try {
if (config.disableShowTouches) { Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
Ln.i("Disabling \"show touches\""); } catch (SettingsException e) {
try { Ln.e("Could not restore \"show_touches\"", e);
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\"");
try { if (restoreStayOn != -1) {
Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); Ln.i("Restoring \"stay awake\"");
} catch (SettingsException e) { try {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
} } catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
} }
} }
if (Device.isScreenOn()) { if (Device.isScreenOn()) {
if (config.powerOffScreen) { if (powerOffScreen) {
Ln.i("Power off screen"); Ln.i("Power off screen");
Device.powerOffScreen(config.displayId); Device.powerOffScreen(displayId);
} else if (config.restoreNormalPowerMode) { } else if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode"); Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }

@ -28,6 +28,7 @@ public class Controller implements AsyncProcessor {
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final CleanUp cleanUp;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
private final boolean clipboardAutosync; private final boolean clipboardAutosync;
private final boolean powerOn; private final boolean powerOn;
@ -41,9 +42,10 @@ public class Controller implements AsyncProcessor {
private boolean keepPowerModeOff; private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync, boolean powerOn) { public Controller(Device device, DesktopConnection connection, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
this.cleanUp = cleanUp;
this.clipboardAutosync = clipboardAutosync; this.clipboardAutosync = clipboardAutosync;
this.powerOn = powerOn; this.powerOn = powerOn;
initPointers(); initPointers();
@ -170,6 +172,10 @@ public class Controller implements AsyncProcessor {
if (setPowerModeOk) { if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF; keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
if (cleanUp != null) {
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
}
} }
} }
break; break;

@ -51,46 +51,47 @@ public final class Server {
// not instantiable // not instantiable
} }
private static void initAndCleanUp(Options options) { private static void initAndCleanUp(Options options, CleanUp cleanUp) {
boolean mustDisableShowTouchesOnCleanUp = false; // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
int restoreStayOn = -1; // and for all, they cannot be changed from another thread)
boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled
if (options.getShowTouches() || options.getStayAwake()) { if (options.getShowTouches()) {
if (options.getShowTouches()) { try {
try { String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); // If "show touches" was disabled, it must be disabled back on clean up
// If "show touches" was disabled, it must be disabled back on clean up if (!"1".equals(oldValue)) {
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); if (!cleanUp.setDisableShowTouches(true)) {
} catch (SettingsException e) { Ln.e("Could not disable show touch on exit");
Ln.e("Could not change \"show_touches\"", e); }
} }
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
} }
}
if (options.getStayAwake()) { if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; 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 { try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); int restoreStayOn = Integer.parseInt(oldValue);
try { if (restoreStayOn != stayOn) {
restoreStayOn = Integer.parseInt(oldValue); // Restore only if the current value is different
if (restoreStayOn == stayOn) { if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
// No need to restore Ln.e("Could not restore stay on on exit");
restoreStayOn = -1;
} }
} catch (NumberFormatException e) {
restoreStayOn = 0;
} }
} catch (SettingsException e) { } catch (NumberFormatException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e); // ignore
} }
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
} }
} }
if (options.getCleanup()) { if (options.getPowerOffScreenOnClose()) {
try { if (!cleanUp.setPowerOffScreen(true)) {
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, Ln.e("Could not power off screen on exit");
options.getPowerOffScreenOnClose());
} catch (IOException e) {
Ln.e("Could not configure cleanup", e);
} }
} }
} }
@ -101,7 +102,13 @@ public final class Server {
throw new ConfigurationException("Camera mirroring is not supported"); throw new ConfigurationException("Camera mirroring is not supported");
} }
Thread initThread = startInitThread(options); CleanUp cleanUp = null;
Thread initThread = null;
if (options.getCleanup()) {
cleanUp = CleanUp.configure(options.getDisplayId());
initThread = startInitThread(options, cleanUp);
}
int scid = options.getScid(); int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
@ -124,7 +131,7 @@ public final class Server {
} }
if (control) { if (control) {
Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); Controller controller = new Controller(device, connection, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
asyncProcessors.add(controller); asyncProcessors.add(controller);
} }
@ -167,7 +174,9 @@ public final class Server {
completion.await(); completion.await();
} finally { } finally {
initThread.interrupt(); if (initThread != null) {
initThread.interrupt();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop(); asyncProcessor.stop();
} }
@ -175,7 +184,9 @@ public final class Server {
connection.shutdown(); connection.shutdown();
try { try {
initThread.join(); if (initThread != null) {
initThread.join();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join(); asyncProcessor.join();
} }
@ -187,8 +198,8 @@ public final class Server {
} }
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup"); Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
thread.start(); thread.start();
return thread; return thread;
} }

Loading…
Cancel
Save