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

Commit 1dd597d8 authored by Tyler Lacey's avatar Tyler Lacey Committed by Android (Google) Code Review
Browse files

Merge "Add GameSession#takeScreenshot API"

parents 1bc94f4f 197ecb25
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -10958,6 +10958,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