Merge "Allow senders to specify "merged" delivery group policy."

This commit is contained in:
Sudheer Shanka
2022-10-26 15:56:07 +00:00
committed by Android (Google) Code Review
4 changed files with 202 additions and 12 deletions

View File

@@ -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.
*
* <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)
@@ -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;
}
}

View File

@@ -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}.
*
* <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

View File

@@ -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

View File

@@ -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);
}
}
}
}