blob: 684d6a0fc5969495b9956f31de61a46aae360b8b [file] [log] [blame]
/*
* Copyright (C) 2021 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 static android.Manifest.permission.MANAGE_GAME_ACTIVITY;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
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;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
import android.net.Uri;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.games.CreateGameSessionRequest;
import android.service.games.CreateGameSessionResult;
import android.service.games.GameScreenshotResult;
import android.service.games.GameSessionViewHostConfiguration;
import android.service.games.GameStartedEvent;
import android.service.games.IGameService;
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;
import android.window.ScreenCapture;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ServiceConnector;
import com.android.internal.infra.ServiceConnector.ServiceLifecycleCallbacks;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerInternal.TaskSystemBarsListener;
import com.android.server.wm.WindowManagerService;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
final class GameServiceProviderInstanceImpl implements GameServiceProviderInstance {
private static final String TAG = "GameServiceProviderInstance";
private static final int CREATE_GAME_SESSION_TIMEOUT_MS = 10_000;
private static final boolean DEBUG = false;
private final ServiceLifecycleCallbacks<IGameService> mGameServiceLifecycleCallbacks =
new ServiceLifecycleCallbacks<IGameService>() {
@Override
public void onConnected(@NonNull IGameService service) {
try {
service.connected(mGameServiceController);
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to send connected event", ex);
}
}
};
private final ServiceLifecycleCallbacks<IGameSessionService>
mGameSessionServiceLifecycleCallbacks =
new ServiceLifecycleCallbacks<IGameSessionService>() {
@Override
public void onBinderDied() {
mBackgroundExecutor.execute(() -> {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "GameSessionService died. Destroying all sessions");
}
destroyAndClearAllGameSessionsLocked();
}
});
}
};
private final TaskSystemBarsListener mTaskSystemBarsVisibilityListener =
new TaskSystemBarsListener() {
@Override
public void onTransientSystemBarsVisibilityChanged(
int taskId,
boolean visible,
boolean wereRevealedFromSwipeOnSystemBar) {
GameServiceProviderInstanceImpl.this.onTransientSystemBarsVisibilityChanged(
taskId, visible, wereRevealedFromSwipeOnSystemBar);
}
};
private final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException {
if (componentName == null) {
return;
}
mBackgroundExecutor.execute(() -> {
GameServiceProviderInstanceImpl.this.onTaskCreated(taskId, componentName);
});
}
@Override
public void onTaskRemoved(int taskId) throws RemoteException {
mBackgroundExecutor.execute(() -> {
GameServiceProviderInstanceImpl.this.onTaskRemoved(taskId);
});
}
@Override
public void onTaskFocusChanged(int taskId, boolean focused) {
mBackgroundExecutor.execute(() -> {
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);
});
}
@Override
public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
}
};
private final IGameServiceController mGameServiceController =
new IGameServiceController.Stub() {
@Override
@EnforcePermission(MANAGE_GAME_ACTIVITY)
public void createGameSession(int taskId) {
super.createGameSession_enforcePermission();
mBackgroundExecutor.execute(() -> {
GameServiceProviderInstanceImpl.this.createGameSession(taskId);
});
}
};
private final IGameSessionController mGameSessionController =
new IGameSessionController.Stub() {
@Override
@EnforcePermission(MANAGE_GAME_ACTIVITY)
public void takeScreenshot(int taskId,
@NonNull AndroidFuture gameScreenshotResultFuture) {
super.takeScreenshot_enforcePermission();
mBackgroundExecutor.execute(() -> {
GameServiceProviderInstanceImpl.this.takeScreenshot(taskId,
gameScreenshotResultFuture);
});
}
@Override
@EnforcePermission(MANAGE_GAME_ACTIVITY)
public void restartGame(int taskId) {
super.restartGame_enforcePermission();
mBackgroundExecutor.execute(() -> {
GameServiceProviderInstanceImpl.this.restartGame(taskId);
});
}
};
private final Object mLock = new Object();
private final UserHandle mUserHandle;
private final Executor mBackgroundExecutor;
private final Context mContext;
private final GameTaskInfoProvider mGameTaskInfoProvider;
private final IActivityManager mActivityManager;
private final ActivityManagerInternal mActivityManagerInternal;
private final IActivityTaskManager mActivityTaskManager;
private final WindowManagerService mWindowManagerService;
private final WindowManagerInternal mWindowManagerInternal;
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final ScreenshotHelper mScreenshotHelper;
private final ServiceConnector<IGameService> mGameServiceConnector;
private final ServiceConnector<IGameSessionService> mGameSessionServiceConnector;
@GuardedBy("mLock")
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(
@NonNull UserHandle userHandle,
@NonNull Executor backgroundExecutor,
@NonNull Context context,
@NonNull GameTaskInfoProvider gameTaskInfoProvider,
@NonNull IActivityManager activityManager,
@NonNull ActivityManagerInternal activityManagerInternal,
@NonNull IActivityTaskManager activityTaskManager,
@NonNull WindowManagerService windowManagerService,
@NonNull WindowManagerInternal windowManagerInternal,
@NonNull ActivityTaskManagerInternal activityTaskManagerInternal,
@NonNull ServiceConnector<IGameService> gameServiceConnector,
@NonNull ServiceConnector<IGameSessionService> gameSessionServiceConnector,
@NonNull ScreenshotHelper screenshotHelper) {
mUserHandle = userHandle;
mBackgroundExecutor = backgroundExecutor;
mContext = context;
mGameTaskInfoProvider = gameTaskInfoProvider;
mActivityManager = activityManager;
mActivityManagerInternal = activityManagerInternal;
mActivityTaskManager = activityTaskManager;
mWindowManagerService = windowManagerService;
mWindowManagerInternal = windowManagerInternal;
mActivityTaskManagerInternal = activityTaskManagerInternal;
mGameServiceConnector = gameServiceConnector;
mGameSessionServiceConnector = gameSessionServiceConnector;
mScreenshotHelper = screenshotHelper;
}
@Override
public void start() {
synchronized (mLock) {
startLocked();
}
}
@Override
public void stop() {
synchronized (mLock) {
stopLocked();
}
}
@GuardedBy("mLock")
private void startLocked() {
if (mIsRunning) {
return;
}
mIsRunning = true;
mGameServiceConnector.setServiceLifecycleCallbacks(mGameServiceLifecycleCallbacks);
mGameSessionServiceConnector.setServiceLifecycleCallbacks(
mGameSessionServiceLifecycleCallbacks);
AndroidFuture<?> unusedConnectFuture = mGameServiceConnector.connect();
try {
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
} catch (RemoteException 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);
}
@GuardedBy("mLock")
private void stopLocked() {
if (!mIsRunning) {
return;
}
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) {
Slog.w(TAG, "Failed to unregister task stack listener", e);
}
mWindowManagerInternal.unregisterTaskSystemBarsListener(
mTaskSystemBarsVisibilityListener);
destroyAndClearAllGameSessionsLocked();
mGameServiceConnector.post(IGameService::disconnected).whenComplete((result, t) -> {
mGameServiceConnector.unbind();
});
mGameSessionServiceConnector.unbind();
mGameServiceConnector.setServiceLifecycleCallbacks(null);
mGameSessionServiceConnector.setServiceLifecycleCallbacks(null);
}
private void onTaskCreated(int taskId, @NonNull ComponentName componentName) {
final GameTaskInfo taskInfo = mGameTaskInfoProvider.get(taskId, componentName);
if (!taskInfo.mIsGameTask) {
return;
}
synchronized (mLock) {
gameTaskStartedLocked(taskInfo);
}
}
private void onTaskFocusChanged(int taskId, boolean focused) {
synchronized (mLock) {
onTaskFocusChangedLocked(taskId, focused);
}
}
@GuardedBy("mLock")
private void onTaskFocusChangedLocked(int taskId, boolean focused) {
if (DEBUG) {
Slog.d(TAG, "onTaskFocusChangedLocked() id: " + taskId + " focused: " + focused);
}
final GameSessionRecord gameSessionRecord = mGameSessions.get(taskId);
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;
}
try {
gameSessionRecord.getGameSession().onTaskFocusChanged(focused);
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to notify session of task focus change: " + gameSessionRecord);
}
}
@GuardedBy("mLock")
private void maybeCreateGameSessionForFocusedTaskLocked(int taskId) {
if (DEBUG) {
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(gameTaskInfo.mTaskId);
if (existingGameSessionRecord != null) {
Slog.w(TAG, "Existing game session found for task (id: " + gameTaskInfo.mTaskId
+ ") creation. Ignoring.");
return;
}
GameSessionRecord gameSessionRecord = GameSessionRecord.awaitingGameSessionRequest(
gameTaskInfo.mTaskId, gameTaskInfo.mComponentName);
mGameSessions.put(gameTaskInfo.mTaskId, gameSessionRecord);
AndroidFuture<Void> unusedPostGameStartedFuture = mGameServiceConnector.post(
gameService -> {
gameService.gameStarted(
new GameStartedEvent(gameTaskInfo.mTaskId,
gameTaskInfo.mComponentName.getPackageName()));
});
}
private void onTaskRemoved(int taskId) {
synchronized (mLock) {
boolean isTaskAssociatedWithGameSession = mGameSessions.containsKey(taskId);
if (!isTaskAssociatedWithGameSession) {
return;
}
if (DEBUG) {
Slog.i(TAG, "onTaskRemoved() id: " + taskId);
}
removeAndDestroyGameSessionIfNecessaryLocked(taskId);
}
}
private void onTransientSystemBarsVisibilityChanged(
int taskId,
boolean visible,
boolean wereRevealedFromSwipeOnSystemBar) {
if (visible && !wereRevealedFromSwipeOnSystemBar) {
return;
}
GameSessionRecord gameSessionRecord;
synchronized (mLock) {
gameSessionRecord = mGameSessions.get(taskId);
}
if (gameSessionRecord == null) {
return;
}
IGameSession gameSession = gameSessionRecord.getGameSession();
if (gameSession == null) {
return;
}
try {
gameSession.onTransientSystemBarVisibilityFromRevealGestureChanged(visible);
} catch (RemoteException ex) {
Slog.w(TAG,
"Failed to send transient system bars visibility from reveal gesture for task: "
+ taskId);
}
}
private void createGameSession(int taskId) {
synchronized (mLock) {
createGameSessionLocked(taskId);
}
}
@GuardedBy("mLock")
private void createGameSessionLocked(int taskId) {
if (DEBUG) {
Slog.i(TAG, "createGameSessionLocked() id: " + taskId);
}
if (!mIsRunning) {
return;
}
GameSessionRecord existingGameSessionRecord = mGameSessions.get(taskId);
if (existingGameSessionRecord == null) {
Slog.w(TAG, "No existing game session record found for task (id: " + taskId
+ ") creation. Ignoring.");
return;
}
if (!existingGameSessionRecord.isAwaitingGameSessionRequest()) {
Slog.w(TAG, "Existing game session for task (id: " + taskId
+ ") is not awaiting game session request. Ignoring.");
return;
}
GameSessionViewHostConfiguration gameSessionViewHostConfiguration =
createViewHostConfigurationForTask(taskId);
if (gameSessionViewHostConfiguration == null) {
Slog.w(TAG, "Failed to create view host configuration for task (id" + taskId
+ ") creation. Ignoring.");
return;
}
if (DEBUG) {
Slog.d(TAG, "Determined initial view host configuration for task (id: " + taskId + "): "
+ gameSessionViewHostConfiguration);
}
mGameSessions.put(taskId, existingGameSessionRecord.withGameSessionRequested());
AndroidFuture<CreateGameSessionResult> createGameSessionResultFuture =
new AndroidFuture<CreateGameSessionResult>()
.orTimeout(CREATE_GAME_SESSION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.whenCompleteAsync((createGameSessionResult, exception) -> {
if (exception != null || createGameSessionResult == null) {
Slog.w(TAG, "Failed to create GameSession: "
+ existingGameSessionRecord,
exception);
synchronized (mLock) {
removeAndDestroyGameSessionIfNecessaryLocked(taskId);
}
return;
}
synchronized (mLock) {
attachGameSessionLocked(taskId, createGameSessionResult);
}
// The TaskStackListener may have made its task focused call for the
// game session's task before the game session was created, so check if
// the task is already focused so that the game session can be notified.
setGameSessionFocusedIfNecessary(taskId,
createGameSessionResult.getGameSession());
}, mBackgroundExecutor);
AndroidFuture<Void> unusedPostCreateGameSessionFuture =
mGameSessionServiceConnector.post(gameSessionService -> {
CreateGameSessionRequest createGameSessionRequest =
new CreateGameSessionRequest(
taskId,
existingGameSessionRecord.getComponentName().getPackageName());
gameSessionService.create(
mGameSessionController,
createGameSessionRequest,
gameSessionViewHostConfiguration,
createGameSessionResultFuture);
});
}
private void setGameSessionFocusedIfNecessary(int taskId, IGameSession gameSession) {
try {
final ActivityTaskManager.RootTaskInfo rootTaskInfo =
mActivityTaskManager.getFocusedRootTaskInfo();
if (rootTaskInfo != null && rootTaskInfo.taskId == taskId) {
gameSession.onTaskFocusChanged(true);
}
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to set task focused for ID: " + taskId);
}
}
@GuardedBy("mLock")
private void attachGameSessionLocked(
int taskId,
@NonNull CreateGameSessionResult createGameSessionResult) {
if (DEBUG) {
Slog.d(TAG, "attachGameSession() id: " + taskId);
}
GameSessionRecord gameSessionRecord = mGameSessions.get(taskId);
if (gameSessionRecord == null) {
Slog.w(TAG, "No associated game session record. Destroying id: " + taskId);
destroyGameSessionDuringAttach(taskId, createGameSessionResult);
return;
}
if (!gameSessionRecord.isGameSessionRequested()) {
destroyGameSessionDuringAttach(taskId, createGameSessionResult);
return;
}
try {
mWindowManagerInternal.addTrustedTaskOverlay(
taskId,
createGameSessionResult.getSurfacePackage());
} catch (IllegalArgumentException ex) {
Slog.w(TAG, "Failed to add task overlay. Destroying id: " + taskId);
destroyGameSessionDuringAttach(taskId, createGameSessionResult);
return;
}
mGameSessions.put(taskId,
gameSessionRecord.withGameSession(
createGameSessionResult.getGameSession(),
createGameSessionResult.getSurfacePackage()));
}
@GuardedBy("mLock")
private void destroyAndClearAllGameSessionsLocked() {
for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
destroyGameSessionFromRecordLocked(gameSessionRecord);
}
mGameSessions.clear();
}
private void destroyGameSessionDuringAttach(
int taskId,
CreateGameSessionResult createGameSessionResult) {
try {
createGameSessionResult.getGameSession().onDestroyed();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + taskId);
}
}
@GuardedBy("mLock")
private void removeAndDestroyGameSessionIfNecessaryLocked(int taskId) {
if (DEBUG) {
Slog.d(TAG, "destroyGameSession() id: " + taskId);
}
GameSessionRecord gameSessionRecord = mGameSessions.remove(taskId);
if (gameSessionRecord == null) {
if (DEBUG) {
Slog.w(TAG, "No game session found for id: " + taskId);
}
return;
}
destroyGameSessionFromRecordLocked(gameSessionRecord);
}
@GuardedBy("mLock")
private void destroyGameSessionFromRecordLocked(@NonNull GameSessionRecord gameSessionRecord) {
SurfacePackage surfacePackage = gameSessionRecord.getSurfacePackage();
if (surfacePackage != null) {
try {
mWindowManagerInternal.removeTrustedTaskOverlay(
gameSessionRecord.getTaskId(),
surfacePackage);
} catch (IllegalArgumentException ex) {
Slog.i(TAG,
"Failed to remove task overlay. This is expected if the task is already "
+ "destroyed: "
+ gameSessionRecord);
}
}
IGameSession gameSession = gameSessionRecord.getGameSession();
if (gameSession != null) {
try {
gameSession.onDestroyed();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + gameSessionRecord, ex);
}
}
if (mGameSessions.isEmpty()) {
if (DEBUG) {
Slog.d(TAG, "No active game sessions. Disconnecting GameSessionService");
}
mGameSessionServiceConnector.unbind();
}
}
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.i(TAG, "endGameSessionsForPackageLocked(): No more processes for "
+ packageName + ", ending game session with taskId: "
+ gameSessionRecord.getTaskId());
}
RunningTaskInfo runningTaskInfo =
mGameTaskInfoProvider.getRunningTaskInfo(gameSessionRecord.getTaskId());
if (runningTaskInfo != null && (runningTaskInfo.isVisible)) {
if (DEBUG) {
Slog.i(TAG, "Found visible task. Ignoring end game session. taskId:"
+ gameSessionRecord.getTaskId());
}
continue;
}
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 = mGameTaskInfoProvider.getRunningTaskInfo(taskId);
if (runningTaskInfo == null) {
return null;
}
Rect bounds = runningTaskInfo.configuration.windowConfiguration.getBounds();
return new GameSessionViewHostConfiguration(
runningTaskInfo.displayId,
bounds.width(),
bounds.height());
}
@VisibleForTesting
void takeScreenshot(int taskId, @NonNull AndroidFuture callback) {
GameSessionRecord gameSessionRecord;
synchronized (mLock) {
gameSessionRecord = mGameSessions.get(taskId);
if (gameSessionRecord == null) {
Slog.w(TAG, "No game session found for id: " + taskId);
callback.complete(GameScreenshotResult.createInternalErrorResult());
return;
}
}
final SurfacePackage overlaySurfacePackage = gameSessionRecord.getSurfacePackage();
final SurfaceControl overlaySurfaceControl =
overlaySurfacePackage != null ? overlaySurfacePackage.getSurfaceControl() : null;
mBackgroundExecutor.execute(() -> {
final ScreenCapture.LayerCaptureArgs.Builder layerCaptureArgsBuilder =
new ScreenCapture.LayerCaptureArgs.Builder(/* layer */ null);
if (overlaySurfaceControl != null) {
SurfaceControl[] excludeLayers = new SurfaceControl[1];
excludeLayers[0] = overlaySurfaceControl;
layerCaptureArgsBuilder.setExcludeLayers(excludeLayers);
}
final Bitmap bitmap = mWindowManagerService.captureTaskBitmap(taskId,
layerCaptureArgsBuilder);
if (bitmap == null) {
Slog.w(TAG, "Could not get bitmap for id: " + taskId);
callback.complete(GameScreenshotResult.createInternalErrorResult());
} else {
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());
}
final Rect crop = runningTaskInfo.configuration.windowConfiguration.getBounds();
final Consumer<Uri> completionConsumer = (uri) -> {
if (uri == null) {
callback.complete(GameScreenshotResult.createInternalErrorResult());
} else {
callback.complete(GameScreenshotResult.createSuccessResult());
}
};
ScreenshotRequest request = new ScreenshotRequest.Builder(
TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(gameSessionRecord.getComponentName())
.setTaskId(taskId)
.setUserId(mUserHandle.getIdentifier())
.setBitmap(bitmap)
.setBoundsOnScreen(crop)
.setInsets(Insets.NONE)
.build();
mScreenshotHelper.takeScreenshot(
request, BackgroundThread.getHandler(), completionConsumer);
}
});
}
private void restartGame(int taskId) {
String packageName;
synchronized (mLock) {
GameSessionRecord gameSessionRecord = mGameSessions.get(taskId);
if (gameSessionRecord == null) {
return;
}
packageName = gameSessionRecord.getComponentName().getPackageName();
}
if (packageName == null) {
return;
}
mActivityTaskManagerInternal.restartTaskActivityProcessIfVisible(taskId, packageName);
}
}