diff options
| author | 2022-01-19 14:45:33 +0000 | |
|---|---|---|
| committer | 2022-01-19 14:45:33 +0000 | |
| commit | 607e78a56f76514edd5b40b7a06c19532e472806 (patch) | |
| tree | fbe2c19e300e446511e6b761a6dbbd7c7ba7e1df | |
| parent | 9c74d734719393e3604213dd5f6590c712af0216 (diff) | |
| parent | 3d7dc6d0a0014a50d358671c5625ff02e06e9cf5 (diff) | |
Merge "Allow Game Service to trigger the Game Session creation"
9 files changed, 555 insertions, 57 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 313fc75ed668..949dee3d6942 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10877,9 +10877,11 @@ package android.service.games { public class GameService extends android.app.Service { ctor public GameService(); + method public final void createGameSession(@IntRange(from=0) int); method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent); method public void onConnected(); method public void onDisconnected(); + method public void onGameStarted(@NonNull android.service.games.GameStartedEvent); field public static final String ACTION_GAME_SERVICE = "android.service.games.action.GAME_SERVICE"; field public static final String SERVICE_META_DATA = "android.game_service"; } @@ -10897,6 +10899,15 @@ package android.service.games { field public static final String ACTION_GAME_SESSION_SERVICE = "android.service.games.action.GAME_SESSION_SERVICE"; } + public final class GameStartedEvent implements android.os.Parcelable { + ctor public GameStartedEvent(@IntRange(from=0) int, @NonNull String); + method public int describeContents(); + method @NonNull public String getPackageName(); + method @IntRange(from=0) public int getTaskId(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.games.GameStartedEvent> CREATOR; + } + } package android.service.notification { diff --git a/core/java/android/service/games/GameService.java b/core/java/android/service/games/GameService.java index 105c2aa53374..870a7e3f2646 100644 --- a/core/java/android/service/games/GameService.java +++ b/core/java/android/service/games/GameService.java @@ -16,6 +16,8 @@ package android.service.games; +import android.annotation.IntRange; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; @@ -38,6 +40,12 @@ import java.util.Objects; * when a game session should begin. It is always kept running by the system. * Because of this it should be kept as lightweight as possible. * + * <p> Instead of requiring permissions for sensitive actions (e.g., starting a new game session), + * this class is provided with an {@link IGameServiceController} instance which exposes the + * sensitive functionality. This controller is provided by the system server when calling the + * {@link IGameService#connected(IGameServiceController)} method exposed by this class. The system + * server does so only when creating the bound game service. + * * <p>Heavyweight operations (such as showing UI) should be implemented in the * associated {@link GameSessionService} when a game session is taking place. Its * implementation should run in a separate process from the {@link GameService}. @@ -79,12 +87,13 @@ public class GameService extends Service { */ public static final String SERVICE_META_DATA = "android.game_service"; + private IGameServiceController mGameServiceController; private IGameManagerService mGameManagerService; private final IGameService mInterface = new IGameService.Stub() { @Override - public void connected() { + public void connected(IGameServiceController gameServiceController) { Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( - GameService::doOnConnected, GameService.this)); + GameService::doOnConnected, GameService.this, gameServiceController)); } @Override @@ -92,6 +101,12 @@ public class GameService extends Service { Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( GameService::onDisconnected, GameService.this)); } + + @Override + public void gameStarted(GameStartedEvent gameStartedEvent) { + Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( + GameService::onGameStarted, GameService.this, gameStartedEvent)); + } }; private final IBinder.DeathRecipient mGameManagerServiceDeathRecipient = () -> { @@ -111,7 +126,7 @@ public class GameService extends Service { return null; } - private void doOnConnected() { + private void doOnConnected(@NonNull IGameServiceController gameServiceController) { mGameManagerService = IGameManagerService.Stub.asInterface( ServiceManager.getService(Context.GAME_SERVICE)); @@ -122,6 +137,7 @@ public class GameService extends Service { Log.w(TAG, "Unable to link to death with system service"); } + mGameServiceController = gameServiceController; onConnected(); } @@ -138,4 +154,34 @@ public class GameService extends Service { * The service should clean up any resources that it holds at this point. */ public void onDisconnected() {} + + /** + * Called when a game task is started. It is the responsibility of the service to determine what + * action to take (e.g., request that a game session be created). + * + * @param gameStartedEvent Contains information about the game being started. + */ + public void onGameStarted(@NonNull GameStartedEvent gameStartedEvent) {} + + /** + * Call to create a new game session be created for a game. This method may be called + * by a game service following {@link #onGameStarted}, using the task ID provided by the + * provided {@link GameStartedEvent} (using {@link GameStartedEvent#getTaskId}). + * + * If a game session already exists for the game task, this call will be ignored and the + * existing session will continue. + * + * @param taskId The taskId of the game. + */ + public final void createGameSession(@IntRange(from = 0) int taskId) { + if (mGameServiceController == null) { + throw new IllegalStateException("Can not call before connected()"); + } + + try { + mGameServiceController.createGameSession(taskId); + } catch (RemoteException e) { + Log.e(TAG, "Request for game session failed", e); + } + } } diff --git a/core/java/android/service/games/GameStartedEvent.aidl b/core/java/android/service/games/GameStartedEvent.aidl new file mode 100644 index 000000000000..8a5a4a16abab --- /dev/null +++ b/core/java/android/service/games/GameStartedEvent.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 GameStartedEvent;
\ No newline at end of file diff --git a/core/java/android/service/games/GameStartedEvent.java b/core/java/android/service/games/GameStartedEvent.java new file mode 100644 index 000000000000..bf292606fbf6 --- /dev/null +++ b/core/java/android/service/games/GameStartedEvent.java @@ -0,0 +1,119 @@ +/* + * 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.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Event object provided when a game task is started. + * + * This is provided to the Game Service via + * {@link GameService#onGameStarted(GameStartedEvent)}. It includes the game's taskId + * (see {@link #getTaskId}) that the game's package name (see {@link #getPackageName}). + * + * @hide + */ +@SystemApi +public final class GameStartedEvent implements Parcelable { + + @NonNull + public static final Parcelable.Creator<GameStartedEvent> CREATOR = + new Parcelable.Creator<GameStartedEvent>() { + @Override + public GameStartedEvent createFromParcel(Parcel source) { + return new GameStartedEvent( + source.readInt(), + source.readString()); + } + + @Override + public GameStartedEvent[] newArray(int size) { + return new GameStartedEvent[0]; + } + }; + + private final int mTaskId; + private final String mPackageName; + + public GameStartedEvent(@IntRange(from = 0) int taskId, @NonNull String packageName) { + this.mTaskId = taskId; + this.mPackageName = packageName; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mTaskId); + dest.writeString(mPackageName); + } + + /** + * Unique identifier for the task associated with the game. + */ + @IntRange(from = 0) + public int getTaskId() { + return mTaskId; + } + + /** + * The package name for the game. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + @Override + public String toString() { + return "GameStartedEvent{" + + "mTaskId=" + + mTaskId + + ", mPackageName='" + + mPackageName + + "\'}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof GameStartedEvent)) { + return false; + } + + GameStartedEvent that = (GameStartedEvent) o; + return mTaskId == that.mTaskId + && Objects.equals(mPackageName, that.mPackageName); + } + + @Override + public int hashCode() { + return Objects.hash(mTaskId, mPackageName); + } +} diff --git a/core/java/android/service/games/IGameService.aidl b/core/java/android/service/games/IGameService.aidl index 8a0d6365977b..38c8416117e0 100644 --- a/core/java/android/service/games/IGameService.aidl +++ b/core/java/android/service/games/IGameService.aidl @@ -16,10 +16,14 @@ package android.service.games; +import android.service.games.GameStartedEvent; +import android.service.games.IGameServiceController; + /** * @hide */ oneway interface IGameService { - void connected(); + void connected(in IGameServiceController gameServiceController); void disconnected(); + void gameStarted(in GameStartedEvent gameStartedEvent); } diff --git a/core/java/android/service/games/IGameServiceController.aidl b/core/java/android/service/games/IGameServiceController.aidl new file mode 100644 index 000000000000..886f519b6605 --- /dev/null +++ b/core/java/android/service/games/IGameServiceController.aidl @@ -0,0 +1,24 @@ +/* + * 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 + */ +oneway interface IGameServiceController { + void createGameSession(int taskId); +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java index 3f3f257aedc5..cc060e94a52a 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java @@ -24,7 +24,9 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.service.games.CreateGameSessionRequest; +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; @@ -61,6 +63,17 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan }); } }; + + private final IGameServiceController mGameServiceController = + new IGameServiceController.Stub() { + @Override + public void createGameSession(int taskId) { + mBackgroundExecutor.execute(() -> { + GameServiceProviderInstanceImpl.this.createGameSession(taskId); + }); + } + }; + private final Object mLock = new Object(); private final UserHandle mUserHandle; private final Executor mBackgroundExecutor; @@ -114,7 +127,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan // TODO(b/204503192): In cases where the connection to the game service fails retry with // back off mechanism. AndroidFuture<Void> unusedPostConnectedFuture = mGameServiceConnector.post(gameService -> { - gameService.connected(); + gameService.connected(mGameServiceController); }); try { @@ -168,10 +181,38 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } synchronized (mLock) { - createGameSessionLocked(taskId, componentName); + gameTaskStartedLocked(taskId, componentName); } } + @GuardedBy("mLock") + private void gameTaskStartedLocked(int sessionId, @NonNull ComponentName componentName) { + if (DEBUG) { + Slog.i(TAG, "gameStartedLocked() id: " + sessionId + " component: " + componentName); + } + + if (!mIsRunning) { + return; + } + + GameSessionRecord existingGameSessionRecord = mGameSessions.get(sessionId); + if (existingGameSessionRecord != null) { + Slog.w(TAG, "Existing game session found for task (id: " + sessionId + + ") creation. Ignoring."); + return; + } + + GameSessionRecord gameSessionRecord = GameSessionRecord.awaitingGameSessionRequest( + sessionId, componentName); + mGameSessions.put(sessionId, gameSessionRecord); + + AndroidFuture<Void> unusedPostGameStartedFuture = mGameServiceConnector.post( + gameService -> { + gameService.gameStarted( + new GameStartedEvent(sessionId, componentName.getPackageName())); + }); + } + private void onTaskRemoved(int taskId) { synchronized (mLock) { boolean isTaskAssociatedWithGameSession = mGameSessions.containsKey(taskId); @@ -179,14 +220,20 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan return; } - destroyGameSessionLocked(taskId); + destroyGameSessionIfNecessaryLocked(taskId); + } + } + + private void createGameSession(int taskId) { + synchronized (mLock) { + createGameSessionLocked(taskId); } } @GuardedBy("mLock") - private void createGameSessionLocked(int sessionId, @NonNull ComponentName componentName) { + private void createGameSessionLocked(int sessionId) { if (DEBUG) { - Slog.i(TAG, "createGameSession() id: " + sessionId + " component: " + componentName); + Slog.i(TAG, "createGameSessionLocked() id: " + sessionId); } if (!mIsRunning) { @@ -194,15 +241,19 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } GameSessionRecord existingGameSessionRecord = mGameSessions.get(sessionId); - if (existingGameSessionRecord != null) { - Slog.w(TAG, "Existing game session found for task (id: " + sessionId + if (existingGameSessionRecord == null) { + Slog.w(TAG, "No existing game session record found for task (id: " + sessionId + ") creation. Ignoring."); return; } + if (!existingGameSessionRecord.isAwaitingGameSessionRequest()) { + Slog.w(TAG, "Existing game session for task (id: " + sessionId + + ") is not awaiting game session request. Ignoring."); + return; + } + mGameSessions.put(sessionId, existingGameSessionRecord.withGameSessionRequested()); - GameSessionRecord gameSessionRecord = GameSessionRecord.pendingGameSession(sessionId, - componentName); - mGameSessions.put(sessionId, gameSessionRecord); + ComponentName componentName = existingGameSessionRecord.getComponentName(); // 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. @@ -211,10 +262,10 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan .whenCompleteAsync((gameSessionIBinder, exception) -> { IGameSession gameSession = IGameSession.Stub.asInterface(gameSessionIBinder); if (exception != null || gameSession == null) { - Slog.w(TAG, "Failed to create GameSession: " + gameSessionRecord, + Slog.w(TAG, "Failed to create GameSession: " + existingGameSessionRecord, exception); synchronized (mLock) { - destroyGameSessionLocked(sessionId); + destroyGameSessionIfNecessaryLocked(sessionId); } return; } @@ -239,9 +290,19 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } GameSessionRecord gameSessionRecord = mGameSessions.get(sessionId); + boolean isValidAttachRequest = true; 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) { @@ -254,7 +315,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } @GuardedBy("mLock") - private void destroyGameSessionLocked(int sessionId) { + 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 diff --git a/services/core/java/com/android/server/app/GameSessionRecord.java b/services/core/java/com/android/server/app/GameSessionRecord.java index 329e9e8144e0..e9daceb0cd39 100644 --- a/services/core/java/com/android/server/app/GameSessionRecord.java +++ b/services/core/java/com/android/server/app/GameSessionRecord.java @@ -25,28 +25,59 @@ import java.util.Objects; final class GameSessionRecord { + private enum State { + // Game task is running, but GameSession not created. + NO_GAME_SESSION_REQUESTED, + // Game Service provider requested a Game Session and we are in the + // process of creating it. GameSessionRecord.getGameSession() == null; + GAME_SESSION_REQUESTED, + // A Game Session is created and attached. + // GameSessionRecord.getGameSession() != null. + GAME_SESSION_ATTACHED, + } + private final int mTaskId; private final ComponentName mRootComponentName; @Nullable private final IGameSession mIGameSession; + private final State mState; - static GameSessionRecord pendingGameSession(int taskId, ComponentName rootComponentName) { - return new GameSessionRecord(taskId, rootComponentName, /* gameSession= */ null); + static GameSessionRecord awaitingGameSessionRequest(int taskId, + ComponentName rootComponentName) { + return new GameSessionRecord(taskId, rootComponentName, /* gameSession= */ null, + State.NO_GAME_SESSION_REQUESTED); } private GameSessionRecord( int taskId, @NonNull ComponentName rootComponentName, - @Nullable IGameSession gameSession) { + @Nullable IGameSession gameSession, + @NonNull State state) { this.mTaskId = taskId; this.mRootComponentName = rootComponentName; this.mIGameSession = gameSession; + this.mState = state; + } + + public boolean isAwaitingGameSessionRequest() { + return mState == State.NO_GAME_SESSION_REQUESTED; + } + + @NonNull + public GameSessionRecord withGameSessionRequested() { + return new GameSessionRecord(mTaskId, mRootComponentName, /* gameSession=*/ null, + State.GAME_SESSION_REQUESTED); + } + + public boolean isGameSessionRequested() { + return mState == State.GAME_SESSION_REQUESTED; } @NonNull public GameSessionRecord withGameSession(@NonNull IGameSession gameSession) { Objects.requireNonNull(gameSession); - return new GameSessionRecord(mTaskId, mRootComponentName, gameSession); + return new GameSessionRecord(mTaskId, mRootComponentName, gameSession, + State.GAME_SESSION_ATTACHED); } @Nullable @@ -54,6 +85,11 @@ final class GameSessionRecord { return mIGameSession; } + @NonNull + public ComponentName getComponentName() { + return mRootComponentName; + } + @Override public String toString() { return "GameSessionRecord{" @@ -63,6 +99,8 @@ final class GameSessionRecord { + mRootComponentName + ", mIGameSession=" + mIGameSession + + ", mState=" + + mState + '}'; } @@ -78,11 +116,11 @@ final class GameSessionRecord { GameSessionRecord that = (GameSessionRecord) o; return mTaskId == that.mTaskId && mRootComponentName.equals(that.mRootComponentName) - && Objects.equals(mIGameSession, that.mIGameSession); + && Objects.equals(mIGameSession, that.mIGameSession) && mState == that.mState; } @Override public int hashCode() { - return Objects.hash(mTaskId, mRootComponentName, mIGameSession); + return Objects.hash(mTaskId, mRootComponentName, mIGameSession, mState); } } 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 b6c706ed2730..0d513bb2d68c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java @@ -18,6 +18,7 @@ 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.google.common.truth.Truth.assertThat; @@ -35,7 +36,9 @@ import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.service.games.CreateGameSessionRequest; +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; @@ -50,6 +53,7 @@ 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.MockitoSession; @@ -135,7 +139,7 @@ public final class GameServiceProviderInstanceImplTest { public void start_startsGameSession() throws Exception { mGameServiceProviderInstance.start(); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); @@ -146,7 +150,7 @@ public final class GameServiceProviderInstanceImplTest { public void start_multipleTimes_startsGameSessionOnce() throws Exception { mGameServiceProviderInstance.start(); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); @@ -167,7 +171,7 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verify(mMockGameService).disconnected(); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); @@ -183,9 +187,9 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verify(mMockGameService).disconnected(); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verify(mMockGameService).disconnected(); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); @@ -199,7 +203,7 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.stop(); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verify(mMockGameService).disconnected(); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); @@ -231,7 +235,7 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.stop(); dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verify(mMockGameService).disconnected(); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isFalse(); @@ -244,7 +248,7 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); dispatchTaskCreated(10, APP_A_MAIN_ACTIVITY); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); @@ -256,7 +260,7 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance.start(); dispatchTaskCreated(10, null); - mInOrder.verify(mMockGameService).connected(); + mInOrder.verify(mMockGameService).connected(any()); mInOrder.verifyNoMoreInteractions(); assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue(); assertThat(mFakeGameServiceConnector.getConnectCount()).isEqualTo(1); @@ -264,18 +268,84 @@ public final class GameServiceProviderInstanceImplTest { } @Test - public void gameTaskStarted_createsGameSession() throws Exception { + public void gameSessionRequested_withoutTaskDispatch_ignoredAndDoesNotCrash() throws Exception { + mGameServiceProviderInstance.start(); + ArgumentCaptor<IGameServiceController> 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); + } + + @Test + public void gameTaskStarted_noSessionRequest_callsStartGame() throws Exception { + 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); + } + + @Test + public void gameTaskStartedAndSessionRequested_createsGameSession() throws Exception { CreateGameSessionRequest createGameSessionRequest = new CreateGameSessionRequest(10, GAME_A_PACKAGE); Supplier<AndroidFuture<IBinder>> gameSession10Future = captureCreateGameSessionFuture(createGameSessionRequest); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); + + 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<AndroidFuture<IBinder>> gameSession10Future = + captureCreateGameSessionFuture(createGameSessionRequest); + + mGameServiceProviderInstance.start(); + ArgumentCaptor<IGameServiceController> 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); - mInOrder.verify(mMockGameService).connected(); + controllerArgumentCaptor.getValue().createGameSession(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(); @@ -294,12 +364,18 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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(); + 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(); @@ -317,12 +393,18 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); dispatchTaskRemoved(10); - mInOrder.verify(mMockGameService).connected(); + 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(); @@ -333,7 +415,8 @@ public final class GameServiceProviderInstanceImplTest { } @Test - public void gameTaskStarted_multipleTimes_createsMultipleGameSessions() throws Exception { + public void gameTaskStartedAndSessionRequested_multipleTimes_createsMultipleGameSessions() + throws Exception { CreateGameSessionRequest createGameSessionRequest10 = new CreateGameSessionRequest(10, GAME_A_PACKAGE); Supplier<AndroidFuture<IBinder>> gameSession10Future = @@ -345,16 +428,25 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest11); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); - dispatchTaskCreated(11, GAME_A_MAIN_ACTIVITY); + dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, + controllerArgumentCaptor.getValue()); IGameSessionStub gameSession11 = new IGameSessionStub(); gameSession11Future.get().complete(gameSession11); - mInOrder.verify(mMockGameService).connected(); + 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(); @@ -366,6 +458,40 @@ public final class GameServiceProviderInstanceImplTest { } @Test + public void gameTaskStartedTwice_sessionRequestedSecondTimeOnly_createsOneGameSessions() + throws Exception { + CreateGameSessionRequest createGameSessionRequest11 = + new CreateGameSessionRequest(11, GAME_A_PACKAGE); + Supplier<AndroidFuture<IBinder>> gameSession11Future = + captureCreateGameSessionFuture(createGameSessionRequest11); + + // The game task is started twice, but a session is requested only for the second one. + mGameServiceProviderInstance.start(); + ArgumentCaptor<IGameServiceController> 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); + + 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); + } + + @Test public void gameTaskRemoved_afterMultipleCreated_destroysOnlyThatGameSession() throws Exception { CreateGameSessionRequest createGameSessionRequest10 = @@ -379,18 +505,27 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest11); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); - dispatchTaskCreated(11, GAME_A_MAIN_ACTIVITY); + dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, + controllerArgumentCaptor.getValue()); IGameSessionStub gameSession11 = new IGameSessionStub(); gameSession11Future.get().complete(gameSession11); dispatchTaskRemoved(10); - mInOrder.verify(mMockGameService).connected(); + 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(); @@ -414,19 +549,28 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest11); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); - dispatchTaskCreated(11, GAME_A_MAIN_ACTIVITY); + dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, + controllerArgumentCaptor.getValue()); IGameSessionStub gameSession11 = new IGameSessionStub(); gameSession11Future.get().complete(gameSession11); dispatchTaskRemoved(10); dispatchTaskRemoved(11); - mInOrder.verify(mMockGameService).connected(); + 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(); @@ -438,7 +582,7 @@ public final class GameServiceProviderInstanceImplTest { } @Test - public void gameTasksCreated_afterAllPreviousSessionsDestroyed_createsSession() + public void gameTasksCreatedAndSessionsReq_afterAllPreviousSessionsDestroyed_createsSession() throws Exception { CreateGameSessionRequest createGameSessionRequest10 = new CreateGameSessionRequest(10, GAME_A_PACKAGE); @@ -456,24 +600,36 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest12); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); - dispatchTaskCreated(11, GAME_A_MAIN_ACTIVITY); + dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, + controllerArgumentCaptor.getValue()); IGameSessionStub gameSession11 = new IGameSessionStub(); gameSession11Future.get().complete(gameSession11); dispatchTaskRemoved(10); dispatchTaskRemoved(11); - dispatchTaskCreated(12, GAME_A_MAIN_ACTIVITY); + dispatchTaskCreatedAndTriggerSessionRequest(12, GAME_A_MAIN_ACTIVITY, + controllerArgumentCaptor.getValue()); IGameSessionStub gameSession12 = new IGameSessionStub(); gameSession11Future.get().complete(gameSession12); - mInOrder.verify(mMockGameService).connected(); + 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(); @@ -498,16 +654,25 @@ public final class GameServiceProviderInstanceImplTest { captureCreateGameSessionFuture(createGameSessionRequest11); mGameServiceProviderInstance.start(); - dispatchTaskCreated(10, GAME_A_MAIN_ACTIVITY); + ArgumentCaptor<IGameServiceController> 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); - dispatchTaskCreated(11, GAME_A_MAIN_ACTIVITY); + dispatchTaskCreatedAndTriggerSessionRequest(11, GAME_A_MAIN_ACTIVITY, + controllerArgumentCaptor.getValue()); IGameSessionStub gameSession11 = new IGameSessionStub(); gameSession11Future.get().complete(gameSession11); mGameServiceProviderInstance.stop(); - mInOrder.verify(mMockGameService).connected(); + 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(); @@ -536,6 +701,13 @@ public final class GameServiceProviderInstanceImplTest { }); } + 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); |