diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index c2df8022af0ff..13da1901e559b 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -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; /** @@ -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. @@ -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; @@ -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); } /** @@ -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) @@ -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; } } diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 1e1ebeba5c239..e421c61a2bd6b 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -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; @@ -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 diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 2d825955ed6ce..4f640033d1a41 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -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) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 90b1f4ecdcb37..abc32c9339ae1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -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; @@ -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 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()); + } }