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 com.android.systemui.unfold.updates 18 19 import android.content.Context 20 import android.content.res.Configuration 21 import android.content.res.Resources 22 import android.os.Handler 23 import android.os.Looper 24 import android.testing.AndroidTestingRunner 25 import androidx.core.util.Consumer 26 import androidx.test.filters.SmallTest 27 import com.android.systemui.SysuiTestCase 28 import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig 29 import com.android.systemui.unfold.config.UnfoldTransitionConfig 30 import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider 31 import com.android.systemui.unfold.updates.FoldProvider.FoldCallback 32 import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener 33 import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES 34 import com.android.systemui.unfold.updates.hinge.HingeAngleProvider 35 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider 36 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener 37 import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityProvider 38 import com.android.systemui.util.mockito.any 39 import com.android.systemui.util.mockito.capture 40 import com.android.systemui.util.mockito.mock 41 import com.google.common.truth.Truth.assertThat 42 import junit.framework.Assert.fail 43 import java.util.concurrent.Executor 44 import org.junit.Before 45 import org.junit.Test 46 import org.junit.runner.RunWith 47 import org.mockito.ArgumentCaptor 48 import org.mockito.Captor 49 import org.mockito.Mock 50 import org.mockito.Mockito.verify 51 import org.mockito.Mockito.`when` as whenever 52 import org.mockito.MockitoAnnotations 53 54 @RunWith(AndroidTestingRunner::class) 55 @SmallTest 56 class DeviceFoldStateProviderTest : SysuiTestCase() { 57 58 @Mock private lateinit var activityTypeProvider: ActivityManagerActivityTypeProvider 59 60 @Mock private lateinit var rotationChangeProvider: RotationChangeProvider 61 62 @Mock private lateinit var unfoldKeyguardVisibilityProvider: UnfoldKeyguardVisibilityProvider 63 64 @Mock private lateinit var resources: Resources 65 66 @Mock private lateinit var handler: Handler 67 68 @Mock private lateinit var mainLooper: Looper 69 70 @Mock private lateinit var context: Context 71 72 @Mock private lateinit var thread: Thread 73 74 @Captor private lateinit var rotationListener: ArgumentCaptor<RotationListener> 75 76 private val foldProvider = TestFoldProvider() 77 private val screenOnStatusProvider = TestScreenOnStatusProvider() 78 private val testHingeAngleProvider = TestHingeAngleProvider() 79 80 private lateinit var foldStateProvider: DeviceFoldStateProvider 81 82 private val foldUpdates: MutableList<Int> = arrayListOf() 83 private val hingeAngleUpdates: MutableList<Float> = arrayListOf() 84 private val unfoldedScreenAvailabilityUpdates: MutableList<Unit> = arrayListOf() 85 86 private var scheduledRunnable: Runnable? = null 87 private var scheduledRunnableDelay: Long? = null 88 89 @Before 90 fun setUp() { 91 MockitoAnnotations.initMocks(this) 92 93 val config = 94 object : UnfoldTransitionConfig by ResourceUnfoldTransitionConfig() { 95 override val halfFoldedTimeoutMillis: Int 96 get() = HALF_OPENED_TIMEOUT_MILLIS.toInt() 97 } 98 whenever(mainLooper.isCurrentThread).thenReturn(true) 99 whenever(handler.looper).thenReturn(mainLooper) 100 whenever(mainLooper.isCurrentThread).thenReturn(true) 101 whenever(mainLooper.thread).thenReturn(thread) 102 whenever(thread.name).thenReturn("backgroundThread") 103 whenever(context.resources).thenReturn(resources) 104 whenever(context.mainExecutor).thenReturn(mContext.mainExecutor) 105 106 foldStateProvider = 107 DeviceFoldStateProvider( 108 config, 109 testHingeAngleProvider, 110 screenOnStatusProvider, 111 foldProvider, 112 activityTypeProvider, 113 unfoldKeyguardVisibilityProvider, 114 rotationChangeProvider, 115 context, 116 context.mainExecutor, 117 handler 118 ) 119 120 foldStateProvider.addCallback( 121 object : FoldStateProvider.FoldUpdatesListener { 122 override fun onHingeAngleUpdate(angle: Float) { 123 hingeAngleUpdates.add(angle) 124 } 125 126 override fun onFoldUpdate(update: Int) { 127 foldUpdates.add(update) 128 } 129 130 override fun onUnfoldedScreenAvailable() { 131 unfoldedScreenAvailabilityUpdates.add(Unit) 132 } 133 } 134 ) 135 foldStateProvider.start() 136 137 verify(rotationChangeProvider).addCallback(capture(rotationListener)) 138 139 whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock -> 140 scheduledRunnable = invocationOnMock.getArgument<Runnable>(0) 141 scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1) 142 null 143 } 144 145 whenever(handler.removeCallbacks(any<Runnable>())).then { invocationOnMock -> 146 val removedRunnable = invocationOnMock.getArgument<Runnable>(0) 147 if (removedRunnable == scheduledRunnable) { 148 scheduledRunnableDelay = null 149 scheduledRunnable = null 150 } 151 null 152 } 153 154 // By default, we're on launcher. 155 setupForegroundActivityType(isHomeActivity = true) 156 setIsLargeScreen(true) 157 } 158 159 @Test 160 fun testOnFolded_emitsFinishClosedEvent() { 161 setFoldState(folded = true) 162 163 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED) 164 } 165 166 @Test 167 fun testOnUnfolded_emitsStartOpeningEvent() { 168 setFoldState(folded = false) 169 170 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING) 171 } 172 173 @Test 174 fun testOnUnfold_hingeAngleDecreasesBeforeInnerScreenAvailable_emitsOnlyStartAndInnerScreenAvailableEvents() { 175 setFoldState(folded = true) 176 foldUpdates.clear() 177 178 setFoldState(folded = false) 179 screenOnStatusProvider.notifyScreenTurningOn() 180 sendHingeAngleEvent(10) 181 sendHingeAngleEvent(20) 182 sendHingeAngleEvent(10) 183 screenOnStatusProvider.notifyScreenTurnedOn() 184 185 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING) 186 assertThat(unfoldedScreenAvailabilityUpdates).hasSize(1) 187 } 188 189 @Test 190 fun testOnUnfold_hingeAngleDecreasesAfterInnerScreenAvailable_emitsStartInnerScreenAvailableAndStartClosingEvents() { 191 setFoldState(folded = true) 192 foldUpdates.clear() 193 194 setFoldState(folded = false) 195 screenOnStatusProvider.notifyScreenTurningOn() 196 sendHingeAngleEvent(10) 197 sendHingeAngleEvent(20) 198 screenOnStatusProvider.notifyScreenTurnedOn() 199 sendHingeAngleEvent(30) 200 sendHingeAngleEvent(40) 201 sendHingeAngleEvent(10) 202 203 assertThat(foldUpdates) 204 .containsExactly(FOLD_UPDATE_START_OPENING, FOLD_UPDATE_START_CLOSING) 205 assertThat(unfoldedScreenAvailabilityUpdates).hasSize(1) 206 } 207 208 @Test 209 fun testOnFolded_stopsHingeAngleProvider() { 210 setFoldState(folded = true) 211 212 assertThat(testHingeAngleProvider.isStarted).isFalse() 213 } 214 215 @Test 216 fun testOnUnfolded_startsHingeAngleProvider() { 217 setFoldState(folded = false) 218 219 assertThat(testHingeAngleProvider.isStarted).isTrue() 220 } 221 222 @Test 223 fun testFirstScreenOnEventWhenFolded_doesNotEmitEvents() { 224 setFoldState(folded = true) 225 foldUpdates.clear() 226 227 fireScreenOnEvent() 228 229 // Power button turn on 230 assertThat(foldUpdates).isEmpty() 231 } 232 233 @Test 234 fun testFirstScreenOnEventWhenUnfolded_doesNotEmitEvents() { 235 setFoldState(folded = false) 236 foldUpdates.clear() 237 238 fireScreenOnEvent() 239 240 assertThat(foldUpdates).isEmpty() 241 } 242 243 @Test 244 fun testFirstScreenOnEventAfterFoldAndUnfold_emitsUnfoldedScreenAvailableEvent() { 245 setFoldState(folded = false) 246 setFoldState(folded = true) 247 fireScreenOnEvent() 248 setFoldState(folded = false) 249 foldUpdates.clear() 250 251 fireScreenOnEvent() 252 253 assertThat(unfoldedScreenAvailabilityUpdates).hasSize(1) 254 } 255 256 @Test 257 fun testSecondScreenOnEventWhenUnfolded_doesNotEmitEvents() { 258 setFoldState(folded = false) 259 fireScreenOnEvent() 260 foldUpdates.clear() 261 262 fireScreenOnEvent() 263 264 // No events as this is power button turn on 265 assertThat(foldUpdates).isEmpty() 266 } 267 268 @Test 269 fun testUnfoldedOpenedHingeAngleEmitted_isFinishedOpeningIsFalse() { 270 setFoldState(folded = false) 271 272 sendHingeAngleEvent(10) 273 274 assertThat(foldStateProvider.isFinishedOpening).isFalse() 275 } 276 277 @Test 278 fun testFoldedHalfOpenHingeAngleEmitted_isFinishedOpeningIsFalse() { 279 setFoldState(folded = true) 280 281 sendHingeAngleEvent(10) 282 283 assertThat(foldStateProvider.isFinishedOpening).isFalse() 284 } 285 286 @Test 287 fun testFoldedFullyOpenHingeAngleEmitted_isFinishedOpeningIsTrue() { 288 setFoldState(folded = false) 289 290 sendHingeAngleEvent(180) 291 292 assertThat(foldStateProvider.isFinishedOpening).isTrue() 293 } 294 295 @Test 296 fun testUnfoldedHalfOpenOpened_afterTimeout_isFinishedOpeningIsTrue() { 297 setFoldState(folded = false) 298 299 sendHingeAngleEvent(10) 300 simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS) 301 302 assertThat(foldStateProvider.isFinishedOpening).isTrue() 303 } 304 305 @Test 306 fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() { 307 setInitialHingeAngle(90) 308 sendHingeAngleEvent(80) 309 310 simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS) 311 312 assertThat(foldUpdates) 313 .containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_FINISH_HALF_OPEN) 314 } 315 316 @Test 317 fun startClosingEvent_beforeTimeout_abortNotEmitted() { 318 setInitialHingeAngle(90) 319 sendHingeAngleEvent(80) 320 321 simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1) 322 323 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 324 } 325 326 @Test 327 fun startClosingEvent_eventBeforeTimeout_oneEventEmitted() { 328 setInitialHingeAngle(180) 329 sendHingeAngleEvent(90) 330 331 simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1) 332 sendHingeAngleEvent(80) 333 334 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 335 } 336 337 @Test 338 fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() { 339 setInitialHingeAngle(180) 340 sendHingeAngleEvent(90) 341 342 // The timeout should not trigger here. 343 simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1) 344 sendHingeAngleEvent(80) 345 simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS) // The timeout should trigger here. 346 347 assertThat(foldUpdates) 348 .containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_FINISH_HALF_OPEN) 349 } 350 351 @Test 352 fun startClosingEvent_shortTimeBetween_emitsOnlyOneEvents() { 353 setInitialHingeAngle(180) 354 355 sendHingeAngleEvent(90) 356 sendHingeAngleEvent(80) 357 358 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 359 } 360 361 @Test 362 fun startClosingEvent_whileClosing_emittedDespiteInitialAngle() { 363 val maxAngle = 180 - FULLY_OPEN_THRESHOLD_DEGREES.toInt() 364 val minAngle = Math.ceil(HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toDouble()).toInt() + 1 365 for (startAngle in minAngle..maxAngle) { 366 setInitialHingeAngle(startAngle) 367 sendHingeAngleEvent(startAngle - HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toInt() - 1) 368 369 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 370 } 371 } 372 373 @Test 374 fun startClosingEvent_whileNotOnLauncher_doesNotTriggerBeforeThreshold() { 375 setupForegroundActivityType(isHomeActivity = false) 376 setInitialHingeAngle(180) 377 378 sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1) 379 380 assertThat(foldUpdates).isEmpty() 381 } 382 383 @Test 384 fun startClosingEvent_whileActivityTypeNotAvailable_triggerBeforeThreshold() { 385 setupForegroundActivityType(isHomeActivity = null) 386 setInitialHingeAngle(180) 387 388 sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1) 389 390 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 391 } 392 393 @Test 394 fun startClosingEvent_whileOnLauncher_doesTriggerBeforeThreshold() { 395 setupForegroundActivityType(isHomeActivity = true) 396 setInitialHingeAngle(180) 397 398 sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1) 399 400 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 401 } 402 403 @Test 404 fun startClosingEvent_whileNotOnLauncher_triggersAfterThreshold() { 405 setupForegroundActivityType(isHomeActivity = false) 406 setInitialHingeAngle(START_CLOSING_ON_APPS_THRESHOLD_DEGREES) 407 408 sendHingeAngleEvent( 409 START_CLOSING_ON_APPS_THRESHOLD_DEGREES - 410 HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toInt() - 411 1 412 ) 413 414 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 415 } 416 417 @Test 418 fun startClosingEvent_whileNotOnKeyguardAndNotOnLauncher_doesNotTriggerBeforeThreshold() { 419 setKeyguardVisibility(visible = false) 420 setupForegroundActivityType(isHomeActivity = false) 421 setInitialHingeAngle(180) 422 423 sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1) 424 425 assertThat(foldUpdates).isEmpty() 426 } 427 428 @Test 429 fun startClosingEvent_whileKeyguardStateNotAvailable_triggerBeforeThreshold() { 430 setKeyguardVisibility(visible = null) 431 setInitialHingeAngle(180) 432 433 sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1) 434 435 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 436 } 437 438 @Test 439 fun startClosingEvent_whileonKeyguard_doesTriggerBeforeThreshold() { 440 setKeyguardVisibility(visible = true) 441 setInitialHingeAngle(180) 442 443 sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1) 444 445 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 446 } 447 448 @Test 449 fun startOnlyOnce_whenStartTriggeredThrice_startOnlyOnce() { 450 foldStateProvider.start() 451 foldStateProvider.start() 452 foldStateProvider.start() 453 454 assertThat(foldProvider.getNumberOfCallbacks()).isEqualTo(1) 455 } 456 457 @Test(expected = AssertionError::class) 458 fun startMethod_whileNotOnMainThread_throwsException() { 459 whenever(mainLooper.isCurrentThread).thenReturn(true) 460 try { 461 foldStateProvider.start() 462 fail("Should have thrown AssertionError: should be called from the main thread.") 463 } catch (e: AssertionError) { 464 assertThat(e.message).contains("backgroundThread") 465 } 466 } 467 468 @Test 469 fun startClosingEvent_whileNotOnKeyguard_triggersAfterThreshold() { 470 setKeyguardVisibility(visible = false) 471 setInitialHingeAngle(START_CLOSING_ON_APPS_THRESHOLD_DEGREES) 472 473 sendHingeAngleEvent( 474 START_CLOSING_ON_APPS_THRESHOLD_DEGREES - 475 HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES.toInt() - 476 1 477 ) 478 479 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 480 } 481 482 @Test 483 fun startClosingEvent_doesNotTriggerBelowThreshold() { 484 val thresholdAngle = (FULLY_OPEN_DEGREES - FULLY_OPEN_THRESHOLD_DEGREES).toInt() 485 setInitialHingeAngle(180) 486 sendHingeAngleEvent(thresholdAngle + 1) 487 488 assertThat(foldUpdates).isEmpty() 489 } 490 491 @Test 492 fun startClosingEvent_triggersAfterThreshold() { 493 val thresholdAngle = (FULLY_OPEN_DEGREES - FULLY_OPEN_THRESHOLD_DEGREES).toInt() 494 setInitialHingeAngle(180) 495 sendHingeAngleEvent(thresholdAngle + 1) 496 sendHingeAngleEvent(thresholdAngle - 1) 497 498 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 499 } 500 501 @Test 502 fun startClosingEvent_triggersAfterThreshold_fromHalfOpen() { 503 setInitialHingeAngle(120) 504 sendHingeAngleEvent((120 - HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES + 1).toInt()) 505 assertThat(foldUpdates).isEmpty() 506 sendHingeAngleEvent((120 - HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES - 1).toInt()) 507 508 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 509 } 510 511 @Test 512 fun startOpeningAndClosingEvents_triggerWithOpenAndClose() { 513 setInitialHingeAngle(120) 514 sendHingeAngleEvent(130) 515 sendHingeAngleEvent(120) 516 assertThat(foldUpdates) 517 .containsExactly(FOLD_UPDATE_START_OPENING, FOLD_UPDATE_START_CLOSING) 518 } 519 520 @Test 521 fun startClosingEvent_notInterrupted_whenAngleIsSlightlyIncreased() { 522 setInitialHingeAngle(120) 523 sendHingeAngleEvent(110) 524 sendHingeAngleEvent(111) 525 sendHingeAngleEvent(100) 526 527 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 528 } 529 530 @Test 531 fun screenOff_whileFolded_hingeAngleProviderRemainsOff() { 532 setFoldState(folded = true) 533 assertThat(testHingeAngleProvider.isStarted).isFalse() 534 535 screenOnStatusProvider.notifyScreenTurningOff() 536 537 assertThat(testHingeAngleProvider.isStarted).isFalse() 538 } 539 540 @Test 541 fun screenOff_whileUnfolded_hingeAngleProviderStops() { 542 setFoldState(folded = false) 543 assertThat(testHingeAngleProvider.isStarted).isTrue() 544 545 screenOnStatusProvider.notifyScreenTurningOff() 546 547 assertThat(testHingeAngleProvider.isStarted).isFalse() 548 } 549 550 @Test 551 fun screenOn_whileUnfoldedAndScreenOff_hingeAngleProviderStarted() { 552 setFoldState(folded = false) 553 screenOnStatusProvider.notifyScreenTurningOff() 554 assertThat(testHingeAngleProvider.isStarted).isFalse() 555 556 screenOnStatusProvider.notifyScreenTurningOn() 557 558 assertThat(testHingeAngleProvider.isStarted).isTrue() 559 } 560 561 @Test 562 fun screenOn_whileFolded_hingeAngleRemainsOff() { 563 setFoldState(folded = true) 564 assertThat(testHingeAngleProvider.isStarted).isFalse() 565 566 screenOnStatusProvider.notifyScreenTurningOn() 567 568 assertThat(testHingeAngleProvider.isStarted).isFalse() 569 } 570 571 @Test 572 fun onRotationChanged_whileInProgress_cancelled() { 573 setFoldState(folded = false) 574 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING) 575 576 rotationListener.value.onRotationChanged(1) 577 578 assertThat(foldUpdates) 579 .containsExactly(FOLD_UPDATE_START_OPENING, FOLD_UPDATE_FINISH_HALF_OPEN) 580 } 581 582 @Test 583 fun onRotationChanged_whileNotInProgress_noUpdates() { 584 setFoldState(folded = true) 585 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED) 586 587 rotationListener.value.onRotationChanged(1) 588 589 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED) 590 } 591 592 @Test 593 fun onFolding_onSmallScreen_tansitionDoesNotStart() { 594 setIsLargeScreen(false) 595 596 setInitialHingeAngle(120) 597 sendHingeAngleEvent(110) 598 sendHingeAngleEvent(100) 599 600 assertThat(foldUpdates).isEmpty() 601 } 602 603 @Test 604 fun onFolding_onLargeScreen_tansitionStarts() { 605 setIsLargeScreen(true) 606 607 setInitialHingeAngle(120) 608 sendHingeAngleEvent(110) 609 sendHingeAngleEvent(100) 610 611 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) 612 } 613 614 @Test 615 fun onUnfold_onSmallScreen_emitsStartOpening() { 616 // the new display state might arrive later, so it shouldn't be used to decide to send the 617 // start opening event, but only for the closing. 618 setFoldState(folded = true) 619 setIsLargeScreen(false) 620 foldUpdates.clear() 621 622 setFoldState(folded = false) 623 screenOnStatusProvider.notifyScreenTurningOn() 624 sendHingeAngleEvent(10) 625 sendHingeAngleEvent(20) 626 screenOnStatusProvider.notifyScreenTurnedOn() 627 628 assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING) 629 } 630 631 private fun setupForegroundActivityType(isHomeActivity: Boolean?) { 632 whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity) 633 } 634 635 private fun setKeyguardVisibility(visible: Boolean?) { 636 whenever(unfoldKeyguardVisibilityProvider.isKeyguardVisible).thenReturn(visible) 637 } 638 639 private fun simulateTimeout(waitTime: Long = HALF_OPENED_TIMEOUT_MILLIS) { 640 val runnableDelay = scheduledRunnableDelay ?: throw Exception("No runnable scheduled.") 641 if (waitTime >= runnableDelay) { 642 scheduledRunnable?.run() 643 scheduledRunnable = null 644 scheduledRunnableDelay = null 645 } 646 } 647 648 private fun setFoldState(folded: Boolean) { 649 foldProvider.notifyFolded(folded) 650 } 651 652 private fun setIsLargeScreen(isLargeScreen: Boolean) { 653 val smallestScreenWidth = if (isLargeScreen) { 601 } else { 10 } 654 val configuration = Configuration() 655 configuration.smallestScreenWidthDp = smallestScreenWidth 656 whenever(resources.configuration).thenReturn(configuration) 657 } 658 659 private fun fireScreenOnEvent() { 660 screenOnStatusProvider.notifyScreenTurnedOn() 661 } 662 663 private fun sendHingeAngleEvent(angle: Int) { 664 testHingeAngleProvider.notifyAngle(angle.toFloat()) 665 } 666 667 private fun setInitialHingeAngle(angle: Int) { 668 setFoldState(angle == 0) 669 sendHingeAngleEvent(angle) 670 if (scheduledRunnableDelay != null) { 671 simulateTimeout() 672 } 673 hingeAngleUpdates.clear() 674 foldUpdates.clear() 675 unfoldedScreenAvailabilityUpdates.clear() 676 } 677 678 private class TestFoldProvider : FoldProvider { 679 private val callbacks = arrayListOf<FoldCallback>() 680 681 override fun registerCallback(callback: FoldCallback, executor: Executor) { 682 callbacks += callback 683 } 684 685 override fun unregisterCallback(callback: FoldCallback) { 686 callbacks -= callback 687 } 688 689 fun notifyFolded(isFolded: Boolean) { 690 callbacks.forEach { it.onFoldUpdated(isFolded) } 691 } 692 693 fun getNumberOfCallbacks(): Int{ 694 return callbacks.size 695 } 696 } 697 698 private class TestScreenOnStatusProvider : ScreenStatusProvider { 699 private val callbacks = arrayListOf<ScreenListener>() 700 701 override fun addCallback(listener: ScreenListener) { 702 callbacks += listener 703 } 704 705 override fun removeCallback(listener: ScreenListener) { 706 callbacks -= listener 707 } 708 709 fun notifyScreenTurnedOn() { 710 callbacks.forEach { it.onScreenTurnedOn() } 711 } 712 713 fun notifyScreenTurningOn() { 714 callbacks.forEach { it.onScreenTurningOn() } 715 } 716 717 fun notifyScreenTurningOff() { 718 callbacks.forEach { it.onScreenTurningOff() } 719 } 720 } 721 722 private class TestHingeAngleProvider : HingeAngleProvider { 723 private val callbacks = arrayListOf<Consumer<Float>>() 724 var isStarted: Boolean = false 725 726 override fun start() { 727 isStarted = true 728 } 729 730 override fun stop() { 731 isStarted = false 732 } 733 734 override fun addCallback(listener: Consumer<Float>) { 735 callbacks += listener 736 } 737 738 override fun removeCallback(listener: Consumer<Float>) { 739 callbacks -= listener 740 } 741 742 fun notifyAngle(angle: Float) { 743 callbacks.forEach { it.accept(angle) } 744 } 745 } 746 747 companion object { 748 private const val HALF_OPENED_TIMEOUT_MILLIS = 300L 749 } 750 } 751