From c1e3b4d50be1da87d33870f2a4d0a50e8926d031 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 13 Jun 2021 11:36:31 +0200 Subject: [PATCH 01/11] sendBroadcast --- .../java/com/genymobile/scrcpy/Device.java | 5 ++++ .../scrcpy/wrappers/ActivityManager.java | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) 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); + } + } } From 538320add9ec45049e0a22b2875ef7f7d858b496 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 12:31:42 +0100 Subject: [PATCH 02/11] Normalized flags into bit shifts (easier to read and guess next number) --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ec61a1c0..a463e713 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -34,9 +34,9 @@ public final class CleanUp { } }; - private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; - private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; - private static final int FLAG_POWER_OFF_SCREEN = 4; + private static final int FLAG_DISABLE_SHOW_TOUCHES = 1 << 0; + private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 1 << 1; + private static final int FLAG_POWER_OFF_SCREEN = 1 << 2; private int displayId; From fc97b00ac8f7efff2f67cc45d42eab0e7934be13 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 12:46:35 +0100 Subject: [PATCH 03/11] Add intents capability and handling to the server --- .../java/com/genymobile/scrcpy/Intents.java | 36 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Options.java | 12 +++++++ .../java/com/genymobile/scrcpy/Server.java | 9 ++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Intents.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Intents.java b/server/src/main/java/com/genymobile/scrcpy/Intents.java new file mode 100644 index 00000000..e128f434 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Intents.java @@ -0,0 +1,36 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.util.*; + +enum Intents { + START(1), + STOP(30), + CLEANED(31), + ; + + public static final String SCRCPY_PREFIX = "com.genymobile.scrcpy."; + + int shift; + Intents(int shift) { + this.shift = shift; + } + + public static EnumSet fromBitSet(BitSet bits) { + EnumSet es = EnumSet.allOf(Intents.class); + + Iterator it = es.iterator(); + Intents intent; + while (it.hasNext()) { + intent = it.next(); + if (!bits.get(intent.shift - 1)) { + it.remove(); + } + } + return es; + } + + public static String scrcpyPrefix(String unprefixed){ + return SCRCPY_PREFIX + unprefixed; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index cf11df0f..425a342e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,6 +2,9 @@ package com.genymobile.scrcpy; import android.graphics.Rect; +import java.util.BitSet; +import java.util.EnumSet; + public class Options { private Ln.Level logLevel; private int maxSize; @@ -18,6 +21,7 @@ public class Options { private String codecOptions; private String encoderName; private boolean powerOffScreenOnClose; + private EnumSet broadcastIntents; public Ln.Level getLogLevel() { return logLevel; @@ -83,6 +87,10 @@ public class Options { this.sendFrameMeta = sendFrameMeta; } + public void setBroadcastIntents(EnumSet broadcastIntents) { + this.broadcastIntents = broadcastIntents; + } + public boolean getControl() { return control; } @@ -138,4 +146,8 @@ public class Options { public boolean getPowerOffScreenOnClose() { return this.powerOffScreenOnClose; } + + public EnumSet getBroadcastIntents() { + return broadcastIntents; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fdd9db88..fe3294b0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import android.content.Intent; +import android.net.Uri; import com.genymobile.scrcpy.wrappers.ContentProvider; import android.graphics.Rect; @@ -9,6 +11,8 @@ import android.os.BatteryManager; import android.os.Build; import java.io.IOException; +import java.util.BitSet; +import java.util.EnumSet; import java.util.List; import java.util.Locale; @@ -135,7 +139,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 16; + final int expectedParameters = 17; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -188,6 +192,9 @@ public final class Server { boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]); options.setPowerOffScreenOnClose(powerOffScreenOnClose); + EnumSet broadcastIntents = Intents.fromBitSet(BitSet.valueOf(new long[]{Long.parseLong(args[16])})); + options.setBroadcastIntents(broadcastIntents); + return options; } From f29baa77f4447b9123adde2b84a5677ed2d5615d Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 12:47:09 +0100 Subject: [PATCH 04/11] Adding starting and stopping intents to the server --- .../java/com/genymobile/scrcpy/Server.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fe3294b0..2040c5b5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -79,6 +79,10 @@ public final class Server { }); } + if(options.getBroadcastIntents().contains(Intents.START)){ + announceScrcpyStarting(); + } + try { // synchronous screenEncoder.streamScreen(device, connection.getVideoFd()); @@ -92,10 +96,32 @@ public final class Server { if (deviceMessageSenderThread != null) { deviceMessageSenderThread.interrupt(); } + + if(options.getBroadcastIntents().contains(Intents.STOP)){ + Ln.i("Announcing stopped"); + announceScrcpyStopping(); + } } } } + private static void announceScrcpyStarting() { + + Intent starting = new Intent(Intents.scrcpyPrefix("START")); + starting.setData(Uri.parse("scrcpy-status:start")); + starting.putExtra(Intents.scrcpyPrefix("STARTUP"), true); + starting.putExtra(Intents.scrcpyPrefix("SHUTDOWN"), false); + Device.sendBroadcast(starting); + } + private static void announceScrcpyStopping() { + + Intent stopping = new Intent(Intents.scrcpyPrefix("STOP")); + stopping.setData(Uri.parse("scrcpy-status:stop")); + stopping.putExtra(Intents.scrcpyPrefix("STARTUP"), false); + stopping.putExtra(Intents.scrcpyPrefix("SHUTDOWN"), true); + Device.sendBroadcast(stopping); + } + private static Thread startController(final Controller controller) { Thread thread = new Thread(new Runnable() { @Override From eae1c7e5d6ea3985d6a3d75fbe9084beec77477b Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 12:47:59 +0100 Subject: [PATCH 05/11] Add CLEANED intent to the server --- .../java/com/genymobile/scrcpy/CleanUp.java | 28 +++++++++++++++++-- .../java/com/genymobile/scrcpy/Server.java | 3 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index a463e713..57248d71 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import android.content.Intent; +import android.net.Uri; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -37,6 +39,7 @@ public final class CleanUp { private static final int FLAG_DISABLE_SHOW_TOUCHES = 1 << 0; private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 1 << 1; private static final int FLAG_POWER_OFF_SCREEN = 1 << 2; + private static final int FLAG_BROADCAST_CLEANED = 1 << 3; private int displayId; @@ -47,6 +50,7 @@ public final class CleanUp { private boolean disableShowTouches; private boolean restoreNormalPowerMode; private boolean powerOffScreen; + private boolean broadcastCleaned; public Config() { // Default constructor, the fields are initialized by CleanUp.configure() @@ -59,6 +63,7 @@ public final class CleanUp { disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; + broadcastCleaned = (options & FLAG_BROADCAST_CLEANED) != 0; } @Override @@ -75,11 +80,14 @@ public final class CleanUp { if (powerOffScreen) { options |= FLAG_POWER_OFF_SCREEN; } + if (broadcastCleaned) { + options |= FLAG_BROADCAST_CLEANED; + } dest.writeByte(options); } private boolean hasWork() { - return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; + return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen || broadcastCleaned; } @Override @@ -117,7 +125,8 @@ public final class CleanUp { // not instantiable } - public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) + public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, + boolean powerOffScreen, boolean broadcastCleaned) throws IOException { Config config = new Config(); config.displayId = displayId; @@ -125,6 +134,7 @@ public final class CleanUp { config.restoreStayOn = restoreStayOn; config.restoreNormalPowerMode = restoreNormalPowerMode; config.powerOffScreen = powerOffScreen; + config.broadcastCleaned = broadcastCleaned; if (config.hasWork()) { startProcess(config); @@ -187,5 +197,19 @@ public final class CleanUp { Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } + + if(config.broadcastCleaned){ + Ln.i("Announce cleaned"); + announceScrcpyCleaned(); + } + } + + private static void announceScrcpyCleaned() { + + Intent cleaned = new Intent(Intents.scrcpyPrefix("CLEANED")); + cleaned.setData(Uri.parse("scrcpy-status:cleaned")); + cleaned.putExtra(Intents.scrcpyPrefix("STARTUP"), false); + cleaned.putExtra(Intents.scrcpyPrefix("SHUTDOWN"), true); + Device.sendBroadcast(cleaned); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2040c5b5..9ab35fe1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -54,7 +54,8 @@ public final class Server { } } - CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose(), + options.getBroadcastIntents().contains(Intents.CLEANED)); boolean tunnelForward = options.isTunnelForward(); From 5190bf8289a33300b2f2561c034c215686ecf24d Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 12:50:58 +0100 Subject: [PATCH 06/11] Adding intent broadcasts cli options from the client --- app/src/cli.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 10 +++++++++ app/src/server.c | 3 +++ app/src/server.h | 1 + 5 files changed, 69 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 3e5d613d..462d2ea3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -656,6 +656,51 @@ guess_record_format(const char *filename) { return 0; } + +static bool +parse_intent_broadcast(const char *s, uint32_t *intents) { + + // if no arg provided activates all intents for all intents and purposes + if(!s){ + *intents = -1; + return true; + } + + for (;;) { + char *comma = strchr(s, ','); + + assert(!comma || comma > s); + size_t limit = comma ? (size_t) (comma - s) : strlen(s); + + +#define STREQ(literal, s, len) \ + ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) + + if (STREQ("start", s, limit)) { + *intents |= SC_INTENT_BROADCAST_START; + } else if (STREQ("stop", s, limit)) { + *intents |= SC_INTENT_BROADCAST_STOP; + } else if (STREQ("cleaned", s, limit)) { + *intents |= SC_INTENT_BROADCAST_CLEANED; + } else { + LOGE("Unknown broadcast intent: %.*s " + "(must be one of: start, stop, cleaned)", + (int) limit, s); + return false; + } +#undef STREQ + + if (!comma) { + break; + } + + s = comma + 1; + } + + return true; +} + + #define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_WINDOW_TITLE 1001 #define OPT_PUSH_TARGET 1002 @@ -684,6 +729,7 @@ guess_record_format(const char *filename) { #define OPT_ENCODER_NAME 1025 #define OPT_POWER_OFF_ON_CLOSE 1026 #define OPT_V4L2_SINK 1027 +#define OPT_INTENT_BROADCAST 1028 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -739,6 +785,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_WINDOW_BORDERLESS}, {"power-off-on-close", no_argument, NULL, OPT_POWER_OFF_ON_CLOSE}, + {"intent-broadcast", optional_argument, NULL, + OPT_INTENT_BROADCAST}, {NULL, 0, NULL, 0 }, }; @@ -917,6 +965,12 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->v4l2_device = optarg; break; #endif + + case OPT_INTENT_BROADCAST: + if (!parse_intent_broadcast(optarg, &opts->intent_broadcasts)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17902156..52f37b8c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -278,6 +278,7 @@ scrcpy(const struct scrcpy_options *options) { .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, + .intent_broadcasts = options->intent_broadcasts, }; if (!server_start(&s->server, ¶ms)) { goto end; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 405dc7f3..374c417f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -53,6 +53,14 @@ struct sc_port_range { #define SC_WINDOW_POSITION_UNDEFINED (-0x8000) + +enum sc_intent_broadcast { + SC_INTENT_BROADCAST_START = 1 << 0, + SC_INTENT_BROADCAST_STOP = 1 << 30, + SC_INTENT_BROADCAST_CLEANED = 1 << 31, +}; + + struct scrcpy_options { const char *serial; const char *crop; @@ -93,6 +101,7 @@ struct scrcpy_options { bool forward_all_clicks; bool legacy_paste; bool power_off_on_close; + uint32_t intent_broadcasts; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -141,6 +150,7 @@ struct scrcpy_options { .forward_all_clicks = false, \ .legacy_paste = false, \ .power_off_on_close = false, \ + .intent_broadcasts = 0, \ } bool diff --git a/app/src/server.c b/app/src/server.c index 41e8166c..88d98285 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -256,11 +256,13 @@ execute_server(struct server *server, const struct server_params *params) { char max_fps_string[6]; char lock_video_orientation_string[5]; char display_id_string[11]; + char intent_broadcasts_string[11]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); sprintf(display_id_string, "%"PRIu32, params->display_id); + sprintf(intent_broadcasts_string, "%"PRIu32, params->intent_broadcasts); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, @@ -294,6 +296,7 @@ execute_server(struct server *server, const struct server_params *params) { params->codec_options ? params->codec_options : "-", params->encoder_name ? params->encoder_name : "-", params->power_off_on_close ? "true" : "false", + intent_broadcasts_string, }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index c249b374..0130a9b1 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -49,6 +49,7 @@ struct server_params { bool stay_awake; bool force_adb_forward; bool power_off_on_close; + uint32_t intent_broadcasts; }; // init default values From 6c53b258c25bd248bfcb5fdb24ae9b3bfa78a703 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 16:24:27 +0100 Subject: [PATCH 07/11] Documentation for broadcast-intents --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++ app/src/cli.c | 32 +++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/README.md b/README.md index ba68dfb1..a073987d 100644 --- a/README.md +++ b/README.md @@ -721,6 +721,77 @@ The target directory can be changed on start: scrcpy --push-target=/sdcard/Download/ ``` +### Android features + +#### Announce scrcpy state of execution + +**(Advanced feature)** + +Turn on the announcement of scrcpy current status. +Those announcements are done using the [broadcast intents] feature of Android. +If no value is provided with this argument, all intents are turned on. + +[broadcast intents]: https://developer.android.com/reference/android/content/Intent + +Currently, the only events that exist are: + + | Option | Description | [Intent Action] | [Intent Extras] + | ----------|:----------------------------------------------|:---------------------------------|:----------------------------- + | `start` | scrcpy starts | `com.genymobile.scrcpy.START` | STARTUP: true + | `stop` | scrcpy stops (best effort) | `com.genymobile.scrcpy.STOP` | SHUTDOWN: true + | `cleaned` | scrcpy has finished cleaning up (best effort) | `com.genymobile.scrcpy.CLEANED` | SHUTDOWN: true + +[Intent Action]: https://developer.android.com/reference/android/content/Intent#setAction(java.lang.String) +[Intent Extras]: https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable) + + +**Important:** +1. `stop` and `cleaned` **may not happen** in specific cases. For example, + if debugging is turned off, scrcpy process is immediately killed without a chance to cleanup. +2. This option is intended for advanced users. By using this + feature, all apps on your phone will know scrcpy has connected + Unless that is what you want, and you know what that means + do not use this feature +3. In order for this argument to produce visible results you must create + some automation to listen to android broadcast intents. + Such as with your own app or with automation apps such as [Tasker]. + + +Following [Android intent rules], all intents fields/keys prefixed with: +`com.genymobile.scrcpy.` +In case of Actions, it is followed by the intent name in caps. For example, +the 'start' intent has the action: +`com.genymobile.scrcpy.START` + + +[Android intent rules]: https://developer.android.com/reference/android/content/Intent#setAction(java.lang.String) + +Additionally, there are two boolean fields (that may not be present) in the extra data section of the intents: + +1. `com.genymobile.scrcpy.STARTUP` if present and `true`, scrcpy is starting up. +2. `com.genymobile.scrcpy.SHUTDOWN` if present and `true`, scrcpy is shutting down. + +More extra fields will be present in the future. + +For convinience with automation tools such as [Tasker], scrcpy also writes to the data field of the intents. +The scheme is `scrcpy-status`. + +[Tasker]: https://tasker.joaoapps.com/ + +**Example usages:** + +```bash +scrcpy --broadcast-intents +``` + +```bash +scrcpy --broadcast-intents=start +``` + +```bash +scrcpy --broadcast-intents start,cleaned +``` + ### Audio forwarding diff --git a/app/src/cli.c b/app/src/cli.c index 462d2ea3..adf424c5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -28,6 +28,38 @@ scrcpy_print_usage(const char *arg0) { " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is " STR(DEFAULT_BIT_RATE) ".\n" "\n" + " --broadcast-intents [value[, ...]]\n" + " (Advanced feature)\n" + " Turn on the broadcast of intents with the status of scrcpy \n" + " options are: start, stop, cleaned\n" + " Each of these will arm the corresponding intent\n" + " start: announce finished setting up\n" + " stop: announce shut down started (best effort)\n" + " cleaned: announce cleanup finished (best effort)\n" + " \n" + " If you ommit the value, all intents are turned on\n" + " \n" + " All intents have the action and extra fields prefixed with: \n" + " com.genymobile.scrcpy.\n" + " Which is then followed by the intent name in caps. For example,\n" + " the 'start' intent has the action:\n" + " com.genymobile.scrcpy.START\n" + "\n" + " There are two boolean extras use to ease\n" + " the parsing process of the intents:\n" + " 1. com.genymobile.scrcpy.STARTUP if present and true,\n" + " scrcpy is starting up.\n" + " 2. com.genymobile.scrcpy.SHUTDOWN if present and true,\n" + " scrcpy is shutting down.\n" + " \n" + " Notes:\n" + " 1. stop and cleaned may not happen in specific cases. For example, \n" + " if debugging is turned off, scrcpy process is immediately killed \n" + " 2. This option is intended for advanced users. By using this \n" + " feature, all apps on your phone will know scrcpy has connected\n" + " Unless that is what you want, and you know what that means\n" + " do not use this feature\n" + "\n" " --codec-options key[:type]=value[,...]\n" " Set a list of comma-separated key:type=value options for the\n" " device encoder.\n" From 0385d718a5b747b266ed055fae809f6f2e196568 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 18:11:56 +0100 Subject: [PATCH 08/11] Add socket others can listen to Allows other programs to know exactly when scrcpy has exited --- .../java/com/genymobile/scrcpy/Server.java | 74 +++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9ab35fe1..7e58176c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,20 +1,20 @@ package com.genymobile.scrcpy; import android.content.Intent; -import android.net.Uri; -import com.genymobile.scrcpy.wrappers.ContentProvider; - import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; +import android.net.Uri; import android.os.BatteryManager; import android.os.Build; +import com.genymobile.scrcpy.wrappers.ContentProvider; import java.io.IOException; -import java.util.BitSet; -import java.util.EnumSet; -import java.util.List; -import java.util.Locale; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.*; public final class Server { @@ -123,6 +123,66 @@ public final class Server { Device.sendBroadcast(stopping); } + private static Thread scrcpyRunningSocket() { + + // Thread runs until scrcpy exits and doesn't block exiting + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + + while(true) { + + ArrayList acceptedSockets = new ArrayList<>(); + try (ServerSocket ss = new ServerSocket(0, -1, InetAddress.getLocalHost())) { + int localPort = ss.getLocalPort(); + Ln.i("Running socket on " + localPort); + + Intent starting = new Intent(Intents.scrcpyPrefix("SOCKET")); + starting.setData(Uri.parse("scrcpy-status:socket")); + starting.putExtra(Intents.scrcpyPrefix("SOCKET"), localPort); + Device.sendBroadcast(starting); + + while (true) { + Socket accepted = ss.accept(); + if (acceptedSockets.size() < 50) { + acceptedSockets.add(accepted); + Ln.d("Running socket: Added listener"); + } + ListIterator iter = acceptedSockets.listIterator(); + for (Socket s = iter.next(); iter.hasNext(); s = iter.next()) { + try { + s.getOutputStream().write(1); + } catch (SocketException e) { + iter.remove(); + Ln.d("Running socket: Removed listener"); + try { + s.close(); + } catch (IOException ignored) { + } + } + } + + } + } catch (IOException e) { + Ln.e("Running socket: Cannot start server", e); + } catch (Exception e) { + Ln.e("Running socket: failed with an unexpected exception", e); + } + + try { + // Avoid wasting resources in case of infinite loop + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + } + } + }); + thread.setName("Running socket"); + thread.setDaemon(true); + thread.start(); + return thread; + } + private static Thread startController(final Controller controller) { Thread thread = new Thread(new Runnable() { @Override From d0f060712f4c61f750532d90db997b5facf55151 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 18:18:54 +0100 Subject: [PATCH 09/11] Add code to initiate the server --- server/src/main/java/com/genymobile/scrcpy/Intents.java | 1 + server/src/main/java/com/genymobile/scrcpy/Server.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Intents.java b/server/src/main/java/com/genymobile/scrcpy/Intents.java index e128f434..e6b1b1c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Intents.java +++ b/server/src/main/java/com/genymobile/scrcpy/Intents.java @@ -5,6 +5,7 @@ import java.util.*; enum Intents { START(1), + SOCKET(2), STOP(30), CLEANED(31), ; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 7e58176c..dc0a07c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -83,6 +83,9 @@ public final class Server { if(options.getBroadcastIntents().contains(Intents.START)){ announceScrcpyStarting(); } + if(options.getBroadcastIntents().contains(Intents.SOCKET)){ + scrcpyRunningSocket(); + } try { // synchronous From 97be3a4fcf09fa2b2789673ca697889a19cbf02d Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 18:20:13 +0100 Subject: [PATCH 10/11] added socket to cli options --- app/src/cli.c | 4 +++- app/src/scrcpy.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index adf424c5..b2814421 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -710,13 +710,15 @@ parse_intent_broadcast(const char *s, uint32_t *intents) { if (STREQ("start", s, limit)) { *intents |= SC_INTENT_BROADCAST_START; + } else if (STREQ("socket", s, limit)) { + *intents |= SC_INTENT_BROADCAST_SOCKET; } else if (STREQ("stop", s, limit)) { *intents |= SC_INTENT_BROADCAST_STOP; } else if (STREQ("cleaned", s, limit)) { *intents |= SC_INTENT_BROADCAST_CLEANED; } else { LOGE("Unknown broadcast intent: %.*s " - "(must be one of: start, stop, cleaned)", + "(must be one of: start, socket, stop, cleaned)", (int) limit, s); return false; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 374c417f..55444d50 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -56,6 +56,7 @@ struct sc_port_range { enum sc_intent_broadcast { SC_INTENT_BROADCAST_START = 1 << 0, + SC_INTENT_BROADCAST_SOCKET = 1 << 1, SC_INTENT_BROADCAST_STOP = 1 << 30, SC_INTENT_BROADCAST_CLEANED = 1 << 31, }; From c559adfda37bc07904e3846c0a7464d01d8240ae Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 19 Jun 2021 18:23:41 +0100 Subject: [PATCH 11/11] Update documentation to include the socket intnt --- README.md | 26 +++++++++++++++++++------- app/src/cli.c | 9 ++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a073987d..eea7d8cc 100644 --- a/README.md +++ b/README.md @@ -735,11 +735,12 @@ If no value is provided with this argument, all intents are turned on. Currently, the only events that exist are: - | Option | Description | [Intent Action] | [Intent Extras] - | ----------|:----------------------------------------------|:---------------------------------|:----------------------------- - | `start` | scrcpy starts | `com.genymobile.scrcpy.START` | STARTUP: true - | `stop` | scrcpy stops (best effort) | `com.genymobile.scrcpy.STOP` | SHUTDOWN: true - | `cleaned` | scrcpy has finished cleaning up (best effort) | `com.genymobile.scrcpy.CLEANED` | SHUTDOWN: true + | Option | Description | [Intent Action] | [Intent Extras] + | ----------|:---------------------------------------------- |:---------------------------------|:--------------- + | `start` | scrcpy starts | `com.genymobile.scrcpy.START` | STARTUP: true + | `socket` | a socket for the duration of scrcpy's run created | `com.genymobile.scrcpy.SOCKET` | SOCKET: int + | `stop` | scrcpy stops (best effort) | `com.genymobile.scrcpy.STOP` | SHUTDOWN: true + | `cleaned` | scrcpy has finished cleaning up (best effort) | `com.genymobile.scrcpy.CLEANED` | SHUTDOWN: true [Intent Action]: https://developer.android.com/reference/android/content/Intent#setAction(java.lang.String) [Intent Extras]: https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable) @@ -748,11 +749,13 @@ Currently, the only events that exist are: **Important:** 1. `stop` and `cleaned` **may not happen** in specific cases. For example, if debugging is turned off, scrcpy process is immediately killed without a chance to cleanup. -2. This option is intended for advanced users. By using this +2. The only guaranteed way to know if scrcpy has exited is by listening for a connection reset on + the socket +3. This option is intended **for advanced users**. By using this feature, all apps on your phone will know scrcpy has connected Unless that is what you want, and you know what that means do not use this feature -3. In order for this argument to produce visible results you must create +4. In order for this argument to produce visible results you must create some automation to listen to android broadcast intents. Such as with your own app or with automation apps such as [Tasker]. @@ -770,9 +773,18 @@ Additionally, there are two boolean fields (that may not be present) in the extr 1. `com.genymobile.scrcpy.STARTUP` if present and `true`, scrcpy is starting up. 2. `com.genymobile.scrcpy.SHUTDOWN` if present and `true`, scrcpy is shutting down. +3. `com.genymobile.scrcpy.SOCKET` if present and an int, scrcpy has created a socket on the specified + port and you can listen to it to confirm when scrcpy exits More extra fields will be present in the future. +In case you listen to the socket provided by `com.genymobile.scrcpy.SOCKET`, note that **no information will +be exchanged through it**. Even though bytes will be transmitted through it, they are only a test to +ensure the connection is still alive and have no meaning. +A connection reset followed by connection refused when trying to reestablish the connection is the +only infallible way to ensure that scrcpy has turned off. + + For convinience with automation tools such as [Tasker], scrcpy also writes to the data field of the intents. The scheme is `scrcpy-status`. diff --git a/app/src/cli.c b/app/src/cli.c index b2814421..30b6a580 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -31,9 +31,10 @@ scrcpy_print_usage(const char *arg0) { " --broadcast-intents [value[, ...]]\n" " (Advanced feature)\n" " Turn on the broadcast of intents with the status of scrcpy \n" - " options are: start, stop, cleaned\n" + " options are: start, socket, stop, cleaned\n" " Each of these will arm the corresponding intent\n" " start: announce finished setting up\n" + " socket: announce isAlive server port\n" " stop: announce shut down started (best effort)\n" " cleaned: announce cleanup finished (best effort)\n" " \n" @@ -51,6 +52,12 @@ scrcpy_print_usage(const char *arg0) { " scrcpy is starting up.\n" " 2. com.genymobile.scrcpy.SHUTDOWN if present and true,\n" " scrcpy is shutting down.\n" + "\n" + " socket has a different extra\n" + " com.genymobile.scrcpy.SOCKET which is the port where the socket\n" + " listens to.\n" + " Listening for a connection reset on the socket is the only\n" + " guaranteed way to know when scrcpy disconnects or crashes\n" " \n" " Notes:\n" " 1. stop and cleaned may not happen in specific cases. For example, \n"