mirror of
https://github.com/barry-ran/QtScrcpy.git
synced 2025-04-20 11:35:56 +00:00
增加server代码,android studio可直接编译
This commit is contained in:
parent
4c6e017322
commit
1ed6c20548
27 changed files with 1965 additions and 0 deletions
47
server/build.gradle
Normal file
47
server/build.gradle
Normal file
|
@ -0,0 +1,47 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
defaultConfig {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 5
|
||||
versionName "1.4"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
28
server/config/android-checkstyle.gradle
Normal file
28
server/config/android-checkstyle.gradle
Normal file
|
@ -0,0 +1,28 @@
|
|||
apply plugin: 'checkstyle'
|
||||
check.dependsOn 'checkstyle'
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '6.19'
|
||||
}
|
||||
|
||||
task checkstyle(type: Checkstyle) {
|
||||
description = "Check Java style with Checkstyle"
|
||||
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
|
||||
source = javaSources()
|
||||
classpath = files()
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
def javaSources() {
|
||||
def files = []
|
||||
android.sourceSets.each { sourceSet ->
|
||||
sourceSet.java.each { javaSource ->
|
||||
javaSource.getSrcDirs().each {
|
||||
if (it.exists()) {
|
||||
files.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
21
server/proguard-rules.pro
vendored
Normal file
21
server/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
server/src/main/AndroidManifest.xml
Normal file
2
server/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<!-- not a real Android application, it is run by app_process manually -->
|
||||
<manifest package="com.genymobile.scrcpy"/>
|
25
server/src/main/aidl/android/view/IRotationWatcher.aidl
Normal file
25
server/src/main/aidl/android/view/IRotationWatcher.aidl
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* //device/java/android/android/hardware/ISensorListener.aidl
|
||||
**
|
||||
** Copyright 2008, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
/**
|
||||
* {@hide}
|
||||
*/
|
||||
interface IRotationWatcher {
|
||||
oneway void onRotationChanged(int rotation);
|
||||
}
|
105
server/src/main/java/com/genymobile/scrcpy/ControlEvent.java
Normal file
105
server/src/main/java/com/genymobile/scrcpy/ControlEvent.java
Normal file
|
@ -0,0 +1,105 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
/**
|
||||
* Union of all supported event types, identified by their {@code type}.
|
||||
*/
|
||||
public final class ControlEvent {
|
||||
|
||||
public static final int TYPE_KEYCODE = 0;
|
||||
public static final int TYPE_TEXT = 1;
|
||||
public static final int TYPE_MOUSE = 2;
|
||||
public static final int TYPE_SCROLL = 3;
|
||||
public static final int TYPE_COMMAND = 4;
|
||||
|
||||
public static final int COMMAND_BACK_OR_SCREEN_ON = 0;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or COMMAND_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
private Position position;
|
||||
private int hScroll;
|
||||
private int vScroll;
|
||||
|
||||
private ControlEvent() {
|
||||
}
|
||||
|
||||
public static ControlEvent createKeycodeControlEvent(int action, int keycode, int metaState) {
|
||||
ControlEvent event = new ControlEvent();
|
||||
event.type = TYPE_KEYCODE;
|
||||
event.action = action;
|
||||
event.keycode = keycode;
|
||||
event.metaState = metaState;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static ControlEvent createTextControlEvent(String text) {
|
||||
ControlEvent event = new ControlEvent();
|
||||
event.type = TYPE_TEXT;
|
||||
event.text = text;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static ControlEvent createMotionControlEvent(int action, int buttons, Position position) {
|
||||
ControlEvent event = new ControlEvent();
|
||||
event.type = TYPE_MOUSE;
|
||||
event.action = action;
|
||||
event.buttons = buttons;
|
||||
event.position = position;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static ControlEvent createScrollControlEvent(Position position, int hScroll, int vScroll) {
|
||||
ControlEvent event = new ControlEvent();
|
||||
event.type = TYPE_SCROLL;
|
||||
event.position = position;
|
||||
event.hScroll = hScroll;
|
||||
event.vScroll = vScroll;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static ControlEvent createCommandControlEvent(int action) {
|
||||
ControlEvent event = new ControlEvent();
|
||||
event.type = TYPE_COMMAND;
|
||||
event.action = action;
|
||||
return event;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public int getMetaState() {
|
||||
return metaState;
|
||||
}
|
||||
|
||||
public int getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public int getKeycode() {
|
||||
return keycode;
|
||||
}
|
||||
|
||||
public int getButtons() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public int getHScroll() {
|
||||
return hScroll;
|
||||
}
|
||||
|
||||
public int getVScroll() {
|
||||
return vScroll;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ControlEventReader {
|
||||
|
||||
private static final int KEYCODE_PAYLOAD_LENGTH = 9;
|
||||
private static final int MOUSE_PAYLOAD_LENGTH = 13;
|
||||
private static final int SCROLL_PAYLOAD_LENGTH = 16;
|
||||
private static final int COMMAND_PAYLOAD_LENGTH = 1;
|
||||
|
||||
public static final int TEXT_MAX_LENGTH = 300;
|
||||
private static final int RAW_BUFFER_SIZE = 1024;
|
||||
|
||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH];
|
||||
|
||||
public ControlEventReader() {
|
||||
// invariant: the buffer is always in "get" mode
|
||||
buffer.limit(0);
|
||||
}
|
||||
|
||||
public boolean isFull() {
|
||||
return buffer.remaining() == rawBuffer.length;
|
||||
}
|
||||
|
||||
public void readFrom(InputStream input) throws IOException {
|
||||
if (isFull()) {
|
||||
throw new IllegalStateException("Buffer full, call next() to consume");
|
||||
}
|
||||
buffer.compact();
|
||||
int head = buffer.position();
|
||||
int r = input.read(rawBuffer, head, rawBuffer.length - head);
|
||||
if (r == -1) {
|
||||
throw new EOFException("Event controller socket closed");
|
||||
}
|
||||
buffer.position(head + r);
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
public ControlEvent next() {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
int savedPosition = buffer.position();
|
||||
|
||||
int type = buffer.get();
|
||||
ControlEvent controlEvent;
|
||||
switch (type) {
|
||||
case ControlEvent.TYPE_KEYCODE:
|
||||
controlEvent = parseKeycodeControlEvent();
|
||||
break;
|
||||
case ControlEvent.TYPE_TEXT:
|
||||
controlEvent = parseTextControlEvent();
|
||||
break;
|
||||
case ControlEvent.TYPE_MOUSE:
|
||||
controlEvent = parseMouseControlEvent();
|
||||
break;
|
||||
case ControlEvent.TYPE_SCROLL:
|
||||
controlEvent = parseScrollControlEvent();
|
||||
break;
|
||||
case ControlEvent.TYPE_COMMAND:
|
||||
controlEvent = parseCommandControlEvent();
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown event type: " + type);
|
||||
controlEvent = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (controlEvent == null) {
|
||||
// failure, reset savedPosition
|
||||
buffer.position(savedPosition);
|
||||
}
|
||||
return controlEvent;
|
||||
}
|
||||
|
||||
private ControlEvent parseKeycodeControlEvent() {
|
||||
if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
int keycode = buffer.getInt();
|
||||
int metaState = buffer.getInt();
|
||||
return ControlEvent.createKeycodeControlEvent(action, keycode, metaState);
|
||||
}
|
||||
|
||||
private ControlEvent parseTextControlEvent() {
|
||||
if (buffer.remaining() < 1) {
|
||||
return null;
|
||||
}
|
||||
int len = toUnsigned(buffer.getShort());
|
||||
if (buffer.remaining() < len) {
|
||||
return null;
|
||||
}
|
||||
buffer.get(textBuffer, 0, len);
|
||||
String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
||||
return ControlEvent.createTextControlEvent(text);
|
||||
}
|
||||
|
||||
private ControlEvent parseMouseControlEvent() {
|
||||
if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
int buttons = buffer.getInt();
|
||||
Position position = readPosition(buffer);
|
||||
return ControlEvent.createMotionControlEvent(action, buttons, position);
|
||||
}
|
||||
|
||||
private ControlEvent parseScrollControlEvent() {
|
||||
if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
Position position = readPosition(buffer);
|
||||
int hScroll = buffer.getInt();
|
||||
int vScroll = buffer.getInt();
|
||||
return ControlEvent.createScrollControlEvent(position, hScroll, vScroll);
|
||||
}
|
||||
|
||||
private ControlEvent parseCommandControlEvent() {
|
||||
if (buffer.remaining() < COMMAND_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
return ControlEvent.createCommandControlEvent(action);
|
||||
}
|
||||
|
||||
private static Position readPosition(ByteBuffer buffer) {
|
||||
int x = toUnsigned(buffer.getShort());
|
||||
int y = toUnsigned(buffer.getShort());
|
||||
int screenWidth = toUnsigned(buffer.getShort());
|
||||
int screenHeight = toUnsigned(buffer.getShort());
|
||||
return new Position(x, y, screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static int toUnsigned(short value) {
|
||||
return value & 0xffff;
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static int toUnsigned(byte value) {
|
||||
return value & 0xff;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.net.LocalServerSocket;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class DesktopConnection implements Closeable {
|
||||
|
||||
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
|
||||
|
||||
private static final String SOCKET_NAME = "scrcpy";
|
||||
|
||||
private final LocalSocket socket;
|
||||
private final InputStream inputStream;
|
||||
private final FileDescriptor fd;
|
||||
|
||||
private final ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
private DesktopConnection(LocalSocket socket) throws IOException {
|
||||
this.socket = socket;
|
||||
inputStream = socket.getInputStream();
|
||||
fd = socket.getFileDescriptor();
|
||||
}
|
||||
|
||||
private static LocalSocket connect(String abstractName) throws IOException {
|
||||
LocalSocket localSocket = new LocalSocket();
|
||||
localSocket.connect(new LocalSocketAddress(abstractName));
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
private static LocalSocket listenAndAccept(String abstractName) throws IOException {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(abstractName);
|
||||
try {
|
||||
return localServerSocket.accept();
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
||||
LocalSocket socket;
|
||||
if (tunnelForward) {
|
||||
socket = listenAndAccept(SOCKET_NAME);
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
socket.getOutputStream().write(0);
|
||||
} else {
|
||||
socket = connect(SOCKET_NAME);
|
||||
}
|
||||
|
||||
DesktopConnection connection = new DesktopConnection(socket);
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
socket.shutdownInput();
|
||||
socket.shutdownOutput();
|
||||
socket.close();
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private void send(String deviceName, int width, int height) throws IOException {
|
||||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||
|
||||
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length);
|
||||
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
|
||||
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
|
||||
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
|
||||
IO.writeFully(fd, buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public FileDescriptor getFd() {
|
||||
return fd;
|
||||
}
|
||||
|
||||
public ControlEvent receiveControlEvent() throws IOException {
|
||||
ControlEvent event = reader.next();
|
||||
while (event == null) {
|
||||
reader.readFrom(inputStream);
|
||||
event = reader.next();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
}
|
138
server/src/main/java/com/genymobile/scrcpy/Device.java
Normal file
138
server/src/main/java/com/genymobile/scrcpy/Device.java
Normal file
|
@ -0,0 +1,138 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.InputEvent;
|
||||
|
||||
public final class Device {
|
||||
|
||||
public interface RotationListener {
|
||||
void onRotationChanged(int rotation);
|
||||
}
|
||||
|
||||
private final ServiceManager serviceManager = new ServiceManager();
|
||||
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
|
||||
public Device(Options options) {
|
||||
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
|
||||
registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) throws RemoteException {
|
||||
synchronized (Device.this) {
|
||||
screenInfo = screenInfo.withRotation(rotation);
|
||||
|
||||
// notify
|
||||
if (rotationListener != null) {
|
||||
rotationListener.onRotationChanged(rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized ScreenInfo getScreenInfo() {
|
||||
return screenInfo;
|
||||
}
|
||||
|
||||
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
|
||||
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
|
||||
boolean rotated = (displayInfo.getRotation() & 1) != 0;
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||
if (crop != null) {
|
||||
if (rotated) {
|
||||
// the crop (provided by the user) is expressed in the natural orientation
|
||||
crop = flipRect(crop);
|
||||
}
|
||||
if (!contentRect.intersect(crop)) {
|
||||
// intersect() changes contentRect so that it is intersected with crop
|
||||
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
|
||||
contentRect = new Rect(); // empty
|
||||
}
|
||||
}
|
||||
|
||||
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
|
||||
return new ScreenInfo(contentRect, videoSize, rotated);
|
||||
}
|
||||
|
||||
private static String formatCrop(Rect rect) {
|
||||
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Size computeVideoSize(int w, int h, int maxSize) {
|
||||
// Compute the video size and the padding of the content inside this video.
|
||||
// Principle:
|
||||
// - scale down the great side of the screen to maxSize (if necessary);
|
||||
// - scale down the other side so that the aspect ratio is preserved;
|
||||
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
|
||||
w &= ~7; // in case it's not a multiple of 8
|
||||
h &= ~7;
|
||||
if (maxSize > 0) {
|
||||
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
||||
throw new AssertionError("Max size must be a multiple of 8");
|
||||
}
|
||||
boolean portrait = h > w;
|
||||
int major = portrait ? h : w;
|
||||
int minor = portrait ? w : h;
|
||||
if (major > maxSize) {
|
||||
int minorExact = minor * maxSize / major;
|
||||
// +4 to round the value to the nearest multiple of 8
|
||||
minor = (minorExact + 4) & ~7;
|
||||
major = maxSize;
|
||||
}
|
||||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
public Point getPhysicalPoint(Position position) {
|
||||
// it hides the field on purpose, to read it with a lock
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
|
||||
Size videoSize = screenInfo.getVideoSize();
|
||||
Size clientVideoSize = position.getScreenSize();
|
||||
if (!videoSize.equals(clientVideoSize)) {
|
||||
// The client sends a click relative to a video with wrong dimensions,
|
||||
// the device may have been rotated since the event was generated, so ignore the event
|
||||
return null;
|
||||
}
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
Point point = position.getPoint();
|
||||
int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
|
||||
int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
|
||||
return new Point(scaledX, scaledY);
|
||||
}
|
||||
|
||||
public static String getDeviceName() {
|
||||
return Build.MODEL;
|
||||
}
|
||||
|
||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
||||
}
|
||||
|
||||
public boolean isScreenOn() {
|
||||
return serviceManager.getPowerManager().isScreenOn();
|
||||
}
|
||||
|
||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
|
||||
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
|
||||
}
|
||||
|
||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||
this.rotationListener = rotationListener;
|
||||
}
|
||||
|
||||
static Rect flipRect(Rect crop) {
|
||||
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
||||
}
|
||||
}
|
20
server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java
Normal file
20
server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java
Normal file
|
@ -0,0 +1,20 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class DisplayInfo {
|
||||
private final Size size;
|
||||
private final int rotation;
|
||||
|
||||
public DisplayInfo(Size size, int rotation) {
|
||||
this.size = size;
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
public Size getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
}
|
||||
|
180
server/src/main/java/com/genymobile/scrcpy/EventController.java
Normal file
180
server/src/main/java/com/genymobile/scrcpy/EventController.java
Normal file
|
@ -0,0 +1,180 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class EventController {
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
|
||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||
|
||||
private long lastMouseDown;
|
||||
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
|
||||
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
|
||||
|
||||
public EventController(Device device, DesktopConnection connection) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
initPointer();
|
||||
}
|
||||
|
||||
private void initPointer() {
|
||||
MotionEvent.PointerProperties props = pointerProperties[0];
|
||||
props.id = 0;
|
||||
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.orientation = 0;
|
||||
coords.pressure = 1;
|
||||
coords.size = 1;
|
||||
}
|
||||
|
||||
private void setPointerCoords(Point point) {
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.x = point.x;
|
||||
coords.y = point.y;
|
||||
}
|
||||
|
||||
private void setScroll(int hScroll, int vScroll) {
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
}
|
||||
|
||||
public void control() throws IOException {
|
||||
// on start, turn screen on
|
||||
turnScreenOn();
|
||||
|
||||
while (true) {
|
||||
handleEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEvent() throws IOException {
|
||||
ControlEvent controlEvent = connection.receiveControlEvent();
|
||||
switch (controlEvent.getType()) {
|
||||
case ControlEvent.TYPE_KEYCODE:
|
||||
injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState());
|
||||
break;
|
||||
case ControlEvent.TYPE_TEXT:
|
||||
injectText(controlEvent.getText());
|
||||
break;
|
||||
case ControlEvent.TYPE_MOUSE:
|
||||
injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition());
|
||||
break;
|
||||
case ControlEvent.TYPE_SCROLL:
|
||||
injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll());
|
||||
break;
|
||||
case ControlEvent.TYPE_COMMAND:
|
||||
executeCommand(controlEvent.getAction());
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int metaState) {
|
||||
return injectKeyEvent(action, keycode, 0, metaState);
|
||||
}
|
||||
|
||||
private boolean injectChar(char c) {
|
||||
String decomposed = KeyComposition.decompose(c);
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
|
||||
KeyEvent[] events = charMap.getEvents(chars);
|
||||
if (events == null) {
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!injectEvent(event)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean injectText(String text) {
|
||||
for (char c : text.toCharArray()) {
|
||||
if (!injectChar(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean injectMouse(int action, int buttons, Position position) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
lastMouseDown = now;
|
||||
}
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
// ignore event
|
||||
return false;
|
||||
}
|
||||
setPointerCoords(point);
|
||||
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0,
|
||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
// ignore event
|
||||
return false;
|
||||
}
|
||||
setPointerCoords(point);
|
||||
setScroll(hScroll, vScroll);
|
||||
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0,
|
||||
0, InputDevice.SOURCE_MOUSE, 0);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
InputDevice.SOURCE_KEYBOARD);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int keyCode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
|
||||
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||
}
|
||||
|
||||
private boolean injectEvent(InputEvent event) {
|
||||
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean turnScreenOn() {
|
||||
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||
}
|
||||
|
||||
private boolean pressBackOrTurnScreenOn() {
|
||||
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
||||
return injectKeycode(keycode);
|
||||
}
|
||||
|
||||
private boolean executeCommand(int action) {
|
||||
switch (action) {
|
||||
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
|
||||
return pressBackOrTurnScreenOn();
|
||||
default:
|
||||
Ln.w("Unsupported command: " + action);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
31
server/src/main/java/com/genymobile/scrcpy/IO.java
Normal file
31
server/src/main/java/com/genymobile/scrcpy/IO.java
Normal file
|
@ -0,0 +1,31 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class IO {
|
||||
private IO() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
|
||||
while (from.hasRemaining()) {
|
||||
try {
|
||||
Os.write(fd, from);
|
||||
} catch (ErrnoException e) {
|
||||
if (e.errno != OsConstants.EINTR) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
|
||||
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
|
||||
}
|
||||
}
|
174
server/src/main/java/com/genymobile/scrcpy/KeyComposition.java
Normal file
174
server/src/main/java/com/genymobile/scrcpy/KeyComposition.java
Normal file
|
@ -0,0 +1,174 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Decompose accented characters.
|
||||
* <p>
|
||||
* For example, {@link #decompose(char) decompose('é')} returns {@code "\u0301e"}.
|
||||
* <p>
|
||||
* This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])}
|
||||
* KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
|
||||
* <p>
|
||||
* See <a href="https://source.android.com/devices/input/key-character-map-files#behaviors">diacritical dead key characters</a>.
|
||||
*/
|
||||
public final class KeyComposition {
|
||||
|
||||
private static final String KEY_DEAD_GRAVE = "\u0300";
|
||||
private static final String KEY_DEAD_ACUTE = "\u0301";
|
||||
private static final String KEY_DEAD_CIRCUMFLEX = "\u0302";
|
||||
private static final String KEY_DEAD_TILDE = "\u0303";
|
||||
private static final String KEY_DEAD_UMLAUT = "\u0308";
|
||||
|
||||
private static final Map<Character, String> COMPOSITION_MAP = createDecompositionMap();
|
||||
|
||||
private KeyComposition() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static String decompose(char c) {
|
||||
return COMPOSITION_MAP.get(c);
|
||||
}
|
||||
|
||||
private static String grave(char c) {
|
||||
return KEY_DEAD_GRAVE + c;
|
||||
}
|
||||
|
||||
private static String acute(char c) {
|
||||
return KEY_DEAD_ACUTE + c;
|
||||
}
|
||||
|
||||
private static String circumflex(char c) {
|
||||
return KEY_DEAD_CIRCUMFLEX + c;
|
||||
}
|
||||
|
||||
private static String tilde(char c) {
|
||||
return KEY_DEAD_TILDE + c;
|
||||
}
|
||||
|
||||
private static String umlaut(char c) {
|
||||
return KEY_DEAD_UMLAUT + c;
|
||||
}
|
||||
|
||||
private static Map<Character, String> createDecompositionMap() {
|
||||
Map<Character, String> map = new HashMap<>();
|
||||
map.put('À', grave('A'));
|
||||
map.put('È', grave('E'));
|
||||
map.put('Ì', grave('I'));
|
||||
map.put('Ò', grave('O'));
|
||||
map.put('Ù', grave('U'));
|
||||
map.put('à', grave('a'));
|
||||
map.put('è', grave('e'));
|
||||
map.put('ì', grave('i'));
|
||||
map.put('ò', grave('o'));
|
||||
map.put('ù', grave('u'));
|
||||
map.put('Ǹ', grave('N'));
|
||||
map.put('ǹ', grave('n'));
|
||||
map.put('Ẁ', grave('W'));
|
||||
map.put('ẁ', grave('w'));
|
||||
map.put('Ỳ', grave('Y'));
|
||||
map.put('ỳ', grave('y'));
|
||||
|
||||
map.put('Á', acute('A'));
|
||||
map.put('É', acute('E'));
|
||||
map.put('Í', acute('I'));
|
||||
map.put('Ó', acute('O'));
|
||||
map.put('Ú', acute('U'));
|
||||
map.put('Ý', acute('Y'));
|
||||
map.put('á', acute('a'));
|
||||
map.put('é', acute('e'));
|
||||
map.put('í', acute('i'));
|
||||
map.put('ó', acute('o'));
|
||||
map.put('ú', acute('u'));
|
||||
map.put('ý', acute('y'));
|
||||
map.put('Ć', acute('C'));
|
||||
map.put('ć', acute('c'));
|
||||
map.put('Ĺ', acute('L'));
|
||||
map.put('ĺ', acute('l'));
|
||||
map.put('Ń', acute('N'));
|
||||
map.put('ń', acute('n'));
|
||||
map.put('Ŕ', acute('R'));
|
||||
map.put('ŕ', acute('r'));
|
||||
map.put('Ś', acute('S'));
|
||||
map.put('ś', acute('s'));
|
||||
map.put('Ź', acute('Z'));
|
||||
map.put('ź', acute('z'));
|
||||
map.put('Ǵ', acute('G'));
|
||||
map.put('ǵ', acute('g'));
|
||||
map.put('Ḉ', acute('Ç'));
|
||||
map.put('ḉ', acute('ç'));
|
||||
map.put('Ḱ', acute('K'));
|
||||
map.put('ḱ', acute('k'));
|
||||
map.put('Ḿ', acute('M'));
|
||||
map.put('ḿ', acute('m'));
|
||||
map.put('Ṕ', acute('P'));
|
||||
map.put('ṕ', acute('p'));
|
||||
map.put('Ẃ', acute('W'));
|
||||
map.put('ẃ', acute('w'));
|
||||
|
||||
map.put('Â', circumflex('A'));
|
||||
map.put('Ê', circumflex('E'));
|
||||
map.put('Î', circumflex('I'));
|
||||
map.put('Ô', circumflex('O'));
|
||||
map.put('Û', circumflex('U'));
|
||||
map.put('â', circumflex('a'));
|
||||
map.put('ê', circumflex('e'));
|
||||
map.put('î', circumflex('i'));
|
||||
map.put('ô', circumflex('o'));
|
||||
map.put('û', circumflex('u'));
|
||||
map.put('Ĉ', circumflex('C'));
|
||||
map.put('ĉ', circumflex('c'));
|
||||
map.put('Ĝ', circumflex('G'));
|
||||
map.put('ĝ', circumflex('g'));
|
||||
map.put('Ĥ', circumflex('H'));
|
||||
map.put('ĥ', circumflex('h'));
|
||||
map.put('Ĵ', circumflex('J'));
|
||||
map.put('ĵ', circumflex('j'));
|
||||
map.put('Ŝ', circumflex('S'));
|
||||
map.put('ŝ', circumflex('s'));
|
||||
map.put('Ŵ', circumflex('W'));
|
||||
map.put('ŵ', circumflex('w'));
|
||||
map.put('Ŷ', circumflex('Y'));
|
||||
map.put('ŷ', circumflex('y'));
|
||||
map.put('Ẑ', circumflex('Z'));
|
||||
map.put('ẑ', circumflex('z'));
|
||||
|
||||
map.put('Ã', tilde('A'));
|
||||
map.put('Ñ', tilde('N'));
|
||||
map.put('Õ', tilde('O'));
|
||||
map.put('ã', tilde('a'));
|
||||
map.put('ñ', tilde('n'));
|
||||
map.put('õ', tilde('o'));
|
||||
map.put('Ĩ', tilde('I'));
|
||||
map.put('ĩ', tilde('i'));
|
||||
map.put('Ũ', tilde('U'));
|
||||
map.put('ũ', tilde('u'));
|
||||
map.put('Ẽ', tilde('E'));
|
||||
map.put('ẽ', tilde('e'));
|
||||
map.put('Ỹ', tilde('Y'));
|
||||
map.put('ỹ', tilde('y'));
|
||||
|
||||
map.put('Ä', umlaut('A'));
|
||||
map.put('Ë', umlaut('E'));
|
||||
map.put('Ï', umlaut('I'));
|
||||
map.put('Ö', umlaut('O'));
|
||||
map.put('Ü', umlaut('U'));
|
||||
map.put('ä', umlaut('a'));
|
||||
map.put('ë', umlaut('e'));
|
||||
map.put('ï', umlaut('i'));
|
||||
map.put('ö', umlaut('o'));
|
||||
map.put('ü', umlaut('u'));
|
||||
map.put('ÿ', umlaut('y'));
|
||||
map.put('Ÿ', umlaut('Y'));
|
||||
map.put('Ḧ', umlaut('H'));
|
||||
map.put('ḧ', umlaut('h'));
|
||||
map.put('Ẅ', umlaut('W'));
|
||||
map.put('ẅ', umlaut('w'));
|
||||
map.put('Ẍ', umlaut('X'));
|
||||
map.put('ẍ', umlaut('x'));
|
||||
map.put('ẗ', umlaut('t'));
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
58
server/src/main/java/com/genymobile/scrcpy/Ln.java
Normal file
58
server/src/main/java/com/genymobile/scrcpy/Ln.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* directly).
|
||||
*/
|
||||
public final class Ln {
|
||||
|
||||
private static final String TAG = "scrcpy";
|
||||
|
||||
enum Level {
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR;
|
||||
}
|
||||
|
||||
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
||||
|
||||
private Ln() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static boolean isEnabled(Level level) {
|
||||
return level.ordinal() >= THRESHOLD.ordinal();
|
||||
}
|
||||
|
||||
public static void d(String message) {
|
||||
if (isEnabled(Level.DEBUG)) {
|
||||
Log.d(TAG, message);
|
||||
System.out.println("DEBUG: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void i(String message) {
|
||||
if (isEnabled(Level.INFO)) {
|
||||
Log.i(TAG, message);
|
||||
System.out.println("INFO: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String message) {
|
||||
if (isEnabled(Level.WARN)) {
|
||||
Log.w(TAG, message);
|
||||
System.out.println("WARN: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.ERROR)) {
|
||||
Log.e(TAG, message, throwable);
|
||||
System.out.println("ERROR: " + message);
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
42
server/src/main/java/com/genymobile/scrcpy/Options.java
Normal file
42
server/src/main/java/com/genymobile/scrcpy/Options.java
Normal file
|
@ -0,0 +1,42 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
public class Options {
|
||||
private int maxSize;
|
||||
private int bitRate;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
|
||||
public int getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
}
|
||||
|
||||
public void setBitRate(int bitRate) {
|
||||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
return tunnelForward;
|
||||
}
|
||||
|
||||
public void setTunnelForward(boolean tunnelForward) {
|
||||
this.tunnelForward = tunnelForward;
|
||||
}
|
||||
|
||||
public Rect getCrop() {
|
||||
return crop;
|
||||
}
|
||||
|
||||
public void setCrop(Rect crop) {
|
||||
this.crop = crop;
|
||||
}
|
||||
}
|
54
server/src/main/java/com/genymobile/scrcpy/Position.java
Normal file
54
server/src/main/java/com/genymobile/scrcpy/Position.java
Normal file
|
@ -0,0 +1,54 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Point;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Position {
|
||||
private Point point;
|
||||
private Size screenSize;
|
||||
|
||||
public Position(Point point, Size screenSize) {
|
||||
this.point = point;
|
||||
this.screenSize = screenSize;
|
||||
}
|
||||
|
||||
public Position(int x, int y, int screenWidth, int screenHeight) {
|
||||
this(new Point(x, y), new Size(screenWidth, screenHeight));
|
||||
}
|
||||
|
||||
public Point getPoint() {
|
||||
return point;
|
||||
}
|
||||
|
||||
public Size getScreenSize() {
|
||||
return screenSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Position position = (Position) o;
|
||||
return Objects.equals(point, position.point)
|
||||
&& Objects.equals(screenSize, position.screenSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(point, screenSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Position{"
|
||||
+ "point=" + point
|
||||
+ ", screenSize=" + screenSize
|
||||
+ '}';
|
||||
}
|
||||
|
||||
}
|
148
server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
Normal file
148
server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
Normal file
|
@ -0,0 +1,148 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private static final int DEFAULT_FRAME_RATE = 60; // fps
|
||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||
|
||||
private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
|
||||
|
||||
private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
|
||||
private int bitRate;
|
||||
private int frameRate;
|
||||
private int iFrameInterval;
|
||||
|
||||
public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) {
|
||||
this.bitRate = bitRate;
|
||||
this.frameRate = frameRate;
|
||||
this.iFrameInterval = iFrameInterval;
|
||||
}
|
||||
|
||||
public ScreenEncoder(int bitRate) {
|
||||
this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
rotationChanged.set(true);
|
||||
}
|
||||
|
||||
public boolean consumeRotationChange() {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
MediaCodec codec = createCodec();
|
||||
IBinder display = createDisplay();
|
||||
Rect contentRect = device.getScreenInfo().getContentRect();
|
||||
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
|
||||
setSize(format, videoRect.width(), videoRect.height());
|
||||
configure(codec, format);
|
||||
Surface surface = codec.createInputSurface();
|
||||
setDisplaySurface(display, surface, contentRect, videoRect);
|
||||
codec.start();
|
||||
try {
|
||||
alive = encode(codec, fd);
|
||||
} finally {
|
||||
codec.stop();
|
||||
destroyDisplay(display);
|
||||
codec.release();
|
||||
surface.release();
|
||||
}
|
||||
} while (alive);
|
||||
} finally {
|
||||
device.setRotationListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
try {
|
||||
if (consumeRotationChange()) {
|
||||
// must restart encoding with new size
|
||||
break;
|
||||
}
|
||||
if (outputBufferId >= 0) {
|
||||
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
codec.releaseOutputBuffer(outputBufferId, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !eof;
|
||||
}
|
||||
|
||||
private static MediaCodec createCodec() throws IOException {
|
||||
return MediaCodec.createEncoderByType("video/avc");
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, "video/avc");
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
||||
// display the very first frame, and recover from bad quality when no new frames
|
||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
|
||||
return format;
|
||||
}
|
||||
|
||||
private static IBinder createDisplay() {
|
||||
return SurfaceControl.createDisplay("scrcpy", false);
|
||||
}
|
||||
|
||||
private static void configure(MediaCodec codec, MediaFormat format) {
|
||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
}
|
||||
|
||||
private static void setSize(MediaFormat format, int width, int height) {
|
||||
format.setInteger(MediaFormat.KEY_WIDTH, width);
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, height);
|
||||
}
|
||||
|
||||
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
|
||||
SurfaceControl.openTransaction();
|
||||
try {
|
||||
SurfaceControl.setDisplaySurface(display, surface);
|
||||
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
|
||||
SurfaceControl.setDisplayLayerStack(display, 0);
|
||||
} finally {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static void destroyDisplay(IBinder display) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
}
|
||||
}
|
31
server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
Normal file
31
server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
Normal file
|
@ -0,0 +1,31 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
public final class ScreenInfo {
|
||||
private final Rect contentRect; // device size, possibly cropped
|
||||
private final Size videoSize;
|
||||
private final boolean rotated;
|
||||
|
||||
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) {
|
||||
this.contentRect = contentRect;
|
||||
this.videoSize = videoSize;
|
||||
this.rotated = rotated;
|
||||
}
|
||||
|
||||
public Rect getContentRect() {
|
||||
return contentRect;
|
||||
}
|
||||
|
||||
public Size getVideoSize() {
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
public ScreenInfo withRotation(int rotation) {
|
||||
boolean newRotated = (rotation & 1) != 0;
|
||||
if (rotated == newRotated) {
|
||||
return this;
|
||||
}
|
||||
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
|
||||
}
|
||||
}
|
104
server/src/main/java/com/genymobile/scrcpy/Server.java
Normal file
104
server/src/main/java/com/genymobile/scrcpy/Server.java
Normal file
|
@ -0,0 +1,104 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class Server {
|
||||
|
||||
private Server() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
final Device device = new Device(options);
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
|
||||
|
||||
// asynchronous
|
||||
startEventController(device, connection);
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen(device, connection.getFd());
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void startEventController(final Device device, final DesktopConnection connection) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
new EventController(device, connection).control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Event controller stopped");
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Options createOptions(String... args) {
|
||||
Options options = new Options();
|
||||
if (args.length < 1) {
|
||||
return options;
|
||||
}
|
||||
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
|
||||
if (args.length < 2) {
|
||||
return options;
|
||||
}
|
||||
int bitRate = Integer.parseInt(args[1]);
|
||||
options.setBitRate(bitRate);
|
||||
|
||||
if (args.length < 3) {
|
||||
return options;
|
||||
}
|
||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||
boolean tunnelForward = Boolean.parseBoolean(args[2]);
|
||||
options.setTunnelForward(tunnelForward);
|
||||
|
||||
if (args.length < 4) {
|
||||
return options;
|
||||
}
|
||||
Rect crop = parseCrop(args[3]);
|
||||
options.setCrop(crop);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static Rect parseCrop(String crop) {
|
||||
if (crop.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// input format: "width:height:x:y"
|
||||
String[] tokens = crop.split(":");
|
||||
if (tokens.length != 4) {
|
||||
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
|
||||
}
|
||||
int width = Integer.parseInt(tokens[0]);
|
||||
int height = Integer.parseInt(tokens[1]);
|
||||
int x = Integer.parseInt(tokens[2]);
|
||||
int y = Integer.parseInt(tokens[3]);
|
||||
return new Rect(x, y, x + width, y + height);
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
}
|
||||
});
|
||||
|
||||
Options options = createOptions(args);
|
||||
scrcpy(options);
|
||||
}
|
||||
}
|
57
server/src/main/java/com/genymobile/scrcpy/Size.java
Normal file
57
server/src/main/java/com/genymobile/scrcpy/Size.java
Normal file
|
@ -0,0 +1,57 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Size {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
public Size(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public Size rotate() {
|
||||
return new Size(height, width);
|
||||
}
|
||||
|
||||
public Rect toRect() {
|
||||
return new Rect(0, 0, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Size size = (Size) o;
|
||||
return width == size.width
|
||||
&& height == size.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Size{"
|
||||
+ "width=" + width
|
||||
+ ", height=" + height
|
||||
+ '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.DisplayInfo;
|
||||
import com.genymobile.scrcpy.Size;
|
||||
|
||||
import android.os.IInterface;
|
||||
|
||||
public final class DisplayManager {
|
||||
private final IInterface manager;
|
||||
|
||||
public DisplayManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public DisplayInfo getDisplayInfo() {
|
||||
try {
|
||||
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
|
||||
Class<?> cls = displayInfo.getClass();
|
||||
// width and height already take the rotation into account
|
||||
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
|
||||
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
|
||||
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
|
||||
return new DisplayInfo(new Size(width, height), rotation);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.InputEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class InputManager {
|
||||
|
||||
public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||
|
||||
private final IInterface manager;
|
||||
private final Method injectInputEventMethod;
|
||||
|
||||
public InputManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
try {
|
||||
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||
try {
|
||||
return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class PowerManager {
|
||||
private final IInterface manager;
|
||||
private final Method isScreenOnMethod;
|
||||
|
||||
public PowerManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
try {
|
||||
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isScreenOn() {
|
||||
try {
|
||||
return (Boolean) isScreenOnMethod.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
public final class ServiceManager {
|
||||
private final Method getServiceMethod;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private DisplayManager displayManager;
|
||||
private InputManager inputManager;
|
||||
private PowerManager powerManager;
|
||||
|
||||
public ServiceManager() {
|
||||
try {
|
||||
getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private IInterface getService(String service, String type) {
|
||||
try {
|
||||
IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
|
||||
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
|
||||
return (IInterface) asInterfaceMethod.invoke(null, binder);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WindowManager getWindowManager() {
|
||||
if (windowManager == null) {
|
||||
windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
|
||||
}
|
||||
return windowManager;
|
||||
}
|
||||
|
||||
public DisplayManager getDisplayManager() {
|
||||
if (displayManager == null) {
|
||||
displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
|
||||
}
|
||||
return displayManager;
|
||||
}
|
||||
|
||||
public InputManager getInputManager() {
|
||||
if (inputManager == null) {
|
||||
inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
|
||||
}
|
||||
return inputManager;
|
||||
}
|
||||
|
||||
public PowerManager getPowerManager() {
|
||||
if (powerManager == null) {
|
||||
powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
|
||||
}
|
||||
return powerManager;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
public final class SurfaceControl {
|
||||
|
||||
private static final Class<?> CLASS;
|
||||
|
||||
static {
|
||||
try {
|
||||
CLASS = Class.forName("android.view.SurfaceControl");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SurfaceControl() {
|
||||
// only static methods
|
||||
}
|
||||
|
||||
public static void openTransaction() {
|
||||
try {
|
||||
CLASS.getMethod("openTransaction").invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeTransaction() {
|
||||
try {
|
||||
CLASS.getMethod("closeTransaction").invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class)
|
||||
.invoke(null, displayToken, orientation, layerStackRect, displayRect);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDisplaySurface(IBinder displayToken, Surface surface) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static IBinder createDisplay(String name, boolean secure) {
|
||||
try {
|
||||
return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void destroyDisplay(IBinder displayToken) {
|
||||
try {
|
||||
CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
public final class WindowManager {
|
||||
private final IInterface manager;
|
||||
|
||||
public WindowManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
try {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// method changed since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
|
||||
return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
|
||||
try {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// display parameter added since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class ControlEventReaderTest {
|
||||
|
||||
@Test
|
||||
public void testParseKeycodeEvent() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlEvent.TYPE_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlEvent event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseTextEvent() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlEvent.TYPE_TEXT);
|
||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlEvent event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
|
||||
Assert.assertEquals("testé", event.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseLongTextEvent() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlEvent.TYPE_TEXT);
|
||||
byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH];
|
||||
Arrays.fill(text, (byte) 'a');
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlEvent event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
|
||||
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseMouseEvent() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlEvent.TYPE_KEYCODE);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlEvent event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiEvents() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
|
||||
dos.writeByte(ControlEvent.TYPE_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
dos.writeByte(ControlEvent.TYPE_KEYCODE);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
|
||||
ControlEvent event = reader.next();
|
||||
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
|
||||
event = reader.next();
|
||||
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialEvents() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
|
||||
dos.writeByte(ControlEvent.TYPE_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
dos.writeByte(ControlEvent.TYPE_KEYCODE);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
|
||||
ControlEvent event = reader.next();
|
||||
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
|
||||
event = reader.next();
|
||||
Assert.assertNull(event); // the event is not complete
|
||||
|
||||
bos.reset();
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
|
||||
// the event is now complete
|
||||
event = reader.next();
|
||||
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue