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:
committed by
Shannon Chen
parent
aad39a3889
commit
614ba13847
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user