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

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

Allow deferring initial delivery of sticky broadcasts.

If a sticky broadcast is marked as deferrable-until-active
while sending, store and use that while delivering the
broadcast to newly registered receivers for it.

Bug: 278016040
Test: atest services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
Change-Id: Ibc874a6c4296ad0962def5e46bd7b6bfa77266c5
parent 2d094275
Loading
Loading
Loading
Loading
+10 −17
Original line number Diff line number Diff line
@@ -351,6 +351,16 @@ public class BroadcastOptions extends ComponentOptions {
        mDeferralPolicy = opts.getInt(KEY_DEFERRAL_POLICY, DEFERRAL_POLICY_DEFAULT);
    }

    /** @hide */
    @NonNull
    public static BroadcastOptions makeWithDeferUntilActive(boolean deferUntilActive) {
        final BroadcastOptions opts = BroadcastOptions.makeBasic();
        if (deferUntilActive) {
            opts.setDeferralPolicy(DEFERRAL_POLICY_UNTIL_ACTIVE);
        }
        return opts;
    }

    /**
     * Set a duration for which the system should temporary place an application on the
     * power allowlist when this broadcast is being delivered to it.
@@ -774,23 +784,6 @@ public class BroadcastOptions extends ComponentOptions {
        return mIdForResponseEvent;
    }

    /** {@hide} */
    @Deprecated
    public @NonNull BroadcastOptions setDeferUntilActive(boolean shouldDefer) {
        if (shouldDefer) {
            setDeferralPolicy(DEFERRAL_POLICY_UNTIL_ACTIVE);
        } else {
            setDeferralPolicy(DEFERRAL_POLICY_NONE);
        }
        return this;
    }

    /** {@hide} */
    @Deprecated
    public boolean isDeferUntilActive() {
        return (mDeferralPolicy == DEFERRAL_POLICY_UNTIL_ACTIVE);
    }

    /**
     * Sets deferral policy for this broadcast that specifies how this broadcast
     * can be deferred for delivery at some future point.
+1 −1
Original line number Diff line number Diff line
@@ -400,7 +400,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,

    private static final Bundle USER_PRESENT_INTENT_OPTIONS =
            BroadcastOptions.makeBasic()
                    .setDeferUntilActive(true)
                    .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
                    .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
                    .toBundle();

+92 −45
Original line number Diff line number Diff line
@@ -44,7 +44,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BACKUP;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN;
@@ -1169,8 +1168,26 @@ public class ActivityManagerService extends IActivityManager.Stub
     * for stickies that are sent to all users.
     */
    @GuardedBy("this")
    final SparseArray<ArrayMap<String, ArrayList<Intent>>> mStickyBroadcasts =
            new SparseArray<ArrayMap<String, ArrayList<Intent>>>();
    final SparseArray<ArrayMap<String, ArrayList<StickyBroadcast>>> mStickyBroadcasts =
            new SparseArray<>();
    @VisibleForTesting
    static final class StickyBroadcast {
        public Intent intent;
        public boolean deferUntilActive;
        public static StickyBroadcast create(Intent intent, boolean deferUntilActive) {
            final StickyBroadcast b = new StickyBroadcast();
            b.intent = intent;
            b.deferUntilActive = deferUntilActive;
            return b;
        }
        @Override
        public String toString() {
            return "{intent=" + intent + ", defer=" + deferUntilActive + "}";
        }
    }
    final ActiveServices mServices;
@@ -2349,6 +2366,13 @@ public class ActivityManagerService extends IActivityManager.Stub
    /** Provides the basic functionality for unit tests. */
    @VisibleForTesting
    ActivityManagerService(Injector injector, @NonNull ServiceThread handlerThread) {
        this(injector, handlerThread, null);
    }
    /** Provides the basic functionality for unit tests. */
    @VisibleForTesting
    ActivityManagerService(Injector injector, @NonNull ServiceThread handlerThread,
            @Nullable UserController userController) {
        mInjector = injector;
        mContext = mInjector.getContext();
        mUiContext = null;
@@ -2375,7 +2399,7 @@ public class ActivityManagerService extends IActivityManager.Stub
        mSystemThread = null;
        mUiHandler = injector.getUiHandler(null /* service */);
        mUidObserverController = new UidObserverController(mUiHandler);
        mUserController = new UserController(this);
        mUserController = userController == null ? new UserController(this) : userController;
        mInjector.mUserController = mUserController;
        mPendingIntentController =
                new PendingIntentController(handlerThread.getLooper(), mUserController, mConstants);
@@ -10831,12 +10855,12 @@ public class ActivityManagerService extends IActivityManager.Stub
        for (int user=0; user<mStickyBroadcasts.size(); user++) {
            long token = proto.start(ActivityManagerServiceDumpBroadcastsProto.STICKY_BROADCASTS);
            proto.write(StickyBroadcastProto.USER, mStickyBroadcasts.keyAt(user));
            for (Map.Entry<String, ArrayList<Intent>> ent
            for (Map.Entry<String, ArrayList<StickyBroadcast>> ent
                    : mStickyBroadcasts.valueAt(user).entrySet()) {
                long actionToken = proto.start(StickyBroadcastProto.ACTIONS);
                proto.write(StickyBroadcastProto.StickyAction.NAME, ent.getKey());
                for (Intent intent : ent.getValue()) {
                    intent.dumpDebug(proto, StickyBroadcastProto.StickyAction.INTENTS,
                for (StickyBroadcast broadcast : ent.getValue()) {
                    broadcast.intent.dumpDebug(proto, StickyBroadcastProto.StickyAction.INTENTS,
                            false, true, true, false);
                }
                proto.end(actionToken);
@@ -10991,22 +11015,28 @@ public class ActivityManagerService extends IActivityManager.Stub
                pw.print("  Sticky broadcasts for user ");
                        pw.print(mStickyBroadcasts.keyAt(user)); pw.println(":");
                StringBuilder sb = new StringBuilder(128);
                for (Map.Entry<String, ArrayList<Intent>> ent
                for (Map.Entry<String, ArrayList<StickyBroadcast>> ent
                        : mStickyBroadcasts.valueAt(user).entrySet()) {
                    pw.print("  * Sticky action "); pw.print(ent.getKey());
                    if (dumpAll) {
                        pw.println(":");
                        ArrayList<Intent> intents = ent.getValue();
                        final int N = intents.size();
                        ArrayList<StickyBroadcast> broadcasts = ent.getValue();
                        final int N = broadcasts.size();
                        for (int i=0; i<N; i++) {
                            final Intent intent = broadcasts.get(i).intent;
                            final boolean deferUntilActive = broadcasts.get(i).deferUntilActive;
                            sb.setLength(0);
                            sb.append("    Intent: ");
                            intents.get(i).toShortString(sb, false, true, false, false);
                            pw.println(sb.toString());
                            Bundle bundle = intents.get(i).getExtras();
                            intent.toShortString(sb, false, true, false, false);
                            pw.print(sb);
                            if (deferUntilActive) {
                                pw.print(" [D]");
                            }
                            pw.println();
                            Bundle bundle = intent.getExtras();
                            if (bundle != null) {
                                pw.print("      ");
                                pw.println(bundle.toString());
                                pw.print("      extras: ");
                                pw.println(bundle);
                            }
                        }
                    } else {
@@ -13622,7 +13652,7 @@ public class ActivityManagerService extends IActivityManager.Stub
            String callerFeatureId, String receiverId, IIntentReceiver receiver,
            IntentFilter filter, String permission, int userId, int flags) {
        enforceNotIsolatedCaller("registerReceiver");
        ArrayList<Intent> stickyIntents = null;
        ArrayList<StickyBroadcast> stickyBroadcasts = null;
        ProcessRecord callerApp = null;
        final boolean visibleToInstantApps
                = (flags & Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0;
@@ -13698,14 +13728,15 @@ public class ActivityManagerService extends IActivityManager.Stub
            while (actions.hasNext()) {
                String action = actions.next();
                for (int id : userIds) {
                    ArrayMap<String, ArrayList<Intent>> stickies = mStickyBroadcasts.get(id);
                    ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
                            mStickyBroadcasts.get(id);
                    if (stickies != null) {
                        ArrayList<Intent> intents = stickies.get(action);
                        if (intents != null) {
                            if (stickyIntents == null) {
                                stickyIntents = new ArrayList<Intent>();
                        ArrayList<StickyBroadcast> broadcasts = stickies.get(action);
                        if (broadcasts != null) {
                            if (stickyBroadcasts == null) {
                                stickyBroadcasts = new ArrayList<>();
                            }
                            stickyIntents.addAll(intents);
                            stickyBroadcasts.addAll(broadcasts);
                        }
                    }
                }
@@ -13786,12 +13817,13 @@ public class ActivityManagerService extends IActivityManager.Stub
        // Dynamic receivers are exported by default for versions prior to T
        final boolean exported = (flags & Context.RECEIVER_EXPORTED) != 0;
        ArrayList<Intent> allSticky = null;
        if (stickyIntents != null) {
        ArrayList<StickyBroadcast> allSticky = null;
        if (stickyBroadcasts != null) {
            final ContentResolver resolver = mContext.getContentResolver();
            // Look for any matching sticky broadcasts...
            for (int i = 0, N = stickyIntents.size(); i < N; i++) {
                Intent intent = stickyIntents.get(i);
            for (int i = 0, N = stickyBroadcasts.size(); i < N; i++) {
                final StickyBroadcast broadcast = stickyBroadcasts.get(i);
                Intent intent = broadcast.intent;
                // Don't provided intents that aren't available to instant apps.
                if (instantApp &&
                        (intent.getFlags() & Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS) == 0) {
@@ -13803,15 +13835,15 @@ public class ActivityManagerService extends IActivityManager.Stub
                // cannot lock ActivityManagerService here.
                if (filter.match(resolver, intent, true, TAG) >= 0) {
                    if (allSticky == null) {
                        allSticky = new ArrayList<Intent>();
                        allSticky = new ArrayList<>();
                    }
                    allSticky.add(intent);
                    allSticky.add(broadcast);
                }
            }
        }
        // The first sticky in the list is returned directly back to the client.
        Intent sticky = allSticky != null ? allSticky.get(0) : null;
        Intent sticky = allSticky != null ? allSticky.get(0).intent : null;
        if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Register receiver " + filter + ": " + sticky);
        if (receiver == null) {
            return sticky;
@@ -13893,10 +13925,11 @@ public class ActivityManagerService extends IActivityManager.Stub
                final int stickyCount = allSticky.size();
                for (int i = 0; i < stickyCount; i++) {
                    Intent intent = allSticky.get(i);
                    BroadcastQueue queue = broadcastQueueForIntent(intent);
                    BroadcastRecord r = new BroadcastRecord(queue, intent, null,
                            null, null, -1, -1, false, null, null, null, null, OP_NONE, null,
                    final StickyBroadcast broadcast = allSticky.get(i);
                    BroadcastQueue queue = broadcastQueueForIntent(broadcast.intent);
                    BroadcastRecord r = new BroadcastRecord(queue, broadcast.intent, null,
                            null, null, -1, -1, false, null, null, null, null, OP_NONE,
                            BroadcastOptions.makeWithDeferUntilActive(broadcast.deferUntilActive),
                            receivers, null, null, 0, null, null, false, true, true, -1,
                            BackgroundStartPrivileges.NONE,
                            false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
@@ -14274,7 +14307,8 @@ public class ActivityManagerService extends IActivityManager.Stub
        if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
                (sticky ? "Broadcast sticky: ": "Broadcast: ") + intent
                + " ordered=" + ordered + " userid=" + userId);
                        + " ordered=" + ordered + " userid=" + userId
                        + " options=" + (brOptions == null ? "null" : brOptions.toBundle()));
        if ((resultTo != null) && !ordered) {
            if (!mEnableModernQueue) {
                Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!");
@@ -14739,15 +14773,15 @@ public class ActivityManagerService extends IActivityManager.Stub
                // But first, if this is not a broadcast to all users, then
                // make sure it doesn't conflict with an existing broadcast to
                // all users.
                ArrayMap<String, ArrayList<Intent>> stickies = mStickyBroadcasts.get(
                ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(
                        UserHandle.USER_ALL);
                if (stickies != null) {
                    ArrayList<Intent> list = stickies.get(intent.getAction());
                    ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
                    if (list != null) {
                        int N = list.size();
                        int i;
                        for (i=0; i<N; i++) {
                            if (intent.filterEquals(list.get(i))) {
                            if (intent.filterEquals(list.get(i).intent)) {
                                throw new IllegalArgumentException(
                                        "Sticky broadcast " + intent + " for user "
                                        + userId + " conflicts with existing global broadcast");
@@ -14756,27 +14790,30 @@ public class ActivityManagerService extends IActivityManager.Stub
                    }
                }
            }
            ArrayMap<String, ArrayList<Intent>> stickies = mStickyBroadcasts.get(userId);
            ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
            if (stickies == null) {
                stickies = new ArrayMap<>();
                mStickyBroadcasts.put(userId, stickies);
            }
            ArrayList<Intent> list = stickies.get(intent.getAction());
            ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
            if (list == null) {
                list = new ArrayList<>();
                stickies.put(intent.getAction(), list);
            }
            final boolean deferUntilActive = BroadcastRecord.calculateDeferUntilActive(
                    callingUid, brOptions, resultTo, ordered,
                    BroadcastRecord.calculateUrgent(intent, brOptions));
            final int stickiesCount = list.size();
            int i;
            for (i = 0; i < stickiesCount; i++) {
                if (intent.filterEquals(list.get(i))) {
                if (intent.filterEquals(list.get(i).intent)) {
                    // This sticky already exists, replace it.
                    list.set(i, new Intent(intent));
                    list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive));
                    break;
                }
            }
            if (i >= stickiesCount) {
                list.add(new Intent(intent));
                list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive));
            }
        }
@@ -14970,6 +15007,16 @@ public class ActivityManagerService extends IActivityManager.Stub
        return ActivityManager.BROADCAST_SUCCESS;
    }
    @VisibleForTesting
    ArrayList<StickyBroadcast> getStickyBroadcasts(String action, int userId) {
        final ArrayMap<String, ArrayList<StickyBroadcast>> stickyBroadcasts =
                mStickyBroadcasts.get(userId);
        if (stickyBroadcasts == null) {
            return null;
        }
        return stickyBroadcasts.get(action);
    }
    /**
     * @return uid from the extra field {@link Intent#EXTRA_UID} if present, Otherwise -1
     */
@@ -15151,14 +15198,14 @@ public class ActivityManagerService extends IActivityManager.Stub
                Slog.w(TAG, msg);
                throw new SecurityException(msg);
            }
            ArrayMap<String, ArrayList<Intent>> stickies = mStickyBroadcasts.get(userId);
            ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
            if (stickies != null) {
                ArrayList<Intent> list = stickies.get(intent.getAction());
                ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
                if (list != null) {
                    int N = list.size();
                    int i;
                    for (i=0; i<N; i++) {
                        if (intent.filterEquals(list.get(i))) {
                        if (intent.filterEquals(list.get(i).intent)) {
                            list.remove(i);
                            break;
                        }
+2 −1
Original line number Diff line number Diff line
@@ -49,7 +49,8 @@ public class ActivityManagerInternalTest {
    private static final long TEST_PROC_STATE_SEQ1 = 1111;
    private static final long TEST_PROC_STATE_SEQ2 = 1112;

    @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
    @Rule public final ApplicationExitInfoTest.ServiceThreadRule
            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();

    @Mock private ActivityManagerService.Injector mMockInjector;

+95 −3
Original line number Diff line number Diff line
@@ -48,8 +48,11 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -59,13 +62,16 @@ import static org.mockito.Mockito.when;

import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.app.IApplicationThread;
import android.app.IUidObserver;
import android.app.SyncNotedAppOp;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManagerInternal;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -84,6 +90,7 @@ import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import com.android.server.LocalServices;
import com.android.server.am.ActivityManagerService.StickyBroadcast;
import com.android.server.am.ProcessList.IsolatedUidRange;
import com.android.server.am.ProcessList.IsolatedUidRangeAllocator;
import com.android.server.am.UidObserverController.ChangeRecord;
@@ -102,10 +109,12 @@ import org.mockito.MockitoAnnotations;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;

@@ -120,6 +129,15 @@ import java.util.function.Function;
public class ActivityManagerServiceTest {
    private static final String TAG = ActivityManagerServiceTest.class.getSimpleName();

    private static final int TEST_USER = 11;

    private static final String TEST_ACTION1 = "com.android.server.am.TEST_ACTION1";
    private static final String TEST_ACTION2 = "com.android.server.am.TEST_ACTION2";
    private static final String TEST_ACTION3 = "com.android.server.am.TEST_ACTION3";

    private static final String TEST_EXTRA_KEY1 = "com.android.server.am.TEST_EXTRA_KEY1";
    private static final String TEST_EXTRA_VALUE1 = "com.android.server.am.TEST_EXTRA_VALUE1";

    private static final int TEST_UID = 11111;
    private static final int USER_ID = 666;

@@ -150,11 +168,14 @@ public class ActivityManagerServiceTest {
        LocalServices.removeServiceForTest(PackageManagerInternal.class);
    }

    @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
    @Rule
    public final ApplicationExitInfoTest.ServiceThreadRule
            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();

    private Context mContext = getInstrumentation().getTargetContext();

    @Mock private AppOpsService mAppOpsService;
    @Mock private UserController mUserController;

    private TestInjector mInjector;
    private ActivityManagerService mAms;
@@ -169,7 +190,14 @@ public class ActivityManagerServiceTest {
        mHandlerThread.start();
        mHandler = new TestHandler(mHandlerThread.getLooper());
        mInjector = new TestInjector(mContext);
        mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
        doAnswer(invocation -> {
            final int userId = invocation.getArgument(2);
            return userId;
        }).when(mUserController).handleIncomingUser(anyInt(), anyInt(), anyInt(), anyBoolean(),
                anyInt(), any(), any());
        doReturn(true).when(mUserController).isUserOrItsParentRunning(anyInt());
        mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread(),
                mUserController);
        mAms.mConstants.mNetworkAccessTimeoutMs = 2000;
        mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
        mAms.mActivityTaskManager.initialize(null, null, mHandler.getLooper());
@@ -311,7 +339,8 @@ public class ActivityManagerServiceTest {
    }

    private void verifyUidRangesNoOverlap(IsolatedUidRange uidRange1, IsolatedUidRange uidRange2) {
        IsolatedUidRange lowRange = uidRange1.mFirstUid <= uidRange2.mFirstUid ? uidRange1 : uidRange2;
        IsolatedUidRange lowRange = uidRange1.mFirstUid <= uidRange2.mFirstUid
                ? uidRange1 : uidRange2;
        IsolatedUidRange highRange = lowRange == uidRange1  ? uidRange2 : uidRange1;

        assertTrue(highRange.mFirstUid > lowRange.mLastUid);
@@ -609,6 +638,69 @@ public class ActivityManagerServiceTest {
        }
    }

    @Test
    public void testBroadcastStickyIntent() {
        final Intent intent1 = new Intent(TEST_ACTION1);
        final Intent intent2 = new Intent(TEST_ACTION2)
                .putExtra(TEST_EXTRA_KEY1, TEST_EXTRA_VALUE1);
        final Intent intent3 = new Intent(TEST_ACTION3);
        final BroadcastOptions options = BroadcastOptions.makeWithDeferUntilActive(true);

        broadcastIntent(intent1, null, true);
        assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER),
                StickyBroadcast.create(intent1, false));
        assertNull(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER));
        assertNull(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER));

        broadcastIntent(intent2, options.toBundle(), true);
        assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER),
                StickyBroadcast.create(intent1, false));
        assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER),
                StickyBroadcast.create(intent2, true));
        assertNull(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER));

        broadcastIntent(intent3, null, true);
        assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER),
                StickyBroadcast.create(intent1, false));
        assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER),
                StickyBroadcast.create(intent2, true));
        assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER),
                StickyBroadcast.create(intent3, false));
    }

    @SuppressWarnings("GuardedBy")
    private void broadcastIntent(Intent intent, Bundle options, boolean sticky) {
        final int res = mAms.broadcastIntentLocked(null, null, null, intent, null, null, 0,
                null, null, null, null, null, 0, options, false, sticky,
                Process.myPid(), Process.myUid(), Process.myUid(), Process.myPid(), TEST_USER);
        assertEquals(ActivityManager.BROADCAST_SUCCESS, res);
    }

    private void assertStickyBroadcasts(ArrayList<StickyBroadcast> actualBroadcasts,
            StickyBroadcast... expectedBroadcasts) {
        final String errMsg = "Expected: " + Arrays.toString(expectedBroadcasts)
                + "; Actual: " + Arrays.toString(actualBroadcasts.toArray());
        assertEquals(errMsg, expectedBroadcasts.length, actualBroadcasts.size());
        for (int i = 0; i < expectedBroadcasts.length; ++i) {
            final StickyBroadcast expected = expectedBroadcasts[i];
            final StickyBroadcast actual = actualBroadcasts.get(i);
            assertTrue(errMsg, areEquals(expected, actual));
        }
    }

    private boolean areEquals(StickyBroadcast a, StickyBroadcast b) {
        if (!Objects.equals(a.intent.getAction(), b.intent.getAction())) {
            return false;
        }
        if (!Bundle.kindofEquals(a.intent.getExtras(), b.intent.getExtras())) {
            return false;
        }
        if (a.deferUntilActive != b.deferUntilActive) {
            return false;
        }
        return true;
    }

    private interface ObserverChangesVerifier {
        void verify(IUidObserver observer, ChangeRecord changeItem) throws RemoteException;
    }