Compare commits

...

13 Commits

Author SHA1 Message Date
b4e714f77d Adapt clean up for installed APK
The cleanup process is slightly different when the server is installed
rather than pushed.
2022-10-02 17:59:04 +02:00
c0e00c485b Expose APK path from Java
The cleanup process will need to know the APK path to execute the
CleanUp java executable.
2022-10-02 17:59:04 +02:00
8578c5072e Add option --reinstall
With --install, the server is installed only if the current version is
not already installed. Passing --reinstall force reinstallation is all
cases.
2022-10-02 17:59:04 +02:00
d459a2052f Add option --install
When this flag is set, the scrcpy is installed rather than pushed to
/data/local/tmp.
2022-10-02 17:59:04 +02:00
ab945efe28 Add "adb uninstall"
This will allow to uninstall the server when necessary.
2022-10-02 17:59:04 +02:00
2ac7384797 Add util function to get the APK version
Use "dumpsys package com.genymobile.scrcpy" to get the APK version.
2022-10-02 17:59:04 +02:00
06bc5da24a Add util function to get the APK path
Use "pm list package -f" to find where the APK is actually installed on
the device.
2022-10-02 17:59:04 +02:00
a5d3b5b827 Make build_without_gradle generate a signed APK 2022-10-02 17:59:04 +02:00
174dc17a80 Sign APK with credentials from keystore.properties 2022-10-02 17:59:04 +02:00
56eb9c3106 Rename server to scrcpy-server.apk
This will allow to install it.
2022-10-02 17:59:04 +02:00
40644994e8 Make ServiceManager and Settings methods static
There were exactly one instance of ServiceManager and Settings, stored
in Device.

Since a Device instance is not created by the CleanUp executable, it was
not straightforward to call wrapper methods on cleanup.

Remove this artificial restriction and expose them publicly via static
methods (this is equivalent to expose a singleton, but less verbose).
2022-10-02 17:57:35 +02:00
7505f7117e Fix typo in logs 2022-09-27 14:12:37 +02:00
949b64dff2 Add fallback to get DisplayInfo
PR #3416 <https://github.com/Genymobile/scrcpy/pull/3416>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-25 16:22:51 +02:00
33 changed files with 872 additions and 90 deletions

View File

@ -188,7 +188,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
# build a "portable" version (with scrcpy-server accessible from the same
# build a "portable" version (with scrcpy-server.apk accessible from the same
# directory as the executable)
conf.set('PORTABLE', get_option('portable'))

View File

@ -110,6 +110,10 @@ However, the option is only available when the HID keyboard is enabled (or a phy
Also see \fB\-\-hid\-mouse\fR.
.TP
.B \-\-install
Install the server (via "adb install") rather than pushing it to /data/local/tmp (via "adb push").
.TP
.B \-\-legacy\-paste
Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v).
@ -242,6 +246,10 @@ option if set, or by the file extension (.mp4 or .mkv).
.BI "\-\-record\-format " format
Force recording format (either mp4 or mkv).
.TP
.B \-\-reinstall
Reinstall the server (via "adb install"), even if the correct version is already installed. Implies \fB\-\-install\fR.
.TP
.BI "\-\-render\-driver " name
Request SDL to use the given render driver (this is just a hint).

View File

@ -329,6 +329,17 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
return process_check_success_intr(intr, pid, "adb install", flags);
}
bool
sc_adb_uninstall(struct sc_intr *intr, const char *serial, const char *pkg,
unsigned flags) {
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "uninstall", pkg);
sc_pid pid = sc_adb_execute(argv, flags);
return process_check_success_intr(intr, pid, "adb uninstall", flags);
}
bool
sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port,
unsigned flags) {
@ -435,6 +446,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
"Please report an issue.");
return false;
}
#undef BUFSIZE
// It is parsed as a NUL-terminated string
buf[r] = '\0';
@ -713,3 +725,99 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
return sc_adb_parse_device_ip(buf);
}
char *
sc_adb_get_installed_apk_path(struct sc_intr *intr, const char *serial,
unsigned flags) {
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "shell", "pm", "list", "package", "-f",
SC_ANDROID_PACKAGE);
sc_pipe pout;
sc_pid pid = sc_adb_execute_p(argv, flags, &pout);
if (pid == SC_PROCESS_NONE) {
LOGD("Could not execute \"pm list packages\"");
return NULL;
}
// "pm list packages -f <package>" output should contain only one line, so
// the output should be short
char buf[1024];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1);
sc_pipe_close(pout);
bool ok = process_check_success_intr(intr, pid, "pm list packages", flags);
if (!ok) {
return NULL;
}
if (r == -1) {
return NULL;
}
assert((size_t) r < sizeof(buf));
if (r == sizeof(buf) - 1) {
// The implementation assumes that the output of "pm list packages"
// fits in the buffer in a single pass
LOGW("Result of \"pm list package\" does not fit in 1Kb. "
"Please report an issue.");
return NULL;
}
// It is parsed as a NUL-terminated string
buf[r] = '\0';
return sc_adb_parse_installed_apk_path(buf);
}
char *
sc_adb_get_installed_apk_version(struct sc_intr *intr, const char *serial,
unsigned flags) {
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "shell", "dumpsys", "package",
SC_ANDROID_PACKAGE);
sc_pipe pout;
sc_pid pid = sc_adb_execute_p(argv, flags, &pout);
if (pid == SC_PROCESS_NONE) {
LOGD("Could not execute \"dumpsys package\"");
return NULL;
}
// "dumpsys package" output can be huge (e.g. 16k), but versionName is at
// the beginning, typically in the first 1024 bytes (64k should be enough
// for the whole output anyway)
#define BUFSIZE 65536
char *buf = malloc(BUFSIZE);
if (!buf) {
return false;
}
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, BUFSIZE - 1);
sc_pipe_close(pout);
bool ok = process_check_success_intr(intr, pid, "dumpsys package", flags);
if (!ok) {
free(buf);
return NULL;
}
if (r == -1) {
free(buf);
return NULL;
}
assert((size_t) r < BUFSIZE);
#undef BUFSIZE
// if r == sizeof(buf), then the output is truncated, but we don't care,
// versionName is at the beginning in practice, and is unlikely to be
// truncated at 64k
// It is parsed as a NUL-terminated string
buf[r] = '\0';
char *version = sc_adb_parse_installed_apk_version(buf);
free(buf);
return version;
}

View File

@ -15,6 +15,8 @@
#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR)
#define SC_ANDROID_PACKAGE "com.genymobile.scrcpy"
const char *
sc_adb_get_executable(void);
@ -64,6 +66,10 @@ bool
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags);
bool
sc_adb_uninstall(struct sc_intr *intr, const char *serial, const char *pkg,
unsigned flags);
/**
* Execute `adb tcpip <port>`
*/
@ -114,4 +120,18 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
char *
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags);
/**
* Return the path of the installed APK for com.genymobile.scrcpy (if any)
*/
char *
sc_adb_get_installed_apk_path(struct sc_intr *intr, const char *serial,
unsigned flags);
/**
* Return the version of the installed APK for com.genymobile.scrcpy (if any)
*/
char *
sc_adb_get_installed_apk_version(struct sc_intr *intr, const char *serial,
unsigned flags);
#endif

View File

@ -225,3 +225,62 @@ sc_adb_parse_device_ip(char *str) {
return NULL;
}
char *
sc_adb_parse_installed_apk_path(char *str) {
// str is expected to look like:
// "package:/data/app/.../base.apk=com.genymobile.scrcpy"
// ^^^^^^^^^^^^^^^^^^^^^^
// We want to extract the path (which may contain '=', even in practice)
if (strncmp(str, "package:", 8)) {
// Does not start with "package:"
return NULL;
}
char *s = str + 8;
size_t len = strcspn(s, " \r\n");
s[len] = '\0';
char *p = strrchr(s, '=');
if (!p) {
// No '=' found
return NULL;
}
// Truncate at the last '='
*p = '\0';
return strdup(s);
}
char *
sc_adb_parse_installed_apk_version(const char *str) {
// str is the (beginning of the) output of `dumpsys package`
// We want to extract the version string from a line starting with 4 spaces
// then `versionName=` then the version string.
#define VERSION_NAME_PREFIX "\n versionName="
char *s = strstr(str, VERSION_NAME_PREFIX);
if (!s) {
// Not found
return NULL;
}
s+= sizeof(VERSION_NAME_PREFIX) - 1;
size_t len = strspn(s, "0123456789.");
if (!len) {
LOGW("Unexpected version name with no value");
return NULL;
}
char *version = malloc(len + 1);
if (!version) {
return NULL;
}
memcpy(version, s, len);
version[len] = '\0';
return version;
}

View File

@ -27,4 +27,24 @@ sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec);
char *
sc_adb_parse_device_ip(char *str);
/**
* Parse the package path from the output of
* `adb shell pm list packages -f <package>`
*
* The parameter must be a NUL-terminated string.
*
* Warning: this function modifies the buffer for optimization purposes.
*/
char *
sc_adb_parse_installed_apk_path(char *str);
/**
* Parse the package version from the output of
* `adb shell dumpsys package <package>`
*
* The parameter must be a NUL-terminated string.
*/
char *
sc_adb_parse_installed_apk_version(const char *str);
#endif

View File

@ -57,6 +57,8 @@
#define OPT_NO_CLEANUP 1037
#define OPT_PRINT_FPS 1038
#define OPT_NO_POWER_ON 1039
#define OPT_INSTALL 1040
#define OPT_REINSTALL 1041
struct sc_option {
char shortopt;
@ -207,6 +209,12 @@ static const struct sc_option options[] = {
.longopt = "help",
.text = "Print this help.",
},
{
.longopt_id = OPT_INSTALL,
.longopt = "install",
.text = "Install the server (via 'adb install') rather than pushing "
"it to /data/local/tmp (via 'adb push').",
},
{
.longopt_id = OPT_LEGACY_PASTE,
.longopt = "legacy-paste",
@ -378,6 +386,13 @@ static const struct sc_option options[] = {
.argdesc = "format",
.text = "Force recording format (either mp4 or mkv).",
},
{
.longopt_id = OPT_REINSTALL,
.longopt = "reinstall",
.text = "Reinstall the server (via 'adb install'), even if the correct "
"version is already installed.\n"
"Implies --install.",
},
{
.longopt_id = OPT_RENDER_DRIVER,
.longopt = "render-driver",
@ -1610,6 +1625,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_PRINT_FPS:
opts->start_fps_counter = true;
break;
case OPT_INSTALL:
opts->install = true;
break;
case OPT_REINSTALL:
opts->install = true;
opts->reinstall = true;
break;
case OPT_OTG:
#ifdef HAVE_USB
opts->otg = true;

View File

@ -37,7 +37,7 @@ static const char *const android_motionevent_action_labels[] = {
"move",
"cancel",
"outside",
"ponter-down",
"pointer-down",
"pointer-up",
"hover-move",
"scroll",

View File

@ -65,4 +65,6 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true,
.start_fps_counter = false,
.power_on = true,
.install = false,
.reinstall = false,
};

View File

@ -140,6 +140,8 @@ struct scrcpy_options {
bool cleanup;
bool start_fps_counter;
bool power_on;
bool install;
bool reinstall;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -325,6 +325,8 @@ scrcpy(struct scrcpy_options *options) {
.tcpip_dst = options->tcpip_dst,
.cleanup = options->cleanup,
.power_on = options->power_on,
.install = options->install,
.reinstall = options->reinstall,
};
static const struct sc_server_callbacks cbs = {

View File

@ -14,10 +14,11 @@
#include "util/process_intr.h"
#include "util/str.h"
#define SC_SERVER_FILENAME "scrcpy-server"
#define SC_SERVER_FILENAME "scrcpy-server.apk"
#define SC_SERVER_PACKAGE "com.genymobile.scrcpy"
#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.apk"
static char *
get_server_path(void) {
@ -104,7 +105,10 @@ error:
}
static bool
push_server(struct sc_intr *intr, const char *serial) {
push_server(struct sc_intr *intr, const char *serial, bool install,
bool reinstall) {
assert(install || !reinstall); // reinstall implies install
char *server_path = get_server_path();
if (!server_path) {
return false;
@ -114,7 +118,28 @@ push_server(struct sc_intr *intr, const char *serial) {
free(server_path);
return false;
}
bool ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0);
bool ok;
if (install) {
char *version = sc_adb_get_installed_apk_version(intr, serial, 0);
bool same_version = version && !strcmp(version, SCRCPY_VERSION);
free(version);
if (!reinstall && same_version) {
LOGI("Server " SCRCPY_VERSION " already installed");
ok = true;
} else {
LOGI("Installing server " SCRCPY_VERSION);
// If a server with a different signature is installed, or if a
// newer server is already installed, we must uninstall it first.
ok = sc_adb_uninstall(intr, serial, SC_SERVER_PACKAGE,
SC_ADB_SILENT);
(void) ok; // expected to fail if it is not installed
ok = sc_adb_install(intr, serial, server_path, 0);
}
} else {
ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0);
}
free(server_path);
return ok;
}
@ -152,6 +177,38 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
return !stopped;
}
static char *
get_classpath_cmd(struct sc_intr *intr, const char *serial, bool install) {
if (!install) {
// In push mode, the path is known statically
char *cp = strdup("CLASSPATH=" SC_DEVICE_SERVER_PATH);
if (!cp) {
LOG_OOM();
}
return cp;
}
char *apk_path = sc_adb_get_installed_apk_path(intr, serial, 0);
if (!apk_path) {
LOGE("Could not get device apk path");
return NULL;
}
#define PREFIX_SIZE (sizeof("CLASSPATH=") - 1)
size_t len = strlen(apk_path);
char *cp = malloc(PREFIX_SIZE + len + 1);
if (!cp) {
LOG_OOM();
free(apk_path);
return NULL;
}
memcpy(cp, "CLASSPATH=", PREFIX_SIZE);
memcpy(cp + PREFIX_SIZE, apk_path, len + 1);
free(apk_path);
return cp;
}
static sc_pid
execute_server(struct sc_server *server,
const struct sc_server_params *params) {
@ -160,13 +217,20 @@ execute_server(struct sc_server *server,
const char *serial = server->serial;
assert(serial);
char *classpath = get_classpath_cmd(&server->intr, serial, params->install);
if (!classpath) {
return SC_PROCESS_NONE;
}
LOGD("Using %s", classpath);
const char *cmd[128];
unsigned count = 0;
cmd[count++] = sc_adb_get_executable();
cmd[count++] = "-s";
cmd[count++] = serial;
cmd[count++] = "shell";
cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH;
cmd[count++] = classpath;
cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER
@ -252,6 +316,10 @@ execute_server(struct sc_server *server,
// By default, power_on is true
ADD_PARAM("power_on=false");
}
if (params->install) {
// By default, installed is false
ADD_PARAM("installed=true");
}
#undef ADD_PARAM
@ -272,6 +340,8 @@ execute_server(struct sc_server *server,
pid = sc_adb_execute(cmd, 0);
end:
free(classpath);
for (unsigned i = dyn_idx; i < count; ++i) {
free((char *) cmd[i]);
}
@ -755,8 +825,9 @@ run_server(void *data) {
assert(serial);
LOGD("Device serial: %s", serial);
ok = push_server(&server->intr, serial);
ok = push_server(&server->intr, serial, params->install, params->reinstall);
if (!ok) {
LOGE("Failed to push server");
goto error_connection_failed;
}

View File

@ -48,6 +48,8 @@ struct sc_server_params {
bool select_tcpip;
bool cleanup;
bool power_on;
bool install;
bool reinstall;
};
struct sc_server {

View File

@ -241,6 +241,54 @@ static void test_get_ip_truncated(void) {
assert(!ip);
}
static void test_apk_path(void) {
char str[] = "package:/data/app/~~71mguyc6p-kNjQdNaNkToA==/com.genymobile."
"scrcpy-l6fiqqUSU7Ok7QLg-rIyJA==/base.apk=com.genymobile."
"scrcpy\n";
const char *expected = "/data/app/~~71mguyc6p-kNjQdNaNkToA==/com.genymobile"
".scrcpy-l6fiqqUSU7Ok7QLg-rIyJA==/base.apk";
char *path = sc_adb_parse_installed_apk_path(str);
assert(!strcmp(path, expected));
free(path);
}
static void test_apk_path_invalid(void) {
// Does not start with "package:"
char str[] = "garbage:/data/app/~~71mguyc6p-kNjQdNaNkToA==/com.genymobile."
"scrcpy-l6fiqqUSU7Ok7QLg-rIyJA==/base.apk=com.genymobile."
"scrcpy\n";
char *path = sc_adb_parse_installed_apk_path(str);
assert(!path);
}
static void test_apk_version(void) {
char str[] =
"Key Set Manager:\n"
" [com.genymobile.scrcpy]\n"
" Signing KeySets: 128\n"
"\n"
"Packages:\n"
" Package [com.genymobile.scrcpy] (89abcdef):\n"
" userId=12345\n"
" pkg=Package{012345 com.genymobile.scrcpy}\n"
" codePath=/data/app/~~abcdef==/com.genymobile.scrcpy-012345==\n"
" resourcePath=/data/app/~~abcdef==/com.genymobile.scrcpy-013245==\n"
" primaryCpuAbi=null\n"
" secondaryCpuAbi=null\n"
" versionCode=12400 minSdk=21 targetSdk=31\n"
" versionName=1.24\n"
" splits=[base]\n"
" apkSigningVersion=2\n"
" applicationInfo=ApplicationInfo{012345 com.genymobile.scrcpy}\n";
const char *expected = "1.24";
char *version = sc_adb_parse_installed_apk_version(str);
assert(!strcmp(version, expected));
free(version);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -263,5 +311,9 @@ int main(int argc, char *argv[]) {
test_get_ip_no_wlan_without_eol();
test_get_ip_truncated();
test_apk_path();
test_apk_path_invalid();
test_apk_version();
return 0;
}

2
run
View File

@ -21,5 +21,5 @@ then
fi
SCRCPY_ICON_PATH="app/data/icon.png" \
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.apk" \
"$BUILDDIR/app/scrcpy" "$@"

1
server/.gitignore vendored
View File

@ -6,3 +6,4 @@
/build
/captures
.externalNativeBuild
/keystore.properties

12
server/HOWTO_keystore.txt Normal file
View File

@ -0,0 +1,12 @@
For an APK to be installable, it must be signed: <https://developer.android.com/training/articles/keystore>
For that purpose, create a keystore by executing this command:
keytool -genkey -v -keystore ~/.android/scrcpy.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias scrcpy -dname cn=scrcpy
(Adapt ~/.android/scrcpy.keystore if you want to generate it to another location.)
Then create server/keystore.properties and edit its properties:
cp keystore.properties.sample keystore.properties
vim keystore.properties # fill the properties

View File

@ -10,10 +10,26 @@ android {
versionName "1.24"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
// to be defined in server/keystore.properties (see server/HOWTO_keystore.txt)
def keystorePropsFile = rootProject.file("server/keystore.properties")
if (keystorePropsFile.exists()) {
def props = new Properties()
props.load(new FileInputStream(keystorePropsFile))
storeFile rootProject.file(props['storeFile'])
storePassword props['storePassword']
keyAlias props['keyAlias']
keyPassword props['keyPassword']
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}

View File

@ -14,14 +14,42 @@ set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.24
SERVER_DIR="$(realpath $(dirname "$0"))"
KEYSTORE_PROPERTIES_FILE="$SERVER_DIR/keystore.properties"
if [[ ! -f "$KEYSTORE_PROPERTIES_FILE" ]]
then
echo "The file '$KEYSTORE_PROPERTIES_FILE' does not exist." >&2
echo "Please read '$SERVER_DIR/HOWTO_keystore.txt'." >&2
exit 1
fi
declare -A props
while IFS='=' read -r key value
do
props["$key"]="$value"
done < "$KEYSTORE_PROPERTIES_FILE"
KEYSTORE_FILE=${props['storeFile']}
KEYSTORE_PASSWORD=${props['storePassword']}
KEYSTORE_KEY_ALIAS=${props['keyAlias']}
KEYSTORE_KEY_PASSWORD=${props['keyPassword']}
if [[ ! -f "$KEYSTORE_FILE" ]]
then
echo "Keystore '$KEYSTORE_FILE' (read from '$KEYSTORE_PROPERTIES_FILE')" \
"does not exist." >&2
echo "Please read '$SERVER_DIR/HOWTO_keystore.txt'." >&2
exit 2
fi
PLATFORM=${ANDROID_PLATFORM:-33}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
SERVER_BINARY=scrcpy-server.apk
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
echo "Platform: android-$PLATFORM"
@ -64,11 +92,7 @@ then
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
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
@ -79,8 +103,24 @@ else
com/genymobile/scrcpy/wrappers/*.class
cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY"
rm -rf classes
unzip -o classes.zip classes.dex # we need the inner classes.dex
fi
echo "Packaging..."
# note: if a res directory exists, add: -S "$SERVER_DIR/src/main/res"
"$BUILD_TOOLS_DIR/aapt" package -f \
-M "$SERVER_DIR/src/main/AndroidManifest.xml" \
-I "$ANDROID_JAR" \
-F "$SERVER_BINARY.unaligned"
"$BUILD_TOOLS_DIR/aapt" add "$SERVER_BINARY.unaligned" classes.dex
"$BUILD_TOOLS_DIR/zipalign" -p 4 "$SERVER_BINARY.unaligned" "$SERVER_BINARY"
rm "$SERVER_BINARY.unaligned"
"$BUILD_TOOLS_DIR/apksigner" sign \
--ks "$KEYSTORE_FILE" \
--ks-pass "pass:$KEYSTORE_PASSWORD" \
--ks-key-alias "$KEYSTORE_KEY_ALIAS" \
--key-pass "pass:$KEYSTORE_KEY_PASSWORD" \
"$SERVER_BINARY"
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View File

@ -0,0 +1,4 @@
storeFile=/path/to/your/keystore
storePassword=PASSWORD
keyAlias=scrcpy
keyPassword=PASSWORD

View File

@ -6,7 +6,7 @@ if prebuilt_server == ''
# gradle is responsible for tracking source changes
build_by_default: true,
build_always_stale: true,
output: 'scrcpy-server',
output: 'scrcpy-server.apk',
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
console: true,
install: true,
@ -18,7 +18,7 @@ else
endif
custom_target('scrcpy-server-prebuilt',
input: prebuilt_server,
output: 'scrcpy-server',
output: 'scrcpy-server.apk',
command: ['cp', '@INPUT@', '@OUTPUT@'],
install: true,
install_dir: 'share/scrcpy')

View File

@ -25,5 +25,5 @@ then
cp "$PROJECT_ROOT/build/outputs/apk/debug/server-debug.apk" "$OUTPUT"
else
"$GRADLE" -p "$PROJECT_ROOT" assembleRelease
cp "$PROJECT_ROOT/build/outputs/apk/release/server-release-unsigned.apk" "$OUTPUT"
cp "$PROJECT_ROOT/build/outputs/apk/release/server-release.apk" "$OUTPUT"
fi

View File

@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
@ -16,7 +14,7 @@ import java.io.IOException;
*/
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.apk";
// A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable {
@ -37,6 +35,8 @@ public final class CleanUp {
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
private static final int FLAG_POWER_OFF_SCREEN = 4;
private boolean installed;
private int displayId;
// Restore the value (between 0 and 7), -1 to not restore
@ -52,6 +52,7 @@ public final class CleanUp {
}
protected Config(Parcel in) {
installed = in.readInt() != 0;
displayId = in.readInt();
restoreStayOn = in.readInt();
byte options = in.readByte();
@ -62,6 +63,7 @@ public final class CleanUp {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(installed ? 1 : 0);
dest.writeInt(displayId);
dest.writeInt(restoreStayOn);
byte options = 0;
@ -116,9 +118,10 @@ public final class CleanUp {
// not instantiable
}
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
throws IOException {
public static void configure(boolean installed, int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode,
boolean powerOffScreen) throws IOException {
Config config = new Config();
config.installed = installed;
config.displayId = displayId;
config.disableShowTouches = disableShowTouches;
config.restoreStayOn = restoreStayOn;
@ -127,8 +130,9 @@ public final class CleanUp {
if (config.hasWork()) {
startProcess(config);
} else {
// There is no additional clean up to do when scrcpy dies
} else if (!installed) {
// There is no additional clean up to do when scrcpy dies.
// If the APK has been pushed to /data/local/tmp, remove it.
unlinkSelf();
}
}
@ -137,7 +141,8 @@ public final class CleanUp {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
String serverPath = config.installed ? Device.getInstalledApkPath() : SERVER_PATH;
builder.environment().put("CLASSPATH", serverPath);
builder.start();
}
@ -150,7 +155,12 @@ public final class CleanUp {
}
public static void main(String... args) {
unlinkSelf();
Config config = Config.fromBase64(args[0]);
if (!config.installed) {
// If the APK has been pushed to /data/local/tmp, remove it.
unlinkSelf();
}
try {
// Wait for the server to die
@ -161,15 +171,11 @@ public final class CleanUp {
Ln.i("Cleaning up");
Config config = Config.fromBase64(args[0]);
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
Settings settings = new Settings(serviceManager);
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
try {
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e);
}
@ -177,7 +183,7 @@ public final class CleanUp {
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
try {
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
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);
}

View File

@ -30,4 +30,14 @@ public final class Command {
}
return result;
}
public static String execReadOutput(String... cmd) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
String output = IO.toString(process.getInputStream());
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
return output;
}
}

View File

@ -7,6 +7,7 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener;
import android.content.pm.ApplicationInfo;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
@ -21,6 +22,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class Device {
private static final String SCRCPY_PACKAGE_NAME = "com.genymobile.scrcpy";
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
@ -31,9 +34,6 @@ public final class Device {
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 {
void onRotationChanged(int rotation);
}
@ -66,9 +66,9 @@ public final class Device {
public Device(Options options) {
displayId = options.getDisplayId();
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId);
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds();
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
@ -82,7 +82,7 @@ public final class Device {
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
layerStack = displayInfo.getLayerStack();
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
synchronized (Device.this) {
@ -98,7 +98,7 @@ public final class Device {
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
@ -192,7 +192,7 @@ public final class Device {
return false;
}
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode);
return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode);
}
public boolean injectEvent(InputEvent event, int injectMode) {
@ -220,7 +220,7 @@ public final class Device {
}
public static boolean isScreenOn() {
return SERVICE_MANAGER.getPowerManager().isScreenOn();
return ServiceManager.getPowerManager().isScreenOn();
}
public synchronized void setRotationListener(RotationListener rotationListener) {
@ -232,19 +232,19 @@ public final class Device {
}
public static void expandNotificationPanel() {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
ServiceManager.getStatusBarManager().expandNotificationsPanel();
}
public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
ServiceManager.getStatusBarManager().expandSettingsPanel();
}
public static void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
ServiceManager.getStatusBarManager().collapsePanels();
}
public static String getClipboardText() {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager == null) {
return null;
}
@ -256,7 +256,7 @@ public final class Device {
}
public boolean setClipboardText(String text) {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager == null) {
return false;
}
@ -299,7 +299,7 @@ public final class Device {
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/
public static void rotateDevice() {
WindowManager wm = SERVICE_MANAGER.getWindowManager();
WindowManager wm = ServiceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen();
@ -316,7 +316,11 @@ public final class Device {
}
}
public static Settings getSettings() {
return SETTINGS;
public static String getInstalledApkPath() {
ApplicationInfo info = ServiceManager.getPackageManager().getApplicationInfo(SCRCPY_PACKAGE_NAME);
if (info == null) {
return null;
}
return info.sourceDir;
}
}

View File

@ -6,7 +6,9 @@ import android.system.OsConstants;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Scanner;
public final class IO {
private IO() {
@ -37,4 +39,13 @@ public final class IO {
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
}
public static String toString(InputStream inputStream) {
StringBuilder builder = new StringBuilder();
Scanner scanner = new Scanner(inputStream);
while (scanner.hasNextLine()) {
builder.append(scanner.nextLine()).append('\n');
}
return builder.toString();
}
}

View File

@ -23,6 +23,7 @@ public class Options {
private boolean downsizeOnError = true;
private boolean cleanup = true;
private boolean powerOn = true;
private boolean installed = false;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly
private boolean sendDeviceMeta = true; // send device name and size
@ -196,4 +197,12 @@ public class Options {
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
public boolean getInstalled() {
return installed;
}
public void setInstalled(boolean installed) {
this.installed = installed;
}
}

View File

@ -20,10 +20,9 @@ public final class Server {
int restoreStayOn = -1;
boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled
if (options.getShowTouches() || options.getStayAwake()) {
Settings settings = Device.getSettings();
if (options.getShowTouches()) {
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
} catch (SettingsException e) {
@ -34,7 +33,7 @@ public final class Server {
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) {
@ -52,8 +51,8 @@ public final class Server {
if (options.getCleanup()) {
try {
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode,
options.getPowerOffScreenOnClose());
CleanUp.configure(options.getInstalled(), options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp,
restoreNormalPowerMode, options.getPowerOffScreenOnClose());
} catch (IOException e) {
Ln.e("Could not configure cleanup", e);
}
@ -252,6 +251,10 @@ public final class Server {
boolean powerOn = Boolean.parseBoolean(value);
options.setPowerOn(powerOn);
break;
case "installed":
boolean installed = Boolean.parseBoolean(value);
options.setInstalled(installed);
break;
case "send_device_meta":
boolean sendDeviceMeta = Boolean.parseBoolean(value);
options.setSendDeviceMeta(sendDeviceMeta);

View File

@ -7,16 +7,14 @@ import android.os.Build;
import java.io.IOException;
public class Settings {
public final 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 Settings() {
/* not instantiable */
}
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
@ -35,10 +33,10 @@ public class Settings {
}
}
public String getValue(String table, String key) throws SettingsException {
public static 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()) {
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);
@ -48,10 +46,10 @@ public class Settings {
return execSettingsGet(table, key);
}
public void putValue(String table, String key, String value) throws SettingsException {
public static 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()) {
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);
@ -61,10 +59,10 @@ public class Settings {
execSettingsPut(table, key, value);
}
public String getAndPutValue(String table, String key, String value) throws SettingsException {
public static 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()) {
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);

View File

@ -1,8 +1,16 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Command;
import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.Size;
import android.view.Display;
import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class DisplayManager {
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
@ -10,11 +18,61 @@ public final class DisplayManager {
this.manager = manager;
}
// public to call it from unit tests
public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) {
Pattern regex = Pattern.compile(
"^ mOverrideDisplayInfo=DisplayInfo\\{\".*?\", displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, "
+ "rotation ([0-9]+).*?, layerStack ([0-9]+)",
Pattern.MULTILINE);
Matcher m = regex.matcher(dumpsysDisplayOutput);
if (!m.find()) {
return null;
}
int flags = parseDisplayFlags(m.group(1));
int width = Integer.parseInt(m.group(2));
int height = Integer.parseInt(m.group(3));
int rotation = Integer.parseInt(m.group(4));
int layerStack = Integer.parseInt(m.group(5));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
}
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
try {
String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display");
return parseDisplayInfo(dumpsysDisplayOutput, displayId);
} catch (Exception e) {
Ln.e("Could not get display info from \"dumpsys display\" output", e);
return null;
}
}
private static int parseDisplayFlags(String text) {
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) {
return 0;
}
int flags = 0;
Matcher m = regex.matcher(text);
while (m.find()) {
String flagString = m.group();
try {
Field filed = Display.class.getDeclaredField(flagString);
flags |= filed.getInt(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
// Silently ignore, some flags reported by "dumpsys display" are @TestApi
}
}
return flags;
}
public DisplayInfo getDisplayInfo(int displayId) {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
if (displayInfo == null) {
return null;
// fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId);
}
Class<?> cls = displayInfo.getClass();
// width and height already take the rotation into account

View File

@ -0,0 +1,36 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.content.pm.ApplicationInfo;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class PackageManager {
private IInterface manager;
private Method getApplicationInfoMethod;
public PackageManager(IInterface manager) {
this.manager = manager;
}
private Method getGetApplicationInfoMethod() throws NoSuchMethodException {
if (getApplicationInfoMethod == null) {
getApplicationInfoMethod = manager.getClass().getDeclaredMethod("getApplicationInfo", String.class, int.class, int.class);
}
return getApplicationInfoMethod;
}
public ApplicationInfo getApplicationInfo(String packageName) {
try {
Method method = getGetApplicationInfoMethod();
return (ApplicationInfo) method.invoke(manager, packageName, 0, ServiceManager.USER_ID);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
Ln.e("Cannot get application info", e);
return null;
}
}
}

View File

@ -13,27 +13,31 @@ public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0;
private final Method getServiceMethod;
private WindowManager windowManager;
private DisplayManager displayManager;
private InputManager inputManager;
private PowerManager powerManager;
private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
private ActivityManager activityManager;
public ServiceManager() {
private static final Method GET_SERVICE_METHOD;
static {
try {
getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private IInterface getService(String service, String type) {
private static WindowManager windowManager;
private static DisplayManager displayManager;
private static InputManager inputManager;
private static PowerManager powerManager;
private static StatusBarManager statusBarManager;
private static ClipboardManager clipboardManager;
private static ActivityManager activityManager;
private static PackageManager packageManager;
private ServiceManager() {
/* not instantiable */
}
private static IInterface getService(String service, String type) {
try {
IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service);
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
return (IInterface) asInterfaceMethod.invoke(null, binder);
} catch (Exception e) {
@ -41,14 +45,14 @@ public final class ServiceManager {
}
}
public WindowManager getWindowManager() {
public static WindowManager getWindowManager() {
if (windowManager == null) {
windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
}
return windowManager;
}
public DisplayManager getDisplayManager() {
public static DisplayManager getDisplayManager() {
if (displayManager == null) {
try {
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
@ -62,7 +66,7 @@ public final class ServiceManager {
return displayManager;
}
public InputManager getInputManager() {
public static InputManager getInputManager() {
if (inputManager == null) {
try {
Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance");
@ -75,21 +79,21 @@ public final class ServiceManager {
return inputManager;
}
public PowerManager getPowerManager() {
public static PowerManager getPowerManager() {
if (powerManager == null) {
powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
}
return powerManager;
}
public StatusBarManager getStatusBarManager() {
public static StatusBarManager getStatusBarManager() {
if (statusBarManager == null) {
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
}
return statusBarManager;
}
public ClipboardManager getClipboardManager() {
public static ClipboardManager getClipboardManager() {
if (clipboardManager == null) {
IInterface clipboard = getService("clipboard", "android.content.IClipboard");
if (clipboard == null) {
@ -103,7 +107,7 @@ public final class ServiceManager {
return clipboardManager;
}
public ActivityManager getActivityManager() {
public static ActivityManager getActivityManager() {
if (activityManager == null) {
try {
// On old Android versions, the ActivityManager is not exposed via AIDL,
@ -119,4 +123,20 @@ public final class ServiceManager {
return activityManager;
}
public static PackageManager getPackageManager() {
if (packageManager == null) {
try {
//IInterface manager = getService("package", "android.content.pm.IPackageManager");
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method getPackageManager = activityThreadClass.getDeclaredMethod("getPackageManager");
IInterface manager = (IInterface) getPackageManager.invoke(null);
return new PackageManager(manager);
} catch (Exception e) {
throw new AssertionError(e);
}
}
return packageManager;
}
}

View File

@ -0,0 +1,186 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import android.view.Display;
import org.junit.Assert;
import org.junit.Test;
public class CommandParserTest {
@Test
public void testParseDisplayInfoFromDumpsysDisplay() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ "mDisplayId=0\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, "
+ "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, "
+ "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, "
+ "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], "
+ "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state OFF, "
+ "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, "
+ "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, "
+ "FLAG_TRUSTED, real 1440 x 3120, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, "
+ "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, "
+ "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities "
+ "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, "
+ "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 "
+ "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo "
+ "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, "
+ "relativeAddress=null}, removeMode 0}\n"
+ " mRequestedMinimalPostProcessing=false\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
// FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported
Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags());
Assert.assertEquals(1440, displayInfo.getSize().getWidth());
Assert.assertEquals(3120, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayWithRotation() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ "mDisplayId=0\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, "
+ "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, "
+ "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, "
+ "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], "
+ "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state ON, "
+ "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, "
+ "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, "
+ "FLAG_TRUSTED, real 3120 x 1440, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, "
+ "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, "
+ "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities "
+ "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, "
+ "minimalPostProcessingSupported false, rotation 3, state ON, type INTERNAL, uniqueId \"local:0\", app 3120 x 1440, density 600 "
+ "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo "
+ "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, "
+ "relativeAddress=null}, removeMode 0}\n"
+ " mRequestedMinimalPostProcessing=false";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(3, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
// FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported
Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags());
Assert.assertEquals(3120, displayInfo.getSize().getWidth());
Assert.assertEquals(1440, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayAPI31() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ " mDisplayId=0\n"
+ " mPhase=1\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 "
+ "Infinity]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, "
+ "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, "
+ "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mRequestedMinimalPostProcessing=false\n"
+ " mFrameRateOverrides=[]\n"
+ " mPendingFrameRateOverrideUids={}\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
// FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported
Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags());
Assert.assertEquals(1080, displayInfo.getSize().getWidth());
Assert.assertEquals(2280, displayInfo.getSize().getHeight());
}
@Test
public void testParseDisplayInfoFromDumpsysDisplayAPI31NoFlags() {
/* @formatter:off */
String partialOutput = "Logical Displays: size=1\n"
+ " Display 0:\n"
+ " mDisplayId=0\n"
+ " mPhase=1\n"
+ " mLayerStack=0\n"
+ " mHasContent=true\n"
+ " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 "
+ "Infinity]}\n"
+ " mRequestedColorMode=0\n"
+ " mDisplayOffset=(0, 0)\n"
+ " mDisplayScalingDisabled=false\n"
+ " mPrimaryDisplayDevice=Built-in Screen\n"
+ " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, "
+ "real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, "
+ "real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff "
+ "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, "
+ "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, "
+ "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state "
+ "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, "
+ "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, "
+ "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, "
+ "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n"
+ " mRequestedMinimalPostProcessing=false\n"
+ " mFrameRateOverrides=[]\n"
+ " mPendingFrameRateOverrideUids={}\n";
DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0);
Assert.assertNotNull(displayInfo);
Assert.assertEquals(0, displayInfo.getDisplayId());
Assert.assertEquals(0, displayInfo.getRotation());
Assert.assertEquals(0, displayInfo.getLayerStack());
Assert.assertEquals(0, displayInfo.getFlags());
Assert.assertEquals(1080, displayInfo.getSize().getWidth());
Assert.assertEquals(2280, displayInfo.getSize().getHeight());
}
}