Skip to content

Commit

Permalink
Keep the display mode daemon process alive
Browse files Browse the repository at this point in the history
On Android 14, a separate process was spawn on every display mode
request (to turn the screen off or on).

But starting a java process takes time (a few hundred milliseconds),
causing a noticeable latency between the request to turn the screen off
(MOD+o) or on (MOD+Shift+o) and the actual power mode change.

To minimize this latency, keep the process alive between requests, so
that only the first one will have to spawn the process.

It would be possible to spawn the process in advance, so that even the
first request would be immediate, but any problem would impact all
Android 14 users even without using the "turn screen off" feature.

PR #4446 <#4446>
  • Loading branch information
rom1v committed Nov 23, 2023
1 parent c1f2932 commit e844c68
Showing 1 changed file with 52 additions and 33 deletions.
85 changes: 52 additions & 33 deletions server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
import android.os.IBinder;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;

/**
* On Android 14, the methods used to turn the device screen off have been moved from SurfaceControl (in framework.jar) to DisplayControl (a system
* server class). As a consequence, they could not be called directly. See {@url https://github.com/Genymobile/scrcpy/issues/3927}.
* <p>
* Instead, run a separate process with a different classpath and LD_PRELOAD just to set the display power mode.
* Instead, run a separate process with a different classpath and LD_PRELOAD just to set the display power mode. The scrcpy server can request to
* this process to set the display mode by writing the mode (a single byte, the value of one of the SurfaceControl.POWER_MODE_* constants,
* typically 0=off, 2=on) to the process stdin. In return, it receives the status of the request (0=ok, 1=error) on the process stdout.
* <p>
* This separate process is started on the first display mode request.
* <p>
* Since the client must not block, and calling/joining a process is blocking (this specific one takes a few hundred milliseconds to complete),
* this class uses an internal thread to execute the requests asynchronously, and serialize them (so that two successive requests are guaranteed to
Expand All @@ -23,18 +29,26 @@ public final class DisplayPowerMode {

private static final class Proxy implements Runnable {

private Process process;
private Thread thread;
private int requestedMode = -1;
private boolean stopped;

synchronized void requestMode(int mode) {
if (thread == null) {
thread = new Thread(this, "DisplayPowerModeProxy");
thread.setDaemon(true);
thread.start();
synchronized boolean requestMode(int mode) {
try {
if (process == null) {
process = executeDisplayPowerModeDaemon();
thread = new Thread(this, "DisplayPowerModeProxy");
thread.setDaemon(true);
thread.start();
}
requestedMode = mode;
notify();
return true;
} catch (IOException e) {
Ln.e("Could not start display power mode daemon", e);
return false;
}
requestedMode = mode;
notify();
}

void stopAndJoin() {
Expand All @@ -60,8 +74,10 @@ void stopAndJoin() {
@Override
public void run() {
try {
int mode;
OutputStream out = process.getOutputStream();
InputStream in = process.getInputStream();
while (true) {
int mode;
synchronized (this) {
while (!stopped && requestedMode == -1) {
wait();
Expand All @@ -76,13 +92,14 @@ public void run() {
}

try {
Process process = executeSystemServerSetDisplayPowerMode(mode);
int status = process.waitFor();
out.write(mode);
out.flush();
int status = in.read();
if (status != 0) {
Ln.e("Set display power mode failed remotely: status=" + status);
}
} catch (Exception e) {
Ln.e("Failed to execute process", e);
} catch (IOException e) {
Ln.e("Could not request display power mode", e);
}
}
} catch (InterruptedException e) {
Expand All @@ -96,18 +113,18 @@ private DisplayPowerMode() {
}

// Called from the scrcpy process
public static void setRemoteDisplayPowerMode(int mode) {
PROXY.requestMode(mode);
public static boolean setRemoteDisplayPowerMode(int mode) {
return PROXY.requestMode(mode);
}

public static void stopAndJoin() {
PROXY.stopAndJoin();
}

// Called from the proxy thread in the scrcpy process
private static Process executeSystemServerSetDisplayPowerMode(int mode) throws IOException {
private static Process executeDisplayPowerModeDaemon() throws IOException {
String[] ldPreloadLibs = {"/system/lib64/libandroid_servers.so"};
String[] cmd = {"app_process", "/", DisplayPowerMode.class.getName(), String.valueOf(mode)};
String[] cmd = {"app_process", "/", DisplayPowerMode.class.getName()};

ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("LD_PRELOAD", String.join(" ", ldPreloadLibs));
Expand Down Expand Up @@ -139,23 +156,25 @@ public static void main(String... args) {
Ln.disableSystemStreams();
Ln.initLogLevel(Ln.Level.DEBUG);

int status = run(args) ? 0 : 1;
System.exit(status);
}

private static boolean run(String... args) {
if (args.length != 1) {
Ln.e("Exactly one argument expected: the value of one of the SurfaceControl.POWER_MODE_* constants (typically 0 or 2)");
return false;
}

try {
int mode = Integer.parseInt(args[0]);
setDisplayPowerModeUsingDisplayControl(mode);
return true;
} catch (Throwable e) {
Ln.e("Could not set display power mode", e);
return false;
while (true) {
// Wait for requests
int request = System.in.read();
if (request == -1) {
// EOF
return;
}
try {
setDisplayPowerModeUsingDisplayControl(request);
System.out.write(0); // ok
} catch (Throwable e) {
Ln.e("Could not set display power mode", e);
System.out.write(1); // error
}
System.out.flush();
}
} catch (IOException e) {
// Expected when the server is dead
}
}
}

0 comments on commit e844c68

Please sign in to comment.