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 
18 package com.android.systemui.lifecycle
19 
20 import android.view.View
21 import android.view.ViewTreeObserver
22 import androidx.lifecycle.Lifecycle
23 import androidx.lifecycle.LifecycleOwner
24 import androidx.test.filters.SmallTest
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.util.Assert
27 import com.google.common.truth.Truth.assertThat
28 import kotlinx.coroutines.CoroutineScope
29 import kotlinx.coroutines.Dispatchers
30 import kotlinx.coroutines.DisposableHandle
31 import kotlinx.coroutines.ExperimentalCoroutinesApi
32 import kotlinx.coroutines.Job
33 import kotlinx.coroutines.awaitCancellation
34 import kotlinx.coroutines.launch
35 import kotlinx.coroutines.test.StandardTestDispatcher
36 import kotlinx.coroutines.test.TestScope
37 import kotlinx.coroutines.test.resetMain
38 import kotlinx.coroutines.test.runCurrent
39 import kotlinx.coroutines.test.runTest
40 import kotlinx.coroutines.test.setMain
41 import org.junit.After
42 import org.junit.Before
43 import org.junit.Rule
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 import org.junit.runners.JUnit4
47 import org.mockito.Mock
48 import org.mockito.Mockito.any
49 import org.mockito.Mockito.verify
50 import org.mockito.Mockito.`when` as whenever
51 import org.mockito.junit.MockitoJUnit
52 import org.mockito.kotlin.KArgumentCaptor
53 import org.mockito.kotlin.argumentCaptor
54 
55 @OptIn(ExperimentalCoroutinesApi::class)
56 @SmallTest
57 @RunWith(JUnit4::class)
58 class RepeatWhenAttachedTest : SysuiTestCase() {
59 
60     @JvmField @Rule val mockito = MockitoJUnit.rule()
61     @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule()
62 
63     @Mock private lateinit var view: View
64     @Mock private lateinit var viewTreeObserver: ViewTreeObserver
65 
66     private lateinit var block: Block
67     private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener>
68     private lateinit var testScope: TestScope
69 
70     @Before
setUpnull71     fun setUp() {
72         val testDispatcher = StandardTestDispatcher()
73         testScope = TestScope(testDispatcher)
74         Dispatchers.setMain(testDispatcher)
75         Assert.setTestThread(Thread.currentThread())
76         whenever(view.viewTreeObserver).thenReturn(viewTreeObserver)
77         whenever(view.windowVisibility).thenReturn(View.GONE)
78         whenever(view.hasWindowFocus()).thenReturn(false)
79         attachListeners = mutableListOf()
80         whenever(view.addOnAttachStateChangeListener(any())).then {
81             attachListeners.add(it.arguments[0] as View.OnAttachStateChangeListener)
82         }
83         whenever(view.removeOnAttachStateChangeListener(any())).then {
84             attachListeners.remove(it.arguments[0] as View.OnAttachStateChangeListener)
85         }
86         block = Block()
87     }
88 
89     @After
tearDownnull90     fun tearDown() {
91         Dispatchers.resetMain()
92     }
93 
94     @Test(expected = IllegalStateException::class)
repeatWhenAttached_enforcesMainThreadnull95     fun repeatWhenAttached_enforcesMainThread() =
96         testScope.runTest {
97             Assert.setTestThread(null)
98 
99             repeatWhenAttached()
100         }
101 
102     @Test(expected = IllegalStateException::class)
repeatWhenAttachedToWindow_enforcesMainThreadnull103     fun repeatWhenAttachedToWindow_enforcesMainThread() =
104         testScope.runTest {
105             Assert.setTestThread(null)
106 
107             view.repeatWhenAttachedToWindow {}
108         }
109 
110     @Test(expected = IllegalStateException::class)
repeatWhenAttached_disposeEnforcesMainThreadnull111     fun repeatWhenAttached_disposeEnforcesMainThread() =
112         testScope.runTest {
113             val disposableHandle = repeatWhenAttached()
114             Assert.setTestThread(null)
115 
116             disposableHandle.dispose()
117         }
118 
119     @Test
repeatWhenAttached_viewStartsDetached_runsBlockWhenAttachednull120     fun repeatWhenAttached_viewStartsDetached_runsBlockWhenAttached() =
121         testScope.runTest {
122             whenever(view.isAttachedToWindow).thenReturn(false)
123             repeatWhenAttached()
124             assertThat(block.invocationCount).isEqualTo(0)
125 
126             whenever(view.isAttachedToWindow).thenReturn(true)
127             attachListeners.last().onViewAttachedToWindow(view)
128 
129             runCurrent()
130             assertThat(block.invocationCount).isEqualTo(1)
131             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
132         }
133 
134     @Test
repeatWhenAttachedToWindow_viewAlreadyAttached_immediatelyRunsBlocknull135     fun repeatWhenAttachedToWindow_viewAlreadyAttached_immediatelyRunsBlock() =
136         testScope.runTest {
137             whenever(view.isAttachedToWindow).thenReturn(true)
138 
139             var innerJob: Job? = null
140             backgroundScope.launch {
141                 view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } }
142             }
143             runCurrent()
144 
145             assertThat(innerJob?.isActive).isEqualTo(true)
146         }
147 
148     @Test
repeatWhenAttachedToWindow_viewStartsDetached_runsBlockWhenAttachednull149     fun repeatWhenAttachedToWindow_viewStartsDetached_runsBlockWhenAttached() =
150         testScope.runTest {
151             whenever(view.isAttachedToWindow).thenReturn(false)
152             var innerJob: Job? = null
153             backgroundScope.launch {
154                 view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } }
155             }
156             runCurrent()
157 
158             assertThat(innerJob?.isActive).isNotEqualTo(true)
159 
160             whenever(view.isAttachedToWindow).thenReturn(true)
161             attachListeners.last().onViewAttachedToWindow(view)
162             runCurrent()
163 
164             assertThat(innerJob?.isActive).isEqualTo(true)
165         }
166 
167     @Test
repeatWhenAttachedToWindow_viewGetsDetached_cancelsBlocknull168     fun repeatWhenAttachedToWindow_viewGetsDetached_cancelsBlock() =
169         testScope.runTest {
170             whenever(view.isAttachedToWindow).thenReturn(true)
171             var innerJob: Job? = null
172             backgroundScope.launch {
173                 view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } }
174             }
175             runCurrent()
176 
177             assertThat(innerJob?.isActive).isEqualTo(true)
178 
179             whenever(view.isAttachedToWindow).thenReturn(false)
180             attachListeners.last().onViewDetachedFromWindow(view)
181             runCurrent()
182 
183             assertThat(innerJob?.isActive).isNotEqualTo(true)
184         }
185 
186     @Test
repeatWhenAttached_viewAlreadyAttached_immediatelyRunsBlocknull187     fun repeatWhenAttached_viewAlreadyAttached_immediatelyRunsBlock() =
188         testScope.runTest {
189             whenever(view.isAttachedToWindow).thenReturn(true)
190 
191             repeatWhenAttached()
192 
193             runCurrent()
194             assertThat(block.invocationCount).isEqualTo(1)
195             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
196         }
197 
198     @Test
repeatWhenAttached_startsVisibleWithoutFocus_STARTEDnull199     fun repeatWhenAttached_startsVisibleWithoutFocus_STARTED() =
200         testScope.runTest {
201             whenever(view.isAttachedToWindow).thenReturn(true)
202             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
203 
204             repeatWhenAttached()
205 
206             runCurrent()
207             assertThat(block.invocationCount).isEqualTo(1)
208             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
209         }
210 
211     @Test
repeatWhenWindowIsVisible_startsAlreadyVisible_immediatelyRunsBlocknull212     fun repeatWhenWindowIsVisible_startsAlreadyVisible_immediatelyRunsBlock() =
213         testScope.runTest {
214             whenever(view.isAttachedToWindow).thenReturn(true)
215             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
216 
217             var innerJob: Job? = null
218             backgroundScope.launch {
219                 view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } }
220             }
221             runCurrent()
222 
223             assertThat(innerJob?.isActive).isEqualTo(true)
224         }
225 
226     @Test
repeatWhenWindowIsVisible_startsInvisible_runsBlockWhenVisiblenull227     fun repeatWhenWindowIsVisible_startsInvisible_runsBlockWhenVisible() =
228         testScope.runTest {
229             whenever(view.isAttachedToWindow).thenReturn(true)
230             whenever(view.windowVisibility).thenReturn(View.INVISIBLE)
231 
232             var innerJob: Job? = null
233             backgroundScope.launch {
234                 view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } }
235             }
236             runCurrent()
237 
238             assertThat(innerJob?.isActive).isNotEqualTo(true)
239 
240             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
241             argCaptor { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture()) }
242                 .forEach { it.onWindowVisibilityChanged(View.VISIBLE) }
243             runCurrent()
244 
245             assertThat(innerJob?.isActive).isEqualTo(true)
246         }
247 
248     @Test
repeatWhenWindowIsVisible_becomesInvisible_cancelsBlocknull249     fun repeatWhenWindowIsVisible_becomesInvisible_cancelsBlock() =
250         testScope.runTest {
251             whenever(view.isAttachedToWindow).thenReturn(true)
252             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
253 
254             var innerJob: Job? = null
255             backgroundScope.launch {
256                 view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } }
257             }
258             runCurrent()
259 
260             assertThat(innerJob?.isActive).isEqualTo(true)
261 
262             whenever(view.windowVisibility).thenReturn(View.INVISIBLE)
263             argCaptor { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture()) }
264                 .forEach { it.onWindowVisibilityChanged(View.INVISIBLE) }
265             runCurrent()
266 
267             assertThat(innerJob?.isActive).isNotEqualTo(true)
268         }
269 
270     @Test
repeatWhenAttached_startsWithFocusButInvisible_CREATEDnull271     fun repeatWhenAttached_startsWithFocusButInvisible_CREATED() =
272         testScope.runTest {
273             whenever(view.isAttachedToWindow).thenReturn(true)
274             whenever(view.hasWindowFocus()).thenReturn(true)
275 
276             repeatWhenAttached()
277 
278             runCurrent()
279             assertThat(block.invocationCount).isEqualTo(1)
280             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
281         }
282 
283     @Test
repeatWhenAttached_startsVisibleAndWithFocus_RESUMEDnull284     fun repeatWhenAttached_startsVisibleAndWithFocus_RESUMED() =
285         testScope.runTest {
286             whenever(view.isAttachedToWindow).thenReturn(true)
287             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
288             whenever(view.hasWindowFocus()).thenReturn(true)
289 
290             repeatWhenAttached()
291 
292             runCurrent()
293             assertThat(block.invocationCount).isEqualTo(1)
294             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
295         }
296 
297     @Test
repeatWhenWindowHasFocus_startsWithFocus_immediatelyRunsBlocknull298     fun repeatWhenWindowHasFocus_startsWithFocus_immediatelyRunsBlock() =
299         testScope.runTest {
300             whenever(view.isAttachedToWindow).thenReturn(true)
301             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
302             whenever(view.hasWindowFocus()).thenReturn(true)
303 
304             var innerJob: Job? = null
305             backgroundScope.launch {
306                 view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } }
307             }
308             runCurrent()
309 
310             assertThat(innerJob?.isActive).isEqualTo(true)
311         }
312 
313     @Test
repeatWhenWindowHasFocus_startsWithoutFocus_runsBlockWhenFocusednull314     fun repeatWhenWindowHasFocus_startsWithoutFocus_runsBlockWhenFocused() =
315         testScope.runTest {
316             whenever(view.isAttachedToWindow).thenReturn(true)
317             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
318             whenever(view.hasWindowFocus()).thenReturn(false)
319 
320             var innerJob: Job? = null
321             backgroundScope.launch {
322                 view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } }
323             }
324             runCurrent()
325 
326             assertThat(innerJob?.isActive).isNotEqualTo(true)
327 
328             whenever(view.hasWindowFocus()).thenReturn(true)
329 
330             argCaptor { verify(viewTreeObserver).addOnWindowFocusChangeListener(capture()) }
331                 .forEach { it.onWindowFocusChanged(true) }
332             runCurrent()
333 
334             assertThat(innerJob?.isActive).isEqualTo(true)
335         }
336 
337     @Test
repeatWhenWindowHasFocus_losesFocus_cancelsBlocknull338     fun repeatWhenWindowHasFocus_losesFocus_cancelsBlock() =
339         testScope.runTest {
340             whenever(view.isAttachedToWindow).thenReturn(true)
341             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
342             whenever(view.hasWindowFocus()).thenReturn(true)
343 
344             var innerJob: Job? = null
345             backgroundScope.launch {
346                 view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } }
347             }
348             runCurrent()
349 
350             assertThat(innerJob?.isActive).isEqualTo(true)
351 
352             whenever(view.hasWindowFocus()).thenReturn(false)
353             argCaptor { verify(viewTreeObserver).addOnWindowFocusChangeListener(capture()) }
354                 .forEach { it.onWindowFocusChanged(false) }
355             runCurrent()
356 
357             assertThat(innerJob?.isActive).isNotEqualTo(true)
358         }
359 
360     @Test
repeatWhenAttached_becomesVisibleWithoutFocus_STARTEDnull361     fun repeatWhenAttached_becomesVisibleWithoutFocus_STARTED() =
362         testScope.runTest {
363             whenever(view.isAttachedToWindow).thenReturn(true)
364             repeatWhenAttached()
365             val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
366             verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture())
367 
368             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
369             listenerCaptor.lastValue.onWindowVisibilityChanged(View.VISIBLE)
370 
371             runCurrent()
372             assertThat(block.invocationCount).isEqualTo(1)
373             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
374         }
375 
376     @Test
repeatWhenAttached_gainsFocusButInvisible_CREATEDnull377     fun repeatWhenAttached_gainsFocusButInvisible_CREATED() =
378         testScope.runTest {
379             whenever(view.isAttachedToWindow).thenReturn(true)
380             repeatWhenAttached()
381             val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
382             verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture())
383 
384             whenever(view.hasWindowFocus()).thenReturn(true)
385             listenerCaptor.lastValue.onWindowFocusChanged(true)
386 
387             runCurrent()
388             assertThat(block.invocationCount).isEqualTo(1)
389             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
390         }
391 
392     @Test
repeatWhenAttached_becomesVisibleAndGainsFocus_RESUMEDnull393     fun repeatWhenAttached_becomesVisibleAndGainsFocus_RESUMED() =
394         testScope.runTest {
395             whenever(view.isAttachedToWindow).thenReturn(true)
396             repeatWhenAttached()
397             val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
398             verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture())
399             val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
400             verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture())
401 
402             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
403             visibleCaptor.lastValue.onWindowVisibilityChanged(View.VISIBLE)
404             whenever(view.hasWindowFocus()).thenReturn(true)
405             focusCaptor.lastValue.onWindowFocusChanged(true)
406 
407             runCurrent()
408             assertThat(block.invocationCount).isEqualTo(1)
409             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
410         }
411 
412     @Test
repeatWhenAttached_viewGetsDetached_destroysTheLifecyclenull413     fun repeatWhenAttached_viewGetsDetached_destroysTheLifecycle() =
414         testScope.runTest {
415             whenever(view.isAttachedToWindow).thenReturn(true)
416             repeatWhenAttached()
417 
418             whenever(view.isAttachedToWindow).thenReturn(false)
419             attachListeners.last().onViewDetachedFromWindow(view)
420 
421             runCurrent()
422             assertThat(block.invocationCount).isEqualTo(1)
423             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
424         }
425 
426     @Test
repeatWhenAttached_viewGetsReattached_recreatesAlifecyclenull427     fun repeatWhenAttached_viewGetsReattached_recreatesAlifecycle() =
428         testScope.runTest {
429             whenever(view.isAttachedToWindow).thenReturn(true)
430             repeatWhenAttached()
431             whenever(view.isAttachedToWindow).thenReturn(false)
432             attachListeners.last().onViewDetachedFromWindow(view)
433 
434             whenever(view.isAttachedToWindow).thenReturn(true)
435             attachListeners.last().onViewAttachedToWindow(view)
436 
437             runCurrent()
438             assertThat(block.invocationCount).isEqualTo(2)
439             assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
440             assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED)
441         }
442 
443     @Test
repeatWhenAttached_disposeAttachednull444     fun repeatWhenAttached_disposeAttached() =
445         testScope.runTest {
446             whenever(view.isAttachedToWindow).thenReturn(true)
447             val handle = repeatWhenAttached()
448 
449             handle.dispose()
450 
451             runCurrent()
452             assertThat(attachListeners).isEmpty()
453             assertThat(block.invocationCount).isEqualTo(1)
454             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
455         }
456 
457     @Test
repeatWhenAttached_disposeNeverAttachednull458     fun repeatWhenAttached_disposeNeverAttached() =
459         testScope.runTest {
460             whenever(view.isAttachedToWindow).thenReturn(false)
461             val handle = repeatWhenAttached()
462 
463             handle.dispose()
464 
465             assertThat(attachListeners).isEmpty()
466             assertThat(block.invocationCount).isEqualTo(0)
467         }
468 
469     @Test
repeatWhenAttached_disposePreviouslyAttachedNowDetachednull470     fun repeatWhenAttached_disposePreviouslyAttachedNowDetached() =
471         testScope.runTest {
472             whenever(view.isAttachedToWindow).thenReturn(true)
473             val handle = repeatWhenAttached()
474             attachListeners.last().onViewDetachedFromWindow(view)
475 
476             handle.dispose()
477 
478             runCurrent()
479             assertThat(attachListeners).isEmpty()
480             assertThat(block.invocationCount).isEqualTo(1)
481             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
482         }
483 
CoroutineScopenull484     private fun CoroutineScope.repeatWhenAttached(): DisposableHandle {
485         return view.repeatWhenAttached(
486             coroutineContext = coroutineContext,
487             block = block,
488         )
489     }
490 
491     private class Block : suspend LifecycleOwner.(View) -> Unit {
492         data class Invocation(
493             val lifecycleOwner: LifecycleOwner,
494         ) {
495             val lifecycleState: Lifecycle.State
496                 get() = lifecycleOwner.lifecycle.currentState
497         }
498 
499         private val _invocations = mutableListOf<Invocation>()
500         val invocations: List<Invocation> = _invocations
501         val invocationCount: Int
502             get() = _invocations.size
503 
504         val latestLifecycleState: Lifecycle.State
505             get() = _invocations.last().lifecycleState
506 
invokenull507         override suspend fun invoke(lifecycleOwner: LifecycleOwner, view: View) {
508             _invocations.add(Invocation(lifecycleOwner))
509         }
510     }
511 }
512 
argCaptornull513 private inline fun <reified T : Any> argCaptor(block: KArgumentCaptor<T>.() -> Unit) =
514     argumentCaptor<T>().apply { block() }
515 
forEachnull516 private inline fun <reified T : Any> KArgumentCaptor<T>.forEach(block: (T) -> Unit): Unit =
517     allValues.forEach(block)
518