mirror of
https://github.com/barry-ran/QtScrcpy.git
synced 2025-09-26 11:19:06 +00:00
feat: update server
This commit is contained in:
parent
5e1cc1b44f
commit
f2641816d1
29 changed files with 1204 additions and 288 deletions
8
server/.gitignore
vendored
Normal file
8
server/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
|
@ -1,13 +1,13 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 31
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 31
|
||||||
versionCode 16
|
versionCode 12100
|
||||||
versionName "1.14"
|
versionName "1.21"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -20,7 +20,7 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
||||||
|
|
88
server/build_without_gradle.sh
Normal file
88
server/build_without_gradle.sh
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# This script generates the scrcpy binary "manually" (without gradle).
|
||||||
|
#
|
||||||
|
# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and
|
||||||
|
# ANDROID_BUILD_TOOLS environment variables).
|
||||||
|
#
|
||||||
|
# Then execute:
|
||||||
|
#
|
||||||
|
# BUILD_DIR=my_build_dir ./build_without_gradle.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRCPY_DEBUG=false
|
||||||
|
SCRCPY_VERSION_NAME=1.21
|
||||||
|
|
||||||
|
PLATFORM_VERSION=31
|
||||||
|
PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION}
|
||||||
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0}
|
||||||
|
|
||||||
|
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||||
|
CLASSES_DIR="$BUILD_DIR/classes"
|
||||||
|
SERVER_DIR=$(dirname "$0")
|
||||||
|
SERVER_BINARY=scrcpy-server
|
||||||
|
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
||||||
|
|
||||||
|
echo "Platform: android-$PLATFORM"
|
||||||
|
echo "Build-tools: $BUILD_TOOLS"
|
||||||
|
echo "Build dir: $BUILD_DIR"
|
||||||
|
|
||||||
|
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||||
|
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
|
||||||
|
|
||||||
|
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public final class BuildConfig {
|
||||||
|
public static final boolean DEBUG = $SCRCPY_DEBUG;
|
||||||
|
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Generating java from aidl..."
|
||||||
|
cd "$SERVER_DIR/src/main/aidl"
|
||||||
|
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
|
||||||
|
android/view/IRotationWatcher.aidl
|
||||||
|
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
|
||||||
|
android/content/IOnPrimaryClipChangedListener.aidl
|
||||||
|
|
||||||
|
echo "Compiling java sources..."
|
||||||
|
cd ../java
|
||||||
|
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
|
||||||
|
-source 1.8 -target 1.8 \
|
||||||
|
com/genymobile/scrcpy/*.java \
|
||||||
|
com/genymobile/scrcpy/wrappers/*.java
|
||||||
|
|
||||||
|
echo "Dexing..."
|
||||||
|
cd "$CLASSES_DIR"
|
||||||
|
|
||||||
|
if [[ $PLATFORM_VERSION -lt 31 ]]
|
||||||
|
then
|
||||||
|
# use dx
|
||||||
|
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
|
||||||
|
--output "$BUILD_DIR/classes.dex" \
|
||||||
|
android/view/*.class \
|
||||||
|
android/content/*.class \
|
||||||
|
com/genymobile/scrcpy/*.class \
|
||||||
|
com/genymobile/scrcpy/wrappers/*.class
|
||||||
|
|
||||||
|
echo "Archiving..."
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
jar cvf "$SERVER_BINARY" classes.dex
|
||||||
|
rm -rf classes.dex classes
|
||||||
|
else
|
||||||
|
# use d8
|
||||||
|
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \
|
||||||
|
--output "$BUILD_DIR/classes.zip" \
|
||||||
|
android/view/*.class \
|
||||||
|
android/content/*.class \
|
||||||
|
com/genymobile/scrcpy/*.class \
|
||||||
|
com/genymobile/scrcpy/wrappers/*.class
|
||||||
|
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
mv classes.zip "$SERVER_BINARY"
|
||||||
|
rm -rf classes
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
|
25
server/meson.build
Normal file
25
server/meson.build
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# It may be useful to use a prebuilt server, so that no Android SDK is required
|
||||||
|
# to build. If the 'prebuilt_server' option is set, just copy the file as is.
|
||||||
|
prebuilt_server = get_option('prebuilt_server')
|
||||||
|
if prebuilt_server == ''
|
||||||
|
custom_target('scrcpy-server',
|
||||||
|
# gradle is responsible for tracking source changes
|
||||||
|
build_by_default: true,
|
||||||
|
build_always_stale: true,
|
||||||
|
output: 'scrcpy-server',
|
||||||
|
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
|
||||||
|
console: true,
|
||||||
|
install: true,
|
||||||
|
install_dir: 'share/scrcpy')
|
||||||
|
else
|
||||||
|
if not prebuilt_server.startswith('/')
|
||||||
|
# relative path needs some trick
|
||||||
|
prebuilt_server = meson.source_root() + '/' + prebuilt_server
|
||||||
|
endif
|
||||||
|
custom_target('scrcpy-server-prebuilt',
|
||||||
|
input: prebuilt_server,
|
||||||
|
output: 'scrcpy-server',
|
||||||
|
command: ['cp', '@INPUT@', '@OUTPUT@'],
|
||||||
|
install: true,
|
||||||
|
install_dir: 'share/scrcpy')
|
||||||
|
endif
|
|
@ -1,8 +1,11 @@
|
||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -15,22 +18,123 @@ public final class CleanUp {
|
||||||
|
|
||||||
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
||||||
|
|
||||||
|
// A simple struct to be passed from the main process to the cleanup process
|
||||||
|
public static class Config implements Parcelable {
|
||||||
|
|
||||||
|
public static final Creator<Config> CREATOR = new Creator<Config>() {
|
||||||
|
@Override
|
||||||
|
public Config createFromParcel(Parcel in) {
|
||||||
|
return new Config(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Config[] newArray(int size) {
|
||||||
|
return new Config[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
|
||||||
|
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
|
||||||
|
private static final int FLAG_POWER_OFF_SCREEN = 4;
|
||||||
|
|
||||||
|
private int displayId;
|
||||||
|
|
||||||
|
// Restore the value (between 0 and 7), -1 to not restore
|
||||||
|
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
|
||||||
|
private int restoreStayOn = -1;
|
||||||
|
|
||||||
|
private boolean disableShowTouches;
|
||||||
|
private boolean restoreNormalPowerMode;
|
||||||
|
private boolean powerOffScreen;
|
||||||
|
|
||||||
|
public Config() {
|
||||||
|
// Default constructor, the fields are initialized by CleanUp.configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Config(Parcel in) {
|
||||||
|
displayId = in.readInt();
|
||||||
|
restoreStayOn = in.readInt();
|
||||||
|
byte options = in.readByte();
|
||||||
|
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
|
||||||
|
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
|
||||||
|
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeInt(displayId);
|
||||||
|
dest.writeInt(restoreStayOn);
|
||||||
|
byte options = 0;
|
||||||
|
if (disableShowTouches) {
|
||||||
|
options |= FLAG_DISABLE_SHOW_TOUCHES;
|
||||||
|
}
|
||||||
|
if (restoreNormalPowerMode) {
|
||||||
|
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
|
||||||
|
}
|
||||||
|
if (powerOffScreen) {
|
||||||
|
options |= FLAG_POWER_OFF_SCREEN;
|
||||||
|
}
|
||||||
|
dest.writeByte(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasWork() {
|
||||||
|
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] serialize() {
|
||||||
|
Parcel parcel = Parcel.obtain();
|
||||||
|
writeToParcel(parcel, 0);
|
||||||
|
byte[] bytes = parcel.marshall();
|
||||||
|
parcel.recycle();
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Config deserialize(byte[] bytes) {
|
||||||
|
Parcel parcel = Parcel.obtain();
|
||||||
|
parcel.unmarshall(bytes, 0, bytes.length);
|
||||||
|
parcel.setDataPosition(0);
|
||||||
|
return CREATOR.createFromParcel(parcel);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Config fromBase64(String base64) {
|
||||||
|
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
|
||||||
|
return deserialize(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toBase64() {
|
||||||
|
byte[] bytes = serialize();
|
||||||
|
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CleanUp() {
|
private CleanUp() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException {
|
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
|
||||||
boolean needProcess = disableShowTouches || restoreStayOn != -1;
|
throws IOException {
|
||||||
if (needProcess) {
|
Config config = new Config();
|
||||||
startProcess(disableShowTouches, restoreStayOn);
|
config.displayId = displayId;
|
||||||
|
config.disableShowTouches = disableShowTouches;
|
||||||
|
config.restoreStayOn = restoreStayOn;
|
||||||
|
config.restoreNormalPowerMode = restoreNormalPowerMode;
|
||||||
|
config.powerOffScreen = powerOffScreen;
|
||||||
|
|
||||||
|
if (config.hasWork()) {
|
||||||
|
startProcess(config);
|
||||||
} else {
|
} else {
|
||||||
// There is no additional clean up to do when scrcpy dies
|
// There is no additional clean up to do when scrcpy dies
|
||||||
unlinkSelf();
|
unlinkSelf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException {
|
private static void startProcess(Config config) throws IOException {
|
||||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)};
|
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
|
||||||
|
|
||||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||||
builder.environment().put("CLASSPATH", SERVER_PATH);
|
builder.environment().put("CLASSPATH", SERVER_PATH);
|
||||||
|
@ -57,20 +161,36 @@ public final class CleanUp {
|
||||||
|
|
||||||
Ln.i("Cleaning up");
|
Ln.i("Cleaning up");
|
||||||
|
|
||||||
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
|
Config config = Config.fromBase64(args[0]);
|
||||||
int restoreStayOn = Integer.parseInt(args[1]);
|
|
||||||
|
|
||||||
if (disableShowTouches || restoreStayOn != -1) {
|
if (config.disableShowTouches || config.restoreStayOn != -1) {
|
||||||
ServiceManager serviceManager = new ServiceManager();
|
ServiceManager serviceManager = new ServiceManager();
|
||||||
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
|
Settings settings = new Settings(serviceManager);
|
||||||
if (disableShowTouches) {
|
if (config.disableShowTouches) {
|
||||||
Ln.i("Disabling \"show touches\"");
|
Ln.i("Disabling \"show touches\"");
|
||||||
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
|
try {
|
||||||
|
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.e("Could not restore \"show_touches\"", e);
|
||||||
}
|
}
|
||||||
if (restoreStayOn != -1) {
|
}
|
||||||
|
if (config.restoreStayOn != -1) {
|
||||||
Ln.i("Restoring \"stay awake\"");
|
Ln.i("Restoring \"stay awake\"");
|
||||||
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
|
try {
|
||||||
}
|
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Device.isScreenOn()) {
|
||||||
|
if (config.powerOffScreen) {
|
||||||
|
Ln.i("Power off screen");
|
||||||
|
Device.powerOffScreen(config.displayId);
|
||||||
|
} else if (config.restoreNormalPowerMode) {
|
||||||
|
Ln.i("Restoring normal power mode");
|
||||||
|
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ public class CodecOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<CodecOption> parse(String codecOptions) {
|
public static List<CodecOption> parse(String codecOptions) {
|
||||||
if ("-".equals(codecOptions)) {
|
if (codecOptions.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
33
server/src/main/java/com/genymobile/scrcpy/Command.java
Normal file
33
server/src/main/java/com/genymobile/scrcpy/Command.java
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public final class Command {
|
||||||
|
private Command() {
|
||||||
|
// not instantiable
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void exec(String... cmd) throws IOException, InterruptedException {
|
||||||
|
Process process = Runtime.getRuntime().exec(cmd);
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String execReadLine(String... cmd) throws IOException, InterruptedException {
|
||||||
|
String result = null;
|
||||||
|
Process process = Runtime.getRuntime().exec(cmd);
|
||||||
|
Scanner scanner = new Scanner(process.getInputStream());
|
||||||
|
if (scanner.hasNextLine()) {
|
||||||
|
result = scanner.nextLine();
|
||||||
|
}
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,13 +11,18 @@ public final class ControlMessage {
|
||||||
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
|
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
|
||||||
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
|
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
|
||||||
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
|
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
|
||||||
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
|
public static final int TYPE_EXPAND_SETTINGS_PANEL = 6;
|
||||||
public static final int TYPE_GET_CLIPBOARD = 7;
|
public static final int TYPE_COLLAPSE_PANELS = 7;
|
||||||
public static final int TYPE_SET_CLIPBOARD = 8;
|
public static final int TYPE_GET_CLIPBOARD = 8;
|
||||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
public static final int TYPE_SET_CLIPBOARD = 9;
|
||||||
public static final int TYPE_ROTATE_DEVICE = 10;
|
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
|
||||||
|
public static final int TYPE_ROTATE_DEVICE = 11;
|
||||||
|
|
||||||
public static final int FLAGS_PASTE = 1;
|
public static final long SEQUENCE_INVALID = 0;
|
||||||
|
|
||||||
|
public static final int COPY_KEY_NONE = 0;
|
||||||
|
public static final int COPY_KEY_COPY = 1;
|
||||||
|
public static final int COPY_KEY_CUT = 2;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
|
@ -30,16 +35,20 @@ public final class ControlMessage {
|
||||||
private Position position;
|
private Position position;
|
||||||
private int hScroll;
|
private int hScroll;
|
||||||
private int vScroll;
|
private int vScroll;
|
||||||
private int flags;
|
private int copyKey;
|
||||||
|
private boolean paste;
|
||||||
|
private int repeat;
|
||||||
|
private long sequence;
|
||||||
|
|
||||||
private ControlMessage() {
|
private ControlMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
|
public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_INJECT_KEYCODE;
|
msg.type = TYPE_INJECT_KEYCODE;
|
||||||
msg.action = action;
|
msg.action = action;
|
||||||
msg.keycode = keycode;
|
msg.keycode = keycode;
|
||||||
|
msg.repeat = repeat;
|
||||||
msg.metaState = metaState;
|
msg.metaState = metaState;
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
@ -71,13 +80,26 @@ public final class ControlMessage {
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ControlMessage createSetClipboard(String text, boolean paste) {
|
public static ControlMessage createBackOrScreenOn(int action) {
|
||||||
|
ControlMessage msg = new ControlMessage();
|
||||||
|
msg.type = TYPE_BACK_OR_SCREEN_ON;
|
||||||
|
msg.action = action;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ControlMessage createGetClipboard(int copyKey) {
|
||||||
|
ControlMessage msg = new ControlMessage();
|
||||||
|
msg.type = TYPE_GET_CLIPBOARD;
|
||||||
|
msg.copyKey = copyKey;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) {
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_SET_CLIPBOARD;
|
msg.type = TYPE_SET_CLIPBOARD;
|
||||||
|
msg.sequence = sequence;
|
||||||
msg.text = text;
|
msg.text = text;
|
||||||
if (paste) {
|
msg.paste = paste;
|
||||||
msg.flags = FLAGS_PASTE;
|
|
||||||
}
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +163,19 @@ public final class ControlMessage {
|
||||||
return vScroll;
|
return vScroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getFlags() {
|
public int getCopyKey() {
|
||||||
return flags;
|
return copyKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getPaste() {
|
||||||
|
return paste;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRepeat() {
|
||||||
|
return repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSequence() {
|
||||||
|
return sequence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,20 +8,21 @@ import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class ControlMessageReader {
|
public class ControlMessageReader {
|
||||||
|
|
||||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
|
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||||
|
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||||
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
|
static final int GET_CLIPBOARD_LENGTH = 1;
|
||||||
|
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
|
||||||
|
|
||||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length)
|
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||||
|
|
||||||
|
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
|
||||||
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
||||||
|
|
||||||
private static final int RAW_BUFFER_SIZE = 4096;
|
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||||
|
|
||||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
|
||||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||||
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
|
|
||||||
|
|
||||||
public ControlMessageReader() {
|
public ControlMessageReader() {
|
||||||
// invariant: the buffer is always in "get" mode
|
// invariant: the buffer is always in "get" mode
|
||||||
|
@ -67,16 +68,21 @@ public class ControlMessageReader {
|
||||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||||
msg = parseInjectScrollEvent();
|
msg = parseInjectScrollEvent();
|
||||||
break;
|
break;
|
||||||
|
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||||
|
msg = parseBackOrScreenOnEvent();
|
||||||
|
break;
|
||||||
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
|
msg = parseGetClipboard();
|
||||||
|
break;
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
msg = parseSetClipboard();
|
msg = parseSetClipboard();
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
msg = parseSetScreenPowerMode();
|
msg = parseSetScreenPowerMode();
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
msg = ControlMessage.createEmpty(type);
|
msg = ControlMessage.createEmpty(type);
|
||||||
break;
|
break;
|
||||||
|
@ -99,20 +105,23 @@ public class ControlMessageReader {
|
||||||
}
|
}
|
||||||
int action = toUnsigned(buffer.get());
|
int action = toUnsigned(buffer.get());
|
||||||
int keycode = buffer.getInt();
|
int keycode = buffer.getInt();
|
||||||
|
int repeat = buffer.getInt();
|
||||||
int metaState = buffer.getInt();
|
int metaState = buffer.getInt();
|
||||||
return ControlMessage.createInjectKeycode(action, keycode, metaState);
|
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String parseString() {
|
private String parseString() {
|
||||||
if (buffer.remaining() < 2) {
|
if (buffer.remaining() < 4) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int len = toUnsigned(buffer.getShort());
|
int len = buffer.getInt();
|
||||||
if (buffer.remaining() < len) {
|
if (buffer.remaining() < len) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
buffer.get(textBuffer, 0, len);
|
int position = buffer.position();
|
||||||
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
// Move the buffer position to consume the text
|
||||||
|
buffer.position(position + len);
|
||||||
|
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseInjectText() {
|
private ControlMessage parseInjectText() {
|
||||||
|
@ -148,16 +157,33 @@ public class ControlMessageReader {
|
||||||
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
|
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ControlMessage parseBackOrScreenOnEvent() {
|
||||||
|
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int action = toUnsigned(buffer.get());
|
||||||
|
return ControlMessage.createBackOrScreenOn(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControlMessage parseGetClipboard() {
|
||||||
|
if (buffer.remaining() < GET_CLIPBOARD_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int copyKey = toUnsigned(buffer.get());
|
||||||
|
return ControlMessage.createGetClipboard(copyKey);
|
||||||
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetClipboard() {
|
private ControlMessage parseSetClipboard() {
|
||||||
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
boolean parse = buffer.get() != 0;
|
long sequence = buffer.getLong();
|
||||||
|
boolean paste = buffer.get() != 0;
|
||||||
String text = parseString();
|
String text = parseString();
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ControlMessage.createSetClipboard(text, parse);
|
return ControlMessage.createSetClipboard(sequence, text, paste);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetScreenPowerMode() {
|
private ControlMessage parseSetScreenPowerMode() {
|
||||||
|
|
|
@ -8,14 +8,20 @@ import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class Controller {
|
public class Controller {
|
||||||
|
|
||||||
private static final int DEVICE_ID_VIRTUAL = -1;
|
private static final int DEFAULT_DEVICE_ID = 0;
|
||||||
|
|
||||||
|
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
private final Device device;
|
private final Device device;
|
||||||
private final DesktopConnection connection;
|
private final DesktopConnection connection;
|
||||||
private final DeviceMessageSender sender;
|
private final DeviceMessageSender sender;
|
||||||
|
private final boolean clipboardAutosync;
|
||||||
|
|
||||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||||
|
|
||||||
|
@ -24,9 +30,12 @@ public class Controller {
|
||||||
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
||||||
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||||
|
|
||||||
public Controller(Device device, DesktopConnection connection) {
|
private boolean keepPowerModeOff;
|
||||||
|
|
||||||
|
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
this.clipboardAutosync = clipboardAutosync;
|
||||||
initPointers();
|
initPointers();
|
||||||
sender = new DeviceMessageSender(connection);
|
sender = new DeviceMessageSender(connection);
|
||||||
}
|
}
|
||||||
|
@ -38,7 +47,7 @@ public class Controller {
|
||||||
|
|
||||||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||||
coords.orientation = 0;
|
coords.orientation = 0;
|
||||||
coords.size = 1;
|
coords.size = 0;
|
||||||
|
|
||||||
pointerProperties[i] = props;
|
pointerProperties[i] = props;
|
||||||
pointerCoords[i] = coords;
|
pointerCoords[i] = coords;
|
||||||
|
@ -47,8 +56,8 @@ public class Controller {
|
||||||
|
|
||||||
public void control() throws IOException {
|
public void control() throws IOException {
|
||||||
// on start, power on the device
|
// on start, power on the device
|
||||||
if (!device.isScreenOn()) {
|
if (!Device.isScreenOn()) {
|
||||||
device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||||
|
|
||||||
// dirty hack
|
// dirty hack
|
||||||
// After POWER is injected, the device is powered on asynchronously.
|
// After POWER is injected, the device is powered on asynchronously.
|
||||||
|
@ -74,7 +83,7 @@ public class Controller {
|
||||||
switch (msg.getType()) {
|
switch (msg.getType()) {
|
||||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
|
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_TEXT:
|
case ControlMessage.TYPE_INJECT_TEXT:
|
||||||
|
@ -94,44 +103,47 @@ public class Controller {
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
pressBackOrTurnScreenOn();
|
pressBackOrTurnScreenOn(msg.getAction());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
device.expandNotificationPanel();
|
Device.expandNotificationPanel();
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||||
device.collapsePanels();
|
Device.expandSettingsPanel();
|
||||||
|
break;
|
||||||
|
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||||
|
Device.collapsePanels();
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
String clipboardText = device.getClipboardText();
|
getClipboard(msg.getCopyKey());
|
||||||
if (clipboardText != null) {
|
|
||||||
sender.pushClipboardText(clipboardText);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||||
setClipboard(msg.getText(), paste);
|
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
int mode = msg.getAction();
|
int mode = msg.getAction();
|
||||||
boolean setPowerModeOk = device.setScreenPowerMode(mode);
|
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
|
||||||
if (setPowerModeOk) {
|
if (setPowerModeOk) {
|
||||||
|
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
device.rotateDevice();
|
Device.rotateDevice();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectKeycode(int action, int keycode, int metaState) {
|
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||||
return device.injectKeyEvent(action, keycode, 0, metaState);
|
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||||
|
schedulePowerModeOff();
|
||||||
|
}
|
||||||
|
return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectChar(char c) {
|
private boolean injectChar(char c) {
|
||||||
|
@ -142,7 +154,7 @@ public class Controller {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (KeyEvent event : events) {
|
for (KeyEvent event : events) {
|
||||||
if (!device.injectEvent(event)) {
|
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,7 +178,7 @@ public class Controller {
|
||||||
|
|
||||||
Point point = device.getPhysicalPoint(position);
|
Point point = device.getPhysicalPoint(position);
|
||||||
if (point == null) {
|
if (point == null) {
|
||||||
// ignore event
|
Ln.w("Ignore touch event, it was generated for a different device size");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,10 +207,18 @@ public class Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right-click and middle-click only work if the source is a mouse
|
||||||
|
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
|
||||||
|
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
|
||||||
|
if (source != InputDevice.SOURCE_MOUSE) {
|
||||||
|
// Buttons must not be set for touch events
|
||||||
|
buttons = 0;
|
||||||
|
}
|
||||||
|
|
||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
0);
|
||||||
return device.injectEvent(event);
|
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||||
|
@ -219,17 +239,62 @@ public class Controller {
|
||||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||||
|
|
||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
InputDevice.SOURCE_MOUSE, 0);
|
||||||
return device.injectEvent(event);
|
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pressBackOrTurnScreenOn() {
|
/**
|
||||||
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
* Schedule a call to set power mode to off after a small delay.
|
||||||
return device.injectKeycode(keycode);
|
*/
|
||||||
|
private static void schedulePowerModeOff() {
|
||||||
|
EXECUTOR.schedule(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Ln.i("Forcing screen off");
|
||||||
|
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||||
|
}
|
||||||
|
}, 200, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean setClipboard(String text, boolean paste) {
|
private boolean pressBackOrTurnScreenOn(int action) {
|
||||||
|
if (Device.isScreenOn()) {
|
||||||
|
return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen is off
|
||||||
|
// Only press POWER on ACTION_DOWN
|
||||||
|
if (action != KeyEvent.ACTION_DOWN) {
|
||||||
|
// do nothing,
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepPowerModeOff) {
|
||||||
|
schedulePowerModeOff();
|
||||||
|
}
|
||||||
|
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getClipboard(int copyKey) {
|
||||||
|
// On Android >= 7, press the COPY or CUT key if requested
|
||||||
|
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||||
|
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
|
||||||
|
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
|
||||||
|
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in
|
||||||
|
// particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than
|
||||||
|
// copying an old clipboard content.
|
||||||
|
if (!clipboardAutosync) {
|
||||||
|
String clipboardText = Device.getClipboardText();
|
||||||
|
if (clipboardText != null) {
|
||||||
|
sender.pushClipboardText(clipboardText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setClipboard(String text, boolean paste, long sequence) {
|
||||||
boolean ok = device.setClipboardText(text);
|
boolean ok = device.setClipboardText(text);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
Ln.i("Device clipboard set");
|
Ln.i("Device clipboard set");
|
||||||
|
@ -237,7 +302,12 @@ public class Controller {
|
||||||
|
|
||||||
// On Android >= 7, also press the PASTE key if requested
|
// On Android >= 7, also press the PASTE key if requested
|
||||||
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||||
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
|
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
||||||
|
// Acknowledgement requested
|
||||||
|
sender.pushAckClipboard(sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok;
|
return ok;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
|
@ -24,6 +24,16 @@ public final class Device {
|
||||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||||
|
|
||||||
|
public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
|
||||||
|
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
|
||||||
|
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
|
||||||
|
|
||||||
|
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||||
|
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||||
|
|
||||||
|
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
||||||
|
private static final Settings SETTINGS = new Settings(SERVICE_MANAGER);
|
||||||
|
|
||||||
public interface RotationListener {
|
public interface RotationListener {
|
||||||
void onRotationChanged(int rotation);
|
void onRotationChanged(int rotation);
|
||||||
}
|
}
|
||||||
|
@ -32,8 +42,6 @@ public final class Device {
|
||||||
void onClipboardTextChanged(String text);
|
void onClipboardTextChanged(String text);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ServiceManager serviceManager = new ServiceManager();
|
|
||||||
|
|
||||||
private ScreenInfo screenInfo;
|
private ScreenInfo screenInfo;
|
||||||
private RotationListener rotationListener;
|
private RotationListener rotationListener;
|
||||||
private ClipboardListener clipboardListener;
|
private ClipboardListener clipboardListener;
|
||||||
|
@ -53,18 +61,18 @@ public final class Device {
|
||||||
|
|
||||||
public Device(Options options) {
|
public Device(Options options) {
|
||||||
displayId = options.getDisplayId();
|
displayId = options.getDisplayId();
|
||||||
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
|
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId);
|
||||||
if (displayInfo == null) {
|
if (displayInfo == null) {
|
||||||
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
|
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds();
|
||||||
throw new InvalidDisplayIdException(displayId, displayIds);
|
throw new InvalidDisplayIdException(displayId, displayIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
int displayInfoFlags = displayInfo.getFlags();
|
int displayInfoFlags = displayInfo.getFlags();
|
||||||
|
|
||||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
|
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation());
|
||||||
layerStack = displayInfo.getLayerStack();
|
layerStack = displayInfo.getLayerStack();
|
||||||
|
|
||||||
serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
|
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||||
@Override
|
@Override
|
||||||
public void onRotationChanged(int rotation) {
|
public void onRotationChanged(int rotation) {
|
||||||
synchronized (Device.this) {
|
synchronized (Device.this) {
|
||||||
|
@ -78,9 +86,11 @@ public final class Device {
|
||||||
}
|
}
|
||||||
}, displayId);
|
}, displayId);
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl() && options.getClipboardAutosync()) {
|
||||||
// If control is enabled, synchronize Android clipboard to the computer automatically
|
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
|
||||||
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||||
|
if (clipboardManager != null) {
|
||||||
|
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||||
@Override
|
@Override
|
||||||
public void dispatchPrimaryClipChanged() {
|
public void dispatchPrimaryClipChanged() {
|
||||||
if (isSettingClipboard.get()) {
|
if (isSettingClipboard.get()) {
|
||||||
|
@ -97,6 +107,9 @@ public final class Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Ln.w("No clipboard manager, copy-paste between device and computer will not work");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||||
|
@ -147,12 +160,16 @@ public final class Device {
|
||||||
return Build.MODEL;
|
return Build.MODEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean supportsInputEvents(int displayId) {
|
||||||
|
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean supportsInputEvents() {
|
public boolean supportsInputEvents() {
|
||||||
return supportsInputEvents;
|
return supportsInputEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectEvent(InputEvent inputEvent, int mode) {
|
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
|
||||||
if (!supportsInputEvents()) {
|
if (!supportsInputEvents(displayId)) {
|
||||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,26 +177,35 @@ public final class Device {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectEvent(InputEvent event) {
|
public boolean injectEvent(InputEvent event, int injectMode) {
|
||||||
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
return injectEvent(event, displayId, injectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
|
||||||
long now = SystemClock.uptimeMillis();
|
long now = SystemClock.uptimeMillis();
|
||||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||||
InputDevice.SOURCE_KEYBOARD);
|
InputDevice.SOURCE_KEYBOARD);
|
||||||
return injectEvent(event);
|
return injectEvent(event, displayId, injectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectKeycode(int keyCode) {
|
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
|
||||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isScreenOn() {
|
public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) {
|
||||||
return serviceManager.getPowerManager().isScreenOn();
|
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode)
|
||||||
|
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean pressReleaseKeycode(int keyCode, int injectMode) {
|
||||||
|
return pressReleaseKeycode(keyCode, displayId, injectMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isScreenOn() {
|
||||||
|
return SERVICE_MANAGER.getPowerManager().isScreenOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||||
|
@ -190,16 +216,24 @@ public final class Device {
|
||||||
this.clipboardListener = clipboardListener;
|
this.clipboardListener = clipboardListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandNotificationPanel() {
|
public static void expandNotificationPanel() {
|
||||||
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void collapsePanels() {
|
public static void expandSettingsPanel() {
|
||||||
serviceManager.getStatusBarManager().collapsePanels();
|
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClipboardText() {
|
public static void collapsePanels() {
|
||||||
CharSequence s = serviceManager.getClipboardManager().getText();
|
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClipboardText() {
|
||||||
|
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||||
|
if (clipboardManager == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
CharSequence s = clipboardManager.getText();
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -207,16 +241,30 @@ public final class Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean setClipboardText(String text) {
|
public boolean setClipboardText(String text) {
|
||||||
|
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||||
|
if (clipboardManager == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentClipboard = getClipboardText();
|
||||||
|
if (currentClipboard != null && currentClipboard.equals(text)) {
|
||||||
|
// The clipboard already contains the requested text.
|
||||||
|
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
|
||||||
|
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
|
||||||
|
// problem, do not explicitly set the clipboard text if it already contains the expected content.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
isSettingClipboard.set(true);
|
isSettingClipboard.set(true);
|
||||||
boolean ok = serviceManager.getClipboardManager().setText(text);
|
boolean ok = clipboardManager.setText(text);
|
||||||
isSettingClipboard.set(false);
|
isSettingClipboard.set(false);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
* @param mode one of the {@code POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public boolean setScreenPowerMode(int mode) {
|
public static boolean setScreenPowerMode(int mode) {
|
||||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
Ln.e("Could not get built-in display");
|
Ln.e("Could not get built-in display");
|
||||||
|
@ -225,11 +273,18 @@ public final class Device {
|
||||||
return SurfaceControl.setDisplayPowerMode(d, mode);
|
return SurfaceControl.setDisplayPowerMode(d, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean powerOffScreen(int displayId) {
|
||||||
|
if (!isScreenOn()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
||||||
*/
|
*/
|
||||||
public void rotateDevice() {
|
public static void rotateDevice() {
|
||||||
WindowManager wm = serviceManager.getWindowManager();
|
WindowManager wm = SERVICE_MANAGER.getWindowManager();
|
||||||
|
|
||||||
boolean accelerometerRotation = !wm.isRotationFrozen();
|
boolean accelerometerRotation = !wm.isRotationFrozen();
|
||||||
|
|
||||||
|
@ -246,7 +301,7 @@ public final class Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentProvider createSettingsProvider() {
|
public static Settings getSettings() {
|
||||||
return serviceManager.getActivityManager().createSettingsProvider();
|
return SETTINGS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,13 @@ package com.genymobile.scrcpy;
|
||||||
public final class DeviceMessage {
|
public final class DeviceMessage {
|
||||||
|
|
||||||
public static final int TYPE_CLIPBOARD = 0;
|
public static final int TYPE_CLIPBOARD = 0;
|
||||||
|
public static final int TYPE_ACK_CLIPBOARD = 1;
|
||||||
|
|
||||||
|
public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
|
private long sequence;
|
||||||
|
|
||||||
private DeviceMessage() {
|
private DeviceMessage() {
|
||||||
}
|
}
|
||||||
|
@ -17,6 +21,13 @@ public final class DeviceMessage {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DeviceMessage createAckClipboard(long sequence) {
|
||||||
|
DeviceMessage event = new DeviceMessage();
|
||||||
|
event.type = TYPE_ACK_CLIPBOARD;
|
||||||
|
event.sequence = sequence;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
public int getType() {
|
public int getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
@ -24,4 +35,8 @@ public final class DeviceMessage {
|
||||||
public String getText() {
|
public String getText() {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getSequence() {
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ public final class DeviceMessageSender {
|
||||||
|
|
||||||
private String clipboardText;
|
private String clipboardText;
|
||||||
|
|
||||||
|
private long ack;
|
||||||
|
|
||||||
public DeviceMessageSender(DesktopConnection connection) {
|
public DeviceMessageSender(DesktopConnection connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
}
|
}
|
||||||
|
@ -17,18 +19,34 @@ public final class DeviceMessageSender {
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void pushAckClipboard(long sequence) {
|
||||||
|
ack = sequence;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
public void loop() throws IOException, InterruptedException {
|
public void loop() throws IOException, InterruptedException {
|
||||||
while (true) {
|
while (true) {
|
||||||
String text;
|
String text;
|
||||||
|
long sequence;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
while (clipboardText == null) {
|
while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) {
|
||||||
wait();
|
wait();
|
||||||
}
|
}
|
||||||
text = clipboardText;
|
text = clipboardText;
|
||||||
clipboardText = null;
|
clipboardText = null;
|
||||||
|
|
||||||
|
sequence = ack;
|
||||||
|
ack = DeviceMessage.SEQUENCE_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sequence != DeviceMessage.SEQUENCE_INVALID) {
|
||||||
|
DeviceMessage event = DeviceMessage.createAckClipboard(sequence);
|
||||||
|
connection.sendDeviceMessage(event);
|
||||||
|
}
|
||||||
|
if (text != null) {
|
||||||
DeviceMessage event = DeviceMessage.createClipboard(text);
|
DeviceMessage event = DeviceMessage.createClipboard(text);
|
||||||
connection.sendDeviceMessage(event);
|
connection.sendDeviceMessage(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,24 +7,28 @@ import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class DeviceMessageWriter {
|
public class DeviceMessageWriter {
|
||||||
|
|
||||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||||
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3;
|
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
|
||||||
|
|
||||||
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE];
|
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||||
|
|
||||||
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);
|
buffer.put((byte) msg.getType());
|
||||||
switch (msg.getType()) {
|
switch (msg.getType()) {
|
||||||
case DeviceMessage.TYPE_CLIPBOARD:
|
case DeviceMessage.TYPE_CLIPBOARD:
|
||||||
String text = msg.getText();
|
String text = msg.getText();
|
||||||
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
||||||
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
||||||
buffer.putShort((short) len);
|
buffer.putInt(len);
|
||||||
buffer.put(raw, 0, len);
|
buffer.put(raw, 0, len);
|
||||||
output.write(rawBuffer, 0, buffer.position());
|
output.write(rawBuffer, 0, buffer.position());
|
||||||
break;
|
break;
|
||||||
|
case DeviceMessage.TYPE_ACK_CLIPBOARD:
|
||||||
|
buffer.putLong(msg.getSequence());
|
||||||
|
output.write(rawBuffer, 0, buffer.position());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Ln.w("Unknown device message: " + msg.getType());
|
Ln.w("Unknown device message: " + msg.getType());
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
|
||||||
|
public class InvalidEncoderException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final MediaCodecInfo[] availableEncoders;
|
||||||
|
|
||||||
|
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
|
||||||
|
super("There is no encoder having name '" + name + '"');
|
||||||
|
this.name = name;
|
||||||
|
this.availableEncoders = availableEncoders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaCodecInfo[] getAvailableEncoders() {
|
||||||
|
return availableEncoders;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ public final class Ln {
|
||||||
private static final String PREFIX = "[server] ";
|
private static final String PREFIX = "[server] ";
|
||||||
|
|
||||||
enum Level {
|
enum Level {
|
||||||
DEBUG, INFO, WARN, ERROR
|
VERBOSE, DEBUG, INFO, WARN, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Level threshold = Level.INFO;
|
private static Level threshold = Level.INFO;
|
||||||
|
@ -36,6 +36,13 @@ public final class Ln {
|
||||||
return level.ordinal() >= threshold.ordinal();
|
return level.ordinal() >= threshold.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void v(String message) {
|
||||||
|
if (isEnabled(Level.VERBOSE)) {
|
||||||
|
Log.v(TAG, message);
|
||||||
|
System.out.println(PREFIX + "VERBOSE: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void d(String message) {
|
public static void d(String message) {
|
||||||
if (isEnabled(Level.DEBUG)) {
|
if (isEnabled(Level.DEBUG)) {
|
||||||
Log.d(TAG, message);
|
Log.d(TAG, message);
|
||||||
|
@ -50,12 +57,19 @@ public final class Ln {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void w(String message) {
|
public static void w(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.WARN)) {
|
if (isEnabled(Level.WARN)) {
|
||||||
Log.w(TAG, message);
|
Log.w(TAG, message, throwable);
|
||||||
System.out.println(PREFIX + "WARN: " + message);
|
System.out.println(PREFIX + "WARN: " + message);
|
||||||
|
if (throwable != null) {
|
||||||
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void w(String message) {
|
||||||
|
w(message, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static void e(String message, Throwable throwable) {
|
public static void e(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.ERROR)) {
|
if (isEnabled(Level.ERROR)) {
|
||||||
|
|
|
@ -2,20 +2,25 @@ package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class Options {
|
public class Options {
|
||||||
private Ln.Level logLevel;
|
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private int bitRate;
|
private int bitRate = 8000000;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private int lockedVideoOrientation;
|
private int lockVideoOrientation = -1;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
private Rect crop;
|
private Rect crop;
|
||||||
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||||
private boolean control;
|
private boolean control = true;
|
||||||
private int displayId;
|
private int displayId;
|
||||||
private boolean showTouches;
|
private boolean showTouches;
|
||||||
private boolean stayAwake;
|
private boolean stayAwake;
|
||||||
private String codecOptions;
|
private List<CodecOption> codecOptions;
|
||||||
|
private String encoderName;
|
||||||
|
private boolean powerOffScreenOnClose;
|
||||||
|
private boolean clipboardAutosync = true;
|
||||||
|
|
||||||
public Ln.Level getLogLevel() {
|
public Ln.Level getLogLevel() {
|
||||||
return logLevel;
|
return logLevel;
|
||||||
|
@ -49,12 +54,12 @@ public class Options {
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLockedVideoOrientation() {
|
public int getLockVideoOrientation() {
|
||||||
return lockedVideoOrientation;
|
return lockVideoOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLockedVideoOrientation(int lockedVideoOrientation) {
|
public void setLockVideoOrientation(int lockVideoOrientation) {
|
||||||
this.lockedVideoOrientation = lockedVideoOrientation;
|
this.lockVideoOrientation = lockVideoOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTunnelForward() {
|
public boolean isTunnelForward() {
|
||||||
|
@ -113,11 +118,35 @@ public class Options {
|
||||||
this.stayAwake = stayAwake;
|
this.stayAwake = stayAwake;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCodecOptions() {
|
public List<CodecOption> getCodecOptions() {
|
||||||
return codecOptions;
|
return codecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCodecOptions(String codecOptions) {
|
public void setCodecOptions(List<CodecOption> codecOptions) {
|
||||||
this.codecOptions = codecOptions;
|
this.codecOptions = codecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getEncoderName() {
|
||||||
|
return encoderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncoderName(String encoderName) {
|
||||||
|
this.encoderName = encoderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
|
||||||
|
this.powerOffScreenOnClose = powerOffScreenOnClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getPowerOffScreenOnClose() {
|
||||||
|
return this.powerOffScreenOnClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getClipboardAutosync() {
|
||||||
|
return clipboardAutosync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClipboardAutosync(boolean clipboardAutosync) {
|
||||||
|
this.clipboardAutosync = clipboardAutosync;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,17 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaCodecList;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
@ -26,17 +30,19 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
|
private String encoderName;
|
||||||
private List<CodecOption> codecOptions;
|
private List<CodecOption> codecOptions;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private boolean sendFrameMeta;
|
private boolean sendFrameMeta;
|
||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
this.codecOptions = codecOptions;
|
this.codecOptions = codecOptions;
|
||||||
|
this.encoderName = encoderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -50,17 +56,13 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
Workarounds.prepareMainLooper();
|
Workarounds.prepareMainLooper();
|
||||||
|
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||||
try {
|
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||||
internalStreamScreen(device, fd);
|
// <https://github.com/Genymobile/scrcpy/issues/2656>
|
||||||
} catch (NullPointerException e) {
|
|
||||||
// Retry with workarounds enabled:
|
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/365>
|
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/940>
|
|
||||||
Ln.d("Applying workarounds to avoid NullPointerException");
|
|
||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
internalStreamScreen(device, fd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internalStreamScreen(device, fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
|
@ -69,7 +71,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
MediaCodec codec = createCodec();
|
MediaCodec codec = createCodec(encoderName);
|
||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
ScreenInfo screenInfo = device.getScreenInfo();
|
ScreenInfo screenInfo = device.getScreenInfo();
|
||||||
Rect contentRect = screenInfo.getContentRect();
|
Rect contentRect = screenInfo.getContentRect();
|
||||||
|
@ -150,8 +152,30 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
IO.writeFully(fd, headerBuffer);
|
IO.writeFully(fd, headerBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaCodec createCodec() throws IOException {
|
private static MediaCodecInfo[] listEncoders() {
|
||||||
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
List<MediaCodecInfo> result = new ArrayList<>();
|
||||||
|
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
|
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
||||||
|
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||||
|
result.add(codecInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaCodec createCodec(String encoderName) throws IOException {
|
||||||
|
if (encoderName != null) {
|
||||||
|
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||||
|
try {
|
||||||
|
return MediaCodec.createByCodecName(encoderName);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
MediaCodecInfo[] encoders = listEncoders();
|
||||||
|
throw new InvalidEncoderException(encoderName, encoders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
|
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||||
|
return codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||||
|
@ -198,7 +222,11 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IBinder createDisplay() {
|
private static IBinder createDisplay() {
|
||||||
return SurfaceControl.createDisplay("scrcpy", true);
|
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
|
||||||
|
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
|
||||||
|
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S"
|
||||||
|
.equals(Build.VERSION.CODENAME));
|
||||||
|
return SurfaceControl.createDisplay("scrcpy", secure);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void configure(MediaCodec codec, MediaFormat format) {
|
private static void configure(MediaCodec codec, MediaFormat format) {
|
||||||
|
|
|
@ -82,6 +82,12 @@ public final class ScreenInfo {
|
||||||
|
|
||||||
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||||
int rotation = displayInfo.getRotation();
|
int rotation = displayInfo.getRotation();
|
||||||
|
|
||||||
|
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
|
||||||
|
// The user requested to lock the video orientation to the current orientation
|
||||||
|
lockedVideoOrientation = rotation;
|
||||||
|
}
|
||||||
|
|
||||||
Size deviceSize = displayInfo.getSize();
|
Size deviceSize = displayInfo.getSize();
|
||||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||||
if (crop != null) {
|
if (crop != null) {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
import android.os.BatteryManager;
|
import android.os.BatteryManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
|
@ -18,24 +17,25 @@ public final class Server {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void scrcpy(Options options) throws IOException {
|
private static void initAndCleanUp(Options options) {
|
||||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
|
||||||
final Device device = new Device(options);
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
|
|
||||||
|
|
||||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||||
int restoreStayOn = -1;
|
int restoreStayOn = -1;
|
||||||
if (options.getShowTouches() || options.getStayAwake()) {
|
if (options.getShowTouches() || options.getStayAwake()) {
|
||||||
try (ContentProvider settings = device.createSettingsProvider()) {
|
Settings settings = Device.getSettings();
|
||||||
if (options.getShowTouches()) {
|
if (options.getShowTouches()) {
|
||||||
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
|
try {
|
||||||
|
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||||
// If "show touches" was disabled, it must be disabled back on clean up
|
// If "show touches" was disabled, it must be disabled back on clean up
|
||||||
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.e("Could not change \"show_touches\"", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.getStayAwake()) {
|
if (options.getStayAwake()) {
|
||||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||||
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
try {
|
||||||
|
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||||
try {
|
try {
|
||||||
restoreStayOn = Integer.parseInt(oldValue);
|
restoreStayOn = Integer.parseInt(oldValue);
|
||||||
if (restoreStayOn == stayOn) {
|
if (restoreStayOn == stayOn) {
|
||||||
|
@ -45,23 +45,40 @@ public final class Server {
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
restoreStayOn = 0;
|
restoreStayOn = 0;
|
||||||
}
|
}
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
try {
|
||||||
|
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
|
||||||
|
} catch (IOException e) {
|
||||||
|
Ln.e("Could not configure cleanup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scrcpy(Options options) throws IOException {
|
||||||
|
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
|
final Device device = new Device(options);
|
||||||
|
List<CodecOption> codecOptions = options.getCodecOptions();
|
||||||
|
|
||||||
|
Thread initThread = startInitThread(options);
|
||||||
|
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||||
|
options.getEncoderName());
|
||||||
|
|
||||||
|
Thread controllerThread = null;
|
||||||
|
Thread deviceMessageSenderThread = null;
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
final Controller controller = new Controller(device, connection);
|
final Controller controller = new Controller(device, connection, options.getClipboardAutosync());
|
||||||
|
|
||||||
// asynchronous
|
// asynchronous
|
||||||
startController(controller);
|
controllerThread = startController(controller);
|
||||||
startDeviceMessageSender(controller.getSender());
|
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||||
|
|
||||||
device.setClipboardListener(new Device.ClipboardListener() {
|
device.setClipboardListener(new Device.ClipboardListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -77,12 +94,31 @@ public final class Server {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// this is expected on close
|
// this is expected on close
|
||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
|
} finally {
|
||||||
|
initThread.interrupt();
|
||||||
|
if (controllerThread != null) {
|
||||||
|
controllerThread.interrupt();
|
||||||
|
}
|
||||||
|
if (deviceMessageSenderThread != null) {
|
||||||
|
deviceMessageSenderThread.interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void startController(final Controller controller) {
|
private static Thread startInitThread(final Options options) {
|
||||||
new Thread(new Runnable() {
|
Thread thread = new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
initAndCleanUp(options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread.start();
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Thread startController(final Controller controller) {
|
||||||
|
Thread thread = new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
@ -92,11 +128,13 @@ public final class Server {
|
||||||
Ln.d("Controller stopped");
|
Ln.d("Controller stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).start();
|
});
|
||||||
|
thread.start();
|
||||||
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void startDeviceMessageSender(final DeviceMessageSender sender) {
|
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||||
new Thread(new Runnable() {
|
Thread thread = new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
@ -106,7 +144,9 @@ public final class Server {
|
||||||
Ln.d("Device message sender stopped");
|
Ln.d("Device message sender stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).start();
|
});
|
||||||
|
thread.start();
|
||||||
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Options createOptions(String... args) {
|
private static Options createOptions(String... args) {
|
||||||
|
@ -120,58 +160,93 @@ public final class Server {
|
||||||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
final int expectedParameters = 14;
|
|
||||||
if (args.length != expectedParameters) {
|
|
||||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
|
||||||
}
|
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
|
|
||||||
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
|
for (int i = 1; i < args.length; ++i) {
|
||||||
|
String arg = args[i];
|
||||||
|
int equalIndex = arg.indexOf('=');
|
||||||
|
if (equalIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
|
||||||
|
}
|
||||||
|
String key = arg.substring(0, equalIndex);
|
||||||
|
String value = arg.substring(equalIndex + 1);
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||||
options.setLogLevel(level);
|
options.setLogLevel(level);
|
||||||
|
break;
|
||||||
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
|
case "max_size":
|
||||||
|
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||||
options.setMaxSize(maxSize);
|
options.setMaxSize(maxSize);
|
||||||
|
break;
|
||||||
int bitRate = Integer.parseInt(args[3]);
|
case "bit_rate":
|
||||||
|
int bitRate = Integer.parseInt(value);
|
||||||
options.setBitRate(bitRate);
|
options.setBitRate(bitRate);
|
||||||
|
break;
|
||||||
int maxFps = Integer.parseInt(args[4]);
|
case "max_fps":
|
||||||
|
int maxFps = Integer.parseInt(value);
|
||||||
options.setMaxFps(maxFps);
|
options.setMaxFps(maxFps);
|
||||||
|
break;
|
||||||
int lockedVideoOrientation = Integer.parseInt(args[5]);
|
case "lock_video_orientation":
|
||||||
options.setLockedVideoOrientation(lockedVideoOrientation);
|
int lockVideoOrientation = Integer.parseInt(value);
|
||||||
|
options.setLockVideoOrientation(lockVideoOrientation);
|
||||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
break;
|
||||||
boolean tunnelForward = Boolean.parseBoolean(args[6]);
|
case "tunnel_forward":
|
||||||
|
boolean tunnelForward = Boolean.parseBoolean(value);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
break;
|
||||||
Rect crop = parseCrop(args[7]);
|
case "crop":
|
||||||
|
Rect crop = parseCrop(value);
|
||||||
options.setCrop(crop);
|
options.setCrop(crop);
|
||||||
|
break;
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
|
case "send_frame_meta":
|
||||||
|
boolean sendFrameMeta = Boolean.parseBoolean(value);
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
|
break;
|
||||||
boolean control = Boolean.parseBoolean(args[9]);
|
case "control":
|
||||||
|
boolean control = Boolean.parseBoolean(value);
|
||||||
options.setControl(control);
|
options.setControl(control);
|
||||||
|
break;
|
||||||
int displayId = Integer.parseInt(args[10]);
|
case "display_id":
|
||||||
|
int displayId = Integer.parseInt(value);
|
||||||
options.setDisplayId(displayId);
|
options.setDisplayId(displayId);
|
||||||
|
break;
|
||||||
boolean showTouches = Boolean.parseBoolean(args[11]);
|
case "show_touches":
|
||||||
|
boolean showTouches = Boolean.parseBoolean(value);
|
||||||
options.setShowTouches(showTouches);
|
options.setShowTouches(showTouches);
|
||||||
|
break;
|
||||||
boolean stayAwake = Boolean.parseBoolean(args[12]);
|
case "stay_awake":
|
||||||
|
boolean stayAwake = Boolean.parseBoolean(value);
|
||||||
options.setStayAwake(stayAwake);
|
options.setStayAwake(stayAwake);
|
||||||
|
break;
|
||||||
String codecOptions = args[13];
|
case "codec_options":
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse(value);
|
||||||
options.setCodecOptions(codecOptions);
|
options.setCodecOptions(codecOptions);
|
||||||
|
break;
|
||||||
|
case "encoder_name":
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
options.setEncoderName(value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "power_off_on_close":
|
||||||
|
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||||
|
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
||||||
|
break;
|
||||||
|
case "clipboard_autosync":
|
||||||
|
boolean clipboardAutosync = Boolean.parseBoolean(value);
|
||||||
|
options.setClipboardAutosync(clipboardAutosync);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Ln.w("Unknown server option: " + key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Rect parseCrop(String crop) {
|
private static Rect parseCrop(String crop) {
|
||||||
if ("-".equals(crop)) {
|
if (crop.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// input format: "width:height:x:y"
|
// input format: "width:height:x:y"
|
||||||
|
@ -206,6 +281,15 @@ public final class Server {
|
||||||
Ln.e(" scrcpy --display " + id);
|
Ln.e(" scrcpy --display " + id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (e instanceof InvalidEncoderException) {
|
||||||
|
InvalidEncoderException iee = (InvalidEncoderException) e;
|
||||||
|
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
|
||||||
|
if (encoders != null && encoders.length > 0) {
|
||||||
|
Ln.e("Try to use one of the available encoders:");
|
||||||
|
for (MediaCodecInfo encoder : encoders) {
|
||||||
|
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
84
server/src/main/java/com/genymobile/scrcpy/Settings.java
Normal file
84
server/src/main/java/com/genymobile/scrcpy/Settings.java
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class Settings {
|
||||||
|
|
||||||
|
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
|
||||||
|
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
|
||||||
|
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
|
||||||
|
|
||||||
|
private final ServiceManager serviceManager;
|
||||||
|
|
||||||
|
public Settings(ServiceManager serviceManager) {
|
||||||
|
this.serviceManager = serviceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
|
||||||
|
try {
|
||||||
|
Command.exec("settings", "put", table, key, value);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
throw new SettingsException("put", table, key, value, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String execSettingsGet(String table, String key) throws SettingsException {
|
||||||
|
try {
|
||||||
|
return Command.execReadLine("settings", "get", table, key);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
throw new SettingsException("get", table, key, null, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue(String table, String key) throws SettingsException {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||||
|
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||||
|
return provider.getValue(table, key);
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return execSettingsGet(table, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putValue(String table, String key, String value) throws SettingsException {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||||
|
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||||
|
provider.putValue(table, key, value);
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execSettingsPut(table, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAndPutValue(String table, String key, String value) throws SettingsException {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||||
|
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||||
|
String oldValue = provider.getValue(table, key);
|
||||||
|
if (!value.equals(oldValue)) {
|
||||||
|
provider.putValue(table, key, value);
|
||||||
|
}
|
||||||
|
return oldValue;
|
||||||
|
} catch (SettingsException e) {
|
||||||
|
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String oldValue = getValue(table, key);
|
||||||
|
if (!value.equals(oldValue)) {
|
||||||
|
putValue(table, key, value);
|
||||||
|
}
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public class SettingsException extends Exception {
|
||||||
|
private static String createMessage(String method, String table, String key, String value) {
|
||||||
|
return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingsException(String method, String table, String key, String value, Throwable cause) {
|
||||||
|
super(createMessage(method, table, key, value), cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ public final class Workarounds {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public static void prepareMainLooper() {
|
public static void prepareMainLooper() {
|
||||||
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
||||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||||
|
|
|
@ -14,7 +14,7 @@ public class ActivityManager {
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getContentProviderExternalMethod;
|
private Method getContentProviderExternalMethod;
|
||||||
private boolean getContentProviderExternalMethodLegacy;
|
private boolean getContentProviderExternalMethodNewVersion = true;
|
||||||
private Method removeContentProviderExternalMethod;
|
private Method removeContentProviderExternalMethod;
|
||||||
|
|
||||||
public ActivityManager(IInterface manager) {
|
public ActivityManager(IInterface manager) {
|
||||||
|
@ -29,7 +29,7 @@ public class ActivityManager {
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
// old version
|
// old version
|
||||||
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
|
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
|
||||||
getContentProviderExternalMethodLegacy = true;
|
getContentProviderExternalMethodNewVersion = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getContentProviderExternalMethod;
|
return getContentProviderExternalMethod;
|
||||||
|
@ -46,7 +46,7 @@ public class ActivityManager {
|
||||||
try {
|
try {
|
||||||
Method method = getGetContentProviderExternalMethod();
|
Method method = getGetContentProviderExternalMethod();
|
||||||
Object[] args;
|
Object[] args;
|
||||||
if (!getContentProviderExternalMethodLegacy) {
|
if (getContentProviderExternalMethodNewVersion) {
|
||||||
// new version
|
// new version
|
||||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
import com.genymobile.scrcpy.SettingsException;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
@ -35,7 +37,9 @@ public class ContentProvider implements Closeable {
|
||||||
private final IBinder token;
|
private final IBinder token;
|
||||||
|
|
||||||
private Method callMethod;
|
private Method callMethod;
|
||||||
private boolean callMethodLegacy;
|
private int callMethodVersion;
|
||||||
|
|
||||||
|
private Object attributionSource;
|
||||||
|
|
||||||
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
|
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
@ -44,32 +48,69 @@ public class ContentProvider implements Closeable {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
private Method getCallMethod() throws NoSuchMethodException {
|
private Method getCallMethod() throws NoSuchMethodException {
|
||||||
if (callMethod == null) {
|
if (callMethod == null) {
|
||||||
|
try {
|
||||||
|
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
|
||||||
|
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
|
||||||
|
callMethodVersion = 0;
|
||||||
|
} catch (NoSuchMethodException | ClassNotFoundException e0) {
|
||||||
|
// old versions
|
||||||
|
try {
|
||||||
|
callMethod = provider.getClass()
|
||||||
|
.getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class);
|
||||||
|
callMethodVersion = 1;
|
||||||
|
} catch (NoSuchMethodException e1) {
|
||||||
try {
|
try {
|
||||||
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
|
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
|
||||||
} catch (NoSuchMethodException e) {
|
callMethodVersion = 2;
|
||||||
// old version
|
} catch (NoSuchMethodException e2) {
|
||||||
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
|
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
|
||||||
callMethodLegacy = true;
|
callMethodVersion = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return callMethod;
|
return callMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bundle call(String callMethod, String arg, Bundle extras) {
|
@SuppressLint("PrivateApi")
|
||||||
|
private Object getAttributionSource()
|
||||||
|
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||||
|
if (attributionSource == null) {
|
||||||
|
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
|
||||||
|
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
|
||||||
|
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
|
||||||
|
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributionSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||||
|
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
||||||
try {
|
try {
|
||||||
Method method = getCallMethod();
|
Method method = getCallMethod();
|
||||||
Object[] args;
|
Object[] args;
|
||||||
if (!callMethodLegacy) {
|
switch (callMethodVersion) {
|
||||||
|
case 0:
|
||||||
|
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||||
} else {
|
break;
|
||||||
|
default:
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return (Bundle) method.invoke(provider, args);
|
return (Bundle) method.invoke(provider, args);
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
return null;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,30 +144,31 @@ public class ContentProvider implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getValue(String table, String key) {
|
public String getValue(String table, String key) throws SettingsException {
|
||||||
String method = getGetMethod(table);
|
String method = getGetMethod(table);
|
||||||
Bundle arg = new Bundle();
|
Bundle arg = new Bundle();
|
||||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||||
|
try {
|
||||||
Bundle bundle = call(method, key, arg);
|
Bundle bundle = call(method, key, arg);
|
||||||
if (bundle == null) {
|
if (bundle == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return bundle.getString("value");
|
return bundle.getString("value");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new SettingsException(table, "get", key, null, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putValue(String table, String key, String value) {
|
}
|
||||||
|
|
||||||
|
public void putValue(String table, String key, String value) throws SettingsException {
|
||||||
String method = getPutMethod(table);
|
String method = getPutMethod(table);
|
||||||
Bundle arg = new Bundle();
|
Bundle arg = new Bundle();
|
||||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||||
|
try {
|
||||||
call(method, key, arg);
|
call(method, key, arg);
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
throw new SettingsException(table, "put", key, value, e);
|
||||||
public String getAndPutValue(String table, String key, String value) {
|
}
|
||||||
String oldValue = getValue(table, key);
|
|
||||||
if (!value.equals(oldValue)) {
|
|
||||||
putValue(table, key, value);
|
|
||||||
}
|
|
||||||
return oldValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,14 @@ public final class ServiceManager {
|
||||||
|
|
||||||
public ClipboardManager getClipboardManager() {
|
public ClipboardManager getClipboardManager() {
|
||||||
if (clipboardManager == null) {
|
if (clipboardManager == null) {
|
||||||
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
|
IInterface clipboard = getService("clipboard", "android.content.IClipboard");
|
||||||
|
if (clipboard == null) {
|
||||||
|
// Some devices have no clipboard manager
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
clipboardManager = new ClipboardManager(clipboard);
|
||||||
}
|
}
|
||||||
return clipboardManager;
|
return clipboardManager;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ public class StatusBarManager {
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method expandNotificationsPanelMethod;
|
private Method expandNotificationsPanelMethod;
|
||||||
|
private boolean expandNotificationPanelMethodCustomVersion;
|
||||||
|
private Method expandSettingsPanelMethod;
|
||||||
|
private boolean expandSettingsPanelMethodNewVersion = true;
|
||||||
private Method collapsePanelsMethod;
|
private Method collapsePanelsMethod;
|
||||||
|
|
||||||
public StatusBarManager(IInterface manager) {
|
public StatusBarManager(IInterface manager) {
|
||||||
|
@ -19,11 +22,31 @@ public class StatusBarManager {
|
||||||
|
|
||||||
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
|
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
|
||||||
if (expandNotificationsPanelMethod == null) {
|
if (expandNotificationsPanelMethod == null) {
|
||||||
|
try {
|
||||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// Custom version for custom vendor ROM: <https://github.com/Genymobile/scrcpy/issues/2551>
|
||||||
|
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class);
|
||||||
|
expandNotificationPanelMethodCustomVersion = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return expandNotificationsPanelMethod;
|
return expandNotificationsPanelMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Method getExpandSettingsPanel() throws NoSuchMethodException {
|
||||||
|
if (expandSettingsPanelMethod == null) {
|
||||||
|
try {
|
||||||
|
// Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/
|
||||||
|
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// old version
|
||||||
|
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel");
|
||||||
|
expandSettingsPanelMethodNewVersion = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expandSettingsPanelMethod;
|
||||||
|
}
|
||||||
|
|
||||||
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
|
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
|
||||||
if (collapsePanelsMethod == null) {
|
if (collapsePanelsMethod == null) {
|
||||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||||
|
@ -34,7 +57,26 @@ public class StatusBarManager {
|
||||||
public void expandNotificationsPanel() {
|
public void expandNotificationsPanel() {
|
||||||
try {
|
try {
|
||||||
Method method = getExpandNotificationsPanelMethod();
|
Method method = getExpandNotificationsPanelMethod();
|
||||||
|
if (expandNotificationPanelMethodCustomVersion) {
|
||||||
|
method.invoke(manager, 0);
|
||||||
|
} else {
|
||||||
method.invoke(manager);
|
method.invoke(manager);
|
||||||
|
}
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expandSettingsPanel() {
|
||||||
|
try {
|
||||||
|
Method method = getExpandSettingsPanel();
|
||||||
|
if (expandSettingsPanelMethodNewVersion) {
|
||||||
|
// new version
|
||||||
|
method.invoke(manager, (Object) null);
|
||||||
|
} else {
|
||||||
|
// old version
|
||||||
|
method.invoke(manager);
|
||||||
|
}
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -25,6 +24,7 @@ public class ControlMessageReaderTest {
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(KeyEvent.ACTION_UP);
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||||
|
dos.writeInt(5); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ public class ControlMessageReaderTest {
|
||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||||
|
Assert.assertEquals(5, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ public class ControlMessageReaderTest {
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||||
dos.writeShort(text.length);
|
dos.writeInt(text.length);
|
||||||
dos.write(text);
|
dos.write(text);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
@ -68,7 +69,7 @@ public class ControlMessageReaderTest {
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||||
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
|
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
|
||||||
Arrays.fill(text, (byte) 'a');
|
Arrays.fill(text, (byte) 'a');
|
||||||
dos.writeShort(text.length);
|
dos.writeInt(text.length);
|
||||||
dos.write(text);
|
dos.write(text);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
@ -152,6 +153,7 @@ public class ControlMessageReaderTest {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
|
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
|
||||||
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
@ -159,6 +161,7 @@ public class ControlMessageReaderTest {
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
|
||||||
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -178,19 +181,35 @@ public class ControlMessageReaderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseCollapseNotificationPanelEvent() throws IOException {
|
public void testParseExpandSettingsPanelEvent() throws IOException {
|
||||||
ControlMessageReader reader = new ControlMessageReader();
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL);
|
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseCollapsePanelsEvent() throws IOException {
|
||||||
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
|
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
|
||||||
|
|
||||||
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
|
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -200,6 +219,7 @@ public class ControlMessageReaderTest {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
|
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
|
||||||
|
dos.writeByte(ControlMessage.COPY_KEY_COPY);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
@ -207,6 +227,7 @@ public class ControlMessageReaderTest {
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
|
||||||
|
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -216,9 +237,10 @@ public class ControlMessageReaderTest {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||||
|
dos.writeLong(0x0102030405060708L); // sequence
|
||||||
dos.writeByte(1); // paste
|
dos.writeByte(1); // paste
|
||||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||||
dos.writeShort(text.length);
|
dos.writeInt(text.length);
|
||||||
dos.write(text);
|
dos.write(text);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
@ -227,10 +249,9 @@ public class ControlMessageReaderTest {
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||||
|
Assert.assertEquals(0x0102030405060708L, event.getSequence());
|
||||||
Assert.assertEquals("testé", event.getText());
|
Assert.assertEquals("testé", event.getText());
|
||||||
|
Assert.assertTrue(event.getPaste());
|
||||||
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
|
||||||
Assert.assertTrue(parse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -242,11 +263,12 @@ public class ControlMessageReaderTest {
|
||||||
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||||
|
|
||||||
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
|
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
|
||||||
|
dos.writeLong(0x0807060504030201L); // sequence
|
||||||
dos.writeByte(1); // paste
|
dos.writeByte(1); // paste
|
||||||
Arrays.fill(rawText, (byte) 'a');
|
Arrays.fill(rawText, (byte) 'a');
|
||||||
String text = new String(rawText, 0, rawText.length);
|
String text = new String(rawText, 0, rawText.length);
|
||||||
|
|
||||||
dos.writeShort(rawText.length);
|
dos.writeInt(rawText.length);
|
||||||
dos.write(rawText);
|
dos.write(rawText);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
@ -255,10 +277,9 @@ public class ControlMessageReaderTest {
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||||
|
Assert.assertEquals(0x0807060504030201L, event.getSequence());
|
||||||
Assert.assertEquals(text, event.getText());
|
Assert.assertEquals(text, event.getText());
|
||||||
|
Assert.assertTrue(event.getPaste());
|
||||||
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
|
||||||
Assert.assertTrue(parse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -308,11 +329,13 @@ public class ControlMessageReaderTest {
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(KeyEvent.ACTION_UP);
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||||
|
dos.writeInt(0); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
|
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||||
|
dos.writeInt(1); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
@ -322,12 +345,14 @@ public class ControlMessageReaderTest {
|
||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||||
|
Assert.assertEquals(0, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
|
|
||||||
event = reader.next();
|
event = reader.next();
|
||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||||
|
Assert.assertEquals(1, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +366,7 @@ public class ControlMessageReaderTest {
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(KeyEvent.ACTION_UP);
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||||
|
dos.writeInt(4); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
|
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
|
@ -353,6 +379,7 @@ public class ControlMessageReaderTest {
|
||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||||
|
Assert.assertEquals(4, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
|
|
||||||
event = reader.next();
|
event = reader.next();
|
||||||
|
@ -360,6 +387,7 @@ public class ControlMessageReaderTest {
|
||||||
|
|
||||||
bos.reset();
|
bos.reset();
|
||||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||||
|
dos.writeInt(5); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
packet = bos.toByteArray();
|
packet = bos.toByteArray();
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
|
@ -369,6 +397,7 @@ public class ControlMessageReaderTest {
|
||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||||
|
Assert.assertEquals(5, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class DeviceMessageWriterTest {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
||||||
dos.writeShort(data.length);
|
dos.writeInt(data.length);
|
||||||
dos.write(data);
|
dos.write(data);
|
||||||
|
|
||||||
byte[] expected = bos.toByteArray();
|
byte[] expected = bos.toByteArray();
|
||||||
|
@ -32,4 +32,24 @@ public class DeviceMessageWriterTest {
|
||||||
|
|
||||||
Assert.assertArrayEquals(expected, actual);
|
Assert.assertArrayEquals(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeAckSetClipboard() throws IOException {
|
||||||
|
DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
|
dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD);
|
||||||
|
dos.writeLong(0x0102030405060708L);
|
||||||
|
|
||||||
|
byte[] expected = bos.toByteArray();
|
||||||
|
|
||||||
|
DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L);
|
||||||
|
bos = new ByteArrayOutputStream();
|
||||||
|
writer.writeTo(msg, bos);
|
||||||
|
|
||||||
|
byte[] actual = bos.toByteArray();
|
||||||
|
|
||||||
|
Assert.assertArrayEquals(expected, actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue