Compare commits
10 Commits
android-fr
...
install
Author | SHA1 | Date | |
---|---|---|---|
b4e714f77d | |||
c0e00c485b | |||
8578c5072e | |||
d459a2052f | |||
ab945efe28 | |||
2ac7384797 | |||
06bc5da24a | |||
a5d3b5b827 | |||
174dc17a80 | |||
56eb9c3106 |
@ -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'))
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -65,4 +65,6 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.cleanup = true,
|
||||
.start_fps_counter = false,
|
||||
.power_on = true,
|
||||
.install = false,
|
||||
.reinstall = false,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,8 @@ struct sc_server_params {
|
||||
bool select_tcpip;
|
||||
bool cleanup;
|
||||
bool power_on;
|
||||
bool install;
|
||||
bool reinstall;
|
||||
};
|
||||
|
||||
struct sc_server {
|
||||
|
@ -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
2
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" "$@"
|
||||
|
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@ -6,3 +6,4 @@
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
/keystore.properties
|
||||
|
12
server/HOWTO_keystore.txt
Normal file
12
server/HOWTO_keystore.txt
Normal 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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
4
server/keystore.properties.sample
Normal file
4
server/keystore.properties.sample
Normal file
@ -0,0 +1,4 @@
|
||||
storeFile=/path/to/your/keystore
|
||||
storePassword=PASSWORD
|
||||
keyAlias=scrcpy
|
||||
keyPassword=PASSWORD
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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\"");
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user