diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 8908c546..7ed3f40e 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -80,6 +80,13 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; + case CONTROL_MSG_TYPE_SCAN_MEDIA: + { + size_t len = write_string(msg->scan_media.path, + CONTROL_MSG_SCAN_MEDIA_PATH_MAX_LENGTH, + &buf[1]); + return 1 + len; + } case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -102,6 +109,9 @@ control_msg_destroy(struct control_msg *msg) { case CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; + case CONTROL_MSG_TYPE_SCAN_MEDIA: + free(msg->scan_media.path); + break; default: // do nothing break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index c1099c79..e63c30fa 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -17,6 +17,8 @@ // type: 1 byte; paste flag: 1 byte; length: 4 bytes #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) +#define CONTROL_MSG_SCAN_MEDIA_PATH_MAX_LENGTH 256 + #define POINTER_ID_MOUSE UINT64_C(-1); #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2); @@ -33,6 +35,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, CONTROL_MSG_TYPE_ROTATE_DEVICE, + CONTROL_MSG_TYPE_SCAN_MEDIA, }; enum screen_power_mode { @@ -76,6 +79,9 @@ struct control_msg { struct { enum screen_power_mode mode; } set_screen_power_mode; + struct { + char *path; // owned, to be freed by free() + } scan_media; }; }; diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 27fe6fa3..a309cd4f 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -4,6 +4,8 @@ #include #include "adb.h" +#include "control_msg.h" +#include "controller.h" #include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" @@ -14,7 +16,8 @@ file_handler_request_destroy(struct file_handler_request *req) { } bool -file_handler_init(struct file_handler *file_handler, const char *serial, +file_handler_init(struct file_handler *file_handler, + struct controller *controller, const char *serial, const char *push_target) { cbuf_init(&file_handler->queue); @@ -50,6 +53,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial, file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; + file_handler->controller = controller; + return true; } @@ -103,6 +108,24 @@ file_handler_request(struct file_handler *file_handler, return res; } +static bool +request_scan_media(struct file_handler *file_handler) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SCAN_MEDIA; + msg.scan_media.path = strdup(file_handler->push_target); + if (!msg.scan_media.path) { + LOGW("Could not strdup() media path"); + return false; + } + + if (!controller_push_msg(file_handler->controller, &msg)) { + LOGW("Could not request 'scan media'"); + return false; + } + + return true; +} + static int run_file_handler(void *data) { struct file_handler *file_handler = data; @@ -145,6 +168,7 @@ run_file_handler(void *data) { if (process_check_success(process, "adb push", false)) { LOGI("%s successfully pushed to %s", req.file, file_handler->push_target); + request_scan_media(file_handler); } else { LOGE("Failed to push %s to %s", req.file, file_handler->push_target); diff --git a/app/src/file_handler.h b/app/src/file_handler.h index fe1d1804..630193a4 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -22,6 +22,7 @@ struct file_handler_request { struct file_handler_request_queue CBUF(struct file_handler_request, 16); struct file_handler { + struct controller *controller; char *serial; const char *push_target; sc_thread thread; @@ -34,7 +35,8 @@ struct file_handler { }; bool -file_handler_init(struct file_handler *file_handler, const char *serial, +file_handler_init(struct file_handler *file_handler, + struct controller *controller, const char *serial, const char *push_target); void diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17902156..70f62deb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -297,14 +297,6 @@ scrcpy(const struct scrcpy_options *options) { goto end; } - if (options->display && options->control) { - if (!file_handler_init(&s->file_handler, s->server.serial, - options->push_target)) { - goto end; - } - file_handler_initialized = true; - } - struct decoder *dec = NULL; bool needs_decoder = options->display; #ifdef HAVE_V4L2 @@ -353,6 +345,12 @@ scrcpy(const struct scrcpy_options *options) { goto end; } controller_started = true; + + if (!file_handler_init(&s->file_handler, &s->controller, + s->server.serial, options->push_target)) { + goto end; + } + file_handler_initialized = true; } const char *window_title = diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ef9247ca..650eda34 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -278,6 +278,28 @@ static void test_serialize_rotate_device(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_scan_media(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SCAN_MEDIA, + .scan_media = { + .path = "/sdcard/Download/", + }, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 22); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SCAN_MEDIA, + 0x00, 0x00, 0x00, 0x11, // path length + '/', 's', 'd', 'c', 'a', 'r', 'd', '/', + 'D', 'o', 'w', 'n', 'l', 'o', 'a', 'd', + '/' // path + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -295,5 +317,6 @@ int main(int argc, char *argv[]) { test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); + test_serialize_scan_media(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index f8edd53c..b7b3f53b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,6 +17,7 @@ public final class ControlMessage { public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_ROTATE_DEVICE = 11; + public static final int TYPE_SCAN_MEDIA = 12; private int type; private String text; @@ -97,6 +98,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createScanMedia(String path) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_SCAN_MEDIA; + msg.text = path; + return msg; + } + public static ControlMessage createEmpty(int type) { ControlMessage msg = new ControlMessage(); msg.type = type; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index e4ab8402..2f993695 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -76,6 +76,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: msg = parseSetScreenPowerMode(); break; + case ControlMessage.TYPE_SCAN_MEDIA: + msg = parseScanMedia(); + break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: @@ -182,6 +185,14 @@ public class ControlMessageReader { return ControlMessage.createSetScreenPowerMode(mode); } + private ControlMessage parseScanMedia() { + String path = parseString(); + if (path == null) { + return null; + } + return ControlMessage.createScanMedia(path); + } + private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 92986241..619ce824 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; @@ -7,6 +9,7 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; +import java.io.File; import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -135,6 +138,13 @@ public class Controller { case ControlMessage.TYPE_ROTATE_DEVICE: Device.rotateDevice(); break; + case ControlMessage.TYPE_SCAN_MEDIA: + String path = msg.getText(); + @SuppressWarnings("deprecation") + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(new File(path))); + Device.sendBroadcast(intent); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 3e71fe9c..411cee2f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -8,6 +8,7 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; import android.content.IOnPrimaryClipChangedListener; +import android.content.Intent; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; @@ -299,4 +300,8 @@ public final class Device { public static ContentProvider createSettingsProvider() { return SERVICE_MANAGER.getActivityManager().createSettingsProvider(); } + + public static void sendBroadcast(Intent intent) { + SERVICE_MANAGER.getActivityManager().sendBroadcast(intent); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 93ed4528..786c5b2c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -2,7 +2,9 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import android.content.Intent; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; @@ -16,6 +18,7 @@ public class ActivityManager { private Method getContentProviderExternalMethod; private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; + private Method broadcastIntentMethod; public ActivityManager(IInterface manager) { this.manager = manager; @@ -42,6 +45,22 @@ public class ActivityManager { return removeContentProviderExternalMethod; } + private Method getBroadcastIntentMethod() throws NoSuchMethodException { + if (broadcastIntentMethod == null) { + try { + Class iApplicationThreadClass = Class.forName("android.app.IApplicationThread"); + Class iIntentReceiverClass = Class.forName("android.content.IIntentReceiver"); + broadcastIntentMethod = manager.getClass() + .getMethod("broadcastIntent", iApplicationThreadClass, Intent.class, String.class, iIntentReceiverClass, int.class, + String.class, Bundle.class, String[].class, int.class, Bundle.class, boolean.class, boolean.class, int.class); + return broadcastIntentMethod; + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + } + return broadcastIntentMethod; + } + private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); @@ -84,4 +103,13 @@ public class ActivityManager { public ContentProvider createSettingsProvider() { return getContentProviderExternal("settings", new Binder()); } + + public void sendBroadcast(Intent intent) { + try { + Method method = getBroadcastIntentMethod(); + method.invoke(manager, null, intent, null, null, 0, null, null, null, -1, null, true, false, ServiceManager.USER_ID); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index da568486..6c0901a0 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -314,6 +314,26 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); } + @Test + public void testScanMedia() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SCAN_MEDIA); + byte[] text = "/sdcard/Download/".getBytes(StandardCharsets.UTF_8); + dos.writeInt(text.length); + dos.write(text); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SCAN_MEDIA, event.getType()); + Assert.assertEquals("/sdcard/Download/", event.getText()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader();