Add a GameSession API for restarting the associated game.

Since restarting a process must be done from the system server side,
we accomplish this by passing a IBinder through to the GameSession
allowing the GameSession to make IPCs to its hosting
GameServiceProviderInstanceImpl. As an additional layer of security,
the implementation takes a taskId and only restarts the given
game if the taskId matches an active game session.

Test: atest FrameworksMockingServicesTests and manual e2e testing
Bug: 210127167
Bug: 202414447
Bug: 202417255
CTS-Coverage-Bug: 206128693
Change-Id: I944da060323ac1779cd2c61d1e411e9868caed91
This commit is contained in:
shannonchen
2022-01-21 21:14:37 +00:00
committed by Shannon Chen
parent aad39a3889
commit 614ba13847
6 changed files with 179 additions and 7 deletions

View File

@@ -11182,6 +11182,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);
}

View File

@@ -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;
@@ -270,6 +271,25 @@ public abstract class GameSession {
mGameSessionRootView.addView(view, layoutParams);
}
/**
* 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

View File

@@ -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);
}
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES)")
void restartGame(in int taskId);
}

View File

@@ -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),

View File

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

View File

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