If necessary, re-create game session when game task is focused.
This change is needed because the game session may have been destroyed when the game task was put in the background by pressing the back button, but not re-created when the game task was restrored via the Recents UI. Because looking up RunningTaskInfo and checking for game classification may be too expensive when run every time a task (game or otherwise) is focused, also introduce an LRU cache for game task info. Bug: 227364480 Test: Manual testing Change-Id: I49b310e50287cc898172ddee3c3f3b0e98176a12
This commit is contained in:
@@ -20,9 +20,11 @@ import android.annotation.NonNull;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManagerInternal;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.IActivityTaskManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
import android.service.games.GameService;
|
||||
import android.service.games.GameSessionService;
|
||||
import android.service.games.IGameService;
|
||||
@@ -47,14 +49,17 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide
|
||||
@Override
|
||||
public GameServiceProviderInstance create(
|
||||
@NonNull GameServiceComponentConfiguration configuration) {
|
||||
final UserHandle userHandle = configuration.getUserHandle();
|
||||
final IActivityTaskManager activityTaskManager = ActivityTaskManager.getService();
|
||||
return new GameServiceProviderInstanceImpl(
|
||||
configuration.getUserHandle(),
|
||||
userHandle,
|
||||
BackgroundThread.getExecutor(),
|
||||
mContext,
|
||||
new GameClassifierImpl(mContext.getPackageManager()),
|
||||
new GameTaskInfoProvider(userHandle, activityTaskManager,
|
||||
new GameClassifierImpl(mContext.getPackageManager())),
|
||||
ActivityManager.getService(),
|
||||
LocalServices.getService(ActivityManagerInternal.class),
|
||||
ActivityTaskManager.getService(),
|
||||
activityTaskManager,
|
||||
(WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE),
|
||||
LocalServices.getService(WindowManagerInternal.class),
|
||||
new GameServiceConnector(mContext, configuration),
|
||||
|
||||
@@ -64,7 +64,6 @@ import com.android.server.wm.WindowManagerInternal;
|
||||
import com.android.server.wm.WindowManagerInternal.TaskSystemBarsListener;
|
||||
import com.android.server.wm.WindowManagerService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -218,7 +217,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
private final UserHandle mUserHandle;
|
||||
private final Executor mBackgroundExecutor;
|
||||
private final Context mContext;
|
||||
private final GameClassifier mGameClassifier;
|
||||
private final GameTaskInfoProvider mGameTaskInfoProvider;
|
||||
private final IActivityManager mActivityManager;
|
||||
private final ActivityManagerInternal mActivityManagerInternal;
|
||||
private final IActivityTaskManager mActivityTaskManager;
|
||||
@@ -244,7 +243,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
@NonNull UserHandle userHandle,
|
||||
@NonNull Executor backgroundExecutor,
|
||||
@NonNull Context context,
|
||||
@NonNull GameClassifier gameClassifier,
|
||||
@NonNull GameTaskInfoProvider gameTaskInfoProvider,
|
||||
@NonNull IActivityManager activityManager,
|
||||
@NonNull ActivityManagerInternal activityManagerInternal,
|
||||
@NonNull IActivityTaskManager activityTaskManager,
|
||||
@@ -256,7 +255,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
mUserHandle = userHandle;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
mContext = context;
|
||||
mGameClassifier = gameClassifier;
|
||||
mGameTaskInfoProvider = gameTaskInfoProvider;
|
||||
mActivityManager = activityManager;
|
||||
mActivityManagerInternal = activityManagerInternal;
|
||||
mActivityTaskManager = activityTaskManager;
|
||||
@@ -344,13 +343,14 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
}
|
||||
|
||||
private void onTaskCreated(int taskId, @NonNull ComponentName componentName) {
|
||||
String packageName = componentName.getPackageName();
|
||||
if (!mGameClassifier.isGame(packageName, mUserHandle)) {
|
||||
final GameTaskInfo taskInfo = mGameTaskInfoProvider.get(taskId, componentName);
|
||||
|
||||
if (!taskInfo.mIsGameTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
gameTaskStartedLocked(taskId, componentName);
|
||||
gameTaskStartedLocked(taskInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,17 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
}
|
||||
|
||||
final GameSessionRecord gameSessionRecord = mGameSessions.get(taskId);
|
||||
if (gameSessionRecord == null || gameSessionRecord.getGameSession() == null) {
|
||||
if (gameSessionRecord == null) {
|
||||
if (focused) {
|
||||
// The game session for a game task may have been destroyed when the game task
|
||||
// was put into the background by pressing the back button. If the task is restored
|
||||
// via the Recents UI there will be no TaskStackListener#onCreated call for the
|
||||
// restoration, so this focus event is the first opportunity to re-create the game
|
||||
// session.
|
||||
maybeCreateGameSessionForFocusedTaskLocked(taskId);
|
||||
}
|
||||
return;
|
||||
} else if (gameSessionRecord.getGameSession() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -379,30 +389,50 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void gameTaskStartedLocked(int taskId, @NonNull ComponentName componentName) {
|
||||
private void maybeCreateGameSessionForFocusedTaskLocked(int taskId) {
|
||||
if (DEBUG) {
|
||||
Slog.i(TAG, "gameStartedLocked() id: " + taskId + " component: " + componentName);
|
||||
Slog.d(TAG, "maybeRecreateGameSessionForFocusedTaskLocked() id: " + taskId);
|
||||
}
|
||||
|
||||
final GameTaskInfo taskInfo = mGameTaskInfoProvider.get(taskId);
|
||||
if (taskInfo == null) {
|
||||
Slog.w(TAG, "No task info for focused task: " + taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!taskInfo.mIsGameTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
gameTaskStartedLocked(taskInfo);
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void gameTaskStartedLocked(@NonNull GameTaskInfo gameTaskInfo) {
|
||||
if (DEBUG) {
|
||||
Slog.i(TAG, "gameStartedLocked(): " + gameTaskInfo);
|
||||
}
|
||||
|
||||
if (!mIsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSessionRecord existingGameSessionRecord = mGameSessions.get(taskId);
|
||||
GameSessionRecord existingGameSessionRecord = mGameSessions.get(gameTaskInfo.mTaskId);
|
||||
if (existingGameSessionRecord != null) {
|
||||
Slog.w(TAG, "Existing game session found for task (id: " + taskId
|
||||
Slog.w(TAG, "Existing game session found for task (id: " + gameTaskInfo.mTaskId
|
||||
+ ") creation. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameSessionRecord gameSessionRecord = GameSessionRecord.awaitingGameSessionRequest(
|
||||
taskId, componentName);
|
||||
mGameSessions.put(taskId, gameSessionRecord);
|
||||
gameTaskInfo.mTaskId, gameTaskInfo.mComponentName);
|
||||
mGameSessions.put(gameTaskInfo.mTaskId, gameSessionRecord);
|
||||
|
||||
AndroidFuture<Void> unusedPostGameStartedFuture = mGameServiceConnector.post(
|
||||
gameService -> {
|
||||
gameService.gameStarted(
|
||||
new GameStartedEvent(taskId, componentName.getPackageName()));
|
||||
new GameStartedEvent(gameTaskInfo.mTaskId,
|
||||
gameTaskInfo.mComponentName.getPackageName()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -769,7 +799,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
|
||||
@Nullable
|
||||
private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) {
|
||||
RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId);
|
||||
RunningTaskInfo runningTaskInfo = mGameTaskInfoProvider.getRunningTaskInfo(taskId);
|
||||
if (runningTaskInfo == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -781,28 +811,6 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
bounds.height());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RunningTaskInfo getRunningTaskInfoForTask(int taskId) {
|
||||
List<RunningTaskInfo> runningTaskInfos;
|
||||
try {
|
||||
runningTaskInfos = mActivityTaskManager.getTasks(
|
||||
/* maxNum= */ Integer.MAX_VALUE,
|
||||
/* filterOnlyVisibleRecents= */ true,
|
||||
/* keepIntentExtra= */ false);
|
||||
} catch (RemoteException ex) {
|
||||
Slog.w(TAG, "Failed to fetch running tasks");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (RunningTaskInfo taskInfo : runningTaskInfos) {
|
||||
if (taskInfo.taskId == taskId) {
|
||||
return taskInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void takeScreenshot(int taskId, @NonNull AndroidFuture callback) {
|
||||
GameSessionRecord gameSessionRecord;
|
||||
@@ -834,7 +842,8 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
|
||||
} else {
|
||||
final Bundle bundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(
|
||||
bitmap);
|
||||
final RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId);
|
||||
final RunningTaskInfo runningTaskInfo =
|
||||
mGameTaskInfoProvider.getRunningTaskInfo(taskId);
|
||||
if (runningTaskInfo == null) {
|
||||
Slog.w(TAG, "Could not get running task info for id: " + taskId);
|
||||
callback.complete(GameScreenshotResult.createInternalErrorResult());
|
||||
|
||||
66
services/core/java/com/android/server/app/GameTaskInfo.java
Normal file
66
services/core/java/com/android/server/app/GameTaskInfo.java
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.app;
|
||||
|
||||
import android.content.ComponentName;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class GameTaskInfo {
|
||||
final int mTaskId;
|
||||
final boolean mIsGameTask;
|
||||
final ComponentName mComponentName;
|
||||
|
||||
GameTaskInfo(int taskId, boolean isGameTask, ComponentName componentName) {
|
||||
mTaskId = taskId;
|
||||
mIsGameTask = isGameTask;
|
||||
mComponentName = componentName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GameTaskInfo{"
|
||||
+ "mTaskId="
|
||||
+ mTaskId
|
||||
+ ", mIsGameTask="
|
||||
+ mIsGameTask
|
||||
+ ", mComponentName="
|
||||
+ mComponentName
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof GameTaskInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GameTaskInfo that = (GameTaskInfo) o;
|
||||
return mTaskId == that.mTaskId
|
||||
&& mIsGameTask == that.mIsGameTask
|
||||
&& mComponentName.equals(that.mComponentName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mTaskId, mIsGameTask, mComponentName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.app;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityManager.RunningTaskInfo;
|
||||
import android.app.IActivityTaskManager;
|
||||
import android.content.ComponentName;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.LruCache;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class GameTaskInfoProvider {
|
||||
private static final String TAG = "GameTaskInfoProvider";
|
||||
private static final int TASK_INFO_CACHE_MAX_SIZE = 50;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final LruCache<Integer, GameTaskInfo> mGameTaskInfoCache = new LruCache<>(
|
||||
TASK_INFO_CACHE_MAX_SIZE);
|
||||
|
||||
private final UserHandle mUserHandle;
|
||||
private final IActivityTaskManager mActivityTaskManager;
|
||||
private final GameClassifier mGameClassifier;
|
||||
|
||||
GameTaskInfoProvider(@NonNull UserHandle userHandle,
|
||||
@NonNull IActivityTaskManager activityTaskManager,
|
||||
@NonNull GameClassifier gameClassifier) {
|
||||
mUserHandle = userHandle;
|
||||
mActivityTaskManager = activityTaskManager;
|
||||
mGameClassifier = gameClassifier;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
GameTaskInfo get(int taskId) {
|
||||
synchronized (mLock) {
|
||||
final GameTaskInfo cachedTaskInfo = mGameTaskInfoCache.get(taskId);
|
||||
if (cachedTaskInfo != null) {
|
||||
return cachedTaskInfo;
|
||||
}
|
||||
}
|
||||
|
||||
final RunningTaskInfo runningTaskInfo = getRunningTaskInfo(taskId);
|
||||
if (runningTaskInfo == null || runningTaskInfo.baseActivity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return generateGameInfo(taskId, runningTaskInfo.baseActivity);
|
||||
}
|
||||
|
||||
GameTaskInfo get(int taskId, @NonNull ComponentName componentName) {
|
||||
synchronized (mLock) {
|
||||
final GameTaskInfo cachedTaskInfo = mGameTaskInfoCache.get(taskId);
|
||||
if (cachedTaskInfo != null) {
|
||||
if (cachedTaskInfo.mComponentName.equals(componentName)) {
|
||||
Slog.w(TAG, "Found cached task info for taskId " + taskId
|
||||
+ " but cached component name " + cachedTaskInfo.mComponentName
|
||||
+ " does not match " + componentName);
|
||||
} else {
|
||||
return cachedTaskInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return generateGameInfo(taskId, componentName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
RunningTaskInfo getRunningTaskInfo(int taskId) {
|
||||
List<RunningTaskInfo> runningTaskInfos;
|
||||
try {
|
||||
runningTaskInfos = mActivityTaskManager.getTasks(
|
||||
/* maxNum= */ Integer.MAX_VALUE,
|
||||
/* filterOnlyVisibleRecents= */ false,
|
||||
/* keepIntentExtra= */ false);
|
||||
} catch (RemoteException ex) {
|
||||
Slog.w(TAG, "Failed to fetch running tasks");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (RunningTaskInfo taskInfo : runningTaskInfos) {
|
||||
if (taskInfo.taskId == taskId) {
|
||||
return taskInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private GameTaskInfo generateGameInfo(int taskId, @NonNull ComponentName componentName) {
|
||||
final GameTaskInfo gameTaskInfo = new GameTaskInfo(taskId,
|
||||
mGameClassifier.isGame(componentName.getPackageName(), mUserHandle), componentName);
|
||||
|
||||
synchronized (mLock) {
|
||||
mGameTaskInfoCache.put(taskId, gameTaskInfo);
|
||||
}
|
||||
|
||||
return gameTaskInfo;
|
||||
}
|
||||
}
|
||||
@@ -216,11 +216,12 @@ public final class GameServiceProviderInstanceImplTest {
|
||||
mRunningTaskInfos);
|
||||
|
||||
|
||||
final UserHandle userHandle = new UserHandle(USER_ID);
|
||||
mGameServiceProviderInstance = new GameServiceProviderInstanceImpl(
|
||||
new UserHandle(USER_ID),
|
||||
userHandle,
|
||||
ConcurrentUtils.DIRECT_EXECUTOR,
|
||||
mMockContext,
|
||||
mFakeGameClassifier,
|
||||
new GameTaskInfoProvider(userHandle, mMockActivityTaskManager, mFakeGameClassifier),
|
||||
mMockActivityManager,
|
||||
mMockActivityManagerInternal,
|
||||
mMockActivityTaskManager,
|
||||
@@ -787,6 +788,36 @@ public final class GameServiceProviderInstanceImplTest {
|
||||
assertThat(gameSession10.mIsDestroyed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gameTaskFocusedWithCreateAfterRemoved_gameSessionRecreated() 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));
|
||||
|
||||
stopTask(10);
|
||||
|
||||
assertThat(gameSession10.mIsDestroyed).isTrue();
|
||||
|
||||
// If the game task is restored via the Recents UI, the task will be running again but
|
||||
// we would not expect any call to TaskStackListener#onTaskCreated.
|
||||
addRunningTaskInfo(10, GAME_A_MAIN_ACTIVITY);
|
||||
|
||||
// We now receive a task focused event for the task. This will occur if the game task is
|
||||
// restored via the Recents UI.
|
||||
dispatchTaskFocused(10, /*focused=*/ true);
|
||||
mFakeGameService.requestCreateGameSession(10);
|
||||
|
||||
// Verify that a new pending game session is created for the game's taskId.
|
||||
assertNotNull(mFakeGameSessionService.removePendingFutureForTaskId(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gameTaskRemoved_removesTaskOverlay() throws Exception {
|
||||
mGameServiceProviderInstance.start();
|
||||
@@ -1144,13 +1175,18 @@ public final class GameServiceProviderInstanceImplTest {
|
||||
}
|
||||
|
||||
private void startTask(int taskId, ComponentName componentName) {
|
||||
addRunningTaskInfo(taskId, componentName);
|
||||
|
||||
dispatchTaskCreated(taskId, componentName);
|
||||
}
|
||||
|
||||
private void addRunningTaskInfo(int taskId, ComponentName componentName) {
|
||||
RunningTaskInfo runningTaskInfo = new RunningTaskInfo();
|
||||
runningTaskInfo.taskId = taskId;
|
||||
runningTaskInfo.baseActivity = componentName;
|
||||
runningTaskInfo.displayId = 1;
|
||||
runningTaskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 500, 800));
|
||||
mRunningTaskInfos.add(runningTaskInfo);
|
||||
|
||||
dispatchTaskCreated(taskId, componentName);
|
||||
}
|
||||
|
||||
private void stopTask(int taskId) {
|
||||
|
||||
Reference in New Issue
Block a user