Merge "Tie game session lifetime to game process lifetime." into tm-dev

This commit is contained in:
Tyler Lacey
2022-03-09 20:12:41 +00:00
committed by Android (Google) Code Review
4 changed files with 458 additions and 8 deletions

View File

@@ -18,6 +18,7 @@ package com.android.server.app;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager; import android.app.ActivityTaskManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -52,6 +53,7 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide
mContext, mContext,
new GameClassifierImpl(mContext.getPackageManager()), new GameClassifierImpl(mContext.getPackageManager()),
ActivityManager.getService(), ActivityManager.getService(),
LocalServices.getService(ActivityManagerInternal.class),
ActivityTaskManager.getService(), ActivityTaskManager.getService(),
(WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE), (WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE),
LocalServices.getService(WindowManagerInternal.class), LocalServices.getService(WindowManagerInternal.class),

View File

@@ -21,9 +21,11 @@ import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.annotation.RequiresPermission; import android.annotation.RequiresPermission;
import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager; import android.app.ActivityTaskManager;
import android.app.IActivityManager; import android.app.IActivityManager;
import android.app.IActivityTaskManager; import android.app.IActivityTaskManager;
import android.app.IProcessObserver;
import android.app.TaskStackListener; import android.app.TaskStackListener;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -45,6 +47,7 @@ import android.service.games.IGameServiceController;
import android.service.games.IGameSession; import android.service.games.IGameSession;
import android.service.games.IGameSessionController; import android.service.games.IGameSessionController;
import android.service.games.IGameSessionService; import android.service.games.IGameSessionService;
import android.text.TextUtils;
import android.util.Slog; import android.util.Slog;
import android.view.SurfaceControl; import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost.SurfacePackage; import android.view.SurfaceControlViewHost.SurfacePackage;
@@ -143,12 +146,42 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
GameServiceProviderInstanceImpl.this.onTaskFocusChanged(taskId, focused); 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 * The TaskStackListener declared above gives us good visibility into game task lifecycle.
// move into the background and for all associated processes to die and for the Game Session * However, it is possible for the Android system to kill all the processes associated with a
// provider's GameSessionService to continue to be running. Ideally we could unbind the * game task (e.g., when the system is under memory pressure or reaches a background process
// service when this happens. * 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 = private final IGameServiceController mGameServiceController =
@@ -192,6 +225,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
private final Context mContext; private final Context mContext;
private final GameClassifier mGameClassifier; private final GameClassifier mGameClassifier;
private final IActivityManager mActivityManager; private final IActivityManager mActivityManager;
private final ActivityManagerInternal mActivityManagerInternal;
private final IActivityTaskManager mActivityTaskManager; private final IActivityTaskManager mActivityTaskManager;
private final WindowManagerService mWindowManagerService; private final WindowManagerService mWindowManagerService;
private final WindowManagerInternal mWindowManagerInternal; private final WindowManagerInternal mWindowManagerInternal;
@@ -203,6 +237,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
private final ConcurrentHashMap<Integer, GameSessionRecord> mGameSessions = private final ConcurrentHashMap<Integer, GameSessionRecord> mGameSessions =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
@GuardedBy("mLock") @GuardedBy("mLock")
private final ConcurrentHashMap<Integer, String> mPidToPackageMap = new ConcurrentHashMap<>();
@GuardedBy("mLock")
private final ConcurrentHashMap<String, Integer> mPackageNameToProcessCountMap =
new ConcurrentHashMap<>();
@GuardedBy("mLock")
private volatile boolean mIsRunning; private volatile boolean mIsRunning;
GameServiceProviderInstanceImpl( GameServiceProviderInstanceImpl(
@@ -211,6 +251,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
@NonNull Context context, @NonNull Context context,
@NonNull GameClassifier gameClassifier, @NonNull GameClassifier gameClassifier,
@NonNull IActivityManager activityManager, @NonNull IActivityManager activityManager,
@NonNull ActivityManagerInternal activityManagerInternal,
@NonNull IActivityTaskManager activityTaskManager, @NonNull IActivityTaskManager activityTaskManager,
@NonNull WindowManagerService windowManagerService, @NonNull WindowManagerService windowManagerService,
@NonNull WindowManagerInternal windowManagerInternal, @NonNull WindowManagerInternal windowManagerInternal,
@@ -222,6 +263,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
mContext = context; mContext = context;
mGameClassifier = gameClassifier; mGameClassifier = gameClassifier;
mActivityManager = activityManager; mActivityManager = activityManager;
mActivityManagerInternal = activityManagerInternal;
mActivityTaskManager = activityTaskManager; mActivityTaskManager = activityTaskManager;
mWindowManagerService = windowManagerService; mWindowManagerService = windowManagerService;
mWindowManagerInternal = windowManagerInternal; mWindowManagerInternal = windowManagerInternal;
@@ -263,6 +305,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
Slog.w(TAG, "Failed to register task stack listener", e); 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); mWindowManagerInternal.registerTaskSystemBarsListener(mTaskSystemBarsVisibilityListener);
} }
@@ -273,6 +321,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
} }
mIsRunning = false; mIsRunning = false;
try {
mActivityManager.unregisterProcessObserver(mProcessObserver);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to unregister process observer", e);
}
try { try {
mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
} catch (RemoteException e) { } 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 @Nullable
private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) { private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) {
RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId); RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId);

View File

@@ -35,6 +35,10 @@ final class GameSessionRecord {
// A Game Session is created and attached. // A Game Session is created and attached.
// GameSessionRecord.getGameSession() != null. // GameSessionRecord.getGameSession() != null.
GAME_SESSION_ATTACHED, 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; private final int mTaskId;
@@ -98,6 +102,20 @@ final class GameSessionRecord {
surfacePackage); 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 @NonNull
public int getTaskId() { public int getTaskId() {
return mTaskId; return mTaskId;

View File

@@ -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.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; 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.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.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.app.GameServiceProviderInstanceImplTest.FakeGameService.GameServiceState; 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 com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verifyZeroInteractions;
import android.Manifest; import android.Manifest;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager; import android.app.ActivityTaskManager;
import android.app.IActivityManager; import android.app.IActivityManager;
import android.app.IActivityTaskManager; import android.app.IActivityTaskManager;
import android.app.IProcessObserver;
import android.app.ITaskStackListener; import android.app.ITaskStackListener;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -135,6 +138,8 @@ public final class GameServiceProviderInstanceImplTest {
private MockitoSession mMockingSession; private MockitoSession mMockingSession;
private GameServiceProviderInstance mGameServiceProviderInstance; private GameServiceProviderInstance mGameServiceProviderInstance;
@Mock @Mock
private ActivityManagerInternal mMockActivityManagerInternal;
@Mock
private IActivityTaskManager mMockActivityTaskManager; private IActivityTaskManager mMockActivityTaskManager;
@Mock @Mock
private WindowManagerService mMockWindowManagerService; private WindowManagerService mMockWindowManagerService;
@@ -151,6 +156,7 @@ public final class GameServiceProviderInstanceImplTest {
private FakeGameSessionService mFakeGameSessionService; private FakeGameSessionService mFakeGameSessionService;
private FakeServiceConnector<IGameSessionService> mFakeGameSessionServiceConnector; private FakeServiceConnector<IGameSessionService> mFakeGameSessionServiceConnector;
private ArrayList<ITaskStackListener> mTaskStackListeners; private ArrayList<ITaskStackListener> mTaskStackListeners;
private ArrayList<IProcessObserver> mProcessObservers;
private ArrayList<TaskSystemBarsListener> mTaskSystemBarsListeners; private ArrayList<TaskSystemBarsListener> mTaskSystemBarsListeners;
private ArrayList<RunningTaskInfo> mRunningTaskInfos; private ArrayList<RunningTaskInfo> mRunningTaskInfos;
@@ -185,6 +191,16 @@ public final class GameServiceProviderInstanceImplTest {
return null; return null;
}).when(mMockActivityTaskManager).unregisterTaskStackListener(any()); }).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<>(); mTaskSystemBarsListeners = new ArrayList<>();
doAnswer(invocation -> { doAnswer(invocation -> {
mTaskSystemBarsListeners.add(invocation.getArgument(0)); mTaskSystemBarsListeners.add(invocation.getArgument(0));
@@ -206,6 +222,7 @@ public final class GameServiceProviderInstanceImplTest {
mMockContext, mMockContext,
mFakeGameClassifier, mFakeGameClassifier,
mMockActivityManager, mMockActivityManager,
mMockActivityManagerInternal,
mMockActivityTaskManager, mMockActivityTaskManager,
mMockWindowManagerService, mMockWindowManagerService,
mMockWindowManagerInternal, mMockWindowManagerInternal,
@@ -428,6 +445,214 @@ public final class GameServiceProviderInstanceImplTest {
verify(mMockWindowManagerInternal).addTrustedTaskOverlay(eq(10), eq(mockSurfacePackage10)); 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 @Test
public void taskSystemBarsListenerChanged_noAssociatedGameSession_doesNothing() { public void taskSystemBarsListenerChanged_noAssociatedGameSession_doesNothing() {
mGameServiceProviderInstance.start(); mGameServiceProviderInstance.start();
@@ -900,7 +1125,8 @@ public final class GameServiceProviderInstanceImplTest {
mFakeGameSessionService.getCapturedCreateInvocations()) mFakeGameSessionService.getCapturedCreateInvocations())
.mGameSessionController.restartGame(11); .mGameSessionController.restartGame(11);
verifyZeroInteractions(mMockActivityManager); verify(mMockActivityManager).registerProcessObserver(any());
verifyNoMoreInteractions(mMockActivityManager);
assertThat(mMockContext.getLastStartedIntent()).isNull(); assertThat(mMockContext.getLastStartedIntent()).isNull();
} }
@@ -932,7 +1158,6 @@ public final class GameServiceProviderInstanceImplTest {
dispatchTaskRemoved(taskId); dispatchTaskRemoved(taskId);
} }
private void dispatchTaskRemoved(int taskId) { private void dispatchTaskRemoved(int taskId) {
dispatchTaskChangeEvent(taskStackListener -> { dispatchTaskChangeEvent(taskStackListener -> {
taskStackListener.onTaskRemoved(taskId); 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<IProcessObserver> processObserverConsumer) {
for (IProcessObserver processObserver : mProcessObservers) {
processObserverConsumer.accept(processObserver);
}
}
private void mockPermissionGranted(String permission) { private void mockPermissionGranted(String permission) {
mMockContext.setPermission(permission, PackageManager.PERMISSION_GRANTED); mMockContext.setPermission(permission, PackageManager.PERMISSION_GRANTED);
} }