Merge "Track the child processes that are forked by app processes"
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index efea953..d7393ca 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -32,6 +32,7 @@
 import libcore.io.IoUtils;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 import java.util.Map;
 import java.util.concurrent.TimeoutException;
 
@@ -1317,33 +1318,16 @@
      */
     public static void waitForProcessDeath(int pid, int timeout)
             throws InterruptedException, TimeoutException {
-        FileDescriptor pidfd = null;
-        if (sPidFdSupported == PIDFD_UNKNOWN) {
-            int fd = -1;
+        boolean fallback = supportsPidFd();
+        if (!fallback) {
+            FileDescriptor pidfd = null;
             try {
-                fd = nativePidFdOpen(pid, 0);
-                sPidFdSupported = PIDFD_SUPPORTED;
-            } catch (ErrnoException e) {
-                sPidFdSupported = e.errno != OsConstants.ENOSYS
-                    ? PIDFD_SUPPORTED : PIDFD_UNSUPPORTED;
-            } finally {
+                final int fd = nativePidFdOpen(pid, 0);
                 if (fd >= 0) {
                     pidfd = new FileDescriptor();
                     pidfd.setInt$(fd);
-                }
-            }
-        }
-        boolean fallback = sPidFdSupported == PIDFD_UNSUPPORTED;
-        if (!fallback) {
-            try {
-                if (pidfd == null) {
-                    int fd = nativePidFdOpen(pid, 0);
-                    if (fd >= 0) {
-                        pidfd = new FileDescriptor();
-                        pidfd.setInt$(fd);
-                    } else {
-                        fallback = true;
-                    }
+                } else {
+                    fallback = true;
                 }
                 if (pidfd != null) {
                     StructPollfd[] fds = new StructPollfd[] {
@@ -1392,5 +1376,59 @@
         throw new TimeoutException();
     }
 
+    /**
+     * Determine whether the system supports pidfd APIs
+     *
+     * @return Returns true if the system supports pidfd APIs
+     * @hide
+     */
+    public static boolean supportsPidFd() {
+        if (sPidFdSupported == PIDFD_UNKNOWN) {
+            int fd = -1;
+            try {
+                fd = nativePidFdOpen(myPid(), 0);
+                sPidFdSupported = PIDFD_SUPPORTED;
+            } catch (ErrnoException e) {
+                sPidFdSupported = e.errno != OsConstants.ENOSYS
+                        ? PIDFD_SUPPORTED : PIDFD_UNSUPPORTED;
+            } finally {
+                if (fd >= 0) {
+                    final FileDescriptor f = new FileDescriptor();
+                    f.setInt$(fd);
+                    IoUtils.closeQuietly(f);
+                }
+            }
+        }
+        return sPidFdSupported == PIDFD_SUPPORTED;
+    }
+
+    /**
+     * Open process file descriptor for given pid.
+     *
+     * @param pid The process ID to open for
+     * @param flags Reserved, unused now, must be 0
+     * @return The process file descriptor for given pid
+     * @throws IOException if it can't be opened
+     *
+     * @hide
+     */
+    public static @Nullable FileDescriptor openPidFd(int pid, int flags) throws IOException {
+        if (!supportsPidFd()) {
+            return null;
+        }
+        if (flags != 0) {
+            throw new IllegalArgumentException();
+        }
+        try {
+            FileDescriptor pidfd = new FileDescriptor();
+            pidfd.setInt$(nativePidFdOpen(pid, flags));
+            return pidfd;
+        } catch (ErrnoException e) {
+            IOException ex = new IOException();
+            ex.initCause(e);
+            throw ex;
+        }
+    }
+
     private static native int nativePidFdOpen(int pid, int flags) throws ErrnoException;
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index fede1d2..48055b5 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -26,6 +26,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
+import android.os.Message;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
@@ -124,6 +125,7 @@
     private static final long DEFAULT_TOP_TO_FGS_GRACE_DURATION = 15 * 1000;
     private static final int DEFAULT_PENDINGINTENT_WARNING_THRESHOLD = 2000;
     private static final int DEFAULT_MIN_CRASH_INTERVAL = 2 * 60 * 1000;
+    private static final int DEFAULT_MAX_PHANTOM_PROCESSES = 32;
 
 
     // Flag stored in the DeviceConfig API.
@@ -133,6 +135,11 @@
     private static final String KEY_MAX_CACHED_PROCESSES = "max_cached_processes";
 
     /**
+     * Maximum number of cached processes.
+     */
+    private static final String KEY_MAX_PHANTOM_PROCESSES = "max_phantom_processes";
+
+    /**
      * Default value for mFlagBackgroundActivityStartsEnabled if not explicitly set in
      * Settings.Global. This allows it to be set experimentally unless it has been
      * enabled/disabled in developer options. Defaults to false.
@@ -364,6 +371,11 @@
      */
     public final ArraySet<ComponentName> KEEP_WARMING_SERVICES = new ArraySet<ComponentName>();
 
+    /**
+     * Maximum number of phantom processes.
+     */
+    public int MAX_PHANTOM_PROCESSES = DEFAULT_MAX_PHANTOM_PROCESSES;
+
     private List<String> mDefaultImperceptibleKillExemptPackages;
     private List<Integer> mDefaultImperceptibleKillExemptProcStates;
 
@@ -481,6 +493,9 @@
                             case KEY_BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD:
                                 updateBinderHeavyHitterWatcher();
                                 break;
+                            case KEY_MAX_PHANTOM_PROCESSES:
+                                updateMaxPhantomProcesses();
+                                break;
                             default:
                                 break;
                         }
@@ -599,6 +614,8 @@
                 // with defaults.
                 Slog.e("ActivityManagerConstants", "Bad activity manager config settings", e);
             }
+            final long currentPowerCheckInterval = POWER_CHECK_INTERVAL;
+
             BACKGROUND_SETTLE_TIME = mParser.getLong(KEY_BACKGROUND_SETTLE_TIME,
                     DEFAULT_BACKGROUND_SETTLE_TIME);
             FGSERVICE_MIN_SHOWN_TIME = mParser.getLong(KEY_FGSERVICE_MIN_SHOWN_TIME,
@@ -664,6 +681,13 @@
             PENDINGINTENT_WARNING_THRESHOLD = mParser.getInt(KEY_PENDINGINTENT_WARNING_THRESHOLD,
                     DEFAULT_PENDINGINTENT_WARNING_THRESHOLD);
 
+            if (POWER_CHECK_INTERVAL != currentPowerCheckInterval) {
+                mService.mHandler.removeMessages(
+                        ActivityManagerService.CHECK_EXCESSIVE_POWER_USE_MSG);
+                final Message msg = mService.mHandler.obtainMessage(
+                        ActivityManagerService.CHECK_EXCESSIVE_POWER_USE_MSG);
+                mService.mHandler.sendMessageDelayed(msg, POWER_CHECK_INTERVAL);
+            }
             // For new flags that are intended for server-side experiments, please use the new
             // DeviceConfig package.
         }
@@ -811,6 +835,16 @@
         mService.scheduleUpdateBinderHeavyHitterWatcherConfig();
     }
 
+    private void updateMaxPhantomProcesses() {
+        final int oldVal = MAX_PHANTOM_PROCESSES;
+        MAX_PHANTOM_PROCESSES = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_MAX_PHANTOM_PROCESSES,
+                DEFAULT_MAX_PHANTOM_PROCESSES);
+        if (oldVal > MAX_PHANTOM_PROCESSES) {
+            mService.mHandler.post(mService.mPhantomProcessList::trimPhantomProcessesIfNecessary);
+        }
+    }
+
     void dump(PrintWriter pw) {
         pw.println("ACTIVITY MANAGER SETTINGS (dumpsys activity settings) "
                 + Settings.Global.ACTIVITY_MANAGER_CONSTANTS + ":");
@@ -897,6 +931,8 @@
         pw.println(BINDER_HEAVY_HITTER_AUTO_SAMPLER_BATCHSIZE);
         pw.print("  "); pw.print(KEY_BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD); pw.print("=");
         pw.println(BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD);
+        pw.print("  "); pw.print(KEY_MAX_PHANTOM_PROCESSES); pw.print("=");
+        pw.println(MAX_PHANTOM_PROCESSES);
 
         pw.println();
         if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b55d555..b1b4018 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -635,6 +635,12 @@
     final ProcessList mProcessList;
 
     /**
+     * The list of phantom processes.
+     * @see PhantomProcessRecord
+     */
+    final PhantomProcessList mPhantomProcessList;
+
+    /**
      * Tracking long-term execution of processes to look for abuse and other
      * bad app behavior.
      */
@@ -1996,6 +2002,7 @@
         mProcessList = injector.getProcessList(this);
         mProcessList.init(this, activeUids, mPlatformCompat);
         mAppProfiler = new AppProfiler(this, BackgroundThread.getHandler().getLooper(), null);
+        mPhantomProcessList = new PhantomProcessList(this);
         mOomAdjuster = hasHandlerThread
                 ? new OomAdjuster(this, mProcessList, activeUids, handlerThread) : null;
 
@@ -2053,6 +2060,7 @@
         mProcessList.init(this, activeUids, mPlatformCompat);
         mAppProfiler = new AppProfiler(this, BackgroundThread.getHandler().getLooper(),
                 new LowMemDetector(this));
+        mPhantomProcessList = new PhantomProcessList(this);
         mOomAdjuster = new OomAdjuster(this, mProcessList, activeUids);
 
         // Broadcast policy parameters
@@ -9209,6 +9217,10 @@
             }
         }
 
+        if (dumpAll) {
+            mPhantomProcessList.dump(pw, "  ");
+        }
+
         if (mImportantProcesses.size() > 0) {
             synchronized (mPidsSelfLocked) {
                 boolean printed = false;
@@ -14832,44 +14844,24 @@
             int i = mProcessList.mLruProcesses.size();
             while (i > 0) {
                 i--;
-                ProcessRecord app = mProcessList.mLruProcesses.get(i);
+                final ProcessRecord app = mProcessList.mLruProcesses.get(i);
                 if (app.setProcState >= ActivityManager.PROCESS_STATE_HOME) {
-                    if (app.lastCpuTime <= 0) {
-                        continue;
+                    int cpuLimit;
+                    long checkDur = curUptime - app.getWhenUnimportant();
+                    if (checkDur <= mConstants.POWER_CHECK_INTERVAL) {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_1;
+                    } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 2)
+                            || app.setProcState <= ActivityManager.PROCESS_STATE_HOME) {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_2;
+                    } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 3)) {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_3;
+                    } else {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_4;
                     }
-                    long cputimeUsed = app.curCpuTime - app.lastCpuTime;
-                    if (DEBUG_POWER) {
-                        StringBuilder sb = new StringBuilder(128);
-                        sb.append("CPU for ");
-                        app.toShortString(sb);
-                        sb.append(": over ");
-                        TimeUtils.formatDuration(uptimeSince, sb);
-                        sb.append(" used ");
-                        TimeUtils.formatDuration(cputimeUsed, sb);
-                        sb.append(" (");
-                        sb.append((cputimeUsed * 100) / uptimeSince);
-                        sb.append("%)");
-                        Slog.i(TAG_POWER, sb.toString());
-                    }
-                    // If the process has used too much CPU over the last duration, the
-                    // user probably doesn't want this, so kill!
-                    if (doCpuKills && uptimeSince > 0) {
-                        // What is the limit for this process?
-                        int cpuLimit;
-                        long checkDur = curUptime - app.getWhenUnimportant();
-                        if (checkDur <= mConstants.POWER_CHECK_INTERVAL) {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_1;
-                        } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 2)
-                                || app.setProcState <= ActivityManager.PROCESS_STATE_HOME) {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_2;
-                        } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 3)) {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_3;
-                        } else {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_4;
-                        }
-                        if (((cputimeUsed * 100) / uptimeSince) >= cpuLimit) {
-                            mBatteryStatsService.reportExcessiveCpu(app.info.uid, app.processName,
-                                        uptimeSince, cputimeUsed);
+                    if (app.lastCpuTime > 0) {
+                        final long cputimeUsed = app.curCpuTime - app.lastCpuTime;
+                        if (checkExcessivePowerUsageLocked(uptimeSince, doCpuKills, cputimeUsed,
+                                app.processName, app.toShortString(), cpuLimit, app)) {
                             app.kill("excessive cpu " + cputimeUsed + " during " + uptimeSince
                                     + " dur=" + checkDur + " limit=" + cpuLimit,
                                     ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE,
@@ -14878,23 +14870,73 @@
                             synchronized (mProcessStats.mLock) {
                                 app.baseProcessTracker.reportExcessiveCpu(app.pkgList.mPkgList);
                             }
-                            for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) {
-                                ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg);
-                                FrameworkStatsLog.write(
-                                        FrameworkStatsLog.EXCESSIVE_CPU_USAGE_REPORTED,
-                                        app.info.uid,
-                                        holder.state.getName(),
-                                        holder.state.getPackage(),
-                                        holder.appVersion);
-                            }
                         }
                     }
+
                     app.lastCpuTime = app.curCpuTime;
+
+                    // Also check the phantom processes if there is any
+                    final long chkDur = checkDur;
+                    final int cpuLmt = cpuLimit;
+                    final boolean doKill = doCpuKills;
+                    mPhantomProcessList.forEachPhantomProcessOfApp(app, r -> {
+                        if (r.mLastCputime > 0) {
+                            final long cputimeUsed = r.mCurrentCputime - r.mLastCputime;
+                            if (checkExcessivePowerUsageLocked(uptimeSince, doKill, cputimeUsed,
+                                    app.processName, r.toString(), cpuLimit, app)) {
+                                mPhantomProcessList.killPhantomProcessGroupLocked(app, r,
+                                        ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE,
+                                        ApplicationExitInfo.SUBREASON_EXCESSIVE_CPU,
+                                        "excessive cpu " + cputimeUsed + " during "
+                                        + uptimeSince + " dur=" + chkDur + " limit=" + cpuLmt);
+                                return false;
+                            }
+                        }
+                        r.mLastCputime = r.mCurrentCputime;
+                        return true;
+                    });
                 }
             }
         }
     }
 
+    private boolean checkExcessivePowerUsageLocked(final long uptimeSince, boolean doCpuKills,
+            final long cputimeUsed, final String processName, final String description,
+            final int cpuLimit, final ProcessRecord app) {
+        if (DEBUG_POWER) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("CPU for ");
+            sb.append(description);
+            sb.append(": over ");
+            TimeUtils.formatDuration(uptimeSince, sb);
+            sb.append(" used ");
+            TimeUtils.formatDuration(cputimeUsed, sb);
+            sb.append(" (");
+            sb.append((cputimeUsed * 100.0) / uptimeSince);
+            sb.append("%)");
+            Slog.i(TAG_POWER, sb.toString());
+        }
+        // If the process has used too much CPU over the last duration, the
+        // user probably doesn't want this, so kill!
+        if (doCpuKills && uptimeSince > 0) {
+            if (((cputimeUsed * 100) / uptimeSince) >= cpuLimit) {
+                mBatteryStatsService.reportExcessiveCpu(app.info.uid, app.processName,
+                        uptimeSince, cputimeUsed);
+                for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) {
+                    ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg);
+                    FrameworkStatsLog.write(
+                            FrameworkStatsLog.EXCESSIVE_CPU_USAGE_REPORTED,
+                            app.info.uid,
+                            processName,
+                            holder.state.getPackage(),
+                            holder.appVersion);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
     final void setProcessTrackerStateLocked(ProcessRecord proc, int memFactor, long now) {
         synchronized (mProcessStats.mLock) {
             if (proc.thread != null && proc.baseProcessTracker != null) {
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 0b5d585..31ffb35 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -1257,6 +1257,10 @@
                 }
             }
 
+            if (haveNewCpuStats) {
+                mService.mPhantomProcessList.updateProcessCpuStatesLocked(mProcessCpuTracker);
+            }
+
             final BatteryStatsImpl bstats = mService.mBatteryStatsService.getActiveStatistics();
             synchronized (bstats) {
                 if (haveNewCpuStats) {
diff --git a/services/core/java/com/android/server/am/PhantomProcessList.java b/services/core/java/com/android/server/am/PhantomProcessList.java
new file mode 100644
index 0000000..e2fcf08
--- /dev/null
+++ b/services/core/java/com/android/server/am/PhantomProcessList.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2020 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 android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
+import android.os.Handler;
+import android.os.Process;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.ProcessCpuTracker;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.function.Function;
+
+/**
+ * Activity manager code dealing with phantom processes.
+ */
+public final class PhantomProcessList {
+    static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessList" : TAG_AM;
+
+    final Object mLock = new Object();
+
+    /**
+     * All of the phantom process record we track, key is the pid of the process.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<PhantomProcessRecord> mPhantomProcesses = new SparseArray<>();
+
+    /**
+     * The mapping between app processes and their phantom processess, outer key is the pid of
+     * the app process, while the inner key is the pid of the phantom process.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<SparseArray<PhantomProcessRecord>> mAppPhantomProcessMap =
+            new SparseArray<>();
+
+    /**
+     * The mapping of the pidfd to PhantomProcessRecord.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<PhantomProcessRecord> mPhantomProcessesPidFds = new SparseArray<>();
+
+    /**
+     * The list of phantom processes tha's being signaled to be killed but still undead yet.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<PhantomProcessRecord> mZombiePhantomProcesses = new SparseArray<>();
+
+    @GuardedBy("mLock")
+    private final ArrayList<PhantomProcessRecord> mTempPhantomProcesses = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private boolean mTrimPhantomProcessScheduled = false;
+
+    @GuardedBy("mLock")
+    int mUpdateSeq;
+
+    private final ActivityManagerService mService;
+    private final Handler mKillHandler;
+
+    PhantomProcessList(final ActivityManagerService service) {
+        mService = service;
+        mKillHandler = service.mProcessList.sKillHandler;
+    }
+
+    /**
+     * Get the existing phantom process record, or create if it's not existing yet;
+     * however, before creating it, we'll check if this is really a phantom process
+     * and we'll return null if it's not.
+     */
+    @GuardedBy("mLock")
+    PhantomProcessRecord getOrCreatePhantomProcessIfNeededLocked(final String processName,
+            final int uid, final int pid) {
+        // First check if it's actually an app process we know
+        if (isAppProcess(pid)) {
+            return null;
+        }
+
+        // Have we already been aware of this?
+        final int index = mPhantomProcesses.indexOfKey(pid);
+        if (index >= 0) {
+            final PhantomProcessRecord proc = mPhantomProcesses.valueAt(index);
+            if (proc.equals(processName, uid, pid)) {
+                return proc;
+            }
+            // Somehow our record doesn't match, remove it anyway
+            Slog.w(TAG, "Stale " + proc + ", removing");
+            mPhantomProcesses.removeAt(index);
+        } else {
+            // Is this one of the zombie processes we've known?
+            final int idx = mZombiePhantomProcesses.indexOfKey(pid);
+            if (idx >= 0) {
+                final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(idx);
+                if (proc.equals(processName, uid, pid)) {
+                    return proc;
+                }
+                // Our zombie process information is outdated, let's remove this one, it shoud
+                // have been gone.
+                mZombiePhantomProcesses.removeAt(idx);
+            }
+        }
+
+        int ppid = getParentPid(pid);
+
+        // Walk through its parents and see if it could be traced back to an app process.
+        while (ppid > 1) {
+            if (isAppProcess(ppid)) {
+                // It's a phantom process, bookkeep it
+                try {
+                    final PhantomProcessRecord proc = new PhantomProcessRecord(
+                            processName, uid, pid, ppid, mService,
+                            this::onPhantomProcessKilledLocked);
+                    proc.mUpdateSeq = mUpdateSeq;
+                    mPhantomProcesses.put(pid, proc);
+                    SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(ppid);
+                    if (array == null) {
+                        array = new SparseArray<>();
+                        mAppPhantomProcessMap.put(ppid, array);
+                    }
+                    array.put(pid, proc);
+                    if (proc.mPidFd != null) {
+                        mKillHandler.getLooper().getQueue().addOnFileDescriptorEventListener(
+                                proc.mPidFd, EVENT_INPUT | EVENT_ERROR,
+                                this::onPhantomProcessFdEvent);
+                        mPhantomProcessesPidFds.put(proc.mPidFd.getInt$(), proc);
+                    }
+                    scheduleTrimPhantomProcessesLocked();
+                    return proc;
+                } catch (IllegalStateException e) {
+                    return null;
+                }
+            }
+
+            ppid = getParentPid(ppid);
+        }
+        return null;
+    }
+
+    private static int getParentPid(int pid) {
+        try {
+            return Process.getParentPid(pid);
+        } catch (Exception e) {
+        }
+        return -1;
+    }
+
+    private boolean isAppProcess(int pid) {
+        synchronized (mService.mPidsSelfLocked) {
+            return mService.mPidsSelfLocked.get(pid) != null;
+        }
+    }
+
+    private int onPhantomProcessFdEvent(FileDescriptor fd, int events) {
+        synchronized (mLock) {
+            final PhantomProcessRecord proc = mPhantomProcessesPidFds.get(fd.getInt$());
+            if ((events & EVENT_INPUT) != 0) {
+                proc.onProcDied(true);
+            } else {
+                // EVENT_ERROR, kill the process
+                proc.killLocked("Process error", true);
+            }
+        }
+        return 0;
+    }
+
+    @GuardedBy("mLock")
+    private void onPhantomProcessKilledLocked(final PhantomProcessRecord proc) {
+        if (proc.mPidFd != null && proc.mPidFd.valid()) {
+            mKillHandler.getLooper().getQueue()
+                    .removeOnFileDescriptorEventListener(proc.mPidFd);
+            mPhantomProcessesPidFds.remove(proc.mPidFd.getInt$());
+            IoUtils.closeQuietly(proc.mPidFd);
+        }
+        mPhantomProcesses.remove(proc.mPid);
+        final int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
+        if (index < 0) {
+            return;
+        }
+        SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.valueAt(index);
+        array.remove(proc.mPid);
+        if (array.size() == 0) {
+            mAppPhantomProcessMap.removeAt(index);
+        }
+        if (proc.mZombie) {
+            // If it's not really dead, bookkeep it
+            mZombiePhantomProcesses.put(proc.mPid, proc);
+        } else {
+            // In case of race condition, let's try to remove it from zombie list
+            mZombiePhantomProcesses.remove(proc.mPid);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void scheduleTrimPhantomProcessesLocked() {
+        if (!mTrimPhantomProcessScheduled) {
+            mTrimPhantomProcessScheduled = true;
+            mService.mHandler.post(this::trimPhantomProcessesIfNecessary);
+        }
+    }
+
+    /**
+     * Clamp the number of phantom processes to
+     * {@link ActivityManagerConstants#MAX_PHANTOM_PROCESSE}, kills those surpluses in the
+     * order of the oom adjs of their parent process.
+     */
+    void trimPhantomProcessesIfNecessary() {
+        synchronized (mLock) {
+            mTrimPhantomProcessScheduled = false;
+            if (mService.mConstants.MAX_PHANTOM_PROCESSES < mPhantomProcesses.size()) {
+                for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
+                    mTempPhantomProcesses.add(mPhantomProcesses.valueAt(i));
+                }
+                synchronized (mService.mPidsSelfLocked) {
+                    Collections.sort(mTempPhantomProcesses, (a, b) -> {
+                        final ProcessRecord ra = mService.mPidsSelfLocked.get(a.mPpid);
+                        if (ra == null) {
+                            // parent is gone, this process should have been killed too
+                            return 1;
+                        }
+                        final ProcessRecord rb = mService.mPidsSelfLocked.get(b.mPpid);
+                        if (rb == null) {
+                            // parent is gone, this process should have been killed too
+                            return -1;
+                        }
+                        if (ra.curAdj != rb.curAdj) {
+                            return ra.curAdj - rb.curAdj;
+                        }
+                        if (a.mKnownSince != b.mKnownSince) {
+                            // In case of identical oom adj, younger one first
+                            return a.mKnownSince < b.mKnownSince ? 1 : -1;
+                        }
+                        return 0;
+                    });
+                }
+                for (int i = mTempPhantomProcesses.size() - 1;
+                        i >= mService.mConstants.MAX_PHANTOM_PROCESSES; i--) {
+                    final PhantomProcessRecord proc = mTempPhantomProcesses.get(i);
+                    proc.killLocked("Trimming phantom processes", true);
+                }
+                mTempPhantomProcesses.clear();
+            }
+        }
+    }
+
+    /**
+     * Remove all entries with outdated seq num.
+     */
+    @GuardedBy("mLock")
+    void pruneStaleProcessesLocked() {
+        for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
+            final PhantomProcessRecord proc = mPhantomProcesses.valueAt(i);
+            if (proc.mUpdateSeq < mUpdateSeq) {
+                if (DEBUG_PROCESSES) {
+                    Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
+                }
+                proc.killLocked("Stale process", true);
+            }
+        }
+        for (int i = mZombiePhantomProcesses.size() - 1; i >= 0; i--) {
+            final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(i);
+            if (proc.mUpdateSeq < mUpdateSeq) {
+                if (DEBUG_PROCESSES) {
+                    Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
+                }
+            }
+        }
+    }
+
+    /**
+     * Kill the given phantom process, all its siblings (if any) and their parent process
+     */
+    @GuardedBy("mService")
+    void killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc,
+            @Reason int reasonCode, @SubReason int subReason, String msg) {
+        synchronized (mLock) {
+            int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
+            if (index >= 0) {
+                final SparseArray<PhantomProcessRecord> array =
+                        mAppPhantomProcessMap.valueAt(index);
+                for (int i = array.size() - 1; i >= 0; i--) {
+                    final PhantomProcessRecord r = array.valueAt(i);
+                    if (r == proc) {
+                        r.killLocked(msg, true);
+                    } else {
+                        r.killLocked("Caused by siling process: " + msg, false);
+                    }
+                }
+            }
+        }
+        // Lastly, kill the parent process too
+        app.kill("Caused by child process: " + msg, reasonCode, subReason, true);
+    }
+
+    /**
+     * Iterate all phantom process belonging to the given app, and invokve callback
+     * for each of them.
+     */
+    void forEachPhantomProcessOfApp(final ProcessRecord app,
+            final Function<PhantomProcessRecord, Boolean> callback) {
+        synchronized (mLock) {
+            int index = mAppPhantomProcessMap.indexOfKey(app.pid);
+            if (index >= 0) {
+                final SparseArray<PhantomProcessRecord> array =
+                        mAppPhantomProcessMap.valueAt(index);
+                for (int i = array.size() - 1; i >= 0; i--) {
+                    final PhantomProcessRecord r = array.valueAt(i);
+                    if (!callback.apply(r)) {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    @GuardedBy("tracker")
+    void updateProcessCpuStatesLocked(ProcessCpuTracker tracker) {
+        synchronized (mLock) {
+            // refresh the phantom process list with the latest cpu stats results.
+            mUpdateSeq++;
+            for (int i = tracker.countStats() - 1; i >= 0; i--) {
+                final ProcessCpuTracker.Stats st = tracker.getStats(i);
+                final PhantomProcessRecord r =
+                        getOrCreatePhantomProcessIfNeededLocked(st.name, st.uid, st.pid);
+                if (r != null) {
+                    r.mUpdateSeq = mUpdateSeq;
+                    r.mCurrentCputime += st.rel_utime + st.rel_stime;
+                    if (r.mLastCputime == 0) {
+                        r.mLastCputime = r.mCurrentCputime;
+                    }
+                    r.updateAdjLocked();
+                }
+            }
+            // remove the stale ones
+            pruneStaleProcessesLocked();
+        }
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        synchronized (mLock) {
+            dumpPhantomeProcessLocked(pw, prefix, "All Active App Child Processes:",
+                    mPhantomProcesses);
+            dumpPhantomeProcessLocked(pw, prefix, "All Zombie App Child Processes:",
+                    mZombiePhantomProcesses);
+        }
+    }
+
+    void dumpPhantomeProcessLocked(PrintWriter pw, String prefix, String headline,
+            SparseArray<PhantomProcessRecord> list) {
+        final int size = list.size();
+        if (size == 0) {
+            return;
+        }
+        pw.println();
+        pw.print(prefix);
+        pw.println(headline);
+        for (int i = 0; i < size; i++) {
+            final PhantomProcessRecord proc = list.valueAt(i);
+            pw.print(prefix);
+            pw.print("  proc #");
+            pw.print(i);
+            pw.print(": ");
+            pw.println(proc.toString());
+            proc.dump(pw, prefix + "    ");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/PhantomProcessRecord.java b/services/core/java/com/android/server/am/PhantomProcessRecord.java
new file mode 100644
index 0000000..0156ee5
--- /dev/null
+++ b/services/core/java/com/android/server/am/PhantomProcessRecord.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2020 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 android.os.Process.PROC_NEWLINE_TERM;
+import static android.os.Process.PROC_OUT_LONG;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.os.Handler;
+import android.os.Process;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * The "phantom" app processes, which are forked by app processes so we are not aware of
+ * them until we walk through the process list in /proc.
+ */
+public final class PhantomProcessRecord {
+    static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessRecord" : TAG_AM;
+
+    static final long[] LONG_OUT = new long[1];
+    static final int[] LONG_FORMAT = new int[] {PROC_NEWLINE_TERM | PROC_OUT_LONG};
+
+    final String mProcessName;   // name of the process
+    final int mUid;              // uid of the process
+    final int mPid;              // The id of the process
+    final int mPpid;             // Ancestor (managed app process) pid of the process
+    final long mKnownSince;      // The timestamp when we're aware of the process
+    final FileDescriptor mPidFd; // The fd to monitor the termination of this process
+
+    long mLastCputime;           // How long proc has run CPU at last check
+    long mCurrentCputime;        // How long proc has run CPU most recently
+    int mUpdateSeq;              // Seq no, indicating the last check on this process
+    int mAdj;                    // The last known oom adj score
+    boolean mKilled;             // Whether it has been killed by us or not
+    boolean mZombie;             // Whether it was signaled to be killed but timed out
+    String mStringName;          // Caching of the toString() result
+
+    final ActivityManagerService mService;
+    final Object mLock;
+    final Consumer<PhantomProcessRecord> mOnKillListener;
+    final Handler mKillHandler;
+
+    PhantomProcessRecord(final String processName, final int uid, final int pid,
+            final int ppid, final ActivityManagerService service,
+            final Consumer<PhantomProcessRecord> onKillListener) throws IllegalStateException {
+        mProcessName = processName;
+        mUid = uid;
+        mPid = pid;
+        mPpid = ppid;
+        mKilled = false;
+        mAdj = ProcessList.NATIVE_ADJ;
+        mKnownSince = SystemClock.elapsedRealtime();
+        mService = service;
+        mLock = service.mPhantomProcessList.mLock;
+        mOnKillListener = onKillListener;
+        mKillHandler = service.mProcessList.sKillHandler;
+        if (Process.supportsPidFd()) {
+            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+            try {
+                mPidFd = Process.openPidFd(pid, 0);
+                if (mPidFd == null) {
+                    throw new IllegalStateException();
+                }
+            } catch (IOException e) {
+                // Maybe a race condition, the process is gone.
+                Slog.w(TAG, "Unable to open process " + pid + ", it might be gone");
+                IllegalStateException ex = new IllegalStateException();
+                ex.initCause(e);
+                throw ex;
+            } finally {
+                StrictMode.setThreadPolicy(oldPolicy);
+            }
+        } else {
+            mPidFd = null;
+        }
+    }
+
+    @GuardedBy("mLock")
+    void killLocked(String reason, boolean noisy) {
+        if (!mKilled) {
+            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");
+            if (noisy || mUid == mService.mCurOomAdjUid) {
+                mService.reportUidInfoMessageLocked(TAG,
+                        "Killing " + toString() + ": " + reason, mUid);
+            }
+            if (mPid > 0) {
+                EventLog.writeEvent(EventLogTags.AM_KILL, UserHandle.getUserId(mUid),
+                        mPid, mProcessName, mAdj, reason);
+                if (!Process.supportsPidFd()) {
+                    onProcDied(false);
+                } else {
+                    // We'll notify the listener when we're notified it's dead.
+                    // Meanwhile, we'd also need handle the case of zombie processes.
+                    mKillHandler.postDelayed(mProcKillTimer, this,
+                            ProcessList.PROC_KILL_TIMEOUT);
+                }
+                Process.killProcessQuiet(mPid);
+                ProcessList.killProcessGroup(mUid, mPid);
+            }
+            mKilled = true;
+            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+        }
+    }
+
+    private Runnable mProcKillTimer = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                // The process is maybe in either D or Z state.
+                Slog.w(TAG, "Process " + toString() + " is still alive after "
+                        + ProcessList.PROC_KILL_TIMEOUT + "ms");
+                // Force a cleanup as we can't keep the fd open forever
+                mZombie = true;
+                onProcDied(false);
+                // But still bookkeep it, so it won't be added as a new one if it's spotted again.
+            }
+        }
+    };
+
+    @GuardedBy("mLock")
+    void updateAdjLocked() {
+        if (Process.readProcFile("/proc/" + mPid + "/oom_score_adj",
+                LONG_FORMAT, null, LONG_OUT, null)) {
+            mAdj = (int) LONG_OUT[0];
+        }
+    }
+
+    @GuardedBy("mLock")
+    void onProcDied(boolean reallyDead) {
+        if (reallyDead) {
+            Slog.i(TAG, "Process " + toString() + " died");
+        }
+        mKillHandler.removeCallbacks(mProcKillTimer, this);
+        if (mOnKillListener != null) {
+            mOnKillListener.accept(this);
+        }
+    }
+
+    @Override
+    public String toString() {
+        if (mStringName != null) {
+            return mStringName;
+        }
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("PhantomProcessRecord {");
+        sb.append(Integer.toHexString(System.identityHashCode(this)));
+        sb.append(' ');
+        sb.append(mPid);
+        sb.append(':');
+        sb.append(mPpid);
+        sb.append(':');
+        sb.append(mProcessName);
+        sb.append('/');
+        if (mUid < Process.FIRST_APPLICATION_UID) {
+            sb.append(mUid);
+        } else {
+            sb.append('u');
+            sb.append(UserHandle.getUserId(mUid));
+            int appId = UserHandle.getAppId(mUid);
+            if (appId >= Process.FIRST_APPLICATION_UID) {
+                sb.append('a');
+                sb.append(appId - Process.FIRST_APPLICATION_UID);
+            } else {
+                sb.append('s');
+                sb.append(appId);
+            }
+            if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) {
+                sb.append('i');
+                sb.append(appId - Process.FIRST_ISOLATED_UID);
+            }
+        }
+        sb.append('}');
+        return mStringName = sb.toString();
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        final long now = SystemClock.elapsedRealtime();
+        pw.print(prefix);
+        pw.print("user #");
+        pw.print(UserHandle.getUserId(mUid));
+        pw.print(" uid=");
+        pw.print(mUid);
+        pw.print(" pid=");
+        pw.print(mPid);
+        pw.print(" ppid=");
+        pw.print(mPpid);
+        pw.print(" knownSince=");
+        TimeUtils.formatDuration(mKnownSince, now, pw);
+        pw.print(" killed=");
+        pw.println(mKilled);
+        pw.print(prefix);
+        pw.print("lastCpuTime=");
+        pw.print(mLastCputime);
+        if (mLastCputime > 0) {
+            pw.print(" timeUsed=");
+            TimeUtils.formatDuration(mCurrentCputime - mLastCputime, pw);
+        }
+        pw.print(" oom adj=");
+        pw.print(mAdj);
+        pw.print(" seq=");
+        pw.println(mUpdateSeq);
+    }
+
+    boolean equals(final String processName, final int uid, final int pid) {
+        return mUid == uid && mPid == pid && TextUtils.equals(mProcessName, processName);
+    }
+}
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index ced2f0f..5e65563 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -329,7 +329,7 @@
     /**
      * How long between a process kill and we actually receive its death recipient
      */
-    private static final int PROC_KILL_TIMEOUT = 2000; // 2 seconds;
+    static final int PROC_KILL_TIMEOUT = 2000; // 2 seconds;
 
     /**
      * Native heap allocations will now have a non-zero tag in the most significant byte.
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java
new file mode 100644
index 0000000..04e8b63
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2020 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.am.ActivityManagerService.Injector;
+import com.android.server.appop.AppOpsService;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
+
+import java.io.File;
+
+@Presubmit
+public class AppChildProcessTest {
+    private static final String TAG = AppChildProcessTest.class.getSimpleName();
+
+    @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
+    @Mock private AppOpsService mAppOpsService;
+    @Mock private PackageManagerInternal mPackageManagerInt;
+    private StaticMockitoSession mMockitoSession;
+
+    private Context mContext = getInstrumentation().getTargetContext();
+    private TestInjector mInjector;
+    private ActivityManagerService mAms;
+    private ProcessList mProcessList;
+    private PhantomProcessList mPhantomProcessList;
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+
+    @BeforeClass
+    public static void setUpOnce() {
+        System.setProperty("dexmaker.share_classloader", "true");
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mMockitoSession = mockitoSession()
+            .spyStatic(Process.class)
+            .strictness(Strictness.LENIENT)
+            .startMocking();
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        final ProcessList pList = new ProcessList();
+        mProcessList = spy(pList);
+
+        mInjector = new TestInjector(mContext);
+        mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
+        mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
+        mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
+        mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal());
+        mAms.mPackageManagerInt = mPackageManagerInt;
+        pList.mService = mAms;
+        mPhantomProcessList = mAms.mPhantomProcessList;
+        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+        doReturn(false).when(() -> Process.supportsPidFd());
+        // Remove stale instance of PackageManagerInternal if there is any
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+    }
+
+    @After
+    public void tearDown() {
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        mMockitoSession.finishMocking();
+        mHandlerThread.quit();
+    }
+
+    @Test
+    public void testManageAppChildProcesses() throws Exception {
+        final int initPid = 1;
+        final int rootUid = 0;
+        final int zygote64Pid = 100;
+        final int zygote32Pid = 101;
+        final int app1Pid = 200;
+        final int app2Pid = 201;
+        final int app1Uid = 10000;
+        final int app2Uid = 10001;
+        final int child1Pid = 300;
+        final int child2Pid = 301;
+        final int nativePid = 400;
+        final String zygote64ProcessName = "zygote64";
+        final String zygote32ProcessName = "zygote32";
+        final String app1ProcessName = "test1";
+        final String app2ProcessName = "test2";
+        final String child1ProcessName = "test1_child1";
+        final String child2ProcessName = "test1_child1_child2";
+        final String nativeProcessName = "test_native";
+
+        makeParent(zygote64Pid, initPid);
+        makeParent(zygote32Pid, initPid);
+
+        makeAppProcess(app1Pid, app1Uid, app1ProcessName, app1ProcessName);
+        makeParent(app1Pid, zygote64Pid);
+        makeAppProcess(app2Pid, app2Uid, app2ProcessName, app2ProcessName);
+        makeParent(app2Pid, zygote64Pid);
+
+        assertEquals(0, mPhantomProcessList.mPhantomProcesses.size());
+
+        // Verify zygote itself isn't a phantom process
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                zygote64ProcessName, rootUid, zygote64Pid));
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                zygote32ProcessName, rootUid, zygote32Pid));
+        // Verify none of the app isn't a phantom process
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                app1ProcessName, app1Uid, app1Pid));
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                app2ProcessName, app2Uid, app2Pid));
+
+        // "Fork" an app child process
+        makeParent(child1Pid, app1Pid);
+        PhantomProcessRecord pr = mPhantomProcessList
+                .getOrCreatePhantomProcessIfNeededLocked(child1ProcessName, app1Uid, child1Pid);
+        assertTrue(pr != null);
+        assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
+        assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
+        verifyPhantomProcessRecord(pr, child1ProcessName, app1Uid, child1Pid);
+
+        // Create another native process from init
+        makeParent(nativePid, initPid);
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                nativeProcessName, rootUid, nativePid));
+        assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
+        assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
+
+        // "Fork" another app child process
+        makeParent(child2Pid, child1Pid);
+        PhantomProcessRecord pr2 = mPhantomProcessList
+                .getOrCreatePhantomProcessIfNeededLocked(child2ProcessName, app1Uid, child2Pid);
+        assertTrue(pr2 != null);
+        assertEquals(2, mPhantomProcessList.mPhantomProcesses.size());
+        verifyPhantomProcessRecord(pr2, child2ProcessName, app1Uid, child2Pid);
+
+        ArraySet<PhantomProcessRecord> set = new ArraySet<>();
+        set.add(pr);
+        set.add(pr2);
+        for (int i = mPhantomProcessList.mPhantomProcesses.size() - 1; i >= 0; i--) {
+            set.remove(mPhantomProcessList.mPhantomProcesses.valueAt(i));
+        }
+        assertEquals(0, set.size());
+    }
+
+    private void verifyPhantomProcessRecord(PhantomProcessRecord pr,
+            String processName, int uid, int pid) {
+        assertEquals(processName, pr.mProcessName);
+        assertEquals(uid, pr.mUid);
+        assertEquals(pid, pr.mPid);
+    }
+
+    private void makeAppProcess(int pid, int uid, String packageName, String processName) {
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = packageName;
+        ProcessRecord app = new ProcessRecord(mAms, ai, processName, uid);
+        app.pid = pid;
+        mAms.mPidsSelfLocked.doAddInternal(app);
+    }
+
+    private void makeParent(int pid, int ppid) {
+        doReturn(ppid).when(() -> Process.getParentPid(eq(pid)));
+    }
+
+    private class TestInjector extends Injector {
+        TestInjector(Context context) {
+            super(context);
+        }
+
+        @Override
+        public AppOpsService getAppOpsService(File file, Handler handler) {
+            return mAppOpsService;
+        }
+
+        @Override
+        public Handler getUiHandler(ActivityManagerService service) {
+            return mHandler;
+        }
+
+        @Override
+        public ProcessList getProcessList(ActivityManagerService service) {
+            return mProcessList;
+        }
+    }
+
+    static class ServiceThreadRule implements TestRule {
+        private ServiceThread mThread;
+
+        ServiceThread getThread() {
+            return mThread;
+        }
+
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    mThread = new ServiceThread("TestServiceThread",
+                            Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */);
+                    mThread.start();
+                    try {
+                        base.evaluate();
+                    } finally {
+                        mThread.getThreadHandler().runWithScissors(mThread::quit, 0 /* timeout */);
+                    }
+                }
+            };
+        }
+    }
+
+}