From c864322d5d158b9c56dd7c996311181c2146deef Mon Sep 17 00:00:00 2001 From: eret9616 <947416983@qq.com> Date: Tue, 19 May 2026 14:33:53 +0800 Subject: [PATCH 1/4] Add --media-scan option to trigger MediaStore scan after push When a file is dropped on the scrcpy window, it is pushed to the device with "adb push" but MediaStore is not updated, so gallery apps cannot see the new file. Many gallery apps also filter out /sdcard/Download/, which is the default push target. With --media-scan enabled: - if --push-target is not set, image files (.jpg, .png, .heic, ...) are pushed to /sdcard/Pictures/ and video files (.mp4, .mkv, ...) to /sdcard/Movies/ instead of /sdcard/Download/; - after each successful push, "cmd media_scanner scan-file " is invoked on the device to trigger a MediaStore scan. An explicit --push-target still wins for all file types and is scanned from that location. The cmd media_scanner shell command is available since Android 8.0. --- app/src/adb/adb.c | 41 +++++++++++++++++ app/src/adb/adb.h | 8 ++++ app/src/cli.c | 17 +++++++ app/src/file_pusher.c | 105 ++++++++++++++++++++++++++++++++++++++++-- app/src/file_pusher.h | 5 +- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 3 +- doc/control.md | 15 ++++++ 9 files changed, 188 insertions(+), 8 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index e388d86233..f45b505659 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -352,6 +352,47 @@ 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_media_scan(struct sc_intr *intr, const char *serial, + const char *remote_path, unsigned flags) { + assert(serial); + assert(remote_path); + + // First try `cmd media_scanner scan-file` (stock Android 8.0+). + { + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "shell", "cmd", "media_scanner", + "scan-file", remote_path); + sc_pid pid = sc_adb_execute(argv, flags | SC_ADB_SILENT); + if (process_check_success_intr(intr, pid, + "adb shell cmd media_scanner", + flags | SC_ADB_SILENT)) { + return true; + } + } + + // Fallback: MEDIA_SCANNER_SCAN_FILE broadcast. Deprecated in Android 12+ + // but still honored on many OEM ROMs (ColorOS/OxygenOS, MIUI) which strip + // the `media_scanner` shell service. + size_t uri_len = strlen("file://") + strlen(remote_path) + 1; + char *uri = malloc(uri_len); + if (!uri) { + LOG_OOM(); + return false; + } + snprintf(uri, uri_len, "file://%s", remote_path); + + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "shell", "am", "broadcast", + "-a", "android.intent.action.MEDIA_SCANNER_SCAN_FILE", + "-d", uri); + sc_pid pid = sc_adb_execute(argv, flags); + bool ok = process_check_success_intr(intr, pid, "adb shell am broadcast", + flags); + free(uri); + return ok; +} + bool sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags) { diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index e490390215..ec7d022efa 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -70,6 +70,14 @@ bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); +/** + * Trigger a MediaStore scan for a remote file path, so that gallery apps can + * pick it up. Uses "cmd media_scanner scan-file" (available on Android 8.0+). + */ +bool +sc_adb_media_scan(struct sc_intr *intr, const char *serial, + const char *remote_path, unsigned flags); + /** * Execute `adb tcpip ` */ diff --git a/app/src/cli.c b/app/src/cli.c index 785e8b9f09..e3de7abc29 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -109,6 +109,7 @@ enum { OPT_KEEP_ACTIVE, OPT_BACKGROUND_COLOR, OPT_RENDER_FIT, + OPT_MEDIA_SCAN, }; struct sc_option { @@ -522,6 +523,19 @@ static const struct sc_option options[] = { .text = "Limit the frame rate of screen capture (officially supported " "since Android 10, but may work on earlier versions).", }, + { + .longopt_id = OPT_MEDIA_SCAN, + .longopt = "media-scan", + .text = "When pushing a file by drag & drop, trigger a MediaStore scan " + "afterwards so that the gallery and other media apps can pick " + "it up.\n" + "If --push-target is not set, image files are pushed to " + "\"/sdcard/Pictures/\" and video files to \"/sdcard/Movies/\". " + "Other files keep the default target.\n" + "If --push-target is set explicitly, all files go there and " + "are scanned from that location.\n" + "Requires Android 8.0 (API 26) or higher on the device.", + }, { .longopt_id = OPT_MIN_SIZE_ALIGNMENT, .longopt = "min-size-alignment", @@ -2632,6 +2646,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_PUSH_TARGET: opts->push_target = optarg; break; + case OPT_MEDIA_SCAN: + opts->media_scan = true; + break; case OPT_PREFER_TEXT: if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) { LOGE("--prefer-text is incompatible with --raw-key-events"); diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 681fb5d657..c1ff512809 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -3,11 +3,93 @@ #include #include #include +#include #include "adb/adb.h" #include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" +#define MEDIA_SCAN_IMAGE_TARGET "/sdcard/Pictures/" +#define MEDIA_SCAN_VIDEO_TARGET "/sdcard/Movies/" + +static const char *const IMAGE_EXTS[] = { + ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".heic", ".heif", + ".avif", +}; + +static const char *const VIDEO_EXTS[] = { + ".mp4", ".mkv", ".mov", ".webm", ".3gp", ".avi", ".m4v", +}; + +static bool +ext_matches(const char *file, const char *const *exts, size_t exts_count) { + const char *dot = strrchr(file, '.'); + if (!dot) { + return false; + } + for (size_t i = 0; i < exts_count; ++i) { + if (!strcasecmp(dot, exts[i])) { + return true; + } + } + return false; +} + +static bool +is_image_file(const char *file) { + return ext_matches(file, IMAGE_EXTS, + sizeof(IMAGE_EXTS) / sizeof(IMAGE_EXTS[0])); +} + +static bool +is_video_file(const char *file) { + return ext_matches(file, VIDEO_EXTS, + sizeof(VIDEO_EXTS) / sizeof(VIDEO_EXTS[0])); +} + +static const char * +resolve_push_target(struct sc_file_pusher *fp, const char *file) { + // An explicit --push-target always wins, regardless of file type. + if (fp->push_target) { + return fp->push_target; + } + if (fp->media_scan) { + if (is_image_file(file)) { + return MEDIA_SCAN_IMAGE_TARGET; + } + if (is_video_file(file)) { + return MEDIA_SCAN_VIDEO_TARGET; + } + } + return DEFAULT_PUSH_TARGET; +} + +static const char * +basename_of(const char *path) { + const char *sep = strrchr(path, '/'); +#ifdef _WIN32 + const char *bsl = strrchr(path, '\\'); + if (bsl && (!sep || bsl > sep)) { + sep = bsl; + } +#endif + return sep ? sep + 1 : path; +} + +static char * +build_remote_path(const char *target, const char *local_file) { + const char *base = basename_of(local_file); + size_t target_len = strlen(target); + bool needs_sep = target_len == 0 || target[target_len - 1] != '/'; + size_t total = target_len + (needs_sep ? 1 : 0) + strlen(base) + 1; + char *result = malloc(total); + if (!result) { + LOG_OOM(); + return NULL; + } + snprintf(result, total, "%s%s%s", target, needs_sep ? "/" : "", base); + return result; +} static void sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { @@ -16,7 +98,7 @@ sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target) { + const char *push_target, bool media_scan) { assert(serial); sc_vecdeque_init(&fp->queue); @@ -53,7 +135,10 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, fp->stopped = false; - fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; + // Keep the user-supplied push_target as-is (possibly NULL); the actual + // target is resolved per request, see resolve_push_target(). + fp->push_target = push_target; + fp->media_scan = media_scan; return true; } @@ -116,9 +201,6 @@ run_file_pusher(void *data) { const char *serial = fp->serial; assert(serial); - const char *push_target = fp->push_target; - assert(push_target); - for (;;) { sc_mutex_lock(&fp->mutex); while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) { @@ -143,10 +225,23 @@ run_file_pusher(void *data) { LOGE("Failed to install %s", req.file); } } else { + const char *push_target = resolve_push_target(fp, req.file); LOGI("Pushing %s...", req.file); bool ok = sc_adb_push(intr, serial, req.file, push_target, 0); if (ok) { LOGI("%s successfully pushed to %s", req.file, push_target); + if (fp->media_scan) { + char *remote = build_remote_path(push_target, req.file); + if (remote) { + if (sc_adb_media_scan(intr, serial, remote, 0)) { + LOGI("MediaStore scan triggered for %s", remote); + } else { + LOGW("MediaStore scan failed for %s " + "(requires Android 8.0+)", remote); + } + free(remote); + } + } } else { LOGE("Failed to push %s to %s", req.file, push_target); } diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h index 0ffb372191..7dfdf6b444 100644 --- a/app/src/file_pusher.h +++ b/app/src/file_pusher.h @@ -23,7 +23,8 @@ struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request); struct sc_file_pusher { char *serial; - const char *push_target; + const char *push_target; // may be NULL if not explicitly set by user + bool media_scan; sc_thread thread; sc_mutex mutex; sc_cond event_cond; @@ -36,7 +37,7 @@ struct sc_file_pusher { bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target); + const char *push_target, bool media_scan); void sc_file_pusher_destroy(struct sc_file_pusher *fp); diff --git a/app/src/options.c b/app/src/options.c index 55a7089817..ec97d081a9 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -96,6 +96,7 @@ const struct scrcpy_options scrcpy_options_default = { .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, + .media_scan = false, .downsize_on_error = true, .tcpip = false, .tcpip_dst = NULL, diff --git a/app/src/options.h b/app/src/options.h index cf4b2fbe11..3a7fe7f46a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -313,6 +313,7 @@ struct scrcpy_options { bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; + bool media_scan; bool downsize_on_error; bool tcpip; const char *tcpip_dst; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7738800abc..93b7e59848 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -497,7 +497,8 @@ scrcpy(struct scrcpy_options *options) { if (options->window && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, - options->push_target)) { + options->push_target, + options->media_scan)) { goto end; } fp = &s->file_pusher; diff --git a/doc/control.md b/doc/control.md index 86c0efe6e5..9c720c02f4 100644 --- a/doc/control.md +++ b/doc/control.md @@ -138,3 +138,18 @@ The target directory can be changed on start: ```bash scrcpy --push-target=/sdcard/Movies/ ``` + +To make pushed images and videos visible in the gallery, enable `--media-scan`: + +```bash +scrcpy --media-scan +``` + +When `--media-scan` is enabled and `--push-target` is *not* set, image files +are pushed to `/sdcard/Pictures/` and video files to `/sdcard/Movies/` (other +files keep the default `/sdcard/Download/`). After each push, a MediaStore scan +is triggered so that gallery apps pick the file up. + +If `--push-target` is set explicitly, all files go there regardless of type and +that location is scanned. Requires Android 8.0 (API 26) or higher on the +device. From b7ec6b30ae1c74b3ccbfbbaaad80e2f863a9de91 Mon Sep 17 00:00:00 2001 From: eret9616 <947416983@qq.com> Date: Wed, 20 May 2026 17:59:35 +0800 Subject: [PATCH 2/4] file_pusher: include stdio.h for snprintf snprintf is used in build_remote_path() but the header was missing, which fails on toolchains that don't pull it in transitively (e.g. gcc on Linux with -std=c11). --- app/src/file_pusher.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index c1ff512809..66777c9c63 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -1,6 +1,7 @@ #include "file_pusher.h" #include +#include #include #include #include From 7030349ef86d5812633ce1bd8963d0e81d84845a Mon Sep 17 00:00:00 2001 From: eret9616 <947416983@qq.com> Date: Fri, 22 May 2026 02:26:35 +0800 Subject: [PATCH 3/4] file_pusher: trigger MediaStore scan via control protocol The previous implementation used `adb shell cmd media_scanner scan-file` with `am broadcast MEDIA_SCANNER_SCAN_FILE` as fallback. Both are unreliable on modern Android: - `cmd media_scanner` is missing on stock Android 16 (Pixel) and many OEM ROMs; - `MEDIA_SCANNER_SCAN_FILE` broadcast was deprecated in Android 12. Instead, send a new SCAN_FILE control message after a successful push, and let the scrcpy server invoke MediaStore's `scan_file` provider call directly through `ContentResolver.call(MediaStore.AUTHORITY, "scan_file", path, null)`, reusing the existing FakeContext. This is the same path that `MediaStore.scanFile()` and `adb shell content call --uri content://media --method scan_file --arg ` go through, and works on every Android version exposing MediaStore. Verified on OnePlus PGZ110 (Android 13): files dragged into the scrcpy window appear in the gallery within seconds, with owner_package_name= com.android.shell as expected. --- app/src/adb/adb.c | 41 ------------------- app/src/adb/adb.h | 8 ---- app/src/control_msg.c | 11 +++++ app/src/control_msg.h | 4 ++ app/src/file_pusher.c | 20 +++++---- app/src/file_pusher.h | 6 ++- app/src/scrcpy.c | 3 +- .../scrcpy/control/ControlMessage.java | 8 ++++ .../scrcpy/control/ControlMessageReader.java | 7 ++++ .../genymobile/scrcpy/control/Controller.java | 23 +++++++++++ 10 files changed, 73 insertions(+), 58 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index f45b505659..e388d86233 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -352,47 +352,6 @@ 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_media_scan(struct sc_intr *intr, const char *serial, - const char *remote_path, unsigned flags) { - assert(serial); - assert(remote_path); - - // First try `cmd media_scanner scan-file` (stock Android 8.0+). - { - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "shell", "cmd", "media_scanner", - "scan-file", remote_path); - sc_pid pid = sc_adb_execute(argv, flags | SC_ADB_SILENT); - if (process_check_success_intr(intr, pid, - "adb shell cmd media_scanner", - flags | SC_ADB_SILENT)) { - return true; - } - } - - // Fallback: MEDIA_SCANNER_SCAN_FILE broadcast. Deprecated in Android 12+ - // but still honored on many OEM ROMs (ColorOS/OxygenOS, MIUI) which strip - // the `media_scanner` shell service. - size_t uri_len = strlen("file://") + strlen(remote_path) + 1; - char *uri = malloc(uri_len); - if (!uri) { - LOG_OOM(); - return false; - } - snprintf(uri, uri_len, "file://%s", remote_path); - - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "shell", "am", "broadcast", - "-a", "android.intent.action.MEDIA_SCANNER_SCAN_FILE", - "-d", uri); - sc_pid pid = sc_adb_execute(argv, flags); - bool ok = process_check_success_intr(intr, pid, "adb shell am broadcast", - flags); - free(uri); - return ok; -} - bool sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags) { diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index ec7d022efa..e490390215 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -70,14 +70,6 @@ bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); -/** - * Trigger a MediaStore scan for a remote file path, so that gallery apps can - * pick it up. Uses "cmd media_scanner scan-file" (available on Android 8.0+). - */ -bool -sc_adb_media_scan(struct sc_intr *intr, const char *serial, - const char *remote_path, unsigned flags); - /** * Execute `adb tcpip ` */ diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fa8ce4800e..b331cb53d7 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -189,6 +189,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { sc_write16be(&buf[1], msg->resize_display.width); sc_write16be(&buf[3], msg->resize_display.height); return 5; + case SC_CONTROL_MSG_TYPE_SCAN_FILE: { + size_t len = write_string(&buf[1], msg->scan_file.path, + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + return 1 + len; + } case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -341,6 +346,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT: LOG_CMSG("camera zoom out"); break; + case SC_CONTROL_MSG_TYPE_SCAN_FILE: + LOG_CMSG("scan file \"%s\"", msg->scan_file.path); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; @@ -369,6 +377,9 @@ sc_control_msg_destroy(struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_START_APP: free(msg->start_app.name); break; + case SC_CONTROL_MSG_TYPE_SCAN_FILE: + free(msg->scan_file.path); + break; default: // do nothing break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2f69e1f01f..732a1b6838 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -47,6 +47,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN, SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT, SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY, + SC_CONTROL_MSG_TYPE_SCAN_FILE, }; enum sc_copy_key { @@ -122,6 +123,9 @@ struct sc_control_msg { uint16_t width; uint16_t height; } resize_display; + struct { + char *path; // owned, to be freed by free() + } scan_file; }; }; diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 66777c9c63..8da2bcb8f2 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -7,6 +7,8 @@ #include #include "adb/adb.h" +#include "control_msg.h" +#include "controller.h" #include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" @@ -99,8 +101,10 @@ sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target, bool media_scan) { + const char *push_target, bool media_scan, + struct sc_controller *controller) { assert(serial); + assert(!media_scan || controller); sc_vecdeque_init(&fp->queue); @@ -140,6 +144,7 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, // target is resolved per request, see resolve_push_target(). fp->push_target = push_target; fp->media_scan = media_scan; + fp->controller = controller; return true; } @@ -234,13 +239,14 @@ run_file_pusher(void *data) { if (fp->media_scan) { char *remote = build_remote_path(push_target, req.file); if (remote) { - if (sc_adb_media_scan(intr, serial, remote, 0)) { - LOGI("MediaStore scan triggered for %s", remote); - } else { - LOGW("MediaStore scan failed for %s " - "(requires Android 8.0+)", remote); + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SCAN_FILE; + msg.scan_file.path = remote; // ownership transferred + if (!sc_controller_push_msg(fp->controller, &msg)) { + LOGW("Could not request MediaStore scan for %s", + remote); + free(remote); } - free(remote); } } } else { diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h index 7dfdf6b444..c1916e35d8 100644 --- a/app/src/file_pusher.h +++ b/app/src/file_pusher.h @@ -9,6 +9,8 @@ #include "util/thread.h" #include "util/vecdeque.h" +struct sc_controller; + enum sc_file_pusher_action { SC_FILE_PUSHER_ACTION_INSTALL_APK, SC_FILE_PUSHER_ACTION_PUSH_FILE, @@ -25,6 +27,7 @@ struct sc_file_pusher { char *serial; const char *push_target; // may be NULL if not explicitly set by user bool media_scan; + struct sc_controller *controller; // used to send SCAN_FILE requests, may be NULL sc_thread thread; sc_mutex mutex; sc_cond event_cond; @@ -37,7 +40,8 @@ struct sc_file_pusher { bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target, bool media_scan); + const char *push_target, bool media_scan, + struct sc_controller *controller); void sc_file_pusher_destroy(struct sc_file_pusher *fp); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 93b7e59848..9b570c1e83 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -498,7 +498,8 @@ scrcpy(struct scrcpy_options *options) { if (options->window && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target, - options->media_scan)) { + options->media_scan, + options->media_scan ? &s->controller : NULL)) { goto end; } fp = &s->file_pusher; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index d7a5c78ee0..777873b113 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -29,6 +29,7 @@ public final class ControlMessage { public static final int TYPE_CAMERA_ZOOM_IN = 19; public static final int TYPE_CAMERA_ZOOM_OUT = 20; public static final int TYPE_RESIZE_DISPLAY = 21; + public static final int TYPE_SCAN_FILE = 22; public static final long SEQUENCE_INVALID = 0; @@ -187,6 +188,13 @@ public static ControlMessage createResizeDisplay(int width, int height) { return msg; } + public static ControlMessage createScanFile(String path) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_SCAN_FILE; + msg.text = path; + return msg; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index a2e3a4a7c6..b2591d6d8b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -62,6 +62,8 @@ public ControlMessage read() throws IOException { return parseCameraSetTorch(); case ControlMessage.TYPE_RESIZE_DISPLAY: return parseResizeDisplay(); + case ControlMessage.TYPE_SCAN_FILE: + return parseScanFile(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -183,6 +185,11 @@ private ControlMessage parseResizeDisplay() throws IOException { return ControlMessage.createResizeDisplay(width, height); } + private ControlMessage parseScanFile() throws IOException { + String path = parseString(); + return ControlMessage.createScanFile(path); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 2e604b3541..e2a8d82267 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -3,6 +3,7 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.display.DisplayInfo; @@ -23,7 +24,10 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import android.content.Intent; +import android.net.Uri; import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; import android.os.SystemClock; import android.util.Pair; import android.view.InputDevice; @@ -335,6 +339,9 @@ private boolean handleEvent() throws IOException { case ControlMessage.TYPE_RESET_VIDEO: resetVideo(); return true; + case ControlMessage.TYPE_SCAN_FILE: + scanFile(msg.getText()); + return true; default: // fall through } @@ -875,4 +882,20 @@ private void resizeDisplay(int width, int height) { NewDisplayCapture newDisplayCapture = (NewDisplayCapture) surfaceCapture; newDisplayCapture.requestResize(width, height); } + + private void scanFile(String path) { + if (path == null || path.isEmpty()) { + return; + } + try { + // MediaStore.scanFile() is @hide; call the same provider method directly. + // Equivalent to: adb shell content call --uri content://media --method scan_file --arg + Bundle out = FakeContext.get().getContentResolver() + .call(MediaStore.AUTHORITY, "scan_file", path, null); + Uri uri = (out != null) ? out.getParcelable(Intent.EXTRA_STREAM) : null; + Ln.i("MediaStore scan " + path + " -> " + uri); + } catch (Throwable t) { + Ln.e("MediaStore scan failed for " + path, t); + } + } } From fda146c79f25ca7eda13f77a55fbcde2c6f6abcd Mon Sep 17 00:00:00 2001 From: eret9616 <947416983@qq.com> Date: Mon, 25 May 2026 17:39:11 +0800 Subject: [PATCH 4/4] file_pusher: scan after every push, drop --media-scan and routing Per review feedback from rom1v on #6852: - drop the --media-scan opt-in flag: MediaStore scan is now always triggered after a successful drag-and-drop push; - drop the image/video extension routing to /sdcard/Pictures/ and /sdcard/Movies/: tests on Pixel 8 / Android 17 and OnePlus PGZ110 / ColorOS 13 confirm Google Photos main view only surfaces DCIM folders regardless of target dir, while scan alone is enough to make files reachable via the system Photo Picker and gallery folder views. The CLI surface goes back to a single --push-target option; the SCAN_FILE control message path introduced previously is preserved. --- app/src/cli.c | 17 -------- app/src/file_pusher.c | 91 +++++++------------------------------------ app/src/file_pusher.h | 7 ++-- app/src/options.c | 1 - app/src/options.h | 1 - app/src/scrcpy.c | 3 +- doc/control.md | 19 +++------ 7 files changed, 24 insertions(+), 115 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index e3de7abc29..785e8b9f09 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -109,7 +109,6 @@ enum { OPT_KEEP_ACTIVE, OPT_BACKGROUND_COLOR, OPT_RENDER_FIT, - OPT_MEDIA_SCAN, }; struct sc_option { @@ -523,19 +522,6 @@ static const struct sc_option options[] = { .text = "Limit the frame rate of screen capture (officially supported " "since Android 10, but may work on earlier versions).", }, - { - .longopt_id = OPT_MEDIA_SCAN, - .longopt = "media-scan", - .text = "When pushing a file by drag & drop, trigger a MediaStore scan " - "afterwards so that the gallery and other media apps can pick " - "it up.\n" - "If --push-target is not set, image files are pushed to " - "\"/sdcard/Pictures/\" and video files to \"/sdcard/Movies/\". " - "Other files keep the default target.\n" - "If --push-target is set explicitly, all files go there and " - "are scanned from that location.\n" - "Requires Android 8.0 (API 26) or higher on the device.", - }, { .longopt_id = OPT_MIN_SIZE_ALIGNMENT, .longopt = "min-size-alignment", @@ -2646,9 +2632,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_PUSH_TARGET: opts->push_target = optarg; break; - case OPT_MEDIA_SCAN: - opts->media_scan = true; - break; case OPT_PREFER_TEXT: if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) { LOGE("--prefer-text is incompatible with --raw-key-events"); diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 8da2bcb8f2..fc45eb5696 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -4,7 +4,6 @@ #include #include #include -#include #include "adb/adb.h" #include "control_msg.h" @@ -12,60 +11,6 @@ #include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" -#define MEDIA_SCAN_IMAGE_TARGET "/sdcard/Pictures/" -#define MEDIA_SCAN_VIDEO_TARGET "/sdcard/Movies/" - -static const char *const IMAGE_EXTS[] = { - ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".heic", ".heif", - ".avif", -}; - -static const char *const VIDEO_EXTS[] = { - ".mp4", ".mkv", ".mov", ".webm", ".3gp", ".avi", ".m4v", -}; - -static bool -ext_matches(const char *file, const char *const *exts, size_t exts_count) { - const char *dot = strrchr(file, '.'); - if (!dot) { - return false; - } - for (size_t i = 0; i < exts_count; ++i) { - if (!strcasecmp(dot, exts[i])) { - return true; - } - } - return false; -} - -static bool -is_image_file(const char *file) { - return ext_matches(file, IMAGE_EXTS, - sizeof(IMAGE_EXTS) / sizeof(IMAGE_EXTS[0])); -} - -static bool -is_video_file(const char *file) { - return ext_matches(file, VIDEO_EXTS, - sizeof(VIDEO_EXTS) / sizeof(VIDEO_EXTS[0])); -} - -static const char * -resolve_push_target(struct sc_file_pusher *fp, const char *file) { - // An explicit --push-target always wins, regardless of file type. - if (fp->push_target) { - return fp->push_target; - } - if (fp->media_scan) { - if (is_image_file(file)) { - return MEDIA_SCAN_IMAGE_TARGET; - } - if (is_video_file(file)) { - return MEDIA_SCAN_VIDEO_TARGET; - } - } - return DEFAULT_PUSH_TARGET; -} static const char * basename_of(const char *path) { @@ -101,10 +46,10 @@ sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target, bool media_scan, + const char *push_target, struct sc_controller *controller) { assert(serial); - assert(!media_scan || controller); + assert(controller); sc_vecdeque_init(&fp->queue); @@ -140,10 +85,7 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, fp->stopped = false; - // Keep the user-supplied push_target as-is (possibly NULL); the actual - // target is resolved per request, see resolve_push_target(). - fp->push_target = push_target; - fp->media_scan = media_scan; + fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; fp->controller = controller; return true; @@ -231,26 +173,23 @@ run_file_pusher(void *data) { LOGE("Failed to install %s", req.file); } } else { - const char *push_target = resolve_push_target(fp, req.file); LOGI("Pushing %s...", req.file); - bool ok = sc_adb_push(intr, serial, req.file, push_target, 0); + bool ok = sc_adb_push(intr, serial, req.file, fp->push_target, 0); if (ok) { - LOGI("%s successfully pushed to %s", req.file, push_target); - if (fp->media_scan) { - char *remote = build_remote_path(push_target, req.file); - if (remote) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SCAN_FILE; - msg.scan_file.path = remote; // ownership transferred - if (!sc_controller_push_msg(fp->controller, &msg)) { - LOGW("Could not request MediaStore scan for %s", - remote); - free(remote); - } + LOGI("%s successfully pushed to %s", req.file, fp->push_target); + char *remote = build_remote_path(fp->push_target, req.file); + if (remote) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SCAN_FILE; + msg.scan_file.path = remote; // ownership transferred + if (!sc_controller_push_msg(fp->controller, &msg)) { + LOGW("Could not request MediaStore scan for %s", + remote); + free(remote); } } } else { - LOGE("Failed to push %s to %s", req.file, push_target); + LOGE("Failed to push %s to %s", req.file, fp->push_target); } } diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h index c1916e35d8..f0e7b59da3 100644 --- a/app/src/file_pusher.h +++ b/app/src/file_pusher.h @@ -25,9 +25,8 @@ struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request); struct sc_file_pusher { char *serial; - const char *push_target; // may be NULL if not explicitly set by user - bool media_scan; - struct sc_controller *controller; // used to send SCAN_FILE requests, may be NULL + const char *push_target; + struct sc_controller *controller; // used to send SCAN_FILE requests sc_thread thread; sc_mutex mutex; sc_cond event_cond; @@ -40,7 +39,7 @@ struct sc_file_pusher { bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target, bool media_scan, + const char *push_target, struct sc_controller *controller); void diff --git a/app/src/options.c b/app/src/options.c index ec97d081a9..55a7089817 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -96,7 +96,6 @@ const struct scrcpy_options scrcpy_options_default = { .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, - .media_scan = false, .downsize_on_error = true, .tcpip = false, .tcpip_dst = NULL, diff --git a/app/src/options.h b/app/src/options.h index 3a7fe7f46a..cf4b2fbe11 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -313,7 +313,6 @@ struct scrcpy_options { bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; - bool media_scan; bool downsize_on_error; bool tcpip; const char *tcpip_dst; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9b570c1e83..7a354e3d36 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -498,8 +498,7 @@ scrcpy(struct scrcpy_options *options) { if (options->window && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target, - options->media_scan, - options->media_scan ? &s->controller : NULL)) { + &s->controller)) { goto end; } fp = &s->file_pusher; diff --git a/doc/control.md b/doc/control.md index 9c720c02f4..29d690db2e 100644 --- a/doc/control.md +++ b/doc/control.md @@ -139,17 +139,8 @@ The target directory can be changed on start: scrcpy --push-target=/sdcard/Movies/ ``` -To make pushed images and videos visible in the gallery, enable `--media-scan`: - -```bash -scrcpy --media-scan -``` - -When `--media-scan` is enabled and `--push-target` is *not* set, image files -are pushed to `/sdcard/Pictures/` and video files to `/sdcard/Movies/` (other -files keep the default `/sdcard/Download/`). After each push, a MediaStore scan -is triggered so that gallery apps pick the file up. - -If `--push-target` is set explicitly, all files go there regardless of type and -that location is scanned. Requires Android 8.0 (API 26) or higher on the -device. +After each successful push, _scrcpy_ asks `MediaStore` to scan the pushed file +so that it appears in media apps. Note that some gallery apps only show files +from a fixed list of folders (typically `DCIM/Camera`) in their main view; the +file is then still reachable through the system Photo Picker and the folder +view, but may not appear on the gallery home screen.