1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.service.games; 18 19 import android.annotation.Hide; 20 import android.annotation.IntDef; 21 import android.annotation.MainThread; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.annotation.SystemApi; 26 import android.app.ActivityTaskManager; 27 import android.app.Instrumentation; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.res.Configuration; 31 import android.graphics.Rect; 32 import android.os.Binder; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.RemoteException; 37 import android.os.UserHandle; 38 import android.util.Slog; 39 import android.view.SurfaceControlViewHost; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.FrameLayout; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.internal.infra.AndroidFuture; 46 import com.android.internal.util.function.pooled.PooledLambda; 47 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.Objects; 51 import java.util.concurrent.Executor; 52 53 /** 54 * An active game session, providing a facility for the implementation to interact with the game. 55 * 56 * A Game Service provider should extend the {@link GameSession} to provide their own implementation 57 * which is then returned when a game session is created via 58 * {@link GameSessionService#onNewSession(CreateGameSessionRequest)}. 59 * 60 * This class exposes various lifecycle methods which are guaranteed to be called in the following 61 * fashion: 62 * 63 * {@link #onCreate()}: Will always be the first lifecycle method to be called, once the game 64 * session is created. 65 * 66 * {@link #onGameTaskFocusChanged(boolean)}: Will be called after {@link #onCreate()} with 67 * focused=true when the game task first comes into focus (if it does). If the game task is focused 68 * when the game session is created, this method will be called immediately after 69 * {@link #onCreate()} with focused=true. After this method is called with focused=true, it will be 70 * called again with focused=false when the task goes out of focus. If this method is ever called 71 * with focused=true, it is guaranteed to be called again with focused=false before 72 * {@link #onDestroy()} is called. If the game task never comes into focus during the session 73 * lifetime, this method will never be called. 74 * 75 * {@link #onDestroy()}: Will always be called after {@link #onCreate()}. If the game task ever 76 * comes into focus before the game session is destroyed, then this method will be called after one 77 * or more pairs of calls to {@link #onGameTaskFocusChanged(boolean)}. 78 * 79 * @hide 80 */ 81 @SystemApi 82 public abstract class GameSession { 83 private static final String TAG = "GameSession"; 84 private static final boolean DEBUG = false; 85 86 final IGameSession mInterface = new IGameSession.Stub() { 87 @Override 88 public void onDestroyed() { 89 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 90 GameSession::doDestroy, GameSession.this)); 91 } 92 93 @Override 94 public void onTransientSystemBarVisibilityFromRevealGestureChanged( 95 boolean visibleDueToGesture) { 96 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 97 GameSession::dispatchTransientSystemBarVisibilityFromRevealGestureChanged, 98 GameSession.this, 99 visibleDueToGesture)); 100 } 101 102 @Override 103 public void onTaskFocusChanged(boolean focused) { 104 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 105 GameSession::moveToState, GameSession.this, 106 focused ? LifecycleState.TASK_FOCUSED : LifecycleState.TASK_UNFOCUSED)); 107 } 108 }; 109 110 /** 111 * @hide 112 */ 113 @VisibleForTesting 114 public enum LifecycleState { 115 // Initial state; may transition to CREATED. 116 INITIALIZED, 117 // May transition to TASK_FOCUSED or DESTROYED. 118 CREATED, 119 // May transition to TASK_UNFOCUSED. 120 TASK_FOCUSED, 121 // May transition to TASK_FOCUSED or DESTROYED. 122 TASK_UNFOCUSED, 123 // May not transition once reached. 124 DESTROYED 125 } 126 127 private LifecycleState mLifecycleState = LifecycleState.INITIALIZED; 128 private boolean mAreTransientInsetsVisibleDueToGesture = false; 129 private IGameSessionController mGameSessionController; 130 private Context mContext; 131 private int mTaskId; 132 private GameSessionRootView mGameSessionRootView; 133 private SurfaceControlViewHost mSurfaceControlViewHost; 134 135 /** 136 * @hide 137 */ 138 @VisibleForTesting attach( IGameSessionController gameSessionController, int taskId, @NonNull Context context, @NonNull SurfaceControlViewHost surfaceControlViewHost, int widthPx, int heightPx)139 public void attach( 140 IGameSessionController gameSessionController, 141 int taskId, 142 @NonNull Context context, 143 @NonNull SurfaceControlViewHost surfaceControlViewHost, 144 int widthPx, 145 int heightPx) { 146 mGameSessionController = gameSessionController; 147 mTaskId = taskId; 148 mContext = context; 149 mSurfaceControlViewHost = surfaceControlViewHost; 150 mGameSessionRootView = new GameSessionRootView(context, mSurfaceControlViewHost); 151 surfaceControlViewHost.setView(mGameSessionRootView, widthPx, heightPx); 152 } 153 154 @Hide doCreate()155 void doCreate() { 156 moveToState(LifecycleState.CREATED); 157 } 158 159 @Hide doDestroy()160 private void doDestroy() { 161 mSurfaceControlViewHost.release(); 162 moveToState(LifecycleState.DESTROYED); 163 } 164 165 /** @hide */ 166 @VisibleForTesting 167 @MainThread dispatchTransientSystemBarVisibilityFromRevealGestureChanged( boolean visibleDueToGesture)168 public void dispatchTransientSystemBarVisibilityFromRevealGestureChanged( 169 boolean visibleDueToGesture) { 170 boolean didValueChange = mAreTransientInsetsVisibleDueToGesture != visibleDueToGesture; 171 mAreTransientInsetsVisibleDueToGesture = visibleDueToGesture; 172 if (didValueChange) { 173 onTransientSystemBarVisibilityFromRevealGestureChanged(visibleDueToGesture); 174 } 175 } 176 177 /** 178 * @hide 179 */ 180 @VisibleForTesting 181 @MainThread moveToState(LifecycleState newLifecycleState)182 public void moveToState(LifecycleState newLifecycleState) { 183 if (DEBUG) { 184 Slog.d(TAG, "moveToState: " + mLifecycleState + " -> " + newLifecycleState); 185 } 186 187 if (Looper.myLooper() != Looper.getMainLooper()) { 188 throw new RuntimeException("moveToState should be used only from the main thread"); 189 } 190 191 if (mLifecycleState == newLifecycleState) { 192 // Nothing to do. 193 return; 194 } 195 196 switch (mLifecycleState) { 197 case INITIALIZED: 198 if (newLifecycleState == LifecycleState.CREATED) { 199 onCreate(); 200 } else if (newLifecycleState == LifecycleState.DESTROYED) { 201 onCreate(); 202 onDestroy(); 203 } else { 204 if (DEBUG) { 205 Slog.d(TAG, "Ignoring moveToState: INITIALIZED -> " + newLifecycleState); 206 } 207 return; 208 } 209 break; 210 case CREATED: 211 if (newLifecycleState == LifecycleState.TASK_FOCUSED) { 212 onGameTaskFocusChanged(/*focused=*/ true); 213 } else if (newLifecycleState == LifecycleState.DESTROYED) { 214 onDestroy(); 215 } else { 216 if (DEBUG) { 217 Slog.d(TAG, "Ignoring moveToState: CREATED -> " + newLifecycleState); 218 } 219 return; 220 } 221 break; 222 case TASK_FOCUSED: 223 if (newLifecycleState == LifecycleState.TASK_UNFOCUSED) { 224 onGameTaskFocusChanged(/*focused=*/ false); 225 } else if (newLifecycleState == LifecycleState.DESTROYED) { 226 onGameTaskFocusChanged(/*focused=*/ false); 227 onDestroy(); 228 } else { 229 if (DEBUG) { 230 Slog.d(TAG, "Ignoring moveToState: TASK_FOCUSED -> " + newLifecycleState); 231 } 232 return; 233 } 234 break; 235 case TASK_UNFOCUSED: 236 if (newLifecycleState == LifecycleState.TASK_FOCUSED) { 237 onGameTaskFocusChanged(/*focused=*/ true); 238 } else if (newLifecycleState == LifecycleState.DESTROYED) { 239 onDestroy(); 240 } else { 241 if (DEBUG) { 242 Slog.d(TAG, "Ignoring moveToState: TASK_UNFOCUSED -> " + newLifecycleState); 243 } 244 return; 245 } 246 break; 247 case DESTROYED: 248 if (DEBUG) { 249 Slog.d(TAG, "Ignoring moveToState: DESTROYED -> " + newLifecycleState); 250 } 251 return; 252 } 253 254 mLifecycleState = newLifecycleState; 255 } 256 257 /** 258 * Initializer called when the game session is starting. 259 * 260 * This should be used perform any setup required now that the game session is created. 261 */ onCreate()262 public void onCreate() { 263 } 264 265 /** 266 * Finalizer called when the game session is ending. This method will always be called after a 267 * call to {@link #onCreate()}. If the game task is ever in focus, this method will be called 268 * after one or more pairs of calls to {@link #onGameTaskFocusChanged(boolean)}. 269 * 270 * This should be used to perform any cleanup before the game session is destroyed. 271 */ onDestroy()272 public void onDestroy() { 273 } 274 275 /** 276 * Called when the game task for this session is or unfocused. The initial call to this method 277 * will always come after a call to {@link #onCreate()} with focused=true (when the game task 278 * first comes into focus after the session is created, or immediately after the session is 279 * created if the game task is already focused). 280 * 281 * This should be used to perform any setup required when the game task comes into focus or any 282 * cleanup that is required when the game task goes out of focus. 283 * 284 * @param focused True if the game task is focused, false if the game task is unfocused. 285 */ onGameTaskFocusChanged(boolean focused)286 public void onGameTaskFocusChanged(boolean focused) { 287 } 288 289 /** 290 * Called when the visibility of the transient system bars changed due to the user performing 291 * the reveal gesture. The reveal gesture is defined as a swipe to reveal the transient system 292 * bars that originates from the system bars. 293 * 294 * @param visibleDueToGesture if the transient bars triggered by the reveal gesture are visible. 295 * This is {@code true} when the transient system bars become visible 296 * due to user performing the reveal gesture. This is {@code false} 297 * when the transient system bars are hidden or become permanently 298 * visible. 299 */ onTransientSystemBarVisibilityFromRevealGestureChanged( boolean visibleDueToGesture)300 public void onTransientSystemBarVisibilityFromRevealGestureChanged( 301 boolean visibleDueToGesture) { 302 } 303 304 /** 305 * Sets the task overlay content to an explicit view. This view is placed directly into the game 306 * session's task overlay view hierarchy. It can itself be a complex view hierarchy. The size 307 * the task overlay view will always match the dimensions of the associated task's window. The 308 * {@code View} may not be cleared once set, but may be replaced by invoking 309 * {@link #setTaskOverlayView(View, ViewGroup.LayoutParams)} again. 310 * 311 * <p><b>WARNING</b>: Callers <b>must</b> ensure that only trusted views are provided. 312 * 313 * @param view The desired content to display. 314 * @param layoutParams Layout parameters for the view. 315 */ setTaskOverlayView( @onNull View view, @NonNull ViewGroup.LayoutParams layoutParams)316 public void setTaskOverlayView( 317 @NonNull View view, 318 @NonNull ViewGroup.LayoutParams layoutParams) { 319 mGameSessionRootView.removeAllViews(); 320 mGameSessionRootView.addView(view, layoutParams); 321 } 322 323 /** 324 * Attempts to force stop and relaunch the game associated with the current session. This may 325 * be useful, for example, after applying settings that will not take effect until the game is 326 * restarted. 327 * 328 * @return {@code true} if the game was successfully restarted; otherwise, {@code false}. 329 */ 330 @RequiresPermission(android.Manifest.permission.MANAGE_GAME_ACTIVITY) restartGame()331 public final boolean restartGame() { 332 try { 333 mGameSessionController.restartGame(mTaskId); 334 } catch (RemoteException e) { 335 Slog.w(TAG, "Failed to restart game", e); 336 return false; 337 } 338 339 return true; 340 } 341 342 /** 343 * Root view of the {@link SurfaceControlViewHost} associated with the {@link GameSession} 344 * instance. It is responsible for observing changes in the size of the window and resizing 345 * itself to match. 346 */ 347 private static final class GameSessionRootView extends FrameLayout { 348 private final SurfaceControlViewHost mSurfaceControlViewHost; 349 GameSessionRootView(@onNull Context context, SurfaceControlViewHost surfaceControlViewHost)350 GameSessionRootView(@NonNull Context context, 351 SurfaceControlViewHost surfaceControlViewHost) { 352 super(context); 353 mSurfaceControlViewHost = surfaceControlViewHost; 354 } 355 356 @Override onConfigurationChanged(Configuration newConfig)357 protected void onConfigurationChanged(Configuration newConfig) { 358 super.onConfigurationChanged(newConfig); 359 360 // TODO(b/204504596): Investigate skipping the relayout in cases where the size has 361 // not changed. 362 Rect bounds = newConfig.windowConfiguration.getBounds(); 363 mSurfaceControlViewHost.relayout(bounds.width(), bounds.height()); 364 } 365 } 366 367 /** 368 * Interface for handling result of {@link #takeScreenshot}. 369 */ 370 public interface ScreenshotCallback { 371 372 /** 373 * The status of a failed screenshot attempt provided by {@link #onFailure}. 374 * 375 * @hide 376 */ 377 @IntDef(flag = false, prefix = {"ERROR_TAKE_SCREENSHOT_"}, value = { 378 ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, // 0 379 }) 380 @Retention(RetentionPolicy.SOURCE) 381 @interface ScreenshotFailureStatus { 382 } 383 384 /** 385 * An error code indicating that an internal error occurred when attempting to take a 386 * screenshot of the game task. If this code is returned, the caller should verify that the 387 * conditions for taking a screenshot are met (device screen is on and the game task is 388 * visible). To do so, the caller can monitor the lifecycle methods for this session to 389 * make sure that the game task is focused. If the conditions are met, then the caller may 390 * try again immediately. 391 */ 392 int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 0; 393 394 /** 395 * Called when taking the screenshot failed. 396 * 397 * @param statusCode Indicates the reason for failure. 398 */ onFailure(@creenshotFailureStatus int statusCode)399 void onFailure(@ScreenshotFailureStatus int statusCode); 400 401 /** 402 * Called when taking the screenshot succeeded. 403 */ onSuccess()404 void onSuccess(); 405 } 406 407 /** 408 * Takes a screenshot of the associated game. For this call to succeed, the device screen 409 * must be turned on and the game task must be visible. 410 * 411 * If the callback is called with {@link ScreenshotCallback#onSuccess}, the screenshot is 412 * taken successfully. 413 * 414 * If the callback is called with {@link ScreenshotCallback#onFailure}, the provided status 415 * code should be checked. 416 * 417 * If the status code is {@link ScreenshotCallback#ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR}, 418 * then the caller should verify that the conditions for calling this method are met (device 419 * screen is on and the game task is visible). To do so, the caller can monitor the lifecycle 420 * methods for this session to make sure that the game task is focused. If the conditions are 421 * met, then the caller may try again immediately. 422 * 423 * @param executor Executor on which to run the callback. 424 * @param callback The callback invoked when taking screenshot has succeeded 425 * or failed. 426 * @throws IllegalStateException if this method is called prior to {@link #onCreate}. 427 */ 428 @RequiresPermission(android.Manifest.permission.MANAGE_GAME_ACTIVITY) takeScreenshot(@onNull Executor executor, @NonNull ScreenshotCallback callback)429 public void takeScreenshot(@NonNull Executor executor, @NonNull ScreenshotCallback callback) { 430 if (mGameSessionController == null) { 431 throw new IllegalStateException("Can not call before onCreate()"); 432 } 433 434 AndroidFuture<GameScreenshotResult> takeScreenshotResult = 435 new AndroidFuture<GameScreenshotResult>().whenCompleteAsync((result, error) -> { 436 handleScreenshotResult(callback, result, error); 437 }, executor); 438 439 try { 440 mGameSessionController.takeScreenshot(mTaskId, takeScreenshotResult); 441 } catch (RemoteException ex) { 442 takeScreenshotResult.completeExceptionally(ex); 443 } 444 } 445 handleScreenshotResult( @onNull ScreenshotCallback callback, @NonNull GameScreenshotResult result, @NonNull Throwable error)446 private void handleScreenshotResult( 447 @NonNull ScreenshotCallback callback, 448 @NonNull GameScreenshotResult result, 449 @NonNull Throwable error) { 450 if (error != null) { 451 Slog.w(TAG, error.getMessage(), error.getCause()); 452 callback.onFailure( 453 ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR); 454 return; 455 } 456 457 @GameScreenshotResult.GameScreenshotStatus int status = result.getStatus(); 458 switch (status) { 459 case GameScreenshotResult.GAME_SCREENSHOT_SUCCESS: 460 callback.onSuccess(); 461 break; 462 case GameScreenshotResult.GAME_SCREENSHOT_ERROR_INTERNAL_ERROR: 463 Slog.w(TAG, "Error taking screenshot"); 464 callback.onFailure( 465 ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR); 466 break; 467 } 468 } 469 470 /** 471 * Launches an activity within the same activity stack as the {@link GameSession}. When the 472 * target activity exits, {@link GameSessionActivityCallback#onActivityResult(int, Intent)} will 473 * be invoked with the result code and result data directly from the target activity (in other 474 * words, the result code and data set via the target activity's 475 * {@link android.app.Activity#startActivityForResult} call). The caller is expected to handle 476 * the results that the target activity returns. 477 * 478 * <p>Any activity that an app would normally be able to start via {@link 479 * android.app.Activity#startActivityForResult} will be startable via this method. 480 * 481 * <p>Started activities may see a different calling package than the game session's package 482 * when calling {@link android.app.Activity#getCallingPackage()}. 483 * 484 * <p> If an exception is thrown while handling {@code intent}, 485 * {@link GameSessionActivityCallback#onActivityStartFailed(Throwable)} will be called instead 486 * of {@link GameSessionActivityCallback#onActivityResult(int, Intent)}. 487 * 488 * @param intent The intent to start. 489 * @param options Additional options for how the Activity should be started. See 490 * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle)} for 491 * more details. This value may be null. 492 * @param executor Executor on which {@code callback} should be invoked. 493 * @param callback Callback to be invoked once the started activity has finished. 494 */ 495 @RequiresPermission(android.Manifest.permission.MANAGE_GAME_ACTIVITY) startActivityFromGameSessionForResult( @onNull Intent intent, @Nullable Bundle options, @NonNull Executor executor, @NonNull GameSessionActivityCallback callback)496 public final void startActivityFromGameSessionForResult( 497 @NonNull Intent intent, @Nullable Bundle options, @NonNull Executor executor, 498 @NonNull GameSessionActivityCallback callback) { 499 Objects.requireNonNull(intent); 500 Objects.requireNonNull(executor); 501 Objects.requireNonNull(callback); 502 503 AndroidFuture<GameSessionActivityResult> future = 504 new AndroidFuture<GameSessionActivityResult>() 505 .whenCompleteAsync((result, ex) -> { 506 if (ex != null) { 507 callback.onActivityStartFailed(ex); 508 return; 509 } 510 callback.onActivityResult(result.getResultCode(), result.getData()); 511 }, executor); 512 513 final Intent trampolineIntent = 514 GameSessionTrampolineActivity.createIntent( 515 intent, 516 options, 517 future); 518 519 try { 520 int result = ActivityTaskManager.getService().startActivityFromGameSession( 521 mContext.getIApplicationThread(), mContext.getPackageName(), "GameSession", 522 Binder.getCallingPid(), Binder.getCallingUid(), trampolineIntent, mTaskId, 523 UserHandle.myUserId()); 524 Instrumentation.checkStartActivityResult(result, trampolineIntent); 525 } catch (Throwable t) { 526 executor.execute(() -> callback.onActivityStartFailed(t)); 527 } 528 } 529 } 530