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.android.systemui.util.mockito.argumentCaptor
28 import com.google.common.truth.Truth.assertThat
29 import kotlinx.coroutines.CoroutineScope
30 import kotlinx.coroutines.Dispatchers
31 import kotlinx.coroutines.DisposableHandle
32 import kotlinx.coroutines.ExperimentalCoroutinesApi
33 import kotlinx.coroutines.test.StandardTestDispatcher
34 import kotlinx.coroutines.test.TestScope
35 import kotlinx.coroutines.test.resetMain
36 import kotlinx.coroutines.test.runCurrent
37 import kotlinx.coroutines.test.runTest
38 import kotlinx.coroutines.test.setMain
39 import org.junit.After
40 import org.junit.Before
41 import org.junit.Rule
42 import org.junit.Test
43 import org.junit.runner.RunWith
44 import org.junit.runners.JUnit4
45 import org.mockito.Mock
46 import org.mockito.Mockito.any
47 import org.mockito.Mockito.verify
48 import org.mockito.Mockito.`when` as whenever
49 import org.mockito.junit.MockitoJUnit
50 
51 @OptIn(ExperimentalCoroutinesApi::class)
52 @SmallTest
53 @RunWith(JUnit4::class)
54 class RepeatWhenAttachedTest : SysuiTestCase() {
55 
56     @JvmField @Rule val mockito = MockitoJUnit.rule()
57     @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule()
58 
59     @Mock private lateinit var view: View
60     @Mock private lateinit var viewTreeObserver: ViewTreeObserver
61 
62     private lateinit var block: Block
63     private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener>
64     private lateinit var testScope: TestScope
65 
66     @Before
setUpnull67     fun setUp() {
68         val testDispatcher = StandardTestDispatcher()
69         testScope = TestScope(testDispatcher)
70         Dispatchers.setMain(testDispatcher)
71         Assert.setTestThread(Thread.currentThread())
72         whenever(view.viewTreeObserver).thenReturn(viewTreeObserver)
73         whenever(view.windowVisibility).thenReturn(View.GONE)
74         whenever(view.hasWindowFocus()).thenReturn(false)
75         attachListeners = mutableListOf()
76         whenever(view.addOnAttachStateChangeListener(any())).then {
77             attachListeners.add(it.arguments[0] as View.OnAttachStateChangeListener)
78         }
79         whenever(view.removeOnAttachStateChangeListener(any())).then {
80             attachListeners.remove(it.arguments[0] as View.OnAttachStateChangeListener)
81         }
82         block = Block()
83     }
84 
85     @After
tearDownnull86     fun tearDown() {
87         Dispatchers.resetMain()
88     }
89 
90     @Test(expected = IllegalStateException::class)
repeatWhenAttached_enforcesMainThreadnull91     fun repeatWhenAttached_enforcesMainThread() =
92         testScope.runTest {
93             Assert.setTestThread(null)
94 
95             repeatWhenAttached()
96         }
97 
98     @Test(expected = IllegalStateException::class)
repeatWhenAttached_disposeEnforcesMainThreadnull99     fun repeatWhenAttached_disposeEnforcesMainThread() =
100         testScope.runTest {
101             val disposableHandle = repeatWhenAttached()
102             Assert.setTestThread(null)
103 
104             disposableHandle.dispose()
105         }
106 
107     @Test
repeatWhenAttached_viewStartsDetached_runsBlockWhenAttachednull108     fun repeatWhenAttached_viewStartsDetached_runsBlockWhenAttached() =
109         testScope.runTest {
110             whenever(view.isAttachedToWindow).thenReturn(false)
111             repeatWhenAttached()
112             assertThat(block.invocationCount).isEqualTo(0)
113 
114             whenever(view.isAttachedToWindow).thenReturn(true)
115             attachListeners.last().onViewAttachedToWindow(view)
116 
117             runCurrent()
118             assertThat(block.invocationCount).isEqualTo(1)
119             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
120         }
121 
122     @Test
repeatWhenAttached_viewAlreadyAttached_immediatelyRunsBlocknull123     fun repeatWhenAttached_viewAlreadyAttached_immediatelyRunsBlock() =
124         testScope.runTest {
125             whenever(view.isAttachedToWindow).thenReturn(true)
126 
127             repeatWhenAttached()
128 
129             runCurrent()
130             assertThat(block.invocationCount).isEqualTo(1)
131             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
132         }
133 
134     @Test
repeatWhenAttached_startsVisibleWithoutFocus_STARTEDnull135     fun repeatWhenAttached_startsVisibleWithoutFocus_STARTED() =
136         testScope.runTest {
137             whenever(view.isAttachedToWindow).thenReturn(true)
138             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
139 
140             repeatWhenAttached()
141 
142             runCurrent()
143             assertThat(block.invocationCount).isEqualTo(1)
144             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
145         }
146 
147     @Test
repeatWhenAttached_startsWithFocusButInvisible_CREATEDnull148     fun repeatWhenAttached_startsWithFocusButInvisible_CREATED() =
149         testScope.runTest {
150             whenever(view.isAttachedToWindow).thenReturn(true)
151             whenever(view.hasWindowFocus()).thenReturn(true)
152 
153             repeatWhenAttached()
154 
155             runCurrent()
156             assertThat(block.invocationCount).isEqualTo(1)
157             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
158         }
159 
160     @Test
repeatWhenAttached_startsVisibleAndWithFocus_RESUMEDnull161     fun repeatWhenAttached_startsVisibleAndWithFocus_RESUMED() =
162         testScope.runTest {
163             whenever(view.isAttachedToWindow).thenReturn(true)
164             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
165             whenever(view.hasWindowFocus()).thenReturn(true)
166 
167             repeatWhenAttached()
168 
169             runCurrent()
170             assertThat(block.invocationCount).isEqualTo(1)
171             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
172         }
173 
174     @Test
repeatWhenAttached_becomesVisibleWithoutFocus_STARTEDnull175     fun repeatWhenAttached_becomesVisibleWithoutFocus_STARTED() =
176         testScope.runTest {
177             whenever(view.isAttachedToWindow).thenReturn(true)
178             repeatWhenAttached()
179             val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
180             verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture())
181 
182             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
183             listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
184 
185             runCurrent()
186             assertThat(block.invocationCount).isEqualTo(1)
187             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
188         }
189 
190     @Test
repeatWhenAttached_gainsFocusButInvisible_CREATEDnull191     fun repeatWhenAttached_gainsFocusButInvisible_CREATED() =
192         testScope.runTest {
193             whenever(view.isAttachedToWindow).thenReturn(true)
194             repeatWhenAttached()
195             val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
196             verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture())
197 
198             whenever(view.hasWindowFocus()).thenReturn(true)
199             listenerCaptor.value.onWindowFocusChanged(true)
200 
201             runCurrent()
202             assertThat(block.invocationCount).isEqualTo(1)
203             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
204         }
205 
206     @Test
repeatWhenAttached_becomesVisibleAndGainsFocus_RESUMEDnull207     fun repeatWhenAttached_becomesVisibleAndGainsFocus_RESUMED() =
208         testScope.runTest {
209             whenever(view.isAttachedToWindow).thenReturn(true)
210             repeatWhenAttached()
211             val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
212             verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture())
213             val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
214             verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture())
215 
216             whenever(view.windowVisibility).thenReturn(View.VISIBLE)
217             visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
218             whenever(view.hasWindowFocus()).thenReturn(true)
219             focusCaptor.value.onWindowFocusChanged(true)
220 
221             runCurrent()
222             assertThat(block.invocationCount).isEqualTo(1)
223             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
224         }
225 
226     @Test
repeatWhenAttached_viewGetsDetached_destroysTheLifecyclenull227     fun repeatWhenAttached_viewGetsDetached_destroysTheLifecycle() =
228         testScope.runTest {
229             whenever(view.isAttachedToWindow).thenReturn(true)
230             repeatWhenAttached()
231 
232             whenever(view.isAttachedToWindow).thenReturn(false)
233             attachListeners.last().onViewDetachedFromWindow(view)
234 
235             runCurrent()
236             assertThat(block.invocationCount).isEqualTo(1)
237             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
238         }
239 
240     @Test
repeatWhenAttached_viewGetsReattached_recreatesAlifecyclenull241     fun repeatWhenAttached_viewGetsReattached_recreatesAlifecycle() =
242         testScope.runTest {
243             whenever(view.isAttachedToWindow).thenReturn(true)
244             repeatWhenAttached()
245             whenever(view.isAttachedToWindow).thenReturn(false)
246             attachListeners.last().onViewDetachedFromWindow(view)
247 
248             whenever(view.isAttachedToWindow).thenReturn(true)
249             attachListeners.last().onViewAttachedToWindow(view)
250 
251             runCurrent()
252             assertThat(block.invocationCount).isEqualTo(2)
253             assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
254             assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED)
255         }
256 
257     @Test
repeatWhenAttached_disposeAttachednull258     fun repeatWhenAttached_disposeAttached() =
259         testScope.runTest {
260             whenever(view.isAttachedToWindow).thenReturn(true)
261             val handle = repeatWhenAttached()
262 
263             handle.dispose()
264 
265             runCurrent()
266             assertThat(attachListeners).isEmpty()
267             assertThat(block.invocationCount).isEqualTo(1)
268             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
269         }
270 
271     @Test
repeatWhenAttached_disposeNeverAttachednull272     fun repeatWhenAttached_disposeNeverAttached() =
273         testScope.runTest {
274             whenever(view.isAttachedToWindow).thenReturn(false)
275             val handle = repeatWhenAttached()
276 
277             handle.dispose()
278 
279             assertThat(attachListeners).isEmpty()
280             assertThat(block.invocationCount).isEqualTo(0)
281         }
282 
283     @Test
repeatWhenAttached_disposePreviouslyAttachedNowDetachednull284     fun repeatWhenAttached_disposePreviouslyAttachedNowDetached() =
285         testScope.runTest {
286             whenever(view.isAttachedToWindow).thenReturn(true)
287             val handle = repeatWhenAttached()
288             attachListeners.last().onViewDetachedFromWindow(view)
289 
290             handle.dispose()
291 
292             runCurrent()
293             assertThat(attachListeners).isEmpty()
294             assertThat(block.invocationCount).isEqualTo(1)
295             assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
296         }
297 
CoroutineScopenull298     private fun CoroutineScope.repeatWhenAttached(): DisposableHandle {
299         return view.repeatWhenAttached(
300             coroutineContext = coroutineContext,
301             block = block,
302         )
303     }
304 
305     private class Block : suspend LifecycleOwner.(View) -> Unit {
306         data class Invocation(
307             val lifecycleOwner: LifecycleOwner,
308         ) {
309             val lifecycleState: Lifecycle.State
310                 get() = lifecycleOwner.lifecycle.currentState
311         }
312 
313         private val _invocations = mutableListOf<Invocation>()
314         val invocations: List<Invocation> = _invocations
315         val invocationCount: Int
316             get() = _invocations.size
317         val latestLifecycleState: Lifecycle.State
318             get() = _invocations.last().lifecycleState
319 
invokenull320         override suspend fun invoke(lifecycleOwner: LifecycleOwner, view: View) {
321             _invocations.add(Invocation(lifecycleOwner))
322         }
323     }
324 }
325