Merge "Tie game session lifetime to game process lifetime." into tm-dev
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user