Merge "Tie game session lifetime to game process lifetime." into tm-dev am: 4359c9d3d5
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16979564 Change-Id: I2654fce666a5e1ef68f44928fc08daea426f4323
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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<Integer, GameSessionRecord> mGameSessions =
|
||||
new ConcurrentHashMap<>();
|
||||
@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;
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<IGameSessionService> mFakeGameSessionServiceConnector;
|
||||
private ArrayList<ITaskStackListener> mTaskStackListeners;
|
||||
private ArrayList<IProcessObserver> mProcessObservers;
|
||||
private ArrayList<TaskSystemBarsListener> mTaskSystemBarsListeners;
|
||||
private ArrayList<RunningTaskInfo> 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<IProcessObserver> processObserverConsumer) {
|
||||
for (IProcessObserver processObserver : mProcessObservers) {
|
||||
processObserverConsumer.accept(processObserver);
|
||||
}
|
||||
}
|
||||
|
||||
private void mockPermissionGranted(String permission) {
|
||||
mMockContext.setPermission(permission, PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user