diff options
4 files changed, 202 insertions, 12 deletions
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 3ee3d9e60a00..48638d1fdff4 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. @@ -218,6 +220,12 @@ public class BroadcastOptions extends ComponentOptions { "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. * @hide @@ -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); } /** @@ -775,12 +794,35 @@ public class BroadcastOptions extends ComponentOptions { } /** + * Set the {@link BundleMerger} that specifies how to merge the extras data from + * broadcasts in a delivery group. + * + * <p>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) * Context.sendBroadcast(Intent)} and related methods. * 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 43fa61782bf6..f2ebec6306f8 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; @@ -11072,6 +11073,20 @@ public class Intent implements Parcelable, Cloneable { } /** + * Merge the extras data in this intent with that of other supplied intent using the + * strategy specified using {@code extrasMerger}. + * + * <p> 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 * {@link #equals equals()} and {@link #hashCode hashCode()} methods as diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 2e662b4117f3..ab19ae33a502 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 ee3815428b25..5b7b8f4ca21f 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<String> componentNameList) { + final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); + packageChangedIntent.putExtra(Intent.EXTRA_UID, uid); + packageChangedIntent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + componentNameList.toArray()); + return packageChangedIntent; + } + private void verifyPendingRecords(BroadcastProcessQueue queue, List<Intent> intents) { for (int i = 0; i < intents.size(); i++) { @@ -481,9 +542,45 @@ public class BroadcastQueueModernImplTest { + ", actual_extras=" + actualIntent.getExtras() + ", expected_extras=" + expectedIntent.getExtras(); assertTrue(errMsg, actualIntent.filterEquals(expectedIntent)); - assertTrue(errMsg, Bundle.kindofEquals( - actualIntent.getExtras(), expectedIntent.getExtras())); + assertBundleEquals(expectedIntent.getExtras(), actualIntent.getExtras()); } assertTrue(queue.isEmpty()); } + + private void assertBundleEquals(Bundle expected, Bundle actual) { + final String errMsg = "expected=" + expected + ", actual=" + actual; + if (expected == actual) { + return; + } else if (expected == null || actual == null) { + fail(errMsg); + } + if (!expected.keySet().equals(actual.keySet())) { + fail(errMsg); + } + for (String key : expected.keySet()) { + final Object expectedValue = expected.get(key); + final Object actualValue = actual.get(key); + if (expectedValue == actualValue) { + continue; + } else if (expectedValue == null || actualValue == null) { + fail(errMsg); + } + assertEquals(errMsg, expectedValue.getClass(), actualValue.getClass()); + if (expectedValue.getClass().isArray()) { + assertEquals(errMsg, Array.getLength(expectedValue), Array.getLength(actualValue)); + for (int i = 0; i < Array.getLength(expectedValue); ++i) { + assertEquals(errMsg, Array.get(expectedValue, i), Array.get(actualValue, i)); + } + } else if (expectedValue instanceof ArrayList) { + final ArrayList<?> expectedList = (ArrayList<?>) expectedValue; + final ArrayList<?> actualList = (ArrayList<?>) actualValue; + assertEquals(errMsg, expectedList.size(), actualList.size()); + for (int i = 0; i < expectedList.size(); ++i) { + assertEquals(errMsg, expectedList.get(i), actualList.get(i)); + } + } else { + assertEquals(errMsg, expectedValue, actualValue); + } + } + } } |