diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java index b0a389d8ac681..a76eb8f1e55df 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java @@ -18,6 +18,7 @@ package com.android.server.app; import android.annotation.NonNull; import android.app.ActivityManager; +import android.app.ActivityManagerInternal; import android.app.ActivityTaskManager; import android.content.Context; import android.content.Intent; @@ -52,6 +53,7 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide mContext, new GameClassifierImpl(mContext.getPackageManager()), ActivityManager.getService(), + LocalServices.getService(ActivityManagerInternal.class), ActivityTaskManager.getService(), (WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE), LocalServices.getService(WindowManagerInternal.class), diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java index faf5c38269643..e9205230a4ad9 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java @@ -21,9 +21,11 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityManagerInternal; import android.app.ActivityTaskManager; import android.app.IActivityManager; import android.app.IActivityTaskManager; +import android.app.IProcessObserver; import android.app.TaskStackListener; import android.content.ComponentName; import android.content.Context; @@ -45,6 +47,7 @@ import android.service.games.IGameServiceController; import android.service.games.IGameSession; import android.service.games.IGameSessionController; import android.service.games.IGameSessionService; +import android.text.TextUtils; import android.util.Slog; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost.SurfacePackage; @@ -143,12 +146,42 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan GameServiceProviderInstanceImpl.this.onTaskFocusChanged(taskId, focused); }); } + }; - // TODO(b/204503192): Limit the lifespan of the game session in the Game Service provider - // to only when the associated task is running. Right now it is possible for a task to - // move into the background and for all associated processes to die and for the Game Session - // provider's GameSessionService to continue to be running. Ideally we could unbind the - // service when this happens. + /** + * The TaskStackListener declared above gives us good visibility into game task lifecycle. + * However, it is possible for the Android system to kill all the processes associated with a + * game task (e.g., when the system is under memory pressure or reaches a background process + * limit). When this happens, the game task remains (and no TaskStackListener callbacks are + * invoked), but we would nonetheless want to destroy a game session associated with the task + * if this were to happen. + * + * This process observer gives us visibility into process lifecycles and lets us track all the + * processes associated with each package so that any game sessions associated with the package + * are destroyed if the process count for a given package reaches zero (most packages will + * have at most one task). If processes for a given package are started up again, the destroyed + * game sessions will be re-created. + */ + private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() { + @Override + public void onForegroundActivitiesChanged(int pid, int uid, boolean fg) { + // This callback is used to track how many processes are running for a given package. + // Then, when a process dies, we will know if it was the only process running for that + // package and the associated game sessions should be destroyed. + mBackgroundExecutor.execute(() -> { + GameServiceProviderInstanceImpl.this.onForegroundActivitiesChanged(pid); + }); + } + + @Override + public void onProcessDied(int pid, int uid) { + mBackgroundExecutor.execute(() -> { + GameServiceProviderInstanceImpl.this.onProcessDied(pid); + }); + } + + @Override + public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {} }; private final IGameServiceController mGameServiceController = @@ -192,6 +225,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan private final Context mContext; private final GameClassifier mGameClassifier; private final IActivityManager mActivityManager; + private final ActivityManagerInternal mActivityManagerInternal; private final IActivityTaskManager mActivityTaskManager; private final WindowManagerService mWindowManagerService; private final WindowManagerInternal mWindowManagerInternal; @@ -203,6 +237,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan private final ConcurrentHashMap mGameSessions = new ConcurrentHashMap<>(); @GuardedBy("mLock") + private final ConcurrentHashMap mPidToPackageMap = new ConcurrentHashMap<>(); + @GuardedBy("mLock") + private final ConcurrentHashMap mPackageNameToProcessCountMap = + new ConcurrentHashMap<>(); + + @GuardedBy("mLock") private volatile boolean mIsRunning; GameServiceProviderInstanceImpl( @@ -211,6 +251,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan @NonNull Context context, @NonNull GameClassifier gameClassifier, @NonNull IActivityManager activityManager, + @NonNull ActivityManagerInternal activityManagerInternal, @NonNull IActivityTaskManager activityTaskManager, @NonNull WindowManagerService windowManagerService, @NonNull WindowManagerInternal windowManagerInternal, @@ -222,6 +263,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan mContext = context; mGameClassifier = gameClassifier; mActivityManager = activityManager; + mActivityManagerInternal = activityManagerInternal; mActivityTaskManager = activityTaskManager; mWindowManagerService = windowManagerService; mWindowManagerInternal = windowManagerInternal; @@ -263,6 +305,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan Slog.w(TAG, "Failed to register task stack listener", e); } + try { + mActivityManager.registerProcessObserver(mProcessObserver); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to register process observer", e); + } + mWindowManagerInternal.registerTaskSystemBarsListener(mTaskSystemBarsVisibilityListener); } @@ -273,6 +321,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } mIsRunning = false; + try { + mActivityManager.unregisterProcessObserver(mProcessObserver); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to unregister process observer", e); + } + try { mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); } catch (RemoteException e) { @@ -596,6 +650,126 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } } + private void onForegroundActivitiesChanged(int pid) { + synchronized (mLock) { + onForegroundActivitiesChangedLocked(pid); + } + } + + @GuardedBy("mLock") + private void onForegroundActivitiesChangedLocked(int pid) { + if (mPidToPackageMap.containsKey(pid)) { + // We are already tracking this pid, nothing to do. + return; + } + + final String packageName = mActivityManagerInternal.getPackageNameByPid(pid); + if (TextUtils.isEmpty(packageName)) { + // Game processes should always have a package name. + return; + } + + if (!gameSessionExistsForPackageNameLocked(packageName)) { + // We only need to track processes for tasks with game session records. + return; + } + + mPidToPackageMap.put(pid, packageName); + final int processCountForPackage = mPackageNameToProcessCountMap.getOrDefault(packageName, + 0) + 1; + mPackageNameToProcessCountMap.put(packageName, processCountForPackage); + + if (DEBUG) { + Slog.d(TAG, "onForegroundActivitiesChangedLocked: tracking pid " + pid + ", for " + + packageName + ". Process count for package: " + processCountForPackage); + } + + // If there are processes for the package, we may need to re-create game sessions + // that are associated with the package + if (processCountForPackage > 0) { + recreateEndedGameSessionsLocked(packageName); + } + } + + @GuardedBy("mLock") + private void recreateEndedGameSessionsLocked(String packageName) { + for (GameSessionRecord gameSessionRecord : mGameSessions.values()) { + if (gameSessionRecord.isGameSessionEndedForProcessDeath() && packageName.equals( + gameSessionRecord.getComponentName().getPackageName())) { + if (DEBUG) { + Slog.d(TAG, + "recreateGameSessionsLocked(): re-creating game session for: " + + packageName + " with taskId: " + + gameSessionRecord.getTaskId()); + } + + final int taskId = gameSessionRecord.getTaskId(); + mGameSessions.put(taskId, GameSessionRecord.awaitingGameSessionRequest(taskId, + gameSessionRecord.getComponentName())); + createGameSessionLocked(gameSessionRecord.getTaskId()); + } + } + } + + private void onProcessDied(int pid) { + synchronized (mLock) { + onProcessDiedLocked(pid); + } + } + + @GuardedBy("mLock") + private void onProcessDiedLocked(int pid) { + final String packageName = mPidToPackageMap.remove(pid); + if (packageName == null) { + // We weren't tracking this process. + return; + } + + final Integer oldProcessCountForPackage = mPackageNameToProcessCountMap.get(packageName); + if (oldProcessCountForPackage == null) { + // This should never happen; we should have a process count for all tracked packages. + Slog.w(TAG, "onProcessDiedLocked(): Missing process count for package"); + return; + } + + final int processCountForPackage = oldProcessCountForPackage - 1; + mPackageNameToProcessCountMap.put(packageName, processCountForPackage); + + // If there are no more processes for the game, then we will terminate any game sessions + // running for the package. + if (processCountForPackage <= 0) { + endGameSessionsForPackageLocked(packageName); + } + } + + @GuardedBy("mLock") + private void endGameSessionsForPackageLocked(String packageName) { + for (GameSessionRecord gameSessionRecord : mGameSessions.values()) { + if (gameSessionRecord.getGameSession() != null && packageName.equals( + gameSessionRecord.getComponentName().getPackageName())) { + if (DEBUG) { + Slog.d(TAG, "endGameSessionsForPackageLocked(): No more processes for " + + packageName + ", ending game session with taskId: " + + gameSessionRecord.getTaskId()); + } + mGameSessions.put(gameSessionRecord.getTaskId(), + gameSessionRecord.withGameSessionEndedOnProcessDeath()); + destroyGameSessionFromRecordLocked(gameSessionRecord); + } + } + } + + @GuardedBy("mLock") + private boolean gameSessionExistsForPackageNameLocked(String packageName) { + for (GameSessionRecord gameSessionRecord : mGameSessions.values()) { + if (packageName.equals(gameSessionRecord.getComponentName().getPackageName())) { + return true; + } + } + + return false; + } + @Nullable private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) { RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId); diff --git a/services/core/java/com/android/server/app/GameSessionRecord.java b/services/core/java/com/android/server/app/GameSessionRecord.java index a241812f7868d..74e538ef5011a 100644 --- a/services/core/java/com/android/server/app/GameSessionRecord.java +++ b/services/core/java/com/android/server/app/GameSessionRecord.java @@ -35,6 +35,10 @@ final class GameSessionRecord { // A Game Session is created and attached. // GameSessionRecord.getGameSession() != null. GAME_SESSION_ATTACHED, + // A Game Session did exist for a given game task but was destroyed because the last process + // for the game died. + // GameSessionRecord.getGameSession() == null. + GAME_SESSION_ENDED_PROCESS_DEATH, } private final int mTaskId; @@ -98,6 +102,20 @@ final class GameSessionRecord { surfacePackage); } + @NonNull + public GameSessionRecord withGameSessionEndedOnProcessDeath() { + return new GameSessionRecord( + mTaskId, + State.GAME_SESSION_ENDED_PROCESS_DEATH, + mRootComponentName, + /* gameSession=*/ null, + /* surfacePackage=*/ null); + } + + public boolean isGameSessionEndedForProcessDeath() { + return mState == State.GAME_SESSION_ENDED_PROCESS_DEATH; + } + @NonNull public int getTaskId() { return mTaskId; diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java index 575e3513eae47..319a769bb1dee 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java @@ -19,6 +19,7 @@ package com.android.server.app; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.app.GameServiceProviderInstanceImplTest.FakeGameService.GameServiceState; @@ -26,19 +27,21 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verifyZeroInteractions; import android.Manifest; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityManagerInternal; import android.app.ActivityTaskManager; import android.app.IActivityManager; import android.app.IActivityTaskManager; +import android.app.IProcessObserver; import android.app.ITaskStackListener; import android.content.ComponentName; import android.content.Context; @@ -135,6 +138,8 @@ public final class GameServiceProviderInstanceImplTest { private MockitoSession mMockingSession; private GameServiceProviderInstance mGameServiceProviderInstance; @Mock + private ActivityManagerInternal mMockActivityManagerInternal; + @Mock private IActivityTaskManager mMockActivityTaskManager; @Mock private WindowManagerService mMockWindowManagerService; @@ -151,6 +156,7 @@ public final class GameServiceProviderInstanceImplTest { private FakeGameSessionService mFakeGameSessionService; private FakeServiceConnector mFakeGameSessionServiceConnector; private ArrayList mTaskStackListeners; + private ArrayList mProcessObservers; private ArrayList mTaskSystemBarsListeners; private ArrayList mRunningTaskInfos; @@ -185,6 +191,16 @@ public final class GameServiceProviderInstanceImplTest { return null; }).when(mMockActivityTaskManager).unregisterTaskStackListener(any()); + mProcessObservers = new ArrayList<>(); + doAnswer(invocation -> { + mProcessObservers.add(invocation.getArgument(0)); + return null; + }).when(mMockActivityManager).registerProcessObserver(any()); + doAnswer(invocation -> { + mProcessObservers.remove(invocation.getArgument(0)); + return null; + }).when(mMockActivityManager).unregisterProcessObserver(any()); + mTaskSystemBarsListeners = new ArrayList<>(); doAnswer(invocation -> { mTaskSystemBarsListeners.add(invocation.getArgument(0)); @@ -206,6 +222,7 @@ public final class GameServiceProviderInstanceImplTest { mMockContext, mFakeGameClassifier, mMockActivityManager, + mMockActivityManagerInternal, mMockActivityTaskManager, mMockWindowManagerService, mMockWindowManagerInternal, @@ -428,6 +445,214 @@ public final class GameServiceProviderInstanceImplTest { verify(mMockWindowManagerInternal).addTrustedTaskOverlay(eq(10), eq(mockSurfacePackage10)); } + @Test + public void gameProcessStopped_soleProcess_destroysGameSession() throws Exception { + int gameProcessId = 1000; + + mGameServiceProviderInstance.start(); + + startTask(10, GAME_A_MAIN_ACTIVITY); + startProcessForPackage(gameProcessId, GAME_A_PACKAGE); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + assertThat(gameSession10.mIsDestroyed).isFalse(); + + // Death of the sole game process destroys the game session. + dispatchProcessDied(gameProcessId); + assertThat(gameSession10.mIsDestroyed).isTrue(); + } + + @Test + public void gameProcessStopped_soleProcess_destroysMultipleGameSessionsForSamePackage() + throws Exception { + int gameProcessId = 1000; + + mGameServiceProviderInstance.start(); + + // Multiple tasks exist for the same package. + startTask(10, GAME_A_MAIN_ACTIVITY); + startTask(11, GAME_A_MAIN_ACTIVITY); + startProcessForPackage(gameProcessId, GAME_A_PACKAGE); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); + + assertThat(gameSession10.mIsDestroyed).isFalse(); + assertThat(gameSession11.mIsDestroyed).isFalse(); + + // Death of the sole game process destroys both game sessions. + dispatchProcessDied(gameProcessId); + assertThat(gameSession10.mIsDestroyed).isTrue(); + assertThat(gameSession11.mIsDestroyed).isTrue(); + } + + @Test + public void gameProcessStopped_multipleProcesses_gameSessionDestroyedWhenAllDead() + throws Exception { + int firstGameProcessId = 1000; + int secondGameProcessId = 1001; + + mGameServiceProviderInstance.start(); + + startTask(10, GAME_A_MAIN_ACTIVITY); + startProcessForPackage(firstGameProcessId, GAME_A_PACKAGE); + startProcessForPackage(secondGameProcessId, GAME_A_PACKAGE); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + assertThat(gameSession10.mIsDestroyed).isFalse(); + + // Death of the first process (with the second one still alive) does not destroy the game + // session. + dispatchProcessDied(firstGameProcessId); + assertThat(gameSession10.mIsDestroyed).isFalse(); + + // Death of the second process does destroy the game session. + dispatchProcessDied(secondGameProcessId); + assertThat(gameSession10.mIsDestroyed).isTrue(); + } + + @Test + public void gameProcessCreatedAfterInitialProcessDead_newGameSessionCreated() throws Exception { + int firstGameProcessId = 1000; + int secondGameProcessId = 1000; + + mGameServiceProviderInstance.start(); + + startTask(10, GAME_A_MAIN_ACTIVITY); + startProcessForPackage(firstGameProcessId, GAME_A_PACKAGE); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + assertThat(gameSession10.mIsDestroyed).isFalse(); + + // After the first game process dies, the game session should be destroyed. + dispatchProcessDied(firstGameProcessId); + assertThat(gameSession10.mIsDestroyed).isTrue(); + + // However, when a new process for the game starts, a new game session should be created. + startProcessForPackage(secondGameProcessId, GAME_A_PACKAGE); + // Verify that a new pending game session is created for the game's taskId. + assertNotNull(mFakeGameSessionService.removePendingFutureForTaskId(10)); + } + + @Test + public void gameProcessCreatedAfterInitialProcessDead_multipleGameSessionsCreatedSamePackage() + throws Exception { + int firstGameProcessId = 1000; + int secondGameProcessId = 1000; + + mGameServiceProviderInstance.start(); + + // Multiple tasks exist for the same package. + startTask(10, GAME_A_MAIN_ACTIVITY); + startTask(11, GAME_A_MAIN_ACTIVITY); + startProcessForPackage(firstGameProcessId, GAME_A_PACKAGE); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + + mFakeGameService.requestCreateGameSession(10); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); + + assertThat(gameSession10.mIsDestroyed).isFalse(); + assertThat(gameSession11.mIsDestroyed).isFalse(); + + // After the first game process dies, both game sessions for the package should be + // destroyed. + dispatchProcessDied(firstGameProcessId); + assertThat(gameSession10.mIsDestroyed).isTrue(); + assertThat(gameSession11.mIsDestroyed).isTrue(); + + // However, when a new process for the game starts, new game sessions for the same + // package should be created. + startProcessForPackage(secondGameProcessId, GAME_A_PACKAGE); + // Verify that new pending game sessions were created for each of the game's taskIds. + assertNotNull(mFakeGameSessionService.removePendingFutureForTaskId(10)); + assertNotNull(mFakeGameSessionService.removePendingFutureForTaskId(11)); + } + + @Test + public void gameProcessStarted_gameSessionNotRequested_doesNothing() throws Exception { + int gameProcessId = 1000; + + mGameServiceProviderInstance.start(); + + // A game task and process are started, but requestCreateGameSession is never called. + startTask(10, GAME_A_MAIN_ACTIVITY); + startProcessForPackage(gameProcessId, GAME_A_PACKAGE); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + + // No game session should be created. + assertThat(mFakeGameSessionService.getCapturedCreateInvocations()).isEmpty(); + } + + @Test + public void processActivityAndDeath_notForGame_gameSessionUnaffected() throws Exception { + mGameServiceProviderInstance.start(); + + startTask(10, GAME_A_MAIN_ACTIVITY); + + mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + // Process activity for a process without a known package is ignored. + startProcessForPackage(1000, /*packageName=*/ null); + dispatchProcessActivity(1000); + dispatchProcessDied(1000); + + // Process activity for a process with a different package is ignored + startProcessForPackage(1001, GAME_B_PACKAGE); + dispatchProcessActivity(1001); + dispatchProcessDied(1001); + + // Death of a process for which there was no activity is ignored + dispatchProcessDied(1002); + + // Despite all the process activity and death, the game session is not destroyed. + assertThat(gameSession10.mIsDestroyed).isFalse(); + } + @Test public void taskSystemBarsListenerChanged_noAssociatedGameSession_doesNothing() { mGameServiceProviderInstance.start(); @@ -900,7 +1125,8 @@ public final class GameServiceProviderInstanceImplTest { mFakeGameSessionService.getCapturedCreateInvocations()) .mGameSessionController.restartGame(11); - verifyZeroInteractions(mMockActivityManager); + verify(mMockActivityManager).registerProcessObserver(any()); + verifyNoMoreInteractions(mMockActivityManager); assertThat(mMockContext.getLastStartedIntent()).isNull(); } @@ -932,7 +1158,6 @@ public final class GameServiceProviderInstanceImplTest { dispatchTaskRemoved(taskId); } - private void dispatchTaskRemoved(int taskId) { dispatchTaskChangeEvent(taskStackListener -> { taskStackListener.onTaskRemoved(taskId); @@ -958,6 +1183,37 @@ public final class GameServiceProviderInstanceImplTest { } } + private void startProcessForPackage(int processId, @Nullable String packageName) { + if (packageName != null) { + when(mMockActivityManagerInternal.getPackageNameByPid(processId)).thenReturn( + packageName); + } + + dispatchProcessActivity(processId); + } + + private void dispatchProcessActivity(int processId) { + dispatchProcessChangedEvent(processObserver -> { + // Neither uid nor foregroundActivities are used by the implementation being tested. + processObserver.onForegroundActivitiesChanged(processId, /*uid=*/ + 0, /*foregroundActivities=*/ false); + }); + } + + private void dispatchProcessDied(int processId) { + dispatchProcessChangedEvent(processObserver -> { + // The uid param is not used by the implementation being tested. + processObserver.onProcessDied(processId, /*uid=*/ 0); + }); + } + + private void dispatchProcessChangedEvent( + ThrowingConsumer processObserverConsumer) { + for (IProcessObserver processObserver : mProcessObservers) { + processObserverConsumer.accept(processObserver); + } + } + private void mockPermissionGranted(String permission) { mMockContext.setPermission(permission, PackageManager.PERMISSION_GRANTED); }