From 331ef23f78d36d224ca402742cae0fa0237b660a Mon Sep 17 00:00:00 2001 From: Zim Date: Mon, 24 Oct 2022 11:53:10 +0100 Subject: [PATCH] Split attachApplication to more accurately identify app startup Previously, while handling the attachApplication from an app at startup, the system started the broadcast (and service) AnR timeout countdown as soon as it sent async bindApplication request to the app. This increased the likelyhood of AnRs under CPU contention since some fixed process initialization costs were included in the broadcast handling delay. Now, we split the the attachApplication into 2 phases, there's now a new finishAttachApplication which apps call right before they start executing any custom code. This new call unblocks any pending broadcast/service scheduling and starts the appropriate AnR timeout countdowns. Test: atest AsyncProcessStartTest Bug: 253908737 Change-Id: I61046201281acf6686290d0ab83174ab134d5ef2 --- core/java/android/app/ActivityThread.java | 11 + core/java/android/app/IActivityManager.aidl | 5 +- .../server/am/ActivityManagerService.java | 283 ++++++++++-------- .../com/android/server/am/ProcessList.java | 2 +- .../com/android/server/am/ProcessRecord.java | 9 + .../server/am/AsyncProcessStartTest.java | 282 +++++++++++++++++ 6 files changed, 467 insertions(+), 125 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 884870bff3af5..96ced41f36ca6 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -502,6 +502,7 @@ public final class ActivityThread extends ClientTransactionHandler @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) static volatile Handler sMainThreadHandler; // set once in main() + private long mStartSeq; // Only accesssed from the main thread Bundle mCoreSettings = null; @@ -6809,6 +6810,14 @@ public final class ActivityThread extends ClientTransactionHandler Application app; final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites(); final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy(); + + final IActivityManager mgr = ActivityManager.getService(); + try { + mgr.finishAttachApplication(mStartSeq); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + try { // If the app is being launched for full backup or restore, bring it up in // a restricted environment with the base application class. @@ -7649,6 +7658,8 @@ public final class ActivityThread extends ClientTransactionHandler sCurrentActivityThread = this; mConfigurationController = new ConfigurationController(this); mSystemThread = system; + mStartSeq = startSeq; + if (!system) { android.ddm.DdmHandleAppName.setAppName("", UserHandle.myUserId()); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 902f172b6ad7e..3edaabde5f2a7 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -147,6 +147,7 @@ interface IActivityManager { oneway void finishReceiver(in IBinder who, int resultCode, in String resultData, in Bundle map, boolean abortBroadcast, int flags); void attachApplication(in IApplicationThread app, long startSeq); + void finishAttachApplication(long startSeq); List getTasks(int maxNum); @UnsupportedAppUsage void moveTaskToFront(in IApplicationThread caller, in String callingPackage, int task, @@ -718,8 +719,8 @@ interface IActivityManager { /** * Control the app freezer state. Returns true in case of success, false if the operation - * didn't succeed (for example, when the app freezer isn't supported). - * Handling the freezer state via this method is reentrant, that is it can be + * didn't succeed (for example, when the app freezer isn't supported). + * Handling the freezer state via this method is reentrant, that is it can be * disabled and re-enabled multiple times in parallel. As long as there's a 1:1 disable to * enable match, the freezer is re-enabled at last enable only. * @param enable set it to true to enable the app freezer, false to disable it. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 35b46c1104f7b..50be45804e4f6 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -87,6 +87,7 @@ import static android.os.Process.getTotalMemory; import static android.os.Process.isSdkSandboxUid; import static android.os.Process.isThreadInProcess; import static android.os.Process.killProcess; +import static android.os.Process.killProcessGroup; import static android.os.Process.killProcessQuiet; import static android.os.Process.myPid; import static android.os.Process.myUid; @@ -952,13 +953,6 @@ public class ActivityManagerService extends IActivityManager.Stub } return false; } - - boolean doRemoveIfNoThreadInternal(int pid, ProcessRecord app) { - if (app == null || app.getThread() != null) { - return false; - } - return doRemoveInternal(pid, app); - } } private final PendingStartActivityUids mPendingStartActivityUids; @@ -990,7 +984,7 @@ public class ActivityManagerService extends IActivityManager.Stub * method. */ @GuardedBy("this") - void removePidLocked(int pid, ProcessRecord app) { + boolean removePidLocked(int pid, ProcessRecord app) { final boolean removed; synchronized (mPidsSelfLocked) { removed = mPidsSelfLocked.doRemoveInternal(pid, app); @@ -1001,26 +995,6 @@ public class ActivityManagerService extends IActivityManager.Stub } mAtmInternal.onProcessUnMapped(pid); } - } - - /** - * Removes the process record from the map if it doesn't have a thread. - *

NOTE: Callers should avoid acquiring the mPidsSelfLocked lock before calling this - * method. - */ - @GuardedBy("this") - private boolean removePidIfNoThreadLocked(ProcessRecord app) { - final boolean removed; - final int pid = app.getPid(); - synchronized (mPidsSelfLocked) { - removed = mPidsSelfLocked.doRemoveIfNoThreadInternal(pid, app); - } - if (removed) { - synchronized (sActiveProcessInfoSelfLocked) { - sActiveProcessInfoSelfLocked.remove(pid); - } - mAtmInternal.onProcessUnMapped(pid); - } return removed; } @@ -2364,7 +2338,7 @@ public class ActivityManagerService extends IActivityManager.Stub mAppErrors = null; mPackageWatchdog = null; mAppOpsService = mInjector.getAppOpsService(null /* file */, null /* handler */); - mBatteryStatsService = null; + mBatteryStatsService = mInjector.getBatteryStatsService(); mHandler = new MainHandler(handlerThread.getLooper()); mHandlerThread = handlerThread; mConstants = new ActivityManagerConstants(mContext, this, mHandler); @@ -2379,7 +2353,7 @@ public class ActivityManagerService extends IActivityManager.Stub mIntentFirewall = null; mProcessStats = new ProcessStatsService(this, mContext.getCacheDir()); mCpHelper = new ContentProviderHelper(this, false); - mServices = null; + mServices = mInjector.getActiveServices(this); mSystemThread = null; mUiHandler = injector.getUiHandler(null /* service */); mUidObserverController = new UidObserverController(mUiHandler); @@ -4771,7 +4745,7 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("this") void handleProcessStartOrKillTimeoutLocked(ProcessRecord app, boolean isKillTimeout) { final int pid = app.getPid(); - boolean gone = isKillTimeout || removePidIfNoThreadLocked(app); + boolean gone = isKillTimeout || removePidLocked(pid, app); if (gone) { if (isKillTimeout) { @@ -4852,7 +4826,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") - private boolean attachApplicationLocked(@NonNull IApplicationThread thread, + private void attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) { // Find the application record that is being attached... either via @@ -4917,7 +4891,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Ignore exceptions. } } - return false; + return; } // If this application record is still attached to a previous @@ -4942,7 +4916,7 @@ public class ActivityManagerService extends IActivityManager.Stub mProcessList.startProcessLocked(app, new HostingRecord(HostingRecord.HOSTING_TYPE_LINK_FAIL, processName), ZYGOTE_POLICY_FLAG_EMPTY); - return false; + return; } EventLogTags.writeAmProcBound(app.userId, pid, app.processName); @@ -4965,8 +4939,6 @@ public class ActivityManagerService extends IActivityManager.Stub app.setUnlocked(StorageManager.isUserKeyUnlocked(app.userId)); } - mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); - boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info); List providers = normalMode ? mCpHelper.generateApplicationProvidersLocked(app) @@ -5132,7 +5104,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.killLocked("error during bind", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true); handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */); - return false; + return; } // Remove this record from the list of starting applications. @@ -5140,91 +5112,6 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_PROCESSES && mProcessesOnHold.contains(app)) Slog.v(TAG_PROCESSES, "Attach application locked removing on hold: " + app); mProcessesOnHold.remove(app); - - boolean badApp = false; - boolean didSomething = false; - - // See if the top visible activity is waiting to run in this process... - if (normalMode) { - try { - didSomething = mAtmInternal.attachApplication(app.getWindowProcessController()); - } catch (Exception e) { - Slog.wtf(TAG, "Exception thrown launching activities in " + app, e); - badApp = true; - } - } - - // Find any services that should be running in this process... - if (!badApp) { - try { - didSomething |= mServices.attachApplicationLocked(app, processName); - checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked"); - } catch (Exception e) { - Slog.wtf(TAG, "Exception thrown starting services in " + app, e); - badApp = true; - } - } - - // Check if a next-broadcast receiver is in this process... - if (!badApp) { - try { - for (BroadcastQueue queue : mBroadcastQueues) { - didSomething |= queue.onApplicationAttachedLocked(app); - } - checkTime(startTime, "attachApplicationLocked: after dispatching broadcasts"); - } catch (Exception e) { - // If the app died trying to launch the receiver we declare it 'bad' - Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e); - badApp = true; - } - } - - // Check whether the next backup agent is in this process... - if (!badApp && backupTarget != null && backupTarget.app == app) { - if (DEBUG_BACKUP) Slog.v(TAG_BACKUP, - "New app is backup target, launching agent for " + app); - notifyPackageUse(backupTarget.appInfo.packageName, - PackageManager.NOTIFY_PACKAGE_USE_BACKUP); - try { - thread.scheduleCreateBackupAgent(backupTarget.appInfo, - backupTarget.backupMode, backupTarget.userId, - backupTarget.backupDestination); - } catch (Exception e) { - Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e); - badApp = true; - } - } - - if (badApp) { - app.killLocked("error during init", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, - true); - handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */); - return false; - } - - if (!didSomething) { - updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN); - checkTime(startTime, "attachApplicationLocked: after updateOomAdjLocked"); - } - - - final HostingRecord hostingRecord = app.getHostingRecord(); - String shortAction = getShortAction(hostingRecord.getAction()); - FrameworkStatsLog.write( - FrameworkStatsLog.PROCESS_START_TIME, - app.info.uid, - pid, - app.info.packageName, - FrameworkStatsLog.PROCESS_START_TIME__TYPE__COLD, - app.getStartElapsedTime(), - (int) (bindApplicationTimeMillis - app.getStartUptime()), - (int) (SystemClock.uptimeMillis() - app.getStartUptime()), - hostingRecord.getType(), - hostingRecord.getName(), - shortAction, - HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()), - HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType())); - return true; } @Override @@ -5241,6 +5128,143 @@ public class ActivityManagerService extends IActivityManager.Stub } } + private void finishAttachApplicationInner(long startSeq, int uid, int pid) { + final long startTime = SystemClock.uptimeMillis(); + // Find the application record that is being attached... either via + // the pid if we are running in multiple processes, or just pull the + // next app record if we are emulating process with anonymous threads. + final ProcessRecord app; + synchronized (mPidsSelfLocked) { + app = mPidsSelfLocked.get(pid); + } + + if (app != null && app.getStartUid() == uid && app.getStartSeq() == startSeq) { + mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); + } else { + Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid + + ". Uid: " + uid); + killProcess(pid); + killProcessGroup(uid, pid); + mProcessList.noteAppKill(pid, uid, + ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, + ApplicationExitInfo.SUBREASON_UNKNOWN, + "wrong startSeq"); + app.killLocked("unexpected process record", + ApplicationExitInfo.REASON_OTHER, true); + return; + } + + synchronized (this) { + final boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info); + final String processName = app.processName; + boolean badApp = false; + boolean didSomething = false; + + // See if the top visible activity is waiting to run in this process... + if (normalMode) { + try { + didSomething = mAtmInternal.attachApplication(app.getWindowProcessController()); + } catch (Exception e) { + Slog.wtf(TAG, "Exception thrown launching activities in " + app, e); + badApp = true; + } + } + + // Find any services that should be running in this process... + if (!badApp) { + try { + didSomething |= mServices.attachApplicationLocked(app, processName); + checkTime(startTime, "finishAttachApplicationInner: " + + "after mServices.attachApplicationLocked"); + } catch (Exception e) { + Slog.wtf(TAG, "Exception thrown starting services in " + app, e); + badApp = true; + } + } + + // Check if a next-broadcast receiver is in this process... + if (!badApp) { + try { + for (BroadcastQueue queue : mBroadcastQueues) { + didSomething |= queue.onApplicationAttachedLocked(app); + } + checkTime(startTime, "finishAttachApplicationInner: " + + "after dispatching broadcasts"); + } catch (Exception e) { + // If the app died trying to launch the receiver we declare it 'bad' + Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e); + badApp = true; + } + } + + // Check whether the next backup agent is in this process... + final BackupRecord backupTarget = mBackupTargets.get(app.userId); + if (!badApp && backupTarget != null && backupTarget.app == app) { + if (DEBUG_BACKUP) { + Slog.v(TAG_BACKUP, + "New app is backup target, launching agent for " + app); + } + + notifyPackageUse(backupTarget.appInfo.packageName, + PackageManager.NOTIFY_PACKAGE_USE_BACKUP); + try { + app.getThread().scheduleCreateBackupAgent(backupTarget.appInfo, + backupTarget.backupMode, backupTarget.userId, + backupTarget.backupDestination); + } catch (Exception e) { + Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e); + badApp = true; + } + } + + if (badApp) { + app.killLocked("error during init", + ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true); + handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */); + return; + } + + if (!didSomething) { + updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN); + checkTime(startTime, "finishAttachApplicationInner: after updateOomAdjLocked"); + } + + final HostingRecord hostingRecord = app.getHostingRecord(); + final String shortAction = getShortAction(hostingRecord.getAction()); + FrameworkStatsLog.write( + FrameworkStatsLog.PROCESS_START_TIME, + app.info.uid, + pid, + app.info.packageName, + FrameworkStatsLog.PROCESS_START_TIME__TYPE__COLD, + app.getStartElapsedTime(), + (int) (app.getBindApplicationTime() - app.getStartUptime()), + (int) (SystemClock.uptimeMillis() - app.getStartUptime()), + hostingRecord.getType(), + hostingRecord.getName(), + shortAction, + HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()), + HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType())); + } + } + + @Override + public final void finishAttachApplication(long startSeq) { + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + + if (pid == MY_PID && uid == SYSTEM_UID) { + return; + } + + final long origId = Binder.clearCallingIdentity(); + try { + finishAttachApplicationInner(startSeq, uid, pid); + } finally { + Binder.restoreCallingIdentity(origId); + } + } + /** * @return The last part of the string of an intent's action. */ @@ -18805,6 +18829,21 @@ public class ActivityManagerService extends IActivityManager.Stub return new ProcessList(); } + /** + * Returns the {@link BatteryStatsService} instance + */ + public BatteryStatsService getBatteryStatsService() { + return new BatteryStatsService(mContext, SystemServiceManager.ensureSystemDir(), + BackgroundThread.get().getHandler()); + } + + /** + * Returns the {@link ActiveServices} instance + */ + public ActiveServices getActiveServices(ActivityManagerService service) { + return new ActiveServices(service); + } + private boolean ensureHasNetworkManagementInternal() { if (mNmi == null) { mNmi = LocalServices.getService(NetworkManagementInternal.class); diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index ecea96e927e32..937bbc9cced69 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2508,7 +2508,7 @@ public final class ProcessList { } @GuardedBy("mService") - private String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) { + String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) { StringBuilder sb = null; if (app.isKilledByAm()) { if (sb == null) sb = new StringBuilder(); diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 0a8c6400a6fd0..4706c26889def 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -199,6 +199,11 @@ class ProcessRecord implements WindowProcessListener { */ private volatile long mStartElapsedTime; + /** + * When the process was sent the bindApplication request + */ + private volatile long mBindApplicationTime; + /** * This will be same as {@link #uid} usually except for some apps used during factory testing. */ @@ -739,6 +744,10 @@ class ProcessRecord implements WindowProcessListener { return mStartElapsedTime; } + long getBindApplicationTime() { + return mBindApplicationTime; + } + int getStartUid() { return mStartUid; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java new file mode 100644 index 0000000000000..ea14ffbe35722 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2022 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.myPid; +import static android.os.Process.myUid; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManagerInternal; +import android.app.IApplicationThread; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.SystemClock; +import android.util.Log; + +import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.DropBoxManagerInternal; +import com.android.server.LocalServices; +import com.android.server.am.ActivityManagerService.Injector; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.Arrays; + + +/** + * Tests to verify process starts are completed or timeout correctly + */ +@MediumTest +@SuppressWarnings("GuardedBy") +public class AsyncProcessStartTest { + private static final String TAG = "AsyncProcessStartTest"; + + private static final String PACKAGE = "com.foo"; + + @Rule + public final ApplicationExitInfoTest.ServiceThreadRule + mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); + + private Context mContext; + private HandlerThread mHandlerThread; + + @Mock + private AppOpsService mAppOpsService; + @Mock + private DropBoxManagerInternal mDropBoxManagerInt; + @Mock + private PackageManagerInternal mPackageManagerInt; + @Mock + private UsageStatsManagerInternal mUsageStatsManagerInt; + @Mock + private ActivityManagerInternal mActivityManagerInt; + @Mock + private ActivityTaskManagerInternal mActivityTaskManagerInt; + @Mock + private BatteryStatsService mBatteryStatsService; + + private ActivityManagerService mRealAms; + private ActivityManagerService mAms; + + private ProcessList mRealProcessList = new ProcessList(); + private ProcessList mProcessList; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + + LocalServices.removeServiceForTest(DropBoxManagerInternal.class); + LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt); + + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInt); + + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInt); + + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + doReturn(true).when(mActivityTaskManagerInt).attachApplication(any()); + doNothing().when(mActivityTaskManagerInt).onProcessMapped(anyInt(), any()); + + mRealAms = new ActivityManagerService( + new TestInjector(mContext), mServiceThreadRule.getThread()); + mRealAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + mRealAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + mRealAms.mAtmInternal = mActivityTaskManagerInt; + mRealAms.mPackageManagerInt = mPackageManagerInt; + mRealAms.mUsageStatsService = mUsageStatsManagerInt; + mRealAms.mProcessesReady = true; + mAms = spy(mRealAms); + mRealProcessList.mService = mAms; + mProcessList = spy(mRealProcessList); + + doAnswer((invocation) -> { + Log.v(TAG, "Intercepting isProcStartValidLocked() for " + + Arrays.toString(invocation.getArguments())); + return null; + }).when(mProcessList).isProcStartValidLocked(any(), anyLong()); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + } + + 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 mHandlerThread.getThreadHandler(); + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mRealProcessList; + } + + @Override + public BatteryStatsService getBatteryStatsService() { + return mBatteryStatsService; + } + } + + private ProcessRecord makeActiveProcessRecord(String packageName, boolean wedge) + throws Exception { + final ApplicationInfo ai = makeApplicationInfo(packageName); + return makeActiveProcessRecord(ai, wedge); + } + + private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, boolean wedge) + throws Exception { + final IApplicationThread thread = mock(IApplicationThread.class); + final IBinder threadBinder = new Binder(); + doReturn(threadBinder).when(thread).asBinder(); + doAnswer((invocation) -> { + Log.v(TAG, "Intercepting bindApplication() for " + + Arrays.toString(invocation.getArguments())); + if (!wedge) { + mRealAms.finishAttachApplication(0); + } + return null; + }).when(thread).bindApplication( + any(), any(), + any(), any(), + any(), any(), + any(), any(), + any(), + any(), anyInt(), + anyBoolean(), anyBoolean(), + anyBoolean(), anyBoolean(), any(), + any(), any(), any(), + any(), any(), + any(), any(), + any(), + anyLong(), anyLong()); + + final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid)); + r.setPid(myPid()); + r.setStartUid(myUid()); + r.setHostingRecord(new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST)); + r.makeActive(thread, mAms.mProcessStats); + doNothing().when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(), + anyBoolean()); + + return r; + } + + static ApplicationInfo makeApplicationInfo(String packageName) { + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.processName = packageName; + ai.uid = myUid(); + return ai; + } + + /** + * Verify that we don't kill a normal process + */ + @Test + public void testNormal() throws Exception { + ProcessRecord app = startProcessAndWait(false); + + verify(app, never()).killLocked(any(), anyInt(), anyBoolean()); + } + + /** + * Verify that we kill a wedged process after the process start timeout + */ + @Test + public void testWedged() throws Exception { + ProcessRecord app = startProcessAndWait(true); + + verify(app).killLocked(any(), anyInt(), anyBoolean()); + } + + private ProcessRecord startProcessAndWait(boolean wedge) throws Exception { + final ProcessRecord app = makeActiveProcessRecord(PACKAGE, wedge); + final ApplicationInfo appInfo = makeApplicationInfo(PACKAGE); + + mProcessList.handleProcessStartedLocked(app, app.getPid(), /* usingWrapper */ false, + /* expectedStartSeq */ 0, /* procAttached */ false); + + app.getThread().bindApplication(PACKAGE, appInfo, + null, null, + null, + null, + null, null, + null, + null, 0, + false, false, + true, false, + null, + null, null, + null, + null, null, null, + null, null, + 0, 0); + + // Sleep until timeout should have triggered + SystemClock.sleep(ActivityManagerService.PROC_START_TIMEOUT + 1000); + + return app; + } +}