diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index f5d1c106705dc..fe5888df238ed 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -236,6 +236,15 @@ public class BroadcastConstants { private static final int DEFAULT_MAX_HISTORY_SUMMARY_SIZE = ActivityManager.isLowRamDeviceStatic() ? 25 : 300; + /** + * For {@link BroadcastQueueModernImpl}: Maximum number of broadcast receivers to process in a + * single synchronized block. Up to this many messages may be dispatched in a single binder + * call. Set this to 1 (or zero) for pre-batch behavior. + */ + public int MAX_BROADCAST_BATCH_SIZE = DEFAULT_MAX_BROADCAST_BATCH_SIZE; + private static final String KEY_MAX_BROADCAST_BATCH_SIZE = "bcast_max_batch_size"; + private static final int DEFAULT_MAX_BROADCAST_BATCH_SIZE = 1; + // Settings override tracking for this instance private String mSettingsKey; private SettingsObserver mSettingsObserver; @@ -373,6 +382,8 @@ public class BroadcastConstants { DEFAULT_MAX_HISTORY_COMPLETE_SIZE); MAX_HISTORY_SUMMARY_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_SUMMARY_SIZE, DEFAULT_MAX_HISTORY_SUMMARY_SIZE); + MAX_BROADCAST_BATCH_SIZE = getDeviceConfigInt(KEY_MAX_BROADCAST_BATCH_SIZE, + DEFAULT_MAX_BROADCAST_BATCH_SIZE); } } @@ -418,6 +429,7 @@ public class BroadcastConstants { MAX_CONSECUTIVE_URGENT_DISPATCHES).println(); pw.print(KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES, MAX_CONSECUTIVE_NORMAL_DISPATCHES).println(); + pw.print(KEY_MAX_BROADCAST_BATCH_SIZE, MAX_BROADCAST_BATCH_SIZE).println(); pw.decreaseIndent(); pw.println(); } diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 1a72fef0683e2..8946ada1daf97 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -73,6 +73,7 @@ import android.util.Slog; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.TimeoutRecord; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; @@ -186,6 +187,14 @@ public class BroadcastQueueImpl extends BroadcastQueue { } } + /** + * This single object allows the queue to dispatch receivers using scheduleReceiverList + * without constantly allocating new ReceiverInfo objects or ArrayLists. This queue + * implementation is known to have a maximum size of one entry. + */ + @VisibleForTesting + final BroadcastReceiverBatch mReceiverBatch = new BroadcastReceiverBatch(1); + BroadcastQueueImpl(ActivityManagerService service, Handler handler, String name, BroadcastConstants constants, boolean allowDelayBehindServices, int schedGroup) { @@ -388,10 +397,11 @@ public class BroadcastQueueImpl extends BroadcastQueue { + ": " + r); mService.notifyPackageUse(r.intent.getComponent().getPackageName(), PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER); - thread.scheduleReceiver(prepareReceiverIntent(r.intent, r.curFilteredExtras), + thread.scheduleReceiverList(mReceiverBatch.manifestReceiver( + prepareReceiverIntent(r.intent, r.curFilteredExtras), r.curReceiver, null /* compatInfo (unused but need to keep method signature) */, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId, - app.mState.getReportedProcState()); + app.mState.getReportedProcState())); if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Process cur broadcast " + r + " DELIVERED for app " + app); started = true; @@ -725,9 +735,10 @@ public class BroadcastQueueImpl extends BroadcastQueue { // If we have an app thread, do the call through that so it is // correctly ordered with other one-way calls. try { - thread.scheduleRegisteredReceiver(receiver, intent, resultCode, + thread.scheduleReceiverList(mReceiverBatch.registeredReceiver( + receiver, intent, resultCode, data, extras, ordered, sticky, sendingUser, - app.mState.getReportedProcState()); + app.mState.getReportedProcState())); } catch (RemoteException ex) { // Failed to call into the process. It's either dying or wedged. Kill it gently. synchronized (mService) { diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index eb5c03bec5606..baa906fc3db0c 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -201,6 +201,14 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private final BroadcastConstants mFgConstants; private final BroadcastConstants mBgConstants; + /** + * This single object allows the queue to dispatch receivers using scheduleReceiverList + * without constantly allocating new ReceiverInfo objects or ArrayLists. This queue + * implementation is known to have a maximum size of one entry. + */ + @VisibleForTesting + final BroadcastReceiverBatch mReceiverBatch = new BroadcastReceiverBatch(1); + /** * Timestamp when last {@link #testAllProcessQueues} failure was observed; * used for throttling log messages. @@ -763,7 +771,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * Examine a receiver and possibly skip it. The method returns true if the receiver is * skipped (and therefore no more work is required). */ - private boolean maybeSkipReceiver(BroadcastProcessQueue queue, BroadcastRecord r, int index) { + private boolean maybeSkipReceiver(@NonNull BroadcastProcessQueue queue, + @NonNull BroadcastRecord r, int index) { final int oldDeliveryState = getDeliveryState(r, index); final ProcessRecord app = queue.app; final Object receiver = r.receivers.get(index); @@ -803,14 +812,15 @@ class BroadcastQueueModernImpl extends BroadcastQueue { /** * Return true if this receiver should be assumed to have been delivered. */ - private boolean isAssumedDelivered(BroadcastRecord r, int index) { + private boolean isAssumedDelivered(@NonNull BroadcastRecord r, int index) { return (r.receivers.get(index) instanceof BroadcastFilter) && !r.ordered; } /** * A receiver is about to be dispatched. Start ANR timers, if necessary. */ - private void dispatchReceivers(BroadcastProcessQueue queue, BroadcastRecord r, int index) { + private void dispatchReceivers(@NonNull BroadcastProcessQueue queue, + @NonNull BroadcastRecord r, int index) { final ProcessRecord app = queue.app; final Object receiver = r.receivers.get(index); @@ -849,16 +859,16 @@ class BroadcastQueueModernImpl extends BroadcastQueue { setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED, "scheduleReceiverWarmLocked"); - final Intent receiverIntent = r.getReceiverIntent(receiver); final IApplicationThread thread = app.getOnewayThread(); if (thread != null) { try { + final Intent receiverIntent = r.getReceiverIntent(receiver); if (receiver instanceof BroadcastFilter) { notifyScheduleRegisteredReceiver(app, r, (BroadcastFilter) receiver); - thread.scheduleRegisteredReceiver( + thread.scheduleReceiverList(mReceiverBatch.registeredReceiver( ((BroadcastFilter) receiver).receiverList.receiver, receiverIntent, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.initialSticky, - r.userId, app.mState.getReportedProcState()); + r.userId, app.mState.getReportedProcState())); // TODO: consider making registered receivers of unordered // broadcasts report results to detect ANRs @@ -868,9 +878,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } } else { notifyScheduleReceiver(app, r, (ResolveInfo) receiver); - thread.scheduleReceiver(receiverIntent, ((ResolveInfo) receiver).activityInfo, + thread.scheduleReceiverList(mReceiverBatch.manifestReceiver( + receiverIntent, ((ResolveInfo) receiver).activityInfo, null, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId, - app.mState.getReportedProcState()); + app.mState.getReportedProcState())); } } catch (RemoteException e) { final String msg = "Failed to schedule " + r + " to " + receiver @@ -898,9 +909,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily( app, OOM_ADJ_REASON_FINISH_RECEIVER); try { - thread.scheduleRegisteredReceiver(r.resultTo, r.intent, + thread.scheduleReceiverList(mReceiverBatch.registeredReceiver(r.resultTo, r.intent, r.resultCode, r.resultData, r.resultExtras, false, r.initialSticky, - r.userId, app.mState.getReportedProcState()); + r.userId, app.mState.getReportedProcState())); } catch (RemoteException e) { final String msg = "Failed to schedule result of " + r + " via " + app + ": " + e; logw(msg); diff --git a/services/core/java/com/android/server/am/BroadcastReceiverBatch.java b/services/core/java/com/android/server/am/BroadcastReceiverBatch.java new file mode 100644 index 0000000000000..a8264582b8b36 --- /dev/null +++ b/services/core/java/com/android/server/am/BroadcastReceiverBatch.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ReceiverInfo; +import android.content.Intent; +import android.content.IIntentReceiver; +import android.content.pm.ActivityInfo; +import android.content.res.CompatibilityInfo; +import android.os.Bundle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A batch of receiver instructions. This includes a list of finish requests and a list of + * receivers. The instructions are for a single queue. It is constructed and consumed in a single + * call to {@link BroadcastQueueModernImpl#scheduleReceiverWarmLocked}. The list size is bounded by + * {@link BroadcastConstants#MAX_BROADCAST_BATCH_SIZE}. Because this class is ephemeral and its use + * is bounded, it is pre-allocated to avoid allocating new objects every time it is used. + * + * The {@link #single} methods support the use of this class in {@link BroadcastQueueImpl}. These + * methods simplify the use of {@link IApplicationThread#scheduleReceiverList} as a replacement + * for scheduleReceiver and scheduleRegisteredReceiver. + * + * This class is designed to be allocated once and constantly reused (call {@link #reset} between + * uses). Objects needed by the instance are kept in a pool - all object allocation occurs when + * the instance is created, and no allocation occurs thereafter. However, if the variable + * mDeepReceiverCopy is set true (it is false by default) then the method {@link #receivers} always + * returns newly allocated objects. This is required for mock testing. + * + * This class is not thread-safe. Instances must be protected by the caller. + * @hide + */ +final class BroadcastReceiverBatch { + + /** + * If this is true then receivers() returns a deep copy of the ReceiveInfo array. If this is + * false, receivers() returns a reference to the array. A deep copy is needed only for the + * broadcast queue mocking tests. + */ + @VisibleForTesting + boolean mDeepReceiverCopy = false; + + /** + * A private pool implementation this class. + */ + private static class Pool { + final int size; + final ArrayList pool; + int next; + Pool(int n, @NonNull Class c) { + size = n; + pool = new ArrayList<>(size); + try { + for (int i = 0; i < size; i++) { + pool.add(c.getDeclaredConstructor().newInstance()); + } + } catch (Exception e) { + // This class is only used locally. Turn any exceptions into something fatal. + throw new RuntimeException(e); + } + } + T next() { + return pool.get(next++); + } + void reset() { + next = 0; + } + } + + /** + * The information needed to finish off a receiver. This is valid only in the context of + * a queue. + */ + static class FinishInfo { + BroadcastRecord r = null; + int index = 0; + int deliveryState = 0; + String reason = null; + FinishInfo set(@Nullable BroadcastRecord r, int index, + int deliveryState, @Nullable String reason) { + this.r = r; + this.index = index; + this.deliveryState = deliveryState; + this.reason = reason; + return this; + } + } + + /** + * The information needed to recreate a receiver info. The broadcast record can be null if the + * caller does not expect to need it later. + */ + static class ReceiverCookie { + BroadcastRecord r = null; + int index = 0; + ReceiverCookie set(@Nullable BroadcastRecord r, int index) { + this.r = r; + this.index = index; + return this; + } + } + + // The object pools. + final int mSize; + private final Pool receiverPool; + private final Pool finishPool; + private final Pool cookiePool; + + // The accumulated data. The receivers should be an ArrayList to be directly compatible + // with scheduleReceiverList(). The receivers array is not final because a new array must + // be created for every new call to scheduleReceiverList(). + private final ArrayList mReceivers; + private final ArrayList mCookies; + private final ArrayList mFinished; + // The list of finish records to complete if the binder succeeds or fails. + private final ArrayList mSuccess; + + BroadcastReceiverBatch(int size) { + mSize = size; + mReceivers = new ArrayList<>(mSize); + mCookies = new ArrayList<>(mSize); + mFinished = new ArrayList<>(mSize); + mSuccess = new ArrayList<>(mSize); + + receiverPool = new Pool<>(mSize, ReceiverInfo.class); + finishPool = new Pool<>(mSize, FinishInfo.class); + cookiePool = new Pool<>(mSize, ReceiverCookie.class); + mStats = new Statistics(mSize); + reset(); + } + + void reset() { + mReceivers.clear(); + mCookies.clear(); + mFinished.clear(); + mSuccess.clear(); + + receiverPool.reset(); + finishPool.reset(); + cookiePool.reset(); + } + + void finish(@Nullable BroadcastRecord r, int index, + int deliveryState, @Nullable String reason) { + mFinished.add(finishPool.next().set(r, index, deliveryState, reason)); + } + void success(@Nullable BroadcastRecord r, int index, + int deliveryState, @Nullable String reason) { + mSuccess.add(finishPool.next().set(r, index, deliveryState, reason)); + } + // Add a ReceiverInfo for a registered receiver. + void schedule(@Nullable IIntentReceiver receiver, Intent intent, + int resultCode, @Nullable String data, @Nullable Bundle extras, boolean ordered, + boolean sticky, int sendingUser, int processState, + @Nullable BroadcastRecord r, int index) { + ReceiverInfo ri = new ReceiverInfo(); + ri.intent = intent; + ri.data = data; + ri.extras = extras; + ri.sendingUser = sendingUser; + ri.processState = processState; + ri.resultCode = resultCode; + ri.registered = true; + ri.receiver = receiver; + ri.ordered = ordered; + ri.sticky = sticky; + mReceivers.add(ri); + mCookies.add(cookiePool.next().set(r, index)); + } + // Add a ReceiverInfo for a manifest receiver. + void schedule(@Nullable Intent intent, @Nullable ActivityInfo activityInfo, + @Nullable CompatibilityInfo compatInfo, int resultCode, @Nullable String data, + @Nullable Bundle extras, boolean sync, int sendingUser, int processState, + @Nullable BroadcastRecord r, int index) { + ReceiverInfo ri = new ReceiverInfo(); + ri.intent = intent; + ri.data = data; + ri.extras = extras; + ri.sendingUser = sendingUser; + ri.processState = processState; + ri.resultCode = resultCode; + ri.registered = false; + ri.activityInfo = activityInfo; + ri.compatInfo = compatInfo; + ri.sync = sync; + mReceivers.add(ri); + mCookies.add(cookiePool.next().set(r, index)); + } + + /** + * Two convenience functions for dispatching a single receiver. The functions start with a + * reset. Then they create the ReceiverInfo array and return it. Statistics are not + * collected. + */ + ArrayList registeredReceiver(@Nullable IIntentReceiver receiver, + @Nullable Intent intent, int resultCode, @Nullable String data, + @Nullable Bundle extras, boolean ordered, boolean sticky, + int sendingUser, int processState) { + reset(); + schedule(receiver, intent, resultCode, data, extras, ordered, sticky, + sendingUser, processState, null, 0); + return receivers(); + } + + ArrayList manifestReceiver(@Nullable Intent intent, + @Nullable ActivityInfo activityInfo, @Nullable CompatibilityInfo compatInfo, + int resultCode, @Nullable String data, @Nullable Bundle extras, boolean sync, + int sendingUser, int processState) { + reset(); + schedule(intent, activityInfo, compatInfo, resultCode, data, extras, sync, + sendingUser, processState, null, 0); + return receivers(); + } + + // Return true if the batch is full. Adding any more entries will throw an exception. + boolean isFull() { + return (mFinished.size() + mReceivers.size()) >= mSize; + } + int finishCount() { + return mFinished.size() + mSuccess.size(); + } + int receiverCount() { + return mReceivers.size(); + } + + /** + * Create a deep copy of the receiver list. This is only for testing which is confused when + * objects are reused. + */ + private ArrayList copyReceiverInfo() { + ArrayList copy = new ArrayList<>(); + for (int i = 0; i < mReceivers.size(); i++) { + final ReceiverInfo r = mReceivers.get(i); + final ReceiverInfo n = new ReceiverInfo(); + n.intent = r.intent; + n.data = r.data; + n.extras = r.extras; + n.sendingUser = r.sendingUser; + n.processState = r.processState; + n.resultCode = r.resultCode; + n.registered = r.registered; + n.receiver = r.receiver; + n.ordered = r.ordered; + n.sticky = r.sticky; + n.activityInfo = r.activityInfo; + n.compatInfo = r.compatInfo; + n.sync = r.sync; + copy.add(n); + } + return copy; + } + + /** + * Accessors for the accumulated instructions. The important accessor is receivers(), since + * it can be modified to return a deep copy of the mReceivers array. + */ + @NonNull + ArrayList receivers() { + if (!mDeepReceiverCopy) { + return mReceivers; + } else { + return copyReceiverInfo(); + } + } + @NonNull + ArrayList cookies() { + return mCookies; + } + @NonNull + ArrayList finished() { + return mFinished; + } + @NonNull + ArrayList success() { + return mSuccess; + } + + /** + * A simple POD for statistics. The parameter is the size of the BroadcastReceiverBatch. + */ + static class Statistics { + final int[] finish; + final int[] local; + final int[] remote; + Statistics(int size) { + finish = new int[size+1]; + local = new int[size+1]; + remote = new int[size+1]; + } + } + + private final Statistics mStats; + + /** + * A unique counter that identifies individual transmission groups. This is only used for + * debugging. It is used to determine which receivers were sent in the same batch, and in + * which order. This is static to distinguish between batches across all queues in the + * system. + */ + private static final AtomicInteger sTransmitGroup = new AtomicInteger(0); + + /** + * Record statistics for this batch of instructions. This updates the local statistics and it + * updates the transmitGroup and transmitOrder fields of the BroadcastRecords being + * dispatched. + */ + void recordBatch(boolean local) { + final int group = sTransmitGroup.addAndGet(1); + for (int i = 0; i < cookies().size(); i++) { + final var cookie = cookies().get(i); + cookie.r.transmitGroup[cookie.index] = group; + cookie.r.transmitOrder[cookie.index] = i; + } + mStats.finish[finishCount()]++; + if (local) { + mStats.local[receiverCount()]++; + } else { + mStats.remote[receiverCount()]++; + } + } + + @NonNull + Statistics getStatistics() { + return mStats; + } +} diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 100b2db691de4..24cf3d2800638 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -113,6 +113,8 @@ final class BroadcastRecord extends Binder { @UptimeMillisLong long finishTime; // when broadcast finished final @UptimeMillisLong long[] scheduledTime; // when each receiver was scheduled final @UptimeMillisLong long[] terminalTime; // when each receiver was terminal + final int[] transmitGroup; // the batch group for each receiver + final int[] transmitOrder; // the position of the receiver in the group final boolean timeoutExempt; // true if this broadcast is not subject to receiver timeouts int resultCode; // current result code value. @Nullable String resultData; // current result data value. @@ -380,6 +382,8 @@ final class BroadcastRecord extends Binder { blockedUntilTerminalCount = calculateBlockedUntilTerminalCount(receivers, _serialized); scheduledTime = new long[delivery.length]; terminalTime = new long[delivery.length]; + transmitGroup = new int[delivery.length]; + transmitOrder = new int[delivery.length]; resultToApp = _resultToApp; resultTo = _resultTo; resultCode = _resultCode; @@ -433,6 +437,8 @@ final class BroadcastRecord extends Binder { blockedUntilTerminalCount = from.blockedUntilTerminalCount; scheduledTime = from.scheduledTime; terminalTime = from.terminalTime; + transmitGroup = from.transmitGroup; + transmitOrder = from.transmitOrder; resultToApp = from.resultToApp; resultTo = from.resultTo; enqueueTime = from.enqueueTime; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index b9615f6b3771c..d79c4d8d83af8 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -50,6 +50,7 @@ import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.IApplicationThread; +import android.app.ReceiverInfo; import android.app.usage.UsageEvents.Event; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; @@ -62,6 +63,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; +import android.content.res.CompatibilityInfo; import android.os.Binder; import android.os.Bundle; import android.os.DeadObjectException; @@ -262,6 +264,7 @@ public class BroadcastQueueTest { return res; }).when(mAms).startProcessLocked(any(), any(), anyBoolean(), anyInt(), any(), anyInt(), anyBoolean(), anyBoolean()); + doAnswer((invocation) -> { final String processName = invocation.getArgument(0); final int uid = invocation.getArgument(1); @@ -295,12 +298,16 @@ public class BroadcastQueueTest { }; if (mImpl == Impl.DEFAULT) { - mQueue = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG, + var q = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG, constants, emptySkipPolicy, emptyHistory, false, ProcessList.SCHED_GROUP_DEFAULT); + q.mReceiverBatch.mDeepReceiverCopy = true; + mQueue = q; } else if (mImpl == Impl.MODERN) { - mQueue = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), + var q = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), constants, constants, emptySkipPolicy, emptyHistory); + q.mReceiverBatch.mDeepReceiverCopy = true; + mQueue = q; } else { throw new UnsupportedOperationException(); } @@ -398,6 +405,43 @@ public class BroadcastQueueTest { UnaryOperator.identity()); } + private void doRegisteredReceiver(ProcessRecord r, boolean wedge, boolean abort, + UnaryOperator extrasOperator, ReceiverInfo info) { + final Intent intent = info.intent; + final Bundle extras = info.extras; + final boolean ordered = info.ordered; + mScheduledBroadcasts.add(makeScheduledBroadcast(r, intent)); + if (!wedge && ordered) { + assertTrue(r.mReceivers.numberOfCurReceivers() > 0); + assertNotEquals(ProcessList.SCHED_GROUP_UNDEFINED, + mQueue.getPreferredSchedulingGroupLocked(r)); + mHandlerThread.getThreadHandler().post(() -> { + synchronized (mAms) { + mQueue.finishReceiverLocked(r, Activity.RESULT_OK, + null, extrasOperator.apply(extras), abort, false); + } + }); + } + } + + private void doManifestReceiver(ProcessRecord r, boolean wedge, boolean abort, + UnaryOperator extrasOperator, ReceiverInfo info) { + final Intent intent = info.intent; + final Bundle extras = info.extras; + mScheduledBroadcasts.add(makeScheduledBroadcast(r, intent)); + if (!wedge) { + assertTrue(r.mReceivers.numberOfCurReceivers() > 0); + assertNotEquals(ProcessList.SCHED_GROUP_UNDEFINED, + mQueue.getPreferredSchedulingGroupLocked(r)); + mHandlerThread.getThreadHandler().post(() -> { + synchronized (mAms) { + mQueue.finishReceiverLocked(r, Activity.RESULT_OK, null, + extrasOperator.apply(extras), abort, false); + } + }); + } + } + private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, String processName, ProcessBehavior behavior, UnaryOperator extrasOperator) throws Exception { final boolean wedge = (behavior == ProcessBehavior.WEDGE); @@ -441,47 +485,22 @@ public class BroadcastQueueTest { if (dead) return r; doAnswer((invocation) -> { - Log.v(TAG, "Intercepting scheduleReceiver() for " + Log.v(TAG, "Intercepting scheduleReceiverList() for " + Arrays.toString(invocation.getArguments())); - final Intent intent = invocation.getArgument(0); - final Bundle extras = invocation.getArgument(5); - mScheduledBroadcasts.add(makeScheduledBroadcast(r, intent)); - if (!wedge) { - assertTrue(r.mReceivers.numberOfCurReceivers() > 0); - assertNotEquals(ProcessList.SCHED_GROUP_UNDEFINED, - mQueue.getPreferredSchedulingGroupLocked(r)); - mHandlerThread.getThreadHandler().post(() -> { - synchronized (mAms) { - mQueue.finishReceiverLocked(r, Activity.RESULT_OK, null, - extrasOperator.apply(extras), abort, false); - } - }); + final List data = invocation.getArgument(0); + for (int i = 0; i < data.size(); i++) { + ReceiverInfo info = data.get(i); + // The logic here mimics the logic in ActivityThread: elements of the list are + // forwarded to a handler for manifest receivers or to a handler for registered + // receivers. + if (info.registered) { + doRegisteredReceiver(r, wedge, abort, extrasOperator, info); + } else { + doManifestReceiver(r, wedge, abort, extrasOperator, info); + } } return null; - }).when(thread).scheduleReceiver(any(), any(), any(), anyInt(), any(), any(), anyBoolean(), - anyInt(), anyInt()); - - doAnswer((invocation) -> { - Log.v(TAG, "Intercepting scheduleRegisteredReceiver() for " - + Arrays.toString(invocation.getArguments())); - final Intent intent = invocation.getArgument(1); - final Bundle extras = invocation.getArgument(4); - final boolean ordered = invocation.getArgument(5); - mScheduledBroadcasts.add(makeScheduledBroadcast(r, intent)); - if (!wedge && ordered) { - assertTrue(r.mReceivers.numberOfCurReceivers() > 0); - assertNotEquals(ProcessList.SCHED_GROUP_UNDEFINED, - mQueue.getPreferredSchedulingGroupLocked(r)); - mHandlerThread.getThreadHandler().post(() -> { - synchronized (mAms) { - mQueue.finishReceiverLocked(r, Activity.RESULT_OK, - null, extrasOperator.apply(extras), abort, false); - } - }); - } - return null; - }).when(thread).scheduleRegisteredReceiver(any(), any(), anyInt(), any(), any(), - anyBoolean(), anyBoolean(), anyInt(), anyInt()); + }).when(thread).scheduleReceiverList(any()); return r; } @@ -623,6 +642,123 @@ public class BroadcastQueueTest { }; } + private static boolean matchElement(T a, T b) { + return a == null || a.equals(b); + } + private static boolean matchObject(ArgumentMatcher m, T b) { + return m == null || m.matches(b); + } + + /** + * Create an ArgumentMatcher for a manifest receiver. The parameters are in the order of + * {@link IApplicationThread#scheduleReceiver} but the names correspond to the field names in + * {@link ReceiverInfo}. For every parameter, a null means "don't care". + */ + private ArgumentMatcher manifestReceiverMatcher( + ArgumentMatcher intent, + ArgumentMatcher activityInfo, + ArgumentMatcher compatInfo, + Integer resultCode, + ArgumentMatcher data, + ArgumentMatcher extras, + Boolean sync, + Integer sendingUser, + Integer processState) { + return (test) -> { + return test.registered == false + && matchObject(intent, test.intent) + && matchObject(activityInfo, test.activityInfo) + && matchObject(compatInfo, test.compatInfo) + && matchElement(resultCode, test.resultCode) + && matchObject(data, test.data) + && matchObject(extras, test.extras) + && matchElement(sync, test.sync) + && matchElement(sendingUser, test.sendingUser) + && matchElement(processState, test.processState); + }; + } + + + /** + * Create an argument suitable for the verify() mock methods, when the goal is to find a call + * containing a manifest receiver. + */ + private List manifestReceiver( + ArgumentMatcher intent, + ArgumentMatcher activityInfo, + ArgumentMatcher compatInfo, + Integer resultCode, + ArgumentMatcher data, + ArgumentMatcher extras, + Boolean sync, + Integer sendingUser, + Integer processState) { + return argThat(receiverList(manifestReceiverMatcher(intent, activityInfo, compatInfo, + resultCode, data, extras, sync, sendingUser, processState))); + } + + /** + * Create an ArgumentMatcher for a registered receiver. The parameters are in the order of + * {@link IApplicationThread#scheduleRegisteredReceiver} but the names correspond to the field + * names in {@link ReceiverInfo}. For every parameter, a null means "don't care". + */ + private ArgumentMatcher registeredReceiverMatcher( + ArgumentMatcher receiver, + ArgumentMatcher intent, + Integer resultCode, + ArgumentMatcher data, + ArgumentMatcher extras, + Boolean ordered, + Boolean sticky, + Integer sendingUser, + Integer processState) { + return (test) -> { + return test.registered == true + && matchObject(receiver, test.receiver) + && matchObject(intent, test.intent) + && matchElement(resultCode, test.resultCode) + && matchObject(data, test.data) + && matchObject(extras, test.extras) + && matchElement(ordered, test.ordered) + && matchElement(sticky, test.sticky) + && matchElement(sendingUser, test.sendingUser) + && matchElement(processState, test.processState); + }; + } + + /** + * Create an argument suitable for the verify() mock methods, when the goal is to find a call + * containing a registered receiver. + */ + private List registeredReceiver( + ArgumentMatcher receiver, + ArgumentMatcher intent, + Integer resultCode, + ArgumentMatcher data, + ArgumentMatcher extras, + Boolean ordered, + Boolean sticky, + Integer sendingUser, + Integer processState) { + return argThat(receiverList(registeredReceiverMatcher(receiver, intent, resultCode, + data, extras, ordered, sticky, sendingUser, processState))); + } + + /** + * Apply a matcher to every element in a ReceiverInfo list. + */ + private ArgumentMatcher> receiverList(ArgumentMatcher a) { + return (test) -> { + for (int i = 0; i < test.size(); i++) { + ReceiverInfo r = test.get(i); + if (a.matches(r)) { + return true; + } + } + return false; + }; + } + private ArgumentMatcher bundleEquals(Bundle bundle) { return (test) -> { // TODO: check values in addition to keys @@ -653,27 +789,28 @@ public class BroadcastQueueTest { verifyScheduleReceiver(mode, app, intent, UserHandle.USER_SYSTEM); } - private void verifyScheduleReceiver(VerificationMode mode, ProcessRecord app, Intent intent, - int userId) throws Exception { - verify(app.getThread(), mode).scheduleReceiver( - argThat(filterEqualsIgnoringComponent(intent)), any(), any(), anyInt(), any(), - any(), eq(false), eq(userId), anyInt()); - } - private void verifyScheduleReceiver(VerificationMode mode, ProcessRecord app, Intent intent, ComponentName component) throws Exception { final Intent targetedIntent = new Intent(intent); targetedIntent.setComponent(component); - verify(app.getThread(), mode).scheduleReceiver( - argThat(filterEquals(targetedIntent)), any(), any(), anyInt(), any(), - any(), eq(false), eq(UserHandle.USER_SYSTEM), anyInt()); + verify(app.getThread(), mode).scheduleReceiverList( + manifestReceiver(filterEquals(targetedIntent), + null, null, null, null, null, null, UserHandle.USER_SYSTEM, null)); } - private void verifyScheduleRegisteredReceiver(ProcessRecord app, Intent intent) + private void verifyScheduleReceiver(VerificationMode mode, ProcessRecord app, + Intent intent, int userId) throws Exception { + verify(app.getThread(), mode).scheduleReceiverList( + manifestReceiver(filterEqualsIgnoringComponent(intent), + null, null, null, null, null, null, userId, null)); + } + + private void verifyScheduleRegisteredReceiver(ProcessRecord app, + Intent intent) throws Exception { - verify(app.getThread()).scheduleRegisteredReceiver(any(), - argThat(filterEqualsIgnoringComponent(intent)), anyInt(), any(), any(), - anyBoolean(), anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + verify(app.getThread()).scheduleReceiverList( + registeredReceiver(null, filterEqualsIgnoringComponent(intent), + null, null, null, null, null, UserHandle.USER_SYSTEM, null)); } static final int USER_GUEST = 11; @@ -1237,23 +1374,24 @@ public class BroadcastQueueTest { final InOrder inOrder = inOrder(greenThread, blueThread, yellowThread, redThread); final Bundle expectedExtras = new Bundle(); expectedExtras.putBoolean(PACKAGE_RED, true); - inOrder.verify(greenThread).scheduleReceiver( - argThat(filterEqualsIgnoringComponent(airplane)), any(), any(), - eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(true), - eq(UserHandle.USER_SYSTEM), anyInt()); - inOrder.verify(blueThread).scheduleReceiver( - argThat(filterEqualsIgnoringComponent(airplane)), any(), any(), - eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(true), - eq(UserHandle.USER_SYSTEM), anyInt()); + inOrder.verify(greenThread).scheduleReceiverList(manifestReceiver( + filterEqualsIgnoringComponent(airplane), null, null, + Activity.RESULT_OK, null, bundleEquals(expectedExtras), true, + UserHandle.USER_SYSTEM, null)); + inOrder.verify(blueThread).scheduleReceiverList(manifestReceiver( + filterEqualsIgnoringComponent(airplane), null, null, + Activity.RESULT_OK, null, bundleEquals(expectedExtras), true, + UserHandle.USER_SYSTEM, null)); expectedExtras.putBoolean(PACKAGE_BLUE, true); - inOrder.verify(yellowThread).scheduleReceiver( - argThat(filterEqualsIgnoringComponent(airplane)), any(), any(), - eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(true), - eq(UserHandle.USER_SYSTEM), anyInt()); + inOrder.verify(yellowThread).scheduleReceiverList(manifestReceiver( + filterEqualsIgnoringComponent(airplane), null, null, + Activity.RESULT_OK, null, bundleEquals(expectedExtras), true, + UserHandle.USER_SYSTEM, null)); expectedExtras.putBoolean(PACKAGE_YELLOW, true); - inOrder.verify(redThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)), - eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(false), - anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + inOrder.verify(redThread).scheduleReceiverList(registeredReceiver( + null, filterEquals(airplane), + Activity.RESULT_OK, null, bundleEquals(expectedExtras), false, + null, UserHandle.USER_SYSTEM, null)); // Finally, verify that we thawed the final receiver verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(callerApp), @@ -1316,22 +1454,24 @@ public class BroadcastQueueTest { // have invoked or skipped the second receiver depending on the intent // flag policy; we always deliver to final receiver regardless of abort final InOrder inOrder = inOrder(greenThread, blueThread, redThread); - inOrder.verify(greenThread).scheduleReceiver( - argThat(filterEqualsIgnoringComponent(intent)), any(), any(), - eq(Activity.RESULT_OK), any(), any(), eq(true), eq(UserHandle.USER_SYSTEM), - anyInt()); + inOrder.verify(greenThread).scheduleReceiverList(manifestReceiver( + filterEqualsIgnoringComponent(intent), null, null, + Activity.RESULT_OK, null, null, true, UserHandle.USER_SYSTEM, + null)); if ((intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0) { - inOrder.verify(blueThread).scheduleReceiver( - argThat(filterEqualsIgnoringComponent(intent)), any(), any(), - eq(Activity.RESULT_OK), any(), any(), eq(true), eq(UserHandle.USER_SYSTEM), - anyInt()); + inOrder.verify(blueThread).scheduleReceiverList(manifestReceiver( + filterEqualsIgnoringComponent(intent), null, null, + Activity.RESULT_OK, null, null, true, UserHandle.USER_SYSTEM, + null)); } else { - inOrder.verify(blueThread, never()).scheduleReceiver(any(), any(), any(), anyInt(), - any(), any(), anyBoolean(), anyInt(), anyInt()); + inOrder.verify(blueThread, never()).scheduleReceiverList(manifestReceiver( + null, null, null, null, + null, null, null, null, null)); } - inOrder.verify(redThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(intent)), - eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), - eq(false), anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + inOrder.verify(redThread).scheduleReceiverList(registeredReceiver( + null, filterEquals(intent), + Activity.RESULT_OK, null, bundleEquals(expectedExtras), + false, null, UserHandle.USER_SYSTEM, null)); } /** @@ -1351,9 +1491,10 @@ public class BroadcastQueueTest { orderedResultTo, orderedExtras)); waitForIdle(); - verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)), - eq(Activity.RESULT_OK), any(), argThat(bundleEquals(orderedExtras)), eq(false), - anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + verify(callerThread).scheduleReceiverList(registeredReceiver( + null, filterEquals(airplane), + Activity.RESULT_OK, null, bundleEquals(orderedExtras), false, + null, UserHandle.USER_SYSTEM, null)); } /** @@ -1371,9 +1512,10 @@ public class BroadcastQueueTest { makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)), resultTo)); waitForIdle(); - verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)), - eq(Activity.RESULT_OK), any(), any(), eq(false), - anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + verify(callerThread).scheduleReceiverList(registeredReceiver( + null, filterEquals(airplane), + Activity.RESULT_OK, null, null, false, + null, UserHandle.USER_SYSTEM, null)); } /** @@ -1536,28 +1678,28 @@ public class BroadcastQueueTest { final InOrder inOrder = inOrder(callerThread, blueThread); // First broadcast is canceled - inOrder.verify(callerThread).scheduleRegisteredReceiver(any(), - argThat(filterAndExtrasEquals(timezoneFirst)), eq(Activity.RESULT_CANCELED), any(), - any(), eq(false), anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + inOrder.verify(callerThread).scheduleReceiverList(registeredReceiver(null, + filterAndExtrasEquals(timezoneFirst), Activity.RESULT_CANCELED, null, + null, false, null, UserHandle.USER_SYSTEM, null)); // We deliver second broadcast to app timezoneSecond.setClassName(PACKAGE_BLUE, CLASS_GREEN); - inOrder.verify(blueThread).scheduleReceiver( - argThat(filterAndExtrasEquals(timezoneSecond)), - any(), any(), anyInt(), any(), any(), eq(true), anyInt(), anyInt()); + inOrder.verify(blueThread).scheduleReceiverList(manifestReceiver( + filterAndExtrasEquals(timezoneSecond), + null, null, null, null, null, true, null, null)); // Second broadcast is finished timezoneSecond.setComponent(null); - inOrder.verify(callerThread).scheduleRegisteredReceiver(any(), - argThat(filterAndExtrasEquals(timezoneSecond)), eq(Activity.RESULT_OK), any(), - any(), eq(false), anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + inOrder.verify(callerThread).scheduleReceiverList(registeredReceiver(null, + filterAndExtrasEquals(timezoneSecond), Activity.RESULT_OK, null, + null, false, null, UserHandle.USER_SYSTEM, null)); // Since we "replaced" the first broadcast in its original position, // only now do we see the airplane broadcast airplane.setClassName(PACKAGE_BLUE, CLASS_RED); - inOrder.verify(blueThread).scheduleReceiver( - argThat(filterEquals(airplane)), - any(), any(), anyInt(), any(), any(), eq(false), anyInt(), anyInt()); + inOrder.verify(blueThread).scheduleReceiverList(manifestReceiver( + filterEquals(airplane), + null, null, null, null, null, false, null, null)); } @Test