Compare commits

..

6 Commits

Author SHA1 Message Date
Romain Vimont
c5c2734db9 Keep the display mode daemon process alive
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 <https://github.com/Genymobile/scrcpy/pull/4446>
2023-11-23 10:02:10 +01:00
Romain Vimont
c1f2932db8 Fix turn screen off on Android 14
On Android 14, execute a separate process with a different classpath and
LD_PRELOAD to execute the methods required to turn the device screen
off.

Fixes #3927 <https://github.com/Genymobile/scrcpy/issues/3927>
Refs #3927 comment <https://github.com/Genymobile/scrcpy/issues/3927#issuecomment-1790031953>
PR #4446 <https://github.com/Genymobile/scrcpy/pull/4446>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-11-23 09:27:38 +01:00
Romain Vimont
890ba529c3 Keep server file on the device until clean up
The server was unlinked (removed) just after it started.

In order to execute a new process using the server jarfile at any time
(typically to set the display power mode from another process), keep the
file until the server closes.
2023-11-21 17:01:17 +01:00
Romain Vimont
798727aa58 Use a different server path for each session
When scrcpy is run, a server is pushed to
/data/local/tmp/scrcpy-server.jar.

Running simultaneous scrcpy instances on the same device was not a
problem, because the file was unlinked (removed) almost immediately once
it started, avoiding any conflicts.

In order to support executing new process using the scrcpy-server.jar at
any time (to change the display power mode from a separate process), the
server file must not be unlinked, so using different names are necessary
to avoid conflicts.

Reuse the scid mechanism already used for generating device socket
names.

Refs 4315be1648
Refs 439a1fd4ed
2023-11-21 17:01:06 +01:00
sam80180
e76ff25e31 Do not hardcode server path on the device
The path can be retrieved from the classpath.

PR #4416 <https://github.com/Genymobile/scrcpy/pull/4416>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-21 16:59:37 +01:00
Romain Vimont
ec80be1eda Fix meson deprecated 'pkgconfig' to 'pkg-config'
When running ./release.sh:

> DEPRECATION: "pkgconfig" entry is deprecated and should be replaced by
> "pkg-config"
2023-11-21 16:59:37 +01:00
9 changed files with 240 additions and 15 deletions

View File

@@ -18,7 +18,6 @@
#define SC_SERVER_FILENAME "scrcpy-server"
#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
#define SC_ADB_PORT_DEFAULT 5555
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
@@ -117,7 +116,7 @@ error:
}
static bool
push_server(struct sc_intr *intr, const char *serial) {
push_server(struct sc_intr *intr, uint32_t scid, const char *serial) {
char *server_path = get_server_path();
if (!server_path) {
return false;
@@ -127,7 +126,16 @@ push_server(struct sc_intr *intr, const char *serial) {
free(server_path);
return false;
}
bool ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0);
char *device_server_path;
if (asprintf(&device_server_path, "/data/local/tmp/scrcpy-server-%08x.jar",
scid) == -1) {
LOG_OOM();
free(server_path);
return false;
}
bool ok = sc_adb_push(intr, serial, server_path, device_server_path, 0);
free(device_server_path);
free(server_path);
return ok;
}
@@ -209,13 +217,20 @@ execute_server(struct sc_server *server,
const char *serial = server->serial;
assert(serial);
char *classpath;
if (asprintf(&classpath, "CLASSPATH=/data/local/tmp/scrcpy-server-%08x.jar",
params->scid) == -1) {
LOG_OOM();
return SC_PROCESS_NONE;
}
const char *cmd[128];
unsigned count = 0;
cmd[count++] = sc_adb_get_executable();
cmd[count++] = "-s";
cmd[count++] = serial;
cmd[count++] = "shell";
cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH;
cmd[count++] = classpath;
cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER
@@ -388,6 +403,7 @@ end:
for (unsigned i = dyn_idx; i < count; ++i) {
free((char *) cmd[i]);
}
free(classpath);
return pid;
}
@@ -937,7 +953,7 @@ run_server(void *data) {
assert(serial);
LOGD("Device serial: %s", serial);
ok = push_server(&server->intr, serial);
ok = push_server(&server->intr, params->scid, serial);
if (!ok) {
goto error_connection_failed;
}

View File

@@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc'
cpp = 'i686-w64-mingw32-g++'
ar = 'i686-w64-mingw32-ar'
strip = 'i686-w64-mingw32-strip'
pkgconfig = 'i686-w64-mingw32-pkg-config'
pkg-config = 'i686-w64-mingw32-pkg-config'
windres = 'i686-w64-mingw32-windres'
[host_machine]

View File

@@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc'
cpp = 'x86_64-w64-mingw32-g++'
ar = 'x86_64-w64-mingw32-ar'
strip = 'x86_64-w64-mingw32-strip'
pkgconfig = 'x86_64-w64-mingw32-pkg-config'
pkg-config = 'x86_64-w64-mingw32-pkg-config'
windres = 'x86_64-w64-mingw32-windres'
[host_machine]

View File

@@ -14,8 +14,6 @@ import java.io.IOException;
*/
public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
// A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable {
@@ -135,21 +133,19 @@ public final class CleanUp {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
builder.environment().put("CLASSPATH", Server.SERVER_PATH);
builder.start();
}
public static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
new File(Server.SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
public static void main(String... args) {
unlinkSelf();
try {
// Wait for the server to die
System.in.read();
@@ -187,7 +183,11 @@ public final class CleanUp {
} else if (config.restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
// Make sure the request is performed before exiting
DisplayPowerMode.stopAndJoin();
}
}
unlinkSelf();
}
}

View File

@@ -315,6 +315,14 @@ public final class Device {
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod()) {
// On Android 14+, these internal methods have been moved to system server classes.
// Run a separate process with the correct classpath and LD_PRELOAD to change the display power mode.
DisplayPowerMode.setRemoteDisplayPowerMode(mode);
// The call is asynchronous (we don't want to block)
return true;
}
// Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) {

View File

@@ -0,0 +1,184 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.os.IBinder;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
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. 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
* be executed in order). In addition, it only executes the last pending request (setting power mode to value X then to value Y is equivalent to
* just setting it to value Y).
*/
public final class DisplayPowerMode {
private static final Proxy PROXY = new Proxy();
private static final class Proxy implements Runnable {
private Process process;
private Thread thread;
private int requestedMode = -1;
private boolean stopped;
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;
}
}
void stopAndJoin() {
boolean hasThread;
synchronized (this) {
hasThread = thread != null;
if (thread != null) {
stopped = true;
notify();
}
}
if (hasThread) {
// Join the thread without holding the mutex (that would cause a deadlock)
try {
thread.join();
} catch (InterruptedException e) {
Ln.e("Thread join interrupted", e);
}
}
}
@Override
public void run() {
try {
OutputStream out = process.getOutputStream();
InputStream in = process.getInputStream();
while (true) {
int mode;
synchronized (this) {
while (!stopped && requestedMode == -1) {
wait();
}
mode = requestedMode;
requestedMode = -1;
}
// Even if stopped, the last request must be executed to restore the display power mode to normal
if (mode == -1) {
return;
}
try {
out.write(mode);
out.flush();
int status = in.read();
if (status != 0) {
Ln.e("Set display power mode failed remotely: status=" + status);
}
} catch (IOException e) {
Ln.e("Could not request display power mode", e);
}
}
} catch (InterruptedException e) {
// end of thread
}
}
}
private DisplayPowerMode() {
// not instantiable
}
// Called from the scrcpy process
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 executeDisplayPowerModeDaemon() throws IOException {
String[] ldPreloadLibs = {"/system/lib64/libandroid_servers.so"};
String[] cmd = {"app_process", "/", DisplayPowerMode.class.getName()};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("LD_PRELOAD", String.join(" ", ldPreloadLibs));
builder.environment().put("CLASSPATH", Server.SERVER_PATH + ":/system/framework/services.jar");
return builder.start();
}
// Executed in the DisplayPowerMode-specific process
@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi"})
private static void setDisplayPowerModeUsingDisplayControl(int mode) throws Exception {
System.loadLibrary("android_servers");
@SuppressLint("PrivateApi")
Class<?> displayControlClass = Class.forName("com.android.server.display.DisplayControl");
Method getPhysicalDisplayIdsMethod = displayControlClass.getDeclaredMethod("getPhysicalDisplayIds");
Method getPhysicalDisplayTokenMethod = displayControlClass.getDeclaredMethod("getPhysicalDisplayToken", long.class);
Class<?> surfaceControlClass = Class.forName("android.view.SurfaceControl");
Method setDisplayPowerModeMethod = surfaceControlClass.getDeclaredMethod("setDisplayPowerMode", IBinder.class, int.class);
long[] displayIds = (long[]) getPhysicalDisplayIdsMethod.invoke(null);
for (long displayId : displayIds) {
Object token = getPhysicalDisplayTokenMethod.invoke(null, displayId);
setDisplayPowerModeMethod.invoke(null, token, mode);
}
}
public static void main(String... args) {
// This process uses stdin/stdout to communicate with the caller, make sure nothing else writes to stdout
// (and never use Ln methods other than Ln.w() and Ln.e()).
PrintStream nullStream = new PrintStream(new Ln.NullOutputStream());
System.setOut(nullStream);
PrintStream stdout = Ln.CONSOLE_OUT;
try {
while (true) {
// Wait for requests
int request = System.in.read();
if (request == -1) {
// EOF
return;
}
try {
setDisplayPowerModeUsingDisplayControl(request);
stdout.write(0); // ok
} catch (Throwable e) {
Ln.e("Could not set display power mode", e);
stdout.write(1); // error
}
stdout.flush();
}
} catch (IOException e) {
// Expected when the server is dead
}
}
}

View File

@@ -16,8 +16,8 @@ public final class Ln {
private static final String TAG = "scrcpy";
private static final String PREFIX = "[server] ";
private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
public static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
public static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
enum Level {
VERBOSE, DEBUG, INFO, WARN, ERROR

View File

@@ -3,12 +3,20 @@ package com.genymobile.scrcpy;
import android.os.BatteryManager;
import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public final class Server {
public static final String SERVER_PATH;
static {
String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
// By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
SERVER_PATH = classPaths[0];
}
private static class Completion {
private int running;
private boolean fatalError;

View File

@@ -139,6 +139,15 @@ public final class SurfaceControl {
return getPhysicalDisplayIdsMethod;
}
public static boolean hasPhysicalDisplayIdsMethod() {
try {
getGetPhysicalDisplayIdsMethod();
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
public static long[] getPhysicalDisplayIds() {
try {
Method method = getGetPhysicalDisplayIdsMethod();