From 19519e58a3ce27e517093f4da6e3c151e265e8c4 Mon Sep 17 00:00:00 2001 From: David Samuelson Date: Mon, 10 Jan 2022 00:41:34 +0000 Subject: [PATCH] Add support for rendering a View ontop of the GameSession's task. Test: Manual e2e testing Bug: 202414447 Bug: 202417255 Bug: 204504596 CTS-Coverage-Bug: 206128693 Change-Id: Ie3d0d74511bede4cffc18fdbfe558491797bc2d2 --- core/api/system-current.txt | 1 + .../games/CreateGameSessionResult.aidl | 23 + .../games/CreateGameSessionResult.java | 84 ++ .../android/service/games/GameSession.java | 75 +- .../service/games/GameSessionService.java | 47 +- .../GameSessionViewHostConfiguration.aidl | 22 + .../GameSessionViewHostConfiguration.java | 96 +++ .../service/games/IGameSessionService.aidl | 4 +- ...ameServiceProviderInstanceFactoryImpl.java | 3 + .../app/GameServiceProviderInstanceImpl.java | 244 ++++-- .../android/server/app/GameSessionRecord.java | 70 +- .../GameServiceProviderInstanceImplTest.java | 744 +++++++++--------- 12 files changed, 935 insertions(+), 478 deletions(-) create mode 100644 core/java/android/service/games/CreateGameSessionResult.aidl create mode 100644 core/java/android/service/games/CreateGameSessionResult.java create mode 100644 core/java/android/service/games/GameSessionViewHostConfiguration.aidl create mode 100644 core/java/android/service/games/GameSessionViewHostConfiguration.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2269750f1c7b3..73d333a6d7139 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10896,6 +10896,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 { diff --git a/core/java/android/service/games/CreateGameSessionResult.aidl b/core/java/android/service/games/CreateGameSessionResult.aidl new file mode 100644 index 0000000000000..b7c5e16028917 --- /dev/null +++ b/core/java/android/service/games/CreateGameSessionResult.aidl @@ -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; diff --git a/core/java/android/service/games/CreateGameSessionResult.java b/core/java/android/service/games/CreateGameSessionResult.java new file mode 100644 index 0000000000000..8448b0f433b27 --- /dev/null +++ b/core/java/android/service/games/CreateGameSessionResult.java @@ -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 CREATOR = + new Parcelable.Creator() { + @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); + } +} diff --git a/core/java/android/service/games/GameSession.java b/core/java/android/service/games/GameSession.java index 0ff08c08932bc..1a5331f105257 100644 --- a/core/java/android/service/games/GameSession.java +++ b/core/java/android/service/games/GameSession.java @@ -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()); + } + } } diff --git a/core/java/android/service/games/GameSessionService.java b/core/java/android/service/games/GameSessionService.java index c1a3eb5286c4b..195a0f233307b 100644 --- a/core/java/android/service/games/GameSessionService.java +++ b/core/java/android/service/games/GameSessionService.java @@ -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 gameSessionFuture) { + private void doCreate( + CreateGameSessionRequest createGameSessionRequest, + GameSessionViewHostConfiguration gameSessionViewHostConfiguration, + AndroidFuture 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(); } diff --git a/core/java/android/service/games/GameSessionViewHostConfiguration.aidl b/core/java/android/service/games/GameSessionViewHostConfiguration.aidl new file mode 100644 index 0000000000000..b900b9d09b07c --- /dev/null +++ b/core/java/android/service/games/GameSessionViewHostConfiguration.aidl @@ -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; diff --git a/core/java/android/service/games/GameSessionViewHostConfiguration.java b/core/java/android/service/games/GameSessionViewHostConfiguration.java new file mode 100644 index 0000000000000..53db0dfae8b23 --- /dev/null +++ b/core/java/android/service/games/GameSessionViewHostConfiguration.java @@ -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 CREATOR = + new Creator() { + @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 + + '}'; + } +} diff --git a/core/java/android/service/games/IGameSessionService.aidl b/core/java/android/service/games/IGameSessionService.aidl index 2a53ea7f8e4ac..dcbcbc16a3744 100644 --- a/core/java/android/service/games/IGameSessionService.aidl +++ b/core/java/android/service/games/IGameSessionService.aidl @@ -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); } diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java index d5ac03ab7c0d5..48e66b6c6aeb5 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java @@ -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)); } diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java index cc060e94a52ac..31eb8c1c9429b 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java @@ -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 mGameServiceConnector; private final ServiceConnector 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 gameServiceConnector, @NonNull ServiceConnector 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 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 gameSessionFuture = new AndroidFuture() - .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 createGameSessionResultFuture = + new AndroidFuture() + .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 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 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; + } } diff --git a/services/core/java/com/android/server/app/GameSessionRecord.java b/services/core/java/com/android/server/app/GameSessionRecord.java index e9daceb0cd393..a241812f7868d 100644 --- a/services/core/java/com/android/server/app/GameSessionRecord.java +++ b/services/core/java/com/android/server/app/GameSessionRecord.java @@ -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); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java index 0d513bb2d68c8..167090693a101 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java @@ -18,29 +18,38 @@ package com.android.server.app; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.app.GameServiceProviderInstanceImplTest.FakeGameService.GameServiceState; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.app.ActivityManager.RunningTaskInfo; import android.app.IActivityTaskManager; import android.app.ITaskStackListener; import android.content.ComponentName; import android.content.pm.PackageManager; -import android.os.IBinder; +import android.graphics.Rect; import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; 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.view.SurfaceControlViewHost.SurfacePackage; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -48,20 +57,20 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.infra.AndroidFuture; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.FunctionalUtils.ThrowingConsumer; +import com.android.internal.util.Preconditions; +import com.android.server.wm.WindowManagerInternal; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; +import java.util.HashMap; /** @@ -72,6 +81,9 @@ import java.util.function.Supplier; @Presubmit public final class GameServiceProviderInstanceImplTest { + private static final GameSessionViewHostConfiguration + DEFAULT_GAME_SESSION_VIEW_HOST_CONFIGURATION = + new GameSessionViewHostConfiguration(1, 500, 800); private static final int USER_ID = 10; private static final String APP_A_PACKAGE = "com.package.app.a"; private static final ComponentName APP_A_MAIN_ACTIVITY = @@ -86,14 +98,14 @@ public final class GameServiceProviderInstanceImplTest { @Mock private IActivityTaskManager mMockActivityTaskManager; @Mock - private IGameService mMockGameService; - @Mock - private IGameSessionService mMockGameSessionService; + private WindowManagerInternal mMockWindowManagerInternal; private FakeGameClassifier mFakeGameClassifier; + private FakeGameService mFakeGameService; private FakeServiceConnector mFakeGameServiceConnector; + private FakeGameSessionService mFakeGameSessionService; private FakeServiceConnector mFakeGameSessionServiceConnector; private ArrayList mTaskStackListeners; - private InOrder mInOrder; + private ArrayList mRunningTaskInfos; @Before public void setUp() throws PackageManager.NameNotFoundException, RemoteException { @@ -102,13 +114,13 @@ public final class GameServiceProviderInstanceImplTest { .strictness(Strictness.LENIENT) .startMocking(); - mInOrder = inOrder(mMockGameService, mMockGameSessionService); - mFakeGameClassifier = new FakeGameClassifier(); mFakeGameClassifier.recordGamePackage(GAME_A_PACKAGE); - mFakeGameServiceConnector = new FakeServiceConnector<>(mMockGameService); - mFakeGameSessionServiceConnector = new FakeServiceConnector<>(mMockGameSessionService); + mFakeGameService = new FakeGameService(); + mFakeGameServiceConnector = new FakeServiceConnector<>(mFakeGameService); + mFakeGameSessionService = new FakeGameSessionService(); + mFakeGameSessionServiceConnector = new FakeServiceConnector<>(mFakeGameSessionService); mTaskStackListeners = new ArrayList<>(); doAnswer(invocation -> { @@ -116,6 +128,10 @@ public final class GameServiceProviderInstanceImplTest { return null; }).when(mMockActivityTaskManager).registerTaskStackListener(any()); + mRunningTaskInfos = new ArrayList<>(); + when(mMockActivityTaskManager.getTasks(anyInt(), anyBoolean(), anyBoolean())).thenReturn( + mRunningTaskInfos); + doAnswer(invocation -> { mTaskStackListeners.remove(invocation.getArgument(0)); return null; @@ -126,6 +142,7 @@ public final class GameServiceProviderInstanceImplTest { ConcurrentUtils.DIRECT_EXECUTOR, mFakeGameClassifier, mMockActivityTaskManager, + mMockWindowManagerInternal, mFakeGameServiceConnector, mFakeGameSessionServiceConnector); } @@ -139,8 +156,7 @@ public final class GameServiceProviderInstanceImplTest { public void start_startsGameSession() throws Exception { mGameServiceProviderInstance.start(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verifyNoMoreInteractions(); + assertThat(mFakeGameService.getState()).isEqualTo(GameServiceState.CONNECTED); assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); @@ -149,9 +165,9 @@ public final class GameServiceProviderInstanceImplTest { @Test public void start_multipleTimes_startsGameSessionOnce() throws Exception { mGameServiceProviderInstance.start(); + mGameServiceProviderInstance.start(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verifyNoMoreInteractions(); + assertThat(mFakeGameService.getState()).isEqualTo(GameServiceState.CONNECTED); assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); @@ -161,9 +177,10 @@ public final class GameServiceProviderInstanceImplTest { public void stop_neverStarted_doesNothing() throws Exception { mGameServiceProviderInstance.stop(); + + assertThat(mFakeGameService.getState()).isEqualTo(GameServiceState.DISCONNECTED); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(0); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); - mInOrder.verifyNoMoreInteractions(); } @Test @@ -171,9 +188,8 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verifyNoMoreInteractions(); + assertThat(mFakeGameService.getState()).isEqualTo(GameServiceState.DISCONNECTED); + assertThat(mFakeGameService.getConnectedCount()).isEqualTo(1); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); @@ -187,11 +203,8 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verifyNoMoreInteractions(); + assertThat(mFakeGameService.getState()).isEqualTo(GameServiceState.DISCONNECTED); + assertThat(mFakeGameService.getConnectedCount()).isEqualTo(2); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(2); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); @@ -203,9 +216,8 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.stop(); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verifyNoMoreInteractions(); + assertThat(mFakeGameService.getState()).isEqualTo(GameServiceState.DISCONNECTED); + assertThat(mFakeGameService.getConnectedCount()).isEqualTo(1); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); @@ -215,7 +227,6 @@ public final class GameServiceProviderInstanceImplTest { public void gameTaskStarted_neverStarted_doesNothing() throws Exception { dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); - mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(0); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); } @@ -224,35 +235,25 @@ public final class GameServiceProviderInstanceImplTest { public void gameTaskRemoved_neverStarted_doesNothing() throws Exception { dispatchTaskRemoved(10); - mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(0); assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); } @Test - public void gameTaskStarted_afterStopped_doesNothing() throws Exception { + public void gameTaskStarted_afterStopped_doesNotSendGameStartedEvent() throws Exception { mGameServiceProviderInstance.start(); mGameServiceProviderInstance.stop(); dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verifyNoMoreInteractions(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); + assertThat(mFakeGameService.getGameStartedEvents()).isEmpty(); } @Test - public void appTaskStarted_doesNothing() throws Exception { + public void appTaskStarted_doesNotSendGameStartedEvent() throws Exception { mGameServiceProviderInstance.start(); dispatchTaskCreated(10, APP_A_MAIN_ACTIVITY); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verifyNoMoreInteractions(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); + assertThat(mFakeGameService.getGameStartedEvents()).isEmpty(); } @Test @@ -260,26 +261,17 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); dispatchTaskCreated(10, null); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verifyNoMoreInteractions(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); + assertThat(mFakeGameService.getGameStartedEvents()).isEmpty(); } @Test - public void gameSessionRequested_withoutTaskDispatch_ignoredAndDoesNotCrash() throws Exception { + public void gameSessionRequested_withoutTaskDispatch_doesNotCrashAndDoesNotCreateGameSession() + throws Exception { mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - controllerArgumentCaptor.getValue().createGameSession(10); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verifyNoMoreInteractions(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); + mFakeGameService.requestCreateGameSession(10); + + assertThat(mFakeGameSessionService.getCapturedCreateInvocations()).isEmpty(); } @Test @@ -287,427 +279,323 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verifyNoMoreInteractions(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(0); + GameStartedEvent expectedGameStartedEvent = new GameStartedEvent(10, GAME_A_PACKAGE); + assertThat(mFakeGameService.getGameStartedEvents()) + .containsExactly(expectedGameStartedEvent).inOrder(); + } + + @Test + public void gameTaskStarted_requestToCreateGameSessionIncludesTaskConfiguration() + throws Exception { + mGameServiceProviderInstance.start(); + startTask(10, GAME_A_MAIN_ACTIVITY); + + mFakeGameService.requestCreateGameSession(10); + + FakeGameSessionService.CapturedCreateInvocation capturedCreateInvocation = + getOnlyElement(mFakeGameSessionService.getCapturedCreateInvocations()); + assertThat(capturedCreateInvocation.mGameSessionViewHostConfiguration) + .isEqualTo(DEFAULT_GAME_SESSION_VIEW_HOST_CONFIGURATION); + } + + @Test + public void gameTaskStarted_failsToDetermineTaskOverlayConfiguration_gameSessionNotCreated() + throws Exception { + mGameServiceProviderInstance.start(); + dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + + mFakeGameService.requestCreateGameSession(10); + + assertThat(mFakeGameSessionService.getCapturedCreateInvocations()).isEmpty(); } @Test public void gameTaskStartedAndSessionRequested_createsGameSession() throws Exception { - CreateGameSessionRequest createGameSessionRequest = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest), any()); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isFalse(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); } @Test public void gameTaskStartedAndSessionRequested_secondSessionRequest_ignoredAndDoesNotCrash() throws Exception { - CreateGameSessionRequest createGameSessionRequest = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); + startTask(10, GAME_A_MAIN_ACTIVITY); - controllerArgumentCaptor.getValue().createGameSession(10); + mFakeGameService.requestCreateGameSession(10); + mFakeGameService.requestCreateGameSession(10); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest), any()); - mInOrder.verifyNoMoreInteractions(); - assertThat(gameSession10.mIsDestroyed).isFalse(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); + CreateGameSessionRequest expectedCreateGameSessionRequest = new CreateGameSessionRequest(10, + GAME_A_PACKAGE); + assertThat(getOnlyElement( + mFakeGameSessionService.getCapturedCreateInvocations()).mCreateGameSessionRequest) + .isEqualTo(expectedCreateGameSessionRequest); + } + + @Test + public void gameSessionSuccessfullyCreated_createsTaskOverlay() throws Exception { + mGameServiceProviderInstance.start(); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + verify(mMockWindowManagerInternal).addTaskOverlay(eq(10), eq(mockSurfacePackage10)); + verifyNoMoreInteractions(mMockWindowManagerInternal); } @Test public void gameTaskRemoved_whileAwaitingGameSessionAttached_destroysGameSession() throws Exception { - CreateGameSessionRequest createGameSessionRequest = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - dispatchTaskRemoved(10); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest), any()); - mInOrder.verifyNoMoreInteractions(); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + dispatchTaskRemoved(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + assertThat(gameSession10.mIsDestroyed).isTrue(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isFalse(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); } @Test - public void gameTaskRemoved_destroysGameSession() throws Exception { - CreateGameSessionRequest createGameSessionRequest = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest); - + public void gameTaskRemoved_whileGameSessionAttached_destroysGameSession() throws Exception { mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); + + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + dispatchTaskRemoved(10); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest), any()); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isTrue(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isFalse(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); + } + + @Test + public void gameTaskRemoved_removesTaskOverlay() throws Exception { + mGameServiceProviderInstance.start(); + + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + stopTask(10); + + verify(mMockWindowManagerInternal).addTaskOverlay(eq(10), eq(mockSurfacePackage10)); + verify(mMockWindowManagerInternal).removeTaskOverlay(eq(10), eq(mockSurfacePackage10)); + verifyNoMoreInteractions(mMockWindowManagerInternal); } @Test public void gameTaskStartedAndSessionRequested_multipleTimes_createsMultipleGameSessions() throws Exception { - CreateGameSessionRequest createGameSessionRequest10 = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest10); - - CreateGameSessionRequest createGameSessionRequest11 = - new CreateGameSessionRequest(11, GAME_A_PACKAGE); - Supplier> gameSession11Future = - captureCreateGameSessionFuture(createGameSessionRequest11); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); - dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession11 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession11); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + startTask(11, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest10), any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(11, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest11), any()); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isFalse(); assertThat(gameSession11.mIsDestroyed).isFalse(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); } @Test - public void gameTaskStartedTwice_sessionRequestedSecondTimeOnly_createsOneGameSessions() + public void gameTaskStartedTwice_sessionRequestedSecondTimeOnly_createsOneGameSession() throws Exception { - CreateGameSessionRequest createGameSessionRequest11 = - new CreateGameSessionRequest(11, GAME_A_PACKAGE); - Supplier> gameSession11Future = - captureCreateGameSessionFuture(createGameSessionRequest11); - - // The game task is started twice, but a session is requested only for the second one. mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); - dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession11 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession11); + startTask(10, GAME_A_MAIN_ACTIVITY); + startTask(11, GAME_A_MAIN_ACTIVITY); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(11, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest11), any()); - mInOrder.verifyNoMoreInteractions(); - assertThat(gameSession11.mIsDestroyed).isFalse(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); - assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + assertThat(gameSession10.mIsDestroyed).isFalse(); + assertThat(mFakeGameSessionService.getCapturedCreateInvocations()).hasSize(1); } @Test - public void gameTaskRemoved_afterMultipleCreated_destroysOnlyThatGameSession() + public void gameTaskRemoved_multipleSessions_destroysOnlyThatGameSession() throws Exception { - CreateGameSessionRequest createGameSessionRequest10 = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest10); - - CreateGameSessionRequest createGameSessionRequest11 = - new CreateGameSessionRequest(11, GAME_A_PACKAGE); - Supplier> gameSession11Future = - captureCreateGameSessionFuture(createGameSessionRequest11); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); - dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession11 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession11); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + startTask(11, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); dispatchTaskRemoved(10); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest10), any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(11, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest11), any()); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isTrue(); assertThat(gameSession11.mIsDestroyed).isFalse(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); } @Test - public void allGameTasksRemoved_destroysAllGameSessions() throws Exception { - CreateGameSessionRequest createGameSessionRequest10 = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest10); - - CreateGameSessionRequest createGameSessionRequest11 = - new CreateGameSessionRequest(11, GAME_A_PACKAGE); - Supplier> gameSession11Future = - captureCreateGameSessionFuture(createGameSessionRequest11); - + public void allGameTasksRemoved_destroysAllGameSessionsAndGameSessionServiceIsDisconnected() { mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); - dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession11 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession11); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + startTask(11, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); dispatchTaskRemoved(10); dispatchTaskRemoved(11); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest10), any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(11, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest11), any()); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isTrue(); assertThat(gameSession11.mIsDestroyed).isTrue(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isFalse(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); } @Test - public void gameTasksCreatedAndSessionsReq_afterAllPreviousSessionsDestroyed_createsSession() + public void createSessionRequested_afterAllPreviousSessionsDestroyed_createsSession() throws Exception { - CreateGameSessionRequest createGameSessionRequest10 = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest10); - - CreateGameSessionRequest createGameSessionRequest11 = - new CreateGameSessionRequest(11, GAME_A_PACKAGE); - Supplier> gameSession11Future = - captureCreateGameSessionFuture(createGameSessionRequest11); - - CreateGameSessionRequest createGameSessionRequest12 = - new CreateGameSessionRequest(12, GAME_A_PACKAGE); - Supplier> unusedGameSession12Future = - captureCreateGameSessionFuture(createGameSessionRequest12); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); - dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession11 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession11); + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + startTask(11, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); dispatchTaskRemoved(10); dispatchTaskRemoved(11); - dispatchTaskCreatedAndTriggerSessionRequest(12, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession12 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession12); + startTask(12, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(12); + + FakeGameSession gameSession12 = new FakeGameSession(); + SurfacePackage mockSurfacePackage12 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(12) + .complete(new CreateGameSessionResult(gameSession12, mockSurfacePackage12)); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest10), any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(11, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest11), any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(12, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest12), any()); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isTrue(); assertThat(gameSession11.mIsDestroyed).isTrue(); assertThat(gameSession12.mIsDestroyed).isFalse(); - assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isTrue(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(2); } @Test public void stop_severalActiveGameSessions_destroysGameSessionsAndUnbinds() throws Exception { - CreateGameSessionRequest createGameSessionRequest10 = - new CreateGameSessionRequest(10, GAME_A_PACKAGE); - Supplier> gameSession10Future = - captureCreateGameSessionFuture(createGameSessionRequest10); - - CreateGameSessionRequest createGameSessionRequest11 = - new CreateGameSessionRequest(11, GAME_A_PACKAGE); - Supplier> gameSession11Future = - captureCreateGameSessionFuture(createGameSessionRequest11); - mGameServiceProviderInstance.start(); - ArgumentCaptor controllerArgumentCaptor = ArgumentCaptor.forClass( - IGameServiceController.class); - verify(mMockGameService).connected(controllerArgumentCaptor.capture()); - dispatchTaskCreatedAndTriggerSessionRequest(10, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession10 = new IGameSessionStub(); - gameSession10Future.get().complete(gameSession10); - dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, - controllerArgumentCaptor.getValue()); - IGameSessionStub gameSession11 = new IGameSessionStub(); - gameSession11Future.get().complete(gameSession11); + + startTask(10, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(10); + + FakeGameSession gameSession10 = new FakeGameSession(); + SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(10) + .complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10)); + + startTask(11, GAME_A_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); + mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(10, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest10), any()); - mInOrder.verify(mMockGameService).gameStarted( - eq(new GameStartedEvent(11, GAME_A_PACKAGE))); - mInOrder.verify(mMockGameSessionService).create(eq(createGameSessionRequest11), any()); - mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verifyNoMoreInteractions(); assertThat(gameSession10.mIsDestroyed).isTrue(); assertThat(gameSession11.mIsDestroyed).isTrue(); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); - assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isFalse(); - assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1); } - private Supplier> captureCreateGameSessionFuture( - CreateGameSessionRequest expectedCreateGameSessionRequest) throws Exception { - final AtomicReference> gameSessionFuture = new AtomicReference<>(); - doAnswer(invocation -> { - gameSessionFuture.set(invocation.getArgument(1)); - return null; - }).when(mMockGameSessionService).create(eq(expectedCreateGameSessionRequest), any()); + private void startTask(int taskId, ComponentName componentName) { + RunningTaskInfo runningTaskInfo = new RunningTaskInfo(); + runningTaskInfo.taskId = taskId; + runningTaskInfo.displayId = 1; + runningTaskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 500, 800)); + mRunningTaskInfos.add(runningTaskInfo); - return gameSessionFuture::get; + dispatchTaskCreated(taskId, componentName); } + private void stopTask(int taskId) { + mRunningTaskInfos.removeIf(runningTaskInfo -> runningTaskInfo.taskId == taskId); + dispatchTaskRemoved(taskId); + } + + private void dispatchTaskRemoved(int taskId) { dispatchTaskChangeEvent(taskStackListener -> { taskStackListener.onTaskRemoved(taskId); }); } - private void dispatchTaskCreatedAndTriggerSessionRequest(int taskId, - @Nullable ComponentName componentName, IGameServiceController gameServiceController) - throws Exception { - dispatchTaskCreated(taskId, componentName); - gameServiceController.createGameSession(taskId); - } - private void dispatchTaskCreated(int taskId, @Nullable ComponentName componentName) { dispatchTaskChangeEvent(taskStackListener -> { taskStackListener.onTaskCreated(taskId, componentName); @@ -721,7 +609,113 @@ public final class GameServiceProviderInstanceImplTest { } } - private static class IGameSessionStub extends IGameSession.Stub { + static final class FakeGameService extends IGameService.Stub { + private IGameServiceController mGameServiceController; + + public enum GameServiceState { + DISCONNECTED, + CONNECTED, + } + + private ArrayList mGameStartedEvents = new ArrayList<>(); + private int mConnectedCount = 0; + private GameServiceState mGameServiceState = GameServiceState.DISCONNECTED; + + public GameServiceState getState() { + return mGameServiceState; + } + + public int getConnectedCount() { + return mConnectedCount; + } + + public ArrayList getGameStartedEvents() { + return mGameStartedEvents; + } + + @Override + public void connected(IGameServiceController gameServiceController) { + Preconditions.checkState(mGameServiceState == GameServiceState.DISCONNECTED); + + mGameServiceState = GameServiceState.CONNECTED; + mConnectedCount += 1; + mGameServiceController = gameServiceController; + } + + @Override + public void disconnected() { + Preconditions.checkState(mGameServiceState == GameServiceState.CONNECTED); + + mGameServiceState = GameServiceState.DISCONNECTED; + mGameServiceController = null; + } + + @Override + public void gameStarted(GameStartedEvent gameStartedEvent) { + Preconditions.checkState(mGameServiceState == GameServiceState.CONNECTED); + + mGameStartedEvents.add(gameStartedEvent); + } + + public void requestCreateGameSession(int task) { + Preconditions.checkState(mGameServiceState == GameServiceState.CONNECTED); + + try { + mGameServiceController.createGameSession(task); + } catch (RemoteException ex) { + throw new AssertionError(ex); + } + } + } + + static final class FakeGameSessionService extends IGameSessionService.Stub { + + private final ArrayList mCapturedCreateInvocations = + new ArrayList<>(); + private final HashMap> + mPendingCreateGameSessionResultFutures = + new HashMap<>(); + + public static final class CapturedCreateInvocation { + private final CreateGameSessionRequest mCreateGameSessionRequest; + private final GameSessionViewHostConfiguration mGameSessionViewHostConfiguration; + + CapturedCreateInvocation( + CreateGameSessionRequest createGameSessionRequest, + GameSessionViewHostConfiguration gameSessionViewHostConfiguration) { + mCreateGameSessionRequest = createGameSessionRequest; + mGameSessionViewHostConfiguration = gameSessionViewHostConfiguration; + } + } + + public ArrayList getCapturedCreateInvocations() { + return mCapturedCreateInvocations; + } + + public AndroidFuture removePendingFutureForTaskId(int taskId) { + return mPendingCreateGameSessionResultFutures.remove(taskId); + } + + @Override + public void create( + CreateGameSessionRequest createGameSessionRequest, + GameSessionViewHostConfiguration gameSessionViewHostConfiguration, + AndroidFuture createGameSessionResultFuture) { + + mCapturedCreateInvocations.add( + new CapturedCreateInvocation( + createGameSessionRequest, + gameSessionViewHostConfiguration)); + + Preconditions.checkState(!mPendingCreateGameSessionResultFutures.containsKey( + createGameSessionRequest.getTaskId())); + mPendingCreateGameSessionResultFutures.put( + createGameSessionRequest.getTaskId(), + createGameSessionResultFuture); + } + } + + private static class FakeGameSession extends IGameSession.Stub { boolean mIsDestroyed = false; @Override