summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/system-current.txt1
-rw-r--r--core/java/android/service/games/GameSession.java150
-rw-r--r--core/java/android/service/games/IGameSession.aidl3
-rw-r--r--services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java54
-rw-r--r--services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java207
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java55
6 files changed, 456 insertions, 14 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 3f71d5668dba..ab2d3d50c2a8 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11093,6 +11093,7 @@ package android.service.games {
ctor public GameSession();
method public void onCreate();
method public void onDestroy();
+ method public void onGameTaskFocusChanged(boolean);
method public void setTaskOverlayView(@NonNull android.view.View, @NonNull android.view.ViewGroup.LayoutParams);
method public void takeScreenshot(@NonNull java.util.concurrent.Executor, @NonNull android.service.games.GameSession.ScreenshotCallback);
}
diff --git a/core/java/android/service/games/GameSession.java b/core/java/android/service/games/GameSession.java
index b6fe067cfc71..cb5c19b72bd0 100644
--- a/core/java/android/service/games/GameSession.java
+++ b/core/java/android/service/games/GameSession.java
@@ -18,6 +18,7 @@ package android.service.games;
import android.annotation.Hide;
import android.annotation.IntDef;
+import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.Context;
@@ -25,6 +26,7 @@ import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Handler;
+import android.os.Looper;
import android.os.RemoteException;
import android.util.Slog;
import android.view.SurfaceControlViewHost;
@@ -47,21 +49,65 @@ import java.util.concurrent.Executor;
* which is then returned when a game session is created via
* {@link GameSessionService#onNewSession(CreateGameSessionRequest)}.
*
+ * This class exposes various lifecycle methods which are guaranteed to be called in the following
+ * fashion:
+ *
+ * {@link #onCreate()}: Will always be the first lifecycle method to be called, once the game
+ * session is created.
+ *
+ * {@link #onGameTaskFocusChanged(boolean)}: Will be called after {@link #onCreate()} with
+ * focused=true when the game task first comes into focus (if it does). If the game task is focused
+ * when the game session is created, this method will be called immediately after
+ * {@link #onCreate()} with focused=true. After this method is called with focused=true, it will be
+ * called again with focused=false when the task goes out of focus. If this method is ever called
+ * with focused=true, it is guaranteed to be called again with focused=false before
+ * {@link #onDestroy()} is called. If the game task never comes into focus during the session
+ * lifetime, this method will never be called.
+ *
+ * {@link #onDestroy()}: Will always be called after {@link #onCreate()}. If the game task ever
+ * comes into focus before the game session is destroyed, then this method will be called after one
+ * or more pairs of calls to {@link #onGameTaskFocusChanged(boolean)}.
+ *
* @hide
*/
@SystemApi
public abstract class GameSession {
-
private static final String TAG = "GameSession";
+ private static final boolean DEBUG = false;
final IGameSession mInterface = new IGameSession.Stub() {
@Override
- public void destroy() {
+ public void onDestroyed() {
Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
GameSession::doDestroy, GameSession.this));
}
+
+ @Override
+ public void onTaskFocusChanged(boolean focused) {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ GameSession::moveToState, GameSession.this,
+ focused ? LifecycleState.TASK_FOCUSED : LifecycleState.TASK_UNFOCUSED));
+ }
};
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public enum LifecycleState {
+ // Initial state; may transition to CREATED.
+ INITIALIZED,
+ // May transition to TASK_FOCUSED or DESTROYED.
+ CREATED,
+ // May transition to TASK_UNFOCUSED.
+ TASK_FOCUSED,
+ // May transition to TASK_FOCUSED or DESTROYED.
+ TASK_UNFOCUSED,
+ // May not transition once reached.
+ DESTROYED
+ }
+
+ private LifecycleState mLifecycleState = LifecycleState.INITIALIZED;
private IGameSessionController mGameSessionController;
private int mTaskId;
private GameSessionRootView mGameSessionRootView;
@@ -87,13 +133,93 @@ public abstract class GameSession {
@Hide
void doCreate() {
- onCreate();
+ moveToState(LifecycleState.CREATED);
}
@Hide
void doDestroy() {
- onDestroy();
mSurfaceControlViewHost.release();
+ moveToState(LifecycleState.DESTROYED);
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ @MainThread
+ public void moveToState(LifecycleState newLifecycleState) {
+ if (DEBUG) {
+ Slog.d(TAG, "moveToState: " + mLifecycleState + " -> " + newLifecycleState);
+ }
+
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new RuntimeException("moveToState should be used only from the main thread");
+ }
+
+ if (mLifecycleState == newLifecycleState) {
+ // Nothing to do.
+ return;
+ }
+
+ switch (mLifecycleState) {
+ case INITIALIZED:
+ if (newLifecycleState == LifecycleState.CREATED) {
+ onCreate();
+ } else if (newLifecycleState == LifecycleState.DESTROYED) {
+ onCreate();
+ onDestroy();
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Ignoring moveToState: INITIALIZED -> " + newLifecycleState);
+ }
+ return;
+ }
+ break;
+ case CREATED:
+ if (newLifecycleState == LifecycleState.TASK_FOCUSED) {
+ onGameTaskFocusChanged(/*focused=*/ true);
+ } else if (newLifecycleState == LifecycleState.DESTROYED) {
+ onDestroy();
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Ignoring moveToState: CREATED -> " + newLifecycleState);
+ }
+ return;
+ }
+ break;
+ case TASK_FOCUSED:
+ if (newLifecycleState == LifecycleState.TASK_UNFOCUSED) {
+ onGameTaskFocusChanged(/*focused=*/ false);
+ } else if (newLifecycleState == LifecycleState.DESTROYED) {
+ onGameTaskFocusChanged(/*focused=*/ false);
+ onDestroy();
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Ignoring moveToState: TASK_FOCUSED -> " + newLifecycleState);
+ }
+ return;
+ }
+ break;
+ case TASK_UNFOCUSED:
+ if (newLifecycleState == LifecycleState.TASK_FOCUSED) {
+ onGameTaskFocusChanged(/*focused=*/ true);
+ } else if (newLifecycleState == LifecycleState.DESTROYED) {
+ onDestroy();
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Ignoring moveToState: TASK_UNFOCUSED -> " + newLifecycleState);
+ }
+ return;
+ }
+ break;
+ case DESTROYED:
+ if (DEBUG) {
+ Slog.d(TAG, "Ignoring moveToState: DESTROYED -> " + newLifecycleState);
+ }
+ return;
+ }
+
+ mLifecycleState = newLifecycleState;
}
/**
@@ -105,13 +231,27 @@ public abstract class GameSession {
}
/**
- * Finalizer called when the game session is ending.
+ * Finalizer called when the game session is ending. This method will always be called after a
+ * call to {@link #onCreate()}. If the game task is ever in focus, this method will be called
+ * after one or more pairs of calls to {@link #onGameTaskFocusChanged(boolean)}.
*
* This should be used to perform any cleanup before the game session is destroyed.
*/
public void onDestroy() {
}
+ /**
+ * Called when the game task for this session is or unfocused. The initial call to this method
+ * will always come after a call to {@link #onCreate()} with focused=true (when the game task
+ * first comes into focus after the session is created, or immediately after the session is
+ * created if the game task is already focused).
+ *
+ * This should be used to perform any setup required when the game task comes into focus or any
+ * cleanup that is required when the game task goes out of focus.
+ *
+ * @param focused True if the game task is focused, false if the game task is unfocused.
+ */
+ public void onGameTaskFocusChanged(boolean focused) {}
/**
* Sets the task overlay content to an explicit view. This view is placed directly into the game
diff --git a/core/java/android/service/games/IGameSession.aidl b/core/java/android/service/games/IGameSession.aidl
index b2e9f1d21f6e..71da6302b63d 100644
--- a/core/java/android/service/games/IGameSession.aidl
+++ b/core/java/android/service/games/IGameSession.aidl
@@ -20,5 +20,6 @@ package android.service.games;
* @hide
*/
oneway interface IGameSession {
- void destroy();
+ void onDestroyed();
+ void onTaskFocusChanged(boolean focused);
}
diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
index 8996140256d1..43c9839a04d8 100644
--- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
+++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
@@ -19,6 +19,7 @@ package com.android.server.app;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.TaskStackListener;
import android.content.ComponentName;
@@ -75,6 +76,13 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
});
}
+ @Override
+ public void onTaskFocusChanged(int taskId, boolean focused) {
+ mBackgroundExecutor.execute(() -> {
+ GameServiceProviderInstanceImpl.this.onTaskFocusChanged(taskId, focused);
+ });
+ }
+
// 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
@@ -212,6 +220,30 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
}
+ private void onTaskFocusChanged(int taskId, boolean focused) {
+ synchronized (mLock) {
+ onTaskFocusChangedLocked(taskId, focused);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void onTaskFocusChangedLocked(int taskId, boolean focused) {
+ if (DEBUG) {
+ Slog.d(TAG, "onTaskFocusChangedLocked() id: " + taskId + " focused: " + focused);
+ }
+
+ final GameSessionRecord gameSessionRecord = mGameSessions.get(taskId);
+ if (gameSessionRecord == null || gameSessionRecord.getGameSession() == null) {
+ return;
+ }
+
+ try {
+ gameSessionRecord.getGameSession().onTaskFocusChanged(focused);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify session of task focus change: " + gameSessionRecord);
+ }
+ }
+
@GuardedBy("mLock")
private void gameTaskStartedLocked(int taskId, @NonNull ComponentName componentName) {
if (DEBUG) {
@@ -311,6 +343,12 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
synchronized (mLock) {
attachGameSessionLocked(taskId, createGameSessionResult);
}
+
+ // The TaskStackListener may have made its task focused call for the
+ // game session's task before the game session was created, so check if
+ // the task is already focused so that the game session can be notified.
+ setGameSessionFocusedIfNecessary(taskId,
+ createGameSessionResult.getGameSession());
}, mBackgroundExecutor);
AndroidFuture<Void> unusedPostCreateGameSessionFuture =
@@ -327,6 +365,18 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
});
}
+ private void setGameSessionFocusedIfNecessary(int taskId, IGameSession gameSession) {
+ try {
+ final ActivityTaskManager.RootTaskInfo rootTaskInfo =
+ mActivityTaskManager.getFocusedRootTaskInfo();
+ if (rootTaskInfo != null && rootTaskInfo.taskId == taskId) {
+ gameSession.onTaskFocusChanged(true);
+ }
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to set task focused for ID: " + taskId);
+ }
+ }
+
@GuardedBy("mLock")
private void attachGameSessionLocked(
int taskId,
@@ -368,7 +418,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
int taskId,
CreateGameSessionResult createGameSessionResult) {
try {
- createGameSessionResult.getGameSession().destroy();
+ createGameSessionResult.getGameSession().onDestroyed();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + taskId);
}
@@ -408,7 +458,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
IGameSession gameSession = gameSessionRecord.getGameSession();
if (gameSession != null) {
try {
- gameSession.destroy();
+ gameSession.onDestroyed();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to destroy session: " + gameSessionRecord, ex);
}
diff --git a/services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java b/services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java
index fe6af949e219..fec9b1249d17 100644
--- a/services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java
+++ b/services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java
@@ -20,6 +20,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -29,11 +31,12 @@ import static org.mockito.ArgumentMatchers.anyInt;
import android.graphics.Bitmap;
import android.platform.test.annotations.Presubmit;
import android.service.games.GameSession.ScreenshotCallback;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
import android.view.SurfaceControlViewHost;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.infra.AndroidFuture;
@@ -44,15 +47,18 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoSession;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Unit tests for the {@link android.service.games.GameSession}.
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
@SmallTest
@Presubmit
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
public final class GameSessionTest {
private static final long WAIT_FOR_CALLBACK_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(1);
private static final Bitmap TEST_BITMAP = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
@@ -61,8 +67,7 @@ public final class GameSessionTest {
private IGameSessionController mMockGameSessionController;
@Mock
SurfaceControlViewHost mSurfaceControlViewHost;
- private GameSession mGameSession;
-
+ private LifecycleTrackingGameSession mGameSession;
private MockitoSession mMockitoSession;
@Before
@@ -71,7 +76,7 @@ public final class GameSessionTest {
.initMocks(this)
.startMocking();
- mGameSession = new GameSession() {};
+ mGameSession = new LifecycleTrackingGameSession() {};
mGameSession.attach(mMockGameSessionController, /* taskId= */ 10,
InstrumentationRegistry.getContext(),
mSurfaceControlViewHost,
@@ -191,4 +196,196 @@ public final class GameSessionTest {
assertTrue(countDownLatch.await(
WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
+
+ @Test
+ public void moveState_InitializedToInitialized_noLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.INITIALIZED);
+
+ assertThat(mGameSession.mLifecycleMethodCalls.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void moveState_FullLifecycle_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
+
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
+ }
+
+ @Test
+ public void moveState_DestroyedWhenInitialized_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
+
+ // ON_CREATE is always called before ON_DESTROY.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
+ }
+
+ @Test
+ public void moveState_DestroyedWhenFocused_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
+
+ // The ON_GAME_TASK_UNFOCUSED lifecycle event is implied because the session is destroyed
+ // while in focus.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
+ }
+
+ @Test
+ public void moveState_FocusCycled_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
+
+ // Both cycles from focus and unfocus are captured.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED).inOrder();
+ }
+
+ @Test
+ public void moveState_MultipleFocusAndUnfocusCalls_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
+
+ // The second TASK_FOCUSED call and the second TASK_UNFOCUSED call are ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED).inOrder();
+ }
+
+ @Test
+ public void moveState_CreatedAfterFocused_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+
+ // The second CREATED call is ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED).inOrder();
+ }
+
+ @Test
+ public void moveState_UnfocusedWithoutFocused_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
+
+ // The TASK_UNFOCUSED call without an earlier TASK_FOCUSED call is ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE).inOrder();
+ }
+
+ @Test
+ public void moveState_NeverFocused_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
+
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
+ }
+
+ @Test
+ public void moveState_MultipleFocusCalls_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+
+ // The extra TASK_FOCUSED moves are ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED).inOrder();
+ }
+
+ @Test
+ public void moveState_MultipleCreateCalls_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+
+ // The extra CREATE moves are ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE).inOrder();
+ }
+
+ @Test
+ public void moveState_FocusBeforeCreate_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+
+ // The TASK_FOCUSED move before CREATE is ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void moveState_UnfocusBeforeCreate_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
+
+ // The TASK_UNFOCUSED move before CREATE is ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void moveState_FocusWhenDestroyed_ExpectedLifecycleCalls() throws Exception {
+ mGameSession.moveToState(GameSession.LifecycleState.CREATED);
+ mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
+ mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
+
+ // The TASK_FOCUSED move after DESTROYED is ignored.
+ assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
+ LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
+ }
+
+ private static class LifecycleTrackingGameSession extends GameSession {
+ private enum LifecycleMethodCall {
+ ON_CREATE,
+ ON_DESTROY,
+ ON_GAME_TASK_FOCUSED,
+ ON_GAME_TASK_UNFOCUSED
+ }
+
+ final List<LifecycleMethodCall> mLifecycleMethodCalls = new ArrayList<>();
+
+ @Override
+ public void onCreate() {
+ mLifecycleMethodCalls.add(LifecycleMethodCall.ON_CREATE);
+ }
+
+ @Override
+ public void onDestroy() {
+ mLifecycleMethodCalls.add(LifecycleMethodCall.ON_DESTROY);
+ }
+
+ @Override
+ public void onGameTaskFocusChanged(boolean focused) {
+ if (focused) {
+ mLifecycleMethodCalls.add(LifecycleMethodCall.ON_GAME_TASK_FOCUSED);
+ } else {
+ mLifecycleMethodCalls.add(LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED);
+ }
+ }
+ }
}
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 aacd015a04e1..bdfa3bfedb55 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
@@ -34,6 +34,7 @@ import static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.ITaskStackListener;
import android.content.ComponentName;
@@ -331,6 +332,7 @@ public final class GameServiceProviderInstanceImplTest {
.complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10));
assertThat(gameSession10.mIsDestroyed).isFalse();
+ assertThat(gameSession10.mIsFocused).isFalse();
}
@Test
@@ -365,6 +367,45 @@ public final class GameServiceProviderInstanceImplTest {
}
@Test
+ public void gameTaskFocused_propagatedToGameSession() 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));
+
+ assertThat(gameSession10.mIsFocused).isFalse();
+
+ dispatchTaskFocused(10, /*focused=*/ true);
+ assertThat(gameSession10.mIsFocused).isTrue();
+
+ dispatchTaskFocused(10, /*focused=*/ false);
+ assertThat(gameSession10.mIsFocused).isFalse();
+ }
+
+ @Test
+ public void gameTaskAlreadyFocusedWhenGameSessionCreated_propagatedToGameSession()
+ throws Exception {
+ ActivityTaskManager.RootTaskInfo gameATaskInfo = new ActivityTaskManager.RootTaskInfo();
+ gameATaskInfo.taskId = 10;
+ when(mMockActivityTaskManager.getFocusedRootTaskInfo()).thenReturn(gameATaskInfo);
+
+ 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));
+
+ assertThat(gameSession10.mIsFocused).isTrue();
+ }
+
+ @Test
public void gameTaskRemoved_whileAwaitingGameSessionAttached_destroysGameSession()
throws Exception {
mGameServiceProviderInstance.start();
@@ -647,6 +688,12 @@ public final class GameServiceProviderInstanceImplTest {
});
}
+ private void dispatchTaskFocused(int taskId, boolean focused) {
+ dispatchTaskChangeEvent(taskStackListener -> {
+ taskStackListener.onTaskFocusChanged(taskId, focused);
+ });
+ }
+
private void dispatchTaskChangeEvent(
ThrowingConsumer<ITaskStackListener> taskStackListenerConsumer) {
for (ITaskStackListener taskStackListener : mTaskStackListeners) {
@@ -767,10 +814,16 @@ public final class GameServiceProviderInstanceImplTest {
private static class FakeGameSession extends IGameSession.Stub {
boolean mIsDestroyed = false;
+ boolean mIsFocused = false;
@Override
- public void destroy() {
+ public void onDestroyed() {
mIsDestroyed = true;
}
+
+ @Override
+ public void onTaskFocusChanged(boolean focused) {
+ mIsFocused = focused;
+ }
}
} \ No newline at end of file