Merge "Prioritize some broadcasts ahead of strict FIFO"
This commit is contained in:
@@ -62,6 +62,7 @@ public class BroadcastOptions extends ComponentOptions {
|
||||
private long mRequireCompatChangeId = CHANGE_INVALID;
|
||||
private boolean mRequireCompatChangeEnabled = true;
|
||||
private boolean mIsAlarmBroadcast = false;
|
||||
private boolean mIsInteractiveBroadcast = false;
|
||||
private long mIdForResponseEvent;
|
||||
private @Nullable IntentFilter mRemoveMatchingFilter;
|
||||
private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
|
||||
@@ -167,6 +168,13 @@ public class BroadcastOptions extends ComponentOptions {
|
||||
public static final String KEY_ALARM_BROADCAST =
|
||||
"android:broadcast.is_alarm";
|
||||
|
||||
/**
|
||||
* Corresponds to {@link #setInteractiveBroadcast(boolean)}
|
||||
* @hide
|
||||
*/
|
||||
public static final String KEY_INTERACTIVE_BROADCAST =
|
||||
"android:broadcast.is_interactive";
|
||||
|
||||
/**
|
||||
* @hide
|
||||
* @deprecated Use {@link android.os.PowerExemptionManager#
|
||||
@@ -281,6 +289,7 @@ public class BroadcastOptions extends ComponentOptions {
|
||||
mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true);
|
||||
mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
|
||||
mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
|
||||
mIsInteractiveBroadcast = opts.getBoolean(KEY_INTERACTIVE_BROADCAST, false);
|
||||
mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
|
||||
IntentFilter.class);
|
||||
mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
|
||||
@@ -598,6 +607,27 @@ public class BroadcastOptions extends ComponentOptions {
|
||||
return mIsAlarmBroadcast;
|
||||
}
|
||||
|
||||
/**
|
||||
* When set, this broadcast will be understood as having originated from
|
||||
* some direct interaction by the user such as a notification tap or button
|
||||
* press. Only the OS itself may use this option.
|
||||
* @hide
|
||||
* @param broadcastIsInteractive
|
||||
* @see #isInteractiveBroadcast()
|
||||
*/
|
||||
public void setInteractiveBroadcast(boolean broadcastIsInteractive) {
|
||||
mIsInteractiveBroadcast = broadcastIsInteractive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Did this broadcast originate with a direct user interaction?
|
||||
* @return true if this broadcast is the result of an interaction, false otherwise
|
||||
* @hide
|
||||
*/
|
||||
public boolean isInteractiveBroadcast() {
|
||||
return mIsInteractiveBroadcast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Did this broadcast originate from a push message from the server?
|
||||
*
|
||||
@@ -743,6 +773,9 @@ public class BroadcastOptions extends ComponentOptions {
|
||||
if (mIsAlarmBroadcast) {
|
||||
b.putBoolean(KEY_ALARM_BROADCAST, true);
|
||||
}
|
||||
if (mIsInteractiveBroadcast) {
|
||||
b.putBoolean(KEY_INTERACTIVE_BROADCAST, true);
|
||||
}
|
||||
if (mMinManifestReceiverApiLevel != 0) {
|
||||
b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel);
|
||||
}
|
||||
|
||||
@@ -14700,13 +14700,14 @@ public class ActivityManagerService extends IActivityManager.Stub
|
||||
// Non-system callers can't declare that a broadcast is alarm-related.
|
||||
// The PendingIntent invocation case is handled in PendingIntentRecord.
|
||||
if (bOptions != null && callingUid != SYSTEM_UID) {
|
||||
if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
|
||||
if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)
|
||||
|| bOptions.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) {
|
||||
if (DEBUG_BROADCAST) {
|
||||
Slog.w(TAG, "Non-system caller " + callingUid
|
||||
+ " may not flag broadcast as alarm-related");
|
||||
+ " may not flag broadcast as alarm or interactive");
|
||||
}
|
||||
throw new SecurityException(
|
||||
"Non-system callers may not flag broadcasts as alarm-related");
|
||||
"Non-system callers may not flag broadcasts as alarm or interactive");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,13 @@ class BroadcastProcessQueue {
|
||||
*/
|
||||
private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* Ordered collection of "urgent" broadcasts that are waiting to be
|
||||
* dispatched to this process, in the same representation as
|
||||
* {@link #mPending}.
|
||||
*/
|
||||
private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* Broadcast actively being dispatched to this process.
|
||||
*/
|
||||
@@ -172,39 +179,64 @@ class BroadcastProcessQueue {
|
||||
*/
|
||||
public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
|
||||
int blockedUntilTerminalCount) {
|
||||
// If caller wants to replace, walk backwards looking for any matches
|
||||
if (record.isReplacePending()) {
|
||||
final Iterator<SomeArgs> it = mPending.descendingIterator();
|
||||
final Object receiver = record.receivers.get(recordIndex);
|
||||
while (it.hasNext()) {
|
||||
final SomeArgs args = it.next();
|
||||
final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
|
||||
final Object testReceiver = testRecord.receivers.get(args.argi1);
|
||||
if ((record.callingUid == testRecord.callingUid)
|
||||
&& (record.userId == testRecord.userId)
|
||||
&& record.intent.filterEquals(testRecord.intent)
|
||||
&& isReceiverEquals(receiver, testReceiver)) {
|
||||
// Exact match found; perform in-place swap
|
||||
args.arg1 = record;
|
||||
args.argi1 = recordIndex;
|
||||
args.argi2 = blockedUntilTerminalCount;
|
||||
onBroadcastDequeued(testRecord);
|
||||
onBroadcastEnqueued(record);
|
||||
return;
|
||||
}
|
||||
boolean didReplace = replaceBroadcastInQueue(mPending,
|
||||
record, recordIndex, blockedUntilTerminalCount)
|
||||
|| replaceBroadcastInQueue(mPendingUrgent,
|
||||
record, recordIndex, blockedUntilTerminalCount);
|
||||
if (didReplace) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Caller isn't interested in replacing, or we didn't find any pending
|
||||
// item to replace above, so enqueue as a new broadcast
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.arg1 = record;
|
||||
args.argi1 = recordIndex;
|
||||
args.argi2 = blockedUntilTerminalCount;
|
||||
mPending.addLast(args);
|
||||
SomeArgs newBroadcastArgs = SomeArgs.obtain();
|
||||
newBroadcastArgs.arg1 = record;
|
||||
newBroadcastArgs.argi1 = recordIndex;
|
||||
newBroadcastArgs.argi2 = blockedUntilTerminalCount;
|
||||
|
||||
// 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.
|
||||
final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending;
|
||||
queue.addLast(newBroadcastArgs);
|
||||
onBroadcastEnqueued(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches from newest to oldest, and at the first matching pending broadcast
|
||||
* it finds, replaces it in-place and returns -- does not attempt to handle
|
||||
* "duplicate" broadcasts in the queue.
|
||||
* <p>
|
||||
* @return {@code true} if it found and replaced an existing record in the queue;
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
|
||||
@NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) {
|
||||
final Iterator<SomeArgs> 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 Object testReceiver = testRecord.receivers.get(args.argi1);
|
||||
if ((record.callingUid == testRecord.callingUid)
|
||||
&& (record.userId == testRecord.userId)
|
||||
&& record.intent.filterEquals(testRecord.intent)
|
||||
&& isReceiverEquals(receiver, testReceiver)) {
|
||||
// Exact match found; perform in-place swap
|
||||
args.arg1 = record;
|
||||
args.argi1 = recordIndex;
|
||||
args.argi2 = blockedUntilTerminalCount;
|
||||
onBroadcastDequeued(testRecord);
|
||||
onBroadcastEnqueued(record);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface that tests a {@link BroadcastRecord} that has been
|
||||
* previously enqueued in {@link BroadcastProcessQueue}.
|
||||
@@ -233,8 +265,18 @@ class BroadcastProcessQueue {
|
||||
*/
|
||||
public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate,
|
||||
@NonNull BroadcastConsumer consumer, boolean andRemove) {
|
||||
boolean didSomething = forEachMatchingBroadcastInQueue(mPending,
|
||||
predicate, consumer, andRemove);
|
||||
didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent,
|
||||
predicate, consumer, andRemove);
|
||||
return didSomething;
|
||||
}
|
||||
|
||||
private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
|
||||
@NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer,
|
||||
boolean andRemove) {
|
||||
boolean didSomething = false;
|
||||
final Iterator<SomeArgs> it = mPending.iterator();
|
||||
final Iterator<SomeArgs> it = queue.iterator();
|
||||
while (it.hasNext()) {
|
||||
final SomeArgs args = it.next();
|
||||
final BroadcastRecord record = (BroadcastRecord) args.arg1;
|
||||
@@ -309,7 +351,7 @@ class BroadcastProcessQueue {
|
||||
*/
|
||||
public void makeActiveNextPending() {
|
||||
// TODO: what if the next broadcast isn't runnable yet?
|
||||
final SomeArgs next = mPending.removeFirst();
|
||||
final SomeArgs next = removeNextBroadcast();
|
||||
mActive = (BroadcastRecord) next.arg1;
|
||||
mActiveIndex = next.argi1;
|
||||
mActiveBlockedUntilTerminalCount = next.argi2;
|
||||
@@ -413,13 +455,45 @@ class BroadcastProcessQueue {
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return mPending.isEmpty();
|
||||
return mPending.isEmpty() && mPendingUrgent.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return mActive != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will thrown an exception if there are no pending broadcasts; relies on
|
||||
* {@link #isEmpty()} being false.
|
||||
*/
|
||||
SomeArgs removeNextBroadcast() {
|
||||
ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
|
||||
return queue.removeFirst();
|
||||
}
|
||||
|
||||
@Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
|
||||
if (!mPendingUrgent.isEmpty()) {
|
||||
return mPendingUrgent;
|
||||
} else if (!mPending.isEmpty()) {
|
||||
return mPending;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if there are no pending broadcasts
|
||||
*/
|
||||
@Nullable SomeArgs peekNextBroadcast() {
|
||||
ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
|
||||
return (queue != null) ? queue.peekFirst() : null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Nullable BroadcastRecord peekNextBroadcastRecord() {
|
||||
ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
|
||||
return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly determine if this queue has broadcasts that are still waiting to
|
||||
* be delivered at some point in the future.
|
||||
@@ -437,11 +511,13 @@ class BroadcastProcessQueue {
|
||||
return mActive.enqueueTime > barrierTime;
|
||||
}
|
||||
final SomeArgs next = mPending.peekFirst();
|
||||
if (next != null) {
|
||||
return ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
|
||||
}
|
||||
// Nothing running or runnable means we're past the barrier
|
||||
return true;
|
||||
final SomeArgs nextUrgent = mPendingUrgent.peekFirst();
|
||||
// Empty queue is past any barrier
|
||||
final boolean nextLater = next == null
|
||||
|| ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
|
||||
final boolean nextUrgentLater = nextUrgent == null
|
||||
|| ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime;
|
||||
return nextLater && nextUrgentLater;
|
||||
}
|
||||
|
||||
public boolean isRunnable() {
|
||||
@@ -519,7 +595,7 @@ class BroadcastProcessQueue {
|
||||
* Update {@link #getRunnableAt()} if it's currently invalidated.
|
||||
*/
|
||||
private void updateRunnableAt() {
|
||||
final SomeArgs next = mPending.peekFirst();
|
||||
final SomeArgs next = peekNextBroadcast();
|
||||
if (next != null) {
|
||||
final BroadcastRecord r = (BroadcastRecord) next.arg1;
|
||||
final int index = next.argi1;
|
||||
@@ -537,7 +613,7 @@ class BroadcastProcessQueue {
|
||||
|
||||
// If we have too many broadcasts pending, bypass any delays that
|
||||
// might have been applied above to aid draining
|
||||
if (mPending.size() >= constants.MAX_PENDING_BROADCASTS) {
|
||||
if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) {
|
||||
mRunnableAt = runnableAt;
|
||||
mRunnableAtReason = REASON_MAX_PENDING;
|
||||
return;
|
||||
@@ -574,8 +650,8 @@ class BroadcastProcessQueue {
|
||||
*/
|
||||
public void checkHealthLocked() {
|
||||
if (mRunnableAtReason == REASON_BLOCKED) {
|
||||
final SomeArgs next = mPending.peekFirst();
|
||||
Objects.requireNonNull(next, "peekFirst");
|
||||
final SomeArgs next = peekNextBroadcast();
|
||||
Objects.requireNonNull(next, "peekNextBroadcast");
|
||||
|
||||
// If blocked more than 10 minutes, we're likely wedged
|
||||
final BroadcastRecord r = (BroadcastRecord) next.arg1;
|
||||
|
||||
@@ -83,6 +83,7 @@ final class BroadcastRecord extends Binder {
|
||||
final boolean alarm; // originated from an alarm triggering?
|
||||
final boolean pushMessage; // originated from a push message?
|
||||
final boolean pushMessageOverQuota; // originated from a push message which was over quota?
|
||||
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 int userId; // user id this broadcast was for
|
||||
@@ -392,6 +393,7 @@ final class BroadcastRecord extends Binder {
|
||||
alarm = options != null && options.isAlarmBroadcast();
|
||||
pushMessage = options != null && options.isPushMessagingBroadcast();
|
||||
pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
|
||||
interactive = options != null && options.isInteractiveBroadcast();
|
||||
this.filterExtrasForReceiver = filterExtrasForReceiver;
|
||||
}
|
||||
|
||||
@@ -450,6 +452,7 @@ final class BroadcastRecord extends Binder {
|
||||
alarm = from.alarm;
|
||||
pushMessage = from.pushMessage;
|
||||
pushMessageOverQuota = from.pushMessageOverQuota;
|
||||
interactive = from.interactive;
|
||||
filterExtrasForReceiver = from.filterExtrasForReceiver;
|
||||
}
|
||||
|
||||
@@ -611,6 +614,18 @@ final class BroadcastRecord extends Binder {
|
||||
return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core policy determination about this broadcast's delivery prioritization
|
||||
*/
|
||||
boolean isUrgent() {
|
||||
// TODO: flags for controlling policy
|
||||
// TODO: migrate alarm-prioritization flag to BroadcastConstants
|
||||
return (isForeground()
|
||||
|| interactive
|
||||
|| alarm)
|
||||
&& receivers.size() == 1;
|
||||
}
|
||||
|
||||
@NonNull String getHostingRecordTriggerType() {
|
||||
if (alarm) {
|
||||
return HostingRecord.TRIGGER_TYPE_ALARM;
|
||||
|
||||
@@ -443,13 +443,14 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
|
||||
// invocation side effects such as allowlisting.
|
||||
if (options != null && callingUid != Process.SYSTEM_UID
|
||||
&& key.type == ActivityManager.INTENT_SENDER_BROADCAST) {
|
||||
if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
|
||||
if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)
|
||||
|| options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) {
|
||||
if (DEBUG_BROADCAST_LIGHT) {
|
||||
Slog.w(TAG, "Non-system caller " + callingUid
|
||||
+ " may not flag broadcast as alarm-related");
|
||||
+ " may not flag broadcast as alarm or interactive");
|
||||
}
|
||||
throw new SecurityException(
|
||||
"Non-system callers may not flag broadcasts as alarm-related");
|
||||
"Non-system callers may not flag broadcasts as alarm or interactive");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,20 +291,30 @@ public class BroadcastQueueModernImplTest {
|
||||
final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
|
||||
PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
|
||||
|
||||
// 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, 0);
|
||||
|
||||
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, 0);
|
||||
|
||||
// verify that:
|
||||
// (a) the queue is immediately runnable by existence of a fg-priority broadcast
|
||||
// (b) the next one up is the fg-priority broadcast despite its later enqueue time
|
||||
queue.setProcessCached(false);
|
||||
assertTrue(queue.isRunnable());
|
||||
assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
|
||||
assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
|
||||
assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
|
||||
|
||||
queue.setProcessCached(true);
|
||||
assertTrue(queue.isRunnable());
|
||||
assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
|
||||
assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
|
||||
assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user