Compare commits
11 Commits
pr4448
...
turn_scree
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5c2734db9 | ||
|
|
c1f2932db8 | ||
|
|
890ba529c3 | ||
|
|
798727aa58 | ||
|
|
e76ff25e31 | ||
|
|
ec80be1eda | ||
|
|
cdd78273dc | ||
|
|
c546293f35 | ||
|
|
70baf3c384 | ||
|
|
dac0d54c5c | ||
|
|
878bfb1d8b |
@@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
||||
# For some users, the PATH or ADB environment variables are set from the shell
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'"
|
||||
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error"
|
||||
Icon=scrcpy
|
||||
Terminal=true
|
||||
Type=Application
|
||||
|
||||
@@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
||||
# For some users, the PATH or ADB environment variables are set from the shell
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/sh -c "\\$SHELL -i -c scrcpy"
|
||||
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy"
|
||||
Icon=scrcpy
|
||||
Terminal=false
|
||||
Type=Application
|
||||
|
||||
@@ -2138,7 +2138,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->display_orientation = SC_ORIENTATION_180;
|
||||
break;
|
||||
case 3:
|
||||
// rotation 3 was 270° counterclockwise, but orientation
|
||||
// rotation 1 was 270° counterclockwise, but orientation
|
||||
// is expressed clockwise
|
||||
opts->display_orientation = SC_ORIENTATION_90;
|
||||
break;
|
||||
@@ -2531,8 +2531,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
if (opts->record_orientation != SC_ORIENTATION_0) {
|
||||
if (sc_orientation_is_mirror(opts->record_orientation)) {
|
||||
LOGE("Record orientation only supports rotation, not "
|
||||
"flipping: %s",
|
||||
sc_orientation_get_name(opts->record_orientation));
|
||||
"flipping: %s", optarg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +113,10 @@ sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) {
|
||||
// In the final result, we want all the hflips then all the rotations,
|
||||
// so we must move hflip2 to the left:
|
||||
//
|
||||
// hflip1 × hflip2 × rotate1' × rotate2
|
||||
// hflip1 × hflip2 × f(rotate1) × rotate2
|
||||
//
|
||||
// with rotate1' = | rotate1 if src is 0° or 180°
|
||||
// | rotate1 + 180° if src is 90° or 270°
|
||||
// with f(rotate1) = | rotate1 if src is 0 or 180
|
||||
// | rotate1 + 180 if src is 90 or 270
|
||||
|
||||
src_rotation += 2;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ scrcpy --orientation=270 # 270° clockwise
|
||||
scrcpy --orientation=flip0 # hflip
|
||||
scrcpy --orientation=flip90 # hflip + 90° clockwise
|
||||
scrcpy --orientation=flip180 # vflip (hflip + 180°)
|
||||
scrcpy --orientation=flip270 # hflip + 270° clockwise
|
||||
scrcpy --orientation=flip270 # hflip + 270°
|
||||
```
|
||||
|
||||
The orientation can be set separately for display and record if necessary, via
|
||||
|
||||
@@ -11,8 +11,6 @@ public interface AsyncProcessor {
|
||||
}
|
||||
|
||||
void start(TerminationListener listener);
|
||||
|
||||
void stop();
|
||||
|
||||
void join() throws InterruptedException;
|
||||
}
|
||||
|
||||
@@ -289,7 +289,8 @@ public class CameraCapture extends SurfaceCapture {
|
||||
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
|
||||
|
||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor,
|
||||
new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
future.complete(session);
|
||||
|
||||
@@ -146,8 +146,6 @@ public final class CleanUp {
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
unlinkSelf();
|
||||
|
||||
try {
|
||||
// Wait for the server to die
|
||||
System.in.read();
|
||||
@@ -185,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,8 +318,9 @@ public class Controller implements AsyncProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||
0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
@@ -340,8 +341,9 @@ public class Controller implements AsyncProcessor {
|
||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0,
|
||||
InputDevice.SOURCE_MOUSE, 0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
@@ -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) {
|
||||
|
||||
@@ -51,7 +51,6 @@ public final class DeviceMessageSender {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
|
||||
184
server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java
Normal file
184
server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
|
||||
@@ -138,8 +138,8 @@ public final class ClipboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
|
||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.lang.reflect.Method;
|
||||
public final class ServiceManager {
|
||||
|
||||
private static final Method GET_SERVICE_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -4,8 +4,8 @@ import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.IInterface;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.IDisplayFoldListener;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
Reference in New Issue
Block a user