diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1254560bfd421..fc8e4ac302f5d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -806,10 +806,12 @@ package android.app { method @Nullable public android.content.IntentFilter getDeliveryGroupMatchingFilter(); method @Nullable public String getDeliveryGroupMatchingKey(); method public int getDeliveryGroupPolicy(); + method public boolean isDeferUntilActive(); method public boolean isPendingIntentBackgroundActivityLaunchAllowed(); method public static android.app.BroadcastOptions makeBasic(); method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long); method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean); + method @NonNull public android.app.BroadcastOptions setDeferUntilActive(boolean); method @NonNull public android.app.BroadcastOptions setDeliveryGroupMatchingFilter(@NonNull android.content.IntentFilter); method @NonNull public android.app.BroadcastOptions setDeliveryGroupMatchingKey(@NonNull String, @NonNull String); method @NonNull public android.app.BroadcastOptions setDeliveryGroupPolicy(int); diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index e202760c5a4a6..5cf10d069f556 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -63,6 +63,7 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private boolean mRequireCompatChangeEnabled = true; private boolean mIsAlarmBroadcast = false; + private boolean mIsDeferUntilActive = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; @@ -200,6 +201,12 @@ public class BroadcastOptions extends ComponentOptions { private static final String KEY_REMOVE_MATCHING_FILTER = "android:broadcast.removeMatchingFilter"; + /** + * Corresponds to {@link #setDeferUntilActive(boolean)}. + */ + private static final String KEY_DEFER_UNTIL_ACTIVE = + "android:broadcast.deferuntilactive"; + /** * Corresponds to {@link #setDeliveryGroupPolicy(int)}. */ @@ -320,6 +327,7 @@ public class BroadcastOptions extends ComponentOptions { BundleMerger.class); mDeliveryGroupMatchingFilter = opts.getParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, IntentFilter.class); + mIsDeferUntilActive = opts.getBoolean(KEY_DEFER_UNTIL_ACTIVE, false); } /** @@ -699,6 +707,41 @@ public class BroadcastOptions extends ComponentOptions { return mIdForResponseEvent; } + /** + * Sets whether the broadcast should not run until the process is in an active process state + * (ie, a process exists for the app and the app is not in a cached process state). + * + * Whether an app's process state is considered active is independent of its standby bucket. + * + * A broadcast that is deferred until the process is active will not execute until the process + * is brought to an active state by some other action, like a job, alarm, or service binding. As + * a result, the broadcast may be delayed indefinitely. This deferral only applies to runtime + * registered receivers of a broadcast. Any manifest receivers will run immediately, similar to + * how a manifest receiver would start a new process in order to run a broadcast receiver. + * + * Ordered broadcasts, alarm broadcasts, interactive broadcasts, and manifest broadcasts are + * never deferred. + * + * Unordered broadcasts and unordered broadcasts with completion callbacks may be + * deferred. Completion callbacks for broadcasts deferred until active are + * best-effort. Completion callbacks will run when all eligible processes have finished + * executing the broadcast. Processes in inactive process states that defer the broadcast are + * not considered eligible and may not execute the broadcast prior to the completion callback. + * + * @hide + */ + @SystemApi + public @NonNull BroadcastOptions setDeferUntilActive(boolean shouldDefer) { + mIsDeferUntilActive = shouldDefer; + return this; + } + + /** @hide */ + @SystemApi + public boolean isDeferUntilActive() { + return mIsDeferUntilActive; + } + /** * When enqueuing this broadcast, remove all pending broadcasts previously * sent by this app which match the given filter. @@ -963,6 +1006,9 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupMatchingFilter != null) { b.putParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, mDeliveryGroupMatchingFilter); } + if (mIsDeferUntilActive) { + b.putBoolean(KEY_DEFER_UNTIL_ACTIVE, mIsDeferUntilActive); + } return b.isEmpty() ? null : b; } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 3d4347efd17d5..99997b0bd120a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -40,6 +40,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.ActivityTaskManager; import android.app.AlarmManager; +import android.app.BroadcastOptions; import android.app.PendingIntent; import android.app.StatusBarManager; import android.app.WindowConfiguration; @@ -388,6 +389,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); + private static final Bundle USER_PRESENT_INTENT_OPTIONS = + BroadcastOptions.makeBasic() + .setDeferUntilActive(true) + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .toBundle(); + /** * {@link #setKeyguardEnabled} waits on this condition when it re-enables * the keyguard. @@ -2300,7 +2307,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Context.USER_SERVICE); mUiBgExecutor.execute(() -> { for (int profileId : um.getProfileIdsWithDisabled(currentUser.getIdentifier())) { - mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, UserHandle.of(profileId)); + mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, + UserHandle.of(profileId), + null, + USER_PRESENT_INTENT_OPTIONS); } mLockPatternUtils.userPresent(currentUserId); }); diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 71a4c73a4dace..b41664f34c06a 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -189,15 +189,20 @@ public final class BatteryService extends SystemService { private long mLastBatteryLevelChangedSentMs; private Bundle mBatteryChangedOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)).toBundle(); + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)).setDeferUntilActive(true) + .toBundle(); private Bundle mPowerConnectedOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_POWER_DISCONNECTED)).toBundle(); + new IntentFilter(Intent.ACTION_POWER_DISCONNECTED)).setDeferUntilActive(true) + .toBundle(); private Bundle mPowerDisconnectedOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_POWER_CONNECTED)).toBundle(); + new IntentFilter(Intent.ACTION_POWER_CONNECTED)).setDeferUntilActive(true) + .toBundle(); private Bundle mBatteryLowOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_BATTERY_OKAY)).toBundle(); + new IntentFilter(Intent.ACTION_BATTERY_OKAY)).setDeferUntilActive(true) + .toBundle(); private Bundle mBatteryOkayOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_BATTERY_LOW)).toBundle(); + new IntentFilter(Intent.ACTION_BATTERY_LOW)).setDeferUntilActive(true) + .toBundle(); private MetricsLogger mMetricsLogger; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index fc6d30bf58c91..09918dd8e79df 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -14108,8 +14108,16 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, (sticky ? "Broadcast sticky: ": "Broadcast: ") + intent + " ordered=" + ordered + " userid=" + userId); - if ((resultTo != null) && !ordered && !mEnableModernQueue) { - Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!"); + if ((resultTo != null) && !ordered) { + if (!mEnableModernQueue) { + Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!"); + } + if (!UserHandle.isCore(callingUid)) { + String msg = "Unauthorized unordered resultTo broadcast " + + intent + " sent from uid " + callingUid; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } } userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true, @@ -14190,6 +14198,18 @@ public class ActivityManagerService extends IActivityManager.Stub } } + // resultTo broadcasts are always infinitely deferrable. + if ((resultTo != null) && !ordered && mEnableModernQueue) { + if (brOptions == null) { + brOptions = BroadcastOptions.makeBasic(); + } + brOptions.setDeferUntilActive(true); + } + + if (ordered && brOptions != null && brOptions.isDeferUntilActive()) { + throw new IllegalArgumentException("Ordered broadcasts can't be deferred until active"); + } + // Verify that protected broadcasts are only being sent by system code, // and that system code is only sending protected broadcasts. final boolean isProtectedBroadcast; diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index fa7748b9c16d1..41d9a11e3eaf5 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -62,6 +62,7 @@ import java.util.Objects; */ // @NotThreadSafe class BroadcastProcessQueue { + static final boolean VERBOSE = false; final @NonNull BroadcastConstants constants; final @NonNull String processName; final int uid; @@ -168,10 +169,14 @@ class BroadcastProcessQueue { /** * Count of pending broadcasts of these various flavors. */ + private int mCountEnqueued; + private int mCountDeferred; private int mCountForeground; + private int mCountForegroundDeferred; private int mCountOrdered; private int mCountAlarm; private int mCountPrioritized; + private int mCountPrioritizedDeferred; private int mCountInteractive; private int mCountResultTo; private int mCountInstrumented; @@ -226,10 +231,10 @@ class BroadcastProcessQueue { * used for ordered broadcasts and priority traunches. */ public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, - @NonNull BroadcastConsumer replacedBroadcastConsumer) { + @NonNull BroadcastConsumer replacedBroadcastConsumer, boolean wouldBeSkipped) { if (record.isReplacePending()) { final boolean didReplace = replaceBroadcast(record, recordIndex, - replacedBroadcastConsumer); + replacedBroadcastConsumer, wouldBeSkipped); if (didReplace) { return; } @@ -240,13 +245,14 @@ class BroadcastProcessQueue { SomeArgs newBroadcastArgs = SomeArgs.obtain(); newBroadcastArgs.arg1 = record; newBroadcastArgs.argi1 = recordIndex; + newBroadcastArgs.argi2 = (wouldBeSkipped ? 1 : 0); // Cross-broadcast prioritization policy: some broadcasts might warrant being // issued ahead of others that are already pending, for example if this new // broadcast is in a different delivery class or is tied to a direct user interaction // with implicit responsiveness expectations. getQueueForBroadcast(record).addLast(newBroadcastArgs); - onBroadcastEnqueued(record, recordIndex); + onBroadcastEnqueued(record, recordIndex, wouldBeSkipped); } /** @@ -258,11 +264,12 @@ class BroadcastProcessQueue { * {@code false} otherwise. */ private boolean replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, - @NonNull BroadcastConsumer replacedBroadcastConsumer) { + @NonNull BroadcastConsumer replacedBroadcastConsumer, boolean wouldBeSkipped) { final int count = mPendingQueues.size(); for (int i = 0; i < count; ++i) { final ArrayDeque queue = mPendingQueues.get(i); - if (replaceBroadcastInQueue(queue, record, recordIndex, replacedBroadcastConsumer)) { + if (replaceBroadcastInQueue(queue, record, recordIndex, + replacedBroadcastConsumer, wouldBeSkipped)) { return true; } } @@ -279,13 +286,15 @@ class BroadcastProcessQueue { */ private boolean replaceBroadcastInQueue(@NonNull ArrayDeque queue, @NonNull BroadcastRecord record, int recordIndex, - @NonNull BroadcastConsumer replacedBroadcastConsumer) { + @NonNull BroadcastConsumer replacedBroadcastConsumer, + boolean wouldBeSkipped) { final Iterator it = queue.descendingIterator(); final Object receiver = record.receivers.get(recordIndex); while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; final int testRecordIndex = args.argi1; + final boolean testWouldBeSkipped = (args.argi2 == 1); final Object testReceiver = testRecord.receivers.get(testRecordIndex); if ((record.callingUid == testRecord.callingUid) && (record.userId == testRecord.userId) @@ -295,9 +304,10 @@ class BroadcastProcessQueue { // Exact match found; perform in-place swap args.arg1 = record; args.argi1 = recordIndex; + args.argi2 = (wouldBeSkipped ? 1 : 0); record.copyEnqueueTimeFrom(testRecord); - onBroadcastDequeued(testRecord, testRecordIndex); - onBroadcastEnqueued(record, recordIndex); + onBroadcastDequeued(testRecord, testRecordIndex, testWouldBeSkipped); + onBroadcastEnqueued(record, recordIndex, wouldBeSkipped); replacedBroadcastConsumer.accept(testRecord, testRecordIndex); return true; } @@ -352,12 +362,13 @@ class BroadcastProcessQueue { final SomeArgs args = it.next(); final BroadcastRecord record = (BroadcastRecord) args.arg1; final int recordIndex = args.argi1; + final boolean recordWouldBeSkipped = (args.argi2 == 1); if (predicate.test(record, recordIndex)) { consumer.accept(record, recordIndex); if (andRemove) { args.recycle(); it.remove(); - onBroadcastDequeued(record, recordIndex); + onBroadcastDequeued(record, recordIndex, recordWouldBeSkipped); } didSomething = true; } @@ -423,7 +434,7 @@ class BroadcastProcessQueue { } public int getPreferredSchedulingGroupLocked() { - if (mCountForeground > 0) { + if (mCountForeground > mCountForegroundDeferred) { // We have a foreground broadcast somewhere down the queue, so // boost priority until we drain them all return ProcessList.SCHED_GROUP_DEFAULT; @@ -469,10 +480,11 @@ class BroadcastProcessQueue { final SomeArgs next = removeNextBroadcast(); mActive = (BroadcastRecord) next.arg1; mActiveIndex = next.argi1; + final boolean wouldBeSkipped = (next.argi2 == 1); mActiveCountSinceIdle++; mActiveViaColdStart = false; next.recycle(); - onBroadcastDequeued(mActive, mActiveIndex); + onBroadcastDequeued(mActive, mActiveIndex, wouldBeSkipped); } /** @@ -489,8 +501,16 @@ class BroadcastProcessQueue { /** * Update summary statistics when the given record has been enqueued. */ - private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) { + private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex, + boolean wouldBeSkipped) { + mCountEnqueued++; + if (record.deferUntilActive) { + mCountDeferred++; + } if (record.isForeground()) { + if (record.deferUntilActive) { + mCountForegroundDeferred++; + } mCountForeground++; } if (record.ordered) { @@ -500,6 +520,9 @@ class BroadcastProcessQueue { mCountAlarm++; } if (record.prioritized) { + if (record.deferUntilActive) { + mCountPrioritizedDeferred++; + } mCountPrioritized++; } if (record.interactive) { @@ -511,7 +534,8 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented++; } - if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + if (!wouldBeSkipped + && (record.receivers.get(recordIndex) instanceof ResolveInfo)) { mCountManifest++; } invalidateRunnableAt(); @@ -520,8 +544,16 @@ class BroadcastProcessQueue { /** * Update summary statistics when the given record has been dequeued. */ - private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) { + private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex, + boolean wouldBeSkipped) { + mCountEnqueued--; + if (record.deferUntilActive) { + mCountDeferred--; + } if (record.isForeground()) { + if (record.deferUntilActive) { + mCountForegroundDeferred--; + } mCountForeground--; } if (record.ordered) { @@ -531,6 +563,9 @@ class BroadcastProcessQueue { mCountAlarm--; } if (record.prioritized) { + if (record.deferUntilActive) { + mCountPrioritizedDeferred--; + } mCountPrioritized--; } if (record.interactive) { @@ -542,7 +577,8 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented--; } - if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + if (!wouldBeSkipped + && (record.receivers.get(recordIndex) instanceof ResolveInfo)) { mCountManifest--; } invalidateRunnableAt(); @@ -741,6 +777,15 @@ class BroadcastProcessQueue { return mRunnableAt != Long.MAX_VALUE; } + public boolean isDeferredUntilActive() { + if (mRunnableAtInvalidated) updateRunnableAt(); + return mRunnableAtReason == BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER; + } + + public boolean hasDeferredBroadcasts() { + return (mCountDeferred > 0); + } + /** * Return time at which this process is considered runnable. This is * typically the time at which the next pending broadcast was first @@ -776,6 +821,7 @@ class BroadcastProcessQueue { static final int REASON_INSTRUMENTED = 5; static final int REASON_PERSISTENT = 6; static final int REASON_FORCE_DELAYED = 7; + static final int REASON_CACHED_INFINITE_DEFER = 8; static final int REASON_CONTAINS_FOREGROUND = 10; static final int REASON_CONTAINS_ORDERED = 11; static final int REASON_CONTAINS_ALARM = 12; @@ -794,6 +840,7 @@ class BroadcastProcessQueue { REASON_INSTRUMENTED, REASON_PERSISTENT, REASON_FORCE_DELAYED, + REASON_CACHED_INFINITE_DEFER, REASON_CONTAINS_FOREGROUND, REASON_CONTAINS_ORDERED, REASON_CONTAINS_ALARM, @@ -816,6 +863,7 @@ class BroadcastProcessQueue { case REASON_INSTRUMENTED: return "INSTRUMENTED"; case REASON_PERSISTENT: return "PERSISTENT"; case REASON_FORCE_DELAYED: return "FORCE_DELAYED"; + case REASON_CACHED_INFINITE_DEFER: return "INFINITE_DEFER"; case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; @@ -831,9 +879,16 @@ class BroadcastProcessQueue { private boolean blockedOnOrderedDispatch(BroadcastRecord r, int index) { final int blockedUntilTerminalCount = r.blockedUntilTerminalCount[index]; + int existingDeferredCount = 0; + if (r.deferUntilActive) { + for (int i = 0; i < index; i++) { + if (r.deferredUntilActive[i]) existingDeferredCount++; + } + } + // We might be blocked waiting for other receivers to finish, // typically for an ordered broadcast or priority traunches - if (r.terminalCount < blockedUntilTerminalCount + if ((r.terminalCount + existingDeferredCount) < blockedUntilTerminalCount && !isDeliveryStateTerminal(r.getDeliveryState(index))) { return true; } @@ -862,7 +917,7 @@ class BroadcastProcessQueue { if (mForcedDelayedDurationMs > 0) { mRunnableAt = runnableAt + mForcedDelayedDurationMs; mRunnableAtReason = REASON_FORCE_DELAYED; - } else if (mCountForeground > 0) { + } else if (mCountForeground > mCountForegroundDeferred) { mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS; mRunnableAtReason = REASON_CONTAINS_FOREGROUND; } else if (mCountInteractive > 0) { @@ -880,12 +935,9 @@ class BroadcastProcessQueue { } else if (mCountAlarm > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_ALARM; - } else if (mCountPrioritized > 0) { + } else if (mCountPrioritized > mCountPrioritizedDeferred) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_PRIORITIZED; - } else if (mCountResultTo > 0) { - mRunnableAt = runnableAt; - mRunnableAtReason = REASON_CONTAINS_RESULT_TO; } else if (mCountManifest > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_MANIFEST; @@ -893,8 +945,39 @@ class BroadcastProcessQueue { mRunnableAt = runnableAt; mRunnableAtReason = REASON_PERSISTENT; } else if (mProcessCached) { - mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; - mRunnableAtReason = REASON_CACHED; + if (r.deferUntilActive) { + // All enqueued broadcasts are deferrable, defer + if (mCountDeferred == mCountEnqueued) { + mRunnableAt = Long.MAX_VALUE; + mRunnableAtReason = REASON_CACHED_INFINITE_DEFER; + } else { + // At least one enqueued broadcast isn't deferrable, repick time and reason + // for this record. If a later record is not deferrable and is one of these + // special cases, one of the cases above would have already caught that. + if (r.isForeground()) { + mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS; + mRunnableAtReason = REASON_CONTAINS_FOREGROUND; + } else if (r.prioritized) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_PRIORITIZED; + } else if (r.resultTo != null) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_RESULT_TO; + } else { + mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; + mRunnableAtReason = REASON_CACHED; + } + } + } else { + // This record isn't deferrable + mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; + mRunnableAtReason = REASON_CACHED; + } + } else if (mCountResultTo > 0) { + // All resultTo broadcasts are infinitely deferrable, so if the app + // is already cached, they'll be deferred on the line above + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_RESULT_TO; } else { mRunnableAt = runnableAt + constants.DELAY_NORMAL_MILLIS; mRunnableAtReason = REASON_NORMAL; @@ -907,6 +990,15 @@ class BroadcastProcessQueue { mRunnableAt = Math.min(mRunnableAt, runnableAt); mRunnableAtReason = REASON_MAX_PENDING; } + + if (VERBOSE) { + Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, "BroadcastQueue", + ((app != null) ? app.processName : "(null)") + + ":" + r.intent.toString() + ":" + + r.deferUntilActive + + ":" + mRunnableAt + " " + reasonToString(mRunnableAtReason) + + ":" + ((app != null) ? app.isCached() : "false")); + } } else { mRunnableAt = Long.MAX_VALUE; mRunnableAtReason = REASON_EMPTY; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a994b1db7ca33..d819bfcc231c4 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -385,6 +385,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // If app isn't running, and there's nothing in the queue, clean up if (queue.isEmpty() && !queue.isActive() && !queue.isProcessWarm()) { removeProcessQueue(queue.processName, queue.uid); + } else { + updateQueueDeferred(queue); } } @@ -637,11 +639,34 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final ArraySet replacedBroadcasts = new ArraySet<>(); final BroadcastConsumer replacedBroadcastConsumer = (record, i) -> replacedBroadcasts.add(record); + boolean enqueuedBroadcast = false; + for (int i = 0; i < r.receivers.size(); i++) { final Object receiver = r.receivers.get(i); final BroadcastProcessQueue queue = getOrCreateProcessQueue( getReceiverProcessName(receiver), getReceiverUid(receiver)); - queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer); + + boolean wouldBeSkipped = false; + if (receiver instanceof ResolveInfo) { + // If the app is running but would not have been started if the process weren't + // running, we're going to deliver the broadcast but mark that it's not a manifest + // broadcast that would have started the app. This allows BroadcastProcessQueue to + // defer the broadcast as though it were a normal runtime receiver. + wouldBeSkipped = (mSkipPolicy.shouldSkipMessage(r, receiver) != null); + if (wouldBeSkipped && queue.app == null && !queue.getActiveViaColdStart()) { + // Skip receiver if there's no running app, the app is not being started, and + // the app wouldn't be launched for this broadcast + setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED, + "skipped by policy to avoid cold start"); + continue; + } + } + enqueuedBroadcast = true; + queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer, wouldBeSkipped); + if (r.isDeferUntilActive() && queue.isDeferredUntilActive()) { + setDeliveryState(queue, null, r, i, receiver, BroadcastRecord.DELIVERY_DEFERRED, + "deferred at enqueue time"); + } updateRunnableList(queue); enqueueUpdateRunningList(); } @@ -651,7 +676,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { skipAndCancelReplacedBroadcasts(replacedBroadcasts); // If nothing to dispatch, send any pending result immediately - if (r.receivers.isEmpty()) { + if (r.receivers.isEmpty() || !enqueuedBroadcast) { scheduleResultTo(r); notifyFinishBroadcast(r); } @@ -1195,11 +1220,19 @@ class BroadcastQueueModernImpl extends BroadcastQueue { @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) { final int cookie = traceBegin("setDeliveryState"); final int oldDeliveryState = getDeliveryState(r, index); + boolean checkFinished = false; // Only apply state when we haven't already reached a terminal state; // this is how we ignore racing timeout messages if (!isDeliveryStateTerminal(oldDeliveryState)) { r.setDeliveryState(index, newDeliveryState); + if (oldDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { + r.deferredCount--; + } else if (newDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { + // If we're deferring a broadcast, maybe that's enough to unblock the final callback + r.deferredCount++; + checkFinished = true; + } } // Emit any relevant tracing results when we're changing the delivery @@ -1217,7 +1250,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // bookkeeping to update for ordered broadcasts if (!isDeliveryStateTerminal(oldDeliveryState) && isDeliveryStateTerminal(newDeliveryState)) { - if (newDeliveryState != BroadcastRecord.DELIVERY_DELIVERED) { + if (DEBUG_BROADCAST + && newDeliveryState != BroadcastRecord.DELIVERY_DELIVERED) { logw("Delivery state of " + r + " to " + receiver + " via " + app + " changed from " + deliveryStateToString(oldDeliveryState) + " to " @@ -1226,9 +1260,12 @@ class BroadcastQueueModernImpl extends BroadcastQueue { r.terminalCount++; notifyFinishReceiver(queue, r, index, receiver); - - // When entire ordered broadcast finished, deliver final result - final boolean recordFinished = (r.terminalCount == r.receivers.size()); + checkFinished = true; + } + // When entire ordered broadcast finished, deliver final result + if (checkFinished) { + final boolean recordFinished = + ((r.terminalCount + r.deferredCount) == r.receivers.size()); if (recordFinished) { scheduleResultTo(r); } @@ -1329,6 +1366,16 @@ class BroadcastQueueModernImpl extends BroadcastQueue { r.resultExtras = null; }; + private final BroadcastConsumer mBroadcastConsumerDefer = (r, i) -> { + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_DEFERRED, + "mBroadcastConsumerDefer"); + }; + + private final BroadcastConsumer mBroadcastConsumerUndoDefer = (r, i) -> { + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_PENDING, + "mBroadcastConsumerUndoDefer"); + }; + /** * Verify that all known {@link #mProcessQueues} are in the state tested by * the given {@link Predicate}. @@ -1392,6 +1439,21 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } } + private void updateQueueDeferred( + @NonNull BroadcastProcessQueue leaf) { + if (leaf.isDeferredUntilActive()) { + leaf.forEachMatchingBroadcast((r, i) -> { + return r.deferUntilActive && (r.getDeliveryState(i) + == BroadcastRecord.DELIVERY_PENDING); + }, mBroadcastConsumerDefer, false); + } else if (leaf.hasDeferredBroadcasts()) { + leaf.forEachMatchingBroadcast((r, i) -> { + return r.deferUntilActive && (r.getDeliveryState(i) + == BroadcastRecord.DELIVERY_DEFERRED); + }, mBroadcastConsumerUndoDefer, false); + } + } + @Override public void start(@NonNull ContentResolver resolver) { mFgConstants.startObserving(mHandler, resolver); @@ -1404,6 +1466,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { BroadcastProcessQueue leaf = mProcessQueues.get(uid); while (leaf != null) { leaf.setProcessCached(cached); + updateQueueDeferred(leaf); updateRunnableList(leaf); leaf = leaf.processNameNext; } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 1d4d425590c1c..9679fb8f2d10f 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -88,6 +88,7 @@ final class BroadcastRecord extends Binder { final boolean interactive; // originated from user interaction? final boolean initialSticky; // initial broadcast from register to sticky? final boolean prioritized; // contains more than one priority tranche + final boolean deferUntilActive; // infinitely deferrable broadcast final int userId; // user id this broadcast was for final @Nullable String resolvedType; // the resolved data type final @Nullable String[] requiredPermissions; // permissions the caller has required @@ -97,6 +98,7 @@ final class BroadcastRecord extends Binder { final @Nullable BroadcastOptions options; // BroadcastOptions supplied by caller final @NonNull List receivers; // contains BroadcastFilter and ResolveInfo final @DeliveryState int[] delivery; // delivery state of each receiver + final boolean[] deferredUntilActive; // whether each receiver is infinitely deferred final int[] blockedUntilTerminalCount; // blocked until count of each receiver @Nullable ProcessRecord resultToApp; // who receives final result if non-null @Nullable IIntentReceiver resultTo; // who receives final result if non-null @@ -130,6 +132,7 @@ final class BroadcastRecord extends Binder { int manifestCount; // number of manifest receivers dispatched. int manifestSkipCount; // number of manifest receivers skipped. int terminalCount; // number of receivers in terminal state. + int deferredCount; // number of receivers in deferred state. @Nullable BroadcastQueue queue; // the outbound queue handling this broadcast // if set to true, app's process will be temporarily allowed to start activities from background @@ -168,6 +171,8 @@ final class BroadcastRecord extends Binder { static final int DELIVERY_SCHEDULED = 4; /** Terminal state: failure to dispatch */ static final int DELIVERY_FAILURE = 5; + /** Intermediate state: currently deferred while app is cached */ + static final int DELIVERY_DEFERRED = 6; @IntDef(flag = false, prefix = { "DELIVERY_" }, value = { DELIVERY_PENDING, @@ -176,6 +181,7 @@ final class BroadcastRecord extends Binder { DELIVERY_TIMEOUT, DELIVERY_SCHEDULED, DELIVERY_FAILURE, + DELIVERY_DEFERRED, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryState {} @@ -188,6 +194,7 @@ final class BroadcastRecord extends Binder { case DELIVERY_TIMEOUT: return "TIMEOUT"; case DELIVERY_SCHEDULED: return "SCHEDULED"; case DELIVERY_FAILURE: return "FAILURE"; + case DELIVERY_DEFERRED: return "DEFERRED"; default: return Integer.toString(deliveryState); } } @@ -388,6 +395,8 @@ final class BroadcastRecord extends Binder { options = _options; receivers = (_receivers != null) ? _receivers : EMPTY_RECEIVERS; delivery = new int[_receivers != null ? _receivers.size() : 0]; + deferUntilActive = options != null ? options.isDeferUntilActive() : false; + deferredUntilActive = new boolean[deferUntilActive ? delivery.length : 0]; blockedUntilTerminalCount = calculateBlockedUntilTerminalCount(receivers, _serialized); scheduledTime = new long[delivery.length]; terminalTime = new long[delivery.length]; @@ -443,6 +452,8 @@ final class BroadcastRecord extends Binder { options = from.options; receivers = from.receivers; delivery = from.delivery; + deferUntilActive = from.deferUntilActive; + deferredUntilActive = from.deferredUntilActive; blockedUntilTerminalCount = from.blockedUntilTerminalCount; scheduledTime = from.scheduledTime; terminalTime = from.terminalTime; @@ -606,7 +617,7 @@ final class BroadcastRecord extends Binder { */ void setDeliveryState(int index, @DeliveryState int deliveryState) { delivery[index] = deliveryState; - + if (deferUntilActive) deferredUntilActive[index] = false; switch (deliveryState) { case DELIVERY_DELIVERED: case DELIVERY_SKIPPED: @@ -617,6 +628,9 @@ final class BroadcastRecord extends Binder { case DELIVERY_SCHEDULED: scheduledTime[index] = SystemClock.uptimeMillis(); break; + case DELIVERY_DEFERRED: + if (deferUntilActive) deferredUntilActive[index] = true; + break; } } @@ -647,6 +661,10 @@ final class BroadcastRecord extends Binder { return (intent.getFlags() & Intent.FLAG_RECEIVER_OFFLOAD) != 0; } + boolean isDeferUntilActive() { + return deferUntilActive; + } + /** * Core policy determination about this broadcast's delivery prioritization */ diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 481ab17b609eb..6718319c7ac61 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -229,12 +229,7 @@ public class BroadcastSkipPolicy { return "Background execution disabled: receiving " + r.intent + " to " + component.flattenToShortString(); - } else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0) - || (r.intent.getComponent() == null - && r.intent.getPackage() == null - && ((r.intent.getFlags() - & Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND) == 0) - && !isSignaturePerm(r.requiredPermissions))) { + } else if (disallowBackgroundStart(r)) { mService.addBackgroundCheckViolationLocked(r.intent.getAction(), component.getPackageName()); return "Background execution not allowed: receiving " @@ -340,6 +335,18 @@ public class BroadcastSkipPolicy { return null; } + /** + * Determine if the given {@link BroadcastRecord} is eligible to launch processes. + */ + public boolean disallowBackgroundStart(@NonNull BroadcastRecord r) { + return ((r.intent.getFlags() & Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0) + || (r.intent.getComponent() == null + && r.intent.getPackage() == null + && ((r.intent.getFlags() + & Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND) == 0) + && !isSignaturePerm(r.requiredPermissions)); + } + /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link BroadcastFilter}. @@ -624,7 +631,7 @@ public class BroadcastSkipPolicy { /** * Return true if all given permissions are signature-only perms. */ - private boolean isSignaturePerm(String[] perms) { + private static boolean isSignaturePerm(String[] perms) { if (perms == null) { return false; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 77127c536d6da..92570aa8847ef 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -22,7 +22,6 @@ import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_INTERA import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_MANIFEST; import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_ORDERED; import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_PRIORITIZED; -import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_RESULT_TO; import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList; import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList; import static com.android.server.am.BroadcastQueueTest.CLASS_BLUE; @@ -115,8 +114,29 @@ public class BroadcastQueueModernImplTest { mConstants.DELAY_NORMAL_MILLIS = 10_000; mConstants.DELAY_CACHED_MILLIS = 120_000; + final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) { + public boolean shouldSkip(BroadcastRecord r, Object o) { + // Ignored + return false; + } + public String shouldSkipMessage(BroadcastRecord r, Object o) { + // Ignored + return null; + } + public boolean disallowBackgroundStart(BroadcastRecord r) { + // Ignored + return false; + } + }; + final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) { + public void addBroadcastToHistoryLocked(BroadcastRecord original) { + // Ignored + } + }; + + mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), - mConstants, mConstants); + mConstants, mConstants, emptySkipPolicy, emptyHistory); doReturn(1L).when(mQueue1).getRunnableAt(); doReturn(2L).when(mQueue2).getRunnableAt(); @@ -200,7 +220,8 @@ public class BroadcastQueueModernImplTest { private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue, BroadcastRecord record, int recordIndex, long enqueueTime) { - queue.enqueueOrReplaceBroadcast(record, recordIndex, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(record, recordIndex, + null /* replacedBroadcastConsumer */, false); record.enqueueTime = enqueueTime; } @@ -330,7 +351,8 @@ public class BroadcastQueueModernImplTest { final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, + null /* replacedBroadcastConsumer */, false); queue.setProcessCached(false); final long notCachedRunnableAt = queue.getRunnableAt(); @@ -352,12 +374,14 @@ public class BroadcastQueueModernImplTest { // enqueue a bg-priority broadcast then a fg-priority one final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone); - queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, + null /* replacedBroadcastConsumer */, false); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, + null /* replacedBroadcastConsumer */, false); // verify that: // (a) the queue is immediately runnable by existence of a fg-priority broadcast @@ -388,7 +412,8 @@ public class BroadcastQueueModernImplTest { final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null, List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10), withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, + null /* replacedBroadcastConsumer */, false); assertFalse(queue.isRunnable()); assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason()); @@ -411,7 +436,8 @@ public class BroadcastQueueModernImplTest { final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, + null /* replacedBroadcastConsumer */, false); mConstants.MAX_PENDING_BROADCASTS = 128; queue.invalidateRunnableAt(); @@ -437,11 +463,13 @@ public class BroadcastQueueModernImplTest { new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(lazyRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(lazyRecord, 0, + null /* replacedBroadcastConsumer */, false); assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime); assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason); - queue.enqueueOrReplaceBroadcast(testRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(testRecord, 0, + null /* replacedBroadcastConsumer */, false); assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime); assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason); } @@ -458,13 +486,6 @@ public class BroadcastQueueModernImplTest { List.of(makeMockRegisteredReceiver()), null, true), REASON_CONTAINS_ORDERED); } - @Test - public void testRunnableAt_Cached_ResultTo() { - final IIntentReceiver resultTo = mock(IIntentReceiver.class); - doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null, - List.of(makeMockRegisteredReceiver()), resultTo, false), REASON_CONTAINS_RESULT_TO); - } - @Test public void testRunnableAt_Cached_Foreground() { final Intent foregroundIntent = new Intent(); @@ -511,25 +532,25 @@ public class BroadcastQueueModernImplTest { queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED) .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.makeActiveNextPending(); assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction()); 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 8d9dda0863e92..64be0f7c34d2f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -357,6 +357,11 @@ public class BroadcastQueueTest { } receiversToSkip.add(o); } + public boolean disallowBackgroundStart(BroadcastRecord r) { + // Ignored + return false; + } + } private class TestInjector extends Injector {