diff options
6 files changed, 179 insertions, 7 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ea2a6418ad54..5e232291374e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11184,6 +11184,7 @@ package android.service.games { method public void onCreate(); method public void onDestroy(); method public void onGameTaskFocusChanged(boolean); + method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public final boolean restartGame(); 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 cb5c19b72bd0..f4baedc18acf 100644 --- a/core/java/android/service/games/GameSession.java +++ b/core/java/android/service/games/GameSession.java @@ -20,6 +20,7 @@ import android.annotation.Hide; import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; import android.content.res.Configuration; @@ -271,6 +272,25 @@ public abstract class GameSession { } /** + * Attempts to force stop and relaunch the game associated with the current session. This may + * be useful, for example, after applying settings that will not take effect until the game is + * restarted. + * + * @return {@code true} if the game was successfully restarted; otherwise, {@code false}. + */ + @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) + public final boolean restartGame() { + try { + mGameSessionController.restartGame(mTaskId); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to restart game", e); + return false; + } + + return true; + } + + /** * Root view of the {@link SurfaceControlViewHost} associated with the {@link GameSession} * instance. It is responsible for observing changes in the size of the window and resizing * itself to match. diff --git a/core/java/android/service/games/IGameSessionController.aidl b/core/java/android/service/games/IGameSessionController.aidl index fe1d3629918e..84311dc0aedf 100644 --- a/core/java/android/service/games/IGameSessionController.aidl +++ b/core/java/android/service/games/IGameSessionController.aidl @@ -16,6 +16,7 @@ package android.service.games; +import android.content.Intent; import com.android.internal.infra.AndroidFuture; /** @@ -23,4 +24,6 @@ import com.android.internal.infra.AndroidFuture; */ oneway interface IGameSessionController { void takeScreenshot(int taskId, in AndroidFuture gameScreenshotResultFuture); -}
\ No newline at end of file + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES)") + void restartGame(in int taskId); +} diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java index b4c43f6f1b93..73278e471062 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceFactoryImpl.java @@ -17,6 +17,7 @@ package com.android.server.app; import android.annotation.NonNull; +import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.content.Context; import android.content.Intent; @@ -36,17 +37,19 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide private final Context mContext; GameServiceProviderInstanceFactoryImpl(@NonNull Context context) { - this.mContext = context; + mContext = context; } @NonNull @Override - public GameServiceProviderInstance create(@NonNull - GameServiceProviderConfiguration gameServiceProviderConfiguration) { + public GameServiceProviderInstance create( + @NonNull GameServiceProviderConfiguration gameServiceProviderConfiguration) { return new GameServiceProviderInstanceImpl( gameServiceProviderConfiguration.getUserHandle(), BackgroundThread.getExecutor(), + mContext, new GameClassifierImpl(mContext.getPackageManager()), + ActivityManager.getService(), ActivityTaskManager.getService(), (WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE), LocalServices.getService(WindowManagerInternal.class), diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java index 43c9839a04d8..8578de7ac4b4 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java @@ -16,13 +16,18 @@ package com.android.server.app; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; +import android.app.IActivityManager; import android.app.IActivityTaskManager; import android.app.TaskStackListener; import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Rect; import android.os.RemoteException; @@ -110,12 +115,23 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan gameScreenshotResultFuture); }); } + + @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) + public void restartGame(int taskId) { + mContext.enforceCallingPermission(Manifest.permission.FORCE_STOP_PACKAGES, + "restartGame()"); + mBackgroundExecutor.execute(() -> { + GameServiceProviderInstanceImpl.this.restartGame(taskId); + }); + } }; private final Object mLock = new Object(); private final UserHandle mUserHandle; private final Executor mBackgroundExecutor; + private final Context mContext; private final GameClassifier mGameClassifier; + private final IActivityManager mActivityManager; private final IActivityTaskManager mActivityTaskManager; private final WindowManagerService mWindowManagerService; private final WindowManagerInternal mWindowManagerInternal; @@ -131,7 +147,9 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan GameServiceProviderInstanceImpl( @NonNull UserHandle userHandle, @NonNull Executor backgroundExecutor, + @NonNull Context context, @NonNull GameClassifier gameClassifier, + @NonNull IActivityManager activityManager, @NonNull IActivityTaskManager activityTaskManager, @NonNull WindowManagerService windowManagerService, @NonNull WindowManagerInternal windowManagerInternal, @@ -139,7 +157,9 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan @NonNull ServiceConnector<IGameSessionService> gameSessionServiceConnector) { mUserHandle = userHandle; mBackgroundExecutor = backgroundExecutor; + mContext = context; mGameClassifier = gameClassifier; + mActivityManager = activityManager; mActivityTaskManager = activityTaskManager; mWindowManagerService = windowManagerService; mWindowManagerInternal = windowManagerInternal; @@ -310,6 +330,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan + ") is not awaiting game session request. Ignoring."); return; } + mGameSessions.put(taskId, existingGameSessionRecord.withGameSessionRequested()); GameSessionViewHostConfiguration gameSessionViewHostConfiguration = createViewHostConfigurationForTask(taskId); @@ -532,4 +553,27 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } }); } + + private void restartGame(int taskId) { + String packageName; + + synchronized (mLock) { + boolean isTaskAssociatedWithGameSession = mGameSessions.containsKey(taskId); + if (!isTaskAssociatedWithGameSession) { + return; + } + + packageName = mGameSessions.get(taskId).getComponentName().getPackageName(); + } + + try { + mActivityManager.forceStopPackage(packageName, UserHandle.USER_CURRENT); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + Intent launchIntent = + mContext.getPackageManager().getLaunchIntentForPackage(packageName); + mContext.startActivity(launchIntent); + } } 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 bdfa3bfedb55..d5e4710095f2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java @@ -18,6 +18,8 @@ package com.android.server.app; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.app.GameServiceProviderInstanceImplTest.FakeGameService.GameServiceState; import static com.google.common.collect.Iterables.getOnlyElement; @@ -28,16 +30,19 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verifyZeroInteractions; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; +import android.app.IActivityManager; import android.app.IActivityTaskManager; import android.app.ITaskStackListener; import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Rect; @@ -57,6 +62,7 @@ import android.service.games.IGameSessionService; import android.view.SurfaceControlViewHost.SurfacePackage; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.internal.infra.AndroidFuture; @@ -99,6 +105,10 @@ public final class GameServiceProviderInstanceImplTest { private static final ComponentName GAME_A_MAIN_ACTIVITY = new ComponentName(GAME_A_PACKAGE, "com.package.game.a.MainActivity"); + private static final String GAME_B_PACKAGE = "com.package.game.b"; + private static final ComponentName GAME_B_MAIN_ACTIVITY = + new ComponentName(GAME_B_PACKAGE, "com.package.game.b.MainActivity"); + private static final Bitmap TEST_BITMAP = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888); private MockitoSession mMockingSession; @@ -109,6 +119,9 @@ public final class GameServiceProviderInstanceImplTest { private WindowManagerService mMockWindowManagerService; @Mock private WindowManagerInternal mMockWindowManagerInternal; + @Mock + private IActivityManager mMockActivityManager; + private FakeContext mFakeContext; private FakeGameClassifier mFakeGameClassifier; private FakeGameService mFakeGameService; private FakeServiceConnector<IGameService> mFakeGameServiceConnector; @@ -117,6 +130,9 @@ public final class GameServiceProviderInstanceImplTest { private ArrayList<ITaskStackListener> mTaskStackListeners; private ArrayList<RunningTaskInfo> mRunningTaskInfos; + @Mock + private PackageManager mMockPackageManager; + @Before public void setUp() throws PackageManager.NameNotFoundException, RemoteException { mMockingSession = mockitoSession() @@ -124,8 +140,11 @@ public final class GameServiceProviderInstanceImplTest { .strictness(Strictness.LENIENT) .startMocking(); + mFakeContext = new FakeContext(InstrumentationRegistry.getInstrumentation().getContext()); + mFakeGameClassifier = new FakeGameClassifier(); mFakeGameClassifier.recordGamePackage(GAME_A_PACKAGE); + mFakeGameClassifier.recordGamePackage(GAME_B_PACKAGE); mFakeGameService = new FakeGameService(); mFakeGameServiceConnector = new FakeServiceConnector<>(mFakeGameService); @@ -150,7 +169,9 @@ public final class GameServiceProviderInstanceImplTest { mGameServiceProviderInstance = new GameServiceProviderInstanceImpl( new UserHandle(USER_ID), ConcurrentUtils.DIRECT_EXECUTOR, + mFakeContext, mFakeGameClassifier, + mMockActivityManager, mMockActivityTaskManager, mMockWindowManagerService, mMockWindowManagerInternal, @@ -660,6 +681,58 @@ public final class GameServiceProviderInstanceImplTest { assertEquals(TEST_BITMAP, result.getBitmap()); } + @Test + public void restartGame_taskIdAssociatedWithGame_restartsTargetGame() throws Exception { + Intent launchIntent = new Intent("com.test.ACTION_LAUNCH_GAME_PACKAGE") + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + when(mMockPackageManager.getLaunchIntentForPackage(GAME_A_PACKAGE)) + .thenReturn(launchIntent); + + 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)); + + startTask(11, GAME_B_MAIN_ACTIVITY); + mFakeGameService.requestCreateGameSession(11); + + FakeGameSession gameSession11 = new FakeGameSession(); + SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class); + mFakeGameSessionService.removePendingFutureForTaskId(11) + .complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11)); + + mFakeGameSessionService.getCapturedCreateInvocations().get(0) + .mGameSessionController.restartGame(10); + + verify(mMockActivityManager).forceStopPackage(GAME_A_PACKAGE, UserHandle.USER_CURRENT); + assertThat(mFakeContext.getLastStartedIntent()).isEqualTo(launchIntent); + } + + @Test + public void restartGame_taskIdNotAssociatedWithGame_noOp() 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)); + + getOnlyElement( + mFakeGameSessionService.getCapturedCreateInvocations()) + .mGameSessionController.restartGame(11); + + verifyZeroInteractions(mMockActivityManager); + assertThat(mFakeContext.getLastStartedIntent()).isNull(); + } + private void startTask(int taskId, ComponentName componentName) { RunningTaskInfo runningTaskInfo = new RunningTaskInfo(); runningTaskInfo.taskId = taskId; @@ -826,4 +899,32 @@ public final class GameServiceProviderInstanceImplTest { mIsFocused = focused; } } -}
\ No newline at end of file + + private final class FakeContext extends ContextWrapper { + private Intent mLastStartedIntent; + + FakeContext(Context base) { + super(base); + } + + @Override + public PackageManager getPackageManager() { + return mMockPackageManager; + } + + @Override + public void startActivity(Intent intent) { + mLastStartedIntent = intent; + } + + @Override + public void enforceCallingPermission(String permission, @Nullable String message) { + // Do nothing. + } + + Intent getLastStartedIntent() { + return mLastStartedIntent; + } + } + +} |