OSDN Git Service

Defer broadcasts to slow-handling apps
authorChristopher Tate <ctate@google.com>
Fri, 18 Jan 2019 00:58:31 +0000 (16:58 -0800)
committerChristopher Tate <ctate@google.com>
Mon, 28 Jan 2019 20:32:41 +0000 (12:32 -0800)
When an app takes a long time to handle broadcasts, we start deferring
further broadcasts to it to make sure that other broadcast traffic in
the system can continue to make progress.  Global delivery order is
technically rearranged, but delivery order from the point of view of any
given app remains consistent with issuance order.

When alarm broadcasts are issued, we prioritize delivery of deferred
alarms to the alarm recipients (i.e. we suspend the deferral policy and
catch up as promptly as possible) in order to minimize wake time spent
waiting for the alarm broadcast to be delivered.  Once an app with a
deferred broadcast backlog is no longer the target of an in-flight
alarm, we re-impose deferral policy on it.

This policy intentionally trades off increased broadcast delivery
latency to apps that take a "long" time to handle broadcasts, in
exchange for lowering delivery latency to all other apps in the system
that would previously have had to wait behind the slow app.

In addition, broadcast dispatch policy parameters can now be overlaid
via the usual global Settings mechanism.  In particular, configuring the
"bcast_slow_time" parameter to a value in milliseconds higher than the
queue's broadcast timeout period will disable the new slow-receiver
policies.

Bug: 111404343
Test: device boots & runs
Test: tests/ActivityTests
Change-Id: I76ac79bdf41ca3cfcc48515bca779ea0f5744c0b

15 files changed:
core/java/android/app/IActivityManager.aidl
core/java/android/app/PendingIntent.java
core/java/android/provider/Settings.java
core/tests/coretests/src/android/provider/SettingsBackupTest.java
services/core/java/com/android/server/AlarmManagerInternal.java
services/core/java/com/android/server/AlarmManagerService.java
services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
services/core/java/com/android/server/am/ActivityManagerService.java
services/core/java/com/android/server/am/BroadcastConstants.java [new file with mode: 0644]
services/core/java/com/android/server/am/BroadcastDispatcher.java [new file with mode: 0644]
services/core/java/com/android/server/am/BroadcastQueue.java
services/core/java/com/android/server/am/BroadcastRecord.java
tests/ActivityTests/AndroidManifest.xml
tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
tests/ActivityTests/src/com/google/android/test/activity/SlowReceiver.java [new file with mode: 0644]

index fb65da1..412d7f4 100644 (file)
@@ -277,6 +277,7 @@ interface IActivityManager {
     void unstableProviderDied(in IBinder connection);
     boolean isIntentSenderAnActivity(in IIntentSender sender);
     boolean isIntentSenderAForegroundService(in IIntentSender sender);
+    boolean isIntentSenderABroadcast(in IIntentSender sender);
     int startActivityAsUser(in IApplicationThread caller, in String callingPackage,
             in Intent intent, in String resolvedType, in IBinder resultTo, in String resultWho,
             int requestCode, int flags, in ProfilerInfo profilerInfo,
index 75d95b2..55014eb 100644 (file)
@@ -1113,6 +1113,19 @@ public final class PendingIntent implements Parcelable {
 
     /**
      * @hide
+     * Check whether this PendingIntent will launch an Activity.
+     */
+    public boolean isBroadcast() {
+        try {
+            return ActivityManager.getService()
+                .isIntentSenderABroadcast(mTarget);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
      * Return the Intent of this PendingIntent.
      */
     @UnsupportedAppUsage
index 852b65a..6ff0171 100644 (file)
@@ -11766,6 +11766,44 @@ public final class Settings {
         public static final String SYNC_MANAGER_CONSTANTS = "sync_manager_constants";
 
         /**
+         * Broadcast dispatch tuning parameters specific to foreground broadcasts.
+         *
+         * This is encoded as a key=value list, separated by commas. Ex: "foo=1,bar=true"
+         *
+         * The following keys are supported:
+         * <pre>
+         * bcast_timeout                (long)
+         * bcast_slow_time              (long)
+         * bcast_deferral               (long)
+         * bcast_deferral_decay_factor  (float)
+         * bcast_deferral_floor         (long)
+         * </pre>
+         *
+         * @hide
+         */
+        public static final String BROADCAST_FG_CONSTANTS = "bcast_fg_constants";
+
+        /**
+         * Broadcast dispatch tuning parameters specific to background broadcasts.
+         *
+         * This is encoded as a key=value list, separated by commas. Ex: "foo=1,bar=true".
+         * See {@link #BROADCAST_FG_CONSTANTS} for the list of supported keys.
+         *
+         * @hide
+         */
+        public static final String BROADCAST_BG_CONSTANTS = "bcast_bg_constants";
+
+        /**
+         * Broadcast dispatch tuning parameters specific to specific "offline" broadcasts.
+         *
+         * This is encoded as a key=value list, separated by commas. Ex: "foo=1,bar=true".
+         * See {@link #BROADCAST_FG_CONSTANTS} for the list of supported keys.
+         *
+         * @hide
+         */
+        public static final String BROADCAST_OFFLOAD_CONSTANTS = "bcast_offload_constants";
+
+        /**
          * Whether or not App Standby feature is enabled by system. This controls throttling of apps
          * based on usage patterns and predictions. Platform will turn on this feature if both this
          * flag and {@link #ADAPTIVE_BATTERY_MANAGEMENT_ENABLED} is on.
index bd7f852..2a29f83 100644 (file)
@@ -132,6 +132,9 @@ public class SettingsBackupTest {
                     Settings.Global.AUTOMATIC_POWER_SAVER_MODE,
                     Settings.Global.BACKGROUND_ACTIVITY_STARTS_ENABLED,
                     Settings.Global.BATTERY_CHARGING_STATE_UPDATE_DELAY,
+                    Settings.Global.BROADCAST_BG_CONSTANTS,
+                    Settings.Global.BROADCAST_FG_CONSTANTS,
+                    Settings.Global.BROADCAST_OFFLOAD_CONSTANTS,
                     Settings.Global.BATTERY_DISCHARGE_DURATION_THRESHOLD,
                     Settings.Global.BATTERY_DISCHARGE_THRESHOLD,
                     Settings.Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS,
index dbff957..2756610 100644 (file)
 package com.android.server;
 
 public interface AlarmManagerInternal {
-    void removeAlarmsForUid(int uid);
+    // Some other components in the system server need to know about
+    // broadcast alarms currently in flight
+    public interface InFlightListener {
+        /** There is now an alarm pending delivery to the given app */
+        void broadcastAlarmPending(int recipientUid);
+        /** A broadcast alarm targeted to the given app has completed delivery */
+        void broadcastAlarmComplete(int recipientUid);
+    }
+
+    public void removeAlarmsForUid(int uid);
+    public void registerInFlightListener(InFlightListener callback);
 }
index e3dcb7d..10b5327 100644 (file)
@@ -197,6 +197,8 @@ class AlarmManagerService extends SystemService {
     PowerManager.WakeLock mWakeLock;
     ArrayList<Alarm> mPendingNonWakeupAlarms = new ArrayList<>();
     ArrayList<InFlight> mInFlight = new ArrayList<>();
+    private final ArrayList<AlarmManagerInternal.InFlightListener> mInFlightListeners =
+            new ArrayList<>();
     AlarmHandler mHandler;
     AppWakeupHistory mAppWakeupHistory;
     ClockReceiver mClockReceiver;
@@ -1315,6 +1317,10 @@ class AlarmManagerService extends SystemService {
             mAlarmType = alarm.type;
         }
 
+        boolean isBroadcast() {
+            return mPendingIntent != null && mPendingIntent.isBroadcast();
+        }
+
         @Override
         public String toString() {
             return "InFlight{"
@@ -1354,6 +1360,20 @@ class AlarmManagerService extends SystemService {
         }
     }
 
+    private void notifyBroadcastAlarmPendingLocked(int uid) {
+        final int numListeners = mInFlightListeners.size();
+        for (int i = 0; i < numListeners; i++) {
+            mInFlightListeners.get(i).broadcastAlarmPending(uid);
+        }
+    }
+
+    private void notifyBroadcastAlarmCompleteLocked(int uid) {
+        final int numListeners = mInFlightListeners.size();
+        for (int i = 0; i < numListeners; i++) {
+            mInFlightListeners.get(i).broadcastAlarmComplete(uid);
+        }
+    }
+
     static final class FilterStats {
         final BroadcastStats mBroadcastStats;
         final String mTag;
@@ -1976,6 +1996,13 @@ class AlarmManagerService extends SystemService {
                 removeLocked(uid);
             }
         }
+
+        @Override
+        public void registerInFlightListener(InFlightListener callback) {
+            synchronized (mLock) {
+                mInFlightListeners.add(callback);
+            }
+        }
     }
 
     /**
@@ -4426,7 +4453,11 @@ class AlarmManagerService extends SystemService {
 
         private InFlight removeLocked(PendingIntent pi, Intent intent) {
             for (int i = 0; i < mInFlight.size(); i++) {
-                if (mInFlight.get(i).mPendingIntent == pi) {
+                final InFlight inflight = mInFlight.get(i);
+                if (inflight.mPendingIntent == pi) {
+                    if (pi.isBroadcast()) {
+                        notifyBroadcastAlarmCompleteLocked(inflight.mUid);
+                    }
                     return mInFlight.remove(i);
                 }
             }
@@ -4649,6 +4680,9 @@ class AlarmManagerService extends SystemService {
             final InFlight inflight = new InFlight(AlarmManagerService.this, alarm, nowELAPSED);
             mInFlight.add(inflight);
             mBroadcastRefCount++;
+            if (inflight.isBroadcast()) {
+                notifyBroadcastAlarmPendingLocked(alarm.uid);
+            }
             if (allowWhileIdle) {
                 // Record the last time this uid handled an ALLOW_WHILE_IDLE alarm.
                 mLastAllowWhileIdleDispatch.put(alarm.creatorUid, nowELAPSED);
index 0aaea2f..e42666c 100644 (file)
@@ -48,6 +48,7 @@ class ActivityManagerDebugConfig {
     static final boolean DEBUG_BROADCAST = DEBUG_ALL || false;
     static final boolean DEBUG_BROADCAST_BACKGROUND = DEBUG_BROADCAST || false;
     static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false;
+    static final boolean DEBUG_BROADCAST_DEFERRAL = DEBUG_BROADCAST || false;
     static final boolean DEBUG_LRU = DEBUG_ALL || false;
     static final boolean DEBUG_MU = DEBUG_ALL || false;
     static final boolean DEBUG_NETWORK = DEBUG_ALL || false;
index eb643b6..d9f3c02 100644 (file)
@@ -224,7 +224,6 @@ import android.hardware.display.DisplayManagerInternal;
 import android.location.LocationManager;
 import android.media.audiofx.AudioEffect;
 import android.net.Proxy;
-import android.net.ProxyInfo;
 import android.net.Uri;
 import android.os.AppZygote;
 import android.os.BatteryStats;
@@ -2090,6 +2089,8 @@ public class ActivityManagerService extends IActivityManager.Stub
             if (phase == PHASE_SYSTEM_SERVICES_READY) {
                 mService.mBatteryStatsService.systemServicesReady();
                 mService.mServices.systemServicesReady();
+            } else if (phase == PHASE_ACTIVITY_MANAGER_READY) {
+                mService.startBroadcastObservers();
             }
         }
 
@@ -2266,12 +2267,27 @@ public class ActivityManagerService extends IActivityManager.Stub
         mProcessList.init(this, activeUids);
         mOomAdjuster = new OomAdjuster(this, mProcessList, activeUids);
 
+        // Broadcast policy parameters
+        final BroadcastConstants foreConstants = new BroadcastConstants(
+                Settings.Global.BROADCAST_FG_CONSTANTS);
+        foreConstants.TIMEOUT = BROADCAST_FG_TIMEOUT;
+
+        final BroadcastConstants backConstants = new BroadcastConstants(
+                Settings.Global.BROADCAST_BG_CONSTANTS);
+        backConstants.TIMEOUT = BROADCAST_BG_TIMEOUT;
+
+        final BroadcastConstants offloadConstants = new BroadcastConstants(
+                Settings.Global.BROADCAST_OFFLOAD_CONSTANTS);
+        offloadConstants.TIMEOUT = BROADCAST_BG_TIMEOUT;
+        // by default, no "slow" policy in this queue
+        offloadConstants.SLOW_TIME = Integer.MAX_VALUE;
+
         mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
-                "foreground", BROADCAST_FG_TIMEOUT, false);
+                "foreground", foreConstants, false);
         mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
-                "background", BROADCAST_BG_TIMEOUT, true);
+                "background", backConstants, true);
         mOffloadBroadcastQueue = new BroadcastQueue(this, mHandler,
-                "offload", BROADCAST_BG_TIMEOUT, true);
+                "offload", offloadConstants, true);
         mBroadcastQueues[0] = mFgBroadcastQueue;
         mBroadcastQueues[1] = mBgBroadcastQueue;
         mBroadcastQueues[2] = mOffloadBroadcastQueue;
@@ -5226,6 +5242,15 @@ public class ActivityManagerService extends IActivityManager.Stub
     }
 
     @Override
+    public boolean isIntentSenderABroadcast(IIntentSender pendingResult) {
+        if (pendingResult instanceof PendingIntentRecord) {
+            final PendingIntentRecord res = (PendingIntentRecord) pendingResult;
+            return res.key.type == ActivityManager.INTENT_SENDER_BROADCAST;
+        }
+        return false;
+    }
+
+    @Override
     public Intent getIntentForIntentSender(IIntentSender pendingResult) {
         enforceCallingPermission(Manifest.permission.GET_INTENT_SENDER_INTENT,
                 "getIntentForIntentSender()");
@@ -7211,7 +7236,6 @@ public class ActivityManagerService extends IActivityManager.Stub
         synchronized (this) {
             mSystemProvidersInstalled = true;
         }
-
         mConstants.start(mContext.getContentResolver());
         mCoreSettingsObserver = new CoreSettingsObserver(this);
         mActivityTaskManager.installSystemProviders();
@@ -8711,6 +8735,12 @@ public class ActivityManagerService extends IActivityManager.Stub
         }
     }
 
+    private void startBroadcastObservers() {
+        for (BroadcastQueue queue : mBroadcastQueues) {
+            queue.start(mContext.getContentResolver());
+        }
+    }
+
     private void updateForceBackgroundCheck(boolean enabled) {
         synchronized (this) {
             if (mForceBackgroundCheck != enabled) {
@@ -14909,10 +14939,7 @@ public class ActivityManagerService extends IActivityManager.Stub
                     resultData, resultExtras, ordered, sticky, false, userId,
                     allowBackgroundActivityStarts);
 
-            if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r
-                    + ": prev had " + queue.mOrderedBroadcasts.size());
-            if (DEBUG_BROADCAST) Slog.i(TAG_BROADCAST,
-                    "Enqueueing broadcast " + r.intent.getAction());
+            if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
 
             final BroadcastRecord oldRecord =
                     replacePending ? queue.replaceOrderedBroadcastLocked(r) : null;
@@ -15788,13 +15815,12 @@ public class ActivityManagerService extends IActivityManager.Stub
      * Returns true if things are idle enough to perform GCs.
      */
     private final boolean canGcNowLocked() {
-        boolean processingBroadcasts = false;
         for (BroadcastQueue q : mBroadcastQueues) {
-            if (q.mParallelBroadcasts.size() != 0 || q.mOrderedBroadcasts.size() != 0) {
-                processingBroadcasts = true;
+            if (!q.mParallelBroadcasts.isEmpty() || !q.mDispatcher.isEmpty()) {
+                return false;
             }
         }
-        return !processingBroadcasts && mAtmInternal.canGcNow();
+        return mAtmInternal.canGcNow();
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
new file mode 100644 (file)
index 0000000..820caf1
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2018 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.content.ContentResolver;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Tunable parameters for broadcast dispatch policy
+ */
+public class BroadcastConstants {
+    private static final String TAG = "BroadcastConstants";
+
+    // Value element names within the Settings record
+    static final String KEY_TIMEOUT = "bcast_timeout";
+    static final String KEY_SLOW_TIME = "bcast_slow_time";
+    static final String KEY_DEFERRAL = "bcast_deferral";
+    static final String KEY_DEFERRAL_DECAY_FACTOR = "bcast_deferral_decay_factor";
+    static final String KEY_DEFERRAL_FLOOR = "bcast_deferral_floor";
+
+    // All time intervals are in milliseconds
+    private static final long DEFAULT_TIMEOUT = 10_000;
+    private static final long DEFAULT_SLOW_TIME = 5_000;
+    private static final long DEFAULT_DEFERRAL = 5_000;
+    private static final float DEFAULT_DEFERRAL_DECAY_FACTOR = 0.75f;
+    private static final long DEFAULT_DEFERRAL_FLOOR = 0;
+
+    // All time constants are in milliseconds
+
+    // Timeout period for this broadcast queue
+    public long TIMEOUT = DEFAULT_TIMEOUT;
+    // Handling time above which we declare that a broadcast recipient was "slow".  Any
+    // value <= zero is interpreted as disabling broadcast deferral policy entirely.
+    public long SLOW_TIME = DEFAULT_SLOW_TIME;
+    // How long to initially defer broadcasts, if an app is slow to handle one
+    public long DEFERRAL = DEFAULT_DEFERRAL;
+    // Decay factor for successive broadcasts' deferral time
+    public float DEFERRAL_DECAY_FACTOR = DEFAULT_DEFERRAL_DECAY_FACTOR;
+    // Minimum that the deferral time can decay to until the backlog fully clears
+    public long DEFERRAL_FLOOR = DEFAULT_DEFERRAL_FLOOR;
+
+    // Settings override tracking for this instance
+    private String mSettingsKey;
+    private SettingsObserver mSettingsObserver;
+    private ContentResolver mResolver;
+    private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+    class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            updateConstants();
+        }
+    }
+
+    // A given constants instance is configured to observe specific keys from which
+    // that instance's values are drawn.
+    public BroadcastConstants(String settingsKey) {
+        mSettingsKey = settingsKey;
+    }
+
+    /**
+     * Spin up the observer lazily, since it can only happen once the settings provider
+     * has been brought into service
+     */
+    public void startObserving(Handler handler, ContentResolver resolver) {
+        mResolver = resolver;
+
+        mSettingsObserver = new SettingsObserver(handler);
+        mResolver.registerContentObserver(Settings.Global.getUriFor(mSettingsKey),
+                false, mSettingsObserver);
+
+        updateConstants();
+    }
+
+    private void updateConstants() {
+        synchronized (mParser) {
+            try {
+                mParser.setString(Settings.Global.getString(mResolver, mSettingsKey));
+            } catch (IllegalArgumentException e) {
+                Slog.e(TAG, "Bad broadcast settings in key '" + mSettingsKey + "'", e);
+                return;
+            }
+
+            // Unspecified fields retain their current value rather than revert to default
+            TIMEOUT = mParser.getLong(KEY_TIMEOUT, TIMEOUT);
+            SLOW_TIME = mParser.getLong(KEY_SLOW_TIME, SLOW_TIME);
+            DEFERRAL = mParser.getLong(KEY_DEFERRAL, DEFERRAL);
+            DEFERRAL_DECAY_FACTOR = mParser.getFloat(KEY_DEFERRAL_DECAY_FACTOR,
+                    DEFERRAL_DECAY_FACTOR);
+            DEFERRAL_FLOOR = mParser.getLong(KEY_DEFERRAL_FLOOR, DEFERRAL_FLOOR);
+        }
+    }
+
+    /**
+     * Standard dumpsys support; invoked from BroadcastQueue dump
+     */
+    public void dump(PrintWriter pw) {
+        synchronized (mParser) {
+            pw.println();
+            pw.print("  Broadcast parameters (key=");
+            pw.print(mSettingsKey);
+            pw.print(", observing=");
+            pw.print(mSettingsObserver != null);
+            pw.println("):");
+
+            pw.print("    "); pw.print(KEY_TIMEOUT); pw.print(" = ");
+            TimeUtils.formatDuration(TIMEOUT, pw);
+            pw.println();
+
+            pw.print("    "); pw.print(KEY_SLOW_TIME); pw.print(" = ");
+            TimeUtils.formatDuration(SLOW_TIME, pw);
+            pw.println();
+
+            pw.print("    "); pw.print(KEY_DEFERRAL); pw.print(" = ");
+            TimeUtils.formatDuration(DEFERRAL, pw);
+            pw.println();
+
+            pw.print("    "); pw.print(KEY_DEFERRAL_DECAY_FACTOR); pw.print(" = ");
+            pw.println(DEFERRAL_DECAY_FACTOR);
+
+            pw.print("    "); pw.print(KEY_DEFERRAL_FLOOR); pw.print(" = ");
+            TimeUtils.formatDuration(DEFERRAL_FLOOR, pw);
+            pw.println();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/BroadcastDispatcher.java b/services/core/java/com/android/server/am/BroadcastDispatcher.java
new file mode 100644 (file)
index 0000000..0d46379
--- /dev/null
@@ -0,0 +1,687 @@
+/*
+ * Copyright (C) 2018 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 static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_DEFERRAL;
+
+import android.content.Intent;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.AlarmManagerInternal;
+import com.android.server.LocalServices;
+
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Manages ordered broadcast delivery, applying policy to mitigate the effects of
+ * slow receivers.
+ */
+public class BroadcastDispatcher {
+    private static final String TAG = "BroadcastDispatcher";
+
+    // Deferred broadcasts to one app; times are all uptime time base like
+    // other broadcast-related timekeeping
+    static class Deferrals {
+        final int uid;
+        long deferredAt;    // when we started deferring
+        long deferredBy;    // how long did we defer by last time?
+        long deferUntil;    // when does the next element become deliverable?
+        int alarmCount;
+
+        final ArrayList<BroadcastRecord> broadcasts;
+
+        Deferrals(int uid, long now, long backoff, int count) {
+            this.uid = uid;
+            this.deferredAt = now;
+            this.deferredBy = backoff;
+            this.deferUntil = now + backoff;
+            this.alarmCount = count;
+            broadcasts = new ArrayList<>();
+        }
+
+        void add(BroadcastRecord br) {
+            broadcasts.add(br);
+        }
+
+        void writeToProto(ProtoOutputStream proto, long fieldId) {
+            for (BroadcastRecord br : broadcasts) {
+                br.writeToProto(proto, fieldId);
+            }
+        }
+
+        void dumpLocked(Dumper d) {
+            for (BroadcastRecord br : broadcasts) {
+                d.dump(br);
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("Deferrals{uid=");
+            sb.append(uid);
+            sb.append(", deferUntil=");
+            sb.append(deferUntil);
+            sb.append(", #broadcasts=");
+            sb.append(broadcasts.size());
+            sb.append("}");
+            return sb.toString();
+        }
+    }
+
+    // Carrying dump formatting state across multiple concatenated datasets
+    class Dumper {
+        final PrintWriter mPw;
+        final String mQueueName;
+        final String mDumpPackage;
+        final SimpleDateFormat mSdf;
+        boolean mPrinted;
+        boolean mNeedSep;
+        String mHeading;
+        String mLabel;
+        int mOrdinal;
+
+        Dumper(PrintWriter pw, String queueName, String dumpPackage, SimpleDateFormat sdf) {
+            mPw = pw;
+            mQueueName = queueName;
+            mDumpPackage = dumpPackage;
+            mSdf = sdf;
+
+            mPrinted = false;
+            mNeedSep = true;
+        }
+
+        void setHeading(String heading) {
+            mHeading = heading;
+            mPrinted = false;
+        }
+
+        void setLabel(String label) {
+            //"  Active Ordered Broadcast " + mQueueName + " #" + i + ":"
+            mLabel = "  " + label + " " + mQueueName + " #";
+            mOrdinal = 0;
+        }
+
+        boolean didPrint() {
+            return mPrinted;
+        }
+
+        void dump(BroadcastRecord br) {
+            if (mDumpPackage == null || mDumpPackage.equals(br.callerPackage)) {
+                if (!mPrinted) {
+                    if (mNeedSep) {
+                        mPw.println();
+                    }
+                    mPrinted = true;
+                    mNeedSep = true;
+                    mPw.println("  " + mHeading + " [" + mQueueName + "]:");
+                }
+                mPw.println(mLabel + mOrdinal + ":");
+                mOrdinal++;
+
+                br.dump(mPw, "    ", mSdf);
+            }
+        }
+    }
+
+    private final Object mLock;
+    private final BroadcastQueue mQueue;
+    private final BroadcastConstants mConstants;
+    private final Handler mHandler;
+    private AlarmManagerInternal mAlarm;
+
+    // Current alarm targets; mapping uid -> in-flight alarm count
+    final SparseIntArray mAlarmUids = new SparseIntArray();
+    final AlarmManagerInternal.InFlightListener mAlarmListener =
+            new AlarmManagerInternal.InFlightListener() {
+        @Override
+        public void broadcastAlarmPending(final int recipientUid) {
+            synchronized (mLock) {
+                final int newCount = mAlarmUids.get(recipientUid, 0) + 1;
+                mAlarmUids.put(recipientUid, newCount);
+                // any deferred broadcasts to this app now get fast-tracked
+                final int numEntries = mDeferredBroadcasts.size();
+                for (int i = 0; i < numEntries; i++) {
+                    if (recipientUid == mDeferredBroadcasts.get(i).uid) {
+                        Deferrals d = mDeferredBroadcasts.remove(i);
+                        mAlarmBroadcasts.add(d);
+                        break;
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void broadcastAlarmComplete(final int recipientUid) {
+            synchronized (mLock) {
+                final int newCount = mAlarmUids.get(recipientUid, 0) - 1;
+                if (newCount >= 0) {
+                    mAlarmUids.put(recipientUid, newCount);
+                } else {
+                    Slog.wtf(TAG, "Undercount of broadcast alarms in flight for " + recipientUid);
+                    mAlarmUids.put(recipientUid, 0);
+                }
+
+                // No longer an alarm target, so resume ordinary deferral policy
+                if (newCount <= 0) {
+                    final int numEntries = mAlarmBroadcasts.size();
+                    for (int i = 0; i < numEntries; i++) {
+                        if (recipientUid == mAlarmBroadcasts.get(i).uid) {
+                            Deferrals d = mAlarmBroadcasts.remove(i);
+                            insertLocked(mDeferredBroadcasts, d);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    // Queue recheck operation used to tickle broadcast delivery when appropriate
+    final Runnable mScheduleRunnable = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                if (DEBUG_BROADCAST_DEFERRAL) {
+                    Slog.v(TAG, "Deferral recheck of pending broadcasts");
+                }
+                mQueue.scheduleBroadcastsLocked();
+                mRecheckScheduled = false;
+            }
+        }
+    };
+    private boolean mRecheckScheduled = false;
+
+    // Usual issuance-order outbound queue
+    private final ArrayList<BroadcastRecord> mOrderedBroadcasts = new ArrayList<>();
+    // General deferrals not holding up alarms
+    private final ArrayList<Deferrals> mDeferredBroadcasts = new ArrayList<>();
+    // Deferrals that *are* holding up alarms; ordered by alarm dispatch time
+    private final ArrayList<Deferrals> mAlarmBroadcasts = new ArrayList<>();
+
+    // Next outbound broadcast, established by getNextBroadcastLocked()
+    private BroadcastRecord mCurrentBroadcast;
+
+    /**
+     * Constructed & sharing a lock with its associated BroadcastQueue instance
+     */
+    public BroadcastDispatcher(BroadcastQueue queue, BroadcastConstants constants,
+            Handler handler, Object lock) {
+        mQueue = queue;
+        mConstants = constants;
+        mHandler = handler;
+        mLock = lock;
+    }
+
+    /**
+     * Spin up the integration with the alarm manager service; done lazily to manage
+     * service availability ordering during boot.
+     */
+    public void start() {
+        // Set up broadcast alarm tracking
+        mAlarm = LocalServices.getService(AlarmManagerInternal.class);
+        mAlarm.registerInFlightListener(mAlarmListener);
+    }
+
+    /**
+     * Standard contents-are-empty check
+     */
+    public boolean isEmpty() {
+        synchronized (mLock) {
+            return mCurrentBroadcast == null
+                    && mOrderedBroadcasts.isEmpty()
+                    && mDeferredBroadcasts.isEmpty()
+                    && mAlarmBroadcasts.isEmpty();
+        }
+    }
+
+    /**
+     * Not quite the traditional size() measurement; includes any in-process but
+     * not yet retired active outbound broadcast.
+     */
+    public int totalUndelivered() {
+        synchronized (mLock) {
+            return mAlarmBroadcasts.size()
+                    + mDeferredBroadcasts.size()
+                    + mOrderedBroadcasts.size()
+                    + (mCurrentBroadcast == null ? 0 : 1);
+        }
+    }
+
+    // ----------------------------------
+    // BroadcastQueue operation support
+
+    void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
+        mOrderedBroadcasts.add(r);
+    }
+
+    // Returns the now-replaced broadcast record, or null if none
+    BroadcastRecord replaceBroadcastLocked(BroadcastRecord r, String typeForLogging) {
+        // Simple case, in the ordinary queue.
+        BroadcastRecord old = replaceBroadcastLocked(mOrderedBroadcasts, r, typeForLogging);
+
+        // If we didn't find it, less-simple:  in a deferral queue?
+        if (old == null) {
+            old = replaceDeferredBroadcastLocked(mAlarmBroadcasts, r, typeForLogging);
+        }
+        if (old == null) {
+            old = replaceDeferredBroadcastLocked(mDeferredBroadcasts, r, typeForLogging);
+        }
+        return old;
+    }
+
+    private BroadcastRecord replaceDeferredBroadcastLocked(ArrayList<Deferrals> list,
+            BroadcastRecord r, String typeForLogging) {
+        BroadcastRecord old;
+        final int numEntries = list.size();
+        for (int i = 0; i < numEntries; i++) {
+            final Deferrals d = list.get(i);
+            old = replaceBroadcastLocked(d.broadcasts, r, typeForLogging);
+            if (old != null) {
+                return old;
+            }
+        }
+        return null;
+    }
+
+    private BroadcastRecord replaceBroadcastLocked(ArrayList<BroadcastRecord> list,
+            BroadcastRecord r, String typeForLogging) {
+        BroadcastRecord old;
+        final Intent intent = r.intent;
+        // Any in-flight broadcast has already been popped, and cannot be replaced.
+        // (This preserves existing behavior of the replacement API)
+        for (int i = list.size() - 1; i >= 0; i++) {
+            old = list.get(i);
+            if (old.userId == r.userId && intent.filterEquals(old.intent)) {
+                if (DEBUG_BROADCAST) {
+                    Slog.v(TAG, "***** Replacing " + typeForLogging
+                            + " [" + mQueue.mQueueName + "]: " + intent);
+                }
+                // Clone deferral state too if any
+                r.deferred = old.deferred;
+                list.set(i, r);
+                return old;
+            }
+        }
+        return null;
+    }
+
+    boolean cleanupDisabledPackageReceiversLocked(final String packageName,
+            Set<String> filterByClasses, final int userId, final boolean doit) {
+        // Note: fast short circuits when 'doit' is false, as soon as we hit any
+        // "yes we would do something" circumstance
+        boolean didSomething = cleanupBroadcastListDisabledReceiversLocked(mOrderedBroadcasts,
+                packageName, filterByClasses, userId, doit);
+        if (doit || !didSomething) {
+            didSomething |= cleanupDeferralsListDisabledReceiversLocked(mAlarmBroadcasts,
+                    packageName, filterByClasses, userId, doit);
+        }
+        if (doit || !didSomething) {
+            didSomething |= cleanupDeferralsListDisabledReceiversLocked(mDeferredBroadcasts,
+                    packageName, filterByClasses, userId, doit);
+        }
+        if ((doit || !didSomething) && mCurrentBroadcast != null) {
+            didSomething |= mCurrentBroadcast.cleanupDisabledPackageReceiversLocked(
+                    packageName, filterByClasses, userId, doit);
+        }
+
+        return didSomething;
+    }
+
+    private boolean cleanupDeferralsListDisabledReceiversLocked(ArrayList<Deferrals> list,
+            final String packageName, Set<String> filterByClasses, final int userId,
+            final boolean doit) {
+        boolean didSomething = false;
+        for (Deferrals d : list) {
+            didSomething = cleanupBroadcastListDisabledReceiversLocked(d.broadcasts,
+                    packageName, filterByClasses, userId, doit);
+            if (!doit && didSomething) {
+                return true;
+            }
+        }
+        return didSomething;
+    }
+
+    private boolean cleanupBroadcastListDisabledReceiversLocked(ArrayList<BroadcastRecord> list,
+            final String packageName, Set<String> filterByClasses, final int userId,
+            final boolean doit) {
+        boolean didSomething = false;
+        for (BroadcastRecord br : list) {
+            didSomething |= br.cleanupDisabledPackageReceiversLocked(packageName,
+                    filterByClasses, userId, doit);
+            if (!doit && didSomething) {
+                return true;
+            }
+        }
+        return didSomething;
+    }
+
+    /**
+     * Standard proto dump entry point
+     */
+    public void writeToProto(ProtoOutputStream proto, long fieldId) {
+        if (mCurrentBroadcast != null) {
+            mCurrentBroadcast.writeToProto(proto, fieldId);
+        }
+        for (Deferrals d : mAlarmBroadcasts) {
+            d.writeToProto(proto, fieldId);
+        }
+        for (BroadcastRecord br : mOrderedBroadcasts) {
+            br.writeToProto(proto, fieldId);
+        }
+        for (Deferrals d : mDeferredBroadcasts) {
+            d.writeToProto(proto, fieldId);
+        }
+    }
+
+    // ----------------------------------
+    // Dispatch & deferral management
+
+    public BroadcastRecord getActiveBroadcastLocked() {
+        return mCurrentBroadcast;
+    }
+
+    /**
+     * If there is a deferred broadcast that is being sent to an alarm target, return
+     * that one.  If there's no deferred alarm target broadcast but there is one
+     * that has reached the end of its deferral, return that.
+     *
+     * This stages the broadcast internally until it is retired, and returns that
+     * staged record if this is called repeatedly, until retireBroadcast(r) is called.
+     */
+    public BroadcastRecord getNextBroadcastLocked(final long now) {
+        if (mCurrentBroadcast != null) {
+            return mCurrentBroadcast;
+        }
+
+        BroadcastRecord next = null;
+        if (!mAlarmBroadcasts.isEmpty()) {
+            next = popLocked(mAlarmBroadcasts);
+            if (DEBUG_BROADCAST_DEFERRAL && next != null) {
+                Slog.i(TAG, "Next broadcast from alarm targets: " + next);
+            }
+        }
+
+        if (next == null && !mDeferredBroadcasts.isEmpty()) {
+            for (int i = 0; i < mDeferredBroadcasts.size(); i++) {
+                Deferrals d = mDeferredBroadcasts.get(i);
+                if (now < d.deferUntil) {
+                    // No more deferrals due
+                    break;
+                }
+
+                if (d.broadcasts.size() > 0) {
+                    next = d.broadcasts.remove(0);
+                    // apply deferral-interval decay policy and move this uid's
+                    // deferred broadcasts down in the delivery queue accordingly
+                    mDeferredBroadcasts.remove(i); // already 'd'
+                    d.deferredBy = calculateDeferral(d.deferredBy);
+                    d.deferUntil += d.deferredBy;
+                    insertLocked(mDeferredBroadcasts, d);
+                    if (DEBUG_BROADCAST_DEFERRAL) {
+                        Slog.i(TAG, "Next broadcast from deferrals " + next
+                                + ", deferUntil now " + d.deferUntil);
+                    }
+                    break;
+                }
+            }
+        }
+
+        if (next == null && !mOrderedBroadcasts.isEmpty()) {
+            next = mOrderedBroadcasts.remove(0);
+            if (DEBUG_BROADCAST_DEFERRAL) {
+                Slog.i(TAG, "Next broadcast from main queue: " + next);
+            }
+        }
+
+        mCurrentBroadcast = next;
+        return next;
+    }
+
+    /**
+     * Called after the broadcast queue finishes processing the currently
+     * active broadcast (obtained by calling getNextBroadcastLocked()).
+     */
+    public void retireBroadcastLocked(final BroadcastRecord r) {
+        // ERROR if 'r' is not the active broadcast
+        if (r != mCurrentBroadcast) {
+            Slog.wtf(TAG, "Retiring broadcast " + r
+                    + " doesn't match current outgoing " + mCurrentBroadcast);
+        }
+        mCurrentBroadcast = null;
+    }
+
+    /**
+     * Called prior to broadcast dispatch to check whether the intended
+     * recipient is currently subject to deferral policy.
+     */
+    public boolean isDeferringLocked(final int uid) {
+        Deferrals d = findUidLocked(uid);
+        if (d != null && d.broadcasts.isEmpty()) {
+            // once we've caught up with deferred broadcasts to this uid
+            // and time has advanced sufficiently that we wouldn't be
+            // deferring newly-enqueued ones, we're back to normal policy.
+            if (SystemClock.uptimeMillis() >= d.deferUntil) {
+                if (DEBUG_BROADCAST_DEFERRAL) {
+                    Slog.i(TAG, "No longer deferring broadcasts to uid " + d.uid);
+                }
+                removeDeferral(d);
+                return false;
+            }
+        }
+        return (d != null);
+    }
+
+    /**
+     * Defer broadcasts for the given app.  If 'br' is non-null, this also makes
+     * sure that broadcast record is enqueued as the next upcoming broadcast for
+     * the app.
+     */
+    public void startDeferring(final int uid) {
+        synchronized (mLock) {
+            Deferrals d = findUidLocked(uid);
+
+            // If we're not yet tracking this app, set up that bookkeeping
+            if (d == null) {
+                // Start a new deferral
+                final long now = SystemClock.uptimeMillis();
+                d = new Deferrals(uid,
+                        now,
+                        mConstants.DEFERRAL,
+                        mAlarmUids.get(uid, 0));
+                if (DEBUG_BROADCAST_DEFERRAL) {
+                    Slog.i(TAG, "Now deferring broadcasts to " + uid
+                            + " until " + d.deferUntil);
+                }
+                // where it goes depends on whether it is coming into an alarm-related situation
+                if (d.alarmCount == 0) {
+                    // common case, put it in the ordinary priority queue
+                    insertLocked(mDeferredBroadcasts, d);
+                    scheduleDeferralCheckLocked(true);
+                } else {
+                    // alarm-related: strict order-encountered
+                    mAlarmBroadcasts.add(d);
+                }
+            } else {
+                // We're already deferring, but something was slow again.  Reset the
+                // deferral decay progression.
+                d.deferredBy = mConstants.DEFERRAL;
+                if (DEBUG_BROADCAST_DEFERRAL) {
+                    Slog.i(TAG, "Uid " + uid + " slow again, deferral interval reset to "
+                            + d.deferredBy);
+                }
+            }
+        }
+    }
+
+    /**
+     * Key entry point when a broadcast about to be delivered is instead
+     * set aside for deferred delivery
+     */
+    public void addDeferredBroadcast(final int uid, BroadcastRecord br) {
+        if (DEBUG_BROADCAST_DEFERRAL) {
+            Slog.i(TAG, "Enqueuing deferred broadcast " + br);
+        }
+        synchronized (mLock) {
+            Deferrals d = findUidLocked(uid);
+            if (d == null) {
+                Slog.wtf(TAG, "Adding deferred broadcast but not tracking " + uid);
+            } else {
+                if (br == null) {
+                    Slog.wtf(TAG, "Deferring null broadcast to " + uid);
+                } else {
+                    br.deferred = true;
+                    d.add(br);
+                }
+            }
+        }
+    }
+
+    /**
+     * When there are deferred broadcasts, we need to make sure to recheck the
+     * dispatch queue when they come due.  Alarm-sensitive deferrals get dispatched
+     * aggressively, so we only need to use the ordinary deferrals timing to figure
+     * out when to recheck.
+     */
+    public void scheduleDeferralCheckLocked(boolean force) {
+        if ((force || !mRecheckScheduled) && !mDeferredBroadcasts.isEmpty()) {
+            final Deferrals d = mDeferredBroadcasts.get(0);
+            if (!d.broadcasts.isEmpty()) {
+                mHandler.removeCallbacks(mScheduleRunnable);
+                mHandler.postAtTime(mScheduleRunnable, d.deferUntil);
+                mRecheckScheduled = true;
+                if (DEBUG_BROADCAST_DEFERRAL) {
+                    Slog.i(TAG, "Scheduling deferred broadcast recheck at " + d.deferUntil);
+                }
+            }
+        }
+    }
+
+    // ----------------------------------
+
+    /**
+     * If broadcasts to this uid are being deferred, find the deferrals record about it.
+     * @return null if this uid's broadcasts are not being deferred
+     */
+    private Deferrals findUidLocked(final int uid) {
+        // The common case is that they it isn't also an alarm target...
+        Deferrals d = findUidLocked(uid, mDeferredBroadcasts);
+        // ...but if not there, also check alarm-prioritized deferrals
+        if (d == null) {
+            d = findUidLocked(uid, mAlarmBroadcasts);
+        }
+        return d;
+    }
+
+    /**
+     * Remove the given deferral record from whichever queue it might be in at present
+     * @return true if the deferral was in fact found, false if this made no changes
+     */
+    private boolean removeDeferral(Deferrals d) {
+        boolean didRemove = mDeferredBroadcasts.remove(d);
+        if (!didRemove) {
+            didRemove = mAlarmBroadcasts.remove(d);
+        }
+        return didRemove;
+    }
+
+    /**
+     * Find the deferrals record for the given uid in the given list
+     */
+    private static Deferrals findUidLocked(final int uid, ArrayList<Deferrals> list) {
+        final int numElements = list.size();
+        for (int i = 0; i < numElements; i++) {
+            Deferrals d = list.get(i);
+            if (uid == d.uid) {
+                return d;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Pop the next broadcast record from the head of the given deferrals list,
+     * if one exists.
+     */
+    private static BroadcastRecord popLocked(ArrayList<Deferrals> list) {
+        final Deferrals d = list.get(0);
+        return d.broadcasts.isEmpty() ? null : d.broadcasts.remove(0);
+    }
+
+    /**
+     * Insert the given Deferrals into the priority queue, sorted by defer-until milestone
+     */
+    private static void insertLocked(ArrayList<Deferrals> list, Deferrals d) {
+        // Simple linear search is appropriate here because we expect to
+        // have very few entries in the deferral lists (i.e. very few badly-
+        // behaving apps currently facing deferral)
+        int i;
+        final int numElements = list.size();
+        for (i = 0; i < numElements; i++) {
+            if (d.deferUntil < list.get(i).deferUntil) {
+                break;
+            }
+        }
+        list.add(i, d);
+    }
+
+    /**
+     * Calculate a new deferral time based on the previous time.  This should decay
+     * toward zero, though a small nonzero floor is an option.
+     */
+    private long calculateDeferral(long previous) {
+        return Math.max(mConstants.DEFERRAL_FLOOR,
+                (long) (previous * mConstants.DEFERRAL_DECAY_FACTOR));
+    }
+
+    // ----------------------------------
+
+    boolean dumpLocked(PrintWriter pw, String dumpPackage, String queueName,
+            SimpleDateFormat sdf) {
+        final Dumper dumper = new Dumper(pw, queueName, dumpPackage, sdf);
+        boolean printed = false;
+
+        dumper.setHeading("Active ordered broadcasts");
+        dumper.setLabel("Active Ordered Broadcast");
+        for (Deferrals d : mAlarmBroadcasts) {
+            d.dumpLocked(dumper);
+        }
+        printed |= dumper.didPrint();
+
+        for (BroadcastRecord br : mOrderedBroadcasts) {
+            dumper.dump(br);
+        }
+        printed |= dumper.didPrint();
+
+        dumper.setHeading("Deferred ordered broadcasts");
+        dumper.setLabel("Deferred Ordered Broadcast");
+        for (Deferrals d : mDeferredBroadcasts) {
+            d.dumpLocked(dumper);
+        }
+        printed |= dumper.didPrint();
+
+        return printed;
+    }
+}
index cdf6e0e..64a36ef 100644 (file)
@@ -24,6 +24,7 @@ import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.IIntentReceiver;
 import android.content.IIntentSender;
 import android.content.Intent;
@@ -45,6 +46,7 @@ import android.os.Trace;
 import android.os.UserHandle;
 import android.util.EventLog;
 import android.util.Slog;
+import android.util.SparseIntArray;
 import android.util.StatsLog;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
@@ -75,14 +77,15 @@ public final class BroadcastQueue {
     final ActivityManagerService mService;
 
     /**
-     * Recognizable moniker for this queue
+     * Behavioral parameters such as timeouts and deferral policy, tracking Settings
+     * for runtime configurability
      */
-    final String mQueueName;
+    final BroadcastConstants mConstants;
 
     /**
-     * Timeout period for this queue's broadcasts
+     * Recognizable moniker for this queue
      */
-    final long mTimeoutPeriod;
+    final String mQueueName;
 
     /**
      * If true, we can delay broadcasts while waiting services to finish in the previous
@@ -100,13 +103,18 @@ public final class BroadcastQueue {
     final ArrayList<BroadcastRecord> mParallelBroadcasts = new ArrayList<>();
 
     /**
-     * List of all active broadcasts that are to be executed one at a time.
-     * The object at the top of the list is the currently activity broadcasts;
-     * those after it are waiting for the top to finish.  As with parallel
-     * broadcasts, separate background- and foreground-priority queues are
-     * maintained.
+     * Tracking of the ordered broadcast queue, including deferral policy and alarm
+     * prioritization.
      */
-    final ArrayList<BroadcastRecord> mOrderedBroadcasts = new ArrayList<>();
+    final BroadcastDispatcher mDispatcher;
+
+    /**
+     * Refcounting for completion callbacks of split/deferred broadcasts.  The key
+     * is an opaque integer token assigned lazily when a broadcast is first split
+     * into multiple BroadcastRecord objects.
+     */
+    final SparseIntArray mSplitRefcounts = new SparseIntArray();
+    private int mNextToken = 0;
 
     /**
      * Historical data of past broadcasts, for debugging.  This is a ring buffer
@@ -173,7 +181,8 @@ public final class BroadcastQueue {
             switch (msg.what) {
                 case BROADCAST_INTENT_MSG: {
                     if (DEBUG_BROADCAST) Slog.v(
-                            TAG_BROADCAST, "Received BROADCAST_INTENT_MSG");
+                            TAG_BROADCAST, "Received BROADCAST_INTENT_MSG ["
+                            + mQueueName + "]");
                     processNextBroadcast(true);
                 } break;
                 case BROADCAST_TIMEOUT_MSG: {
@@ -201,12 +210,19 @@ public final class BroadcastQueue {
     }
 
     BroadcastQueue(ActivityManagerService service, Handler handler,
-            String name, long timeoutPeriod, boolean allowDelayBehindServices) {
+            String name, BroadcastConstants constants, boolean allowDelayBehindServices) {
         mService = service;
         mHandler = new BroadcastHandler(handler.getLooper());
         mQueueName = name;
-        mTimeoutPeriod = timeoutPeriod;
         mDelayBehindServices = allowDelayBehindServices;
+
+        mConstants = constants;
+        mDispatcher = new BroadcastDispatcher(this, mConstants, mHandler, mService);
+    }
+
+    void start(ContentResolver resolver) {
+        mDispatcher.start();
+        mConstants.startObserving(mHandler, resolver);
     }
 
     @Override
@@ -224,7 +240,7 @@ public final class BroadcastQueue {
     }
 
     public void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
-        mOrderedBroadcasts.add(r);
+        mDispatcher.enqueueOrderedBroadcastLocked(r);
         enqueueBroadcastHelper(r);
     }
 
@@ -255,7 +271,7 @@ public final class BroadcastQueue {
      * the old one.
      */
     public final BroadcastRecord replaceOrderedBroadcastLocked(BroadcastRecord r) {
-        return replaceBroadcastLocked(mOrderedBroadcasts, r, "ORDERED");
+        return mDispatcher.replaceBroadcastLocked(r, "ORDERED");
     }
 
     private BroadcastRecord replaceBroadcastLocked(ArrayList<BroadcastRecord> queue,
@@ -365,14 +381,17 @@ public final class BroadcastQueue {
         }
     }
 
+    // Skip the current receiver, if any, that is in flight to the given process
     public void skipCurrentReceiverLocked(ProcessRecord app) {
         BroadcastRecord r = null;
-        if (mOrderedBroadcasts.size() > 0) {
-            BroadcastRecord br = mOrderedBroadcasts.get(0);
-            if (br.curApp == app) {
-                r = br;
-            }
+        final BroadcastRecord curActive = mDispatcher.getActiveBroadcastLocked();
+        if (curActive != null && curActive.curApp == app) {
+            // confirmed: the current active broadcast is to the given app
+            r = curActive;
         }
+
+        // If the current active broadcast isn't this BUT we're waiting for
+        // mPendingBroadcast to spin up the target app, that's what we use.
         if (r == null && mPendingBroadcast != null && mPendingBroadcast.curApp == app) {
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
                     "[" + mQueueName + "] skip & discard pending app " + r);
@@ -404,20 +423,29 @@ public final class BroadcastQueue {
     }
 
     public BroadcastRecord getMatchingOrderedReceiver(IBinder receiver) {
-        if (mOrderedBroadcasts.size() > 0) {
-            final BroadcastRecord r = mOrderedBroadcasts.get(0);
-            if (r != null && r.receiver == receiver) {
-                return r;
-            }
+        BroadcastRecord br = mDispatcher.getActiveBroadcastLocked();
+        if (br != null && br.receiver == receiver) {
+            return br;
         }
         return null;
     }
 
+    // > 0 only, no worry about "eventual" recycling
+    private int nextSplitTokenLocked() {
+        int next = mNextToken + 1;
+        if (next <= 0) {
+            next = 1;
+        }
+        mNextToken = next;
+        return next;
+    }
+
     public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,
             String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
         final int state = r.state;
         final ActivityInfo receiver = r.curReceiver;
         final long finishTime = SystemClock.uptimeMillis();
+        final long elapsed = finishTime - r.receiverTime;
         r.state = BroadcastRecord.IDLE;
         if (state == BroadcastRecord.IDLE) {
             Slog.w(TAG, "finishReceiver [" + mQueueName + "] called but state is IDLE");
@@ -428,8 +456,22 @@ public final class BroadcastQueue {
         // If we're abandoning this broadcast before any receivers were actually spun up,
         // nextReceiver is zero; in which case time-to-process bookkeeping doesn't apply.
         if (r.nextReceiver > 0) {
-            r.duration[r.nextReceiver - 1] = finishTime - r.receiverTime;
+            r.duration[r.nextReceiver - 1] = elapsed;
+        }
+
+        // if this receiver was slow, impose deferral policy on the app.  This will kick in
+        // when processNextBroadcastLocked() next finds this uid as a receiver identity.
+        if (mConstants.SLOW_TIME > 0 && elapsed > mConstants.SLOW_TIME) {
+            if (DEBUG_BROADCAST_DEFERRAL) {
+                Slog.i(TAG, "Broadcast receiver was slow: " + receiver + " br=" + r);
+            }
+            if (r.curApp != null) {
+                mDispatcher.startDeferring(r.curApp.uid);
+            } else {
+                Slog.d(TAG, "finish receiver curApp is null? " + r);
+            }
         }
+
         r.receiver = null;
         r.intent.setComponent(null);
         if (r.curApp != null && r.curApp.curReceivers.contains(r)) {
@@ -452,9 +494,10 @@ public final class BroadcastQueue {
             r.resultAbort = false;
         }
 
+        // If we want to wait behind services *AND* we're finishing the head/
+        // active broadcast on its queue
         if (waitForServices && r.curComponent != null && r.queue.mDelayBehindServices
-                && r.queue.mOrderedBroadcasts.size() > 0
-                && r.queue.mOrderedBroadcasts.get(0) == r) {
+                && r.queue.mDispatcher.getActiveBroadcastLocked() == r) {
             ActivityInfo nextReceiver;
             if (r.nextReceiver < r.receivers.size()) {
                 Object obj = r.receivers.get(r.nextReceiver);
@@ -488,8 +531,8 @@ public final class BroadcastQueue {
     }
 
     public void backgroundServicesFinishedLocked(int userId) {
-        if (mOrderedBroadcasts.size() > 0) {
-            BroadcastRecord br = mOrderedBroadcasts.get(0);
+        BroadcastRecord br = mDispatcher.getActiveBroadcastLocked();
+        if (br != null) {
             if (br.userId == userId && br.state == BroadcastRecord.WAITING_SERVICES) {
                 Slog.i(TAG, "Resuming delayed broadcast");
                 br.curComponent = null;
@@ -861,7 +904,7 @@ public final class BroadcastQueue {
         if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "processNextBroadcast ["
                 + mQueueName + "]: "
                 + mParallelBroadcasts.size() + " parallel broadcasts, "
-                + mOrderedBroadcasts.size() + " ordered broadcasts");
+                + mDispatcher.totalUndelivered() + " ordered broadcasts");
 
         mService.updateCpuStats();
 
@@ -937,8 +980,12 @@ public final class BroadcastQueue {
         boolean looped = false;
 
         do {
-            if (mOrderedBroadcasts.size() == 0) {
-                // No more broadcasts pending, so all done!
+            final long now = SystemClock.uptimeMillis();
+            r = mDispatcher.getNextBroadcastLocked(now);
+
+            if (r == null) {
+                // No more broadcasts are deliverable right now, so all done!
+                mDispatcher.scheduleDeferralCheckLocked(false);
                 mService.scheduleAppGcsLocked();
                 if (looped) {
                     // If we had finished the last ordered broadcast, then
@@ -954,7 +1001,7 @@ public final class BroadcastQueue {
 
                 return;
             }
-            r = mOrderedBroadcasts.get(0);
+
             boolean forceReceive = false;
 
             // Ensure that even if something goes awry with the timeout
@@ -967,9 +1014,8 @@ public final class BroadcastQueue {
             // significant amounts of time.
             int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
             if (mService.mProcessesReady && r.dispatchTime > 0) {
-                long now = SystemClock.uptimeMillis();
                 if ((numReceivers > 0) &&
-                        (now > r.dispatchTime + (2*mTimeoutPeriod*numReceivers))) {
+                        (now > r.dispatchTime + (2 * mConstants.TIMEOUT * numReceivers))) {
                     Slog.w(TAG, "Hung broadcast ["
                             + mQueueName + "] discarded after timeout failure:"
                             + " now=" + now
@@ -993,27 +1039,53 @@ public final class BroadcastQueue {
                 return;
             }
 
+            // Is the current broadcast is done for any reason?
             if (r.receivers == null || r.nextReceiver >= numReceivers
                     || r.resultAbort || forceReceive) {
-                // No more receivers for this broadcast!  Send the final
-                // result if requested...
+                // Send the final result if requested
                 if (r.resultTo != null) {
-                    try {
-                        if (DEBUG_BROADCAST) Slog.i(TAG_BROADCAST,
-                                "Finishing broadcast [" + mQueueName + "] "
-                                + r.intent.getAction() + " app=" + r.callerApp);
-                        performReceiveLocked(r.callerApp, r.resultTo,
-                            new Intent(r.intent), r.resultCode,
-                            r.resultData, r.resultExtras, false, false, r.userId);
-                        // Set this to null so that the reference
-                        // (local and remote) isn't kept in the mBroadcastHistory.
-                        r.resultTo = null;
-                    } catch (RemoteException e) {
-                        r.resultTo = null;
-                        Slog.w(TAG, "Failure ["
-                                + mQueueName + "] sending broadcast result of "
-                                + r.intent, e);
-
+                    boolean sendResult = true;
+
+                    // if this was part of a split/deferral complex, update the refcount and only
+                    // send the completion when we clear all of them
+                    if (r.splitToken != 0) {
+                        int newCount = mSplitRefcounts.get(r.splitToken) - 1;
+                        if (newCount == 0) {
+                            // done!  clear out this record's bookkeeping and deliver
+                            if (DEBUG_BROADCAST_DEFERRAL) {
+                                Slog.i(TAG, "Sending broadcast completion for split token "
+                                        + r.splitToken);
+                            }
+                            mSplitRefcounts.delete(r.splitToken);
+                        } else {
+                            // still have some split broadcast records in flight; update refcount
+                            // and hold off on the callback
+                            if (DEBUG_BROADCAST_DEFERRAL) {
+                                Slog.i(TAG, "Result refcount " + newCount + " for split token "
+                                        + r.splitToken + " - not sending completion yet");
+                            }
+                            sendResult = false;
+                            mSplitRefcounts.put(r.splitToken, newCount);
+                        }
+                    }
+                    if (sendResult) {
+                        try {
+                            if (DEBUG_BROADCAST) {
+                                Slog.i(TAG_BROADCAST, "Finishing broadcast [" + mQueueName + "] "
+                                        + r.intent.getAction() + " app=" + r.callerApp);
+                            }
+                            performReceiveLocked(r.callerApp, r.resultTo,
+                                    new Intent(r.intent), r.resultCode,
+                                    r.resultData, r.resultExtras, false, false, r.userId);
+                            // Set this to null so that the reference
+                            // (local and remote) isn't kept in the mBroadcastHistory.
+                            r.resultTo = null;
+                        } catch (RemoteException e) {
+                            r.resultTo = null;
+                            Slog.w(TAG, "Failure ["
+                                    + mQueueName + "] sending broadcast result of "
+                                    + r.intent, e);
+                        }
                     }
                 }
 
@@ -1031,11 +1103,76 @@ public final class BroadcastQueue {
                     mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
                             r.manifestCount, r.manifestSkipCount, r.finishTime-r.dispatchTime);
                 }
-                mOrderedBroadcasts.remove(0);
+                mDispatcher.retireBroadcastLocked(r);
                 r = null;
                 looped = true;
                 continue;
             }
+
+            // Check whether the next receiver is under deferral policy, and handle that
+            // accordingly.  If the current broadcast was already part of deferred-delivery
+            // tracking, we know that it must now be deliverable as-is without re-deferral.
+            if (!r.deferred) {
+                final int receiverUid = r.getReceiverUid(r.receivers.get(r.nextReceiver));
+                if (mDispatcher.isDeferringLocked(receiverUid)) {
+                    if (DEBUG_BROADCAST_DEFERRAL) {
+                        Slog.i(TAG_BROADCAST, "Next receiver in " + r + " uid " + receiverUid
+                                + " at " + r.nextReceiver + " is under deferral");
+                    }
+                    // If this is the only (remaining) receiver in the broadcast, "splitting"
+                    // doesn't make sense -- just defer it as-is and retire it as the
+                    // currently active outgoing broadcast.
+                    BroadcastRecord defer;
+                    if (r.nextReceiver + 1 == numReceivers) {
+                        if (DEBUG_BROADCAST_DEFERRAL) {
+                            Slog.i(TAG, "Sole receiver of " + r
+                                    + " is under deferral; setting aside and proceeding");
+                        }
+                        defer = r;
+                        mDispatcher.retireBroadcastLocked(r);
+                    } else {
+                        // Nontrivial case; split out 'uid's receivers to a new broadcast record
+                        // and defer that, then loop and pick up continuing delivery of the current
+                        // record (now absent those receivers).
+
+                        // The split operation is guaranteed to match at least at 'nextReceiver'
+                        defer = r.splitRecipientsLocked(receiverUid, r.nextReceiver);
+                        if (DEBUG_BROADCAST_DEFERRAL) {
+                            Slog.i(TAG_BROADCAST, "Post split:");
+                            Slog.i(TAG_BROADCAST, "Original broadcast receivers:");
+                            for (int i = 0; i < r.receivers.size(); i++) {
+                                Slog.i(TAG_BROADCAST, "  " + r.receivers.get(i));
+                            }
+                            Slog.i(TAG_BROADCAST, "Split receivers:");
+                            for (int i = 0; i < defer.receivers.size(); i++) {
+                                Slog.i(TAG_BROADCAST, "  " + defer.receivers.get(i));
+                            }
+                        }
+                        // Track completion refcount as well if relevant
+                        if (r.resultTo != null) {
+                            int token = r.splitToken;
+                            if (token == 0) {
+                                // first split of this record; refcount for 'r' and 'deferred'
+                                r.splitToken = defer.splitToken = nextSplitTokenLocked();
+                                mSplitRefcounts.put(r.splitToken, 2);
+                            } else {
+                                // new split from an already-refcounted situation; increment count
+                                final int curCount = mSplitRefcounts.get(token);
+                                if (DEBUG_BROADCAST_DEFERRAL) {
+                                    if (curCount == 0) {
+                                        Slog.wtf(TAG, "Split refcount is zero with token for " + r);
+                                    }
+                                }
+                                mSplitRefcounts.put(token, curCount + 1);
+                            }
+                        }
+                    }
+                    mDispatcher.addDeferredBroadcast(receiverUid, defer);
+                    r = null;
+                    looped = true;
+                    continue;
+                }
+            }
         } while (r == null);
 
         // Get the next receiver...
@@ -1066,7 +1203,7 @@ public final class BroadcastQueue {
                     + mQueueName + "] " + r);
         }
         if (! mPendingBroadcastTimeoutMessage) {
-            long timeoutTime = r.receiverTime + mTimeoutPeriod;
+            long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
                     "Submitting BROADCAST_TIMEOUT_MSG ["
                     + mQueueName + "] for " + r + " at " + timeoutTime);
@@ -1474,12 +1611,12 @@ public final class BroadcastQueue {
             mPendingBroadcastTimeoutMessage = false;
         }
 
-        if (mOrderedBroadcasts.size() == 0) {
+        if (mDispatcher.isEmpty() || mDispatcher.getActiveBroadcastLocked() == null) {
             return;
         }
 
         long now = SystemClock.uptimeMillis();
-        BroadcastRecord r = mOrderedBroadcasts.get(0);
+        BroadcastRecord r = mDispatcher.getActiveBroadcastLocked();
         if (fromMsg) {
             if (!mService.mProcessesReady) {
                 // Only process broadcast timeouts if the system is ready. That way
@@ -1488,7 +1625,7 @@ public final class BroadcastQueue {
                 return;
             }
 
-            long timeoutTime = r.receiverTime + mTimeoutPeriod;
+            long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
             if (timeoutTime > now) {
                 // We can observe premature timeouts because we do not cancel and reset the
                 // broadcast timeout message after each receiver finishes.  Instead, we set up
@@ -1503,16 +1640,15 @@ public final class BroadcastQueue {
             }
         }
 
-        BroadcastRecord br = mOrderedBroadcasts.get(0);
-        if (br.state == BroadcastRecord.WAITING_SERVICES) {
+        if (r.state == BroadcastRecord.WAITING_SERVICES) {
             // In this case the broadcast had already finished, but we had decided to wait
             // for started services to finish as well before going on.  So if we have actually
             // waited long enough time timeout the broadcast, let's give up on the whole thing
             // and just move on to the next.
-            Slog.i(TAG, "Waited long enough for: " + (br.curComponent != null
-                    ? br.curComponent.flattenToShortString() : "(null)"));
-            br.curComponent = null;
-            br.state = BroadcastRecord.IDLE;
+            Slog.i(TAG, "Waited long enough for: " + (r.curComponent != null
+                    ? r.curComponent.flattenToShortString() : "(null)"));
+            r.curComponent = null;
+            r.state = BroadcastRecord.IDLE;
             processNextBroadcast(false);
             return;
         }
@@ -1619,13 +1755,8 @@ public final class BroadcastQueue {
             }
         }
 
-        for (int i = mOrderedBroadcasts.size() - 1; i >= 0; i--) {
-            didSomething |= mOrderedBroadcasts.get(i).cleanupDisabledPackageReceiversLocked(
-                    packageName, filterByClasses, userId, doit);
-            if (!doit && didSomething) {
-                return true;
-            }
-        }
+        didSomething |= mDispatcher.cleanupDisabledPackageReceiversLocked(packageName,
+                filterByClasses, userId, doit);
 
         return didSomething;
     }
@@ -1665,7 +1796,7 @@ public final class BroadcastQueue {
     }
 
     final boolean isIdle() {
-        return mParallelBroadcasts.isEmpty() && mOrderedBroadcasts.isEmpty()
+        return mParallelBroadcasts.isEmpty() && mDispatcher.isEmpty()
                 && (mPendingBroadcast == null);
     }
 
@@ -1677,10 +1808,7 @@ public final class BroadcastQueue {
         for (int i = N - 1; i >= 0; i--) {
             mParallelBroadcasts.get(i).writeToProto(proto, BroadcastQueueProto.PARALLEL_BROADCASTS);
         }
-        N = mOrderedBroadcasts.size();
-        for (int i = N - 1; i >= 0; i--) {
-            mOrderedBroadcasts.get(i).writeToProto(proto, BroadcastQueueProto.ORDERED_BROADCASTS);
-        }
+        mDispatcher.writeToProto(proto, BroadcastQueueProto.ORDERED_BROADCASTS);
         if (mPendingBroadcast != null) {
             mPendingBroadcast.writeToProto(proto, BroadcastQueueProto.PENDING_BROADCAST);
         }
@@ -1721,7 +1849,7 @@ public final class BroadcastQueue {
     final boolean dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args,
             int opti, boolean dumpAll, String dumpPackage, boolean needSep) {
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-        if (mParallelBroadcasts.size() > 0 || mOrderedBroadcasts.size() > 0
+        if (!mParallelBroadcasts.isEmpty() || !mDispatcher.isEmpty()
                 || mPendingBroadcast != null) {
             boolean printed = false;
             for (int i = mParallelBroadcasts.size() - 1; i >= 0; i--) {
@@ -1740,29 +1868,12 @@ public final class BroadcastQueue {
                 pw.println("  Active Broadcast " + mQueueName + " #" + i + ":");
                 br.dump(pw, "    ", sdf);
             }
-            printed = false;
-            needSep = true;
-            for (int i = mOrderedBroadcasts.size() - 1; i >= 0; i--) {
-                BroadcastRecord br = mOrderedBroadcasts.get(i);
-                if (dumpPackage != null && !dumpPackage.equals(br.callerPackage)) {
-                    continue;
-                }
-                if (!printed) {
-                    if (needSep) {
-                        pw.println();
-                    }
-                    needSep = true;
-                    printed = true;
-                    pw.println("  Active ordered broadcasts [" + mQueueName + "]:");
-                }
-                pw.println("  Active Ordered Broadcast " + mQueueName + " #" + i + ":");
-                mOrderedBroadcasts.get(i).dump(pw, "    ", sdf);
-            }
+
+            mDispatcher.dumpLocked(pw, dumpPackage, mQueueName, sdf);
+
             if (dumpPackage == null || (mPendingBroadcast != null
                     && dumpPackage.equals(mPendingBroadcast.callerPackage))) {
-                if (needSep) {
-                    pw.println();
-                }
+                pw.println();
                 pw.println("  Pending broadcast [" + mQueueName + "]:");
                 if (mPendingBroadcast != null) {
                     mPendingBroadcast.dump(pw, "    ", sdf);
@@ -1773,6 +1884,8 @@ public final class BroadcastQueue {
             }
         }
 
+        mConstants.dump(pw);
+
         int i;
         boolean printed = false;
 
index 9e799f6..d9e03f8 100644 (file)
@@ -36,10 +36,12 @@ import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * An active intent broadcast.
@@ -64,6 +66,9 @@ final class BroadcastRecord extends Binder {
     final int[] delivery;   // delivery state of each receiver
     final long[] duration;   // duration a receiver took to process broadcast
     IIntentReceiver resultTo; // who receives final result if non-null
+    boolean deferred;
+    int splitCount;         // refcount for result callback, when split
+    int splitToken;         // identifier for cross-BroadcastRecord refcount
     long enqueueClockTime;  // the clock time the broadcast was enqueued
     long dispatchTime;      // when dispatch started on this set of receivers
     long dispatchClockTime; // the clock time the dispatch started
@@ -106,6 +111,9 @@ final class BroadcastRecord extends Binder {
     ComponentName curComponent; // the receiver class that is currently running.
     ActivityInfo curReceiver;   // info about the receiver that is currently running.
 
+    // Private refcount-management bookkeeping; start > 0
+    static AtomicInteger sNextToken = new AtomicInteger(1);
+
     void dump(PrintWriter pw, String prefix, SimpleDateFormat sdf) {
         final long now = SystemClock.uptimeMillis();
 
@@ -304,6 +312,52 @@ final class BroadcastRecord extends Binder {
         allowBackgroundActivityStarts = from.allowBackgroundActivityStarts;
     }
 
+    /**
+     * Split off a new BroadcastRecord that clones this one, but contains only the
+     * recipient records for the current (just-finished) receiver's app, starting
+     * after the just-finished receiver [i.e. at r.nextReceiver].  Returns null
+     * if there are no matching subsequent receivers in this BroadcastRecord.
+     */
+    BroadcastRecord splitRecipientsLocked(int slowAppUid, int startingAt) {
+        // Do we actually have any matching receivers down the line...?
+        ArrayList splitReceivers = null;
+        for (int i = startingAt; i < receivers.size(); ) {
+            Object o = receivers.get(i);
+            if (getReceiverUid(o) == slowAppUid) {
+                if (splitReceivers == null) {
+                    splitReceivers = new ArrayList<>();
+                }
+                splitReceivers.add(o);
+                receivers.remove(i);
+                break;
+            } else {
+                i++;
+            }
+        }
+
+        // No later receivers in the same app, so we have no more to do
+        if (splitReceivers == null) {
+            return null;
+        }
+
+        // build a new BroadcastRecord around that single-target list
+        BroadcastRecord split = new BroadcastRecord(queue, intent, callerApp,
+                callerPackage, callingPid, callingUid, callerInstantApp, resolvedType,
+                requiredPermissions, appOp, options, splitReceivers, resultTo, resultCode,
+                resultData, resultExtras, ordered, sticky, initialSticky, userId,
+                allowBackgroundActivityStarts);
+
+        return split;
+    }
+
+    int getReceiverUid(Object receiver) {
+        if (receiver instanceof BroadcastFilter) {
+            return ((BroadcastFilter) receiver).owningUid;
+        } else /* if (receiver instanceof ResolveInfo) */ {
+            return ((ResolveInfo) receiver).activityInfo.applicationInfo.uid;
+        }
+    }
+
     public BroadcastRecord maybeStripForHistory() {
         if (!intent.canStripForHistory()) {
             return this;
index b381cbf..0b3bd70 100644 (file)
@@ -79,6 +79,7 @@
             android:singleUser="true" android:exported="true" />
         <receiver android:name="TrackTimeReceiver" />
         <receiver android:name="AlarmSpamReceiver" />
+        <receiver android:name="SlowReceiver" />
         <activity android:name="DisableScreenshotsActivity"
                 android:label="DisableScreenshots"
                 android:theme="@style/DisableScreenshots">
index 0f49608..2581e08 100644 (file)
 
 package com.google.android.test.activity;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.AlarmManager;
 import android.app.AlertDialog;
 import android.app.PendingIntent;
-import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProviderClient;
+import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
@@ -42,21 +42,18 @@ import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.graphics.Bitmap;
 import android.provider.Settings;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import android.widget.ScrollView;
-import android.widget.Toast;
+import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.content.Context;
-import android.content.pm.UserInfo;
-import android.content.res.Configuration;
-import android.util.Log;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
 
+import java.util.ArrayList;
+import java.util.List;
 
 public class ActivityTestMain extends Activity {
     static final String TAG = "ActivityTest";
@@ -73,8 +70,13 @@ public class ActivityTestMain extends Activity {
 
     ServiceConnection mIsolatedConnection;
 
+    static final String SLOW_RECEIVER_ACTION = "com.google.android.test.activity.SLOW_ACTION";
+    static final String SLOW_RECEIVER_EXTRA = "slow_ordinal";
+
     static final int MSG_SPAM = 1;
     static final int MSG_SPAM_ALARM = 2;
+    static final int MSG_SLOW_RECEIVER = 3;
+    static final int MSG_SLOW_ALARM_RECEIVER = 4;
 
     final Handler mHandler = new Handler() {
         @Override
@@ -100,11 +102,58 @@ public class ActivityTestMain extends Activity {
                     mAlarm.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME, when+(30*1000), pi);
                     scheduleSpamAlarm(30*1000);
                 } break;
+                case MSG_SLOW_RECEIVER: {
+                    // Several back to back, to illustrate dispatch policy
+                    Intent intent = new Intent(ActivityTestMain.this, SlowReceiver.class);
+                    intent.setAction(SLOW_RECEIVER_ACTION);
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 1);
+                    sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler,
+                            Activity.RESULT_OK, null, null);
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 2);
+                    sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler,
+                            Activity.RESULT_OK, null, null);
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 3);
+                    sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler,
+                            Activity.RESULT_OK, null, null);
+                } break;
+                case MSG_SLOW_ALARM_RECEIVER: {
+                    // Several back to back, to illustrate dispatch policy
+                    Intent intent = new Intent(ActivityTestMain.this, SlowReceiver.class);
+                    intent.setAction(SLOW_RECEIVER_ACTION);
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 1);
+                    sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler,
+                            Activity.RESULT_OK, null, null);
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 2);
+                    sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler,
+                            Activity.RESULT_OK, null, null);
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 3);
+                    sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler,
+                            Activity.RESULT_OK, null, null);
+
+                    // Also send a broadcast alarm to evaluate the alarm fast-forward policy
+                    intent.putExtra(SLOW_RECEIVER_EXTRA, 4);
+                    PendingIntent pi = PendingIntent.getBroadcast(ActivityTestMain.this, 1, intent, 0);
+                    AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+                    long now = SystemClock.elapsedRealtime();
+                    Log.i(TAG, "Setting alarm for now + 5 seconds");
+                    am.setExact(AlarmManager.ELAPSED_REALTIME, now + 5_000, pi);
+                } break;
             }
             super.handleMessage(msg);
         }
     };
 
+    final BroadcastReceiver mSlowReceiverCompletion = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final int extra = intent.getIntExtra(SLOW_RECEIVER_EXTRA, -1);
+            final String msg = "Slow receiver " + extra + " completed";
+            Toast.makeText(ActivityTestMain.this, msg, Toast.LENGTH_LONG)
+                    .show();
+            Log.i(TAG, msg);
+        }
+    };
+
     class BroadcastResultReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -387,6 +436,18 @@ public class ActivityTestMain extends Activity {
                 return true;
             }
         });
+        menu.add("Slow receiver").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+            @Override public boolean onMenuItemClick(MenuItem item) {
+                scheduleSlowReceiver();
+                return true;
+            }
+        });
+        menu.add("Slow alarm receiver").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+            @Override public boolean onMenuItemClick(MenuItem item) {
+                scheduleSlowAlarmReceiver();
+                return true;
+            }
+        });
         menu.add("Spam!").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
             @Override public boolean onMenuItemClick(MenuItem item) {
                 scheduleSpam(false);
@@ -469,6 +530,7 @@ public class ActivityTestMain extends Activity {
     protected void onStop() {
         super.onStop();
         mHandler.removeMessages(MSG_SPAM_ALARM);
+        mHandler.removeMessages(MSG_SLOW_RECEIVER);
         for (ServiceConnection conn : mConnections) {
             unbindService(conn);
         }
@@ -544,6 +606,16 @@ public class ActivityTestMain extends Activity {
         mHandler.sendMessageDelayed(msg, delay);
     }
 
+    void scheduleSlowReceiver() {
+        mHandler.removeMessages(MSG_SLOW_RECEIVER);
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SLOW_RECEIVER), 500);
+    }
+
+    void scheduleSlowAlarmReceiver() {
+        mHandler.removeMessages(MSG_SLOW_ALARM_RECEIVER);
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SLOW_ALARM_RECEIVER), 500);
+    }
+
     private View scrollWrap(View view) {
         ScrollView scroller = new ScrollView(this);
         scroller.addView(view, new ScrollView.LayoutParams(ScrollView.LayoutParams.MATCH_PARENT,
diff --git a/tests/ActivityTests/src/com/google/android/test/activity/SlowReceiver.java b/tests/ActivityTests/src/com/google/android/test/activity/SlowReceiver.java
new file mode 100644 (file)
index 0000000..0437a28
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 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.google.android.test.activity;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.util.Log;
+
+public class SlowReceiver extends BroadcastReceiver {
+    private static final String TAG = "SlowReceiver";
+    private static final long RECEIVER_DELAY = 6_000;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final int extra = intent.getIntExtra(ActivityTestMain.SLOW_RECEIVER_EXTRA, -1);
+        if (extra == 1) {
+            Log.i(TAG, "Received broadcast 1; delaying return by " + RECEIVER_DELAY + " ms");
+            long now = SystemClock.elapsedRealtime();
+            final long end = now + RECEIVER_DELAY;
+            while (now < end) {
+                try {
+                    Thread.sleep(end - now);
+                } catch (InterruptedException e) { }
+                now = SystemClock.elapsedRealtime();
+            }
+        } else {
+            Log.i(TAG, "Extra parameter not 1, returning immediately");
+        }
+        Log.i(TAG, "Returning from onReceive()");
+    }
+}