diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java index a76eb8f1e55df..90b1b634f2ee4 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java @@ -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), diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java index b38195aed2508..a200067b8f3fc 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java @@ -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 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 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()); diff --git a/services/core/java/com/android/server/app/GameTaskInfo.java b/services/core/java/com/android/server/app/GameTaskInfo.java new file mode 100644 index 0000000000000..7548dbd06c5cc --- /dev/null +++ b/services/core/java/com/android/server/app/GameTaskInfo.java @@ -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); + } +} diff --git a/services/core/java/com/android/server/app/GameTaskInfoProvider.java b/services/core/java/com/android/server/app/GameTaskInfoProvider.java new file mode 100644 index 0000000000000..f078d98e5950d --- /dev/null +++ b/services/core/java/com/android/server/app/GameTaskInfoProvider.java @@ -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 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 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; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java index 319a769bb1dee..5b551b1c183ca 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java @@ -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) {