Loading core/java/android/app/BroadcastOptions.java +91 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package android.app; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; Loading @@ -34,6 +35,10 @@ import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** Loading @@ -59,6 +64,8 @@ public class BroadcastOptions extends ComponentOptions { private boolean mIsAlarmBroadcast = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; private @Nullable String mDeliveryGroupKey; /** * Change ID which is invalid. Loading Loading @@ -190,6 +197,46 @@ public class BroadcastOptions extends ComponentOptions { private static final String KEY_REMOVE_MATCHING_FILTER = "android:broadcast.removeMatchingFilter"; /** * Corresponds to {@link #setDeliveryGroupPolicy(int)}. */ private static final String KEY_DELIVERY_GROUP_POLICY = "android:broadcast.deliveryGroupPolicy"; /** * Corresponds to {@link #setDeliveryGroupKey(String, String)}. */ private static final String KEY_DELIVERY_GROUP_KEY = "android:broadcast.deliveryGroupKey"; /** * The list of delivery group policies which specify how multiple broadcasts belonging to * the same delivery group has to be handled. * @hide */ @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = { DELIVERY_GROUP_POLICY_ALL, DELIVERY_GROUP_POLICY_MOST_RECENT, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryGroupPolicy {} /** * Delivery group policy that indicates that all the broadcasts in the delivery group * need to be delivered as is. * * @hide */ public static final int DELIVERY_GROUP_POLICY_ALL = 0; /** * Delivery group policy that indicates that only the most recent broadcast in the delivery * group need to be delivered and the rest can be dropped. * * @hide */ public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; Loading Loading @@ -236,6 +283,9 @@ public class BroadcastOptions extends ComponentOptions { mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false); mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER, IntentFilter.class); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY); } /** Loading Loading @@ -638,6 +688,41 @@ public class BroadcastOptions extends ComponentOptions { return mRemoveMatchingFilter; } /** * Set delivery group policy for this broadcast to specify how multiple broadcasts belonging to * the same delivery group has to be handled. * * @hide */ public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) { mDeliveryGroupPolicy = policy; } /** @hide */ public @DeliveryGroupPolicy int getDeliveryGroupPolicy() { return mDeliveryGroupPolicy; } /** * Set namespace and key to identify the delivery group that this broadcast belongs to. * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be * used to identify the delivery group. * * @hide */ public void setDeliveryGroupKey(@NonNull String namespace, @NonNull String key) { Preconditions.checkArgument(!namespace.contains("/"), "namespace should not contain '/'"); Preconditions.checkArgument(!key.contains("/"), "key should not contain '/'"); mDeliveryGroupKey = namespace + "/" + key; } /** @hide */ public String getDeliveryGroupKey() { return mDeliveryGroupKey; } /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) Loading Loading @@ -686,6 +771,12 @@ public class BroadcastOptions extends ComponentOptions { if (mRemoveMatchingFilter != null) { b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter); } if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) { b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy); } if (mDeliveryGroupKey != null) { b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey); } return b.isEmpty() ? null : b; } } services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +12 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ import android.annotation.Nullable; import android.annotation.UptimeMillisLong; import android.app.Activity; import android.app.ActivityManager; import android.app.BroadcastOptions; import android.app.IApplicationThread; import android.app.RemoteServiceException.CannotDeliverBroadcastException; import android.app.UidObserver; Loading Loading @@ -534,6 +535,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue { }, mBroadcastConsumerSkipAndCanceled, true); } final int policy = (r.options != null) ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) { forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { // We only allow caller to remove broadcasts they enqueued return (r.callingUid == testRecord.callingUid) && (r.userId == testRecord.userId) && r.matchesDeliveryGroup(testRecord); }, mBroadcastConsumerSkipAndCanceled, true); } if (r.isReplacePending()) { // Leave the skipped broadcasts intact in queue, so that we can // replace them at their current position during enqueue below Loading services/core/java/com/android/server/am/BroadcastRecord.java +10 −0 Original line number Diff line number Diff line Loading @@ -796,6 +796,16 @@ final class BroadcastRecord extends Binder { } } public boolean matchesDeliveryGroup(@NonNull BroadcastRecord other) { final String key = (options != null) ? options.getDeliveryGroupKey() : null; final String otherKey = (other.options != null) ? other.options.getDeliveryGroupKey() : null; if (key == null && otherKey == null) { return intent.filterEquals(other.intent); } return Objects.equals(key, otherKey); } @Override public String toString() { if (mCachedToString == null) { Loading services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +84 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,8 @@ import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.os.Bundle; import android.os.HandlerThread; import android.os.UserHandle; import android.provider.Settings; Loading Loading @@ -386,4 +388,86 @@ public class BroadcastQueueModernImplTest { assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction()); assertTrue(queue.isEmpty()); } /** * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MOST_RECENT works as expected. */ @Test public void testDeliveryGroupPolicy_mostRecent() { final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); final BroadcastOptions optionsTimeTick = BroadcastOptions.makeBasic(); optionsTimeTick.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); final Intent musicVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION); musicVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_MUSIC); final BroadcastOptions optionsMusicVolumeChanged = BroadcastOptions.makeBasic(); optionsMusicVolumeChanged.setDeliveryGroupPolicy( BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); optionsMusicVolumeChanged.setDeliveryGroupKey("audio", String.valueOf(AudioManager.STREAM_MUSIC)); final Intent alarmVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION); alarmVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_ALARM); final BroadcastOptions optionsAlarmVolumeChanged = BroadcastOptions.makeBasic(); optionsAlarmVolumeChanged.setDeliveryGroupPolicy( BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); optionsAlarmVolumeChanged.setDeliveryGroupKey("audio", String.valueOf(AudioManager.STREAM_ALARM)); // Halt all processing so that we get a consistent view mHandlerThread.getLooper().getQueue().postSyncBarrier(); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN)); // Verify that the older musicVolumeChanged has been removed. verifyPendingRecords(queue, List.of(timeTick, alarmVolumeChanged, musicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); // Verify that the older alarmVolumeChanged has been removed. verifyPendingRecords(queue, List.of(timeTick, musicVolumeChanged, alarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); // Verify that the older timeTick has been removed. verifyPendingRecords(queue, List.of(musicVolumeChanged, alarmVolumeChanged, timeTick)); } private void verifyPendingRecords(BroadcastProcessQueue queue, List<Intent> intents) { for (int i = 0; i < intents.size(); i++) { queue.makeActiveNextPending(); final Intent actualIntent = queue.getActive().intent; final Intent expectedIntent = intents.get(i); final String errMsg = "actual=" + actualIntent + ", expected=" + expectedIntent + ", actual_extras=" + actualIntent.getExtras() + ", expected_extras=" + expectedIntent.getExtras(); assertTrue(errMsg, actualIntent.filterEquals(expectedIntent)); assertTrue(errMsg, Bundle.kindofEquals( actualIntent.getExtras(), expectedIntent.getExtras())); } assertTrue(queue.isEmpty()); } } Loading
core/java/android/app/BroadcastOptions.java +91 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package android.app; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; Loading @@ -34,6 +35,10 @@ import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** Loading @@ -59,6 +64,8 @@ public class BroadcastOptions extends ComponentOptions { private boolean mIsAlarmBroadcast = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; private @Nullable String mDeliveryGroupKey; /** * Change ID which is invalid. Loading Loading @@ -190,6 +197,46 @@ public class BroadcastOptions extends ComponentOptions { private static final String KEY_REMOVE_MATCHING_FILTER = "android:broadcast.removeMatchingFilter"; /** * Corresponds to {@link #setDeliveryGroupPolicy(int)}. */ private static final String KEY_DELIVERY_GROUP_POLICY = "android:broadcast.deliveryGroupPolicy"; /** * Corresponds to {@link #setDeliveryGroupKey(String, String)}. */ private static final String KEY_DELIVERY_GROUP_KEY = "android:broadcast.deliveryGroupKey"; /** * The list of delivery group policies which specify how multiple broadcasts belonging to * the same delivery group has to be handled. * @hide */ @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = { DELIVERY_GROUP_POLICY_ALL, DELIVERY_GROUP_POLICY_MOST_RECENT, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryGroupPolicy {} /** * Delivery group policy that indicates that all the broadcasts in the delivery group * need to be delivered as is. * * @hide */ public static final int DELIVERY_GROUP_POLICY_ALL = 0; /** * Delivery group policy that indicates that only the most recent broadcast in the delivery * group need to be delivered and the rest can be dropped. * * @hide */ public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; Loading Loading @@ -236,6 +283,9 @@ public class BroadcastOptions extends ComponentOptions { mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false); mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER, IntentFilter.class); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY); } /** Loading Loading @@ -638,6 +688,41 @@ public class BroadcastOptions extends ComponentOptions { return mRemoveMatchingFilter; } /** * Set delivery group policy for this broadcast to specify how multiple broadcasts belonging to * the same delivery group has to be handled. * * @hide */ public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) { mDeliveryGroupPolicy = policy; } /** @hide */ public @DeliveryGroupPolicy int getDeliveryGroupPolicy() { return mDeliveryGroupPolicy; } /** * Set namespace and key to identify the delivery group that this broadcast belongs to. * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be * used to identify the delivery group. * * @hide */ public void setDeliveryGroupKey(@NonNull String namespace, @NonNull String key) { Preconditions.checkArgument(!namespace.contains("/"), "namespace should not contain '/'"); Preconditions.checkArgument(!key.contains("/"), "key should not contain '/'"); mDeliveryGroupKey = namespace + "/" + key; } /** @hide */ public String getDeliveryGroupKey() { return mDeliveryGroupKey; } /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) Loading Loading @@ -686,6 +771,12 @@ public class BroadcastOptions extends ComponentOptions { if (mRemoveMatchingFilter != null) { b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter); } if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) { b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy); } if (mDeliveryGroupKey != null) { b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey); } return b.isEmpty() ? null : b; } }
services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +12 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ import android.annotation.Nullable; import android.annotation.UptimeMillisLong; import android.app.Activity; import android.app.ActivityManager; import android.app.BroadcastOptions; import android.app.IApplicationThread; import android.app.RemoteServiceException.CannotDeliverBroadcastException; import android.app.UidObserver; Loading Loading @@ -534,6 +535,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue { }, mBroadcastConsumerSkipAndCanceled, true); } final int policy = (r.options != null) ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) { forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { // We only allow caller to remove broadcasts they enqueued return (r.callingUid == testRecord.callingUid) && (r.userId == testRecord.userId) && r.matchesDeliveryGroup(testRecord); }, mBroadcastConsumerSkipAndCanceled, true); } if (r.isReplacePending()) { // Leave the skipped broadcasts intact in queue, so that we can // replace them at their current position during enqueue below Loading
services/core/java/com/android/server/am/BroadcastRecord.java +10 −0 Original line number Diff line number Diff line Loading @@ -796,6 +796,16 @@ final class BroadcastRecord extends Binder { } } public boolean matchesDeliveryGroup(@NonNull BroadcastRecord other) { final String key = (options != null) ? options.getDeliveryGroupKey() : null; final String otherKey = (other.options != null) ? other.options.getDeliveryGroupKey() : null; if (key == null && otherKey == null) { return intent.filterEquals(other.intent); } return Objects.equals(key, otherKey); } @Override public String toString() { if (mCachedToString == null) { Loading
services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +84 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,8 @@ import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.os.Bundle; import android.os.HandlerThread; import android.os.UserHandle; import android.provider.Settings; Loading Loading @@ -386,4 +388,86 @@ public class BroadcastQueueModernImplTest { assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction()); assertTrue(queue.isEmpty()); } /** * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MOST_RECENT works as expected. */ @Test public void testDeliveryGroupPolicy_mostRecent() { final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); final BroadcastOptions optionsTimeTick = BroadcastOptions.makeBasic(); optionsTimeTick.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); final Intent musicVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION); musicVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_MUSIC); final BroadcastOptions optionsMusicVolumeChanged = BroadcastOptions.makeBasic(); optionsMusicVolumeChanged.setDeliveryGroupPolicy( BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); optionsMusicVolumeChanged.setDeliveryGroupKey("audio", String.valueOf(AudioManager.STREAM_MUSIC)); final Intent alarmVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION); alarmVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_ALARM); final BroadcastOptions optionsAlarmVolumeChanged = BroadcastOptions.makeBasic(); optionsAlarmVolumeChanged.setDeliveryGroupPolicy( BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); optionsAlarmVolumeChanged.setDeliveryGroupKey("audio", String.valueOf(AudioManager.STREAM_ALARM)); // Halt all processing so that we get a consistent view mHandlerThread.getLooper().getQueue().postSyncBarrier(); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN)); // Verify that the older musicVolumeChanged has been removed. verifyPendingRecords(queue, List.of(timeTick, alarmVolumeChanged, musicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); // Verify that the older alarmVolumeChanged has been removed. verifyPendingRecords(queue, List.of(timeTick, musicVolumeChanged, alarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, optionsMusicVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, optionsAlarmVolumeChanged)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); // Verify that the older timeTick has been removed. verifyPendingRecords(queue, List.of(musicVolumeChanged, alarmVolumeChanged, timeTick)); } private void verifyPendingRecords(BroadcastProcessQueue queue, List<Intent> intents) { for (int i = 0; i < intents.size(); i++) { queue.makeActiveNextPending(); final Intent actualIntent = queue.getActive().intent; final Intent expectedIntent = intents.get(i); final String errMsg = "actual=" + actualIntent + ", expected=" + expectedIntent + ", actual_extras=" + actualIntent.getExtras() + ", expected_extras=" + expectedIntent.getExtras(); assertTrue(errMsg, actualIntent.filterEquals(expectedIntent)); assertTrue(errMsg, Bundle.kindofEquals( actualIntent.getExtras(), expectedIntent.getExtras())); } assertTrue(queue.isEmpty()); } }