Convert server to an Android project
To simplify the device server-side build, use gradle to create an APK, even if we use it as a simple jar, by running its main() method.
This commit is contained in:
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);
|
||||
}
|
||||
95
server/src/main/java/com/genymobile/scrcpy/ControlEvent.java
Normal file
95
server/src/main/java/com/genymobile/scrcpy/ControlEvent.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
/**
|
||||
* Union of all supported event types, identified by their {@code type}.
|
||||
*/
|
||||
public 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;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_*
|
||||
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 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,113 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
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 final byte[] rawBuffer = new byte[128];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
private final byte[] textBuffer = new byte[32];
|
||||
|
||||
public ControlEventReader() {
|
||||
// invariant: the buffer is always in "get" mode
|
||||
buffer.limit(0);
|
||||
}
|
||||
|
||||
public boolean isFull() {
|
||||
return buffer.remaining() == rawBuffer.length;
|
||||
}
|
||||
|
||||
public boolean 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) {
|
||||
return false;
|
||||
}
|
||||
buffer.position(head + r);
|
||||
buffer.flip();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ControlEvent next() {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
int savedPosition = buffer.position();
|
||||
|
||||
int type = buffer.get();
|
||||
switch (type) {
|
||||
case ControlEvent.TYPE_KEYCODE: {
|
||||
if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) {
|
||||
break;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
int keycode = buffer.getInt();
|
||||
int metaState = buffer.getInt();
|
||||
return ControlEvent.createKeycodeControlEvent(action, keycode, metaState);
|
||||
}
|
||||
case ControlEvent.TYPE_TEXT: {
|
||||
if (buffer.remaining() < 1) {
|
||||
break;
|
||||
}
|
||||
int len = toUnsigned(buffer.get());
|
||||
if (buffer.remaining() < len) {
|
||||
break;
|
||||
}
|
||||
buffer.get(textBuffer, 0, len);
|
||||
String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
||||
return ControlEvent.createTextControlEvent(text);
|
||||
}
|
||||
case ControlEvent.TYPE_MOUSE: {
|
||||
if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) {
|
||||
break;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
int buttons = buffer.getInt();
|
||||
Position position = readPosition(buffer);
|
||||
return ControlEvent.createMotionControlEvent(action, buttons, position);
|
||||
}
|
||||
case ControlEvent.TYPE_SCROLL: {
|
||||
if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) {
|
||||
break;
|
||||
}
|
||||
Position position = readPosition(buffer);
|
||||
int hScroll = buffer.getInt();
|
||||
int vScroll = buffer.getInt();
|
||||
return ControlEvent.createScrollControlEvent(position, hScroll, vScroll);
|
||||
}
|
||||
default:
|
||||
Ln.w("Unknown event type: " + type);
|
||||
}
|
||||
|
||||
// failure, reset savedPosition
|
||||
buffer.position(savedPosition);
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static int toUnsigned(short value) {
|
||||
return value & 0xffff;
|
||||
}
|
||||
|
||||
private static int toUnsigned(byte value) {
|
||||
return value & 0xff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public 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 OutputStream outputStream;
|
||||
|
||||
private final ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
private DesktopConnection(LocalSocket socket) throws IOException {
|
||||
this.socket = socket;
|
||||
inputStream = socket.getInputStream();
|
||||
outputStream = socket.getOutputStream();
|
||||
}
|
||||
|
||||
private static LocalSocket connect(String abstractName) throws IOException {
|
||||
LocalSocket localSocket = new LocalSocket();
|
||||
localSocket.connect(new LocalSocketAddress(abstractName));
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device) throws IOException {
|
||||
LocalSocket 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();
|
||||
}
|
||||
|
||||
private void send(String deviceName, int width, int height) throws IOException {
|
||||
assert width < 0x10000 : "width may not be stored on 16 bits";
|
||||
assert height < 0x10000 : "height may not be stored on 16 bits";
|
||||
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;
|
||||
outputStream.write(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public void sendVideoStream(byte[] videoStreamBuffer, int len) throws IOException {
|
||||
outputStream.write(videoStreamBuffer, 0, len);
|
||||
}
|
||||
|
||||
public ControlEvent receiveControlEvent() throws IOException {
|
||||
ControlEvent event = reader.next();
|
||||
while (event == null) {
|
||||
reader.readFrom(inputStream);
|
||||
event = reader.next();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
}
|
||||
115
server/src/main/java/com/genymobile/scrcpy/Device.java
Normal file
115
server/src/main/java/com/genymobile/scrcpy/Device.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
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.getMaximumSize());
|
||||
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(int maximumSize) {
|
||||
// Compute the video size and the padding of the content inside this video.
|
||||
// Principle:
|
||||
// - scale down the great side of the screen to maximumSize (if necessary);
|
||||
// - scale down the other side so that the aspect ratio is preserved;
|
||||
// - ceil this value to the next multiple of 8 (H.264 only accepts multiples of 8)
|
||||
// - this may introduce black bands, so store the padding (typically a few pixels)
|
||||
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
|
||||
boolean rotated = (displayInfo.getRotation() & 1) != 0;
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
int w = deviceSize.getWidth();
|
||||
int h = deviceSize.getHeight();
|
||||
int padding = 0;
|
||||
if (maximumSize > 0) {
|
||||
assert maximumSize % 8 == 0;
|
||||
boolean portrait = h > w;
|
||||
int major = portrait ? h : w;
|
||||
int minor = portrait ? w : h;
|
||||
if (major > maximumSize) {
|
||||
int minorExact = minor * maximumSize / major;
|
||||
// +7 to ceil the value on rounding to the next multiple of 8,
|
||||
// so that any necessary black bands to keep the aspect ratio are added to the smallest dimension
|
||||
minor = (minorExact + 7) & ~7;
|
||||
major = maximumSize;
|
||||
padding = minor - minorExact;
|
||||
}
|
||||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
return new ScreenInfo(deviceSize, new Size(w, h), padding, rotated);
|
||||
}
|
||||
|
||||
public Point getPhysicalPoint(Position position) {
|
||||
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;
|
||||
}
|
||||
Size deviceSize = screenInfo.getDeviceSize();
|
||||
int xPadding = screenInfo.getXPadding();
|
||||
int yPadding = screenInfo.getYPadding();
|
||||
int contentWidth = videoSize.getWidth() - xPadding;
|
||||
int contentHeight = videoSize.getHeight() - yPadding;
|
||||
Point point = position.getPoint();
|
||||
int x = point.getX() - xPadding / 2;
|
||||
int y = point.getY() - yPadding / 2;
|
||||
if (x < 0 || x >= contentWidth || y < 0 || y >= contentHeight) {
|
||||
// out of screen
|
||||
return null;
|
||||
}
|
||||
int scaledX = x * deviceSize.getWidth() / videoSize.getWidth();
|
||||
int scaledY = y * deviceSize.getHeight() / videoSize.getHeight();
|
||||
return new Point(scaledX, scaledY);
|
||||
}
|
||||
|
||||
public static String getDeviceName() {
|
||||
return Build.MODEL;
|
||||
}
|
||||
|
||||
public InputManager getInputManager() {
|
||||
return serviceManager.getInputManager();
|
||||
}
|
||||
|
||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
|
||||
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
|
||||
}
|
||||
|
||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||
this.rotationListener = rotationListener;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
139
server/src/main/java/com/genymobile/scrcpy/EventController.java
Normal file
139
server/src/main/java/com/genymobile/scrcpy/EventController.java
Normal file
@@ -0,0 +1,139 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
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 com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EventController {
|
||||
|
||||
private final Device device;
|
||||
private final InputManager inputManager;
|
||||
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;
|
||||
inputManager = device.getInputManager();
|
||||
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;
|
||||
coords.toolMajor = 1;
|
||||
coords.toolMinor = 1;
|
||||
coords.touchMajor = 1;
|
||||
coords.touchMinor = 1;
|
||||
}
|
||||
|
||||
private void setPointerCoords(Point point) {
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.x = point.getX();
|
||||
coords.y = point.getY();
|
||||
}
|
||||
|
||||
private void setScroll(int hScroll, int vScroll) {
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.setAxisValue(MotionEvent.AXIS_SCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
}
|
||||
|
||||
public void control() throws IOException {
|
||||
while (handleEvent());
|
||||
}
|
||||
|
||||
private boolean handleEvent() throws IOException {
|
||||
ControlEvent controlEvent = connection.receiveControlEvent();
|
||||
if (controlEvent == null) {
|
||||
return false;
|
||||
}
|
||||
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());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int metaState) {
|
||||
return injectKeyEvent(action, keycode, 0, metaState);
|
||||
}
|
||||
|
||||
private boolean injectText(String text) {
|
||||
KeyEvent[] events = charMap.getEvents(text.toCharArray());
|
||||
if (events == null) {
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!injectEvent(event)) {
|
||||
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 injectEvent(InputEvent event) {
|
||||
return inputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
}
|
||||
}
|
||||
37
server/src/main/java/com/genymobile/scrcpy/Ln.java
Normal file
37
server/src/main/java/com/genymobile/scrcpy/Ln.java
Normal file
@@ -0,0 +1,37 @@
|
||||
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 class Ln {
|
||||
|
||||
private static final String TAG = "scrcpy";
|
||||
|
||||
private Ln() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void d(String message) {
|
||||
Log.d(TAG, message);
|
||||
System.out.println("DEBUG: " + message);
|
||||
}
|
||||
|
||||
public static void i(String message) {
|
||||
Log.i(TAG, message);
|
||||
System.out.println("INFO: " + message);
|
||||
}
|
||||
|
||||
public static void w(String message) {
|
||||
Log.w(TAG, message);
|
||||
System.out.println("WARN: " + message);
|
||||
}
|
||||
|
||||
public static void e(String message, Throwable throwable) {
|
||||
Log.e(TAG, message, throwable);
|
||||
System.out.println("ERROR: " + message);
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
13
server/src/main/java/com/genymobile/scrcpy/Options.java
Normal file
13
server/src/main/java/com/genymobile/scrcpy/Options.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public class Options {
|
||||
private int maximumSize;
|
||||
|
||||
public int getMaximumSize() {
|
||||
return maximumSize;
|
||||
}
|
||||
|
||||
public void setMaximumSize(int maximumSize) {
|
||||
this.maximumSize = maximumSize;
|
||||
}
|
||||
}
|
||||
43
server/src/main/java/com/genymobile/scrcpy/Point.java
Normal file
43
server/src/main/java/com/genymobile/scrcpy/Point.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Point {
|
||||
private int x;
|
||||
private int y;
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Point point = (Point) o;
|
||||
return x == point.x &&
|
||||
y == point.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Point{" +
|
||||
"x=" + x +
|
||||
", y=" + y +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
48
server/src/main/java/com/genymobile/scrcpy/Position.java
Normal file
48
server/src/main/java/com/genymobile/scrcpy/Position.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
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 +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
58
server/src/main/java/com/genymobile/scrcpy/ScrCpyServer.java
Normal file
58
server/src/main/java/com/genymobile/scrcpy/ScrCpyServer.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ScrCpyServer {
|
||||
|
||||
private static final String TAG = "scrcpy";
|
||||
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
final Device device = new Device(options);
|
||||
try (DesktopConnection connection = DesktopConnection.open(device)) {
|
||||
final ScreenStreamer streamer = new ScreenStreamer(device, connection);
|
||||
device.setRotationListener(new Device.RotationListener() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
streamer.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// asynchronous
|
||||
startEventController(device, connection);
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
streamer.streamScreen();
|
||||
} catch (IOException e) {
|
||||
Ln.e("Screen streaming interrupted", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
Options options = new Options();
|
||||
if (args.length > 0) {
|
||||
int maximumSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
|
||||
options.setMaximumSize(maximumSize);
|
||||
}
|
||||
try {
|
||||
scrcpy(options);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
Normal file
39
server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class ScreenInfo {
|
||||
private final Size deviceSize;
|
||||
private final Size videoSize;
|
||||
private final int padding; // padding inside the video stream, along the smallest dimension
|
||||
private final boolean rotated;
|
||||
|
||||
public ScreenInfo(Size deviceSize, Size videoSize, int padding, boolean rotated) {
|
||||
this.deviceSize = deviceSize;
|
||||
this.videoSize = videoSize;
|
||||
this.padding = padding;
|
||||
this.rotated = rotated;
|
||||
}
|
||||
|
||||
public Size getDeviceSize() {
|
||||
return deviceSize;
|
||||
}
|
||||
|
||||
public Size getVideoSize() {
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
public int getXPadding() {
|
||||
return videoSize.getWidth() < videoSize.getHeight() ? padding : 0;
|
||||
}
|
||||
|
||||
public int getYPadding() {
|
||||
return videoSize.getHeight() < videoSize.getWidth() ? padding : 0;
|
||||
}
|
||||
|
||||
public ScreenInfo withRotation(int rotation) {
|
||||
boolean newRotated = (rotation & 1) != 0;
|
||||
if (rotated == newRotated) {
|
||||
return this;
|
||||
}
|
||||
return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), padding, newRotated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
|
||||
public class ScreenStreamer {
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private ScreenStreamerSession currentStreamer; // protected by 'this'
|
||||
|
||||
public ScreenStreamer(Device device, DesktopConnection connection) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private synchronized ScreenStreamerSession newScreenStreamerSession() {
|
||||
currentStreamer = new ScreenStreamerSession(device, connection);
|
||||
return currentStreamer;
|
||||
}
|
||||
|
||||
public void streamScreen() throws IOException {
|
||||
while (true) {
|
||||
try {
|
||||
ScreenStreamerSession screenStreamer = newScreenStreamerSession();
|
||||
screenStreamer.streamScreen();
|
||||
} catch (InterruptedIOException e) {
|
||||
// the current screenrecord process has probably been killed due to reset(), start a new one without failing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
if (currentStreamer != null) {
|
||||
// it will stop the blocking call to streamScreen(), so a new streamer will be started
|
||||
currentStreamer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScreenStreamerSession {
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private Process screenRecordProcess; // protected by 'this'
|
||||
private final AtomicBoolean stopped = new AtomicBoolean();
|
||||
private final byte[] buffer = new byte[0x10000];
|
||||
|
||||
public ScreenStreamerSession(Device device, DesktopConnection connection) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public void streamScreen() throws IOException {
|
||||
// screenrecord may not record more than 3 minutes, so restart it on EOF
|
||||
while (!stopped.get() && streamScreenOnce()) ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts screenrecord once and relay its output to the desktop connection.
|
||||
*
|
||||
* @return {@code true} if EOF is reached, {@code false} otherwise (i.e. requested to stop).
|
||||
* @throws IOException if an I/O error occurred
|
||||
*/
|
||||
private boolean streamScreenOnce() throws IOException {
|
||||
Ln.d("Recording...");
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
Process process = startScreenRecord(videoSize);
|
||||
setCurrentProcess(process);
|
||||
InputStream inputStream = process.getInputStream();
|
||||
int r;
|
||||
while ((r = inputStream.read(buffer)) != -1 && !stopped.get()) {
|
||||
connection.sendVideoStream(buffer, r);
|
||||
}
|
||||
return r != -1;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// let the thread stop itself without breaking the video stream
|
||||
stopped.set(true);
|
||||
killCurrentProcess();
|
||||
}
|
||||
|
||||
private static Process startScreenRecord(Size videoSize) throws IOException {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("screenrecord");
|
||||
command.add("--output-format=h264");
|
||||
command.add("--size=" + videoSize.getWidth() + "x" + videoSize.getHeight());
|
||||
command.add("-");
|
||||
Process process = new ProcessBuilder(command).start();
|
||||
process.getOutputStream().close();
|
||||
return process;
|
||||
}
|
||||
|
||||
private synchronized void setCurrentProcess(Process screenRecordProcess) {
|
||||
this.screenRecordProcess = screenRecordProcess;
|
||||
}
|
||||
|
||||
private synchronized void killCurrentProcess() {
|
||||
if (screenRecordProcess != null) {
|
||||
screenRecordProcess.destroy();
|
||||
screenRecordProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
server/src/main/java/com/genymobile/scrcpy/Size.java
Normal file
47
server/src/main/java/com/genymobile/scrcpy/Size.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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 android.os.IInterface;
|
||||
|
||||
import com.genymobile.scrcpy.DisplayInfo;
|
||||
import com.genymobile.scrcpy.Size;
|
||||
|
||||
public 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 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,40 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ServiceManager {
|
||||
private final Method getServiceMethod;
|
||||
|
||||
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() {
|
||||
return new WindowManager(getService("window", "android.view.IWindowManager"));
|
||||
}
|
||||
|
||||
public DisplayManager getDisplayManager() {
|
||||
return new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
|
||||
}
|
||||
|
||||
public InputManager getInputManager() {
|
||||
return new InputManager(getService("input", "android.hardware.input.IInputManager"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user