diff options
| author | Tyler Lacey <tlacey@google.com> | 2021-12-29 17:06:25 +0000 |
|---|---|---|
| committer | Tyler Lacey <tlacey@google.com> | 2022-01-21 16:54:39 +0000 |
| commit | 197ecb2557ba015c6fc6db8d992f2b4d02c82167 (patch) | |
| tree | ba5dee7aa1ae5d3dbe17ef0242edbe1c4bfb3051 /core/java | |
| parent | eadc950b1e57c4af69bfd9f753d4b9e463b3667b (diff) | |
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
Diffstat (limited to 'core/java')
5 files changed, 341 insertions, 6 deletions
diff --git a/core/java/android/service/games/GameScreenshotResult.java b/core/java/android/service/games/GameScreenshotResult.java new file mode 100644 index 000000000000..ae76e08c7971 --- /dev/null +++ b/core/java/android/service/games/GameScreenshotResult.java @@ -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); + } +} diff --git a/core/java/android/service/games/GameSession.java b/core/java/android/service/games/GameSession.java index 1a5331f10525..b6fe067cfc71 100644 --- a/core/java/android/service/games/GameSession.java +++ b/core/java/android/service/games/GameSession.java @@ -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; + } + } } diff --git a/core/java/android/service/games/GameSessionService.java b/core/java/android/service/games/GameSessionService.java index 195a0f233307..df5bad5c53b2 100644 --- a/core/java/android/service/games/GameSessionService.java +++ b/core/java/android/service/games/GameSessionService.java @@ -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(); } diff --git a/core/java/android/service/games/IGameSessionController.aidl b/core/java/android/service/games/IGameSessionController.aidl new file mode 100644 index 000000000000..fe1d3629918e --- /dev/null +++ b/core/java/android/service/games/IGameSessionController.aidl @@ -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); +}
\ No newline at end of file diff --git a/core/java/android/service/games/IGameSessionService.aidl b/core/java/android/service/games/IGameSessionService.aidl index dcbcbc16a374..37cde561f549 100644 --- a/core/java/android/service/games/IGameSessionService.aidl +++ b/core/java/android/service/games/IGameSessionService.aidl @@ -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); |
