Merge "Update `OWNERS` files under `frameworks/base`." into main
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 75cfba0..121f348 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -1037,7 +1037,7 @@
 
             optional int32 uid = 1;
             repeated .android.app.ApplicationExitInfoProto app_exit_info = 2;
-            repeated .android.app.ApplicationExitInfoProto app_recoverable_crash = 3;
+            repeated .android.app.ApplicationExitInfoProto app_recoverable_crash = 3 [deprecated=true];
         }
         repeated User users = 2;
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 796dc77..30b1c6f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -15909,6 +15909,7 @@
             String[] excludedPackages, int appOp, Bundle bOptions,
             boolean serialized, boolean sticky, int userId) {
         enforceNotIsolatedCaller("broadcastIntent");
+
         synchronized(this) {
             intent = verifyBroadcastLocked(intent);
 
@@ -15922,6 +15923,12 @@
             // Permission regimes around sender-supplied broadcast options.
             enforceBroadcastOptionPermissionsInternal(bOptions, callingUid);
 
+            final ComponentName cn = intent.getComponent();
+
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                    "broadcastIntent:" + (cn != null ? cn.toString() : intent.getAction()));
+
             final long origId = Binder.clearCallingIdentity();
             try {
                 return broadcastIntentLocked(callerApp,
@@ -15932,6 +15939,7 @@
                         callingPid, userId, BackgroundStartPrivileges.NONE, null, null);
             } finally {
                 Binder.restoreCallingIdentity(origId);
+                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
             }
         }
     }
diff --git a/services/core/java/com/android/server/am/AppExitInfoTracker.java b/services/core/java/com/android/server/am/AppExitInfoTracker.java
index 666e560..47b65eb 100644
--- a/services/core/java/com/android/server/am/AppExitInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppExitInfoTracker.java
@@ -88,6 +88,7 @@
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.zip.GZIPOutputStream;
@@ -104,9 +105,9 @@
     private static final long APP_EXIT_INFO_PERSIST_INTERVAL = TimeUnit.MINUTES.toMillis(30);
 
     /** These are actions that the forEach* should take after each iteration */
-    private static final int FOREACH_ACTION_NONE = 0;
-    private static final int FOREACH_ACTION_REMOVE_ITEM = 1;
-    private static final int FOREACH_ACTION_STOP_ITERATION = 2;
+    @VisibleForTesting static final int FOREACH_ACTION_NONE = 0;
+    @VisibleForTesting static final int FOREACH_ACTION_REMOVE_ITEM = 1;
+    @VisibleForTesting static final int FOREACH_ACTION_STOP_ITERATION = 2;
 
     private static final int APP_EXIT_RAW_INFO_POOL_SIZE = 8;
 
@@ -125,7 +126,7 @@
 
     private static final String APP_TRACE_FILE_SUFFIX = ".gz";
 
-    private final Object mLock = new Object();
+    @VisibleForTesting final Object mLock = new Object();
 
     /**
      * Initialized in {@link #init} and read-only after that.
@@ -410,6 +411,23 @@
     }
 
     /**
+     * Certain types of crashes should not be updated. This could end up deleting valuable
+     * information, for example, if a test application crashes and then the `am instrument`
+     * finishes, then the crash whould be replaced with a `reason == USER_REQUESTED`
+     * ApplicationExitInfo from ActivityManager, and the original crash would be lost.
+     */
+    private boolean preventExitInfoUpdate(final ApplicationExitInfo exitInfo) {
+        switch (exitInfo.getReason()) {
+            case ApplicationExitInfo.REASON_ANR:
+            case ApplicationExitInfo.REASON_CRASH:
+            case ApplicationExitInfo.REASON_CRASH_NATIVE:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
      * Make note when ActivityManagerService decides to kill an application process.
      */
     @VisibleForTesting
@@ -418,10 +436,10 @@
         ApplicationExitInfo info = getExitInfoLocked(
                 raw.getPackageName(), raw.getPackageUid(), raw.getPid());
 
-        if (info == null) {
+        if (info == null || preventExitInfoUpdate(info)) {
             info = addExitInfoLocked(raw);
         } else {
-            // always override the existing info since we are now more informational.
+            // Override the existing info since we have more information.
             info.setReason(raw.getReason());
             info.setSubReason(raw.getSubReason());
             info.setStatus(0);
@@ -431,24 +449,8 @@
         scheduleLogToStatsdLocked(info, true);
     }
 
-    /**
-     * Make note when ActivityManagerService gets a recoverable native crash, as the process isn't
-     * being killed but the crash should still be added to AppExitInfo. Also, because we're not
-     * crashing, don't log out to statsd.
-     */
-    @VisibleForTesting
-    @GuardedBy("mLock")
-    void handleNoteAppRecoverableCrashLocked(final ApplicationExitInfo raw) {
-        addExitInfoLocked(raw, /* recoverable */ true);
-    }
-
     @GuardedBy("mLock")
     private ApplicationExitInfo addExitInfoLocked(ApplicationExitInfo raw) {
-        return addExitInfoLocked(raw, /* recoverable */ false);
-    }
-
-    @GuardedBy("mLock")
-    private ApplicationExitInfo addExitInfoLocked(ApplicationExitInfo raw, boolean recoverable) {
         if (!mAppExitInfoLoaded.get()) {
             Slog.w(TAG, "Skipping saving the exit info due to ongoing loading from storage");
             return null;
@@ -464,13 +466,13 @@
             }
         }
         for (int i = 0; i < packages.length; i++) {
-            addExitInfoInnerLocked(packages[i], uid, info, recoverable);
+            addExitInfoInnerLocked(packages[i], uid, info);
         }
 
         // SDK sandbox exits are stored under both real and package UID
         if (Process.isSdkSandboxUid(uid)) {
             for (int i = 0; i < packages.length; i++) {
-                addExitInfoInnerLocked(packages[i], raw.getPackageUid(), info, recoverable);
+                addExitInfoInnerLocked(packages[i], raw.getPackageUid(), info);
             }
         }
 
@@ -526,72 +528,77 @@
         if (k != null) {
             uid = k;
         }
-        ArrayList<ApplicationExitInfo> tlist = mTmpInfoList;
-        tlist.clear();
         final int targetUid = uid;
+        // Launder the modification bit through a `final` array, as Java doesn't allow you to mutate
+        // a captured boolean inside of a lambda.
+        final boolean[] isModified = {false};
         forEachPackageLocked((packageName, records) -> {
             AppExitInfoContainer container = records.get(targetUid);
             if (container == null) {
                 return FOREACH_ACTION_NONE;
             }
-            tlist.clear();
-            container.getExitInfoLocked(pid, 1, tlist);
-            if (tlist.size() == 0) {
+            mTmpInfoList.clear();
+            container.getExitInfosLocked(pid, /* maxNum */ 0, mTmpInfoList);
+            if (mTmpInfoList.size() == 0) {
                 return FOREACH_ACTION_NONE;
             }
-            ApplicationExitInfo info = tlist.get(0);
-            if (info.getRealUid() != targetUid) {
-                tlist.clear();
-                return FOREACH_ACTION_NONE;
-            }
-            // Okay found it, update its reason.
-            updateExistingExitInfoRecordLocked(info, status, reason);
 
-            return FOREACH_ACTION_STOP_ITERATION;
+            for (int i = 0, size = mTmpInfoList.size(); i < size; i++) {
+                ApplicationExitInfo info = mTmpInfoList.get(i);
+                if (info.getRealUid() != targetUid) {
+                    continue;
+                }
+                // We only update the most recent `ApplicationExitInfo` for this pid, which will
+                // always be the first one we se as `getExitInfosLocked()` returns them sorted
+                // by most-recent-first.
+                isModified[0] = true;
+                updateExistingExitInfoRecordLocked(info, status, reason);
+                return FOREACH_ACTION_STOP_ITERATION;
+            }
+            return FOREACH_ACTION_NONE;
         });
-        return tlist.size() > 0;
+        mTmpInfoList.clear();
+        return isModified[0];
     }
 
     /**
      * Get the exit info with matching package name, filterUid and filterPid (if > 0)
      */
     @VisibleForTesting
-    void getExitInfo(final String packageName, final int filterUid,
-            final int filterPid, final int maxNum, final ArrayList<ApplicationExitInfo> results) {
+    void getExitInfo(final String packageName, final int filterUid, final int filterPid,
+            final int maxNum, final List<ApplicationExitInfo> results) {
         final long identity = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                boolean emptyPackageName = TextUtils.isEmpty(packageName);
-                if (!emptyPackageName) {
-                    // fast path
+                if (!TextUtils.isEmpty(packageName)) {
+                    // Fast path - just a single package.
                     AppExitInfoContainer container = mData.get(packageName, filterUid);
                     if (container != null) {
-                        container.getExitInfoLocked(filterPid, maxNum, results);
+                        container.getExitInfosLocked(filterPid, maxNum, results);
                     }
-                } else {
-                    // slow path
-                    final ArrayList<ApplicationExitInfo> list = mTmpInfoList2;
-                    list.clear();
-                    // get all packages
-                    forEachPackageLocked((name, records) -> {
-                        AppExitInfoContainer container = records.get(filterUid);
-                        if (container != null) {
-                            mTmpInfoList.clear();
-                            list.addAll(container.toListLocked(mTmpInfoList, filterPid));
-                        }
-                        return AppExitInfoTracker.FOREACH_ACTION_NONE;
-                    });
+                    return;
+                }
 
-                    Collections.sort(list,
-                            (a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp()));
-                    int size = list.size();
-                    if (maxNum > 0) {
-                        size = Math.min(size, maxNum);
+                // Slow path - get all the packages.
+                forEachPackageLocked((name, records) -> {
+                    AppExitInfoContainer container = records.get(filterUid);
+                    if (container != null) {
+                        container.getExitInfosLocked(filterPid, /* maxNum */ 0, results);
                     }
-                    for (int i = 0; i < size; i++) {
-                        results.add(list.get(i));
-                    }
-                    list.clear();
+                    return AppExitInfoTracker.FOREACH_ACTION_NONE;
+                });
+
+                // And while the results for each package are sorted, we should
+                // sort over and trim the quantity of global results as well.
+                Collections.sort(
+                        results, (a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp()));
+                if (maxNum <= 0) {
+                    return;
+                }
+
+                int elementsToRemove = results.size() - maxNum;
+                for (int i = 0; i < elementsToRemove; i++) {
+                    results.removeLast();
                 }
             }
         } finally {
@@ -606,12 +613,10 @@
     @GuardedBy("mLock")
     private ApplicationExitInfo getExitInfoLocked(final String packageName,
             final int filterUid, final int filterPid) {
-        ArrayList<ApplicationExitInfo> list = mTmpInfoList;
-        list.clear();
-        getExitInfo(packageName, filterUid, filterPid, 1, list);
-
-        ApplicationExitInfo info = list.size() > 0 ? list.get(0) : null;
-        list.clear();
+        mTmpInfoList.clear();
+        getExitInfo(packageName, filterUid, filterPid, 1, mTmpInfoList);
+        ApplicationExitInfo info = mTmpInfoList.size() > 0 ? mTmpInfoList.getFirst() : null;
+        mTmpInfoList.clear();
         return info;
     }
 
@@ -878,8 +883,7 @@
     }
 
     @GuardedBy("mLock")
-    private void addExitInfoInnerLocked(String packageName, int uid, ApplicationExitInfo info,
-            boolean recoverable) {
+    private void addExitInfoInnerLocked(String packageName, int uid, ApplicationExitInfo info) {
         AppExitInfoContainer container = mData.get(packageName, uid);
         if (container == null) {
             container = new AppExitInfoContainer(mAppExitInfoHistoryListSize);
@@ -893,11 +897,7 @@
             }
             mData.put(packageName, uid, container);
         }
-        if (recoverable) {
-            container.addRecoverableCrashLocked(info);
-        } else {
-            container.addExitInfoLocked(info);
-        }
+        container.addExitInfoLocked(info);
     }
 
     @GuardedBy("mLock")
@@ -1205,7 +1205,7 @@
         forEachPackageLocked((name, records) -> {
             for (int i = records.size() - 1; i >= 0; i--) {
                 final AppExitInfoContainer container = records.valueAt(i);
-                container.forEachRecordLocked((pid, info) -> {
+                container.forEachRecordLocked((info) -> {
                     final File traceFile = info.getTraceFile();
                     if (traceFile != null) {
                         allFiles.remove(traceFile.getName());
@@ -1322,90 +1322,72 @@
      * A container class of {@link android.app.ApplicationExitInfo}
      */
     final class AppExitInfoContainer {
-        private SparseArray<ApplicationExitInfo> mInfos; // index is a pid
-        private SparseArray<ApplicationExitInfo> mRecoverableCrashes; // index is a pid
+        private ArrayList<ApplicationExitInfo> mExitInfos;
         private int mMaxCapacity;
         private int mUid; // Application uid, not isolated uid.
 
         AppExitInfoContainer(final int maxCapacity) {
-            mInfos = new SparseArray<ApplicationExitInfo>();
-            mRecoverableCrashes = new SparseArray<ApplicationExitInfo>();
+            mExitInfos = new ArrayList<ApplicationExitInfo>();
             mMaxCapacity = maxCapacity;
         }
 
+        @VisibleForTesting
         @GuardedBy("mLock")
-        void getInfosLocked(SparseArray<ApplicationExitInfo> map, final int filterPid,
-                final int maxNum, ArrayList<ApplicationExitInfo> results) {
-            if (filterPid > 0) {
-                ApplicationExitInfo r = map.get(filterPid);
-                if (r != null) {
-                    results.add(r);
+        void getExitInfosLocked(
+                final int filterPid, final int maxNum, List<ApplicationExitInfo> results) {
+            if (mExitInfos.size() == 0) {
+                return;
+            }
+
+            // Most of the callers might only be interested with the most recent
+            // ApplicationExitInfo, and so we can special case an O(n) walk.
+            if (maxNum == 1) {
+                ApplicationExitInfo result = null;
+                for (int i = 0, size = mExitInfos.size(); i < size; i++) {
+                    ApplicationExitInfo info = mExitInfos.get(i);
+                    if (filterPid > 0 && info.getPid() != filterPid) {
+                        continue;
+                    }
+
+                    if (result == null || result.getTimestamp() < info.getTimestamp()) {
+                        result = info;
+                    }
                 }
+                if (result != null) {
+                    results.add(result);
+                }
+                return;
+            }
+
+            mTmpInfoList2.clear();
+            if (filterPid <= 0) {
+                mTmpInfoList2.addAll(mExitInfos);
             } else {
-                final int numRep = map.size();
-                if (maxNum <= 0 || numRep <= maxNum) {
-                    // Return all records.
-                    for (int i = 0; i < numRep; i++) {
-                        results.add(map.valueAt(i));
-                    }
-                    Collections.sort(results,
-                            (a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp()));
-                } else {
-                    if (maxNum == 1) {
-                        // Most of the caller might be only interested with the most recent one
-                        ApplicationExitInfo r = map.valueAt(0);
-                        for (int i = 1; i < numRep; i++) {
-                            ApplicationExitInfo t = map.valueAt(i);
-                            if (r.getTimestamp() < t.getTimestamp()) {
-                                r = t;
-                            }
-                        }
-                        results.add(r);
-                    } else {
-                        // Huh, need to sort it out then.
-                        ArrayList<ApplicationExitInfo> list = mTmpInfoList2;
-                        list.clear();
-                        for (int i = 0; i < numRep; i++) {
-                            list.add(map.valueAt(i));
-                        }
-                        Collections.sort(list,
-                                (a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp()));
-                        for (int i = 0; i < maxNum; i++) {
-                            results.add(list.get(i));
-                        }
-                        list.clear();
+                for (int i = 0, size = mExitInfos.size(); i < size; i++) {
+                    ApplicationExitInfo info = mExitInfos.get(i);
+                    if (info.getPid() == filterPid) {
+                        mTmpInfoList2.add(info);
                     }
                 }
             }
-        }
 
-        @GuardedBy("mLock")
-        void getExitInfoLocked(final int filterPid, final int maxNum,
-                ArrayList<ApplicationExitInfo> results) {
-            getInfosLocked(mInfos, filterPid, maxNum, results);
-        }
-
-        @GuardedBy("mLock")
-        void addInfoLocked(SparseArray<ApplicationExitInfo> map, ApplicationExitInfo info) {
-            int size;
-            if ((size = map.size()) >= mMaxCapacity) {
-                int oldestIndex = -1;
-                long oldestTimeStamp = Long.MAX_VALUE;
-                for (int i = 0; i < size; i++) {
-                    ApplicationExitInfo r = map.valueAt(i);
-                    if (r.getTimestamp() < oldestTimeStamp) {
-                        oldestTimeStamp = r.getTimestamp();
-                        oldestIndex = i;
-                    }
-                }
-                if (oldestIndex >= 0) {
-                    final File traceFile = map.valueAt(oldestIndex).getTraceFile();
-                    if (traceFile != null) {
-                        traceFile.delete();
-                    }
-                    map.removeAt(oldestIndex);
-                }
+            Collections.sort(
+                    mTmpInfoList2, (a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp()));
+            if (maxNum <= 0) {
+                results.addAll(mTmpInfoList2);
+                return;
             }
+
+            int elementsToRemove = mTmpInfoList2.size() - maxNum;
+            for (int i = 0; i < elementsToRemove; i++) {
+                mTmpInfoList2.removeLast();
+            }
+            results.addAll(mTmpInfoList2);
+        }
+
+        @VisibleForTesting
+        @GuardedBy("mLock")
+        void addExitInfoLocked(ApplicationExitInfo info) {
             // Claim the state information if there is any
             int uid = info.getPackageUid();
             // SDK sandbox app states and app traces are stored under real UID
@@ -1420,24 +1402,39 @@
             if (info.getTraceFile() == null) {
                 info.setTraceFile(findAndRemoveFromSparse2dArray(mActiveAppTraces, uid, pid));
             }
-
             info.setAppTraceRetriever(mAppTraceRetriever);
-            map.append(pid, info);
+
+            mExitInfos.add(info);
+            if (mExitInfos.size() <= mMaxCapacity) {
+                return;
+            }
+
+            ApplicationExitInfo oldest = null;
+            for (int i = 0, size = mExitInfos.size(); i < size; i++) {
+                ApplicationExitInfo info2 = mExitInfos.get(i);
+                if (oldest == null || info2.getTimestamp() < oldest.getTimestamp()) {
+                    oldest = info2;
+                }
+            }
+            File traceFile = oldest.getTraceFile();
+            if (traceFile != null) {
+                traceFile.delete();
+            }
+            mExitInfos.remove(oldest);
         }
 
         @GuardedBy("mLock")
-        void addExitInfoLocked(ApplicationExitInfo info) {
-            addInfoLocked(mInfos, info);
-        }
-
-        @GuardedBy("mLock")
-        void addRecoverableCrashLocked(ApplicationExitInfo info) {
-            addInfoLocked(mRecoverableCrashes, info);
+        ApplicationExitInfo getLastExitInfoForPid(final int pid) {
+            mTmpInfoList.clear();
+            getExitInfosLocked(pid, /* maxNum */ 1, mTmpInfoList);
+            ApplicationExitInfo info = mTmpInfoList.size() == 0 ? null : mTmpInfoList.getFirst();
+            mTmpInfoList.clear();
+            return info;
         }
 
         @GuardedBy("mLock")
         boolean appendTraceIfNecessaryLocked(final int pid, final File traceFile) {
-            final ApplicationExitInfo r = mInfos.get(pid);
+            final ApplicationExitInfo r = getLastExitInfoForPid(pid);
             if (r != null) {
                 r.setTraceFile(traceFile);
                 r.setAppTraceRetriever(mAppTraceRetriever);
@@ -1447,49 +1444,36 @@
         }
 
         @GuardedBy("mLock")
-        void destroyLocked(SparseArray<ApplicationExitInfo> map) {
-            for (int i = map.size() - 1; i >= 0; i--) {
-                ApplicationExitInfo ai = map.valueAt(i);
-                final File traceFile = ai.getTraceFile();
+        void destroyLocked() {
+            for (int i = 0, size = mExitInfos.size(); i < size; i++) {
+                ApplicationExitInfo info = mExitInfos.get(i);
+                final File traceFile = info.getTraceFile();
                 if (traceFile != null) {
                     traceFile.delete();
                 }
-                ai.setTraceFile(null);
-                ai.setAppTraceRetriever(null);
+                info.setTraceFile(null);
+                info.setAppTraceRetriever(null);
             }
         }
 
+        /**
+         * Go through each record in an *unspecified* order, execute `callback()` on each element,
+         * and potentially do some action (stopping iteration, removing the element, etc.) based on
+         * the return value of the callback.
+         */
         @GuardedBy("mLock")
-        void destroyLocked() {
-            destroyLocked(mInfos);
-            destroyLocked(mRecoverableCrashes);
-        }
-
-        @GuardedBy("mLock")
-        void forEachRecordLocked(final BiFunction<Integer, ApplicationExitInfo, Integer> callback) {
+        void forEachRecordLocked(final Function<ApplicationExitInfo, Integer> callback) {
             if (callback == null) return;
-            for (int i = mInfos.size() - 1; i >= 0; i--) {
-                switch (callback.apply(mInfos.keyAt(i), mInfos.valueAt(i))) {
+            for (int i = mExitInfos.size() - 1; i >= 0; i--) {
+                ApplicationExitInfo info = mExitInfos.get(i);
+                switch (callback.apply(info)) {
                     case FOREACH_ACTION_STOP_ITERATION: return;
                     case FOREACH_ACTION_REMOVE_ITEM:
-                        final File traceFile = mInfos.valueAt(i).getTraceFile();
-                        if (traceFile != null) {
+                        File traceFile;
+                        if ((traceFile = info.getTraceFile()) != null) {
                             traceFile.delete();
                         }
-                        mInfos.removeAt(i);
-                        break;
-                }
-            }
-            for (int i = mRecoverableCrashes.size() - 1; i >= 0; i--) {
-                switch (callback.apply(
-                        mRecoverableCrashes.keyAt(i), mRecoverableCrashes.valueAt(i))) {
-                    case FOREACH_ACTION_STOP_ITERATION: return;
-                    case FOREACH_ACTION_REMOVE_ITEM:
-                        final File traceFile = mRecoverableCrashes.valueAt(i).getTraceFile();
-                        if (traceFile != null) {
-                            traceFile.delete();
-                        }
-                        mRecoverableCrashes.removeAt(i);
+                        mExitInfos.remove(info);
                         break;
                 }
             }
@@ -1497,30 +1481,20 @@
 
         @GuardedBy("mLock")
         void dumpLocked(PrintWriter pw, String prefix, SimpleDateFormat sdf) {
-            ArrayList<ApplicationExitInfo> list = new ArrayList<ApplicationExitInfo>();
-            for (int i = mInfos.size() - 1; i >= 0; i--) {
-                list.add(mInfos.valueAt(i));
+            mTmpInfoList.clear();
+            getExitInfosLocked(/* filterPid */ 0, /* maxNum */ 0, mTmpInfoList);
+            for (int i = 0, size = mTmpInfoList.size(); i < size; i++) {
+                mTmpInfoList.get(i).dump(pw, prefix + "  ", "#" + i, sdf);
             }
-            for (int i = mRecoverableCrashes.size() - 1; i >= 0; i--) {
-                list.add(mRecoverableCrashes.valueAt(i));
-            }
-            Collections.sort(list, (a, b) -> Long.compare(b.getTimestamp(), a.getTimestamp()));
-            int size = list.size();
-            for (int i = 0; i < size; i++) {
-                list.get(i).dump(pw, prefix + "  ", "#" + i, sdf);
-            }
+            mTmpInfoList.clear();
         }
 
         @GuardedBy("mLock")
         void writeToProto(ProtoOutputStream proto, long fieldId) {
             long token = proto.start(fieldId);
             proto.write(AppsExitInfoProto.Package.User.UID, mUid);
-            for (int i = 0; i < mInfos.size(); i++) {
-                mInfos.valueAt(i).writeToProto(proto, AppsExitInfoProto.Package.User.APP_EXIT_INFO);
-            }
-            for (int i = 0; i < mRecoverableCrashes.size(); i++) {
-                mRecoverableCrashes.valueAt(i).writeToProto(
-                        proto, AppsExitInfoProto.Package.User.APP_RECOVERABLE_CRASH);
+            for (int i = 0, size = mExitInfos.size(); i < size; i++) {
+                mExitInfos.get(i).writeToProto(proto, AppsExitInfoProto.Package.User.APP_EXIT_INFO);
             }
             proto.end(token);
         }
@@ -1539,14 +1513,7 @@
                     case (int) AppsExitInfoProto.Package.User.APP_EXIT_INFO: {
                         ApplicationExitInfo info = new ApplicationExitInfo();
                         info.readFromProto(proto, AppsExitInfoProto.Package.User.APP_EXIT_INFO);
-                        mInfos.put(info.getPid(), info);
-                        break;
-                    }
-                    case (int) AppsExitInfoProto.Package.User.APP_RECOVERABLE_CRASH: {
-                        ApplicationExitInfo info = new ApplicationExitInfo();
-                        info.readFromProto(
-                                proto, AppsExitInfoProto.Package.User.APP_RECOVERABLE_CRASH);
-                        mRecoverableCrashes.put(info.getPid(), info);
+                        mExitInfos.add(info);
                         break;
                     }
                 }
@@ -1554,24 +1521,6 @@
             proto.end(token);
             return mUid;
         }
-
-        @GuardedBy("mLock")
-        List<ApplicationExitInfo> toListLocked(List<ApplicationExitInfo> list, int filterPid) {
-            if (list == null) {
-                list = new ArrayList<ApplicationExitInfo>();
-            }
-            for (int i = mInfos.size() - 1; i >= 0; i--) {
-                if (filterPid == 0 || filterPid == mInfos.keyAt(i)) {
-                    list.add(mInfos.valueAt(i));
-                }
-            }
-            for (int i = mRecoverableCrashes.size() - 1; i >= 0; i--) {
-                if (filterPid == 0 || filterPid == mRecoverableCrashes.keyAt(i)) {
-                    list.add(mRecoverableCrashes.valueAt(i));
-                }
-            }
-            return list;
-        }
     }
 
     /**
@@ -1750,7 +1699,11 @@
                 case MSG_APP_RECOVERABLE_CRASH: {
                     ApplicationExitInfo raw = (ApplicationExitInfo) msg.obj;
                     synchronized (mLock) {
-                        handleNoteAppRecoverableCrashLocked(raw);
+                        // Unlike MSG_APP_KILL, this is a recoverable crash, and
+                        // so we want to bypass the statsd app-kill logging.
+                        // Hence, call `addExitInfoLocked()` directly instead of
+                        // `handleNoteAppKillLocked()`.
+                        addExitInfoLocked(raw);
                     }
                     recycleRawRecord(raw);
                 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java
index e15942b..adcbf5c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java
@@ -84,7 +84,11 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Random;
+import java.util.function.Function;
 import java.util.zip.GZIPInputStream;
 
 /**
@@ -940,6 +944,228 @@
         }
     }
 
+    private ApplicationExitInfo createExitInfo(int i) {
+        ApplicationExitInfo info = new ApplicationExitInfo();
+        info.setPid(i);
+        info.setTimestamp(1000 + i);
+        info.setPackageUid(2000);
+        return info;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private ArrayList<ApplicationExitInfo> getExitInfosHelper(
+            AppExitInfoTracker.AppExitInfoContainer container, int filterPid, int maxNum) {
+        ArrayList<ApplicationExitInfo> infos = new ArrayList<ApplicationExitInfo>();
+        container.getExitInfosLocked(filterPid, maxNum, infos);
+        return infos;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private void checkAreHelper(AppExitInfoTracker.AppExitInfoContainer container, int filterPid,
+            int maxNum, List<Integer> expected, Function<ApplicationExitInfo, Integer> func) {
+        ArrayList<Integer> values = new ArrayList<Integer>();
+        getExitInfosHelper(container, filterPid, maxNum)
+                .forEach((exitInfo) -> values.add(func.apply(exitInfo)));
+        assertEquals(values, expected);
+
+        HashMap<Integer, Integer> expectedMultiset = new HashMap<Integer, Integer>();
+        expected.forEach(
+                (elem) -> expectedMultiset.put(elem, expectedMultiset.getOrDefault(elem, 0) + 1));
+        // `maxNum` isn't a parameter supported by `forEachRecordLocked()s`, but we can emulate it
+        // by stopping iteration when we've seen enough elements.
+        int[] numElementsToObserveWrapped = {maxNum};
+        container.forEachRecordLocked((exitInfo) -> {
+            // Same thing as above, `filterPid` isn't a parameter supported out of the box for
+            // `forEachRecordLocked()`, but we emulate it here.
+            if (filterPid > 0 && filterPid != exitInfo.getPid()) {
+                return AppExitInfoTracker.FOREACH_ACTION_NONE;
+            }
+
+            Integer key = func.apply(exitInfo);
+            assertTrue(expectedMultiset.toString(), expectedMultiset.containsKey(key));
+            Integer references = expectedMultiset.get(key);
+            if (references == 1) {
+                expectedMultiset.remove(key);
+            } else {
+                expectedMultiset.put(key, references - 1);
+            }
+            if (--numElementsToObserveWrapped[0] == 0) {
+                return AppExitInfoTracker.FOREACH_ACTION_STOP_ITERATION;
+            }
+            return AppExitInfoTracker.FOREACH_ACTION_NONE;
+        });
+        assertEquals(expectedMultiset.size(), 0);
+    }
+
+    private void checkPidsAre(AppExitInfoTracker.AppExitInfoContainer container, int filterPid,
+            int maxNum, List<Integer> expectedPids) {
+        checkAreHelper(container, filterPid, maxNum, expectedPids, (exitInfo) -> exitInfo.getPid());
+    }
+
+    private void checkPidsAre(
+            AppExitInfoTracker.AppExitInfoContainer container, List<Integer> expectedPids) {
+        checkPidsAre(container, 0, 0, expectedPids);
+    }
+
+    private void checkTimestampsAre(AppExitInfoTracker.AppExitInfoContainer container,
+            int filterPid, int maxNum, List<Integer> expectedTimestamps) {
+        checkAreHelper(container, filterPid, maxNum, expectedTimestamps,
+                (exitInfo) -> (int) exitInfo.getTimestamp());
+    }
+
+    private void checkTimestampsAre(
+            AppExitInfoTracker.AppExitInfoContainer container, List<Integer> expectedTimestamps) {
+        checkTimestampsAre(container, 0, 0, expectedTimestamps);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private AppExitInfoTracker.AppExitInfoContainer createBasicContainer() {
+        AppExitInfoTracker.AppExitInfoContainer container =
+                mAppExitInfoTracker.new AppExitInfoContainer(3);
+        container.addExitInfoLocked(createExitInfo(10));
+        container.addExitInfoLocked(createExitInfo(30));
+        container.addExitInfoLocked(createExitInfo(20));
+        return container;
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerGetExitInfosIsSortedNewestFirst() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        checkPidsAre(container, Arrays.asList(30, 20, 10));
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerRemovesOldestReports() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        container.addExitInfoLocked(createExitInfo(40));
+        checkPidsAre(container, Arrays.asList(40, 30, 20));
+
+        container.addExitInfoLocked(createExitInfo(50));
+        checkPidsAre(container, Arrays.asList(50, 40, 30));
+
+        container.addExitInfoLocked(createExitInfo(45));
+        checkPidsAre(container, Arrays.asList(50, 45, 40));
+
+        // Adding an older report shouldn't remove the newer ones.
+        container.addExitInfoLocked(createExitInfo(15));
+        checkPidsAre(container, Arrays.asList(50, 45, 40));
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerFilterByPid() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        assertEquals(1, getExitInfosHelper(container, 30, 0).size());
+        assertEquals(30, getExitInfosHelper(container, 0, 0).get(0).getPid());
+
+        assertEquals(1, getExitInfosHelper(container, 30, 0).size());
+        assertEquals(20, getExitInfosHelper(container, 20, 0).get(0).getPid());
+
+        assertEquals(1, getExitInfosHelper(container, 10, 0).size());
+        assertEquals(10, getExitInfosHelper(container, 10, 0).get(0).getPid());
+
+        assertEquals(0, getExitInfosHelper(container, 1337, 0).size());
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerLimitQuantityOfResults() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        checkPidsAre(container, /* filterPid */ 30, /* maxNum */ 1, Arrays.asList(30));
+        checkPidsAre(container, /* filterPid */ 30, /* maxNum */ 1000, Arrays.asList(30));
+
+        checkPidsAre(container, /* filterPid */ 20, /* maxNum */ 1, Arrays.asList(20));
+        checkPidsAre(container, /* filterPid */ 20, /* maxNum */ 1000, Arrays.asList(20));
+
+        checkPidsAre(container, /* filterPid */ 10, /* maxNum */ 1, Arrays.asList(10));
+        checkPidsAre(container, /* filterPid */ 10, /* maxNum */ 1000, Arrays.asList(10));
+
+        checkPidsAre(container, /* filterPid */ 1337, /* maxNum */ 1, Arrays.asList());
+        checkPidsAre(container, /* filterPid */ 1337, /* maxNum */ 1000, Arrays.asList());
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerLastExitInfoForPid() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        assertEquals(30, container.getLastExitInfoForPid(30).getPid());
+        assertEquals(20, container.getLastExitInfoForPid(20).getPid());
+        assertEquals(10, container.getLastExitInfoForPid(10).getPid());
+        assertEquals(null, container.getLastExitInfoForPid(1337));
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerCanHoldMultipleFromSamePid() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        ApplicationExitInfo info = createExitInfo(100);
+        ApplicationExitInfo info2 = createExitInfo(100);
+        ApplicationExitInfo info3 = createExitInfo(100);
+        info2.setTimestamp(1337);
+        info3.setTimestamp(31337);
+
+        container.addExitInfoLocked(info);
+        assertEquals(1100, container.getLastExitInfoForPid(100).getTimestamp());
+        container.addExitInfoLocked(info2);
+        assertEquals(1337, container.getLastExitInfoForPid(100).getTimestamp());
+        container.addExitInfoLocked(info3);
+        assertEquals(31337, container.getLastExitInfoForPid(100).getTimestamp());
+
+        checkPidsAre(container, Arrays.asList(100, 100, 100));
+        checkTimestampsAre(container, Arrays.asList(31337, 1337, 1100));
+
+        checkPidsAre(container, /* filterPid */ 100, /* maxNum */ 0, Arrays.asList(100, 100, 100));
+        checkTimestampsAre(
+                container, /* filterPid */ 100, /* maxNum */ 0, Arrays.asList(31337, 1337, 1100));
+
+        checkPidsAre(container, /* filterPid */ 100, /* maxNum */ 2, Arrays.asList(100, 100));
+        checkTimestampsAre(
+                container, /* filterPid */ 100, /* maxNum */ 2, Arrays.asList(31337, 1337));
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerIteration() throws Exception {
+        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+        checkPidsAre(container, Arrays.asList(30, 20, 10));
+
+        // Unfortunately relying on order for this test, which is implemented as "last inserted" ->
+        // "first inserted". Note that this is insertion order, not timestamp. Thus, it's 20 -> 30
+        // -> 10, as defined by `createBasicContainer()`.
+        List<Integer> elements = Arrays.asList(20, 30, 10);
+        for (int i = 0, size = elements.size(); i < size; i++) {
+            ArrayList<Integer> processedEntries = new ArrayList<Integer>();
+            final int finalIndex = i;
+            container.forEachRecordLocked((exitInfo) -> {
+                processedEntries.add(new Integer(exitInfo.getPid()));
+                if (exitInfo.getPid() == elements.get(finalIndex)) {
+                    return AppExitInfoTracker.FOREACH_ACTION_STOP_ITERATION;
+                }
+                return AppExitInfoTracker.FOREACH_ACTION_NONE;
+            });
+            assertEquals(processedEntries, elements.subList(0, i + 1));
+        }
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testContainerIterationRemove() throws Exception {
+        for (int pidToRemove : Arrays.asList(30, 20, 10)) {
+            AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
+            container.forEachRecordLocked((exitInfo) -> {
+                if (exitInfo.getPid() == pidToRemove) {
+                    return AppExitInfoTracker.FOREACH_ACTION_REMOVE_ITEM;
+                }
+                return AppExitInfoTracker.FOREACH_ACTION_NONE;
+            });
+            ArrayList<Integer> pidsRemaining = new ArrayList<Integer>(Arrays.asList(30, 20, 10));
+            pidsRemaining.remove(new Integer(pidToRemove));
+            checkPidsAre(container, pidsRemaining);
+        }
+    }
+
     private static int makeExitStatus(int exitCode) {
         return (exitCode << 8) & 0xff00;
     }