1 /*
2  * Copyright (C) 2022 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
21 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assert.fail;
28 import static org.mockito.ArgumentMatchers.any;
29 import static org.mockito.ArgumentMatchers.anyInt;
30 
31 import android.platform.test.annotations.Presubmit;
32 import android.service.games.GameSession.ScreenshotCallback;
33 import android.testing.AndroidTestingRunner;
34 import android.testing.TestableLooper;
35 import android.view.SurfaceControlViewHost;
36 
37 import androidx.test.InstrumentationRegistry;
38 import androidx.test.filters.SmallTest;
39 
40 import com.android.internal.infra.AndroidFuture;
41 
42 import org.junit.After;
43 import org.junit.Before;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 import org.mockito.Mock;
47 import org.mockito.MockitoSession;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 import java.util.concurrent.CountDownLatch;
52 import java.util.concurrent.TimeUnit;
53 
54 /**
55  * Unit tests for the {@link android.service.games.GameSession}.
56  */
57 @RunWith(AndroidTestingRunner.class)
58 @SmallTest
59 @Presubmit
60 @TestableLooper.RunWithLooper(setAsMainLooper = true)
61 public final class GameSessionTest {
62     private static final long WAIT_FOR_CALLBACK_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(1);
63 
64     @Mock
65     private IGameSessionController mMockGameSessionController;
66     @Mock
67     SurfaceControlViewHost mSurfaceControlViewHost;
68     private LifecycleTrackingGameSession mGameSession;
69     private MockitoSession mMockitoSession;
70 
71     @Before
setUp()72     public void setUp() {
73         mMockitoSession = mockitoSession()
74                 .initMocks(this)
75                 .startMocking();
76 
77         mGameSession = new LifecycleTrackingGameSession() {};
78         mGameSession.attach(mMockGameSessionController, /* taskId= */ 10,
79                 InstrumentationRegistry.getContext(),
80                 mSurfaceControlViewHost,
81                 /* widthPx= */ 0, /* heightPx= */0);
82     }
83 
84     @After
tearDown()85     public void tearDown() {
86         mMockitoSession.finishMocking();
87     }
88 
89     @Test
takeScreenshot_attachNotCalled_throwsIllegalStateException()90     public void takeScreenshot_attachNotCalled_throwsIllegalStateException() throws Exception {
91         GameSession gameSession = new GameSession() {};
92 
93         try {
94             gameSession.takeScreenshot(DIRECT_EXECUTOR,
95                     new ScreenshotCallback() {
96                         @Override
97                         public void onFailure(int statusCode) {
98                             fail();
99                         }
100 
101                         @Override
102                         public void onSuccess() {
103                             fail();
104                         }
105                     });
106             fail();
107         } catch (IllegalStateException expected) {
108 
109         }
110     }
111 
112     @Test
takeScreenshot_gameManagerException_returnsInternalError()113     public void takeScreenshot_gameManagerException_returnsInternalError() throws Exception {
114         doAnswer(invocation -> {
115             AndroidFuture result = invocation.getArgument(1);
116             result.completeExceptionally(new Exception());
117             return null;
118         }).when(mMockGameSessionController).takeScreenshot(anyInt(), any());
119 
120         CountDownLatch countDownLatch = new CountDownLatch(1);
121 
122         mGameSession.takeScreenshot(DIRECT_EXECUTOR,
123                 new ScreenshotCallback() {
124                     @Override
125                     public void onFailure(int statusCode) {
126                         assertEquals(ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
127                                 statusCode);
128                         countDownLatch.countDown();
129                     }
130 
131                     @Override
132                     public void onSuccess() {
133                         fail();
134                     }
135                 });
136 
137         assertTrue(countDownLatch.await(
138                 WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
139     }
140 
141     @Test
takeScreenshot_gameManagerError_returnsInternalError()142     public void takeScreenshot_gameManagerError_returnsInternalError() throws Exception {
143         doAnswer(invocation -> {
144             AndroidFuture result = invocation.getArgument(1);
145             result.complete(GameScreenshotResult.createInternalErrorResult());
146             return null;
147         }).when(mMockGameSessionController).takeScreenshot(anyInt(), any());
148 
149         CountDownLatch countDownLatch = new CountDownLatch(1);
150 
151         mGameSession.takeScreenshot(DIRECT_EXECUTOR,
152                 new ScreenshotCallback() {
153                     @Override
154                     public void onFailure(int statusCode) {
155                         assertEquals(ScreenshotCallback.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
156                                 statusCode);
157                         countDownLatch.countDown();
158                     }
159 
160                     @Override
161                     public void onSuccess() {
162                         fail();
163                     }
164                 });
165 
166         assertTrue(countDownLatch.await(
167                 WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
168     }
169 
170     @Test
takeScreenshot_gameManagerSuccess()171     public void takeScreenshot_gameManagerSuccess() throws Exception {
172         doAnswer(invocation -> {
173             AndroidFuture result = invocation.getArgument(1);
174             result.complete(GameScreenshotResult.createSuccessResult());
175             return null;
176         }).when(mMockGameSessionController).takeScreenshot(anyInt(), any());
177 
178         CountDownLatch countDownLatch = new CountDownLatch(1);
179 
180         mGameSession.takeScreenshot(DIRECT_EXECUTOR,
181                 new ScreenshotCallback() {
182                     @Override
183                     public void onFailure(int statusCode) {
184                         fail();
185                     }
186 
187                     @Override
188                     public void onSuccess() {
189                         countDownLatch.countDown();
190                     }
191                 });
192 
193         assertTrue(countDownLatch.await(
194                 WAIT_FOR_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
195     }
196 
197     @Test
moveState_InitializedToInitialized_noLifecycleCalls()198     public void moveState_InitializedToInitialized_noLifecycleCalls() throws Exception {
199         mGameSession.moveToState(GameSession.LifecycleState.INITIALIZED);
200 
201         assertThat(mGameSession.mLifecycleMethodCalls.isEmpty()).isTrue();
202     }
203 
204     @Test
moveState_FullLifecycle_ExpectedLifecycleCalls()205     public void moveState_FullLifecycle_ExpectedLifecycleCalls() throws Exception {
206         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
207         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
208         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
209         mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
210 
211         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
212                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
213                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
214                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED,
215                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
216     }
217 
218     @Test
moveState_DestroyedWhenInitialized_ExpectedLifecycleCalls()219     public void moveState_DestroyedWhenInitialized_ExpectedLifecycleCalls() throws Exception {
220         mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
221 
222         // ON_CREATE is always called before ON_DESTROY.
223         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
224                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
225                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
226     }
227 
228     @Test
moveState_DestroyedWhenFocused_ExpectedLifecycleCalls()229     public void moveState_DestroyedWhenFocused_ExpectedLifecycleCalls() throws Exception {
230         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
231         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
232         mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
233 
234         // The ON_GAME_TASK_UNFOCUSED lifecycle event is implied because the session is destroyed
235         // while in focus.
236         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
237                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
238                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
239                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED,
240                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
241     }
242 
243     @Test
moveState_FocusCycled_ExpectedLifecycleCalls()244     public void moveState_FocusCycled_ExpectedLifecycleCalls() throws Exception {
245         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
246         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
247         mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
248         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
249         mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
250 
251         // Both cycles from focus and unfocus are captured.
252         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
253                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
254                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
255                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED,
256                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
257                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED).inOrder();
258     }
259 
260     @Test
moveState_MultipleFocusAndUnfocusCalls_ExpectedLifecycleCalls()261     public void moveState_MultipleFocusAndUnfocusCalls_ExpectedLifecycleCalls() throws Exception {
262         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
263         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
264         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
265         mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
266         mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
267 
268         // The second TASK_FOCUSED call and the second TASK_UNFOCUSED call are ignored.
269         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
270                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
271                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED,
272                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED).inOrder();
273     }
274 
275     @Test
moveState_CreatedAfterFocused_ExpectedLifecycleCalls()276     public void moveState_CreatedAfterFocused_ExpectedLifecycleCalls() throws Exception {
277         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
278         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
279         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
280 
281         // The second CREATED call is ignored.
282         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
283                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
284                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED).inOrder();
285     }
286 
287     @Test
moveState_UnfocusedWithoutFocused_ExpectedLifecycleCalls()288     public void moveState_UnfocusedWithoutFocused_ExpectedLifecycleCalls() throws Exception {
289         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
290         mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
291 
292         // The TASK_UNFOCUSED call without an earlier TASK_FOCUSED call is ignored.
293         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
294                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE).inOrder();
295     }
296 
297     @Test
moveState_NeverFocused_ExpectedLifecycleCalls()298     public void moveState_NeverFocused_ExpectedLifecycleCalls() throws Exception {
299         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
300         mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
301 
302         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
303                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
304                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
305     }
306 
307     @Test
moveState_MultipleFocusCalls_ExpectedLifecycleCalls()308     public void moveState_MultipleFocusCalls_ExpectedLifecycleCalls() throws Exception {
309         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
310         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
311         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
312         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
313 
314         // The extra TASK_FOCUSED moves are ignored.
315         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
316                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
317                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_GAME_TASK_FOCUSED).inOrder();
318     }
319 
320     @Test
moveState_MultipleCreateCalls_ExpectedLifecycleCalls()321     public void moveState_MultipleCreateCalls_ExpectedLifecycleCalls() throws Exception {
322         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
323         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
324         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
325 
326         // The extra CREATE moves are ignored.
327         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
328                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE).inOrder();
329     }
330 
331     @Test
moveState_FocusBeforeCreate_ExpectedLifecycleCalls()332     public void moveState_FocusBeforeCreate_ExpectedLifecycleCalls() throws Exception {
333         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
334 
335         // The TASK_FOCUSED move before CREATE is ignored.
336         assertThat(mGameSession.mLifecycleMethodCalls.isEmpty()).isTrue();
337     }
338 
339     @Test
moveState_UnfocusBeforeCreate_ExpectedLifecycleCalls()340     public void moveState_UnfocusBeforeCreate_ExpectedLifecycleCalls() throws Exception {
341         mGameSession.moveToState(GameSession.LifecycleState.TASK_UNFOCUSED);
342 
343         // The TASK_UNFOCUSED move before CREATE is ignored.
344         assertThat(mGameSession.mLifecycleMethodCalls.isEmpty()).isTrue();
345     }
346 
347     @Test
moveState_FocusWhenDestroyed_ExpectedLifecycleCalls()348     public void moveState_FocusWhenDestroyed_ExpectedLifecycleCalls() throws Exception {
349         mGameSession.moveToState(GameSession.LifecycleState.CREATED);
350         mGameSession.moveToState(GameSession.LifecycleState.DESTROYED);
351         mGameSession.moveToState(GameSession.LifecycleState.TASK_FOCUSED);
352 
353         // The TASK_FOCUSED move after DESTROYED is ignored.
354         assertThat(mGameSession.mLifecycleMethodCalls).containsExactly(
355                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_CREATE,
356                 LifecycleTrackingGameSession.LifecycleMethodCall.ON_DESTROY).inOrder();
357     }
358 
359     @Test
dispatchTransientVisibilityChanged_valueUnchanged_doesNotInvokeCallback()360     public void dispatchTransientVisibilityChanged_valueUnchanged_doesNotInvokeCallback() {
361         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(false);
362 
363         assertThat(mGameSession.mCapturedTransientSystemBarVisibilityFromRevealGestures).hasSize(0);
364     }
365 
366     @Test
dispatchTransientVisibilityChanged_valueChanged_invokesCallback()367     public void dispatchTransientVisibilityChanged_valueChanged_invokesCallback() {
368         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(true);
369 
370         assertThat(mGameSession.mCapturedTransientSystemBarVisibilityFromRevealGestures)
371                 .containsExactly(true).inOrder();
372     }
373 
374     @Test
dispatchTransientVisibilityChanged_manyTimes_invokesCallbackWhenValueChanges()375     public void dispatchTransientVisibilityChanged_manyTimes_invokesCallbackWhenValueChanges() {
376         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(false);
377         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(true);
378         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(false);
379         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(false);
380         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(true);
381         mGameSession.dispatchTransientSystemBarVisibilityFromRevealGestureChanged(true);
382 
383         assertThat(mGameSession.mCapturedTransientSystemBarVisibilityFromRevealGestures)
384                 .containsExactly(true, false, true).inOrder();
385     }
386 
387     private static class LifecycleTrackingGameSession extends GameSession {
388         private enum LifecycleMethodCall {
389             ON_CREATE,
390             ON_DESTROY,
391             ON_GAME_TASK_FOCUSED,
392             ON_GAME_TASK_UNFOCUSED
393         }
394 
395         final List<LifecycleMethodCall> mLifecycleMethodCalls = new ArrayList<>();
396         final List<Boolean> mCapturedTransientSystemBarVisibilityFromRevealGestures =
397                 new ArrayList<>();
398 
399         @Override
onCreate()400         public void onCreate() {
401             mLifecycleMethodCalls.add(LifecycleMethodCall.ON_CREATE);
402         }
403 
404         @Override
onDestroy()405         public void onDestroy() {
406             mLifecycleMethodCalls.add(LifecycleMethodCall.ON_DESTROY);
407         }
408 
409         @Override
onGameTaskFocusChanged(boolean focused)410         public void onGameTaskFocusChanged(boolean focused) {
411             if (focused) {
412                 mLifecycleMethodCalls.add(LifecycleMethodCall.ON_GAME_TASK_FOCUSED);
413             } else {
414                 mLifecycleMethodCalls.add(LifecycleMethodCall.ON_GAME_TASK_UNFOCUSED);
415             }
416         }
417 
418         @Override
onTransientSystemBarVisibilityFromRevealGestureChanged( boolean visibleDueToGesture)419         public void onTransientSystemBarVisibilityFromRevealGestureChanged(
420                 boolean visibleDueToGesture) {
421             mCapturedTransientSystemBarVisibilityFromRevealGestures.add(visibleDueToGesture);
422         }
423     }
424 }
425