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:
@@ -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 {
|
||||
|
||||
181
core/java/android/service/games/GameScreenshotResult.java
Normal file
181
core/java/android/service/games/GameScreenshotResult.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
26
core/java/android/service/games/IGameSessionController.aidl
Normal file
26
core/java/android/service/games/IGameSessionController.aidl
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user