diff --git a/README.md b/README.md index 0dfa068c..52ae2dab 100644 --- a/README.md +++ b/README.md @@ -721,6 +721,89 @@ The target directory can be changed on start: scrcpy --push-target=/sdcard/Movies/ ``` +### 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 + | `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) + + +**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. 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 +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]. + + +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. +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`. + +[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 ab35745d..4a0b945e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -28,6 +28,45 @@ 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, 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" + " 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" + " 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" + " 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" @@ -661,6 +700,53 @@ 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("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, socket, 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 @@ -689,6 +775,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[]) { @@ -744,6 +831,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 }, }; @@ -922,6 +1011,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 d0a22e77..fc7d9b0a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -279,6 +279,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 0a2deb71..4c4a9ee3 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -54,6 +54,15 @@ struct sc_port_range { #define SC_WINDOW_POSITION_UNDEFINED (-0x8000) + +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, +}; + + struct scrcpy_options { const char *serial; const char *crop; @@ -94,6 +103,7 @@ struct scrcpy_options { bool forward_all_clicks; bool legacy_paste; bool power_off_on_close; + uint32_t intent_broadcasts; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -142,6 +152,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 a4cdb0c9..f1abdc83 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -258,11 +258,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, @@ -296,6 +298,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 diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ec61a1c0..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; @@ -34,9 +36,10 @@ 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 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/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/Intents.java b/server/src/main/java/com/genymobile/scrcpy/Intents.java new file mode 100644 index 00000000..e6b1b1c2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Intents.java @@ -0,0 +1,37 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.util.*; + +enum Intents { + START(1), + SOCKET(2), + 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..dc0a07c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,16 +1,20 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ContentProvider; - +import android.content.Intent; 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.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 { @@ -50,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(); @@ -75,6 +80,13 @@ public final class Server { }); } + if(options.getBroadcastIntents().contains(Intents.START)){ + announceScrcpyStarting(); + } + if(options.getBroadcastIntents().contains(Intents.SOCKET)){ + scrcpyRunningSocket(); + } + try { // synchronous screenEncoder.streamScreen(device, connection.getVideoFd()); @@ -88,10 +100,92 @@ 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 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 @@ -135,7 +229,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 +282,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; } 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); + } + } }