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

Commit f59a1d1d authored by Tyler Lacey's avatar Tyler Lacey
Browse files

Tie game session lifetime to game process lifetime.

This change adds a process observer which watches for game process
activity and death. Process activity is tracked so it is known
which process is associated with which packages. When a process dies,
if it is the only process for a game with a game session, then the
game session is destroyed. If a process for that game is later seen
again, then a new game session is created.

Bug: 204504136
Test: Manual tests

Change-Id: Ic2a264c85867c17ab84de855240f2bba3e70a89d
parent 2d7e74c5
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.app;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.Intent;
@@ -51,6 +52,7 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide
                mContext,
                new GameClassifierImpl(mContext.getPackageManager()),
                ActivityManager.getService(),
                LocalServices.getService(ActivityManagerInternal.class),
                ActivityTaskManager.getService(),
                (WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE),
                LocalServices.getService(WindowManagerInternal.class),
+179 −5
Original line number Diff line number Diff line
@@ -21,9 +21,11 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager;
import android.app.IActivityManager;
import android.app.IActivityTaskManager;
import android.app.IProcessObserver;
import android.app.TaskStackListener;
import android.content.ComponentName;
import android.content.Context;
@@ -42,6 +44,7 @@ import android.service.games.IGameServiceController;
import android.service.games.IGameSession;
import android.service.games.IGameSessionController;
import android.service.games.IGameSessionService;
import android.text.TextUtils;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost.SurfacePackage;
@@ -136,12 +139,42 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
                GameServiceProviderInstanceImpl.this.onTaskFocusChanged(taskId, focused);
            });
        }
    };

    /**
     * The TaskStackListener declared above gives us good visibility into game task lifecycle.
     * However, it is possible for the Android system to kill all the processes associated with a
     * game task (e.g., when the system is under memory pressure or reaches a background process
     * limit). When this happens, the game task remains (and no TaskStackListener callbacks are
     * invoked), but we would nonetheless want to destroy a game session associated with the task
     * if this were to happen.
     *
     * This process observer gives us visibility into process lifecycles and lets us track all the
     * processes associated with each package so that any game sessions associated with the package
     * are destroyed if the process count for a given package reaches zero (most packages will
     * have at most one task). If processes for a given package are started up again, the destroyed
     * game sessions will be re-created.
     */
    private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
        @Override
        public void onForegroundActivitiesChanged(int pid, int uid, boolean fg) {
            // This callback is used to track how many processes are running for a given package.
            // Then, when a process dies, we will know if it was the only process running for that
            // package and the associated game sessions should be destroyed.
            mBackgroundExecutor.execute(() -> {
                GameServiceProviderInstanceImpl.this.onForegroundActivitiesChanged(pid);
            });
        }

        @Override
        public void onProcessDied(int pid, int uid) {
            mBackgroundExecutor.execute(() -> {
                GameServiceProviderInstanceImpl.this.onProcessDied(pid);
            });
        }

        // TODO(b/204503192): Limit the lifespan of the game session in the Game Service provider
        // to only when the associated task is running. Right now it is possible for a task to
        // move into the background and for all associated processes to die and for the Game Session
        // provider's GameSessionService to continue to be running. Ideally we could unbind the
        // service when this happens.
        @Override
        public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {}
    };

    private final IGameServiceController mGameServiceController =
@@ -185,6 +218,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
    private final Context mContext;
    private final GameClassifier mGameClassifier;
    private final IActivityManager mActivityManager;
    private final ActivityManagerInternal mActivityManagerInternal;
    private final IActivityTaskManager mActivityTaskManager;
    private final WindowManagerService mWindowManagerService;
    private final WindowManagerInternal mWindowManagerInternal;
@@ -195,6 +229,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
    private final ConcurrentHashMap<Integer, GameSessionRecord> mGameSessions =
            new ConcurrentHashMap<>();
    @GuardedBy("mLock")
    private final ConcurrentHashMap<Integer, String> mPidToPackageMap = new ConcurrentHashMap<>();
    @GuardedBy("mLock")
    private final ConcurrentHashMap<String, Integer> mPackageNameToProcessCountMap =
            new ConcurrentHashMap<>();

    @GuardedBy("mLock")
    private volatile boolean mIsRunning;

    GameServiceProviderInstanceImpl(
@@ -203,6 +243,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
            @NonNull Context context,
            @NonNull GameClassifier gameClassifier,
            @NonNull IActivityManager activityManager,
            @NonNull ActivityManagerInternal activityManagerInternal,
            @NonNull IActivityTaskManager activityTaskManager,
            @NonNull WindowManagerService windowManagerService,
            @NonNull WindowManagerInternal windowManagerInternal,
@@ -213,6 +254,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
        mContext = context;
        mGameClassifier = gameClassifier;
        mActivityManager = activityManager;
        mActivityManagerInternal = activityManagerInternal;
        mActivityTaskManager = activityTaskManager;
        mWindowManagerService = windowManagerService;
        mWindowManagerInternal = windowManagerInternal;
@@ -253,6 +295,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
            Slog.w(TAG, "Failed to register task stack listener", e);
        }

        try {
            mActivityManager.registerProcessObserver(mProcessObserver);
        } catch (RemoteException e) {
            Slog.w(TAG, "Failed to register process observer", e);
        }

        mWindowManagerInternal.registerTaskSystemBarsListener(mTaskSystemBarsVisibilityListener);
    }

@@ -263,6 +311,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
        }
        mIsRunning = false;

        try {
            mActivityManager.unregisterProcessObserver(mProcessObserver);
        } catch (RemoteException e) {
            Slog.w(TAG, "Failed to unregister process observer", e);
        }

        try {
            mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
        } catch (RemoteException e) {
@@ -586,6 +640,126 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
        }
    }

    private void onForegroundActivitiesChanged(int pid) {
        synchronized (mLock) {
            onForegroundActivitiesChangedLocked(pid);
        }
    }

    @GuardedBy("mLock")
    private void onForegroundActivitiesChangedLocked(int pid) {
        if (mPidToPackageMap.containsKey(pid)) {
            // We are already tracking this pid, nothing to do.
            return;
        }

        final String packageName = mActivityManagerInternal.getPackageNameByPid(pid);
        if (TextUtils.isEmpty(packageName)) {
            // Game processes should always have a package name.
            return;
        }

        if (!gameSessionExistsForPackageNameLocked(packageName)) {
            // We only need to track processes for tasks with game session records.
            return;
        }

        mPidToPackageMap.put(pid, packageName);
        final int processCountForPackage = mPackageNameToProcessCountMap.getOrDefault(packageName,
                0) + 1;
        mPackageNameToProcessCountMap.put(packageName, processCountForPackage);

        if (DEBUG) {
            Slog.d(TAG, "onForegroundActivitiesChangedLocked: tracking pid " + pid + ", for "
                    + packageName + ". Process count for package: " + processCountForPackage);
        }

        // If there are processes for the package, we may need to re-create game sessions
        // that are associated with the package
        if (processCountForPackage > 0) {
            recreateEndedGameSessionsLocked(packageName);
        }
    }

    @GuardedBy("mLock")
    private void recreateEndedGameSessionsLocked(String packageName) {
        for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
            if (gameSessionRecord.isGameSessionEndedForProcessDeath() && packageName.equals(
                    gameSessionRecord.getComponentName().getPackageName())) {
                if (DEBUG) {
                    Slog.d(TAG,
                            "recreateGameSessionsLocked(): re-creating game session for: "
                                    + packageName + " with taskId: "
                                    + gameSessionRecord.getTaskId());
                }

                final int taskId = gameSessionRecord.getTaskId();
                mGameSessions.put(taskId, GameSessionRecord.awaitingGameSessionRequest(taskId,
                        gameSessionRecord.getComponentName()));
                createGameSessionLocked(gameSessionRecord.getTaskId());
            }
        }
    }

    private void onProcessDied(int pid) {
        synchronized (mLock) {
            onProcessDiedLocked(pid);
        }
    }

    @GuardedBy("mLock")
    private void onProcessDiedLocked(int pid) {
        final String packageName = mPidToPackageMap.remove(pid);
        if (packageName == null) {
            // We weren't tracking this process.
            return;
        }

        final Integer oldProcessCountForPackage = mPackageNameToProcessCountMap.get(packageName);
        if (oldProcessCountForPackage == null) {
            // This should never happen; we should have a process count for all tracked packages.
            Slog.w(TAG, "onProcessDiedLocked(): Missing process count for package");
            return;
        }

        final int processCountForPackage = oldProcessCountForPackage - 1;
        mPackageNameToProcessCountMap.put(packageName, processCountForPackage);

        // If there are no more processes for the game, then we will terminate any game sessions
        // running for the package.
        if (processCountForPackage <= 0) {
            endGameSessionsForPackageLocked(packageName);
        }
    }

    @GuardedBy("mLock")
    private void endGameSessionsForPackageLocked(String packageName) {
        for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
            if (gameSessionRecord.getGameSession() != null && packageName.equals(
                    gameSessionRecord.getComponentName().getPackageName())) {
                if (DEBUG) {
                    Slog.d(TAG, "endGameSessionsForPackageLocked(): No more processes for "
                            + packageName + ", ending game session with taskId: "
                            + gameSessionRecord.getTaskId());
                }
                mGameSessions.put(gameSessionRecord.getTaskId(),
                        gameSessionRecord.withGameSessionEndedOnProcessDeath());
                destroyGameSessionFromRecordLocked(gameSessionRecord);
            }
        }
    }

    @GuardedBy("mLock")
    private boolean gameSessionExistsForPackageNameLocked(String packageName) {
        for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
            if (packageName.equals(gameSessionRecord.getComponentName().getPackageName())) {
                return true;
            }
        }

        return false;
    }

    @Nullable
    private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) {
        RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId);
+18 −0
Original line number Diff line number Diff line
@@ -35,6 +35,10 @@ final class GameSessionRecord {
        // A Game Session is created and attached.
        // GameSessionRecord.getGameSession() != null.
        GAME_SESSION_ATTACHED,
        // A Game Session did exist for a given game task but was destroyed because the last process
        // for the game died.
        // GameSessionRecord.getGameSession() == null.
        GAME_SESSION_ENDED_PROCESS_DEATH,
    }

    private final int mTaskId;
@@ -98,6 +102,20 @@ final class GameSessionRecord {
                surfacePackage);
    }

    @NonNull
    public GameSessionRecord withGameSessionEndedOnProcessDeath() {
        return new GameSessionRecord(
                mTaskId,
                State.GAME_SESSION_ENDED_PROCESS_DEATH,
                mRootComponentName,
                /* gameSession=*/ null,
                /* surfacePackage=*/ null);
    }

    public boolean isGameSessionEndedForProcessDeath() {
        return mState == State.GAME_SESSION_ENDED_PROCESS_DEATH;
    }

    @NonNull
    public int getTaskId() {
        return mTaskId;
+259 −3

File changed.

Preview size limit exceeded, changes collapsed.