diff --git a/app/meson.build b/app/meson.build index fd5418e336..6707099d7b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -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')) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7cb893b7f0..84f7195aa6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -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). diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index e8775a0119..f99697a491 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -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 " 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; +} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index ffd532eac6..5e774f6783 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -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 ` */ @@ -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 diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index ab12134734..ec5077c586 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -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; +} diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index f20349f6f7..686614589d 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -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 ` + * + * 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 ` + * + * The parameter must be a NUL-terminated string. + */ +char * +sc_adb_parse_installed_apk_version(const char *str); + #endif diff --git a/app/src/cli.c b/app/src/cli.c index 538dd3e7ab..04c927fd06 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -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; diff --git a/app/src/options.c b/app/src/options.c index 8b2624d992..7bbb0b60a0 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -65,4 +65,6 @@ const struct scrcpy_options scrcpy_options_default = { .cleanup = true, .start_fps_counter = false, .power_on = true, + .install = false, + .reinstall = false, }; diff --git a/app/src/options.h b/app/src/options.h index 7e542c06a8..fa67dc8786 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -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; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3588e9aef0..6272ad64aa 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -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 = { diff --git a/app/src/server.c b/app/src/server.c index 663ef18bb4..777da9a88d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -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 @@ sc_server_params_copy(struct sc_server_params *dst, } 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; } diff --git a/app/src/server.h b/app/src/server.h index 49ba83c1f8..a34c465eb0 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -48,6 +48,8 @@ struct sc_server_params { bool select_tcpip; bool cleanup; bool power_on; + bool install; + bool reinstall; }; struct sc_server { diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index d95e7ef285..4e71fcea05 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -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; } diff --git a/run b/run index 56f0a4e1c0..2adc0f2c9d 100755 --- a/run +++ b/run @@ -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" "$@" diff --git a/server/.gitignore b/server/.gitignore index 0df7064d66..bf5287d6c1 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -6,3 +6,4 @@ /build /captures .externalNativeBuild +/keystore.properties diff --git a/server/HOWTO_keystore.txt b/server/HOWTO_keystore.txt new file mode 100644 index 0000000000..fb20eac7d4 --- /dev/null +++ b/server/HOWTO_keystore.txt @@ -0,0 +1,12 @@ +For an APK to be installable, it must be signed: + +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 diff --git a/server/build.gradle b/server/build.gradle index 0059038113..1f4d83a4ba 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -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 } } } diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f4fcba1084..b06bce6dc8 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -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" diff --git a/server/keystore.properties.sample b/server/keystore.properties.sample new file mode 100644 index 0000000000..a072c594fa --- /dev/null +++ b/server/keystore.properties.sample @@ -0,0 +1,4 @@ +storeFile=/path/to/your/keystore +storePassword=PASSWORD +keyAlias=scrcpy +keyPassword=PASSWORD diff --git a/server/meson.build b/server/meson.build index 42b97981f5..bb45cd3105 100644 --- a/server/meson.build +++ b/server/meson.build @@ -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') diff --git a/server/scripts/build-wrapper.sh b/server/scripts/build-wrapper.sh index 7e16dc9464..7ff4f3c82b 100755 --- a/server/scripts/build-wrapper.sh +++ b/server/scripts/build-wrapper.sh @@ -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 diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 831dc994ad..b5cd5b8fc3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -14,7 +14,7 @@ */ 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 { @@ -35,6 +35,8 @@ public Config[] newArray(int size) { 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 @@ -50,6 +52,7 @@ public Config() { } protected Config(Parcel in) { + installed = in.readInt() != 0; displayId = in.readInt(); restoreStayOn = in.readInt(); byte options = in.readByte(); @@ -60,6 +63,7 @@ protected Config(Parcel in) { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(installed ? 1 : 0); dest.writeInt(displayId); dest.writeInt(restoreStayOn); byte options = 0; @@ -114,9 +118,10 @@ private 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; @@ -125,8 +130,9 @@ public static void configure(int displayId, int restoreStayOn, boolean disableSh 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(); } } @@ -135,7 +141,8 @@ private static void startProcess(Config config) throws IOException { 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(); } @@ -148,7 +155,12 @@ private static void unlinkSelf() { } 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 @@ -159,8 +171,6 @@ public static void main(String... args) { Ln.i("Cleaning up"); - Config config = Config.fromBase64(args[0]); - if (config.disableShowTouches || config.restoreStayOn != -1) { if (config.disableShowTouches) { Ln.i("Disabling \"show touches\""); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 97b25b2228..f5e1bd4435 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -7,6 +7,7 @@ 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 @@ 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; @@ -312,4 +315,12 @@ public static void rotateDevice() { wm.thawRotation(); } } + + public static String getInstalledApkPath() { + ApplicationInfo info = ServiceManager.getPackageManager().getApplicationInfo(SCRCPY_PACKAGE_NAME); + if (info == null) { + return null; + } + return info.sourceDir; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d1607c200c..1bb1c381cc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -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 boolean getSendDummyByte() { public void setSendDummyByte(boolean sendDummyByte) { this.sendDummyByte = sendDummyByte; } + + public boolean getInstalled() { + return installed; + } + + public void setInstalled(boolean installed) { + this.installed = installed; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ec03515ea0..0a655879dd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -51,8 +51,8 @@ private static void initAndCleanUp(Options options) { 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); } @@ -251,6 +251,10 @@ private static Options createOptions(String... args) { 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); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PackageManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PackageManager.java new file mode 100644 index 0000000000..44bf242327 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PackageManager.java @@ -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; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index cb6863b6d6..50949b9480 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -29,6 +29,7 @@ public final class ServiceManager { private static StatusBarManager statusBarManager; private static ClipboardManager clipboardManager; private static ActivityManager activityManager; + private static PackageManager packageManager; private ServiceManager() { /* not instantiable */ @@ -122,4 +123,20 @@ public static ActivityManager getActivityManager() { 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; + } }