Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 197ecb25 authored by Tyler Lacey's avatar Tyler Lacey
Browse files

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
parent eadc950b
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+125 −2
Original line number Diff line number Diff line
@@ -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;
        }
    }
}
+7 −4
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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
Loading