Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2fa6e18c authored by Tyler Lacey's avatar Tyler Lacey
Browse files

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
parent 765bbec6
Loading
Loading
Loading
Loading
+8 −3
Original line number Diff line number Diff line
@@ -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),
+48 −39
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+121 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+40 −4
Original line number Diff line number Diff line
@@ -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) {