Add GameSession#takeScreenshot API

The screenshot is taken using a new method added to the
WindowManagerService: captureTaskSnapshot. The existing
WindowManagerService methods for getting a TaskSnapshot
are not suitable because they rely on previously cached
snapshots (e.g., taken when the task is put into the background).

To access the WindowManagerService functionality from
the GameSessionService, an IBinder is passed from the
GameServiceProviderInstanceImpl, which is running on the system server
side (and thus can call the new method) to the GameSessionService when
it is created. The GameSessionService then makes this reference
available to each GameSession when the GameSession is created via the
new GameSession#attach method. The GameSession can then use the IBinder
reference to make an IPC back to the GameServiceProviderInstanceImpl
instance which hosts the GameSessionService. This reference is then used
by the GameSession to request a screenshot.

By using the IBinder in this way, only GameSessions which are created
via the system GameService can access the sensitive screenshot
functionality implemented by GameServiceProviderInstanceImpl via the
new captureTaskSnapshot method.

Test: Manual e2e testing
Bug: 210119689
Bug: 202414447
Bug: 202417255
CTS-Coverage-Bug: 206128693
Change-Id: If42dc9a5a5b6068db8670666a371117cf5865f20
This commit is contained in:
Tyler Lacey
2021-12-29 17:06:25 +00:00
parent eadc950b1e
commit 197ecb2557
11 changed files with 673 additions and 8 deletions

View File

@@ -10929,6 +10929,13 @@ package android.service.games {
method public void onCreate();
method public void onDestroy();
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);
}
public static interface GameSession.ScreenshotCallback {
method public void onFailure(int);
method public void onSuccess(@NonNull android.graphics.Bitmap);
field public static final int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 0; // 0x0
}
public abstract class GameSessionService extends android.app.Service {

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.service.games;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.os.Parcel;
import android.os.Parcelable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Result object for calls to {@link IGameSessionController#takeScreenshot}.
*
* It includes a status (see {@link #getStatus}) and, if the status is
* {@link #GAME_SCREENSHOT_SUCCESS} an {@link android.graphics.Bitmap} result (see {@link
* #getBitmap}).
*
* @hide
*/
public final class GameScreenshotResult implements Parcelable {
/**
* The status of a call to {@link IGameSessionController#takeScreenshot} will be represented by
* one of these values.
*
* @hide
*/
@IntDef(flag = false, prefix = {"GAME_SCREENSHOT_"}, value = {
GAME_SCREENSHOT_SUCCESS, // 0
GAME_SCREENSHOT_ERROR_INTERNAL_ERROR, // 1
})
@Retention(RetentionPolicy.SOURCE)
public @interface GameScreenshotStatus {
}
/**
* Indicates that the result of a call to {@link IGameSessionController#takeScreenshot} was
* successful and an {@link android.graphics.Bitmap} result should be available by calling
* {@link #getBitmap}.
*
* @hide
*/
public static final int GAME_SCREENSHOT_SUCCESS = 0;
/**
* Indicates that the result of a call to {@link IGameSessionController#takeScreenshot} failed
* due to an internal error.
*
* This error may occur if the device is not in a suitable state for a screenshot to be taken
* (e.g., the screen is off) or if the game task is not in a suitable state for a screenshot
* to be taken (e.g., the task is not visible). To make sure that the device and game are
* in a suitable state, the caller can monitor the lifecycle methods for the {@link
* GameSession} to make sure that the game task is focused. If the conditions are met, then the
* caller may try again immediately.
*
* @hide
*/
public static final int GAME_SCREENSHOT_ERROR_INTERNAL_ERROR = 1;
@NonNull
public static final Parcelable.Creator<GameScreenshotResult> CREATOR =
new Parcelable.Creator<GameScreenshotResult>() {
@Override
public GameScreenshotResult createFromParcel(Parcel source) {
return new GameScreenshotResult(
source.readInt(),
source.readParcelable(null, Bitmap.class));
}
@Override
public GameScreenshotResult[] newArray(int size) {
return new GameScreenshotResult[0];
}
};
@GameScreenshotStatus
private final int mStatus;
@Nullable
private final Bitmap mBitmap;
/**
* Creates a successful {@link GameScreenshotResult} with the provided bitmap.
*/
public static GameScreenshotResult createSuccessResult(@NonNull Bitmap bitmap) {
return new GameScreenshotResult(GAME_SCREENSHOT_SUCCESS, bitmap);
}
/**
* Creates a failed {@link GameScreenshotResult} with an
* {@link #GAME_SCREENSHOT_ERROR_INTERNAL_ERROR} status.
*/
public static GameScreenshotResult createInternalErrorResult() {
return new GameScreenshotResult(GAME_SCREENSHOT_ERROR_INTERNAL_ERROR, null);
}
private GameScreenshotResult(@GameScreenshotStatus int status, @Nullable Bitmap bitmap) {
this.mStatus = status;
this.mBitmap = bitmap;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mStatus);
dest.writeParcelable(mBitmap, flags);
}
@GameScreenshotStatus
public int getStatus() {
return mStatus;
}
/**
* Gets the {@link Bitmap} result from a successful screenshot attempt.
*
* @return The bitmap.
* @throws IllegalStateException if this method is called when {@link #getStatus} does not
* return {@link #GAME_SCREENSHOT_SUCCESS}.
*/
@NonNull
public Bitmap getBitmap() {
if (mBitmap == null) {
throw new IllegalStateException("Bitmap not available for failed screenshot result");
}
return mBitmap;
}
@Override
public String toString() {
return "GameScreenshotResult{"
+ "mStatus="
+ mStatus
+ ", has bitmap='"
+ mBitmap != null ? "yes" : "no"
+ "\'}";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof GameScreenshotResult)) {
return false;
}
GameScreenshotResult that = (GameScreenshotResult) o;
return mStatus == that.mStatus
&& Objects.equals(mBitmap, that.mBitmap);
}
@Override
public int hashCode() {
return Objects.hash(mStatus, mBitmap);
}
}

View File

@@ -17,19 +17,29 @@
package android.service.games;
import android.annotation.Hide;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Slog;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.function.pooled.PooledLambda;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
/**
* An active game session, providing a facility for the implementation to interact with the game.
*
@@ -42,6 +52,8 @@ import com.android.internal.util.function.pooled.PooledLambda;
@SystemApi
public abstract class GameSession {
private static final String TAG = "GameSession";
final IGameSession mInterface = new IGameSession.Stub() {
@Override
public void destroy() {
@@ -50,15 +62,24 @@ public abstract class GameSession {
}
};
private IGameSessionController mGameSessionController;
private int mTaskId;
private GameSessionRootView mGameSessionRootView;
private SurfaceControlViewHost mSurfaceControlViewHost;
@Hide
void attach(
/**
* @hide
*/
@VisibleForTesting
public void attach(
IGameSessionController gameSessionController,
int taskId,
@NonNull Context context,
@NonNull SurfaceControlViewHost surfaceControlViewHost,
int widthPx,
int heightPx) {
mGameSessionController = gameSessionController;
mTaskId = taskId;
mSurfaceControlViewHost = surfaceControlViewHost;
mGameSessionRootView = new GameSessionRootView(context, mSurfaceControlViewHost);
surfaceControlViewHost.setView(mGameSessionRootView, widthPx, heightPx);
@@ -133,4 +154,106 @@ public abstract class GameSession {
mSurfaceControlViewHost.relayout(bounds.width(), bounds.height());
}
}
/**
* Interface for returning screenshot outcome from calls to {@link #takeScreenshot}.
*/
public interface ScreenshotCallback {
/**
* The status of a failed screenshot attempt provided by {@link #onFailure}.
*
* @hide
*/
@IntDef(flag = false, prefix = {"ERROR_TAKE_SCREENSHOT_"}, value = {
ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, // 0
})
@Retention(RetentionPolicy.SOURCE)
@interface ScreenshotFailureStatus {
}
/**
* An error code indicating that an internal error occurred when attempting to take a
* screenshot of the game task. If this code is returned, the caller should verify that the
* conditions for taking a screenshot are met (device screen is on and the game task is
* visible). To do so, the caller can monitor the lifecycle methods for this session to
* make sure that the game task is focused. If the conditions are met, then the caller may
* try again immediately.
*/
int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 0;
/**
* Called when taking the screenshot failed.
* @param statusCode Indicates the reason for failure.
*/
void onFailure(@ScreenshotFailureStatus int statusCode);
/**
* Called when taking the screenshot succeeded.
* @param bitmap The screenshot.
*/
void onSuccess(@NonNull Bitmap bitmap);
}
/**
* Takes a screenshot of the associated game. For this call to succeed, the device screen
* must be turned on and the game task must be visible.
*
* If the callback is called with {@link ScreenshotCallback#onSuccess}, the provided {@link
* Bitmap} may be used.
*
* If the callback is called with {@link ScreenshotCallback#onFailure}, the provided status
* code should be checked.
*
* If the status code is {@link ScreenshotCallback#ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR},
* then the caller should verify that the conditions for calling this method are met (device
* screen is on and the game task is visible). To do so, the caller can monitor the lifecycle
* methods for this session to make sure that the game task is focused. If the conditions are
* met, then the caller may try again immediately.
*
* @param executor Executor on which to run the callback.
* @param callback The callback invoked when taking screenshot has succeeded
* or failed.
* @throws IllegalStateException if this method is called prior to {@link #onCreate}.
*/
public void takeScreenshot(@NonNull Executor executor, @NonNull ScreenshotCallback callback) {
if (mGameSessionController == null) {
throw new IllegalStateException("Can not call before onCreate()");
}
AndroidFuture<GameScreenshotResult> takeScreenshotResult =
new AndroidFuture<GameScreenshotResult>().whenCompleteAsync((result, error) -> {
handleScreenshotResult(callback, result, error);
}, executor);
try {
mGameSessionController.takeScreenshot(mTaskId, takeScreenshotResult);
} catch (RemoteException ex) {
takeScreenshotResult.completeExceptionally(ex);
}
}
private void handleScreenshotResult(
@NonNull ScreenshotCallback callback,
@NonNull GameScreenshotResult result,
@NonNull Throwable error) {
if (error != null) {
Slog.w(TAG, error.getMessage(), error.getCause());
callback.onFailure(
ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR);
return;
}
@GameScreenshotResult.GameScreenshotStatus int status = result.getStatus();
switch (status) {
case GameScreenshotResult.GAME_SCREENSHOT_SUCCESS:
callback.onSuccess(result.getBitmap());
break;
case GameScreenshotResult.GAME_SCREENSHOT_ERROR_INTERNAL_ERROR:
Slog.w(TAG, "Error taking screenshot");
callback.onFailure(
ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR);
break;
}
}
}

View File

@@ -52,8 +52,6 @@ import java.util.Objects;
*/
@SystemApi
public abstract class GameSessionService extends Service {
private static final String TAG = "GameSessionService";
/**
* The {@link Intent} action used when binding to the service.
* To be supported, the service must require the
@@ -67,11 +65,13 @@ public abstract class GameSessionService extends Service {
private final IGameSessionService mInterface = new IGameSessionService.Stub() {
@Override
public void create(
IGameSessionController gameSessionController,
CreateGameSessionRequest createGameSessionRequest,
GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
AndroidFuture gameSessionFuture) {
Handler.getMain().post(PooledLambda.obtainRunnable(
GameSessionService::doCreate, GameSessionService.this,
gameSessionController,
createGameSessionRequest,
gameSessionViewHostConfiguration,
gameSessionFuture));
@@ -101,6 +101,7 @@ public abstract class GameSessionService extends Service {
}
private void doCreate(
IGameSessionController gameSessionController,
CreateGameSessionRequest createGameSessionRequest,
GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
AndroidFuture<CreateGameSessionResult> createGameSessionResultFuture) {
@@ -119,7 +120,10 @@ public abstract class GameSessionService extends Service {
SurfaceControlViewHost surfaceControlViewHost =
new SurfaceControlViewHost(this, display, hostToken);
gameSession.attach(this,
gameSession.attach(
gameSessionController,
createGameSessionRequest.getTaskId(),
this,
surfaceControlViewHost,
gameSessionViewHostConfiguration.mWidthPx,
gameSessionViewHostConfiguration.mHeightPx);
@@ -130,7 +134,6 @@ public abstract class GameSessionService extends Service {
createGameSessionResultFuture.complete(createGameSessionResult);
gameSession.doCreate();
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.service.games;
import com.android.internal.infra.AndroidFuture;
/**
* @hide
*/
oneway interface IGameSessionController {
void takeScreenshot(int taskId, in AndroidFuture gameScreenshotResultFuture);
}

View File

@@ -16,6 +16,7 @@
package android.service.games;
import android.service.games.IGameSessionController;
import android.service.games.IGameSession;
import android.service.games.CreateGameSessionRequest;
import android.service.games.GameSessionViewHostConfiguration;
@@ -28,6 +29,7 @@ import com.android.internal.infra.AndroidFuture;
*/
oneway interface IGameSessionService {
void create(
in IGameSessionController gameSessionController,
in CreateGameSessionRequest createGameSessionRequest,
in GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
in AndroidFuture /* T=CreateGameSessionResult */ createGameSessionResultFuture);

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.Intent;
import android.os.ServiceManager;
import android.service.games.GameService;
import android.service.games.GameSessionService;
import android.service.games.IGameService;
@@ -29,6 +30,7 @@ import com.android.internal.infra.ServiceConnector;
import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerService;
final class GameServiceProviderInstanceFactoryImpl implements GameServiceProviderInstanceFactory {
private final Context mContext;
@@ -46,6 +48,7 @@ final class GameServiceProviderInstanceFactoryImpl implements GameServiceProvide
BackgroundThread.getExecutor(),
new GameClassifierImpl(mContext.getPackageManager()),
ActivityTaskManager.getService(),
(WindowManagerService) ServiceManager.getService(Context.WINDOW_SERVICE),
LocalServices.getService(WindowManagerInternal.class),
new GameServiceConnector(mContext, gameServiceProviderConfiguration),
new GameSessionServiceConnector(mContext, gameServiceProviderConfiguration));

View File

@@ -22,24 +22,29 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.app.IActivityTaskManager;
import android.app.TaskStackListener;
import android.content.ComponentName;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.games.CreateGameSessionRequest;
import android.service.games.CreateGameSessionResult;
import android.service.games.GameScreenshotResult;
import android.service.games.GameSessionViewHostConfiguration;
import android.service.games.GameStartedEvent;
import android.service.games.IGameService;
import android.service.games.IGameServiceController;
import android.service.games.IGameSession;
import android.service.games.IGameSessionController;
import android.service.games.IGameSessionService;
import android.util.Slog;
import android.view.SurfaceControlViewHost.SurfacePackage;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ServiceConnector;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerService;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -87,11 +92,24 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
};
private final IGameSessionController mGameSessionController =
new IGameSessionController.Stub() {
@Override
public void takeScreenshot(int taskId,
@NonNull AndroidFuture gameScreenshotResultFuture) {
mBackgroundExecutor.execute(() -> {
GameServiceProviderInstanceImpl.this.takeScreenshot(taskId,
gameScreenshotResultFuture);
});
}
};
private final Object mLock = new Object();
private final UserHandle mUserHandle;
private final Executor mBackgroundExecutor;
private final GameClassifier mGameClassifier;
private final IActivityTaskManager mActivityTaskManager;
private final WindowManagerService mWindowManagerService;
private final WindowManagerInternal mWindowManagerInternal;
private final ServiceConnector<IGameService> mGameServiceConnector;
private final ServiceConnector<IGameSessionService> mGameSessionServiceConnector;
@@ -107,6 +125,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
@NonNull Executor backgroundExecutor,
@NonNull GameClassifier gameClassifier,
@NonNull IActivityTaskManager activityTaskManager,
@NonNull WindowManagerService windowManagerService,
@NonNull WindowManagerInternal windowManagerInternal,
@NonNull ServiceConnector<IGameService> gameServiceConnector,
@NonNull ServiceConnector<IGameSessionService> gameSessionServiceConnector) {
@@ -114,6 +133,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
mBackgroundExecutor = backgroundExecutor;
mGameClassifier = gameClassifier;
mActivityTaskManager = activityTaskManager;
mWindowManagerService = windowManagerService;
mWindowManagerInternal = windowManagerInternal;
mGameServiceConnector = gameServiceConnector;
mGameSessionServiceConnector = gameSessionServiceConnector;
@@ -300,6 +320,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
taskId,
existingGameSessionRecord.getComponentName().getPackageName());
gameService.create(
mGameSessionController,
createGameSessionRequest,
gameSessionViewHostConfiguration,
createGameSessionResultFuture);
@@ -404,7 +425,6 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
}
@Nullable
private GameSessionViewHostConfiguration createViewHostConfigurationForTask(int taskId) {
RunningTaskInfo runningTaskInfo = getRunningTaskInfoForTask(taskId);
@@ -440,4 +460,26 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
return null;
}
@VisibleForTesting
void takeScreenshot(int taskId, @NonNull AndroidFuture callback) {
synchronized (mLock) {
boolean isTaskAssociatedWithGameSession = mGameSessions.containsKey(taskId);
if (!isTaskAssociatedWithGameSession) {
Slog.w(TAG, "No game session found for id: " + taskId);
callback.complete(GameScreenshotResult.createInternalErrorResult());
return;
}
}
mBackgroundExecutor.execute(() -> {
final Bitmap bitmap = mWindowManagerService.captureTaskBitmap(taskId);
if (bitmap == null) {
Slog.w(TAG, "Could not get bitmap for id: " + taskId);
callback.complete(GameScreenshotResult.createInternalErrorResult());
} else {
callback.complete(GameScreenshotResult.createSuccessResult(bitmap));
}
});
}
}

View File

@@ -3837,6 +3837,40 @@ public class WindowManagerService extends IWindowManager.Stub
isLowResolution);
}
/**
* Generates and returns an up-to-date {@link Bitmap} for the specified taskId. The returned
* bitmap will be full size and will not include any secure content.
*
* @param taskId The task ID of the task for which a snapshot is requested.
* @return The Bitmap, or null if no task with the specified ID can be found or the bitmap could
* not be generated.
*/
@Nullable public Bitmap captureTaskBitmap(int taskId) {
if (mTaskSnapshotController.shouldDisableSnapshots()) {
return null;
}
synchronized (mGlobalLock) {
final Task task = mRoot.anyTaskForId(taskId);
if (task == null) {
return null;
}
task.getBounds(mTmpRect);
final SurfaceControl sc = task.getSurfaceControl();
final SurfaceControl.ScreenshotHardwareBuffer buffer = SurfaceControl.captureLayers(
new SurfaceControl.LayerCaptureArgs.Builder(sc)
.setSourceCrop(mTmpRect)
.build());
if (buffer == null) {
Slog.w(TAG, "Could not get screenshot buffer for taskId: " + taskId);
return null;
}
return buffer.asBitmap();
}
}
/**
* In case a task write/delete operation was lost because the system crashed, this makes sure to
* clean up the directory to remove obsolete files.

View File

@@ -0,0 +1,194 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.service.games;
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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import android.graphics.Bitmap;
import android.platform.test.annotations.Presubmit;
import android.service.games.GameSession.ScreenshotCallback;
import android.view.SurfaceControlViewHost;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.infra.AndroidFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Unit tests for the {@link android.service.games.GameSession}.
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
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);
@Mock
private IGameSessionController mMockGameSessionController;
@Mock
SurfaceControlViewHost mSurfaceControlViewHost;
private GameSession mGameSession;
private MockitoSession mMockitoSession;
@Before
public void setUp() {
mMockitoSession = mockitoSession()
.initMocks(this)
.startMocking();
mGameSession = new GameSession() {};
mGameSession.attach(mMockGameSessionController, /* taskId= */ 10,
InstrumentationRegistry.getContext(),
mSurfaceControlViewHost,
/* widthPx= */ 0, /* heightPx= */0);
}
@After
public void tearDown() {
mMockitoSession.finishMocking();
}
@Test
public void takeScreenshot_attachNotCalled_throwsIllegalStateException() throws Exception {
GameSession gameSession = new GameSession() {};
try {
gameSession.takeScreenshot(DIRECT_EXECUTOR,
new ScreenshotCallback() {
@Override
public void onFailure(int statusCode) {
fail();
}
@Override
public void onSuccess(Bitmap bitmap) {
fail();
}
});
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void takeScreenshot_gameManagerException_returnsInternalError() throws Exception {
doAnswer(invocation -> {
AndroidFuture result = invocation.getArgument(1);
result.completeExceptionally(new Exception());
return null;
}).when(mMockGameSessionController).takeScreenshot(anyInt(), any());
CountDownLatch countDownLatch = new CountDownLatch(1);
mGameSession.takeScreenshot(DIRECT_EXECUTOR,
new ScreenshotCallback() {
@Override
public void onFailure(int statusCode) {
assertEquals(ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
statusCode);
countDownLatch.countDown();
}
@Override
public void onSuccess(Bitmap bitmap) {
fail();
}
});
assertTrue(countDownLatch.await(
WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void takeScreenshot_gameManagerError_returnsInternalError() throws Exception {
doAnswer(invocation -> {
AndroidFuture result = invocation.getArgument(1);
result.complete(GameScreenshotResult.createInternalErrorResult());
return null;
}).when(mMockGameSessionController).takeScreenshot(anyInt(), any());
CountDownLatch countDownLatch = new CountDownLatch(1);
mGameSession.takeScreenshot(DIRECT_EXECUTOR,
new ScreenshotCallback() {
@Override
public void onFailure(int statusCode) {
assertEquals(ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
statusCode);
countDownLatch.countDown();
}
@Override
public void onSuccess(Bitmap bitmap) {
fail();
}
});
assertTrue(countDownLatch.await(
WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void takeScreenshot_gameManagerSuccess_returnsBitmap() throws Exception {
doAnswer(invocation -> {
AndroidFuture result = invocation.getArgument(1);
result.complete(GameScreenshotResult.createSuccessResult(TEST_BITMAP));
return null;
}).when(mMockGameSessionController).takeScreenshot(anyInt(), any());
CountDownLatch countDownLatch = new CountDownLatch(1);
mGameSession.takeScreenshot(DIRECT_EXECUTOR,
new ScreenshotCallback() {
@Override
public void onFailure(int statusCode) {
fail();
}
@Override
public void onSuccess(Bitmap bitmap) {
assertEquals(TEST_BITMAP, bitmap);
countDownLatch.countDown();
}
});
assertTrue(countDownLatch.await(
WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}

View File

@@ -23,6 +23,7 @@ import static com.android.server.app.GameServiceProviderInstanceImplTest.FakeGam
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -37,17 +38,20 @@ import android.app.IActivityTaskManager;
import android.app.ITaskStackListener;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.service.games.CreateGameSessionRequest;
import android.service.games.CreateGameSessionResult;
import android.service.games.GameScreenshotResult;
import android.service.games.GameSessionViewHostConfiguration;
import android.service.games.GameStartedEvent;
import android.service.games.IGameService;
import android.service.games.IGameServiceController;
import android.service.games.IGameSession;
import android.service.games.IGameSessionController;
import android.service.games.IGameSessionService;
import android.view.SurfaceControlViewHost.SurfacePackage;
@@ -59,6 +63,7 @@ import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.FunctionalUtils.ThrowingConsumer;
import com.android.internal.util.Preconditions;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerService;
import org.junit.After;
import org.junit.Before;
@@ -93,11 +98,15 @@ 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 Bitmap TEST_BITMAP = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
private MockitoSession mMockingSession;
private GameServiceProviderInstance mGameServiceProviderInstance;
@Mock
private IActivityTaskManager mMockActivityTaskManager;
@Mock
private WindowManagerService mMockWindowManagerService;
@Mock
private WindowManagerInternal mMockWindowManagerInternal;
private FakeGameClassifier mFakeGameClassifier;
private FakeGameService mFakeGameService;
@@ -142,6 +151,7 @@ public final class GameServiceProviderInstanceImplTest {
ConcurrentUtils.DIRECT_EXECUTOR,
mFakeGameClassifier,
mMockActivityTaskManager,
mMockWindowManagerService,
mMockWindowManagerInternal,
mFakeGameServiceConnector,
mFakeGameSessionServiceConnector);
@@ -574,6 +584,41 @@ public final class GameServiceProviderInstanceImplTest {
assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isFalse();
}
@Test
public void takeScreenshot_failureNoBitmapCaptured() throws Exception {
mGameServiceProviderInstance.start();
startTask(10, GAME_A_MAIN_ACTIVITY);
mFakeGameService.requestCreateGameSession(10);
IGameSessionController gameSessionController = getOnlyElement(
mFakeGameSessionService.getCapturedCreateInvocations()).mGameSessionController;
AndroidFuture<GameScreenshotResult> resultFuture = new AndroidFuture<>();
gameSessionController.takeScreenshot(10, resultFuture);
GameScreenshotResult result = resultFuture.get();
assertEquals(GameScreenshotResult.GAME_SCREENSHOT_ERROR_INTERNAL_ERROR,
result.getStatus());
verify(mMockWindowManagerService).captureTaskBitmap(10);
}
@Test
public void takeScreenshot_success() throws Exception {
when(mMockWindowManagerService.captureTaskBitmap(10)).thenReturn(TEST_BITMAP);
mGameServiceProviderInstance.start();
startTask(10, GAME_A_MAIN_ACTIVITY);
mFakeGameService.requestCreateGameSession(10);
IGameSessionController gameSessionController = getOnlyElement(
mFakeGameSessionService.getCapturedCreateInvocations()).mGameSessionController;
AndroidFuture<GameScreenshotResult> resultFuture = new AndroidFuture<>();
gameSessionController.takeScreenshot(10, resultFuture);
GameScreenshotResult result = resultFuture.get();
assertEquals(GameScreenshotResult.GAME_SCREENSHOT_SUCCESS, result.getStatus());
assertEquals(TEST_BITMAP, result.getBitmap());
}
private void startTask(int taskId, ComponentName componentName) {
RunningTaskInfo runningTaskInfo = new RunningTaskInfo();
runningTaskInfo.taskId = taskId;
@@ -677,12 +722,15 @@ public final class GameServiceProviderInstanceImplTest {
new HashMap<>();
public static final class CapturedCreateInvocation {
private final IGameSessionController mGameSessionController;
private final CreateGameSessionRequest mCreateGameSessionRequest;
private final GameSessionViewHostConfiguration mGameSessionViewHostConfiguration;
CapturedCreateInvocation(
IGameSessionController gameSessionController,
CreateGameSessionRequest createGameSessionRequest,
GameSessionViewHostConfiguration gameSessionViewHostConfiguration) {
mGameSessionController = gameSessionController;
mCreateGameSessionRequest = createGameSessionRequest;
mGameSessionViewHostConfiguration = gameSessionViewHostConfiguration;
}
@@ -698,12 +746,14 @@ public final class GameServiceProviderInstanceImplTest {
@Override
public void create(
IGameSessionController gameSessionController,
CreateGameSessionRequest createGameSessionRequest,
GameSessionViewHostConfiguration gameSessionViewHostConfiguration,
AndroidFuture createGameSessionResultFuture) {
mCapturedCreateInvocations.add(
new CapturedCreateInvocation(
gameSessionController,
createGameSessionRequest,
gameSessionViewHostConfiguration));
@@ -723,4 +773,4 @@ public final class GameServiceProviderInstanceImplTest {
mIsDestroyed = true;
}
}
}
}