Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c2a19507 authored by Sudheer Shanka's avatar Sudheer Shanka
Browse files

Allow senders to specify "merged" delivery group policy.

Bug: 249160234
Test: atest FrameworksMockingServicesTests:BroadcastQueueTest
Test: atest FrameworksMockingServicesTests:BroadcastQueueModernImplTest
Change-Id: Ic998c98e85b17e8d6958178904d901d4c6fd553b
parent 40d6279e
Loading
Loading
Loading
Loading
+51 −0
Original line number Diff line number Diff line
@@ -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 {}
@@ -245,6 +254,14 @@ public class BroadcastOptions extends ComponentOptions {
     */
    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;
@@ -295,6 +312,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);
    }

    /**
@@ -753,6 +772,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)
@@ -760,6 +798,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() {
@@ -810,6 +852,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;
    }
}
+15 −0
Original line number Diff line number Diff line
@@ -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
+37 −10
Original line number Diff line number Diff line
@@ -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
+99 −2
Original line number Diff line number Diff line
@@ -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;
@@ -467,6 +472,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++) {
@@ -477,9 +538,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);
            }
        }
    }
}