Merge "Add support for rendering a View ontop of the GameSession's task."
This commit is contained in:
committed by
Android (Google) Code Review
commit
33abaa26fe
@@ -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 {
|
||||
|
||||
23
core/java/android/service/games/CreateGameSessionResult.aidl
Normal file
23
core/java/android/service/games/CreateGameSessionResult.aidl
Normal 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;
|
||||
84
core/java/android/service/games/CreateGameSessionResult.java
Normal file
84
core/java/android/service/games/CreateGameSessionResult.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user