diff --git a/.gitignore b/.gitignore index 26d977acaa..6d0af8f4cd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ build/ /x/ local.properties /scrcpy-server +/.vscode/* +/.cache/* +*.DS_Store diff --git a/README.md b/README.md index a610a8ba77..4a2d0de55f 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ documented in the following pages: - [Camera](doc/camera.md) - [Video4Linux](doc/v4l2.md) - [Shortcuts](doc/shortcuts.md) + - [Get app icon](doc/get_app_icon.md) ## Resources diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 52ed4592c3..a3d792936e 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -359,6 +359,40 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, return process_check_success_intr(intr, pid, "adb push", flags); } +bool +sc_adb_pull(struct sc_intr *intr, const char *serial, const char *remote, + const char *local, unsigned flags) { + assert(serial); + +#ifdef __WINDOWS__ + // Windows will parse the string, so the paths must be quoted + // (see sys/win/command.c) + local = sc_str_quote(local); + if (!local) { + LOG_OOM(); + return false; + } + remote = sc_str_quote(remote); + if (!remote) { + LOG_OOM(); + free((void *) local); + return false; + } +#endif + + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "pull", remote, local); + + sc_pid pid = sc_adb_execute(argv, flags); + +#ifdef __WINDOWS__ + free((void *) local); + free((void *) remote); +#endif + + return process_check_success_intr(intr, pid, "adb pull", flags); +} + bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags) { diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index e490390215..94b9f6034c 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -66,6 +66,10 @@ bool sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote, unsigned flags); +bool +sc_adb_pull(struct sc_intr *intr, const char *serial, const char *remote, + const char *local, unsigned flags); + bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a53..6642edf8a3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -107,6 +107,7 @@ enum { OPT_GAMEPAD, OPT_NEW_DISPLAY, OPT_LIST_APPS, + OPT_GET_APP_ICON, OPT_START_APP, OPT_SCREEN_OFF_TIMEOUT, OPT_CAPTURE_ORIENTATION, @@ -511,6 +512,29 @@ static const struct sc_option options[] = { .longopt = "list-apps", .text = "List Android apps installed on the device.", }, + { + .longopt_id = OPT_GET_APP_ICON, + .longopt = "get-app-icon", + .argdesc = "package_names[:path]", + .text = "Get the icon of an Android app installed on the device." + "The required argument is a comma-separated list of package names" + " (e.g. \"com.android.chrome,org.mozilla.firefox\").\n" + "Optionally, each package name may be followed by ':path' to " + "specify where to save the icon (default is the current " + "directory).\n" + "The icon is saved as a PNG file, named .png by " + "default, or if specified.\n" + "If 'all' is provided instead of a package name, then the " + "icon of all installed apps is extracted.\n" + "Examples:\n" + " scrcpy --get-app-icon=com.android.chrome\n" + " scrcpy --get-app-icon=com.android.chrome:~/Pictures/\n" + " scrcpy --get-app-icon=all:~/Pictures/scrcpy-icons/\n" + " scrcpy --get-app-icon=com.android.chrome,org.mozilla.firefox\n" + " scrcpy --get-app-icon=com.android.chrome,org.mozilla.firefox:~/Pictures/\n" + "If multiple package names are provided, and some cannot be " + "found, then their icons are simply skipped (no error).", + }, { .longopt_id = OPT_LIST_CAMERAS, .longopt = "list-cameras", @@ -2721,6 +2745,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_APPS: opts->list |= SC_OPTION_LIST_APPS; break; + case OPT_GET_APP_ICON: + opts->get_app_icon = optarg; + break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; diff --git a/app/src/options.c b/app/src/options.c index 0fe82d291b..a64452ea90 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -110,6 +110,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_dup = false, .new_display = NULL, .start_app = NULL, + .get_app_icon = NULL, .angle = NULL, .vd_destroy_content = true, .vd_system_decorations = true, diff --git a/app/src/options.h b/app/src/options.h index 03b4291344..5b3921c7a7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -319,12 +319,14 @@ struct scrcpy_options { #define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERA_SIZES 0x8 #define SC_OPTION_LIST_APPS 0x10 +#define SC_OPTION_GET_APP_ICON 0x20 uint8_t list; bool window; bool mouse_hover; bool audio_dup; const char *new_display; // [x][/] parsed by the server const char *start_app; + const char *get_app_icon; // package name for app icon bool vd_destroy_content; bool vd_system_decorations; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9cf8..a57532c50f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -474,6 +474,7 @@ scrcpy(struct scrcpy_options *options) { .vd_destroy_content = options->vd_destroy_content, .vd_system_decorations = options->vd_system_decorations, .list = options->list, + .get_app_icon = options->get_app_icon, }; static const struct sc_server_callbacks cbs = { @@ -497,7 +498,7 @@ scrcpy(struct scrcpy_options *options) { server_started = true; - if (options->list) { + if (options->list || options->get_app_icon) { bool ok = await_for_server(NULL); ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; goto end; diff --git a/app/src/server.c b/app/src/server.c index 153219c3f4..a8e9aa76c0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -2,19 +2,39 @@ #include #include +#include +#include +#include #include #include #include -#include +#include +#include +#include +#include #include "adb/adb.h" -#include "util/env.h" +#include "adb/adb_tunnel.h" +#include "compat.h" +#include "decoder.h" +#include "events.h" +#include "icon.h" +#include "input_manager.h" +#include "options.h" +#include "recorder.h" +#include "screen.h" +#include "server.h" +#include "util/str.h" #include "util/file.h" -#include "util/log.h" +#include "util/env.h" #include "util/net_intr.h" -#include "util/process.h" +#include "util/log.h" +#include "util/net.h" +#include "util/process_intr.h" #include "util/str.h" +// moved to util/str.c and util/file.c + #define SC_SERVER_FILENAME "scrcpy-server" #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME @@ -432,6 +452,44 @@ execute_server(struct sc_server *server, if (params->list & SC_OPTION_LIST_APPS) { ADD_PARAM("list_apps=true"); } + if (params->get_app_icon) { + // Strip any ":path" from the first token to avoid special chars like ~ or $ + const char *spec = params->get_app_icon; + const char *comma = strchr(spec, ','); + const char *colon = strchr(spec, ':'); + char *sanitized = NULL; + if (colon && (!comma || colon < comma)) { + size_t first_len = (size_t)(colon - spec); + if (comma) { + // keep ",..." suffix after the first token + size_t tail_len = strlen(comma); + sanitized = malloc(first_len + tail_len + 1); + if (!sanitized) { + LOG_OOM(); + goto end; + } + memcpy(sanitized, spec, first_len); + memcpy(sanitized + first_len, comma, tail_len + 1); + } else { + sanitized = malloc(first_len + 1); + if (!sanitized) { + LOG_OOM(); + goto end; + } + memcpy(sanitized, spec, first_len); + sanitized[first_len] = '\0'; + } + } else { + sanitized = sc_str_dup(spec); + if (!sanitized) { + LOG_OOM(); + goto end; + } + } + VALIDATE_STRING(sanitized); + ADD_PARAM("get_app_icon=%s", sanitized); + free(sanitized); + } #undef ADD_PARAM @@ -1035,13 +1093,206 @@ run_server(void *data) { // If --list-* is passed, then the server just prints the requested data // then exits. - if (params->list) { + if (params->list || params->get_app_icon) { sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { goto error_connection_failed; } sc_process_wait(pid, NULL); // ignore exit code sc_process_close(pid); + // If requesting app icons, only pull the icons extracted by this operation + if (params->get_app_icon) { + const char *serial = server->serial; + assert(serial); + const char *remote_dir = "/data/local/tmp/scrcpy/icons"; + + // Build deterministic local temp dir: /scrcpy/icons/ + char base_tmp[PATH_MAX]; + const char *sys_tmp = sc_get_env("TMPDIR"); + if (!sys_tmp || !*sys_tmp) { + sys_tmp = "/tmp"; + } + snprintf(base_tmp, sizeof(base_tmp), "%s/scrcpy/icons/%s", sys_tmp, serial); + sc_file_mkdirs(base_tmp); + { + // Pull only the success list + char local_success[PATH_MAX]; + snprintf(local_success, sizeof(local_success), "%s/_success.txt", base_tmp); + char remote_success[PATH_MAX]; + snprintf(remote_success, sizeof(remote_success), "%s/_success.txt", remote_dir); + if (!sc_adb_pull(&server->intr, serial, remote_success, local_success, SC_ADB_SILENT)) { + LOGE("Could not pull success list from device"); + } else { + // Read the list of packages + FILE *f = fopen(local_success, "rb"); + if (!f) { + LOGE("Could not open success list: %s", local_success); + } else { + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz > 0 && sz < 1024 * 1024) { + char *buf = malloc((size_t)sz + 1); + if (!buf) { + LOG_OOM(); + } else { + size_t rd = fread(buf, 1, (size_t)sz, f); + buf[rd] = '\0'; + + // Determine destination rules from get_app_icon value + const char *spec = params->get_app_icon; + // if multiple packages, only the first token may contain :path; accept on first + const char *colon = strchr(spec, ':'); + char *dest_path = NULL; + if (colon && colon[1] != '\0') { + char *raw = sc_str_dup(colon + 1); + if (raw) { + char *expanded = sc_file_expand_path(raw); + free(raw); + dest_path = expanded ? expanded : NULL; + } + } + // detect if request is for "all" or multiple packages + bool is_all = false; + const char *comma = strchr(spec, ','); + bool is_multi = comma != NULL; + size_t first_len = (size_t) (colon && (!comma || colon < comma) + ? (size_t)(colon - spec) + : (comma ? (size_t)(comma - spec) + : strlen(spec))); + if (first_len == 3 && strncmp(spec, "all", 3) == 0) { + is_all = true; + } + + // Destination handling per spec: + // - existing dir: place .png inside + // - existing file: overwrite that file + // - non-existing: create parent dir and use last path segment as filename + bool dest_exists_dir = false; + bool dest_exists_file = false; + char *dest_parent = NULL; // may be NULL + const char *dest_basename = NULL; // used when non-existing path + bool dest_is_tmp = false; // if true, keep files in temp + struct stat st; + if (dest_path) { + // If destination equals the temp base or the system tmp dir, do not move + if (!strcmp(dest_path, "tmpDir")) { + dest_is_tmp = true; + } + size_t dlen = strlen(dest_path); + bool has_trailing_slash = dlen > 0 && dest_path[dlen - 1] == '/'; + if (has_trailing_slash) { + // Treat as directory explicitly + sc_file_mkdirs(dest_path); + dest_exists_dir = true; + dest_exists_file = false; + } else if (dest_path && !stat(dest_path, &st)) { + dest_exists_dir = S_ISDIR(st.st_mode); + dest_exists_file = !dest_exists_dir; + } else { + // Non-existing path + if (is_all || is_multi) { + // For "all", treat dest_path as a directory to create + if (dest_path) sc_file_mkdirs(dest_path); + dest_exists_dir = true; + } else { + // Split into parent and basename + char *copy = sc_str_dup(dest_path); + char *slash = strrchr(copy, '/'); + if (slash) { + *slash = '\0'; + dest_parent = sc_str_dup(copy); + *slash = '/'; + dest_basename = slash + 1; + // create parent dirs + sc_file_mkdirs(dest_parent); + } else { + //NO WIL COME HERE BECAUSE OF EXPANDING + // no parent: use CWD and whole dest_path as basename + dest_parent = NULL; + dest_basename = dest_path; + } + //free(copy); + } + } + } + + // Iterate CSV list + char *p = buf; + while (p && *p) { + // find token end + char *comma = strchr(p, ','); + if (comma) *comma = '\0'; + + // trim spaces/newlines + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p; + char *end = p + strlen(p); + while (end > p && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r')) { + *--end = '\0'; + } + + if (*p) { + // pull single png + char remote_png[PATH_MAX]; + snprintf(remote_png, sizeof(remote_png), "%s/%s.png", remote_dir, p); + char local_png[PATH_MAX]; + snprintf(local_png, sizeof(local_png), "%s/%s.png", base_tmp, p); + + + if (sc_adb_pull(&server->intr, serial, remote_png, local_png, 0)) { + // Move to destination + if (!dest_path || dest_is_tmp) { + // No path provided: use CWD/.png + if (!dest_is_tmp) { + char dst_path[PATH_MAX]; + snprintf(dst_path, sizeof(dst_path), "./%s.png", p); + rename(local_png, dst_path); + printf("Renamed to %s\n", dst_path); + } + } else if (is_all || is_multi) { + // For "all": always place in directory dest_path + char dst_path[PATH_MAX]; + snprintf(dst_path, sizeof(dst_path), "%s/%s.png", dest_path, p); + rename(local_png, dst_path); + printf("Renamed to %s\n", dst_path); + } else if (dest_exists_dir) { + // Existing directory: /.png + char dst_path[PATH_MAX]; + snprintf(dst_path, sizeof(dst_path), "%s/%s.png", dest_path, p); + rename(local_png, dst_path); + printf("Renamed to %s\n", dst_path); + } else if (dest_exists_file) { + // Existing file: overwrite + rename(local_png, dest_path); + printf("Renamed to %s\n", dest_path); + } else { + // Non-existing path: parent ensured above; use last segment as filename + char dst_path[PATH_MAX]; + if (dest_parent) { + snprintf(dst_path, sizeof(dst_path), "%s/%s", dest_parent, dest_basename); + } else { + snprintf(dst_path, sizeof(dst_path), "./%s", dest_basename); + } + rename(local_png, dst_path); + printf("Renamed to %s\n", dst_path); + } + } + } + + if (!comma) break; + p = comma + 1; + } + + free(dest_parent); + free(dest_path); + free(buf); + } + } + fclose(f); + } + } + } + } // Wake up await_for_server() server->cbs->on_connected(server, server->cbs_userdata); return 0; diff --git a/app/src/server.h b/app/src/server.h index 5f4592de90..53bdacf2e4 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -71,6 +71,7 @@ struct sc_server_params { bool vd_destroy_content; bool vd_system_decorations; uint8_t list; + const char *get_app_icon; }; struct sc_server { diff --git a/app/src/util/file.c b/app/src/util/file.c index 174e5efd8c..d746730431 100644 --- a/app/src/util/file.c +++ b/app/src/util/file.c @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include "util/log.h" @@ -46,3 +49,143 @@ sc_file_get_local_path(const char *name) { return file_path; } +void +sc_file_mkdirs(const char *path) { + char buf[PATH_MAX]; + size_t n = strlen(path); + if (n >= sizeof(buf)) { + return; + } + memcpy(buf, path, n + 1); + for (char *p = buf + 1; *p; ++p) { + if (*p == '/') { + *p = '\0'; + mkdir(buf, 0755); + *p = '/'; + } + } + mkdir(buf, 0755); +} + +static char * +sc__expand_env_vars(const char *input) { + size_t len = strlen(input); + // Allocate a buffer reasonably larger; grow dynamically if needed + size_t cap = len + 64; + char *out = malloc(cap); + if (!out) { + LOG_OOM(); + return NULL; + } + size_t oi = 0; + for (size_t i = 0; i < len; ) { + if (input[i] == '$') { + const char *name = NULL; + size_t nlen = 0; + if (i + 1 < len && input[i + 1] == '{') { + size_t j = i + 2; + while (j < len && input[j] != '}') { + j++; + } + if (j < len && input[j] == '}') { + name = input + i + 2; + nlen = j - (i + 2); + i = j + 1; + } else { + // No closing '}', copy literally + if (oi + 1 >= cap) { cap *= 2; out = realloc(out, cap); if (!out) { LOG_OOM(); return NULL; } } + out[oi++] = input[i++]; + continue; + } + } else { + size_t j = i + 1; + while (j < len && ((input[j] >= 'A' && input[j] <= 'Z') || (input[j] >= 'a' && input[j] <= 'z') || (input[j] >= '0' && input[j] <= '9') || input[j] == '_')) { + j++; + } + if (j > i + 1) { + name = input + i + 1; + nlen = j - (i + 1); + i = j; + } else { + if (oi + 1 >= cap) { cap *= 2; out = realloc(out, cap); if (!out) { LOG_OOM(); return NULL; } } + out[oi++] = input[i++]; + continue; + } + } + char varname[256]; + if (nlen >= sizeof(varname)) nlen = sizeof(varname) - 1; + memcpy(varname, name, nlen); + varname[nlen] = '\0'; + const char *val = getenv(varname); + if (!val) val = ""; + size_t vlen = strlen(val); + if (oi + vlen + 1 >= cap) { while (oi + vlen + 1 >= cap) cap *= 2; out = realloc(out, cap); if (!out) { LOG_OOM(); return NULL; } } + memcpy(out + oi, val, vlen); + oi += vlen; + } else { + if (oi + 1 >= cap) { cap *= 2; out = realloc(out, cap); if (!out) { LOG_OOM(); return NULL; } } + out[oi++] = input[i++]; + } + } + out[oi] = '\0'; + return out; +} + +char * +sc_file_expand_path(const char *path) { + if (!path) { + return NULL; + } + if (strcmp(path, "tmpDir") == 0) { + return strdup(path); + } + // Step 1: expand leading ~ or ~/... + char *tilde_expanded = NULL; + if (path[0] == '~') { + const char *home = getenv("HOME"); + if (!home) home = ""; + size_t home_len = strlen(home); + size_t rest_len = strlen(path + 1); + size_t len = home_len + rest_len + 1; + tilde_expanded = malloc(len); + if (!tilde_expanded) { LOG_OOM(); return NULL; } + memcpy(tilde_expanded, home, home_len); + memcpy(tilde_expanded + home_len, path + 1, rest_len + 1); + } else { + tilde_expanded = strdup(path); + if (!tilde_expanded) { LOG_OOM(); return NULL; } + } + + // Step 2: expand environment variables + char *env_expanded = sc__expand_env_vars(tilde_expanded); + free(tilde_expanded); + if (!env_expanded) { + return NULL; + } + + // Step 3: if relative, turn into absolute using getcwd() + char *result = NULL; + if (env_expanded[0] == '/') { + result = env_expanded; // already absolute + } else { + char cwd[PATH_MAX]; + if (!getcwd(cwd, sizeof(cwd))) { + // If getcwd fails, return the env_expanded as-is + return env_expanded; + } + size_t cwd_len = strlen(cwd); + size_t add_slash = cwd_len > 0 && cwd[cwd_len - 1] == '/' ? 0 : 1; + size_t plen = strlen(env_expanded); + size_t total = cwd_len + add_slash + plen + 1; + result = malloc(total); + if (!result) { LOG_OOM(); free(env_expanded); return NULL; } + memcpy(result, cwd, cwd_len); + size_t pos = cwd_len; + if (add_slash) result[pos++] = '/'; + memcpy(result + pos, env_expanded, plen + 1); + free(env_expanded); + } + + return result; +} + diff --git a/app/src/util/file.h b/app/src/util/file.h index 089f6f75be..37ad2abfa8 100644 --- a/app/src/util/file.h +++ b/app/src/util/file.h @@ -46,4 +46,23 @@ sc_file_get_local_path(const char *name); bool sc_file_is_regular(const char *path); +/** + * Recursively create directories for a given path (mkdir -p behavior). + */ +void +sc_file_mkdirs(const char *path); + +/** + * Expand a filesystem path: + * - Expand leading '~' to $HOME + * - Expand environment variables $VAR and ${VAR} + * - If relative, prepend current working directory (POSIX) + * + * Returns a newly-allocated string on success (caller must free), or NULL on + * allocation error. If expansion needs variables that are unset, they are + * replaced by an empty string. + */ +char * +sc_file_expand_path(const char *path); + #endif diff --git a/app/src/util/str.c b/app/src/util/str.c index 83d19c4db3..b232e83eac 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -14,6 +14,18 @@ #include "util/log.h" #include "util/strbuf.h" +char * +sc_str_dup(const char *s) { + size_t len = strlen(s) + 1; + char *copy = malloc(len); + if (!copy) { + LOG_OOM(); + return NULL; + } + memcpy(copy, s, len); + return copy; +} + size_t sc_strncpy(char *dest, const char *src, size_t n) { diff --git a/app/src/util/str.h b/app/src/util/str.h index b386b48df8..39eb382288 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -49,6 +49,14 @@ sc_str_quote(const char *src); char * sc_str_concat(const char *start, const char *end); +/** + * Duplicate a C string + * + * Return a new allocated string (caller must free), or NULL on OOM. + */ +char * +sc_str_dup(const char *s); + /** * Parse `s` as an integer into `out` * diff --git a/doc/get_app_icon.md b/doc/get_app_icon.md new file mode 100644 index 0000000000..7a689024a1 --- /dev/null +++ b/doc/get_app_icon.md @@ -0,0 +1,34 @@ +# Get app icon (`--get-app-icon`) + +The `--get-app-icon` option allows you to extract the icon of one or more installed applications from the Android device. + +## Syntax + +``` +scrcpy --get-app-icon=[:path] +``` + +- ``: Required. One or more package IDs, separated by a comma (`,`) (e.g., `com.android.settings,com.android.chrome`). If the special value `all` is used, icons for all installed applications are extracted. +- `[:path]`: Optional. Destination path to save the extracted icons. + +## Path behavior + +- If `path` is an existing folder, each icon is saved as `.png` inside that folder. +- If `path` is an existing file, the extracted icon will overwrite that file, so be careful. +- If `path` does not exist, the containing folder is created and the file is named with the last segment of the path (regardless of extension). +- If `path` is `tmpDir`, the files are stored in the user's temporary folder: `scrcpy/icons/`. + +## Examples + +- `scrcpy --get-app-icon=com.android.settings` +- `scrcpy --get-app-icon=com.android.settings:~/icon.png` +- `scrcpy --get-app-icon=com.android.settings,com.android.chrome:$HOME/icons/` +- `scrcpy --get-app-icon=all:/home/user/all_icons/` +- `scrcpy --get-app-icon=com.android.chrome,com.android.settings:tmpDir` + +## Notes + +- If multiple `packageID`s are specified, all indicated icons are extracted. +- For `all`, the `--list-apps` functionality is reused to obtain all package IDs and extract their icons. +- If `:path` is not specified, icons are saved in the current directory. + diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8a9..edc0ab2827 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -73,6 +73,7 @@ public class Options { private boolean listCameras; private boolean listCameraSizes; private boolean listApps; + private String getAppIcon; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -249,7 +250,7 @@ public boolean getVDSystemDecorations() { } public boolean getList() { - return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; + return listEncoders || listDisplays || listCameras || listCameraSizes || listApps || getAppIcon != null; } public boolean getListEncoders() { @@ -272,6 +273,10 @@ public boolean getListApps() { return listApps; } + public String getGetAppIcon() { + return getAppIcon; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -444,6 +449,9 @@ public static Options parse(String... args) { case "list_apps": options.listApps = Boolean.parseBoolean(value); break; + case "get_app_icon": + options.getAppIcon = value; + break; case "camera_id": if (!value.isEmpty()) { options.cameraId = value; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a08c948c29..4662582a58 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.opengl.OpenGLRunner; @@ -23,16 +24,22 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import com.genymobile.scrcpy.CleanUp; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; import android.os.Build; import android.os.Looper; +import android.view.Surface; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import android.text.TextUtils; public final class Server { @@ -259,6 +266,64 @@ private static void internalMain(String... args) throws Exception { Ln.i("Processing Android apps... (this may take some time)"); Ln.i(LogUtils.buildAppListMessage()); } + if (options.getGetAppIcon() != null) { + Workarounds.apply(); + String value = options.getGetAppIcon(); + // Strip optional ":path" from tokens; do not use path on server + String[] packages; + int colonIdx = value.indexOf(':'); + String firstToken = colonIdx >= 0 ? value.substring(0, colonIdx) : value; + if ("all".equals(firstToken)) { + List apps = Device.listApps(); + packages = new String[apps.size()]; + for (int i = 0; i < apps.size(); i++) { + packages[i] = apps.get(i).getPackageName(); + } + } else { + String[] rawTokens = value.split(","); + packages = new String[rawTokens.length]; + for (int i = 0; i < rawTokens.length; i++) { + String tok = rawTokens[i].trim(); + int ci = tok.indexOf(':'); + packages[i] = ci >= 0 ? tok.substring(0, ci) : tok; + } + } + + File baseDir = new File("/data/local/tmp/scrcpy/icons"); + if (!baseDir.exists() && !baseDir.mkdirs()) { + Ln.e("Could not create icons directory: " + baseDir); + return; + } + + List success = new ArrayList<>(); + for (String pkg : packages) { + pkg = pkg.trim(); + if (pkg.isEmpty()) { + continue; + } + File out = new File(baseDir, pkg + ".png"); + boolean ok = Device.saveAppIconPng(pkg, out); + if (ok) { + success.add(pkg); + } + } + + // persist success list for the client + try { + File result = new File(baseDir, "_success.txt"); + FileOutputStream fos = new FileOutputStream(result); + fos.write(TextUtils.join(",", success).getBytes(StandardCharsets.UTF_8)); + fos.flush(); + fos.close(); + } catch (IOException e) { + Ln.w("Could not write success list: " + e.getMessage()); + } + + Ln.i("Extracted icons: " + success.size()); + if (!success.isEmpty()) { + Ln.i("get_app_icon_success=" + TextUtils.join(",", success)); + } + } // Just print the requested data, do not mirror return; } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 3553dc2784..01975ffd01 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -16,6 +16,7 @@ import android.app.ActivityOptions; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -24,6 +25,13 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -230,6 +238,55 @@ public static List listApps() { return apps; } + + public static Drawable getAppIcon(String packageName) { + PackageManager pm = FakeContext.get().getPackageManager(); + try { + ApplicationInfo appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + return pm.getApplicationIcon(appInfo); + } catch (PackageManager.NameNotFoundException e) { + Ln.e("Package not found: " + packageName); + return null; + } + } + + public static boolean saveAppIconPng(String packageName, File outFile) { + Drawable icon = getAppIcon(packageName); + if (icon == null) { + return false; + } + try { + Bitmap bitmap; + if (icon instanceof BitmapDrawable) { + bitmap = ((BitmapDrawable) icon).getBitmap(); + } else { + int width = Math.max(1, icon.getIntrinsicWidth()); + int height = Math.max(1, icon.getIntrinsicHeight()); + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + icon.draw(canvas); + } + FileOutputStream fos = null; + try { + fos = new FileOutputStream(outFile); + boolean ok = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); + fos.flush(); + return ok; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException ignored) { + // ignore + } + } + } + } catch (Exception e) { + Ln.e("Error saving icon for " + packageName + ": " + e.getMessage()); + return false; + } + } @SuppressLint("QueryPermissionsNeeded") private static List getLaunchableApps(PackageManager pm) { diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 4f8927ec1a..7f8784768e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -12,7 +12,11 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; @@ -21,8 +25,10 @@ import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.os.Build; +import android.util.Base64; import android.util.Range; +import java.io.ByteArrayOutputStream; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -218,6 +224,37 @@ public static String buildAppListMessage() { List apps = Device.listApps(); return buildAppListMessage("List of apps:", apps); } + + public static String buildAppIconMessage(String packageName) { + Drawable icon = Device.getAppIcon(packageName); + if (icon == null) { + return "Icon not found for package: " + packageName; + } + + Bitmap bitmap = drawableToBitmap(icon); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); + byte[] byteArray = byteArrayOutputStream.toByteArray(); + String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT); + + return "App icon for " + packageName + ":\n" + encoded; + } + + private static Bitmap drawableToBitmap(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + + Bitmap bitmap = Bitmap.createBitmap(width > 0 ? width : 1, height > 0 ? height : 1, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } @SuppressLint("QueryPermissionsNeeded") public static String buildAppListMessage(String title, List apps) {