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