Scrcpy for Android is an Android port of Scrcpy. This application mirrors display and touch controls from a remote android device to android device.
Scrcpy for Android uses ADB-Connect interface to connect to android device to be mirrored. The latter needs to have ADB-connect/ADB-wireless/ADB over network enabled in developer settings, then enter its IP in the app on the other device. Full instructions can be found in the app repository's Readme.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/fastlane/metadata/android/en-US/images/phoneScreenshots/home.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | mirror display and touch control between Android devices
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/home.jpg
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/server/build.gradle:
--------------------------------------------------------------------------------
1 |
2 |
3 | apply plugin: 'com.android.application'
4 | android {
5 | namespace 'org.server.scrcpy'
6 |
7 | compileSdkVersion 31
8 | defaultConfig {
9 | applicationId "org.server.scrcpy"
10 | minSdkVersion 21
11 | targetSdkVersion 31
12 | versionCode 3
13 | versionName "1.2"
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 |
18 | buildFeatures{
19 | aidl true
20 | buildConfig true
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | }
27 | }
28 |
29 | tasks.register('copyServer', Copy) {
30 | def buildType = gradle.startParameter.taskNames.any { it.endsWith('Release') } ? 'Release' : 'Debug'
31 | dependsOn 'deleteServer'
32 | dependsOn("assemble${buildType}")
33 |
34 | def release_file = 'build/outputs/apk/release/server-release-unsigned.apk'
35 | def debug_file = 'build/outputs/apk/debug/server-debug.apk'
36 | def file_dest = '../app/src/main/assets/'
37 |
38 | if (buildType == "Debug") {
39 | from file(debug_file)
40 | into file(file_dest)
41 | } else {
42 | from file(release_file)
43 | into file(file_dest)
44 | }
45 | rename { fileName ->
46 | 'scrcpy-server.jar'
47 | }
48 |
49 | }
50 | tasks.register('deleteServer', Delete) {
51 | delete "../app/src/main/assets/scrcpy-server.jar"
52 | }
53 |
54 | // tasks.whenTaskAdded { task ->
55 | // task.finalizedBy(copyServer)
56 | // }
57 | // afterEvaluate {
58 | // packageRelease.finalizedBy(copyServer)
59 | // }
60 | // afterEvaluate {
61 | // packageDebug.finalizedBy(copyServer)
62 | // }
63 | }
64 |
65 | dependencies {
66 | testImplementation 'junit:junit:4.13.2'
67 | }
68 |
--------------------------------------------------------------------------------
/server/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/server/src/main/aidl/android/view/IRotationWatcher.aidl:
--------------------------------------------------------------------------------
1 | /* //device/java/android/android/hardware/ISensorListener.aidl
2 | **
3 | ** Copyright 2008, The Android Open Source Project
4 | **
5 | ** Licensed under the Apache License, Version 2.0 (the "License");
6 | ** you may not use this file except in compliance with the License.
7 | ** You may obtain a copy of the License at
8 | **
9 | ** http://www.apache.org/licenses/LICENSE-2.0
10 | **
11 | ** Unless required by applicable law or agreed to in writing, software
12 | ** distributed under the License is distributed on an "AS IS" BASIS,
13 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | ** See the License for the specific language governing permissions and
15 | ** limitations under the License.
16 | */
17 |
18 | package android.view;
19 |
20 | /**
21 | * {@hide}
22 | */
23 | interface IRotationWatcher {
24 | oneway void onRotationChanged(int rotation);
25 | }
26 |
--------------------------------------------------------------------------------
/server/src/main/java/android/content/IContentProvider.java:
--------------------------------------------------------------------------------
1 | package android.content;
2 |
3 | public interface IContentProvider {
4 | // android.content.IContentProvider is hidden, this is a fake one to expose the type to the project
5 | }
6 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/Device.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | import org.server.scrcpy.device.Point;
4 | import android.os.Build;
5 | import android.os.RemoteException;
6 | import android.view.IRotationWatcher;
7 | import android.view.InputEvent;
8 |
9 | import org.server.scrcpy.wrappers.ServiceManager;
10 |
11 | public final class Device {
12 |
13 | // private final ServiceManager serviceManager = new ServiceManager();
14 | private ScreenInfo screenInfo;
15 | private RotationListener rotationListener;
16 |
17 | public Device(Options options) {
18 | screenInfo = computeScreenInfo(options.getMaxSize());
19 | registerRotationWatcher(new IRotationWatcher.Stub() {
20 | @Override
21 | public void onRotationChanged(int rotation) throws RemoteException {
22 | synchronized (Device.this) {
23 | screenInfo = screenInfo.withRotation(rotation);
24 |
25 | // notify
26 | if (rotationListener != null) {
27 | rotationListener.onRotationChanged(rotation);
28 | }
29 | }
30 | }
31 | });
32 | }
33 |
34 | public static String getDeviceName() {
35 | return Build.MODEL;
36 | }
37 |
38 | public synchronized ScreenInfo getScreenInfo() {
39 | return screenInfo;
40 | }
41 |
42 | @SuppressWarnings("checkstyle:MagicNumber")
43 | private ScreenInfo computeScreenInfo(int maxSize) {
44 | // Compute the video size and the padding of the content inside this video.
45 | // Principle:
46 | // - scale down the great side of the screen to maxSize (if necessary);
47 | // - scale down the other side so that the aspect ratio is preserved;
48 | // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
49 | DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo();
50 | boolean rotated = (displayInfo.getRotation() & 1) != 0;
51 | Size deviceSize = displayInfo.getSize();
52 | int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8
53 | int h = deviceSize.getHeight() & ~7;
54 | if (maxSize > 0) {
55 | if (BuildConfig.DEBUG && maxSize % 8 != 0) {
56 | throw new AssertionError("Max size must be a multiple of 8");
57 | }
58 | boolean portrait = h > w;
59 | int major = portrait ? h : w;
60 | int minor = portrait ? w : h;
61 | if (major > maxSize) {
62 | int minorExact = minor * maxSize / major;
63 | // +4 to round the value to the nearest multiple of 8
64 | minor = (minorExact + 4) & ~7;
65 | major = maxSize;
66 | }
67 | w = portrait ? minor : major;
68 | h = portrait ? major : minor;
69 | }
70 | Size videoSize = new Size(w, h);
71 | return new ScreenInfo(deviceSize, videoSize, rotated);
72 | }
73 |
74 | public Point getPhysicalPoint(Position position) {
75 | @SuppressWarnings("checkstyle:HiddenField") // it hides the field on purpose, to read it with a lock
76 | ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
77 | Size videoSize = screenInfo.getVideoSize();
78 | Size clientVideoSize = position.getScreenSize();
79 | if (!videoSize.equals(clientVideoSize)) {
80 | // The client sends a click relative to a video with wrong dimensions,
81 | // the device may have been rotated since the event was generated, so ignore the event
82 | return null;
83 | }
84 | Size deviceSize = screenInfo.getDeviceSize();
85 | Point point = position.getPoint();
86 | int scaledX = point.getX() * deviceSize.getWidth() / videoSize.getWidth();
87 | int scaledY = point.getY() * deviceSize.getHeight() / videoSize.getHeight();
88 | return new Point(scaledX, scaledY);
89 | }
90 |
91 | public boolean injectInputEvent(InputEvent inputEvent, int mode) {
92 | return ServiceManager.getInputManager().injectInputEvent(inputEvent, mode);
93 | }
94 |
95 | public boolean isScreenOn() {
96 | return ServiceManager.getPowerManager().isScreenOn();
97 | }
98 |
99 | public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
100 | ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
101 | }
102 |
103 | public synchronized void setRotationListener(RotationListener rotationListener) {
104 | this.rotationListener = rotationListener;
105 | }
106 |
107 | public Point NewgetPhysicalPoint(Point point) {
108 | @SuppressWarnings("checkstyle:HiddenField") // it hides the field on purpose, to read it with a lock
109 | ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
110 | Size videoSize = screenInfo.getVideoSize();
111 | // Size clientVideoSize = position.getScreenSize();
112 |
113 | Size deviceSize = screenInfo.getDeviceSize();
114 | // Point point = position.getPoint();
115 | int scaledX = point.getX() * deviceSize.getWidth() / videoSize.getWidth();
116 | int scaledY = point.getY() * deviceSize.getHeight() / videoSize.getHeight();
117 | return new Point(scaledX, scaledY);
118 | }
119 |
120 |
121 | public interface RotationListener {
122 | void onRotationChanged(int rotation);
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/DisplayInfo.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | public final class DisplayInfo {
4 | private int displayId;
5 | private final Size size;
6 | private final int rotation;
7 | private int layerStack;
8 | private int flags;
9 |
10 | public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
11 |
12 | public DisplayInfo(Size size, int rotation) {
13 | this.size = size;
14 | this.rotation = rotation;
15 | }
16 |
17 | public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
18 | this.displayId = displayId;
19 | this.size = size;
20 | this.rotation = rotation;
21 | this.layerStack = layerStack;
22 | this.flags = flags;
23 | }
24 |
25 | public int getDisplayId() {
26 | return displayId;
27 | }
28 |
29 | public Size getSize() {
30 | return size;
31 | }
32 |
33 | public int getRotation() {
34 | return rotation;
35 | }
36 |
37 | public int getLayerStack() {
38 | return layerStack;
39 | }
40 |
41 | public int getFlags() {
42 | return flags;
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/DroidConnection.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 |
4 | import java.io.Closeable;
5 | import java.io.EOFException;
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.io.OutputStream;
9 | import java.net.ServerSocket;
10 | import java.net.Socket;
11 |
12 | public final class DroidConnection implements Closeable {
13 |
14 |
15 | private static Socket socket = null;
16 | private OutputStream outputStream;
17 | private InputStream inputStream;
18 |
19 | private DroidConnection(Socket socket) throws IOException {
20 | this.socket = socket;
21 |
22 | inputStream = socket.getInputStream();
23 | outputStream = socket.getOutputStream();
24 | }
25 |
26 |
27 | private static Socket listenAndAccept() throws IOException {
28 | ServerSocket serverSocket = new ServerSocket(7007);
29 | Socket sock = null;
30 | try {
31 | sock = serverSocket.accept();
32 | } finally {
33 | serverSocket.close();
34 | }
35 | return sock;
36 | }
37 |
38 | public static DroidConnection open(String ip) throws IOException {
39 |
40 | socket = listenAndAccept();
41 | DroidConnection connection = null;
42 | // if (socket.getInetAddress().toString().equals(ip)) {
43 | // connection = new DroidConnection(socket);
44 | // }
45 | if (!socket.getInetAddress().toString().equals(ip)) {
46 | Ln.w("socket connect address != " + ip);
47 | }
48 | // 判断 socket 有一个正确的地址
49 | if (!socket.getInetAddress().toString().isEmpty()) {
50 | connection = new DroidConnection(socket);
51 | }
52 | return connection;
53 | }
54 |
55 | public void close() throws IOException {
56 | socket.shutdownInput();
57 | socket.shutdownOutput();
58 | socket.close();
59 | }
60 |
61 | public OutputStream getOutputStream() {
62 | return outputStream;
63 | }
64 |
65 |
66 | /**
67 | * TODO 需要根据原版 scrcpy 进行改造消息传送,目前仅支持 触控消息
68 | *
69 | * @return
70 | * @throws IOException
71 | */
72 | public int[] NewreceiveControlEvent() throws IOException {
73 |
74 | byte[] buf = new byte[20];
75 | int n = inputStream.read(buf, 0, 20);
76 | if (n == -1) {
77 | throw new EOFException("Event controller socket closed");
78 | }
79 |
80 | final int[] array = new int[buf.length / 4];
81 | for (int i = 0; i < array.length; i++)
82 | array[i] = (((int) (buf[i * 4]) << 24) & 0xFF000000) |
83 | (((int) (buf[i * 4 + 1]) << 16) & 0xFF0000) |
84 | (((int) (buf[i * 4 + 2]) << 8) & 0xFF00) |
85 | ((int) (buf[i * 4 + 3]) & 0xFF);
86 | return array;
87 |
88 |
89 | }
90 |
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/Ln.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | import android.util.Log;
4 |
5 |
6 | /**
7 | * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal
8 | * directly).
9 | */
10 | public final class Ln {
11 |
12 | private static final String TAG = "scrcpy";
13 | private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
14 |
15 | private Ln() {
16 | // not instantiable
17 | }
18 |
19 | public static boolean isEnabled(Level level) {
20 | return level.ordinal() >= THRESHOLD.ordinal();
21 | }
22 |
23 | public static void d(String message) {
24 | if (isEnabled(Level.DEBUG)) {
25 | Log.d(TAG, message);
26 | System.out.println("DEBUG: " + message);
27 | }
28 | }
29 |
30 | public static void i(String message) {
31 | if (isEnabled(Level.INFO)) {
32 | Log.i(TAG, message);
33 | System.out.println("INFO: " + message);
34 | }
35 | }
36 |
37 | public static void w(String message) {
38 | if (isEnabled(Level.WARN)) {
39 | Log.w(TAG, message);
40 | System.out.println("WARN: " + message);
41 | }
42 | }
43 |
44 | public static void e(String message, Throwable throwable) {
45 | if (isEnabled(Level.ERROR)) {
46 | Log.e(TAG, message, throwable);
47 | System.out.println("ERROR: " + message);
48 | throwable.printStackTrace();
49 | }
50 | }
51 |
52 | public static void e(String message) {
53 | if (isEnabled(Level.ERROR)) {
54 | Log.e(TAG, message);
55 | System.out.println("ERROR: " + message);
56 | }
57 | }
58 |
59 | enum Level {
60 | DEBUG,
61 | INFO,
62 | WARN,
63 | ERROR;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/Options.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | public class Options {
4 | private int maxSize;
5 | private int bitRate;
6 | private boolean tunnelForward;
7 |
8 | public int getMaxSize() {
9 | return maxSize;
10 | }
11 |
12 | public void setMaxSize(int maxSize) {
13 | this.maxSize = maxSize;
14 | }
15 |
16 | public int getBitRate() {
17 | return bitRate;
18 | }
19 |
20 | public void setBitRate(int bitRate) {
21 | this.bitRate = bitRate;
22 | }
23 |
24 | public boolean isTunnelForward() {
25 | return tunnelForward;
26 | }
27 |
28 | public void setTunnelForward(boolean tunnelForward) {
29 | this.tunnelForward = tunnelForward;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/Position.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | import org.server.scrcpy.device.Point;
4 |
5 |
6 | import java.util.Objects;
7 | public class Position {
8 | private Point point;
9 | private Size screenSize;
10 |
11 | public Position(Point point, Size screenSize) {
12 | this.point = point;
13 | this.screenSize = screenSize;
14 | }
15 |
16 | public Position(int x, int y, int screenWidth, int screenHeight) {
17 | this(new Point(x, y), new Size(screenWidth, screenHeight));
18 | }
19 |
20 | public Point getPoint() {
21 | return point;
22 | }
23 |
24 | public Size getScreenSize() {
25 | return screenSize;
26 | }
27 |
28 | @Override
29 | public boolean equals(Object o) {
30 | if (this == o) {
31 | return true;
32 | }
33 | if (o == null || getClass() != o.getClass()) {
34 | return false;
35 | }
36 | Position position = (Position) o;
37 | return Objects.equals(point, position.point)
38 | && Objects.equals(screenSize, position.screenSize);
39 | }
40 |
41 | @Override
42 | public int hashCode() {
43 | return Objects.hash(point, screenSize);
44 | }
45 |
46 | @Override
47 | public String toString() {
48 | return "Position{"
49 | + "point=" + point
50 | + ", screenSize=" + screenSize
51 | + '}';
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/ScreenCapture.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | import android.graphics.Rect;
4 | import android.hardware.display.VirtualDisplay;
5 | import android.os.Build;
6 | import android.os.IBinder;
7 | import android.util.Log;
8 | import android.view.Surface;
9 |
10 | import org.server.scrcpy.wrappers.ServiceManager;
11 | import org.server.scrcpy.wrappers.SurfaceControl;
12 |
13 |
14 | public class ScreenCapture {
15 |
16 | private final Device device;
17 | private IBinder display;
18 | private VirtualDisplay virtualDisplay;
19 |
20 | public ScreenCapture(Device device) {
21 | this.device = device;
22 | }
23 |
24 | public void start(Surface surface) {
25 | ScreenInfo screenInfo = device.getScreenInfo();
26 |
27 | Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect();
28 | Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
29 |
30 |
31 | if (display != null) {
32 | SurfaceControl.destroyDisplay(display);
33 | display = null;
34 | }
35 | if (virtualDisplay != null) {
36 | virtualDisplay.release();
37 | virtualDisplay = null;
38 | }
39 |
40 | try {
41 | virtualDisplay = ServiceManager.getDisplayManager()
42 | .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), 0, surface);
43 | Ln.d("Display: using DisplayManager API");
44 | } catch (Exception displayManagerException) {
45 | try {
46 | display = createDisplay();
47 | setDisplaySurface(display, surface, deviceRect, videoRect);
48 | } catch (Exception surfaceControlException) {
49 | throw new AssertionError("Could not create display");
50 | }
51 | }
52 | }
53 |
54 | public void release() {
55 | device.setRotationListener(null);
56 | if (display != null) {
57 | SurfaceControl.destroyDisplay(display);
58 | display = null;
59 | }
60 | if (virtualDisplay != null) {
61 | virtualDisplay.release();
62 | virtualDisplay = null;
63 | }
64 | }
65 |
66 | private static IBinder createDisplay() throws Exception {
67 | // Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
68 | // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
69 | boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(
70 | Build.VERSION.CODENAME));
71 | return SurfaceControl.createDisplay("scrcpy", secure);
72 | }
73 |
74 | private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
75 | SurfaceControl.openTransaction();
76 | try {
77 | SurfaceControl.setDisplaySurface(display, surface);
78 | SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
79 | SurfaceControl.setDisplayLayerStack(display, 0);
80 | } finally {
81 | SurfaceControl.closeTransaction();
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/ScreenInfo.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | public final class ScreenInfo {
4 | private final Size deviceSize;
5 | private final Size videoSize;
6 | private final boolean rotated;
7 |
8 | public ScreenInfo(Size deviceSize, Size videoSize, boolean rotated) {
9 | this.deviceSize = deviceSize;
10 | this.videoSize = videoSize;
11 | this.rotated = rotated;
12 | }
13 |
14 | public Size getDeviceSize() {
15 | return deviceSize;
16 | }
17 |
18 | public Size getVideoSize() {
19 | return videoSize;
20 | }
21 |
22 | public ScreenInfo withRotation(int rotation) {
23 | boolean newRotated = (rotation & 1) != 0;
24 | if (rotated == newRotated) {
25 | return this;
26 | }
27 | return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), newRotated);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/Server.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | import org.server.scrcpy.util.Workarounds;
4 |
5 | import java.io.IOException;
6 |
7 | public final class Server {
8 |
9 | private static String ip = null;
10 |
11 | private Server() {
12 | // not instantiable
13 | }
14 |
15 | private static void scrcpy(Options options) throws IOException {
16 | Workarounds.apply(); // init content
17 |
18 | final Device device = new Device(options);
19 | try (DroidConnection connection = DroidConnection.open(ip)) {
20 | ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
21 |
22 | // asynchronous
23 | startEventController(device, connection);
24 |
25 | try {
26 | // synchronous
27 | screenEncoder.streamScreen(device, connection.getOutputStream());
28 | } catch (IOException e) {
29 | e.printStackTrace();
30 | // this is expected on close
31 | Ln.d("Screen streaming stopped");
32 |
33 | }
34 | }
35 | }
36 |
37 | private static void startEventController(final Device device, final DroidConnection connection) {
38 | new Thread(new Runnable() {
39 | @Override
40 | public void run() {
41 | try {
42 | new EventController(device, connection).control();
43 | } catch (IOException e) {
44 | // this is expected on close
45 | Ln.d("Event controller stopped");
46 | }
47 | }
48 | }).start();
49 | }
50 |
51 | @SuppressWarnings("checkstyle:MagicNumber")
52 | private static Options createOptions(String... args) {
53 | Options options = new Options();
54 |
55 | if (args.length < 1) {
56 | return options;
57 | }
58 | ip = String.valueOf(args[0]);
59 |
60 |
61 | if (args.length < 2) {
62 | return options;
63 | }
64 | int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
65 | options.setMaxSize(maxSize);
66 |
67 | if (args.length < 3) {
68 | return options;
69 | }
70 | int bitRate = Integer.parseInt(args[2]);
71 | options.setBitRate(bitRate);
72 |
73 | if (args.length < 4) {
74 | return options;
75 | }
76 | // use "adb forward" instead of "adb tunnel"? (so the server must listen)
77 | boolean tunnelForward = Boolean.parseBoolean(args[3]);
78 | options.setTunnelForward(tunnelForward);
79 | return options;
80 | }
81 |
82 | public static void main(String... args) throws Exception {
83 | Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
84 | @Override
85 | public void uncaughtException(Thread t, Throwable e) {
86 | Ln.e("Exception on thread " + t, e);
87 | }
88 | });
89 |
90 | try {
91 | Process cmd = Runtime.getRuntime().exec("rm /data/local/tmp/scrcpy-server.jar");
92 | cmd.waitFor();
93 | } catch (IOException e1) {
94 | e1.printStackTrace();
95 | } catch (InterruptedException e1) {
96 | e1.printStackTrace();
97 | }
98 |
99 | Options options = createOptions(args);
100 | scrcpy(options);
101 | }
102 | }
103 |
104 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/Size.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy;
2 |
3 | import android.graphics.Rect;
4 |
5 |
6 | import java.util.Objects;
7 |
8 | public final class Size {
9 | private final int width;
10 | private final int height;
11 |
12 | public Size(int width, int height) {
13 | this.width = width;
14 | this.height = height;
15 | }
16 |
17 | public int getWidth() {
18 | return width;
19 | }
20 |
21 | public int getHeight() {
22 | return height;
23 | }
24 |
25 | public Size rotate() {
26 | return new Size(height, width);
27 | }
28 |
29 | public Rect toRect() {
30 | return new Rect(0, 0, width, height);
31 | }
32 |
33 | @Override
34 | public boolean equals(Object o) {
35 | if (this == o) {
36 | return true;
37 | }
38 | if (o == null || getClass() != o.getClass()) {
39 | return false;
40 | }
41 | Size size = (Size) o;
42 | return width == size.width
43 | && height == size.height;
44 | }
45 |
46 | @Override
47 | public int hashCode() {
48 | return Objects.hash(width, height);
49 | }
50 |
51 | @Override
52 | public String toString() {
53 | return "Size{"
54 | + "width=" + width
55 | + ", height=" + height
56 | + '}';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/audio/AudioCapture.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.audio;
2 |
3 | import android.media.MediaCodec;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | public interface AudioCapture {
8 | void checkCompatibility() throws AudioCaptureException;
9 | void start() throws AudioCaptureException;
10 | void stop();
11 |
12 | /**
13 | * Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples.
14 | *
15 | * @param outDirectBuffer The target buffer
16 | * @param outBufferInfo The info to provide to MediaCodec
17 | * @return the number of bytes actually read.
18 | */
19 | int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo);
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/audio/AudioCaptureException.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.audio;
2 |
3 | /**
4 | * Exception for any audio capture issue.
5 | *
6 | * This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground.
7 | *
8 | * Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before
9 | * throwing the exception).
10 | */
11 | public class AudioCaptureException extends Exception {
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/audio/AudioConfig.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.audio;
2 |
3 | import android.media.AudioFormat;
4 |
5 | public final class AudioConfig {
6 | public static final int SAMPLE_RATE = 48000;
7 | public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
8 | public static final int CHANNELS = 2;
9 | public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
10 | public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
11 | public static final int BYTES_PER_SAMPLE = 2;
12 |
13 | // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
14 | // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
15 | // receive 4 successive blocks without waiting, then we wait for the 4 next ones).
16 | public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
17 |
18 | private AudioConfig() {
19 | // Not instantiable
20 | }
21 |
22 | public static AudioFormat createAudioFormat() {
23 | AudioFormat.Builder builder = new AudioFormat.Builder();
24 | builder.setEncoding(ENCODING);
25 | builder.setSampleRate(SAMPLE_RATE);
26 | builder.setChannelMask(CHANNEL_CONFIG);
27 | return builder.build();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/audio/AudioDirectCapture.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.audio;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.annotation.TargetApi;
5 | import android.content.ComponentName;
6 | import android.content.Intent;
7 | import android.media.AudioRecord;
8 | import android.media.MediaCodec;
9 | import android.media.MediaRecorder;
10 | import android.os.Build;
11 | import android.os.SystemClock;
12 |
13 |
14 | import org.server.scrcpy.Ln;
15 | import org.server.scrcpy.util.FakeContext;
16 | import org.server.scrcpy.util.Workarounds;
17 | import org.server.scrcpy.wrappers.ServiceManager;
18 |
19 | import java.nio.ByteBuffer;
20 |
21 | public class AudioDirectCapture implements AudioCapture {
22 |
23 | private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
24 | private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG;
25 | private static final int CHANNELS = AudioConfig.CHANNELS;
26 | private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK;
27 | private static final int ENCODING = AudioConfig.ENCODING;
28 |
29 | private final int audioSource;
30 |
31 | private AudioRecord recorder;
32 | private AudioRecordReader reader;
33 |
34 | public AudioDirectCapture(AudioSource audioSource) {
35 | this.audioSource = getAudioSourceValue(audioSource);
36 | }
37 |
38 | private static int getAudioSourceValue(AudioSource audioSource) {
39 | switch (audioSource) {
40 | case OUTPUT:
41 | return MediaRecorder.AudioSource.REMOTE_SUBMIX;
42 | case MIC:
43 | return MediaRecorder.AudioSource.MIC;
44 | default:
45 | throw new IllegalArgumentException("Unsupported audio source: " + audioSource);
46 | }
47 | }
48 |
49 | @TargetApi(Build.VERSION_CODES.M)
50 | @SuppressLint({"WrongConstant", "MissingPermission"})
51 | private static AudioRecord createAudioRecord(int audioSource) {
52 | AudioRecord.Builder builder = new AudioRecord.Builder();
53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
54 | // On older APIs, Workarounds.fillAppInfo() must be called beforehand
55 | builder.setContext(FakeContext.get());
56 | }
57 | builder.setAudioSource(audioSource);
58 | builder.setAudioFormat(AudioConfig.createAudioFormat());
59 | int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
60 | if (minBufferSize > 0) {
61 | // This buffer size does not impact latency
62 | builder.setBufferSizeInBytes(8 * minBufferSize);
63 | }
64 |
65 | return builder.build();
66 | }
67 |
68 | private static void startWorkaroundAndroid11() {
69 | // Android 11 requires Apps to be at foreground to record audio.
70 | // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
71 | // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
72 | // shell ("com.android.shell").
73 | // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
74 | // foreground.
75 | Intent intent = new Intent(Intent.ACTION_MAIN);
76 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
77 | intent.addCategory(Intent.CATEGORY_LAUNCHER);
78 | intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
79 | ServiceManager.getActivityManager().startActivity(intent);
80 | }
81 |
82 | private static void stopWorkaroundAndroid11() {
83 | ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
84 | }
85 |
86 | private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException {
87 | while (attempts-- > 0) {
88 | // Wait for activity to start
89 | SystemClock.sleep(delayMs);
90 | try {
91 | startRecording();
92 | return; // it worked
93 | } catch (UnsupportedOperationException e) {
94 | if (attempts == 0) {
95 | Ln.e("Failed to start audio capture");
96 | Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
97 | + "scrcpy.");
98 | throw new AudioCaptureException();
99 | } else {
100 | Ln.d("Failed to start audio capture, retrying...");
101 | }
102 | }
103 | }
104 | }
105 |
106 | private void startRecording() throws AudioCaptureException {
107 | try {
108 | recorder = createAudioRecord(audioSource);
109 | } catch (NullPointerException e) {
110 | // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
111 | // -
112 | // -
113 | recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
114 | }
115 | recorder.startRecording();
116 | reader = new AudioRecordReader(recorder);
117 | }
118 |
119 | @Override
120 | public void checkCompatibility() throws AudioCaptureException {
121 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
122 | Ln.w("Audio disabled: it is not supported before Android 11");
123 | throw new AudioCaptureException();
124 | }
125 | }
126 |
127 | @Override
128 | public void start() throws AudioCaptureException {
129 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
130 | startWorkaroundAndroid11();
131 | try {
132 | tryStartRecording(5, 100);
133 | } finally {
134 | stopWorkaroundAndroid11();
135 | }
136 | } else {
137 | startRecording();
138 | }
139 | }
140 |
141 | @Override
142 | public void stop() {
143 | if (recorder != null) {
144 | // Will call .stop() if necessary, without throwing an IllegalStateException
145 | recorder.release();
146 | }
147 | }
148 |
149 | @Override
150 | @TargetApi(Build.VERSION_CODES.N)
151 | public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
152 | return reader.read(outDirectBuffer, outBufferInfo);
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/audio/AudioRecordReader.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.audio;
2 |
3 | import android.annotation.TargetApi;
4 | import android.media.AudioRecord;
5 | import android.media.AudioTimestamp;
6 | import android.media.MediaCodec;
7 | import android.os.Build;
8 |
9 |
10 | import org.server.scrcpy.Ln;
11 |
12 | import java.nio.ByteBuffer;
13 |
14 | public class AudioRecordReader {
15 |
16 | private static final long ONE_SAMPLE_US =
17 | (1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
18 |
19 | private final AudioRecord recorder;
20 |
21 | private final AudioTimestamp timestamp = new AudioTimestamp();
22 | private long previousRecorderTimestamp = -1;
23 | private long previousPts = 0;
24 | private long nextPts = 0;
25 |
26 | public AudioRecordReader(AudioRecord recorder) {
27 | this.recorder = recorder;
28 | }
29 |
30 | @TargetApi(Build.VERSION_CODES.N)
31 | public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
32 | int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
33 | if (r <= 0) {
34 | return r;
35 | }
36 |
37 | long pts;
38 |
39 | int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
40 | if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
41 | pts = timestamp.nanoTime / 1000;
42 | previousRecorderTimestamp = timestamp.nanoTime;
43 | } else {
44 | if (nextPts == 0) {
45 | Ln.w("Could not get initial audio timestamp");
46 | nextPts = System.nanoTime() / 1000;
47 | }
48 | // compute from previous timestamp and packet size
49 | pts = nextPts;
50 | }
51 |
52 | long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE);
53 | nextPts = pts + durationUs;
54 |
55 | if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
56 | // Audio PTS may come from two sources:
57 | // - recorder.getTimestamp() if the call works;
58 | // - an estimation from the previous PTS and the packet size as a fallback.
59 | //
60 | // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
61 | pts = previousPts + ONE_SAMPLE_US;
62 | }
63 | previousPts = pts;
64 |
65 | outBufferInfo.set(0, r, pts, 0);
66 | return r;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/audio/AudioSource.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.audio;
2 |
3 | public enum AudioSource {
4 | OUTPUT("output"),
5 | MIC("mic"),
6 | PLAYBACK("playback");
7 |
8 | private final String name;
9 |
10 | AudioSource(String name) {
11 | this.name = name;
12 | }
13 |
14 | public boolean isDirect() {
15 | return this != PLAYBACK;
16 | }
17 |
18 | public static AudioSource findByName(String name) {
19 | for (AudioSource audioSource : AudioSource.values()) {
20 | if (name.equals(audioSource.name)) {
21 | return audioSource;
22 | }
23 | }
24 |
25 | return null;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/control/Pointer.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.control;
2 |
3 |
4 | import org.server.scrcpy.device.Point;
5 |
6 | public class Pointer {
7 |
8 | /**
9 | * Pointer id as received from the client.
10 | */
11 | private final long id;
12 |
13 | /**
14 | * Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}.
15 | */
16 | private final int localId;
17 |
18 | private Point point;
19 | private float pressure;
20 | private boolean up;
21 |
22 | public Pointer(long id, int localId) {
23 | this.id = id;
24 | this.localId = localId;
25 | }
26 |
27 | public long getId() {
28 | return id;
29 | }
30 |
31 | public int getLocalId() {
32 | return localId;
33 | }
34 |
35 | public Point getPoint() {
36 | return point;
37 | }
38 |
39 | public void setPoint(Point point) {
40 | this.point = point;
41 | }
42 |
43 | public float getPressure() {
44 | return pressure;
45 | }
46 |
47 | public void setPressure(float pressure) {
48 | this.pressure = pressure;
49 | }
50 |
51 | public boolean isUp() {
52 | return up;
53 | }
54 |
55 | public void setUp(boolean up) {
56 | this.up = up;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/control/PointersState.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.control;
2 |
3 | import android.view.MotionEvent;
4 |
5 |
6 | import org.server.scrcpy.device.Point;
7 |
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public class PointersState {
12 |
13 | public static final int MAX_POINTERS = 10;
14 |
15 | private final List pointers = new ArrayList<>();
16 |
17 | private int indexOf(long id) {
18 | for (int i = 0; i < pointers.size(); ++i) {
19 | Pointer pointer = pointers.get(i);
20 | if (pointer.getId() == id) {
21 | return i;
22 | }
23 | }
24 | return -1;
25 | }
26 |
27 | private boolean isLocalIdAvailable(int localId) {
28 | for (int i = 0; i < pointers.size(); ++i) {
29 | Pointer pointer = pointers.get(i);
30 | if (pointer.getLocalId() == localId) {
31 | return false;
32 | }
33 | }
34 | return true;
35 | }
36 |
37 | private int nextUnusedLocalId() {
38 | for (int localId = 0; localId < MAX_POINTERS; ++localId) {
39 | if (isLocalIdAvailable(localId)) {
40 | return localId;
41 | }
42 | }
43 | return -1;
44 | }
45 |
46 | public Pointer get(int index) {
47 | return pointers.get(index);
48 | }
49 |
50 | public int getPointerIndex(long id) {
51 | int index = indexOf(id);
52 | if (index != -1) {
53 | // already exists, return it
54 | return index;
55 | }
56 | if (pointers.size() >= MAX_POINTERS) {
57 | // it's full
58 | return -1;
59 | }
60 | // id 0 is reserved for mouse events
61 | int localId = nextUnusedLocalId();
62 | if (localId == -1) {
63 | throw new AssertionError("pointers.size() < maxFingers implies that a local id is available");
64 | }
65 | Pointer pointer = new Pointer(id, localId);
66 | pointers.add(pointer);
67 | // return the index of the pointer
68 | return pointers.size() - 1;
69 | }
70 |
71 | /**
72 | * Initialize the motion event parameters.
73 | *
74 | * @param props the pointer properties
75 | * @param coords the pointer coordinates
76 | * @return The number of items initialized (the number of pointers).
77 | */
78 | public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) {
79 | int count = pointers.size();
80 | for (int i = 0; i < count; ++i) {
81 | Pointer pointer = pointers.get(i);
82 |
83 | // id 0 is reserved for mouse events
84 | props[i].id = pointer.getLocalId();
85 |
86 | Point point = pointer.getPoint();
87 | coords[i].x = point.getX();
88 | coords[i].y = point.getY();
89 | coords[i].pressure = pointer.getPressure();
90 | }
91 | cleanUp();
92 | return count;
93 | }
94 |
95 | /**
96 | * Remove all pointers which are UP.
97 | */
98 | private void cleanUp() {
99 | for (int i = pointers.size() - 1; i >= 0; --i) {
100 | Pointer pointer = pointers.get(i);
101 | if (pointer.isUp()) {
102 | pointers.remove(i);
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/device/Point.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.device;
2 |
3 | import java.util.Objects;
4 |
5 | public class Point {
6 | private final int x;
7 | private final int y;
8 |
9 | public Point(int x, int y) {
10 | this.x = x;
11 | this.y = y;
12 | }
13 |
14 | public int getX() {
15 | return x;
16 | }
17 |
18 | public int getY() {
19 | return y;
20 | }
21 |
22 | @Override
23 | public boolean equals(Object o) {
24 | if (this == o) {
25 | return true;
26 | }
27 | if (o == null || getClass() != o.getClass()) {
28 | return false;
29 | }
30 | Point point = (Point) o;
31 | return x == point.x && y == point.y;
32 | }
33 |
34 | @Override
35 | public int hashCode() {
36 | return Objects.hash(x, y);
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return "Point{" + "x=" + x + ", y=" + y + '}';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/model/AudioPacket.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.model;
2 |
3 | import java.nio.ByteBuffer;
4 |
5 | /**
6 | * Created by Alexandr Golovach on 27.06.16.
7 | * https://www.github.com/alexmprog/VideoCodec
8 | */
9 |
10 | public class AudioPacket extends MediaPacket {
11 |
12 | public Flag flag;
13 | public long presentationTimeStamp;
14 | public byte[] data;
15 |
16 | public AudioPacket() {
17 | }
18 |
19 | public AudioPacket(Type type, Flag flag, long presentationTimeStamp, byte[] data) {
20 | this.type = type;
21 | this.flag = flag;
22 | this.presentationTimeStamp = presentationTimeStamp;
23 | this.data = data;
24 | }
25 |
26 | // create packet from byte array
27 | public static AudioPacket fromArray(byte[] values) {
28 | AudioPacket videoPacket = new AudioPacket();
29 |
30 | // should be a type value - 1 byte
31 | byte typeValue = values[0];
32 | // should be a flag value - 1 byte
33 | byte flagValue = values[1];
34 |
35 | videoPacket.type = Type.getType(typeValue);
36 | videoPacket.flag = Flag.getFlag(flagValue);
37 |
38 | // should be 8 bytes for timestamp
39 | byte[] timeStamp = new byte[8];
40 | System.arraycopy(values, 2, timeStamp, 0, 8);
41 | videoPacket.presentationTimeStamp = ByteUtils.bytesToLong(timeStamp);
42 |
43 | // all other bytes is data
44 | int dataLength = values.length - 10;
45 | byte[] data = new byte[dataLength];
46 | System.arraycopy(values, 10, data, 0, dataLength);
47 | videoPacket.data = data;
48 |
49 | return videoPacket;
50 | }
51 |
52 | // create byte array
53 | public static byte[] toArray(Type type, Flag flag, long presentationTimeStamp, byte[] data) {
54 |
55 | // should be 4 bytes for packet size
56 | byte[] bytes = ByteUtils.intToBytes(10 + data.length);
57 |
58 | int packetSize = 14 + data.length; // 4 - inner packet size 1 - type + 1 - flag + 8 - timeStamp + data.length
59 | byte[] values = new byte[packetSize];
60 |
61 | System.arraycopy(bytes, 0, values, 0, 4);
62 |
63 | // set type value
64 | values[4] = type.getType();
65 | // set flag value
66 | values[5] = flag.getFlag();
67 | // set timeStamp
68 | byte[] longToBytes = ByteUtils.longToBytes(presentationTimeStamp);
69 | System.arraycopy(longToBytes, 0, values, 6, longToBytes.length);
70 |
71 | // set data array
72 | System.arraycopy(data, 0, values, 14, data.length);
73 | return values;
74 | }
75 |
76 | // should call on inner packet
77 | public static boolean isVideoPacket(byte[] values) {
78 | return values[0] == Type.VIDEO.getType();
79 | }
80 |
81 | public static StreamSettings getStreamSettings(byte[] buffer) {
82 | byte[] sps, pps;
83 |
84 | ByteBuffer spsPpsBuffer = ByteBuffer.wrap(buffer);
85 | if (spsPpsBuffer.getInt() == 0x00000001) {
86 | System.out.println("parsing sps/pps");
87 | } else {
88 | System.out.println("something is amiss?");
89 | }
90 | int ppsIndex = 0;
91 | while (!(spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x01)) {
92 |
93 | }
94 | ppsIndex = spsPpsBuffer.position();
95 | sps = new byte[ppsIndex - 4];
96 | System.arraycopy(buffer, 0, sps, 0, sps.length);
97 | ppsIndex -= 4;
98 | pps = new byte[buffer.length - ppsIndex];
99 | System.arraycopy(buffer, ppsIndex, pps, 0, pps.length);
100 |
101 | // sps buffer
102 | ByteBuffer spsBuffer = ByteBuffer.wrap(sps, 0, sps.length);
103 |
104 | // pps buffer
105 | ByteBuffer ppsBuffer = ByteBuffer.wrap(pps, 0, pps.length);
106 |
107 | StreamSettings streamSettings = new StreamSettings();
108 | streamSettings.sps = spsBuffer;
109 | streamSettings.pps = ppsBuffer;
110 |
111 | return streamSettings;
112 | }
113 |
114 | public byte[] toByteArray() {
115 | return toArray(type, flag, presentationTimeStamp, data);
116 | }
117 |
118 | public enum Flag {
119 |
120 | FRAME((byte) 0), KEY_FRAME((byte) 1), CONFIG((byte) 2), END((byte) 4);
121 |
122 | private byte type;
123 |
124 | Flag(byte type) {
125 | this.type = type;
126 | }
127 |
128 | public static Flag getFlag(byte value) {
129 | for (Flag type : Flag.values()) {
130 | if (type.getFlag() == value) {
131 | return type;
132 | }
133 | }
134 |
135 | return null;
136 | }
137 |
138 | public byte getFlag() {
139 | return type;
140 | }
141 | }
142 |
143 | public static class StreamSettings {
144 | public ByteBuffer pps;
145 | public ByteBuffer sps;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/model/ByteUtils.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.model;
2 |
3 | import java.math.BigInteger;
4 | import java.nio.ByteBuffer;
5 |
6 | /**
7 | * Created by Alexandr Golovach on 27.06.16.
8 | */
9 |
10 | public class ByteUtils {
11 |
12 | public static byte[] longToBytes(long x) {
13 | ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / 8);
14 | buffer.putLong(0, x);
15 | return buffer.array();
16 | }
17 |
18 | public static long bytesToLong(byte[] bytes) {
19 | return new BigInteger(bytes).longValue();
20 | }
21 |
22 | public static byte[] intToBytes(int x) {
23 | ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / 8);
24 | buffer.putInt(0, x);
25 | return buffer.array();
26 | }
27 |
28 | public static int bytesToInt(byte[] bytes) {
29 | return new BigInteger(bytes).intValue();
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/model/MediaPacket.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.model;
2 |
3 | /**
4 | * Created by Alexandr Golovach on 27.06.16.
5 | */
6 | public class MediaPacket {
7 |
8 | public Type type;
9 |
10 | public enum Type {
11 |
12 | VIDEO((byte) 1), AUDIO((byte) 0);
13 |
14 | private byte type;
15 |
16 | Type(byte type) {
17 | this.type = type;
18 | }
19 |
20 | public static Type getType(byte value) {
21 | for (Type type : Type.values()) {
22 | if (type.getType() == value) {
23 | return type;
24 | }
25 | }
26 |
27 | return null;
28 | }
29 |
30 | public byte getType() {
31 | return type;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/model/VideoPacket.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.model;
2 |
3 | import java.nio.ByteBuffer;
4 |
5 | /**
6 | * Created by Alexandr Golovach on 27.06.16.
7 | */
8 |
9 | public class VideoPacket extends MediaPacket {
10 |
11 | public Flag flag;
12 | public long presentationTimeStamp;
13 | public byte[] data;
14 |
15 | public VideoPacket() {
16 | }
17 |
18 | public VideoPacket(Type type, Flag flag, long presentationTimeStamp, byte[] data) {
19 | this.type = type;
20 | this.flag = flag;
21 | this.presentationTimeStamp = presentationTimeStamp;
22 | this.data = data;
23 | }
24 |
25 | // create packet from byte array
26 | public static VideoPacket fromArray(byte[] values) {
27 | VideoPacket videoPacket = new VideoPacket();
28 |
29 | // should be a type value - 1 byte
30 | byte typeValue = values[0];
31 | // should be a flag value - 1 byte
32 | byte flagValue = values[1];
33 |
34 | videoPacket.type = Type.getType(typeValue);
35 | videoPacket.flag = Flag.getFlag(flagValue);
36 |
37 | // should be 8 bytes for timestamp
38 | byte[] timeStamp = new byte[8];
39 | System.arraycopy(values, 2, timeStamp, 0, 8);
40 | videoPacket.presentationTimeStamp = ByteUtils.bytesToLong(timeStamp);
41 |
42 | // all other bytes is data
43 | int dataLength = values.length - 10;
44 | byte[] data = new byte[dataLength];
45 | System.arraycopy(values, 10, data, 0, dataLength);
46 | videoPacket.data = data;
47 |
48 | return videoPacket;
49 | }
50 |
51 | // create byte array
52 | public static byte[] toArray(Type type, Flag flag, long presentationTimeStamp, byte[] data) {
53 |
54 | // should be 4 bytes for packet size
55 | byte[] bytes = ByteUtils.intToBytes(10 + data.length);
56 |
57 | int packetSize = 14 + data.length; // 4 - inner packet size 1 - type + 1 - flag + 8 - timeStamp + data.length
58 | byte[] values = new byte[packetSize];
59 |
60 | System.arraycopy(bytes, 0, values, 0, 4);
61 |
62 | // set type value
63 | values[4] = type.getType();
64 | // set flag value
65 | values[5] = flag.getFlag();
66 | // set timeStamp
67 | byte[] longToBytes = ByteUtils.longToBytes(presentationTimeStamp);
68 | System.arraycopy(longToBytes, 0, values, 6, longToBytes.length);
69 |
70 | // set data array
71 | System.arraycopy(data, 0, values, 14, data.length);
72 | return values;
73 | }
74 |
75 | // should call on inner packet
76 | public static boolean isVideoPacket(byte[] values) {
77 | return values[0] == Type.VIDEO.getType();
78 | }
79 |
80 | public static StreamSettings getStreamSettings(byte[] buffer) {
81 | byte[] sps, pps;
82 |
83 | ByteBuffer spsPpsBuffer = ByteBuffer.wrap(buffer);
84 | if (spsPpsBuffer.getInt() == 0x00000001) {
85 | System.out.println("parsing sps/pps");
86 | } else {
87 | System.out.println("something is amiss?");
88 | }
89 | int ppsIndex = 0;
90 | while (!(spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x01)) {
91 |
92 | }
93 | ppsIndex = spsPpsBuffer.position();
94 | sps = new byte[ppsIndex - 4];
95 | System.arraycopy(buffer, 0, sps, 0, sps.length);
96 | ppsIndex -= 4;
97 | pps = new byte[buffer.length - ppsIndex];
98 | System.arraycopy(buffer, ppsIndex, pps, 0, pps.length);
99 |
100 | // sps buffer
101 | ByteBuffer spsBuffer = ByteBuffer.wrap(sps, 0, sps.length);
102 |
103 | // pps buffer
104 | ByteBuffer ppsBuffer = ByteBuffer.wrap(pps, 0, pps.length);
105 |
106 | StreamSettings streamSettings = new StreamSettings();
107 | streamSettings.sps = spsBuffer;
108 | streamSettings.pps = ppsBuffer;
109 |
110 | return streamSettings;
111 | }
112 |
113 | public byte[] toByteArray() {
114 | return toArray(type, flag, presentationTimeStamp, data);
115 | }
116 |
117 | public enum Flag {
118 |
119 | FRAME((byte) 0), KEY_FRAME((byte) 1), CONFIG((byte) 2), END((byte) 4);
120 |
121 | private byte type;
122 |
123 | Flag(byte type) {
124 | this.type = type;
125 | }
126 |
127 | public static Flag getFlag(byte value) {
128 | for (Flag type : Flag.values()) {
129 | if (type.getFlag() == value) {
130 | return type;
131 | }
132 | }
133 |
134 | return null;
135 | }
136 |
137 | public byte getFlag() {
138 | return type;
139 | }
140 | }
141 |
142 | public static class StreamSettings {
143 | public ByteBuffer pps;
144 | public ByteBuffer sps;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/util/Command.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.util;
2 |
3 |
4 | import java.io.IOException;
5 | import java.util.Arrays;
6 | import java.util.Scanner;
7 |
8 | public final class Command {
9 | private Command() {
10 | // not instantiable
11 | }
12 |
13 | public static void exec(String... cmd) throws IOException, InterruptedException {
14 | Process process = Runtime.getRuntime().exec(cmd);
15 | int exitCode = process.waitFor();
16 | if (exitCode != 0) {
17 | throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
18 | }
19 | }
20 |
21 | public static String execReadLine(String... cmd) throws IOException, InterruptedException {
22 | String result = null;
23 | Process process = Runtime.getRuntime().exec(cmd);
24 | Scanner scanner = new Scanner(process.getInputStream());
25 | if (scanner.hasNextLine()) {
26 | result = scanner.nextLine();
27 | }
28 | int exitCode = process.waitFor();
29 | if (exitCode != 0) {
30 | throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
31 | }
32 | return result;
33 | }
34 |
35 | public static String execReadOutput(String... cmd) throws IOException, InterruptedException {
36 | Process process = Runtime.getRuntime().exec(cmd);
37 | String output = IO.toString(process.getInputStream());
38 | int exitCode = process.waitFor();
39 | if (exitCode != 0) {
40 | throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
41 | }
42 | return output;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/util/FakeContext.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.util;
2 |
3 | import android.content.AttributionSource;
4 | import android.content.ContentResolver;
5 | import android.content.Context;
6 | import android.content.ContextWrapper;
7 | import android.content.IContentProvider;
8 | import android.os.Binder;
9 | import android.os.Build;
10 | import android.os.Process;
11 |
12 | import org.server.scrcpy.wrappers.ServiceManager;
13 |
14 |
15 | public final class FakeContext extends ContextWrapper {
16 |
17 | public static final String PACKAGE_NAME = "com.android.shell";
18 | public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
19 |
20 | private static final FakeContext INSTANCE = new FakeContext();
21 |
22 | public static FakeContext get() {
23 | return INSTANCE;
24 | }
25 |
26 | private final ContentResolver contentResolver = new ContentResolver(this) {
27 | // @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
28 | protected IContentProvider acquireProvider(Context c, String name) {
29 | return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder());
30 | }
31 |
32 | // @SuppressWarnings("unused")
33 | // @Override (but super-class method not visible)
34 | public boolean releaseProvider(IContentProvider icp) {
35 | return false;
36 | }
37 |
38 | // @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
39 | // @Override (but super-class method not visible)
40 | protected IContentProvider acquireUnstableProvider(Context c, String name) {
41 | return null;
42 | }
43 |
44 | // @SuppressWarnings("unused")
45 | // @Override (but super-class method not visible)
46 | public boolean releaseUnstableProvider(IContentProvider icp) {
47 | return false;
48 | }
49 |
50 | // @SuppressWarnings("unused")
51 | // @Override (but super-class method not visible)
52 | public void unstableProviderDied(IContentProvider icp) {
53 | // ignore
54 | }
55 | };
56 |
57 | private FakeContext() {
58 | super(Workarounds.getSystemContext());
59 | }
60 |
61 | @Override
62 | public String getPackageName() {
63 | return PACKAGE_NAME;
64 | }
65 |
66 | @Override
67 | public String getOpPackageName() {
68 | return PACKAGE_NAME;
69 | }
70 |
71 | @Override
72 | public AttributionSource getAttributionSource() {
73 | AttributionSource.Builder builder = null;
74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
75 | builder = new AttributionSource.Builder(Process.SHELL_UID);
76 | }
77 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
78 | builder.setPackageName(PACKAGE_NAME);
79 | }
80 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
81 | return builder.build();
82 | } else {
83 | return null;
84 | }
85 | }
86 |
87 | // @Override to be added on SDK upgrade for Android 14
88 | @SuppressWarnings("unused")
89 | public int getDeviceId() {
90 | return 0;
91 | }
92 |
93 | @Override
94 | public Context getApplicationContext() {
95 | return this;
96 | }
97 |
98 | @Override
99 | public ContentResolver getContentResolver() {
100 | return contentResolver;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/util/IO.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.util;
2 |
3 |
4 | import java.io.InputStream;
5 | import java.util.Scanner;
6 | public final class IO {
7 | private IO() {
8 | // not instantiable
9 | }
10 |
11 | public static String toString(InputStream inputStream) {
12 | StringBuilder builder = new StringBuilder();
13 | Scanner scanner = new Scanner(inputStream);
14 | while (scanner.hasNextLine()) {
15 | builder.append(scanner.nextLine()).append('\n');
16 | }
17 | return builder.toString();
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/util/SettingsException.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.util;
2 |
3 | public class SettingsException extends Exception {
4 | private static String createMessage(String method, String table, String key, String value) {
5 | return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
6 | }
7 |
8 | public SettingsException(String method, String table, String key, String value, Throwable cause) {
9 | super(createMessage(method, table, key, value), cause);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/ActivityManager.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.annotation.TargetApi;
5 | import android.content.IContentProvider;
6 | import android.content.Intent;
7 | import android.os.Binder;
8 | import android.os.Bundle;
9 | import android.os.IBinder;
10 | import android.os.IInterface;
11 |
12 |
13 | import org.server.scrcpy.Ln;
14 | import org.server.scrcpy.util.FakeContext;
15 |
16 | import java.lang.reflect.Field;
17 | import java.lang.reflect.Method;
18 |
19 | @SuppressLint("PrivateApi,DiscouragedPrivateApi")
20 | public final class ActivityManager {
21 |
22 | private final IInterface manager;
23 | private Method getContentProviderExternalMethod;
24 | private boolean getContentProviderExternalMethodNewVersion = true;
25 | private Method removeContentProviderExternalMethod;
26 | private Method startActivityAsUserMethod;
27 | private Method forceStopPackageMethod;
28 |
29 | static ActivityManager create() {
30 | try {
31 | // On old Android versions, the ActivityManager is not exposed via AIDL,
32 | // so use ActivityManagerNative.getDefault()
33 | Class> cls = Class.forName("android.app.ActivityManagerNative");
34 | Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
35 | IInterface am = (IInterface) getDefaultMethod.invoke(null);
36 | return new ActivityManager(am);
37 | } catch (ReflectiveOperationException e) {
38 | throw new AssertionError(e);
39 | }
40 | }
41 |
42 | private ActivityManager(IInterface manager) {
43 | this.manager = manager;
44 | }
45 |
46 | private Method getGetContentProviderExternalMethod() throws NoSuchMethodException {
47 | if (getContentProviderExternalMethod == null) {
48 | try {
49 | getContentProviderExternalMethod = manager.getClass()
50 | .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class);
51 | } catch (NoSuchMethodException e) {
52 | // old version
53 | getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
54 | getContentProviderExternalMethodNewVersion = false;
55 | }
56 | }
57 | return getContentProviderExternalMethod;
58 | }
59 |
60 | private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException {
61 | if (removeContentProviderExternalMethod == null) {
62 | removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class);
63 | }
64 | return removeContentProviderExternalMethod;
65 | }
66 |
67 | public IContentProvider getContentProviderExternal(String name, IBinder token) {
68 | try {
69 | Method method = getGetContentProviderExternalMethod();
70 | Object[] args;
71 | if (getContentProviderExternalMethodNewVersion) {
72 | // new version
73 | args = new Object[]{name, FakeContext.ROOT_UID, token, null};
74 | } else {
75 | // old version
76 | args = new Object[]{name, FakeContext.ROOT_UID, token};
77 | }
78 | // ContentProviderHolder providerHolder = getContentProviderExternal(...);
79 | Object providerHolder = method.invoke(manager, args);
80 | if (providerHolder == null) {
81 | return null;
82 | }
83 | // IContentProvider provider = providerHolder.provider;
84 | Field providerField = providerHolder.getClass().getDeclaredField("provider");
85 | providerField.setAccessible(true);
86 | return (IContentProvider) providerField.get(providerHolder);
87 | } catch (ReflectiveOperationException e) {
88 | Ln.e("Could not invoke method", e);
89 | return null;
90 | }
91 | }
92 |
93 | void removeContentProviderExternal(String name, IBinder token) {
94 | try {
95 | Method method = getRemoveContentProviderExternalMethod();
96 | method.invoke(manager, name, token);
97 | } catch (ReflectiveOperationException e) {
98 | Ln.e("Could not invoke method", e);
99 | }
100 | }
101 |
102 | public ContentProvider createSettingsProvider() {
103 | IBinder token = new Binder();
104 | IContentProvider provider = getContentProviderExternal("settings", token);
105 | if (provider == null) {
106 | return null;
107 | }
108 | return new ContentProvider(this, provider, "settings", token);
109 | }
110 |
111 | private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException {
112 | if (startActivityAsUserMethod == null) {
113 | Class> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
114 | Class> profilerInfo = Class.forName("android.app.ProfilerInfo");
115 | startActivityAsUserMethod = manager.getClass()
116 | .getMethod("startActivityAsUser", iApplicationThreadClass, String.class, Intent.class, String.class, IBinder.class, String.class,
117 | int.class, int.class, profilerInfo, Bundle.class, int.class);
118 | }
119 | return startActivityAsUserMethod;
120 | }
121 |
122 | public int startActivity(Intent intent) {
123 | return startActivity(intent, null);
124 | }
125 |
126 | @SuppressWarnings("ConstantConditions")
127 | public int startActivity(Intent intent, Bundle options) {
128 | try {
129 | Method method = getStartActivityAsUserMethod();
130 | return (int) method.invoke(
131 | /* this */ manager,
132 | /* caller */ null,
133 | /* callingPackage */ FakeContext.PACKAGE_NAME,
134 | /* intent */ intent,
135 | /* resolvedType */ null,
136 | /* resultTo */ null,
137 | /* resultWho */ null,
138 | /* requestCode */ 0,
139 | /* startFlags */ 0,
140 | /* profilerInfo */ null,
141 | /* bOptions */ options,
142 | /* userId */ /* UserHandle.USER_CURRENT */ -2);
143 | } catch (Throwable e) {
144 | Ln.e("Could not invoke method", e);
145 | return 0;
146 | }
147 | }
148 |
149 | private Method getForceStopPackageMethod() throws NoSuchMethodException {
150 | if (forceStopPackageMethod == null) {
151 | forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class);
152 | }
153 | return forceStopPackageMethod;
154 | }
155 |
156 | public void forceStopPackage(String packageName) {
157 | try {
158 | Method method = getForceStopPackageMethod();
159 | method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2);
160 | } catch (Throwable e) {
161 | Ln.e("Could not invoke method", e);
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/ContentProvider.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.AttributionSource;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.os.IBinder;
8 |
9 |
10 | import org.server.scrcpy.Ln;
11 | import org.server.scrcpy.util.FakeContext;
12 | import org.server.scrcpy.util.SettingsException;
13 |
14 | import java.io.Closeable;
15 | import java.lang.reflect.Method;
16 |
17 | public final class ContentProvider implements Closeable {
18 |
19 | public static final String TABLE_SYSTEM = "system";
20 | public static final String TABLE_SECURE = "secure";
21 | public static final String TABLE_GLOBAL = "global";
22 |
23 | // See android/providerHolder/Settings.java
24 | private static final String CALL_METHOD_GET_SYSTEM = "GET_system";
25 | private static final String CALL_METHOD_GET_SECURE = "GET_secure";
26 | private static final String CALL_METHOD_GET_GLOBAL = "GET_global";
27 |
28 | private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
29 | private static final String CALL_METHOD_PUT_SECURE = "PUT_secure";
30 | private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global";
31 |
32 | private static final String CALL_METHOD_USER_KEY = "_user";
33 |
34 | private static final String NAME_VALUE_TABLE_VALUE = "value";
35 |
36 | private final ActivityManager manager;
37 | // android.content.IContentProvider
38 | private final Object provider;
39 | private final String name;
40 | private final IBinder token;
41 |
42 | private Method callMethod;
43 | private int callMethodVersion;
44 |
45 | ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
46 | this.manager = manager;
47 | this.provider = provider;
48 | this.name = name;
49 | this.token = token;
50 | }
51 |
52 | @SuppressLint("PrivateApi")
53 | private Method getCallMethod() throws NoSuchMethodException {
54 | if (callMethod == null) {
55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
56 | callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
57 | callMethodVersion = 0;
58 | } else {
59 | // old versions
60 | try {
61 | callMethod = provider.getClass()
62 | .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class);
63 | callMethodVersion = 1;
64 | } catch (NoSuchMethodException e1) {
65 | try {
66 | callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
67 | callMethodVersion = 2;
68 | } catch (NoSuchMethodException e2) {
69 | callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
70 | callMethodVersion = 3;
71 | }
72 | }
73 | }
74 | }
75 | return callMethod;
76 | }
77 |
78 | private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException {
79 | try {
80 | Method method = getCallMethod();
81 | Object[] args;
82 |
83 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
84 | args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
85 | } else {
86 | switch (callMethodVersion) {
87 | case 1:
88 | args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
89 | break;
90 | case 2:
91 | args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
92 | break;
93 | default:
94 | args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
95 | break;
96 | }
97 | }
98 | return (Bundle) method.invoke(provider, args);
99 | } catch (ReflectiveOperationException e) {
100 | Ln.e("Could not invoke method", e);
101 | throw e;
102 | }
103 | }
104 |
105 | public void close() {
106 | manager.removeContentProviderExternal(name, token);
107 | }
108 |
109 | private static String getGetMethod(String table) {
110 | switch (table) {
111 | case TABLE_SECURE:
112 | return CALL_METHOD_GET_SECURE;
113 | case TABLE_SYSTEM:
114 | return CALL_METHOD_GET_SYSTEM;
115 | case TABLE_GLOBAL:
116 | return CALL_METHOD_GET_GLOBAL;
117 | default:
118 | throw new IllegalArgumentException("Invalid table: " + table);
119 | }
120 | }
121 |
122 | private static String getPutMethod(String table) {
123 | switch (table) {
124 | case TABLE_SECURE:
125 | return CALL_METHOD_PUT_SECURE;
126 | case TABLE_SYSTEM:
127 | return CALL_METHOD_PUT_SYSTEM;
128 | case TABLE_GLOBAL:
129 | return CALL_METHOD_PUT_GLOBAL;
130 | default:
131 | throw new IllegalArgumentException("Invalid table: " + table);
132 | }
133 | }
134 |
135 | public String getValue(String table, String key) throws SettingsException {
136 | String method = getGetMethod(table);
137 | Bundle arg = new Bundle();
138 | arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
139 | try {
140 | Bundle bundle = call(method, key, arg);
141 | if (bundle == null) {
142 | return null;
143 | }
144 | return bundle.getString("value");
145 | } catch (Exception e) {
146 | throw new SettingsException(table, "get", key, null, e);
147 | }
148 |
149 | }
150 |
151 | public void putValue(String table, String key, String value) throws SettingsException {
152 | String method = getPutMethod(table);
153 | Bundle arg = new Bundle();
154 | arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
155 | arg.putString(NAME_VALUE_TABLE_VALUE, value);
156 | try {
157 | call(method, key, arg);
158 | } catch (Exception e) {
159 | throw new SettingsException(table, "put", key, value, e);
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/DisplayManager.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.hardware.display.VirtualDisplay;
4 | import android.os.IInterface;
5 | import android.view.Display;
6 | import android.view.Surface;
7 |
8 | import org.server.scrcpy.DisplayInfo;
9 | import org.server.scrcpy.Ln;
10 | import org.server.scrcpy.Size;
11 | import org.server.scrcpy.util.Command;
12 |
13 | import java.lang.reflect.Field;
14 | import java.lang.reflect.Method;
15 | import java.util.regex.Matcher;
16 | import java.util.regex.Pattern;
17 | public final class DisplayManager {
18 | private final IInterface manager;
19 |
20 | private Method createVirtualDisplayMethod;
21 |
22 | public DisplayManager(IInterface manager) {
23 | this.manager = manager;
24 | }
25 |
26 | private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
27 | try {
28 | String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display");
29 | return parseDisplayInfo(dumpsysDisplayOutput, displayId);
30 | } catch (Exception e) {
31 | Ln.e("Could not get display info from \"dumpsys display\" output", e);
32 | return null;
33 | }
34 | }
35 |
36 | public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) {
37 | Pattern regex = Pattern.compile(
38 | "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, "
39 | + "rotation ([0-9]+).*?, layerStack ([0-9]+)",
40 | Pattern.MULTILINE);
41 | Matcher m = regex.matcher(dumpsysDisplayOutput);
42 | if (!m.find()) {
43 | return null;
44 | }
45 | int flags = parseDisplayFlags(m.group(1));
46 | int width = Integer.parseInt(m.group(2));
47 | int height = Integer.parseInt(m.group(3));
48 | int rotation = Integer.parseInt(m.group(4));
49 | int layerStack = Integer.parseInt(m.group(5));
50 |
51 | return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
52 | }
53 |
54 | private static int parseDisplayFlags(String text) {
55 | Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
56 | if (text == null) {
57 | return 0;
58 | }
59 |
60 | int flags = 0;
61 | Matcher m = regex.matcher(text);
62 | while (m.find()) {
63 | String flagString = m.group();
64 | try {
65 | Field filed = Display.class.getDeclaredField(flagString);
66 | flags |= filed.getInt(null);
67 | } catch (ReflectiveOperationException e) {
68 | // Silently ignore, some flags reported by "dumpsys display" are @TestApi
69 | }
70 | }
71 | return flags;
72 | }
73 |
74 |
75 | public DisplayInfo getDisplayInfo() {
76 | try {
77 | Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
78 | Class> cls = displayInfo.getClass();
79 | // width and height already take the rotation into account
80 | int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
81 | int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
82 | int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
83 | return new DisplayInfo(new Size(width, height), rotation);
84 | } catch (Exception e) {
85 | throw new AssertionError(e);
86 | }
87 | }
88 |
89 | public DisplayInfo getDisplayInfo(int displayId) {
90 | try {
91 | Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
92 | if (displayInfo == null) {
93 | // fallback when displayInfo is null
94 | return getDisplayInfoFromDumpsysDisplay(displayId);
95 | }
96 | Class> cls = displayInfo.getClass();
97 | // width and height already take the rotation into account
98 | int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
99 | int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
100 | int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
101 | int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
102 | int flags = cls.getDeclaredField("flags").getInt(displayInfo);
103 | return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
104 | } catch (ReflectiveOperationException e) {
105 | throw new AssertionError(e);
106 | }
107 | }
108 | public int[] getDisplayIds() {
109 | try {
110 | return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
111 | } catch (ReflectiveOperationException e) {
112 | throw new AssertionError(e);
113 | }
114 | }
115 |
116 | private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException {
117 | if (createVirtualDisplayMethod == null) {
118 | createVirtualDisplayMethod = android.hardware.display.DisplayManager.class
119 | .getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class);
120 | }
121 | return createVirtualDisplayMethod;
122 | }
123 |
124 | public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception {
125 | Method method = getCreateVirtualDisplayMethod();
126 | return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/InputManager.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.os.IInterface;
4 | import android.view.InputEvent;
5 |
6 | import java.lang.reflect.InvocationTargetException;
7 | import java.lang.reflect.Method;
8 |
9 | public final class InputManager {
10 |
11 | public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
12 | public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
13 | public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
14 |
15 | private final IInterface manager;
16 | private final Method injectInputEventMethod;
17 |
18 | public InputManager(IInterface manager) {
19 | this.manager = manager;
20 | try {
21 | injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
22 | } catch (NoSuchMethodException e) {
23 | throw new AssertionError(e);
24 | }
25 | }
26 |
27 | public boolean injectInputEvent(InputEvent inputEvent, int mode) {
28 | try {
29 | return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
30 | } catch (InvocationTargetException | IllegalAccessException e) {
31 | // throw new AssertionError(e);
32 | return false;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/PowerManager.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.Build;
5 | import android.os.IInterface;
6 |
7 |
8 | import java.lang.reflect.InvocationTargetException;
9 | import java.lang.reflect.Method;
10 | public final class PowerManager {
11 | private final IInterface manager;
12 | private final Method isScreenOnMethod;
13 |
14 | public PowerManager(IInterface manager) {
15 | this.manager = manager;
16 | try {
17 | @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
18 | String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
19 | isScreenOnMethod = manager.getClass().getMethod(methodName);
20 | } catch (NoSuchMethodException e) {
21 | throw new AssertionError(e);
22 | }
23 | }
24 |
25 | public boolean isScreenOn() {
26 | try {
27 | return (Boolean) isScreenOnMethod.invoke(manager);
28 | } catch (InvocationTargetException | IllegalAccessException e) {
29 | // throw new AssertionError(e);
30 | return false;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/ServiceManager.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.IBinder;
5 | import android.os.IInterface;
6 |
7 |
8 | import java.lang.reflect.Method;
9 | @SuppressLint("PrivateApi")
10 | public final class ServiceManager {
11 | private static final Method getServiceMethod;
12 |
13 | private static WindowManager windowManager;
14 | private static DisplayManager displayManager;
15 | private static InputManager inputManager;
16 | private static PowerManager powerManager;
17 |
18 | private static ActivityManager activityManager;
19 |
20 | static {
21 | try {
22 | getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
23 | } catch (Exception e) {
24 | throw new AssertionError(e);
25 | }
26 | }
27 |
28 | private ServiceManager() {
29 |
30 | }
31 |
32 | static IInterface getService(String service, String type) {
33 | try {
34 | IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
35 | Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
36 | return (IInterface) asInterfaceMethod.invoke(null, binder);
37 | } catch (Exception e) {
38 | throw new AssertionError(e);
39 | }
40 | }
41 |
42 | public static WindowManager getWindowManager() {
43 | if (windowManager == null) {
44 | windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
45 | }
46 | return windowManager;
47 | }
48 |
49 | public static DisplayManager getDisplayManager() {
50 | if (displayManager == null) {
51 | displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
52 | }
53 | return displayManager;
54 | }
55 |
56 | public static InputManager getInputManager() {
57 | if (inputManager == null) {
58 | inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
59 | }
60 | return inputManager;
61 | }
62 |
63 | public static PowerManager getPowerManager() {
64 | if (powerManager == null) {
65 | powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
66 | }
67 | return powerManager;
68 | }
69 |
70 | public static ActivityManager getActivityManager() {
71 | if (activityManager == null) {
72 | activityManager = ActivityManager.create();
73 | }
74 | return activityManager;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/SurfaceControl.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.graphics.Rect;
5 | import android.os.IBinder;
6 | import android.view.Surface;
7 |
8 |
9 | @SuppressLint("PrivateApi")
10 | public final class SurfaceControl {
11 |
12 | private static final Class> CLASS;
13 |
14 | static {
15 | try {
16 | CLASS = Class.forName("android.view.SurfaceControl");
17 | } catch (ClassNotFoundException e) {
18 | throw new AssertionError(e);
19 | }
20 | }
21 |
22 | private SurfaceControl() {
23 | // only static methods
24 | }
25 |
26 | public static void openTransaction() {
27 | try {
28 | CLASS.getMethod("openTransaction").invoke(null);
29 | } catch (Exception e) {
30 | throw new AssertionError(e);
31 | }
32 | }
33 |
34 | public static void closeTransaction() {
35 | try {
36 | CLASS.getMethod("closeTransaction").invoke(null);
37 | } catch (Exception e) {
38 | throw new AssertionError(e);
39 | }
40 | }
41 |
42 | public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
43 | try {
44 | CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class)
45 | .invoke(null, displayToken, orientation, layerStackRect, displayRect);
46 | } catch (Exception e) {
47 | throw new AssertionError(e);
48 | }
49 | }
50 |
51 | public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
52 | try {
53 | CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack);
54 | } catch (Exception e) {
55 | throw new AssertionError(e);
56 | }
57 | }
58 |
59 | public static void setDisplaySurface(IBinder displayToken, Surface surface) {
60 | try {
61 | CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface);
62 | } catch (Exception e) {
63 | throw new AssertionError(e);
64 | }
65 | }
66 |
67 | public static IBinder createDisplay(String name, boolean secure) {
68 | try {
69 | return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
70 | } catch (Exception e) {
71 | throw new AssertionError(e);
72 | }
73 | }
74 |
75 | public static void destroyDisplay(IBinder displayToken) {
76 | try {
77 | CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);
78 | } catch (Exception e) {
79 | throw new AssertionError(e);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/server/src/main/java/org/server/scrcpy/wrappers/WindowManager.java:
--------------------------------------------------------------------------------
1 | package org.server.scrcpy.wrappers;
2 |
3 | import android.os.IInterface;
4 | import android.view.IRotationWatcher;
5 |
6 |
7 | public final class WindowManager {
8 | private final IInterface manager;
9 |
10 | public WindowManager(IInterface manager) {
11 | this.manager = manager;
12 | }
13 |
14 | public int getRotation() {
15 | try {
16 | Class> cls = manager.getClass();
17 | try {
18 | return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
19 | } catch (NoSuchMethodException e) {
20 | // method changed since this commit:
21 | // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
22 | return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
23 | }
24 | } catch (Exception e) {
25 | throw new AssertionError(e);
26 | }
27 | }
28 |
29 | public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
30 | try {
31 | Class> cls = manager.getClass();
32 | try {
33 | cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
34 | } catch (NoSuchMethodException e) {
35 | // display parameter added since this commit:
36 | // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
37 | cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
38 | }
39 | } catch (Exception e) {
40 | throw new AssertionError(e);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 |
2 | pluginManagement {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | gradlePluginPortal()
7 | }
8 | plugins {
9 | }
10 | }
11 |
12 | include ':server', ':app'
13 |
--------------------------------------------------------------------------------