Merge "Add GameSession#takeScreenshot API"
This commit is contained in:
@@ -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
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3842,6 +3842,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