Compare commits

...

10 Commits
dev ... install

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
27 changed files with 558 additions and 30 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

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

@ -14,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 {
@ -35,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
@ -50,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();
@ -60,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;
@ -114,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;
@ -125,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();
}
}
@ -135,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();
}
@ -148,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
@ -159,8 +171,6 @@ public final class CleanUp {
Ln.i("Cleaning up");
Config config = Config.fromBase64(args[0]);
if (config.disableShowTouches || config.restoreStayOn != -1) {
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");

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;
@ -312,4 +315,12 @@ public final class Device {
wm.thawRotation();
}
}
public static String getInstalledApkPath() {
ApplicationInfo info = ServiceManager.getPackageManager().getApplicationInfo(SCRCPY_PACKAGE_NAME);
if (info == null) {
return null;
}
return info.sourceDir;
}
}

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

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

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

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