Merge "Add support for rendering a View ontop of the GameSession's task."

This commit is contained in:
David Samuelson
2022-01-20 23:16:07 +00:00
committed by Android (Google) Code Review
12 changed files with 935 additions and 478 deletions

View File

@@ -10905,6 +10905,7 @@ package android.service.games {
ctor public GameSession();
method public void onCreate();
method public void onDestroy();
method public void setTaskOverlayView(@NonNull android.view.View, @NonNull android.view.ViewGroup.LayoutParams);
}
public abstract class GameSessionService extends android.app.Service {

View File

@@ -0,0 +1,23 @@
/*
* 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 android.service.games;
/**
* @hide
*/
parcelable CreateGameSessionResult;

View File

@@ -0,0 +1,84 @@
/*
* 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 android.service.games;
import android.annotation.Hide;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.SurfaceControlViewHost;
/**
* Internal result object that contains the successful creation of a game session.
*
* @see IGameSessionService#create(CreateGameSessionRequest, GameSessionViewHostConfiguration,
* com.android.internal.infra.AndroidFuture)
* @hide
*/
@Hide
public final class CreateGameSessionResult implements Parcelable {
@NonNull
public static final Parcelable.Creator<CreateGameSessionResult> CREATOR =
new Parcelable.Creator<CreateGameSessionResult>() {
@Override
public CreateGameSessionResult createFromParcel(Parcel source) {
return new CreateGameSessionResult(
IGameSession.Stub.asInterface(source.readStrongBinder()),
source.readParcelable(
SurfaceControlViewHost.SurfacePackage.class.getClassLoader(),
SurfaceControlViewHost.SurfacePackage.class));
}
@Override
public CreateGameSessionResult[] newArray(int size) {
return new CreateGameSessionResult[0];
}
};
private final IGameSession mGameSession;
private final SurfaceControlViewHost.SurfacePackage mSurfacePackage;
public CreateGameSessionResult(
@NonNull IGameSession gameSession,
@NonNull SurfaceControlViewHost.SurfacePackage surfacePackage) {
mGameSession = gameSession;
mSurfacePackage = surfacePackage;
}
@NonNull
public IGameSession getGameSession() {
return mGameSession;
}
@NonNull
public SurfaceControlViewHost.SurfacePackage getSurfacePackage() {
return mSurfacePackage;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeStrongBinder(mGameSession.asBinder());
dest.writeParcelable(mSurfacePackage, flags);
}
}

View File

@@ -16,8 +16,17 @@
package android.service.games;
import android.annotation.Hide;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Handler;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -41,12 +50,29 @@ public abstract class GameSession {
}
};
private GameSessionRootView mGameSessionRootView;
private SurfaceControlViewHost mSurfaceControlViewHost;
@Hide
void attach(
@NonNull Context context,
@NonNull SurfaceControlViewHost surfaceControlViewHost,
int widthPx,
int heightPx) {
mSurfaceControlViewHost = surfaceControlViewHost;
mGameSessionRootView = new GameSessionRootView(context, mSurfaceControlViewHost);
surfaceControlViewHost.setView(mGameSessionRootView, widthPx, heightPx);
}
@Hide
void doCreate() {
onCreate();
}
@Hide
void doDestroy() {
onDestroy();
mSurfaceControlViewHost.release();
}
/**
@@ -54,12 +80,57 @@ public abstract class GameSession {
*
* This should be used perform any setup required now that the game session is created.
*/
public void onCreate() {}
public void onCreate() {
}
/**
* Finalizer called when the game session is ending.
*
* This should be used to perform any cleanup before the game session is destroyed.
*/
public void onDestroy() {}
public void onDestroy() {
}
/**
* Sets the task overlay content to an explicit view. This view is placed directly into the game
* session's task overlay view hierarchy. It can itself be a complex view hierarchy. The size
* the task overlay view will always match the dimensions of the associated task's window. The
* {@code View} may not be cleared once set, but may be replaced by invoking
* {@link #setTaskOverlayView(View, ViewGroup.LayoutParams)} again.
*
* @param view The desired content to display.
* @param layoutParams Layout parameters for the view.
*/
public void setTaskOverlayView(
@NonNull View view,
@NonNull ViewGroup.LayoutParams layoutParams) {
mGameSessionRootView.removeAllViews();
mGameSessionRootView.addView(view, layoutParams);
}
/**
* Root view of the {@link SurfaceControlViewHost} associated with the {@link GameSession}
* instance. It is responsible for observing changes in the size of the window and resizing
* itself to match.
*/
private static final class GameSessionRootView extends FrameLayout {
private final SurfaceControlViewHost mSurfaceControlViewHost;
GameSessionRootView(@NonNull Context context,
SurfaceControlViewHost surfaceControlViewHost) {
super(context);
mSurfaceControlViewHost = surfaceControlViewHost;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// TODO(b/204504596): Investigate skipping the relayout in cases where the size has
// not changed.
Rect bounds = newConfig.windowConfiguration.getBounds();
mSurfaceControlViewHost.relayout(bounds.width(), bounds.height());
}
}
}

View File

@@ -22,8 +22,12 @@ import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.view.Display;
import android.view.SurfaceControlViewHost;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -62,15 +66,26 @@ public abstract class GameSessionService extends Service {
private final IGameSessionService mInterface = new IGameSessionService.Stub() {
@Override
public void create(CreateGameSessionRequest createGameSessionRequest,
public void create(
CreateGameSessionRequest createGameSessionRequest,
GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
AndroidFuture gameSessionFuture) {
Handler.getMain().post(PooledLambda.obtainRunnable(
GameSessionService::doCreate, GameSessionService.this,
createGameSessionRequest,
gameSessionViewHostConfiguration,
gameSessionFuture));
}
};
private DisplayManager mDisplayManager;
@Override
public void onCreate() {
super.onCreate();
mDisplayManager = this.getSystemService(DisplayManager.class);
}
@Override
@Nullable
public final IBinder onBind(@Nullable Intent intent) {
@@ -85,12 +100,36 @@ public abstract class GameSessionService extends Service {
return mInterface.asBinder();
}
private void doCreate(CreateGameSessionRequest createGameSessionRequest,
AndroidFuture<IBinder> gameSessionFuture) {
private void doCreate(
CreateGameSessionRequest createGameSessionRequest,
GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
AndroidFuture<CreateGameSessionResult> createGameSessionResultFuture) {
GameSession gameSession = onNewSession(createGameSessionRequest);
Objects.requireNonNull(gameSession);
gameSessionFuture.complete(gameSession.mInterface.asBinder());
Display display = mDisplayManager.getDisplay(gameSessionViewHostConfiguration.mDisplayId);
if (display == null) {
createGameSessionResultFuture.completeExceptionally(
new IllegalStateException("No display found for id: "
+ gameSessionViewHostConfiguration.mDisplayId));
return;
}
IBinder hostToken = new Binder();
SurfaceControlViewHost surfaceControlViewHost =
new SurfaceControlViewHost(this, display, hostToken);
gameSession.attach(this,
surfaceControlViewHost,
gameSessionViewHostConfiguration.mWidthPx,
gameSessionViewHostConfiguration.mHeightPx);
CreateGameSessionResult createGameSessionResult =
new CreateGameSessionResult(gameSession.mInterface,
surfaceControlViewHost.getSurfacePackage());
createGameSessionResultFuture.complete(createGameSessionResult);
gameSession.doCreate();
}

View File

@@ -0,0 +1,22 @@
/*
* 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 android.service.games;
/**
* @hide
*/
parcelable GameSessionViewHostConfiguration;

View File

@@ -0,0 +1,96 @@
/*
* 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 android.service.games;
import android.annotation.Hide;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Objects;
/**
* Represents the configuration of the {@link android.view.SurfaceControlViewHost} used to render
* the overlay for a game session.
*
* @hide
*/
@Hide
public final class GameSessionViewHostConfiguration implements Parcelable {
@NonNull
public static final Creator<GameSessionViewHostConfiguration> CREATOR =
new Creator<GameSessionViewHostConfiguration>() {
@Override
public GameSessionViewHostConfiguration createFromParcel(Parcel source) {
return new GameSessionViewHostConfiguration(
source.readInt(),
source.readInt(),
source.readInt());
}
@Override
public GameSessionViewHostConfiguration[] newArray(int size) {
return new GameSessionViewHostConfiguration[0];
}
};
final int mDisplayId;
final int mWidthPx;
final int mHeightPx;
public GameSessionViewHostConfiguration(int displayId, int widthPx, int heightPx) {
this.mDisplayId = displayId;
this.mWidthPx = widthPx;
this.mHeightPx = heightPx;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mDisplayId);
dest.writeInt(mWidthPx);
dest.writeInt(mHeightPx);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GameSessionViewHostConfiguration)) return false;
GameSessionViewHostConfiguration that = (GameSessionViewHostConfiguration) o;
return mDisplayId == that.mDisplayId && mWidthPx == that.mWidthPx
&& mHeightPx == that.mHeightPx;
}
@Override
public int hashCode() {
return Objects.hash(mDisplayId, mWidthPx, mHeightPx);
}
@Override
public String toString() {
return "GameSessionViewHostConfiguration{"
+ "mDisplayId=" + mDisplayId
+ ", mWidthPx=" + mWidthPx
+ ", mHeightPx=" + mHeightPx
+ '}';
}
}

View File

@@ -18,6 +18,7 @@ package android.service.games;
import android.service.games.IGameSession;
import android.service.games.CreateGameSessionRequest;
import android.service.games.GameSessionViewHostConfiguration;
import com.android.internal.infra.AndroidFuture;
@@ -28,5 +29,6 @@ import com.android.internal.infra.AndroidFuture;
oneway interface IGameSessionService {
void create(
in CreateGameSessionRequest createGameSessionRequest,
in AndroidFuture /* T=IBinder for IGameSession */ gameSessionFuture);
in GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
in AndroidFuture /* T=CreateGameSessionResult */ createGameSessionResultFuture);
}

View File

@@ -27,6 +27,8 @@ import android.service.games.IGameSessionService;
import com.android.internal.infra.ServiceConnector;
import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
import com.android.server.wm.WindowManagerInternal;
final class GameServiceProviderInstanceFactoryImpl implements GameServiceProviderInstanceFactory {
private final Context mContext;
@@ -44,6 +46,7 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide
BackgroundThread.getExecutor(),
new GameClassifierImpl(mContext.getPackageManager()),
ActivityTaskManager.getService(),
LocalServices.getService(WindowManagerInternal.class),
new GameServiceConnector(mContext, gameServiceProviderConfiguration),
new GameSessionServiceConnector(mContext, gameServiceProviderConfiguration));
}

View File

@@ -17,24 +17,31 @@
package com.android.server.app;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.IActivityTaskManager;
import android.app.TaskStackListener;
import android.content.ComponentName;
import android.os.IBinder;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.games.CreateGameSessionRequest;
import android.service.games.CreateGameSessionResult;
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.IGameSessionService;
import android.util.Slog;
import android.view.SurfaceControlViewHost.SurfacePackage;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ServiceConnector;
import com.android.server.wm.WindowManagerInternal;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@@ -62,6 +69,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
GameServiceProviderInstanceImpl.this.onTaskRemoved(taskId);
});
}
// 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.
};
private final IGameServiceController mGameServiceController =
@@ -79,6 +92,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
private final Executor mBackgroundExecutor;
private final GameClassifier mGameClassifier;
private final IActivityTaskManager mActivityTaskManager;
private final WindowManagerInternal mWindowManagerInternal;
private final ServiceConnector<IGameService> mGameServiceConnector;
private final ServiceConnector<IGameSessionService> mGameSessionServiceConnector;
@@ -89,16 +103,18 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
private volatile boolean mIsRunning;
GameServiceProviderInstanceImpl(
UserHandle userHandle,
@NonNull UserHandle userHandle,
@NonNull Executor backgroundExecutor,
@NonNull GameClassifier gameClassifier,
@NonNull IActivityTaskManager activityTaskManager,
@NonNull WindowManagerInternal windowManagerInternal,
@NonNull ServiceConnector<IGameService> gameServiceConnector,
@NonNull ServiceConnector<IGameSessionService> gameSessionServiceConnector) {
mUserHandle = userHandle;
mBackgroundExecutor = backgroundExecutor;
mGameClassifier = gameClassifier;
mActivityTaskManager = activityTaskManager;
mWindowManagerInternal = windowManagerInternal;
mGameServiceConnector = gameServiceConnector;
mGameSessionServiceConnector = gameSessionServiceConnector;
}
@@ -151,16 +167,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
IGameSession gameSession = gameSessionRecord.getGameSession();
if (gameSession == null) {
continue;
}
try {
gameSession.destroy();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + gameSessionRecord, ex);
}
destroyGameSessionFromRecord(gameSessionRecord);
}
mGameSessions.clear();
@@ -186,30 +193,30 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
@GuardedBy("mLock")
private void gameTaskStartedLocked(int sessionId, @NonNull ComponentName componentName) {
private void gameTaskStartedLocked(int taskId, @NonNull ComponentName componentName) {
if (DEBUG) {
Slog.i(TAG, "gameStartedLocked() id: " + sessionId + " component: " + componentName);
Slog.i(TAG, "gameStartedLocked() id: " + taskId + " component: " + componentName);
}
if (!mIsRunning) {
return;
}
GameSessionRecord existingGameSessionRecord = mGameSessions.get(sessionId);
GameSessionRecord existingGameSessionRecord = mGameSessions.get(taskId);
if (existingGameSessionRecord != null) {
Slog.w(TAG, "Existing game session found for task (id: " + sessionId
Slog.w(TAG, "Existing game session found for task (id: " + taskId
+ ") creation. Ignoring.");
return;
}
GameSessionRecord gameSessionRecord = GameSessionRecord.awaitingGameSessionRequest(
sessionId, componentName);
mGameSessions.put(sessionId, gameSessionRecord);
taskId, componentName);
mGameSessions.put(taskId, gameSessionRecord);
AndroidFuture<Void> unusedPostGameStartedFuture = mGameServiceConnector.post(
gameService -> {
gameService.gameStarted(
new GameStartedEvent(sessionId, componentName.getPackageName()));
new GameStartedEvent(taskId, componentName.getPackageName()));
});
}
@@ -220,7 +227,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
return;
}
destroyGameSessionIfNecessaryLocked(taskId);
removeAndDestroyGameSessionIfNecessaryLocked(taskId);
}
}
@@ -231,107 +238,151 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
@GuardedBy("mLock")
private void createGameSessionLocked(int sessionId) {
private void createGameSessionLocked(int taskId) {
if (DEBUG) {
Slog.i(TAG, "createGameSessionLocked() id: " + sessionId);
Slog.i(TAG, "createGameSessionLocked() id: " + taskId);
}
if (!mIsRunning) {
return;
}
GameSessionRecord existingGameSessionRecord = mGameSessions.get(sessionId);
GameSessionRecord existingGameSessionRecord = mGameSessions.get(taskId);
if (existingGameSessionRecord == null) {
Slog.w(TAG, "No existing game session record found for task (id: " + sessionId
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: " + sessionId
Slog.w(TAG, "Existing game session for task (id: " + taskId
+ ") is not awaiting game session request. Ignoring.");
return;
}
mGameSessions.put(sessionId, existingGameSessionRecord.withGameSessionRequested());
ComponentName componentName = existingGameSessionRecord.getComponentName();
GameSessionViewHostConfiguration gameSessionViewHostConfiguration =
createViewHostConfigurationForTask(taskId);
if (gameSessionViewHostConfiguration == null) {
Slog.w(TAG, "Failed to create view host configuration for task (id" + taskId
+ ") creation. Ignoring.");
return;
}
// TODO(b/207035150): Allow the game service provider to determine if a game session
// should be created. For now we will assume all games should have a session.
AndroidFuture<IBinder> gameSessionFuture = new AndroidFuture<IBinder>()
.orTimeout(CREATE_GAME_SESSION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.whenCompleteAsync((gameSessionIBinder, exception) -> {
IGameSession gameSession = IGameSession.Stub.asInterface(gameSessionIBinder);
if (exception != null || gameSession == null) {
Slog.w(TAG, "Failed to create GameSession: " + existingGameSessionRecord,
exception);
synchronized (mLock) {
destroyGameSessionIfNecessaryLocked(sessionId);
}
return;
}
if (DEBUG) {
Slog.d(TAG, "Determined initial view host configuration for task (id: " + taskId + "): "
+ gameSessionViewHostConfiguration);
}
synchronized (mLock) {
attachGameSessionLocked(sessionId, gameSession);
}
}, mBackgroundExecutor);
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);
}
}, mBackgroundExecutor);
AndroidFuture<Void> unusedPostCreateGameSessionFuture =
mGameSessionServiceConnector.post(gameService -> {
CreateGameSessionRequest createGameSessionRequest =
new CreateGameSessionRequest(sessionId, componentName.getPackageName());
gameService.create(createGameSessionRequest, gameSessionFuture);
new CreateGameSessionRequest(
taskId,
existingGameSessionRecord.getComponentName().getPackageName());
gameService.create(
createGameSessionRequest,
gameSessionViewHostConfiguration,
createGameSessionResultFuture);
});
}
@GuardedBy("mLock")
private void attachGameSessionLocked(int sessionId, @NonNull IGameSession gameSession) {
private void attachGameSessionLocked(
int taskId,
@NonNull CreateGameSessionResult createGameSessionResult) {
if (DEBUG) {
Slog.i(TAG, "attachGameSession() id: " + sessionId);
Slog.d(TAG, "attachGameSession() id: " + taskId);
}
GameSessionRecord gameSessionRecord = mGameSessions.get(sessionId);
boolean isValidAttachRequest = true;
GameSessionRecord gameSessionRecord = mGameSessions.get(taskId);
if (gameSessionRecord == null) {
Slog.w(TAG, "No associated game session record. Destroying id: " + sessionId);
isValidAttachRequest = false;
}
if (gameSessionRecord != null && !gameSessionRecord.isGameSessionRequested()) {
Slog.w(TAG,
"Game session not requested for existing game session record. Destroying id: "
+ sessionId);
isValidAttachRequest = false;
}
if (!isValidAttachRequest) {
try {
gameSession.destroy();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + gameSessionRecord, ex);
}
Slog.w(TAG, "No associated game session record. Destroying id: " + taskId);
destroyGameSessionDuringAttach(taskId, createGameSessionResult);
return;
}
mGameSessions.put(sessionId, gameSessionRecord.withGameSession(gameSession));
if (!gameSessionRecord.isGameSessionRequested()) {
destroyGameSessionDuringAttach(taskId, createGameSessionResult);
return;
}
try {
mWindowManagerInternal.addTaskOverlay(
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()));
}
private void destroyGameSessionDuringAttach(
int taskId,
CreateGameSessionResult createGameSessionResult) {
try {
createGameSessionResult.getGameSession().destroy();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + taskId);
}
}
@GuardedBy("mLock")
private void destroyGameSessionIfNecessaryLocked(int sessionId) {
// 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.
private void removeAndDestroyGameSessionIfNecessaryLocked(int taskId) {
if (DEBUG) {
Slog.i(TAG, "destroyGameSession() id: " + sessionId);
Slog.d(TAG, "destroyGameSession() id: " + taskId);
}
GameSessionRecord gameSessionRecord = mGameSessions.remove(sessionId);
GameSessionRecord gameSessionRecord = mGameSessions.remove(taskId);
if (gameSessionRecord == null) {
if (DEBUG) {
Slog.w(TAG, "No game session found for id: " + sessionId);
Slog.w(TAG, "No game session found for id: " + taskId);
}
return;
}
destroyGameSessionFromRecord(gameSessionRecord);
}
private void destroyGameSessionFromRecord(@NonNull GameSessionRecord gameSessionRecord) {
SurfacePackage surfacePackage = gameSessionRecord.getSurfacePackage();
if (surfacePackage != null) {
try {
mWindowManagerInternal.removeTaskOverlay(
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) {
@@ -344,7 +395,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
if (mGameSessions.isEmpty()) {
if (DEBUG) {
Slog.i(TAG, "No active game sessions. Disconnecting GameSessionService");
Slog.d(TAG, "No active game sessions. Disconnecting GameSessionService");
}
if (mGameSessionServiceConnector != null) {
@@ -352,4 +403,41 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
}
}
@Nullable
private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) {
RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId);
if (runningTaskInfo == null) {
return null;
}
Rect bounds = runningTaskInfo.configuration.windowConfiguration.getBounds();
return new GameSessionViewHostConfiguration(
runningTaskInfo.displayId,
bounds.width(),
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;
}
}

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.service.games.IGameSession;
import android.view.SurfaceControlViewHost.SurfacePackage;
import java.util.Objects;
@@ -37,26 +38,34 @@ final class GameSessionRecord {
}
private final int mTaskId;
private final State mState;
private final ComponentName mRootComponentName;
@Nullable
private final IGameSession mIGameSession;
private final State mState;
@Nullable
private final SurfacePackage mSurfacePackage;
static GameSessionRecord awaitingGameSessionRequest(int taskId,
ComponentName rootComponentName) {
return new GameSessionRecord(taskId, rootComponentName, /* gameSession= */ null,
State.NO_GAME_SESSION_REQUESTED);
return new GameSessionRecord(
taskId,
State.NO_GAME_SESSION_REQUESTED,
rootComponentName,
/* gameSession= */ null,
/* surfacePackage= */ null);
}
private GameSessionRecord(
int taskId,
@NonNull State state,
@NonNull ComponentName rootComponentName,
@Nullable IGameSession gameSession,
@NonNull State state) {
@Nullable SurfacePackage surfacePackage) {
this.mTaskId = taskId;
this.mState = state;
this.mRootComponentName = rootComponentName;
this.mIGameSession = gameSession;
this.mState = state;
this.mSurfacePackage = surfacePackage;
}
public boolean isAwaitingGameSessionRequest() {
@@ -65,8 +74,12 @@ final class GameSessionRecord {
@NonNull
public GameSessionRecord withGameSessionRequested() {
return new GameSessionRecord(mTaskId, mRootComponentName, /* gameSession=*/ null,
State.GAME_SESSION_REQUESTED);
return new GameSessionRecord(
mTaskId,
State.GAME_SESSION_REQUESTED,
mRootComponentName,
/* gameSession=*/ null,
/* surfacePackage=*/ null);
}
public boolean isGameSessionRequested() {
@@ -74,15 +87,20 @@ final class GameSessionRecord {
}
@NonNull
public GameSessionRecord withGameSession(@NonNull IGameSession gameSession) {
public GameSessionRecord withGameSession(
@NonNull IGameSession gameSession,
@NonNull SurfacePackage surfacePackage) {
Objects.requireNonNull(gameSession);
return new GameSessionRecord(mTaskId, mRootComponentName, gameSession,
State.GAME_SESSION_ATTACHED);
return new GameSessionRecord(mTaskId,
State.GAME_SESSION_ATTACHED,
mRootComponentName,
gameSession,
surfacePackage);
}
@Nullable
public IGameSession getGameSession() {
return mIGameSession;
@NonNull
public int getTaskId() {
return mTaskId;
}
@NonNull
@@ -90,17 +108,29 @@ final class GameSessionRecord {
return mRootComponentName;
}
@Nullable
public IGameSession getGameSession() {
return mIGameSession;
}
@Nullable
public SurfacePackage getSurfacePackage() {
return mSurfacePackage;
}
@Override
public String toString() {
return "GameSessionRecord{"
+ "mTaskId="
+ mTaskId
+ ", mState="
+ mState
+ ", mRootComponentName="
+ mRootComponentName
+ ", mIGameSession="
+ mIGameSession
+ ", mState="
+ mState
+ ", mSurfacePackage="
+ mSurfacePackage
+ '}';
}
@@ -115,12 +145,16 @@ final class GameSessionRecord {
}
GameSessionRecord that = (GameSessionRecord) o;
return mTaskId == that.mTaskId && mRootComponentName.equals(that.mRootComponentName)
&& Objects.equals(mIGameSession, that.mIGameSession) && mState == that.mState;
return mTaskId == that.mTaskId
&& mState == that.mState
&& mRootComponentName.equals(that.mRootComponentName)
&& Objects.equals(mIGameSession, that.mIGameSession)
&& Objects.equals(mSurfacePackage, that.mSurfacePackage);
}
@Override
public int hashCode() {
return Objects.hash(mTaskId, mRootComponentName, mIGameSession, mState);
return Objects.hash(
mTaskId, mState, mRootComponentName, mIGameSession, mState, mSurfacePackage);
}
}