Merge "Allow Game Service to trigger the Game Session creation"

This commit is contained in:
Tyler Lacey
2022-01-19 14:45:33 +00:00
committed by Android (Google) Code Review
9 changed files with 562 additions and 64 deletions

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.service.games;
/**
* @hide
*/
parcelable GameStartedEvent;

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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,25 +181,14 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
synchronized (mLock) {
createGameSessionLocked(taskId, componentName);
}
}
private void onTaskRemoved(int taskId) {
synchronized (mLock) {
boolean isTaskAssociatedWithGameSession = mGameSessions.containsKey(taskId);
if (!isTaskAssociatedWithGameSession) {
return;
}
destroyGameSessionLocked(taskId);
gameTaskStartedLocked(taskId, componentName);
}
}
@GuardedBy("mLock")
private void createGameSessionLocked(int sessionId, @NonNull ComponentName componentName) {
private void gameTaskStartedLocked(int sessionId, @NonNull ComponentName componentName) {
if (DEBUG) {
Slog.i(TAG, "createGameSession() id: " + sessionId + " component: " + componentName);
Slog.i(TAG, "gameStartedLocked() id: " + sessionId + " component: " + componentName);
}
if (!mIsRunning) {
@@ -200,10 +202,59 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
return;
}
GameSessionRecord gameSessionRecord = GameSessionRecord.pendingGameSession(sessionId,
componentName);
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);
if (!isTaskAssociatedWithGameSession) {
return;
}
destroyGameSessionIfNecessaryLocked(taskId);
}
}
private void createGameSession(int taskId) {
synchronized (mLock) {
createGameSessionLocked(taskId);
}
}
@GuardedBy("mLock")
private void createGameSessionLocked(int sessionId) {
if (DEBUG) {
Slog.i(TAG, "createGameSessionLocked() id: " + sessionId);
}
if (!mIsRunning) {
return;
}
GameSessionRecord existingGameSessionRecord = mGameSessions.get(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());
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.
AndroidFuture<IBinder> gameSessionFuture = new AndroidFuture<IBinder>()
@@ -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

View File

@@ -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);
}
}

View File

@@ -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();
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);
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();
@@ -365,6 +457,40 @@ public final class GameServiceProviderInstanceImplTest {
assertThat(mFakeGameSessionServiceConnector.getConnectCount()).isEqualTo(1);
}
@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 {
@@ -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);