diff options
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 |