├── .gitignore
├── .idea
├── caches
│ └── build_file_checksums.ser
├── codeStyles
│ └── Project.xml
├── gradle.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── libs
│ └── commons-codec-1.10-rep.jar
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── org
│ │ └── las2mile
│ │ └── scrcpy
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ ├── scrcpy-server.jar
│ │ └── scrcpy-server_old.jar
│ ├── java
│ │ └── org
│ │ │ └── las2mile
│ │ │ └── scrcpy
│ │ │ ├── MainActivity.java
│ │ │ ├── Scrcpy.java
│ │ │ ├── SendCommands.java
│ │ │ ├── adblib
│ │ │ ├── AdbBase64.java
│ │ │ ├── AdbConnection.java
│ │ │ ├── AdbCrypto.java
│ │ │ ├── AdbProtocol.java
│ │ │ ├── AdbStream.java
│ │ │ └── LICENSE
│ │ │ ├── decoder
│ │ │ └── VideoDecoder.java
│ │ │ └── model
│ │ │ ├── ByteUtils.java
│ │ │ ├── MediaPacket.java
│ │ │ └── VideoPacket.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout-land
│ │ ├── surface_nav.xml
│ │ └── surface_no_nav.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── surface_nav.xml
│ │ └── surface_no_nav.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── org
│ └── las2mile
│ └── scrcpy
│ └── ExampleUnitTest.java
├── build.gradle
├── config
├── android-checkstyle.gradle
└── checkstyle
│ └── checkstyle.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── release
└── scrcpy-release.apk
├── server
├── .gitignore
├── LICENSE
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── aidl
│ └── android
│ │ └── view
│ │ └── IRotationWatcher.aidl
│ └── java
│ └── org
│ └── las2mile
│ └── scrcpy
│ ├── Device.java
│ ├── DisplayInfo.java
│ ├── DroidConnection.java
│ ├── EventController.java
│ ├── Ln.java
│ ├── Options.java
│ ├── Position.java
│ ├── ScreenEncoder.java
│ ├── ScreenInfo.java
│ ├── Server.java
│ ├── Size.java
│ ├── model
│ ├── ByteUtils.java
│ ├── MediaPacket.java
│ └── VideoPacket.java
│ └── wrappers
│ ├── DisplayManager.java
│ ├── InputManager.java
│ ├── PowerManager.java
│ ├── ServiceManager.java
│ ├── SurfaceControl.java
│ └── WindowManager.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/libraries
5 | /.idea/modules.xml
6 | /.idea/workspace.xml
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 |
--------------------------------------------------------------------------------
/.idea/caches/build_file_checksums.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/.idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | xmlns:android
11 |
12 | ^$
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | xmlns:.*
22 |
23 | ^$
24 |
25 |
26 | BY_NAME
27 |
28 |
29 |
30 |
31 |
32 |
33 | .*:id
34 |
35 | http://schemas.android.com/apk/res/android
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | .*:name
45 |
46 | http://schemas.android.com/apk/res/android
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | name
56 |
57 | ^$
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | style
67 |
68 | ^$
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | .*
78 |
79 | ^$
80 |
81 |
82 | BY_NAME
83 |
84 |
85 |
86 |
87 |
88 |
89 | .*
90 |
91 | http://schemas.android.com/apk/res/android
92 |
93 |
94 | ANDROID_ATTRIBUTE_ORDER
95 |
96 |
97 |
98 |
99 |
100 |
101 | .*
102 |
103 | .*
104 |
105 |
106 | BY_NAME
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scrcpy-android
2 |
3 | - This application is android port to desktop applicaton [**Scrcpy**](https://github.com/Genymobile/scrcpy).
4 |
5 | - This application mirrors display and touch controls from a target android device to scrcpy-android device.
6 |
7 | - scrcpy-android uses ADB-Connect interface to connect to android device to be mirrored.
8 |
9 |
10 | ## Download
11 |
12 | [scrcpy-release-v1.2.apk](https://gitlab.com/las2mile/scrcpy-android/raw/master/release/scrcpy-release.apk)
13 |
14 |
15 | ## Instructions to use
16 |
17 | - Make sure both devices are on same local network.
18 |
19 | - Enable **ADB-connect/ADB-wireless/ADB over network** on the device to be mirrored.
20 |
21 | - Open scrcpy app and enter ip address of device to be mirrored.
22 |
23 | - Select display parameters and bitrate from drop-down menu(1280x720 and 2Mbps works best).
24 |
25 | - Set **Navbar** switch if the device to be mirrored has only hardware navigation buttons.
26 |
27 | - Hit **start** button.
28 |
29 | - Accept and trust(check always allow from this computer) the ADB connection prompt on target device(Some custom roms don't have this prompt).
30 |
31 | - Thats all! You should be seeing the screen of target android device.
32 |
33 | - To wake up device, **double tap anywhere on screen**.
34 |
35 | - To put device to sleep, **close proxmity sensor and double tap anywhere on the screen**.
36 |
37 | - To bring back the local android system navbar while mirroring the remote device, **swipe up from the bottom edge of screen**.
38 |
39 |
40 | ## Building with Gradle
41 |
42 | ./gradlew assembleDebug
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | evaluationDependsOn(':server')
2 |
3 | apply plugin: 'com.android.application'
4 |
5 |
6 | android {
7 | compileSdkVersion 28
8 | defaultConfig {
9 | applicationId "org.las2mile.scrcpy"
10 | minSdkVersion 19
11 | targetSdkVersion 28
12 | versionCode 3
13 | versionName "1.2"
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 | setProperty("archivesBaseName", "scrcpy")
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation 'com.android.support.constraint:constraint-layout:1.1.2'
28 | testImplementation 'junit:junit:4.12'
29 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
30 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
31 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/libs/commons-codec-1.10-rep.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/libs/commons-codec-1.10-rep.jar
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/org/las2mile/scrcpy/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("org.las2mile.scrcpy", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/assets/scrcpy-server.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/assets/scrcpy-server.jar
--------------------------------------------------------------------------------
/app/src/main/assets/scrcpy-server_old.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/assets/scrcpy-server_old.jar
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/MainActivity.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.content.ComponentName;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.ServiceConnection;
9 | import android.content.pm.ActivityInfo;
10 | import android.content.res.AssetManager;
11 | import android.hardware.Sensor;
12 | import android.hardware.SensorEvent;
13 | import android.hardware.SensorEventListener;
14 | import android.hardware.SensorManager;
15 | import android.os.Bundle;
16 | import android.os.IBinder;
17 | import android.os.SystemClock;
18 | import android.util.Base64;
19 | import android.util.DisplayMetrics;
20 | import android.util.Log;
21 | import android.view.Display;
22 | import android.view.MotionEvent;
23 | import android.view.Surface;
24 | import android.view.SurfaceView;
25 | import android.view.View;
26 | import android.view.ViewConfiguration;
27 | import android.widget.AdapterView;
28 | import android.widget.ArrayAdapter;
29 | import android.widget.Button;
30 | import android.widget.EditText;
31 | import android.widget.Spinner;
32 | import android.widget.Switch;
33 | import android.widget.Toast;
34 |
35 | import java.io.IOException;
36 | import java.io.InputStream;
37 | import java.net.Inet4Address;
38 | import java.net.Inet6Address;
39 | import java.net.InetAddress;
40 | import java.net.NetworkInterface;
41 | import java.net.SocketException;
42 | import java.util.Enumeration;
43 |
44 |
45 | public class MainActivity extends Activity implements Scrcpy.ServiceCallbacks, SensorEventListener {
46 |
47 | // private static final String TAG = "MainActivity";
48 | private static final String PREFERENCE_KEY = "default";
49 | private static final String PREFERENCE_SPINNER_RESOLUTION = "spinner_resolution";
50 | private static final String PREFERENCE_SPINNER_BITRATE = "spinner_bitrate";
51 | private static int screenWidth;
52 | private static int screenHeight;
53 | private static boolean landscape = false;
54 | private static boolean first_time = true;
55 | private static boolean resultofRotation = false;
56 | private static boolean serviceBound = false;
57 | private static boolean nav = false;
58 | SensorManager sensorManager;
59 | private static SendCommands sendCommands;
60 | private int videoBitrate;
61 | private String localip;
62 | private Context context;
63 | private String serverAdr = null;
64 | private InputStream inputStream;
65 | private SurfaceView surfaceView;
66 | private Surface surface;
67 | private Scrcpy scrcpy;
68 | private long timestamp = 0;
69 | private byte[] fileBase64;
70 |
71 | private ServiceConnection serviceConnection = new ServiceConnection() {
72 | @Override
73 | public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
74 | scrcpy = ((Scrcpy.MyServiceBinder) iBinder).getService();
75 | scrcpy.setServiceCallbacks(MainActivity.this);
76 | if (first_time) {
77 | scrcpy.start(surface, serverAdr, screenHeight, screenWidth);
78 | } else {
79 | scrcpy.setParms(surface, screenWidth, screenHeight);
80 | }
81 | first_time = false;
82 | serviceBound = true;
83 | }
84 |
85 | @Override
86 | public void onServiceDisconnected(ComponentName componentName) {
87 | serviceBound = false;
88 | }
89 | };
90 |
91 | @Override
92 | protected void onCreate(Bundle savedInstanceState) {
93 | super.onCreate(savedInstanceState);
94 | if (first_time) {
95 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
96 | setContentView(R.layout.activity_main);
97 | final Button startButton = (Button) findViewById(R.id.button_start);
98 | AssetManager assetManager = getAssets();
99 | try {
100 | inputStream = assetManager.open("scrcpy-server.jar");
101 | byte[] buffer = new byte[inputStream.available()];
102 | inputStream.read(buffer);
103 | fileBase64 = Base64.encode(buffer, 2);
104 | } catch (IOException e) {
105 | Log.e("Asset Manager", e.getMessage());
106 | }
107 | sendCommands = new SendCommands();
108 | startButton.setOnClickListener(new View.OnClickListener() {
109 | @Override
110 | public void onClick(View v) {
111 | localip = wifiIpAddress();
112 | getAttributes();
113 | if (!serverAdr.isEmpty()) {
114 | if (sendCommands.SendAdbCommands(context, fileBase64, serverAdr, localip, videoBitrate, Math.max(screenHeight, screenWidth)) == 0) {
115 | if (nav) {
116 | startwithNav();
117 | } else {
118 | startwithoutNav();
119 | }
120 | } else {
121 | Toast.makeText(context, "Network OR ADB connection failed", Toast.LENGTH_SHORT).show();
122 | }
123 | } else {
124 | Toast.makeText(context, "Server Address Empty", Toast.LENGTH_SHORT).show();
125 | }
126 | }
127 | });
128 |
129 | this.context = this;
130 | final EditText editTextServerHost = (EditText) findViewById(R.id.editText_server_host);
131 | final Switch aSwitch = findViewById(R.id.switch1);
132 | editTextServerHost.setText(context.getSharedPreferences(PREFERENCE_KEY, 0).getString("Server Address", ""));
133 | aSwitch.setChecked(context.getSharedPreferences(PREFERENCE_KEY, 0).getBoolean("Nav Switch", false));
134 | setSpinner(R.array.options_resolution_keys, R.id.spinner_video_resolution, PREFERENCE_SPINNER_RESOLUTION);
135 | setSpinner(R.array.options_bitrate_keys, R.id.spinner_video_bitrate, PREFERENCE_SPINNER_BITRATE);
136 | } else {
137 | this.context = this;
138 | if (nav) {
139 | startwithNav();
140 | } else {
141 | startwithoutNav();
142 | }
143 |
144 | }
145 |
146 | sensorManager = (SensorManager) this.getSystemService(SENSOR_SERVICE);
147 | Sensor proximity;
148 | proximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
149 | sensorManager.registerListener(this, proximity, SensorManager.SENSOR_DELAY_NORMAL);
150 |
151 | }
152 |
153 | private void setSpinner(final int textArrayOptionResId, final int textViewResId, final String preferenceId) {
154 |
155 | final Spinner spinner = (Spinner) findViewById(textViewResId);
156 | ArrayAdapter arrayAdapter = ArrayAdapter.createFromResource(this, textArrayOptionResId, android.R.layout.simple_spinner_item);
157 | arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
158 | spinner.setAdapter(arrayAdapter);
159 |
160 | spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
161 | @Override
162 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
163 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putInt(preferenceId, position).apply();
164 | }
165 |
166 | @Override
167 | public void onNothingSelected(AdapterView> parent) {
168 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putInt(preferenceId, 0).apply();
169 | }
170 | });
171 | spinner.setSelection(context.getSharedPreferences(PREFERENCE_KEY, 0).getInt(preferenceId, 0));
172 | }
173 |
174 | private void getAttributes() {
175 |
176 | final EditText editTextServerHost = (EditText) findViewById(R.id.editText_server_host);
177 | serverAdr = editTextServerHost.getText().toString();
178 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putString("Server Address", serverAdr).apply();
179 | final Spinner videoResolutionSpinner = (Spinner) findViewById(R.id.spinner_video_resolution);
180 | final Spinner videoBitrateSpinner = (Spinner) findViewById(R.id.spinner_video_bitrate);
181 | final Switch aSwitch = findViewById(R.id.switch1);
182 | nav = aSwitch.isChecked();
183 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putBoolean("Nav Switch", nav).apply();
184 |
185 |
186 | final String[] videoResolutions = getResources().getStringArray(R.array.options_resolution_values)[videoResolutionSpinner.getSelectedItemPosition()].split(",");
187 | screenHeight = Integer.parseInt(videoResolutions[0]);
188 | screenWidth = Integer.parseInt(videoResolutions[1]);
189 | videoBitrate = getResources().getIntArray(R.array.options_bitrate_values)[videoBitrateSpinner.getSelectedItemPosition()];
190 |
191 | }
192 |
193 |
194 | private void swapDimensions() {
195 | int temp = screenHeight;
196 | screenHeight = screenWidth;
197 | screenWidth = temp;
198 | }
199 |
200 |
201 | @SuppressLint("ClickableViewAccessibility")
202 | private void startwithoutNav() {
203 | setContentView(R.layout.surface_no_nav);
204 | final View decorView = getWindow().getDecorView();
205 | decorView.setSystemUiVisibility(
206 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
207 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
208 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
209 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
210 | | View.SYSTEM_UI_FLAG_FULLSCREEN
211 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
212 |
213 | surfaceView = (SurfaceView) findViewById(R.id.decoder_surface);
214 | surface = surfaceView.getHolder().getSurface();
215 | startScrcpyservice();
216 | DisplayMetrics metrics = new DisplayMetrics();
217 |
218 | if (ViewConfiguration.get(context).hasPermanentMenuKey()) {
219 | getWindowManager().getDefaultDisplay().getMetrics(metrics);
220 |
221 |
222 | } else {
223 | final Display display = getWindowManager().getDefaultDisplay();
224 | display.getRealMetrics(metrics);
225 | }
226 | final int height = metrics.heightPixels;
227 | final int width = metrics.widthPixels;
228 |
229 |
230 | surfaceView.setOnTouchListener(new View.OnTouchListener() {
231 | @Override
232 | public boolean onTouch(View v, MotionEvent event) {
233 | return scrcpy.touchevent(event, width, height);
234 | }
235 | });
236 |
237 |
238 | }
239 |
240 | @SuppressLint("ClickableViewAccessibility")
241 | private void startwithNav() {
242 |
243 | setContentView(R.layout.surface_nav);
244 | final View decorView = getWindow().getDecorView();
245 | decorView.setSystemUiVisibility(
246 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
247 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
248 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
249 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
250 | | View.SYSTEM_UI_FLAG_FULLSCREEN
251 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
252 |
253 | final Button backButton = (Button) findViewById(R.id.back_button);
254 | final Button homeButton = (Button) findViewById(R.id.home_button);
255 | final Button appswitchButton = (Button) findViewById(R.id.appswitch_button);
256 |
257 | surfaceView = (SurfaceView) findViewById(R.id.decoder_surface);
258 | surface = surfaceView.getHolder().getSurface();
259 | startScrcpyservice();
260 | DisplayMetrics metrics = new DisplayMetrics();
261 | int offset = 0;
262 |
263 | if (ViewConfiguration.get(context).hasPermanentMenuKey()) {
264 | final Display display = getWindowManager().getDefaultDisplay();
265 | display.getRealMetrics(metrics);
266 | offset = 100;
267 |
268 | } else {
269 | getWindowManager().getDefaultDisplay().getMetrics(metrics);
270 | }
271 |
272 | final int height = metrics.heightPixels - offset;
273 | final int width = metrics.widthPixels;
274 | surfaceView.setOnTouchListener(new View.OnTouchListener() {
275 | @Override
276 | public boolean onTouch(View v, MotionEvent event) {
277 | return scrcpy.touchevent(event, width, height);
278 |
279 | }
280 | });
281 |
282 | backButton.setOnClickListener(new View.OnClickListener() {
283 | @Override
284 | public void onClick(View v) {
285 | scrcpy.sendKeyevent(4);
286 |
287 | }
288 | });
289 |
290 | homeButton.setOnClickListener(new View.OnClickListener() {
291 | @Override
292 | public void onClick(View v) {
293 | scrcpy.sendKeyevent(3);
294 |
295 | }
296 | });
297 |
298 | appswitchButton.setOnClickListener(new View.OnClickListener() {
299 | @Override
300 | public void onClick(View v) {
301 | scrcpy.sendKeyevent(187);
302 |
303 | }
304 | });
305 |
306 |
307 | }
308 |
309 |
310 | protected String wifiIpAddress() {
311 | //https://stackoverflow.com/questions/6064510/how-to-get-ip-address-of-the-device-from-code
312 | try {
313 | InetAddress ipv4 = null;
314 | InetAddress ipv6 = null;
315 |
316 | for (Enumeration en = NetworkInterface
317 | .getNetworkInterfaces(); en.hasMoreElements(); ) {
318 | NetworkInterface intf = en.nextElement();
319 | for (Enumeration enumIpAddr = intf
320 | .getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
321 | InetAddress inetAddress = enumIpAddr.nextElement();
322 | if (inetAddress instanceof Inet6Address) {
323 | ipv6 = inetAddress;
324 | continue;
325 | }
326 | if (inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
327 | ipv4 = inetAddress;
328 | continue;
329 | }
330 | return inetAddress.getHostAddress();
331 | }
332 | }
333 | if (ipv6 != null) {
334 | return ipv6.getHostAddress();
335 | }
336 | if (ipv4 != null) {
337 | return ipv4.getHostAddress();
338 | }
339 | return null;
340 |
341 | } catch (SocketException ex) {
342 | ex.printStackTrace();
343 | }
344 | return null;
345 |
346 | }
347 |
348 |
349 | private void startScrcpyservice() {
350 | Intent intent = new Intent(this, Scrcpy.class);
351 | startService(intent);
352 | bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
353 | }
354 |
355 |
356 | @Override
357 | public void loadNewRotation() {
358 | unbindService(serviceConnection);
359 | serviceBound = false;
360 | resultofRotation = true;
361 | landscape = !landscape;
362 | swapDimensions();
363 | if (landscape) {
364 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
365 | } else {
366 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
367 | }
368 |
369 | }
370 |
371 |
372 | @Override
373 | protected void onPause() {
374 | super.onPause();
375 | if (serviceBound) {
376 | scrcpy.pause();
377 | }
378 | }
379 |
380 |
381 | @Override
382 | protected void onResume() {
383 | super.onResume();
384 | if (!first_time && !resultofRotation) {
385 | final View decorView = getWindow().getDecorView();
386 | decorView.setSystemUiVisibility(
387 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
388 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
389 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
390 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
391 | | View.SYSTEM_UI_FLAG_FULLSCREEN
392 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
393 | if (serviceBound) {
394 | scrcpy.resume();
395 | }
396 | }
397 | resultofRotation = false;
398 | }
399 |
400 | @Override
401 | public void onBackPressed() {
402 | if (timestamp == 0) {
403 | timestamp = SystemClock.uptimeMillis();
404 | Toast.makeText(context, "Press again to exit", Toast.LENGTH_SHORT).show();
405 | } else {
406 | long now = SystemClock.uptimeMillis();
407 | if (now < timestamp + 1000) {
408 | timestamp = 0;
409 | if (serviceBound) {
410 | scrcpy.StopService();
411 | unbindService(serviceConnection);
412 | }
413 | android.os.Process.killProcess(android.os.Process.myPid());
414 | System.exit(1);
415 | }
416 | timestamp = 0;
417 | }
418 |
419 | }
420 |
421 |
422 | @Override
423 | public void onSensorChanged(SensorEvent sensorEvent) {
424 | if (sensorEvent.sensor.getType() == Sensor.TYPE_PROXIMITY) {
425 | if (sensorEvent.values[0] == 0) {
426 | if (serviceBound) {
427 | scrcpy.sendKeyevent(28);
428 | }
429 | } else {
430 | if (serviceBound) {
431 | scrcpy.sendKeyevent(29);
432 | }
433 | }
434 | }
435 | }
436 |
437 | @Override
438 | public void onAccuracyChanged(Sensor sensor, int i) {
439 |
440 | }
441 | }
442 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/Scrcpy.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.app.Service;
4 | import android.content.Intent;
5 | import android.os.Binder;
6 | import android.os.IBinder;
7 | import android.view.MotionEvent;
8 | import android.view.Surface;
9 |
10 | import org.las2mile.scrcpy.decoder.VideoDecoder;
11 | import org.las2mile.scrcpy.model.ByteUtils;
12 | import org.las2mile.scrcpy.model.MediaPacket;
13 | import org.las2mile.scrcpy.model.VideoPacket;
14 |
15 | import java.io.DataInputStream;
16 | import java.io.DataOutputStream;
17 | import java.io.IOException;
18 | import java.net.Socket;
19 | import java.util.concurrent.atomic.AtomicBoolean;
20 |
21 |
22 | public class Scrcpy extends Service {
23 |
24 | private String serverAdr;
25 | private Surface surface;
26 | private int screenWidth;
27 | private int screenHeight;
28 | private byte[] event = null;
29 | private VideoDecoder videoDecoder;
30 | private AtomicBoolean updateAvailable = new AtomicBoolean(false);
31 | private IBinder mBinder = new MyServiceBinder();
32 | private boolean first_time = true;
33 | private AtomicBoolean LetServceRunning = new AtomicBoolean(true);
34 | private ServiceCallbacks serviceCallbacks;
35 |
36 |
37 | @Override
38 | public IBinder onBind(Intent intent) {
39 | return mBinder;
40 | }
41 |
42 | public void setServiceCallbacks(ServiceCallbacks callbacks) {
43 | serviceCallbacks = callbacks;
44 | }
45 |
46 |
47 | public void setParms(Surface NewSurface, int NewWidth, int NewHeight) {
48 | this.screenWidth = NewWidth;
49 | this.screenHeight = NewHeight;
50 | this.surface = NewSurface;
51 | videoDecoder.start();
52 | updateAvailable.set(true);
53 |
54 | }
55 |
56 | public void start(Surface surface, String serverAdr, int screenHeight, int screenWidth) {
57 | this.videoDecoder = new VideoDecoder();
58 | videoDecoder.start();
59 | this.serverAdr = serverAdr;
60 | this.screenHeight = screenHeight;
61 | this.screenWidth = screenWidth;
62 | this.surface = surface;
63 | Thread thread = new Thread(new Runnable() {
64 | @Override
65 | public void run() {
66 | startConnection();
67 | }
68 | });
69 | thread.start();
70 | }
71 |
72 | public void pause() {
73 | videoDecoder.stop();
74 |
75 | }
76 |
77 | public void resume() {
78 | videoDecoder.start();
79 | updateAvailable.set(true);
80 | }
81 |
82 | public void StopService() {
83 | LetServceRunning.set(false);
84 | stopSelf();
85 | }
86 |
87 |
88 | public boolean touchevent(MotionEvent touch_event, int displayW, int displayH) {
89 |
90 | int[] buf = new int[]{touch_event.getAction(), touch_event.getButtonState(), (int) touch_event.getX() * screenWidth / displayW, (int) touch_event.getY() * screenHeight / displayH};
91 | final byte[] array = new byte[buf.length * 4]; // https://stackoverflow.com/questions/2183240/java-integer-to-byte-array
92 | for (int j = 0; j < buf.length; j++) {
93 | final int c = buf[j];
94 | array[j * 4] = (byte) ((c & 0xFF000000) >> 24);
95 | array[j * 4 + 1] = (byte) ((c & 0xFF0000) >> 16);
96 | array[j * 4 + 2] = (byte) ((c & 0xFF00) >> 8);
97 | array[j * 4 + 3] = (byte) (c & 0xFF);
98 | }
99 | event = array;
100 | return true;
101 | }
102 |
103 | public void sendKeyevent(int keycode) {
104 | int[] buf = new int[]{keycode};
105 |
106 | final byte[] array = new byte[buf.length * 4]; // https://stackoverflow.com/questions/2183240/java-integer-to-byte-array
107 | for (int j = 0; j < buf.length; j++) {
108 | final int c = buf[j];
109 | array[j * 4] = (byte) ((c & 0xFF000000) >> 24);
110 | array[j * 4 + 1] = (byte) ((c & 0xFF0000) >> 16);
111 | array[j * 4 + 2] = (byte) ((c & 0xFF00) >> 8);
112 | array[j * 4 + 3] = (byte) (c & 0xFF);
113 | }
114 | event = array;
115 | }
116 |
117 | private void startConnection() {
118 | videoDecoder = new VideoDecoder();
119 | videoDecoder.start();
120 | DataInputStream dataInputStream;
121 | DataOutputStream dataOutputStream;
122 | Socket socket = null;
123 | VideoPacket.StreamSettings streamSettings = null;
124 | int attempts = 50;
125 | while (attempts != 0) {
126 | try {
127 | socket = new Socket(serverAdr, 7007);
128 | dataInputStream = new DataInputStream(socket.getInputStream());
129 | dataOutputStream = new DataOutputStream(socket.getOutputStream());
130 |
131 | byte[] packetSize;
132 | attempts = 0;
133 | while (LetServceRunning.get()) {
134 | try {
135 | if (event != null) {
136 | dataOutputStream.write(event, 0, event.length);
137 | event = null;
138 | }
139 |
140 | if (dataInputStream.available() > 0) {
141 |
142 | packetSize = new byte[4];
143 | dataInputStream.readFully(packetSize, 0, 4);
144 |
145 | int size = ByteUtils.bytesToInt(packetSize);
146 | byte[] packet = new byte[size];
147 | dataInputStream.readFully(packet, 0, size);
148 | VideoPacket videoPacket = VideoPacket.fromArray(packet);
149 | if (videoPacket.type == MediaPacket.Type.VIDEO) {
150 | byte[] data = videoPacket.data;
151 | if (videoPacket.flag == VideoPacket.Flag.CONFIG || updateAvailable.get()) {
152 | if (!updateAvailable.get()) {
153 | streamSettings = VideoPacket.getStreamSettings(data);
154 | if (!first_time) {
155 | if (serviceCallbacks != null) {
156 | serviceCallbacks.loadNewRotation();
157 | }
158 | while (!updateAvailable.get()) {
159 | // Waiting for new surface
160 | try {
161 | Thread.sleep(100);
162 | } catch (InterruptedException e) {
163 | e.printStackTrace();
164 | }
165 |
166 | }
167 |
168 | }
169 | }
170 | updateAvailable.set(false);
171 | first_time = false;
172 | videoDecoder.configure(surface, screenWidth, screenHeight, streamSettings.sps, streamSettings.pps);
173 | } else if (videoPacket.flag == VideoPacket.Flag.END) {
174 | // need close stream
175 | } else {
176 | videoDecoder.decodeSample(data, 0, data.length, 0, videoPacket.flag.getFlag());
177 |
178 | }
179 | }
180 |
181 | }
182 |
183 |
184 | } catch (IOException e) {
185 | }
186 | }
187 |
188 |
189 | } catch (IOException e) {
190 | try {
191 | attempts = attempts - 1;
192 | Thread.sleep(100);
193 | } catch (InterruptedException ignore) {
194 | }
195 | // Log.e("Scrcpy", e.getMessage());
196 | } finally {
197 | if (socket != null) {
198 | try {
199 | socket.close();
200 | } catch (IOException e) {
201 | e.printStackTrace();
202 | }
203 | }
204 |
205 | }
206 |
207 |
208 | }
209 |
210 | }
211 |
212 | public interface ServiceCallbacks {
213 | void loadNewRotation();
214 | }
215 |
216 | public class MyServiceBinder extends Binder {
217 | public Scrcpy getService() {
218 | return Scrcpy.this;
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/SendCommands.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import org.las2mile.scrcpy.adblib.AdbBase64;
7 | import org.las2mile.scrcpy.adblib.AdbConnection;
8 | import org.las2mile.scrcpy.adblib.AdbCrypto;
9 | import org.las2mile.scrcpy.adblib.AdbStream;
10 |
11 | import java.io.FileInputStream;
12 | import java.io.FileOutputStream;
13 | import java.io.IOException;
14 | import java.net.ConnectException;
15 | import java.net.NoRouteToHostException;
16 | import java.net.Socket;
17 | import java.net.UnknownHostException;
18 | import java.security.NoSuchAlgorithmException;
19 | import java.security.spec.InvalidKeySpecException;
20 |
21 | import static android.org.apache.commons.codec.binary.Base64.encodeBase64String;
22 |
23 | //Uses code from https://github.com/Jolanrensen/ADBPlugin
24 |
25 |
26 | public class SendCommands {
27 |
28 | private Thread thread = null;
29 | private Context context;
30 | private int status;
31 |
32 |
33 | public SendCommands() {
34 |
35 | }
36 |
37 | public static AdbBase64 getBase64Impl() {
38 | return new AdbBase64() {
39 | @Override
40 | public String encodeToString(byte[] arg0) {
41 | return encodeBase64String(arg0);
42 | }
43 | };
44 | }
45 |
46 | private AdbCrypto setupCrypto()
47 | throws NoSuchAlgorithmException, IOException {
48 |
49 | AdbCrypto c = null;
50 | try {
51 | FileInputStream privIn = context.openFileInput("priv.key");
52 | FileInputStream pubIn = context.openFileInput("pub.key");
53 | c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), privIn, pubIn);
54 | } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException | NullPointerException e) {
55 | // Failed to read from file
56 | c = null;
57 | }
58 |
59 |
60 | if (c == null) {
61 | // We couldn't load a key, so let's generate a new one
62 | c = AdbCrypto.generateAdbKeyPair(getBase64Impl());
63 |
64 | // Save it
65 | FileOutputStream privOut = context.openFileOutput("priv.key", Context.MODE_PRIVATE);
66 | FileOutputStream pubOut = context.openFileOutput("pub.key", Context.MODE_PRIVATE);
67 |
68 | c.saveAdbKeyPair(privOut, pubOut);
69 | //Generated new keypair
70 | } else {
71 | //Loaded existing keypair
72 | }
73 |
74 | return c;
75 | }
76 |
77 |
78 | public int SendAdbCommands(Context context, final byte[] fileBase64, final String ip, String localip, int bitrate, int size) {
79 | this.context = context;
80 | status = 1;
81 | final StringBuilder command = new StringBuilder();
82 | command.append(" CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / org.las2mile.scrcpy.Server ");
83 | command.append(" /" + localip + " " + Long.toString(size) + " " + Long.toString(bitrate) + ";");
84 |
85 | thread = new Thread(new Runnable() {
86 | @Override
87 | public void run() {
88 | try {
89 | adbWrite(ip, fileBase64, command.toString());
90 | } catch (IOException e) {
91 | e.printStackTrace();
92 | }
93 | }
94 | });
95 | thread.start();
96 |
97 | while (status == 1) {
98 | Log.e("ADB", "Connecting...");
99 | for (int i = 0; i < 1000000; i++) {
100 | }
101 | }
102 | return status;
103 | }
104 |
105 |
106 | private void adbWrite(String ip, byte[] fileBase64, String command) throws IOException {
107 |
108 | AdbConnection adb = null;
109 | Socket sock = null;
110 | AdbCrypto crypto;
111 | AdbStream stream = null;
112 |
113 | try {
114 | crypto = setupCrypto();
115 | } catch (NoSuchAlgorithmException e) {
116 | e.printStackTrace();
117 | return;
118 | } catch (IOException e) {
119 | e.printStackTrace();
120 | throw new IOException("Couldn't read/write keys");
121 | }
122 |
123 | try {
124 | sock = new Socket(ip, 5555);
125 | } catch (UnknownHostException e) {
126 | status = 2;
127 | throw new UnknownHostException(ip + " is no valid ip address");
128 | } catch (ConnectException e) {
129 | status = 2;
130 | throw new ConnectException("Device at " + ip + ":" + 5555 + " has no adb enabled or connection is refused");
131 | } catch (NoRouteToHostException e) {
132 | status = 2;
133 | throw new NoRouteToHostException("Couldn't find adb device at " + ip + ":" + 5555);
134 | } catch (IOException e) {
135 | e.printStackTrace();
136 | status = 2;
137 | }
138 |
139 | if (sock != null) {
140 | try {
141 | adb = AdbConnection.create(sock, crypto);
142 | adb.connect();
143 | } catch (IllegalStateException e) {
144 | e.printStackTrace();
145 | } catch (IOException | InterruptedException e) {
146 | e.printStackTrace();
147 | return;
148 | }
149 | }
150 |
151 | if (adb != null) {
152 |
153 | try {
154 | stream = adb.open("shell:");
155 | } catch (IOException | InterruptedException e) {
156 | e.printStackTrace();
157 | return;
158 | }
159 | }
160 |
161 | if (stream != null) {
162 | try {
163 | stream.write("" + '\n');
164 | } catch (IOException | InterruptedException e) {
165 | e.printStackTrace();
166 | return;
167 | }
168 | }
169 |
170 |
171 | String responses = "";
172 | boolean done = false;
173 | while (!done && stream != null) {
174 | try {
175 | byte[] responseBytes = stream.read();
176 | String response = new String(responseBytes, "US-ASCII");
177 | if (response.substring(response.length() - 2).equals("$ ") ||
178 | response.substring(response.length() - 2).equals("# ")) {
179 | done = true;
180 | responses += response;
181 | break;
182 | } else {
183 | responses += response;
184 | }
185 | } catch (InterruptedException | IOException e) {
186 | e.printStackTrace();
187 | }
188 | }
189 |
190 |
191 | if (stream != null) {
192 | int len = fileBase64.length;
193 | byte[] filePart = new byte[4056];
194 | int sourceOffset = 0;
195 | try {
196 | stream.write(" cd data/local/tmp " + '\n');
197 | while (sourceOffset < len) {
198 | if (len - sourceOffset >= 4056) {
199 | System.arraycopy(fileBase64, sourceOffset, filePart, 0, 4056); //Writing in 4KB pieces. 4096-40 ---> 40 Bytes for actual command text.
200 | sourceOffset = sourceOffset + 4056;
201 | String ServerBase64part = new String(filePart, "US-ASCII");
202 | stream.write(" echo " + ServerBase64part + " >> serverBase64" + '\n');
203 | done = false;
204 | while (!done) {
205 | byte[] responseBytes = stream.read();
206 | String response = new String(responseBytes, "US-ASCII");
207 | if (response.endsWith("$ ") || response.endsWith("# ")) {
208 | done = true;
209 | }
210 | }
211 | } else {
212 | int rem = len - sourceOffset;
213 | byte[] remPart = new byte[rem];
214 | System.arraycopy(fileBase64, sourceOffset, remPart, 0, rem);
215 | sourceOffset = sourceOffset + rem;
216 | String ServerBase64part = new String(remPart, "US-ASCII");
217 | stream.write(" echo " + ServerBase64part + " >> serverBase64" + '\n');
218 | done = false;
219 | while (!done) {
220 | byte[] responseBytes = stream.read();
221 | String response = new String(responseBytes, "US-ASCII");
222 | if (response.endsWith("$ ") || response.endsWith("# ")) {
223 | done = true;
224 | }
225 | }
226 | }
227 | }
228 | stream.write(" base64 -d < serverBase64 > scrcpy-server.jar && rm serverBase64" + '\n');
229 | stream.write(command + '\n');
230 | } catch (IOException | InterruptedException e) {
231 | e.printStackTrace();
232 | return;
233 | }
234 | }
235 |
236 | status = 0;
237 |
238 | }
239 |
240 | }
241 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/adblib/AdbBase64.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.adblib;
2 |
3 | /**
4 | * This interface specifies the required functions for AdbCrypto to
5 | * perform Base64 encoding of its public key.
6 | *
7 | * @author Cameron Gutman
8 | */
9 | public interface AdbBase64 {
10 | /**
11 | * This function must encoded the specified data as a base 64 string, without
12 | * appending any extra newlines or other characters.
13 | *
14 | * @param data Data to encode
15 | * @return String containing base 64 encoded data
16 | */
17 | public String encodeToString(byte[] data);
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/adblib/AdbConnection.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.adblib;
2 |
3 |
4 | import java.io.Closeable;
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.io.OutputStream;
8 | import java.io.UnsupportedEncodingException;
9 | import java.net.ConnectException;
10 | import java.net.Socket;
11 | import java.util.HashMap;
12 |
13 | /**
14 | * This class represents an ADB connection.
15 | *
16 | * @author Cameron Gutman
17 | */
18 | public class AdbConnection implements Closeable {
19 |
20 | /**
21 | * The output stream that this class uses to read from
22 | * the socket.
23 | */
24 | OutputStream outputStream;
25 | /**
26 | * The underlying socket that this class uses to
27 | * communicate with the target device.
28 | */
29 | private Socket socket;
30 | /**
31 | * The last allocated local stream ID. The ID
32 | * chosen for the next stream will be this value + 1.
33 | */
34 | private int lastLocalId;
35 | /**
36 | * The input stream that this class uses to read from
37 | * the socket.
38 | */
39 | private InputStream inputStream;
40 | /**
41 | * The backend thread that handles responding to ADB packets.
42 | */
43 | private Thread connectionThread;
44 |
45 | /**
46 | * Specifies whether a connect has been attempted
47 | */
48 | private boolean connectAttempted;
49 |
50 | /**
51 | * Specifies whether a CNXN packet has been received from the peer.
52 | */
53 | private boolean connected;
54 |
55 | /**
56 | * Specifies the maximum amount data that can be sent to the remote peer.
57 | * This is only valid after connect() returns successfully.
58 | */
59 | private int maxData;
60 |
61 | /**
62 | * An initialized ADB crypto object that contains a key pair.
63 | */
64 | private AdbCrypto crypto;
65 |
66 | /**
67 | * Specifies whether this connection has already sent a signed token.
68 | */
69 | private boolean sentSignature;
70 |
71 | /**
72 | * A hash map of our open streams indexed by local ID.
73 | **/
74 | private HashMap openStreams;
75 |
76 | /**
77 | * Internal constructor to initialize some internal state
78 | */
79 | private AdbConnection() {
80 | openStreams = new HashMap();
81 | lastLocalId = 0;
82 | connectionThread = createConnectionThread();
83 | }
84 |
85 | /**
86 | * Creates a AdbConnection object associated with the socket and
87 | * crypto object specified.
88 | *
89 | * @param socket The socket that the connection will use for communcation.
90 | * @param crypto The crypto object that stores the key pair for authentication.
91 | * @return A new AdbConnection object.
92 | * @throws IOException If there is a socket error
93 | */
94 | public static AdbConnection create(Socket socket, AdbCrypto crypto) throws IOException {
95 | AdbConnection newConn = new AdbConnection();
96 |
97 | newConn.crypto = crypto;
98 |
99 | newConn.socket = socket;
100 | newConn.inputStream = socket.getInputStream();
101 | newConn.outputStream = socket.getOutputStream();
102 |
103 | /* Disable Nagle because we're sending tiny packets */
104 | socket.setTcpNoDelay(true);
105 |
106 | return newConn;
107 | }
108 |
109 | /**
110 | * Creates a new connection thread.
111 | *
112 | * @return A new connection thread.
113 | */
114 | private Thread createConnectionThread() {
115 | @SuppressWarnings("resource") final AdbConnection conn = this;
116 | return new Thread(new Runnable() {
117 | @Override
118 | public void run() {
119 | while (!connectionThread.isInterrupted()) {
120 | try {
121 | /* Read and parse a message off the socket's input stream */
122 | AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream);
123 |
124 | /* Verify magic and checksum */
125 | if (!AdbProtocol.validateMessage(msg))
126 | continue;
127 |
128 | switch (msg.command) {
129 | /* Stream-oriented commands */
130 | case AdbProtocol.CMD_OKAY:
131 | case AdbProtocol.CMD_WRTE:
132 | case AdbProtocol.CMD_CLSE:
133 | /* We must ignore all packets when not connected */
134 | if (!conn.connected)
135 | continue;
136 |
137 | /* Get the stream object corresponding to the packet */
138 | AdbStream waitingStream = openStreams.get(msg.arg1);
139 | if (waitingStream == null)
140 | continue;
141 |
142 | synchronized (waitingStream) {
143 | if (msg.command == AdbProtocol.CMD_OKAY) {
144 | /* We're ready for writes */
145 | waitingStream.updateRemoteId(msg.arg0);
146 | waitingStream.readyForWrite();
147 |
148 | /* Unwait an open/write */
149 | waitingStream.notify();
150 | } else if (msg.command == AdbProtocol.CMD_WRTE) {
151 | /* Got some data from our partner */
152 | waitingStream.addPayload(msg.payload);
153 |
154 | /* Tell it we're ready for more */
155 | waitingStream.sendReady();
156 | } else if (msg.command == AdbProtocol.CMD_CLSE) {
157 | /* He doesn't like us anymore :-( */
158 | conn.openStreams.remove(msg.arg1);
159 |
160 | /* Notify readers and writers */
161 | waitingStream.notifyClose();
162 | }
163 | }
164 |
165 | break;
166 |
167 | case AdbProtocol.CMD_AUTH:
168 |
169 | byte[] packet;
170 |
171 | if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN) {
172 | /* This is an authentication challenge */
173 | if (conn.sentSignature) {
174 | /* We've already tried our signature, so send our public key */
175 | packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC,
176 | conn.crypto.getAdbPublicKeyPayload());
177 | } else {
178 | /* We'll sign the token */
179 | packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE,
180 | conn.crypto.signAdbTokenPayload(msg.payload));
181 | conn.sentSignature = true;
182 | }
183 |
184 | /* Write the AUTH reply */
185 | conn.outputStream.write(packet);
186 | conn.outputStream.flush();
187 | }
188 | break;
189 |
190 | case AdbProtocol.CMD_CNXN:
191 | synchronized (conn) {
192 | /* We need to store the max data size */
193 | conn.maxData = msg.arg1;
194 |
195 | /* Mark us as connected and unwait anyone waiting on the connection */
196 | conn.connected = true;
197 | conn.notifyAll();
198 | }
199 | break;
200 |
201 | default:
202 | /* Unrecognized packet, just drop it */
203 | break;
204 | }
205 | } catch (Exception e) {
206 | /* The cleanup is taken care of by a combination of this thread
207 | * and close() */
208 | break;
209 | }
210 | }
211 |
212 | /* This thread takes care of cleaning up pending streams */
213 | synchronized (conn) {
214 | cleanupStreams();
215 | conn.notifyAll();
216 | conn.connectAttempted = false;
217 | }
218 | }
219 | });
220 | }
221 |
222 | /**
223 | * Gets the max data size that the remote client supports.
224 | * A connection must have been attempted before calling this routine.
225 | * This routine will block if a connection is in progress.
226 | *
227 | * @return The maximum data size indicated in the connect packet.
228 | * @throws InterruptedException If a connection cannot be waited on.
229 | * @throws IOException if the connection fails
230 | */
231 | public int getMaxData() throws InterruptedException, IOException {
232 | if (!connectAttempted)
233 | throw new IllegalStateException("connect() must be called first");
234 |
235 | synchronized (this) {
236 | /* Block if a connection is pending, but not yet complete */
237 | if (!connected)
238 | wait();
239 |
240 | if (!connected) {
241 | throw new IOException("Connection failed");
242 | }
243 | }
244 |
245 | return maxData;
246 | }
247 |
248 | /**
249 | * Connects to the remote device. This routine will block until the connection
250 | * completes.
251 | *
252 | * @throws IOException If the socket fails while connecting
253 | * @throws InterruptedException If we are unable to wait for the connection to finish
254 | */
255 | public void connect() throws IOException, InterruptedException {
256 | if (connected)
257 | throw new IllegalStateException("Already connected");
258 |
259 | /* Write the CONNECT packet */
260 | outputStream.write(AdbProtocol.generateConnect());
261 | outputStream.flush();
262 |
263 | /* Start the connection thread to respond to the peer */
264 | connectAttempted = true;
265 | connectionThread.start();
266 |
267 | /* Wait for the connection to go live */
268 | synchronized (this) {
269 | if (!connected)
270 | wait();
271 |
272 | if (!connected) {
273 | throw new IOException("Connection failed");
274 | }
275 | }
276 | }
277 |
278 | /**
279 | * Opens an AdbStream object corresponding to the specified destination.
280 | * This routine will block until the connection completes.
281 | *
282 | * @param destination The destination to open on the target
283 | * @return AdbStream object corresponding to the specified destination
284 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
285 | * @throws IOException If the stream fails while sending the packet
286 | * @throws InterruptedException If we are unable to wait for the connection to finish
287 | */
288 | public AdbStream open(String destination) throws UnsupportedEncodingException, IOException, InterruptedException {
289 | int localId = ++lastLocalId;
290 |
291 | if (!connectAttempted)
292 | throw new IllegalStateException("connect() must be called first");
293 |
294 | /* Wait for the connect response */
295 | synchronized (this) {
296 | if (!connected)
297 | wait();
298 |
299 | if (!connected) {
300 | throw new IOException("Connection failed");
301 | }
302 | }
303 |
304 | /* Add this stream to this list of half-open streams */
305 | AdbStream stream = new AdbStream(this, localId);
306 | openStreams.put(localId, stream);
307 |
308 | /* Send the open */
309 | outputStream.write(AdbProtocol.generateOpen(localId, destination));
310 | outputStream.flush();
311 |
312 | /* Wait for the connection thread to receive the OKAY */
313 | synchronized (stream) {
314 | stream.wait();
315 | }
316 |
317 | /* Check if the open was rejected */
318 | if (stream.isClosed())
319 | throw new ConnectException("Stream open actively rejected by remote peer");
320 |
321 | /* We're fully setup now */
322 | return stream;
323 | }
324 |
325 | /**
326 | * This function terminates all I/O on streams associated with this ADB connection
327 | */
328 | private void cleanupStreams() {
329 | /* Close all streams on this connection */
330 | for (AdbStream s : openStreams.values()) {
331 | /* We handle exceptions for each close() call to avoid
332 | * terminating cleanup for one failed close(). */
333 | try {
334 | s.close();
335 | } catch (IOException e) {
336 | }
337 | }
338 |
339 | /* No open streams anymore */
340 | openStreams.clear();
341 | }
342 |
343 | /**
344 | * This routine closes the Adb connection and underlying socket
345 | *
346 | * @throws IOException if the socket fails to close
347 | */
348 | @Override
349 | public void close() throws IOException {
350 | /* If the connection thread hasn't spawned yet, there's nothing to do */
351 | if (connectionThread == null)
352 | return;
353 |
354 | /* Closing the socket will kick the connection thread */
355 | socket.close();
356 |
357 | /* Wait for the connection thread to die */
358 | connectionThread.interrupt();
359 | try {
360 | connectionThread.join();
361 | } catch (InterruptedException e) {
362 | }
363 | }
364 | }
365 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/adblib/AdbCrypto.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.adblib;
2 |
3 | import java.io.FileInputStream;
4 | import java.io.FileOutputStream;
5 | import java.io.IOException;
6 | import java.math.BigInteger;
7 | import java.nio.ByteBuffer;
8 | import java.nio.ByteOrder;
9 | import java.security.GeneralSecurityException;
10 | import java.security.KeyFactory;
11 | import java.security.KeyPair;
12 | import java.security.KeyPairGenerator;
13 | import java.security.NoSuchAlgorithmException;
14 | import java.security.interfaces.RSAPublicKey;
15 | import java.security.spec.EncodedKeySpec;
16 | import java.security.spec.InvalidKeySpecException;
17 | import java.security.spec.PKCS8EncodedKeySpec;
18 | import java.security.spec.X509EncodedKeySpec;
19 |
20 | import javax.crypto.Cipher;
21 |
22 | /**
23 | * This class encapsulates the ADB cryptography functions and provides
24 | * an interface for the storage and retrieval of keys.
25 | *
26 | * @author Cameron Gutman
27 | */
28 | public class AdbCrypto {
29 |
30 | /**
31 | * The ADB RSA key length in bits
32 | */
33 | public static final int KEY_LENGTH_BITS = 2048;
34 | /**
35 | * The ADB RSA key length in bytes
36 | */
37 | public static final int KEY_LENGTH_BYTES = KEY_LENGTH_BITS / 8;
38 | /**
39 | * The ADB RSA key length in words
40 | */
41 | public static final int KEY_LENGTH_WORDS = KEY_LENGTH_BYTES / 4;
42 | /**
43 | * The RSA signature padding as an int array
44 | */
45 | public static final int[] SIGNATURE_PADDING_AS_INT = new int[]
46 | {
47 | 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
48 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
49 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
50 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
51 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
52 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
53 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
54 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
55 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
56 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
57 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
58 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
59 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
60 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
61 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
62 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
63 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
64 | 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00,
65 | 0x04, 0x14
66 | };
67 | /**
68 | * The RSA signature padding as a byte array
69 | */
70 | public static byte[] SIGNATURE_PADDING;
71 |
72 | static {
73 | SIGNATURE_PADDING = new byte[SIGNATURE_PADDING_AS_INT.length];
74 |
75 | for (int i = 0; i < SIGNATURE_PADDING.length; i++)
76 | SIGNATURE_PADDING[i] = (byte) SIGNATURE_PADDING_AS_INT[i];
77 | }
78 |
79 | /**
80 | * An RSA keypair encapsulated by the AdbCrypto object
81 | */
82 | private KeyPair keyPair;
83 | /**
84 | * The base 64 conversion interface to use
85 | */
86 | private AdbBase64 base64;
87 |
88 | /**
89 | * Converts a standard RSAPublicKey object to the special ADB format
90 | *
91 | * @param pubkey RSAPublicKey object to convert
92 | * @return Byte array containing the converted RSAPublicKey object
93 | */
94 | private static byte[] convertRsaPublicKeyToAdbFormat(RSAPublicKey pubkey) {
95 | /*
96 | * ADB literally just saves the RSAPublicKey struct to a file.
97 | *
98 | * typedef struct RSAPublicKey {
99 | * int len; // Length of n[] in number of uint32_t
100 | * uint32_t n0inv; // -1 / n[0] mod 2^32
101 | * uint32_t n[RSANUMWORDS]; // modulus as little endian array
102 | * uint32_t rr[RSANUMWORDS]; // R^2 as little endian array
103 | * int exponent; // 3 or 65537
104 | * } RSAPublicKey;
105 | */
106 |
107 | /* ------ This part is a Java-ified version of RSA_to_RSAPublicKey from adb_host_auth.c ------ */
108 | BigInteger r32, r, rr, rem, n, n0inv;
109 |
110 | r32 = BigInteger.ZERO.setBit(32);
111 | n = pubkey.getModulus();
112 | r = BigInteger.ZERO.setBit(KEY_LENGTH_WORDS * 32);
113 | rr = r.modPow(BigInteger.valueOf(2), n);
114 | rem = n.remainder(r32);
115 | n0inv = rem.modInverse(r32);
116 |
117 | int myN[] = new int[KEY_LENGTH_WORDS];
118 | int myRr[] = new int[KEY_LENGTH_WORDS];
119 | BigInteger res[];
120 | for (int i = 0; i < KEY_LENGTH_WORDS; i++) {
121 | res = rr.divideAndRemainder(r32);
122 | rr = res[0];
123 | rem = res[1];
124 | myRr[i] = rem.intValue();
125 |
126 | res = n.divideAndRemainder(r32);
127 | n = res[0];
128 | rem = res[1];
129 | myN[i] = rem.intValue();
130 | }
131 |
132 | /* ------------------------------------------------------------------------------------------- */
133 |
134 | ByteBuffer bbuf = ByteBuffer.allocate(524).order(ByteOrder.LITTLE_ENDIAN);
135 |
136 |
137 | bbuf.putInt(KEY_LENGTH_WORDS);
138 | bbuf.putInt(n0inv.negate().intValue());
139 | for (int i : myN)
140 | bbuf.putInt(i);
141 | for (int i : myRr)
142 | bbuf.putInt(i);
143 |
144 | bbuf.putInt(pubkey.getPublicExponent().intValue());
145 | return bbuf.array();
146 | }
147 |
148 |
149 | public static AdbCrypto loadAdbKeyPair(AdbBase64 base64, FileInputStream privIn, FileInputStream pubIn) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
150 | AdbCrypto crypto = new AdbCrypto();
151 |
152 | byte[] privKeyBytes = new byte[privIn.available()];
153 | byte[] pubKeyBytes = new byte[pubIn.available()];
154 |
155 | privIn.read(privKeyBytes);
156 | pubIn.read(pubKeyBytes);
157 |
158 | privIn.close();
159 | pubIn.close();
160 |
161 | KeyFactory keyFactory = KeyFactory.getInstance("RSA");
162 | EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
163 | EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pubKeyBytes);
164 |
165 | crypto.keyPair = new KeyPair(keyFactory.generatePublic(publicKeySpec),
166 | keyFactory.generatePrivate(privateKeySpec));
167 | crypto.base64 = base64;
168 |
169 | return crypto;
170 | }
171 |
172 |
173 | /**
174 | * Creates a new AdbCrypto object by generating a new key pair.
175 | *
176 | * @param base64 Implementation of base 64 conversion interface required by ADB
177 | * @return A new AdbCrypto object
178 | * @throws NoSuchAlgorithmException If an RSA key factory cannot be found
179 | */
180 | public static AdbCrypto generateAdbKeyPair(AdbBase64 base64) throws NoSuchAlgorithmException {
181 | AdbCrypto crypto = new AdbCrypto();
182 |
183 | KeyPairGenerator rsaKeyPg = KeyPairGenerator.getInstance("RSA");
184 | rsaKeyPg.initialize(KEY_LENGTH_BITS);
185 |
186 | crypto.keyPair = rsaKeyPg.genKeyPair();
187 | crypto.base64 = base64;
188 |
189 | return crypto;
190 | }
191 |
192 | /**
193 | * Signs the ADB SHA1 payload with the private key of this object.
194 | *
195 | * @param payload SHA1 payload to sign
196 | * @return Signed SHA1 payload
197 | * @throws GeneralSecurityException If signing fails
198 | */
199 | public byte[] signAdbTokenPayload(byte[] payload) throws GeneralSecurityException {
200 | Cipher c = Cipher.getInstance("RSA/ECB/NoPadding");
201 |
202 | c.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
203 |
204 | c.update(SIGNATURE_PADDING);
205 |
206 | return c.doFinal(payload);
207 | }
208 |
209 | /**
210 | * Gets the RSA public key in ADB format.
211 | *
212 | * @return Byte array containing the RSA public key in ADB format.
213 | * @throws IOException If the key cannot be retrived
214 | */
215 | public byte[] getAdbPublicKeyPayload() throws IOException {
216 | byte[] convertedKey = convertRsaPublicKeyToAdbFormat((RSAPublicKey) keyPair.getPublic());
217 | StringBuilder keyString = new StringBuilder(720);
218 |
219 | /* The key is base64 encoded with a user@host suffix and terminated with a NUL */
220 | keyString.append(base64.encodeToString(convertedKey));
221 | keyString.append(" unknown@unknown");
222 | keyString.append('\0');
223 |
224 | return keyString.toString().getBytes("UTF-8");
225 | }
226 |
227 |
228 | public void saveAdbKeyPair(FileOutputStream privOut, FileOutputStream pubOut) throws IOException {
229 |
230 | privOut.write(keyPair.getPrivate().getEncoded());
231 | pubOut.write(keyPair.getPublic().getEncoded());
232 |
233 | privOut.close();
234 | pubOut.close();
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/adblib/AdbProtocol.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.adblib;
2 |
3 |
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.io.UnsupportedEncodingException;
7 | import java.nio.ByteBuffer;
8 | import java.nio.ByteOrder;
9 |
10 |
11 | /**
12 | * This class provides useful functions and fields for ADB protocol details.
13 | *
14 | * @author Cameron Gutman
15 | */
16 | public class AdbProtocol {
17 |
18 | /**
19 | * The length of the ADB message header
20 | */
21 | public static final int ADB_HEADER_LENGTH = 24;
22 |
23 | public static final int CMD_SYNC = 0x434e5953;
24 |
25 | /**
26 | * CNXN is the connect message. No messages (except AUTH)
27 | * are valid before this message is received.
28 | */
29 | public static final int CMD_CNXN = 0x4e584e43;
30 |
31 | /**
32 | * The current version of the ADB protocol
33 | */
34 | public static final int CONNECT_VERSION = 0x01000000;
35 |
36 | /**
37 | * The maximum data payload supported by the ADB implementation
38 | */
39 | public static final int CONNECT_MAXDATA = 4096;
40 | /**
41 | * AUTH is the authentication message. It is part of the
42 | * RSA public key authentication added in Android 4.2.2.
43 | */
44 | public static final int CMD_AUTH = 0x48545541;
45 | /**
46 | * This authentication type represents a SHA1 hash to sign
47 | */
48 | public static final int AUTH_TYPE_TOKEN = 1;
49 | /**
50 | * This authentication type represents the signed SHA1 hash
51 | */
52 | public static final int AUTH_TYPE_SIGNATURE = 2;
53 | /**
54 | * This authentication type represents a RSA public key
55 | */
56 | public static final int AUTH_TYPE_RSA_PUBLIC = 3;
57 | /**
58 | * OPEN is the open stream message. It is sent to open
59 | * a new stream on the target device.
60 | */
61 | public static final int CMD_OPEN = 0x4e45504f;
62 | /**
63 | * OKAY is a success message. It is sent when a write is
64 | * processed successfully.
65 | */
66 | public static final int CMD_OKAY = 0x59414b4f;
67 | /**
68 | * CLSE is the close stream message. It it sent to close an
69 | * existing stream on the target device.
70 | */
71 | public static final int CMD_CLSE = 0x45534c43;
72 | /**
73 | * WRTE is the write stream message. It is sent with a payload
74 | * that is the data to write to the stream.
75 | */
76 | public static final int CMD_WRTE = 0x45545257;
77 | /**
78 | * The payload sent with the connect message
79 | */
80 | public static byte[] CONNECT_PAYLOAD;
81 |
82 | static {
83 | try {
84 | CONNECT_PAYLOAD = "host::\0".getBytes("UTF-8");
85 | } catch (UnsupportedEncodingException e) {
86 | }
87 | }
88 |
89 | /**
90 | * This function performs a checksum on the ADB payload data.
91 | *
92 | * @param payload Payload to checksum
93 | * @return The checksum of the payload
94 | */
95 | private static int getPayloadChecksum(byte[] payload) {
96 | int checksum = 0;
97 |
98 | for (byte b : payload) {
99 | /* We have to manually "unsign" these bytes because Java sucks */
100 | if (b >= 0)
101 | checksum += b;
102 | else
103 | checksum += b + 256;
104 | }
105 |
106 | return checksum;
107 | }
108 |
109 | /**
110 | * This function validate the ADB message by checking
111 | * its command, magic, and payload checksum.
112 | *
113 | * @param msg ADB message to validate
114 | * @return True if the message was valid, false otherwise
115 | */
116 | public static boolean validateMessage(AdbMessage msg) {
117 | /* Magic is cmd ^ 0xFFFFFFFF */
118 | if (msg.command != (msg.magic ^ 0xFFFFFFFF))
119 | return false;
120 |
121 | if (msg.payloadLength != 0) {
122 | if (getPayloadChecksum(msg.payload) != msg.checksum)
123 | return false;
124 | }
125 |
126 | return true;
127 | }
128 |
129 | /**
130 | * This function generates an ADB message given the fields.
131 | *
132 | * @param cmd Command identifier
133 | * @param arg0 First argument
134 | * @param arg1 Second argument
135 | * @param payload Data payload
136 | * @return Byte array containing the message
137 | */
138 | public static byte[] generateMessage(int cmd, int arg0, int arg1, byte[] payload) {
139 | /* struct message {
140 | * unsigned command; // command identifier constant
141 | * unsigned arg0; // first argument
142 | * unsigned arg1; // second argument
143 | * unsigned data_length; // length of payload (0 is allowed)
144 | * unsigned data_check; // checksum of data payload
145 | * unsigned magic; // command ^ 0xffffffff
146 | * };
147 | */
148 |
149 | ByteBuffer message;
150 |
151 | if (payload != null) {
152 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH + payload.length).order(ByteOrder.LITTLE_ENDIAN);
153 | } else {
154 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
155 | }
156 |
157 | message.putInt(cmd);
158 | message.putInt(arg0);
159 | message.putInt(arg1);
160 |
161 | if (payload != null) {
162 | message.putInt(payload.length);
163 | message.putInt(getPayloadChecksum(payload));
164 | } else {
165 | message.putInt(0);
166 | message.putInt(0);
167 | }
168 |
169 | message.putInt(cmd ^ 0xFFFFFFFF);
170 |
171 | if (payload != null) {
172 | message.put(payload);
173 | }
174 |
175 | return message.array();
176 | }
177 |
178 | /**
179 | * Generates a connect message with default parameters.
180 | *
181 | * @return Byte array containing the message
182 | */
183 | public static byte[] generateConnect() {
184 | return generateMessage(CMD_CNXN, CONNECT_VERSION, CONNECT_MAXDATA, CONNECT_PAYLOAD);
185 | }
186 |
187 | /**
188 | * Generates an auth message with the specified type and payload.
189 | *
190 | * @param type Authentication type (see AUTH_TYPE_* constants)
191 | * @param data The payload for the message
192 | * @return Byte array containing the message
193 | */
194 | public static byte[] generateAuth(int type, byte[] data) {
195 | return generateMessage(CMD_AUTH, type, 0, data);
196 | }
197 |
198 | /**
199 | * Generates an open stream message with the specified local ID and destination.
200 | *
201 | * @param localId A unique local ID identifying the stream
202 | * @param dest The destination of the stream on the target
203 | * @return Byte array containing the message
204 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8
205 | */
206 | public static byte[] generateOpen(int localId, String dest) throws UnsupportedEncodingException {
207 | ByteBuffer bbuf = ByteBuffer.allocate(dest.length() + 1);
208 | bbuf.put(dest.getBytes("UTF-8"));
209 | bbuf.put((byte) 0);
210 | return generateMessage(CMD_OPEN, localId, 0, bbuf.array());
211 | }
212 |
213 | /**
214 | * Generates a write stream message with the specified IDs and payload.
215 | *
216 | * @param localId The unique local ID of the stream
217 | * @param remoteId The unique remote ID of the stream
218 | * @param data The data to provide as the write payload
219 | * @return Byte array containing the message
220 | */
221 | public static byte[] generateWrite(int localId, int remoteId, byte[] data) {
222 | return generateMessage(CMD_WRTE, localId, remoteId, data);
223 | }
224 |
225 | /**
226 | * Generates a close stream message with the specified IDs.
227 | *
228 | * @param localId The unique local ID of the stream
229 | * @param remoteId The unique remote ID of the stream
230 | * @return Byte array containing the message
231 | */
232 | public static byte[] generateClose(int localId, int remoteId) {
233 | return generateMessage(CMD_CLSE, localId, remoteId, null);
234 | }
235 |
236 | /**
237 | * Generates an okay message with the specified IDs.
238 | *
239 | * @param localId The unique local ID of the stream
240 | * @param remoteId The unique remote ID of the stream
241 | * @return Byte array containing the message
242 | */
243 | public static byte[] generateReady(int localId, int remoteId) {
244 | return generateMessage(CMD_OKAY, localId, remoteId, null);
245 | }
246 |
247 | /**
248 | * This class provides an abstraction for the ADB message format.
249 | *
250 | * @author Cameron Gutman
251 | */
252 | final static class AdbMessage {
253 | /**
254 | * The command field of the message
255 | */
256 | public int command;
257 | /**
258 | * The arg0 field of the message
259 | */
260 | public int arg0;
261 | /**
262 | * The arg1 field of the message
263 | */
264 | public int arg1;
265 | /**
266 | * The payload length field of the message
267 | */
268 | public int payloadLength;
269 | /**
270 | * The checksum field of the message
271 | */
272 | public int checksum;
273 | /**
274 | * The magic field of the message
275 | */
276 | public int magic;
277 | /**
278 | * The payload of the message
279 | */
280 | public byte[] payload;
281 |
282 | /**
283 | * Read and parse an ADB message from the supplied input stream.
284 | * This message is NOT validated.
285 | *
286 | * @param in InputStream object to read data from
287 | * @return An AdbMessage object represented the message read
288 | * @throws IOException If the stream fails while reading
289 | */
290 | public static AdbMessage parseAdbMessage(InputStream in) throws IOException {
291 | AdbMessage msg = new AdbMessage();
292 | ByteBuffer packet = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
293 |
294 | /* Read the header first */
295 | int dataRead = 0;
296 | do {
297 | int bytesRead = in.read(packet.array(), dataRead, 24 - dataRead);
298 |
299 | if (bytesRead < 0)
300 | throw new IOException("Stream closed");
301 | else
302 | dataRead += bytesRead;
303 | }
304 | while (dataRead < ADB_HEADER_LENGTH);
305 |
306 | /* Pull out header fields */
307 | msg.command = packet.getInt();
308 | msg.arg0 = packet.getInt();
309 | msg.arg1 = packet.getInt();
310 | msg.payloadLength = packet.getInt();
311 | msg.checksum = packet.getInt();
312 | msg.magic = packet.getInt();
313 |
314 | /* If there's a payload supplied, read that too */
315 | if (msg.payloadLength != 0) {
316 | msg.payload = new byte[msg.payloadLength];
317 |
318 | dataRead = 0;
319 | do {
320 | int bytesRead = in.read(msg.payload, dataRead, msg.payloadLength - dataRead);
321 |
322 | if (bytesRead < 0)
323 | throw new IOException("Stream closed");
324 | else
325 | dataRead += bytesRead;
326 | }
327 | while (dataRead < msg.payloadLength);
328 | }
329 |
330 | return msg;
331 | }
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/adblib/AdbStream.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.adblib;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 | import java.util.Queue;
6 | import java.util.concurrent.ConcurrentLinkedQueue;
7 | import java.util.concurrent.atomic.AtomicBoolean;
8 |
9 | /**
10 | * This class abstracts the underlying ADB streams
11 | *
12 | * @author Cameron Gutman
13 | */
14 | public class AdbStream implements Closeable {
15 |
16 | /**
17 | * The AdbConnection object that the stream communicates over
18 | */
19 | private AdbConnection adbConn;
20 |
21 | /**
22 | * The local ID of the stream
23 | */
24 | private int localId;
25 |
26 | /**
27 | * The remote ID of the stream
28 | */
29 | private int remoteId;
30 |
31 | /**
32 | * Indicates whether a write is currently allowed
33 | */
34 | private AtomicBoolean writeReady;
35 |
36 | /**
37 | * A queue of data from the target's write packets
38 | */
39 | private Queue readQueue;
40 |
41 | /**
42 | * Indicates whether the connection is closed already
43 | */
44 | private boolean isClosed;
45 |
46 | /**
47 | * Creates a new AdbStream object on the specified AdbConnection
48 | * with the given local ID.
49 | *
50 | * @param adbConn AdbConnection that this stream is running on
51 | * @param localId Local ID of the stream
52 | */
53 | public AdbStream(AdbConnection adbConn, int localId) {
54 | this.adbConn = adbConn;
55 | this.localId = localId;
56 | this.readQueue = new ConcurrentLinkedQueue();
57 | this.writeReady = new AtomicBoolean(false);
58 | this.isClosed = false;
59 | }
60 |
61 | /**
62 | * Called by the connection thread to indicate newly received data.
63 | *
64 | * @param payload Data inside the write message
65 | */
66 | void addPayload(byte[] payload) {
67 | synchronized (readQueue) {
68 | readQueue.add(payload);
69 | readQueue.notifyAll();
70 | }
71 | }
72 |
73 | /**
74 | * Called by the connection thread to send an OKAY packet, allowing the
75 | * other side to continue transmission.
76 | *
77 | * @throws IOException If the connection fails while sending the packet
78 | */
79 | void sendReady() throws IOException {
80 | /* Generate and send a READY packet */
81 | byte[] packet = AdbProtocol.generateReady(localId, remoteId);
82 | adbConn.outputStream.write(packet);
83 | adbConn.outputStream.flush();
84 | }
85 |
86 | /**
87 | * Called by the connection thread to update the remote ID for this stream
88 | *
89 | * @param remoteId New remote ID
90 | */
91 | void updateRemoteId(int remoteId) {
92 | this.remoteId = remoteId;
93 | }
94 |
95 | /**
96 | * Called by the connection thread to indicate the stream is okay to send data.
97 | */
98 | void readyForWrite() {
99 | writeReady.set(true);
100 | }
101 |
102 | /**
103 | * Called by the connection thread to notify that the stream was closed by the peer.
104 | */
105 | void notifyClose() {
106 | /* We don't call close() because it sends another CLOSE */
107 | isClosed = true;
108 |
109 | /* Unwait readers and writers */
110 | synchronized (this) {
111 | notifyAll();
112 | }
113 | synchronized (readQueue) {
114 | readQueue.notifyAll();
115 | }
116 | }
117 |
118 | /**
119 | * Reads a pending write payload from the other side.
120 | *
121 | * @return Byte array containing the payload of the write
122 | * @throws InterruptedException If we are unable to wait for data
123 | * @throws IOException If the stream fails while waiting
124 | */
125 | public byte[] read() throws InterruptedException, IOException {
126 | byte[] data = null;
127 |
128 | synchronized (readQueue) {
129 | /* Wait for the connection to close or data to be received */
130 | while (!isClosed && (data = readQueue.poll()) == null) {
131 | readQueue.wait();
132 | }
133 |
134 | if (isClosed) {
135 | throw new IOException("Stream closed");
136 | }
137 | }
138 |
139 | return data;
140 | }
141 |
142 | /**
143 | * Sends a write packet with a given String payload.
144 | *
145 | * @param payload Payload in the form of a String
146 | * @throws IOException If the stream fails while sending data
147 | * @throws InterruptedException If we are unable to wait to send data
148 | */
149 | public void write(String payload) throws IOException, InterruptedException {
150 | /* ADB needs null-terminated strings */
151 | write(payload.getBytes("UTF-8"), false);
152 | write(new byte[]{0}, true);
153 | }
154 |
155 | /**
156 | * Sends a write packet with a given byte array payload.
157 | *
158 | * @param payload Payload in the form of a byte array
159 | * @throws IOException If the stream fails while sending data
160 | * @throws InterruptedException If we are unable to wait to send data
161 | */
162 | public void write(byte[] payload) throws IOException, InterruptedException {
163 | write(payload, true);
164 | }
165 |
166 | /**
167 | * Queues a write packet and optionally sends it immediately.
168 | *
169 | * @param payload Payload in the form of a byte array
170 | * @param flush Specifies whether to send the packet immediately
171 | * @throws IOException If the stream fails while sending data
172 | * @throws InterruptedException If we are unable to wait to send data
173 | */
174 | public void write(byte[] payload, boolean flush) throws IOException, InterruptedException {
175 | synchronized (this) {
176 | /* Make sure we're ready for a write */
177 | while (!isClosed && !writeReady.compareAndSet(true, false))
178 | wait();
179 |
180 | if (isClosed) {
181 | throw new IOException("Stream closed");
182 | }
183 | }
184 |
185 | /* Generate a WRITE packet and send it */
186 | byte[] packet = AdbProtocol.generateWrite(localId, remoteId, payload);
187 | adbConn.outputStream.write(packet);
188 |
189 | if (flush)
190 | adbConn.outputStream.flush();
191 | }
192 |
193 | /**
194 | * Closes the stream. This sends a close message to the peer.
195 | *
196 | * @throws IOException If the stream fails while sending the close message.
197 | */
198 | @Override
199 | public void close() throws IOException {
200 | synchronized (this) {
201 | /* This may already be closed by the remote host */
202 | if (isClosed)
203 | return;
204 |
205 | /* Notify readers/writers that we've closed */
206 | notifyClose();
207 | }
208 |
209 | byte[] packet = AdbProtocol.generateClose(localId, remoteId);
210 | adbConn.outputStream.write(packet);
211 | adbConn.outputStream.flush();
212 | }
213 |
214 | /**
215 | * Retreives whether the stream is closed or not
216 | *
217 | * @return True if the stream is close, false if not
218 | */
219 | public boolean isClosed() {
220 | return isClosed;
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/adblib/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Cameron Gutman
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/decoder/VideoDecoder.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.decoder;
2 |
3 | import android.media.MediaCodec;
4 | import android.media.MediaFormat;
5 | import android.os.Build;
6 | import android.view.Surface;
7 |
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 | import java.util.concurrent.atomic.AtomicBoolean;
11 |
12 | public class VideoDecoder {
13 | private MediaCodec mCodec;
14 | private Worker mWorker;
15 | private AtomicBoolean mIsConfigured = new AtomicBoolean(false);
16 |
17 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) {
18 | if (mWorker != null) {
19 | mWorker.decodeSample(data, offset, size, presentationTimeUs, flags);
20 | }
21 | }
22 |
23 | public void configure(Surface surface, int width, int height, ByteBuffer csd0, ByteBuffer csd1) {
24 | if (mWorker != null) {
25 | mWorker.configure(surface, width, height, csd0, csd1);
26 | }
27 | }
28 |
29 |
30 | public void start() {
31 | if (mWorker == null) {
32 | mWorker = new Worker();
33 | mWorker.setRunning(true);
34 | mWorker.start();
35 | }
36 | }
37 |
38 | public void stop() {
39 | if (mWorker != null) {
40 | mWorker.setRunning(false);
41 | mWorker = null;
42 | mIsConfigured.set(false);
43 | mCodec.stop();
44 | }
45 | }
46 |
47 | private class Worker extends Thread {
48 |
49 | private AtomicBoolean mIsRunning = new AtomicBoolean(false);
50 |
51 | Worker() {
52 | }
53 |
54 | private void setRunning(boolean isRunning) {
55 | mIsRunning.set(isRunning);
56 | }
57 |
58 | private void configure(Surface surface, int width, int height, ByteBuffer csd0, ByteBuffer csd1) {
59 | if (mIsConfigured.get()) {
60 | mIsConfigured.set(false);
61 | mCodec.stop();
62 |
63 | }
64 | MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
65 | format.setByteBuffer("csd-0", csd0);
66 | format.setByteBuffer("csd-1", csd1);
67 | try {
68 | mCodec = MediaCodec.createDecoderByType("video/avc");
69 | } catch (IOException e) {
70 | throw new RuntimeException("Failed to create codec", e);
71 | }
72 | mCodec.configure(format, surface, null, 0);
73 | mCodec.start();
74 | mIsConfigured.set(true);
75 | }
76 |
77 |
78 | @SuppressWarnings("deprecation")
79 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) {
80 | if (mIsConfigured.get() && mIsRunning.get()) {
81 | int index = mCodec.dequeueInputBuffer(-1);
82 | if (index >= 0) {
83 | ByteBuffer buffer;
84 |
85 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
86 | buffer = mCodec.getInputBuffers()[index];
87 | buffer.clear();
88 | } else {
89 | buffer = mCodec.getInputBuffer(index);
90 | }
91 | if (buffer != null) {
92 | buffer.put(data, offset, size);
93 | mCodec.queueInputBuffer(index, 0, size, 0, flags);
94 | }
95 | }
96 | }
97 | }
98 |
99 | @Override
100 | public void run() {
101 | try {
102 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
103 | while (mIsRunning.get()) {
104 | if (mIsConfigured.get()) {
105 | int index = mCodec.dequeueOutputBuffer(info, 0);
106 | if (index >= 0) {
107 | // setting true is telling system to render frame onto Surface
108 | mCodec.releaseOutputBuffer(index, true);
109 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
110 | break;
111 | }
112 | }
113 | } else {
114 | // just waiting to be configured, then decode and render
115 | try {
116 | Thread.sleep(10);
117 | } catch (InterruptedException ignore) {
118 | }
119 | }
120 | }
121 | } catch (IllegalStateException e) {
122 | }
123 |
124 | }
125 | }
126 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/model/ByteUtils.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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 | * https://www.github.com/alexmprog/VideoCodec
9 | */
10 |
11 | public class ByteUtils {
12 |
13 | public static byte[] longToBytes(long x) {
14 | ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / 8);
15 | buffer.putLong(0, x);
16 | return buffer.array();
17 | }
18 |
19 | public static long bytesToLong(byte[] bytes) {
20 | return new BigInteger(bytes).longValue();
21 | }
22 |
23 | public static byte[] intToBytes(int x) {
24 | ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / 8);
25 | buffer.putInt(0, x);
26 | return buffer.array();
27 | }
28 |
29 | public static int bytesToInt(byte[] bytes) {
30 | return new BigInteger(bytes).intValue();
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/model/MediaPacket.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.model;
2 |
3 | /**
4 | * Created by Alexandr Golovach on 27.06.16.
5 | * https://www.github.com/alexmprog/VideoCodec
6 | */
7 | public class MediaPacket {
8 |
9 | public Type type;
10 |
11 | public enum Type {
12 |
13 | VIDEO((byte) 1), AUDIO((byte) 0);
14 |
15 | private byte type;
16 |
17 | Type(byte type) {
18 | this.type = type;
19 | }
20 |
21 | public static Type getType(byte value) {
22 | for (Type type : Type.values()) {
23 | if (type.getType() == value) {
24 | return type;
25 | }
26 | }
27 |
28 | return null;
29 | }
30 |
31 | public byte getType() {
32 | return type;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/org/las2mile/scrcpy/model/VideoPacket.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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 VideoPacket extends MediaPacket {
11 |
12 | public Flag flag;
13 | public long presentationTimeStamp;
14 | public byte[] data;
15 |
16 | public VideoPacket() {
17 | }
18 |
19 | public VideoPacket(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 VideoPacket fromArray(byte[] values) {
28 | VideoPacket videoPacket = new VideoPacket();
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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/surface_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
24 |
25 |
26 |
37 |
38 |
39 |
50 |
51 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/surface_no_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
18 |
24 |
25 |
31 |
32 |
33 |
39 |
40 |
45 |
46 |
52 |
53 |
58 |
59 |
60 |
65 |
66 |
67 |
74 |
75 |
76 |
77 |
78 |
83 |
84 |
85 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/surface_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
24 |
25 |
35 |
36 |
46 |
47 |
48 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/surface_no_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | scrcpy
3 |
4 | Server IP
5 |
6 | Resolution
7 | Bitrate
8 | Start
9 | =
10 | o
11 | v
12 |
13 | Nav Bar (check this if target device does not have on-screen nav bar)
14 |
15 |
16 |
17 | - 1920x1080
18 | - 1280x720
19 | - 800x448
20 | - 640x360
21 |
22 |
23 |
24 | - 1920,1080
25 | - 1280,720
26 | - 800,448
27 | - 640,360
28 |
29 |
30 |
31 | - 6 Mbps
32 | - 4 Mbps
33 | - 2 Mbps
34 | - 1 Mbps
35 |
36 |
37 |
38 | - 6144000
39 | - 4096000
40 | - 2048000
41 | - 1024000
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/test/java/org/las2mile/scrcpy/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.6.3'
11 |
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 |
17 | }
18 |
19 | allprojects {
20 | repositories {
21 | google()
22 | jcenter()
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
29 |
--------------------------------------------------------------------------------
/config/android-checkstyle.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'checkstyle'
2 | check.dependsOn 'checkstyle'
3 |
4 | checkstyle {
5 | toolVersion = '6.19'
6 | }
7 |
8 | task checkstyle(type: Checkstyle) {
9 | description = "Check Java style with Checkstyle"
10 | configFile = rootProject.file("config/checkstyle/checkstyle.xml")
11 | source = javaSources()
12 | classpath = files()
13 | ignoreFailures = true
14 | }
15 |
16 | def javaSources() {
17 | def files = []
18 | android.sourceSets.each { sourceSet ->
19 | sourceSet.java.each { javaSource ->
20 | javaSource.getSrcDirs().each {
21 | if (it.exists()) {
22 | files.add(it)
23 | }
24 | }
25 | }
26 | }
27 | return files
28 | }
29 |
--------------------------------------------------------------------------------
/config/checkstyle/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/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=-Xmx1536m
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 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri May 22 23:23:45 IST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/release/scrcpy-release.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/release/scrcpy-release.apk
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/server/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright (C) 2018 Genymobile
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
204 |
--------------------------------------------------------------------------------
/server/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | defaultConfig {
6 | applicationId "org.las2mile.scrcpy"
7 | minSdkVersion 19
8 | targetSdkVersion 28
9 | versionCode 1
10 | versionName "1.0"
11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
12 |
13 | }
14 |
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 |
20 | }
21 | }
22 |
23 | task copyRelease(dependsOn: 'deleteServer', type: Copy){
24 | from file('build/outputs/apk/release/server-release-unsigned.apk')
25 | into file('../app/src/main/assets')
26 | rename('server-release-unsigned.apk', 'scrcpy-server.jar')
27 | }
28 |
29 | task deleteServer(type: Delete){
30 | delete "../app/src/main/assets/scrcpy-server.jar"
31 | }
32 |
33 |
34 | task copyDebug (dependsOn: 'deleteServer', type: Copy){
35 | from file("build/outputs/apk/debug/server-debug.apk")
36 | into file("../app/src/main/assets/")
37 | rename('server-debug.apk', 'scrcpy-server.jar')
38 | }
39 |
40 | afterEvaluate {
41 | packageRelease.finalizedBy(copyRelease)
42 | }
43 |
44 | afterEvaluate {
45 | packageDebug.finalizedBy(copyDebug)
46 | }
47 | }
48 |
49 |
50 |
51 | dependencies {
52 | implementation fileTree(dir: 'libs', include: ['*.jar'])
53 | testImplementation 'junit:junit:4.12'
54 | }
55 |
56 | apply from: "$project.rootDir/config/android-checkstyle.gradle"
57 |
58 |
--------------------------------------------------------------------------------
/server/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/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/org/las2mile/scrcpy/Device.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.graphics.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.las2mile.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.x * deviceSize.getWidth() / videoSize.getWidth();
87 | int scaledY = point.y * 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.x * deviceSize.getWidth() / videoSize.getWidth();
116 | int scaledY = point.y * 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/las2mile/scrcpy/DisplayInfo.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | public final class DisplayInfo {
4 | private final Size size;
5 | private final int rotation;
6 |
7 | public DisplayInfo(Size size, int rotation) {
8 | this.size = size;
9 | this.rotation = rotation;
10 | }
11 |
12 | public Size getSize() {
13 | return size;
14 | }
15 |
16 | public int getRotation() {
17 | return rotation;
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/DroidConnection.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import java.io.Closeable;
4 | import java.io.EOFException;
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.io.OutputStream;
8 | import java.net.ServerSocket;
9 | import java.net.Socket;
10 |
11 | public final class DroidConnection implements Closeable {
12 |
13 |
14 | private static Socket socket = null;
15 | private OutputStream outputStream;
16 | private InputStream inputStream;
17 |
18 | private DroidConnection(Socket socket) throws IOException {
19 | this.socket = socket;
20 |
21 | inputStream = socket.getInputStream();
22 | outputStream = socket.getOutputStream();
23 | }
24 |
25 |
26 | private static Socket listenAndAccept() throws IOException {
27 | ServerSocket serverSocket = new ServerSocket(7007);
28 | Socket sock = null;
29 | try {
30 | sock = serverSocket.accept();
31 | } finally {
32 | serverSocket.close();
33 | }
34 | return sock;
35 | }
36 |
37 | public static DroidConnection open(String ip) throws IOException {
38 |
39 | socket = listenAndAccept();
40 | DroidConnection connection = null;
41 | if (socket.getInetAddress().toString().equals(ip)) {
42 | connection = new DroidConnection(socket);
43 | }
44 | return connection;
45 | }
46 |
47 | public void close() throws IOException {
48 | socket.shutdownInput();
49 | socket.shutdownOutput();
50 | socket.close();
51 | }
52 |
53 | public OutputStream getOutputStream() {
54 | return outputStream;
55 | }
56 |
57 |
58 | public int[] NewreceiveControlEvent() throws IOException {
59 |
60 | byte[] buf = new byte[16];
61 | int n = inputStream.read(buf, 0, 16);
62 | if (n == -1) {
63 | throw new EOFException("Event controller socket closed");
64 | }
65 |
66 |
67 | final int[] array = new int[buf.length / 4];
68 | for (int i = 0; i < array.length; i++)
69 | array[i] = (((int) (buf[i * 4]) << 24) & 0xFF000000) |
70 | (((int) (buf[i * 4 + 1]) << 16) & 0xFF0000) |
71 | (((int) (buf[i * 4 + 2]) << 8) & 0xFF00) |
72 | ((int) (buf[i * 4 + 3]) & 0xFF);
73 | return array;
74 |
75 |
76 | }
77 |
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/EventController.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.graphics.Point;
4 | import android.os.SystemClock;
5 | import android.view.InputDevice;
6 | import android.view.InputEvent;
7 | import android.view.KeyCharacterMap;
8 | import android.view.KeyEvent;
9 | import android.view.MotionEvent;
10 |
11 | import org.las2mile.scrcpy.wrappers.InputManager;
12 |
13 | import java.io.IOException;
14 |
15 |
16 | public class EventController {
17 |
18 | private final Device device;
19 | private final DroidConnection connection;
20 | private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
21 | private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
22 | private long lastMouseDown;
23 | private float then;
24 | private boolean hit = false;
25 | private boolean proximity = false;
26 |
27 | public EventController(Device device, DroidConnection connection) {
28 | this.device = device;
29 | this.connection = connection;
30 | initPointer();
31 | }
32 |
33 | private void initPointer() {
34 | MotionEvent.PointerProperties props = pointerProperties[0];
35 | props.id = 0;
36 | props.toolType = MotionEvent.TOOL_TYPE_FINGER;
37 |
38 | MotionEvent.PointerCoords coords = pointerCoords[0];
39 | coords.orientation = 0;
40 | coords.pressure = 1;
41 | coords.size = 1;
42 | }
43 |
44 | private void setPointerCoords(Point point) {
45 | MotionEvent.PointerCoords coords = pointerCoords[0];
46 | coords.x = point.x;
47 | coords.y = point.y;
48 | }
49 |
50 | private void setScroll(int hScroll, int vScroll) {
51 | MotionEvent.PointerCoords coords = pointerCoords[0];
52 | coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
53 | coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
54 | }
55 |
56 | public void control() throws IOException {
57 | // on start, turn screen on
58 | turnScreenOn();
59 |
60 | while (true) {
61 | // handleEvent();
62 | int[] buffer = connection.NewreceiveControlEvent();
63 | if (buffer != null) {
64 | long now = SystemClock.uptimeMillis();
65 | if (buffer[2] == 0 && buffer[3] == 0) {
66 | if (buffer[0] == 28) {
67 | proximity = true; // Proximity event
68 | } else if (buffer[0] == 29) {
69 | proximity = false;
70 | } else {
71 | injectKeycode(buffer[0]);
72 | }
73 | } else {
74 | int action = buffer[0];
75 | if (action == MotionEvent.ACTION_UP && (!device.isScreenOn() || proximity)) {
76 | if (hit) {
77 | if (now - then < 250) {
78 | then = 0;
79 | hit = false;
80 | injectKeycode(KeyEvent.KEYCODE_POWER);
81 | } else {
82 | then = now;
83 | }
84 | } else {
85 | hit = true;
86 | then = now;
87 | }
88 |
89 | } else {
90 | if (action == MotionEvent.ACTION_DOWN) {
91 | lastMouseDown = now;
92 | }
93 | int button = buffer[1];
94 | int X = buffer[2];
95 | int Y = buffer[3];
96 | Point point = new Point(X, Y);
97 | Point newpoint = device.NewgetPhysicalPoint(point);
98 | setPointerCoords(newpoint);
99 | MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, button, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
100 | injectEvent(event);
101 | }
102 | }
103 |
104 |
105 | }
106 | }
107 | }
108 |
109 |
110 | private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
111 | long now = SystemClock.uptimeMillis();
112 | KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
113 | InputDevice.SOURCE_KEYBOARD);
114 | return injectEvent(event);
115 | }
116 |
117 | private boolean injectKeycode(int keyCode) {
118 | return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
119 | && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
120 | }
121 |
122 | private boolean injectEvent(InputEvent event) {
123 | return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
124 | }
125 |
126 | private boolean turnScreenOn() {
127 | return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/Ln.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.util.Log;
4 |
5 | /**
6 | * 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
7 | * directly).
8 | */
9 | public final class Ln {
10 |
11 | private static final String TAG = "scrcpy";
12 | private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
13 |
14 | private Ln() {
15 | // not instantiable
16 | }
17 |
18 | public static boolean isEnabled(Level level) {
19 | return level.ordinal() >= THRESHOLD.ordinal();
20 | }
21 |
22 | public static void d(String message) {
23 | if (isEnabled(Level.DEBUG)) {
24 | Log.d(TAG, message);
25 | System.out.println("DEBUG: " + message);
26 | }
27 | }
28 |
29 | public static void i(String message) {
30 | if (isEnabled(Level.INFO)) {
31 | Log.i(TAG, message);
32 | System.out.println("INFO: " + message);
33 | }
34 | }
35 |
36 | public static void w(String message) {
37 | if (isEnabled(Level.WARN)) {
38 | Log.w(TAG, message);
39 | System.out.println("WARN: " + message);
40 | }
41 | }
42 |
43 | public static void e(String message, Throwable throwable) {
44 | if (isEnabled(Level.ERROR)) {
45 | Log.e(TAG, message, throwable);
46 | System.out.println("ERROR: " + message);
47 | throwable.printStackTrace();
48 | }
49 | }
50 |
51 | enum Level {
52 | DEBUG,
53 | INFO,
54 | WARN,
55 | ERROR;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/Options.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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/las2mile/scrcpy/Position.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.graphics.Point;
4 |
5 | import java.util.Objects;
6 |
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/las2mile/scrcpy/ScreenEncoder.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.graphics.Rect;
4 | import android.media.MediaCodec;
5 | import android.media.MediaCodecInfo;
6 | import android.media.MediaFormat;
7 | import android.os.Build;
8 | import android.os.IBinder;
9 | import android.view.Surface;
10 |
11 | import org.las2mile.scrcpy.model.MediaPacket;
12 | import org.las2mile.scrcpy.model.VideoPacket;
13 | import org.las2mile.scrcpy.wrappers.SurfaceControl;
14 |
15 | import java.io.IOException;
16 | import java.io.OutputStream;
17 | import java.nio.ByteBuffer;
18 | import java.util.concurrent.atomic.AtomicBoolean;
19 |
20 | public class ScreenEncoder implements Device.RotationListener {
21 |
22 | private static final int DEFAULT_FRAME_RATE = 60; // fps
23 | private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
24 |
25 | private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
26 |
27 | private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
28 |
29 | private final AtomicBoolean rotationChanged = new AtomicBoolean();
30 |
31 | private int bitRate;
32 | private int frameRate;
33 | private int iFrameInterval;
34 |
35 | public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) {
36 | this.bitRate = bitRate;
37 | this.frameRate = frameRate;
38 | this.iFrameInterval = iFrameInterval;
39 | }
40 |
41 | public ScreenEncoder(int bitRate) {
42 | this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
43 | }
44 |
45 | private static MediaCodec createCodec() throws IOException {
46 | return MediaCodec.createEncoderByType("video/avc");
47 | }
48 |
49 | private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
50 | MediaFormat format = new MediaFormat();
51 | format.setString(MediaFormat.KEY_MIME, "video/avc");
52 | format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
53 | format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
54 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
55 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
56 |
57 | // display the very first frame, and recover from bad quality when no new frames
58 | format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
59 | return format;
60 | }
61 |
62 | private static IBinder createDisplay() {
63 | return SurfaceControl.createDisplay("scrcpy", false);
64 | }
65 |
66 | private static void configure(MediaCodec codec, MediaFormat format) {
67 | codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
68 | }
69 |
70 | private static void setSize(MediaFormat format, int width, int height) {
71 | format.setInteger(MediaFormat.KEY_WIDTH, width);
72 | format.setInteger(MediaFormat.KEY_HEIGHT, height);
73 | }
74 |
75 | private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
76 | SurfaceControl.openTransaction();
77 | try {
78 | SurfaceControl.setDisplaySurface(display, surface);
79 | SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
80 | SurfaceControl.setDisplayLayerStack(display, 0);
81 | } finally {
82 | SurfaceControl.closeTransaction();
83 | }
84 | }
85 |
86 | private static void destroyDisplay(IBinder display) {
87 | SurfaceControl.destroyDisplay(display);
88 | }
89 |
90 | @Override
91 | public void onRotationChanged(int rotation) {
92 | rotationChanged.set(true);
93 | }
94 |
95 | public boolean consumeRotationChange() {
96 | return rotationChanged.getAndSet(false);
97 | }
98 |
99 | public void streamScreen(Device device, OutputStream outputStream) throws IOException {
100 | MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
101 | device.setRotationListener(this);
102 | boolean alive;
103 | try {
104 | do {
105 | MediaCodec codec = createCodec();
106 | IBinder display = createDisplay();
107 | Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect();
108 | Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
109 | setSize(format, videoRect.width(), videoRect.height());
110 | configure(codec, format);
111 | Surface surface = codec.createInputSurface();
112 | setDisplaySurface(display, surface, deviceRect, videoRect);
113 | codec.start();
114 | try {
115 | alive = encode(codec, outputStream);
116 | } finally {
117 | codec.stop();
118 | destroyDisplay(display);
119 | codec.release();
120 | surface.release();
121 | }
122 | } while (alive);
123 | } finally {
124 | device.setRotationListener(null);
125 | }
126 | }
127 |
128 | private boolean encode(MediaCodec codec, OutputStream outputStream) throws IOException {
129 | @SuppressWarnings("checkstyle:MagicNumber")
130 | // byte[] buf = new byte[bitRate / 8]; // may contain up to 1 second of video
131 | boolean eof = false;
132 | MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
133 | while (!consumeRotationChange() && !eof) {
134 | int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
135 | eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
136 | try {
137 | if (consumeRotationChange()) {
138 | // must restart encoding with new size
139 | break;
140 | }
141 | if (outputBufferId >= 0) {
142 | ByteBuffer outputBuffer;
143 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
144 | ByteBuffer[] outputBuffers = codec.getOutputBuffers();
145 | outputBuffer = outputBuffers[outputBufferId];
146 | } else {
147 | outputBuffer = codec.getOutputBuffer(outputBufferId);
148 | }
149 |
150 | if (bufferInfo.size > 0 && outputBuffer != null) {
151 | outputBuffer.position(bufferInfo.offset);
152 | outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
153 | byte[] b = new byte[outputBuffer.remaining()];
154 | outputBuffer.get(b);
155 |
156 | MediaPacket.Type type = MediaPacket.Type.VIDEO;
157 | VideoPacket.Flag flag = VideoPacket.Flag.CONFIG;
158 |
159 | if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
160 | flag = VideoPacket.Flag.END;
161 | } else if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
162 | flag = VideoPacket.Flag.KEY_FRAME;
163 | } else if (bufferInfo.flags == 0) {
164 | flag = VideoPacket.Flag.FRAME;
165 | }
166 | VideoPacket packet = new VideoPacket(type, flag, bufferInfo.presentationTimeUs, b);
167 | outputStream.write(packet.toByteArray());
168 | }
169 |
170 | }
171 | } finally {
172 | if (outputBufferId >= 0) {
173 | codec.releaseOutputBuffer(outputBufferId, false);
174 | }
175 | }
176 | }
177 |
178 | return !eof;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/ScreenInfo.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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/las2mile/scrcpy/Server.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import java.io.IOException;
4 |
5 | public final class Server {
6 |
7 | private static String ip = null;
8 |
9 | private Server() {
10 | // not instantiable
11 | }
12 |
13 | private static void scrcpy(Options options) throws IOException {
14 | final Device device = new Device(options);
15 | try (DroidConnection connection = DroidConnection.open(ip)) {
16 | ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
17 |
18 | // asynchronous
19 | startEventController(device, connection);
20 |
21 | try {
22 | // synchronous
23 | screenEncoder.streamScreen(device, connection.getOutputStream());
24 | } catch (IOException e) {
25 | e.printStackTrace();
26 | // this is expected on close
27 | Ln.d("Screen streaming stopped");
28 |
29 |
30 | }
31 | }
32 | }
33 |
34 | private static void startEventController(final Device device, final DroidConnection connection) {
35 | new Thread(new Runnable() {
36 | @Override
37 | public void run() {
38 | try {
39 | new EventController(device, connection).control();
40 | } catch (IOException e) {
41 | // this is expected on close
42 | Ln.d("Event controller stopped");
43 | }
44 | }
45 | }).start();
46 | }
47 |
48 | @SuppressWarnings("checkstyle:MagicNumber")
49 | private static Options createOptions(String... args) {
50 | Options options = new Options();
51 |
52 | if (args.length < 1) {
53 | return options;
54 | }
55 | ip = String.valueOf(args[0]);
56 |
57 |
58 | if (args.length < 2) {
59 | return options;
60 | }
61 | int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
62 | options.setMaxSize(maxSize);
63 |
64 | if (args.length < 3) {
65 | return options;
66 | }
67 | int bitRate = Integer.parseInt(args[2]);
68 | options.setBitRate(bitRate);
69 |
70 | if (args.length < 4) {
71 | return options;
72 | }
73 | // use "adb forward" instead of "adb tunnel"? (so the server must listen)
74 | boolean tunnelForward = Boolean.parseBoolean(args[3]);
75 | options.setTunnelForward(tunnelForward);
76 |
77 | return options;
78 | }
79 |
80 | public static void main(String... args) throws Exception {
81 | Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
82 | @Override
83 | public void uncaughtException(Thread t, Throwable e) {
84 | Ln.e("Exception on thread " + t, e);
85 | }
86 | });
87 |
88 | try {
89 | Process cmd = Runtime.getRuntime().exec("rm /data/local/tmp/scrcpy-server.jar");
90 | cmd.waitFor();
91 | } catch (IOException e1) {
92 | e1.printStackTrace();
93 | } catch (InterruptedException e1) {
94 | e1.printStackTrace();
95 | }
96 |
97 | Options options = createOptions(args);
98 | scrcpy(options);
99 | }
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/Size.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy;
2 |
3 | import android.graphics.Rect;
4 |
5 | import java.util.Objects;
6 |
7 | public final class Size {
8 | private final int width;
9 | private final int height;
10 |
11 | public Size(int width, int height) {
12 | this.width = width;
13 | this.height = height;
14 | }
15 |
16 | public int getWidth() {
17 | return width;
18 | }
19 |
20 | public int getHeight() {
21 | return height;
22 | }
23 |
24 | public Size rotate() {
25 | return new Size(height, width);
26 | }
27 |
28 | public Rect toRect() {
29 | return new Rect(0, 0, width, height);
30 | }
31 |
32 | @Override
33 | public boolean equals(Object o) {
34 | if (this == o) {
35 | return true;
36 | }
37 | if (o == null || getClass() != o.getClass()) {
38 | return false;
39 | }
40 | Size size = (Size) o;
41 | return width == size.width
42 | && height == size.height;
43 | }
44 |
45 | @Override
46 | public int hashCode() {
47 | return Objects.hash(width, height);
48 | }
49 |
50 | @Override
51 | public String toString() {
52 | return "Size{"
53 | + "width=" + width
54 | + ", height=" + height
55 | + '}';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/model/ByteUtils.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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/las2mile/scrcpy/model/MediaPacket.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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/las2mile/scrcpy/model/VideoPacket.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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/las2mile/scrcpy/wrappers/DisplayManager.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.wrappers;
2 |
3 | import android.os.IInterface;
4 |
5 | import org.las2mile.scrcpy.DisplayInfo;
6 | import org.las2mile.scrcpy.Size;
7 |
8 | public final class DisplayManager {
9 | private final IInterface manager;
10 |
11 | public DisplayManager(IInterface manager) {
12 | this.manager = manager;
13 | }
14 |
15 | public DisplayInfo getDisplayInfo() {
16 | try {
17 | Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
18 | Class> cls = displayInfo.getClass();
19 | // width and height already take the rotation into account
20 | int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
21 | int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
22 | int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
23 | return new DisplayInfo(new Size(width, height), rotation);
24 | } catch (Exception e) {
25 | throw new AssertionError(e);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/wrappers/InputManager.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/wrappers/PowerManager.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.Build;
5 | import android.os.IInterface;
6 |
7 | import java.lang.reflect.InvocationTargetException;
8 | import java.lang.reflect.Method;
9 |
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 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/wrappers/ServiceManager.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.wrappers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.IBinder;
5 | import android.os.IInterface;
6 |
7 | import java.lang.reflect.Method;
8 |
9 | @SuppressLint("PrivateApi")
10 | public final class ServiceManager {
11 | private final Method getServiceMethod;
12 |
13 | private WindowManager windowManager;
14 | private DisplayManager displayManager;
15 | private InputManager inputManager;
16 | private PowerManager powerManager;
17 |
18 | public ServiceManager() {
19 | try {
20 | getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
21 | } catch (Exception e) {
22 | throw new AssertionError(e);
23 | }
24 | }
25 |
26 | private IInterface getService(String service, String type) {
27 | try {
28 | IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
29 | Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
30 | return (IInterface) asInterfaceMethod.invoke(null, binder);
31 | } catch (Exception e) {
32 | throw new AssertionError(e);
33 | }
34 | }
35 |
36 | public WindowManager getWindowManager() {
37 | if (windowManager == null) {
38 | windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
39 | }
40 | return windowManager;
41 | }
42 |
43 | public DisplayManager getDisplayManager() {
44 | if (displayManager == null) {
45 | displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
46 | }
47 | return displayManager;
48 | }
49 |
50 | public InputManager getInputManager() {
51 | if (inputManager == null) {
52 | inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
53 | }
54 | return inputManager;
55 | }
56 |
57 | public PowerManager getPowerManager() {
58 | if (powerManager == null) {
59 | powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
60 | }
61 | return powerManager;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/wrappers/SurfaceControl.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.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 | @SuppressLint("PrivateApi")
9 | public final class SurfaceControl {
10 |
11 | private static final Class> CLASS;
12 |
13 | static {
14 | try {
15 | CLASS = Class.forName("android.view.SurfaceControl");
16 | } catch (ClassNotFoundException e) {
17 | throw new AssertionError(e);
18 | }
19 | }
20 |
21 | private SurfaceControl() {
22 | // only static methods
23 | }
24 |
25 | public static void openTransaction() {
26 | try {
27 | CLASS.getMethod("openTransaction").invoke(null);
28 | } catch (Exception e) {
29 | throw new AssertionError(e);
30 | }
31 | }
32 |
33 | public static void closeTransaction() {
34 | try {
35 | CLASS.getMethod("closeTransaction").invoke(null);
36 | } catch (Exception e) {
37 | throw new AssertionError(e);
38 | }
39 | }
40 |
41 | public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
42 | try {
43 | CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class)
44 | .invoke(null, displayToken, orientation, layerStackRect, displayRect);
45 | } catch (Exception e) {
46 | throw new AssertionError(e);
47 | }
48 | }
49 |
50 | public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
51 | try {
52 | CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack);
53 | } catch (Exception e) {
54 | throw new AssertionError(e);
55 | }
56 | }
57 |
58 | public static void setDisplaySurface(IBinder displayToken, Surface surface) {
59 | try {
60 | CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface);
61 | } catch (Exception e) {
62 | throw new AssertionError(e);
63 | }
64 | }
65 |
66 | public static IBinder createDisplay(String name, boolean secure) {
67 | try {
68 | return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
69 | } catch (Exception e) {
70 | throw new AssertionError(e);
71 | }
72 | }
73 |
74 | public static void destroyDisplay(IBinder displayToken) {
75 | try {
76 | CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);
77 | } catch (Exception e) {
78 | throw new AssertionError(e);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/server/src/main/java/org/las2mile/scrcpy/wrappers/WindowManager.java:
--------------------------------------------------------------------------------
1 | package org.las2mile.scrcpy.wrappers;
2 |
3 | import android.os.IInterface;
4 | import android.view.IRotationWatcher;
5 |
6 | public final class WindowManager {
7 | private final IInterface manager;
8 |
9 | public WindowManager(IInterface manager) {
10 | this.manager = manager;
11 | }
12 |
13 | public int getRotation() {
14 | try {
15 | Class> cls = manager.getClass();
16 | try {
17 | return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
18 | } catch (NoSuchMethodException e) {
19 | // method changed since this commit:
20 | // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
21 | return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
22 | }
23 | } catch (Exception e) {
24 | throw new AssertionError(e);
25 | }
26 | }
27 |
28 | public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
29 | try {
30 | Class> cls = manager.getClass();
31 | try {
32 | cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
33 | } catch (NoSuchMethodException e) {
34 | // display parameter added since this commit:
35 | // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
36 | cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
37 | }
38 | } catch (Exception e) {
39 | throw new AssertionError(e);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':server', ':app'
2 |
--------------------------------------------------------------------------------