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