Loading core/api/system-current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -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); } core/java/android/service/games/GameSession.java +145 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; } /** Loading @@ -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 Loading core/java/android/service/games/IGameSession.aidl +2 −1 Original line number Diff line number Diff line Loading @@ -20,5 +20,6 @@ package android.service.games; * @hide */ oneway interface IGameSession { void destroy(); void onDestroyed(); void onTaskFocusChanged(boolean focused); } services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +52 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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) { Loading Loading @@ -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 = Loading @@ -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, Loading Loading @@ -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); } Loading Loading @@ -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); } Loading services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java +202 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -61,8 +67,7 @@ public final class GameSessionTest { private IGameSessionController mMockGameSessionController; @Mock SurfaceControlViewHost mSurfaceControlViewHost; private GameSession mGameSession; private LifecycleTrackingGameSession mGameSession; private MockitoSession mMockitoSession; @Before Loading @@ -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, Loading Loading @@ -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); } } } } Loading
core/api/system-current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -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); }
core/java/android/service/games/GameSession.java +145 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; } /** Loading @@ -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 Loading
core/java/android/service/games/IGameSession.aidl +2 −1 Original line number Diff line number Diff line Loading @@ -20,5 +20,6 @@ package android.service.games; * @hide */ oneway interface IGameSession { void destroy(); void onDestroyed(); void onTaskFocusChanged(boolean focused); }
services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +52 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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) { Loading Loading @@ -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 = Loading @@ -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, Loading Loading @@ -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); } Loading Loading @@ -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); } Loading
services/tests/mockingservicestests/src/android/service/games/GameSessionTest.java +202 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -61,8 +67,7 @@ public final class GameSessionTest { private IGameSessionController mMockGameSessionController; @Mock SurfaceControlViewHost mSurfaceControlViewHost; private GameSession mGameSession; private LifecycleTrackingGameSession mGameSession; private MockitoSession mMockitoSession; @Before Loading @@ -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, Loading Loading @@ -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); } } } }