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