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:
Romain Vimont
2018-01-29 17:06:44 +01:00
parent 89f6a3cfe7
commit b67907e24e
36 changed files with 390 additions and 121 deletions

View File

@@ -0,0 +1,2 @@
<!-- not a real Android application, it is run by app_process manually -->
<manifest package="com.genymobile.scrcpy"/>

View 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);
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}

View 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 +
'}';
}
}

View 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 +
'}';
}
}

View 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;
}
}
}

View 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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View 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 +
'}';
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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"));
}
}

View File

@@ -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);
}
}
}