diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 3ee3d9e60a002..48638d1fdff46 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; +import android.os.BundleMerger; import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; @@ -67,6 +68,7 @@ public class BroadcastOptions extends ComponentOptions { private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; private @Nullable String mDeliveryGroupKey; + private @Nullable BundleMerger mDeliveryGroupExtrasMerger; /** * Change ID which is invalid. @@ -217,6 +219,12 @@ public class BroadcastOptions extends ComponentOptions { private static final String KEY_DELIVERY_GROUP_KEY = "android:broadcast.deliveryGroupKey"; + /** + * Corresponds to {@link #setDeliveryGroupExtrasMerger(BundleMerger)}. + */ + private static final String KEY_DELIVERY_GROUP_EXTRAS_MERGER = + "android:broadcast.deliveryGroupExtrasMerger"; + /** * The list of delivery group policies which specify how multiple broadcasts belonging to * the same delivery group has to be handled. @@ -225,6 +233,7 @@ public class BroadcastOptions extends ComponentOptions { @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = { DELIVERY_GROUP_POLICY_ALL, DELIVERY_GROUP_POLICY_MOST_RECENT, + DELIVERY_GROUP_POLICY_MERGED, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryGroupPolicy {} @@ -247,6 +256,14 @@ public class BroadcastOptions extends ComponentOptions { @SystemApi public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; + /** + * Delivery group policy that indicates that the extras data from the broadcasts in the + * delivery group need to be merged into a single broadcast and the rest can be dropped. + * + * @hide + */ + public static final int DELIVERY_GROUP_POLICY_MERGED = 2; + public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; @@ -297,6 +314,8 @@ public class BroadcastOptions extends ComponentOptions { mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY); + mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, + BundleMerger.class); } /** @@ -774,6 +793,25 @@ public class BroadcastOptions extends ComponentOptions { return mDeliveryGroupKey; } + /** + * Set the {@link BundleMerger} that specifies how to merge the extras data from + * broadcasts in a delivery group. + * + *
Note that this value will be ignored if the delivery group policy is not set as + * {@link #DELIVERY_GROUP_POLICY_MERGED}. + * + * @hide + */ + public void setDeliveryGroupExtrasMerger(@NonNull BundleMerger extrasMerger) { + Preconditions.checkNotNull(extrasMerger); + mDeliveryGroupExtrasMerger = extrasMerger; + } + + /** @hide */ + public @Nullable BundleMerger getDeliveryGroupExtrasMerger() { + return mDeliveryGroupExtrasMerger; + } + /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) @@ -781,6 +819,10 @@ public class BroadcastOptions extends ComponentOptions { * Note that the returned Bundle is still owned by the BroadcastOptions * object; you must not modify it, but can supply it to the sendBroadcast * methods that take an options Bundle. + * + * @throws IllegalStateException if the broadcast option values are inconsistent. For example, + * if the delivery group policy is specified as "MERGED" but no + * extras merger is supplied. */ @Override public Bundle toBundle() { @@ -831,6 +873,15 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupKey != null) { b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey); } + if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) { + if (mDeliveryGroupExtrasMerger != null) { + b.putParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, + mDeliveryGroupExtrasMerger); + } else { + throw new IllegalStateException("Extras merger cannot be empty " + + "when delivery group policy is 'MERGED'"); + } + } return b.isEmpty() ? null : b; } } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 43fa61782bf6a..f2ebec6306f80 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -49,6 +49,7 @@ import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.BundleMerger; import android.os.IBinder; import android.os.IncidentManager; import android.os.Parcel; @@ -11071,6 +11072,20 @@ public class Intent implements Parcelable, Cloneable { return changes; } + /** + * Merge the extras data in this intent with that of other supplied intent using the + * strategy specified using {@code extrasMerger}. + * + *
Note the extras data in this intent is treated as the {@code first} param
+ * and the extras data in {@code other} intent is treated as the {@code last} param
+ * when using the passed in {@link BundleMerger} object.
+ *
+ * @hide
+ */
+ public void mergeExtras(@NonNull Intent other, @NonNull BundleMerger extrasMerger) {
+ mExtras = extrasMerger.merge(mExtras, other.mExtras);
+ }
+
/**
* Wrapper class holding an Intent and implementing comparisons on it for
* the purpose of filtering. The class implements its
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 2e662b4117f3f..ab19ae33a5020 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -58,6 +58,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
+import android.os.BundleMerger;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
@@ -543,16 +544,7 @@ 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);
- }
+ applyDeliveryGroupPolicy(r);
if (r.isReplacePending()) {
// Leave the skipped broadcasts intact in queue, so that we can
@@ -609,6 +601,41 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
}
}
+ private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
+ final int policy = (r.options != null)
+ ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
+ final BroadcastConsumer broadcastConsumer;
+ switch (policy) {
+ case BroadcastOptions.DELIVERY_GROUP_POLICY_ALL:
+ // Older broadcasts need to be left as is in this case, so nothing more to do.
+ return;
+ case BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT:
+ broadcastConsumer = mBroadcastConsumerSkipAndCanceled;
+ break;
+ case BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED:
+ final BundleMerger extrasMerger = r.options.getDeliveryGroupExtrasMerger();
+ if (extrasMerger == null) {
+ // Extras merger is required to be able to merge the extras. So, if it's not
+ // supplied, then ignore the delivery group policy.
+ return;
+ }
+ broadcastConsumer = (record, recordIndex) -> {
+ r.intent.mergeExtras(record.intent, extrasMerger);
+ mBroadcastConsumerSkipAndCanceled.accept(record, recordIndex);
+ };
+ break;
+ default:
+ logw("Unknown delivery group policy: " + policy);
+ return;
+ }
+ 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);
+ }, broadcastConsumer, true);
+ }
+
/**
* Schedule the currently active broadcast on the given queue when we know
* the process is cold. This kicks off a cold start and will eventually call
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 ee3815428b25b..5b7b8f4ca21fb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -33,6 +33,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import android.annotation.NonNull;
@@ -43,6 +44,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Bundle;
+import android.os.BundleMerger;
import android.os.HandlerThread;
import android.os.UserHandle;
import android.provider.Settings;
@@ -57,12 +59,15 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(MockitoJUnitRunner.class)
public class BroadcastQueueModernImplTest {
private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID;
+ private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1;
@Mock ActivityManagerService mAms;
@Mock ProcessRecord mProcess;
@@ -471,6 +476,62 @@ public class BroadcastQueueModernImplTest {
List.of(musicVolumeChanged, alarmVolumeChanged, timeTick));
}
+ /**
+ * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MERGED works as expected.
+ */
+ @Test
+ public void testDeliveryGroupPolicy_merged() {
+ final BundleMerger extrasMerger = new BundleMerger();
+ extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+ BundleMerger.STRATEGY_ARRAY_APPEND);
+
+ final Intent packageChangedForUid = createPackageChangedIntent(TEST_UID,
+ List.of("com.testuid.component1"));
+ final BroadcastOptions optionsPackageChangedForUid = BroadcastOptions.makeBasic();
+ optionsPackageChangedForUid.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+ optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID));
+ optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger);
+
+ final Intent secondPackageChangedForUid = createPackageChangedIntent(TEST_UID,
+ List.of("com.testuid.component2", "com.testuid.component3"));
+
+ final Intent packageChangedForUid2 = createPackageChangedIntent(TEST_UID2,
+ List.of("com.testuid2.component1"));
+ final BroadcastOptions optionsPackageChangedForUid2 = BroadcastOptions.makeBasic();
+ optionsPackageChangedForUid.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+ optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID2));
+ optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger);
+
+ // Halt all processing so that we get a consistent view
+ mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid,
+ optionsPackageChangedForUid));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid2,
+ optionsPackageChangedForUid2));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(secondPackageChangedForUid,
+ optionsPackageChangedForUid));
+
+ final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ final Intent expectedPackageChangedForUid = createPackageChangedIntent(TEST_UID,
+ List.of("com.testuid.component2", "com.testuid.component3",
+ "com.testuid.component1"));
+ // Verify that packageChangedForUid and secondPackageChangedForUid broadcasts
+ // have been merged.
+ verifyPendingRecords(queue, List.of(packageChangedForUid2, expectedPackageChangedForUid));
+ }
+
+ private Intent createPackageChangedIntent(int uid, List