• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.testing.TestableLooper.RunWithLooper
21 import android.view.View
22 import android.view.ViewTreeObserver
23 import androidx.lifecycle.Lifecycle
24 import androidx.lifecycle.LifecycleOwner
25 import androidx.test.filters.SmallTest
26 import com.android.systemui.SysuiTestCase
27 import com.android.systemui.util.Assert
28 import com.android.systemui.util.mockito.argumentCaptor
29 import com.google.common.truth.Truth.assertThat
30 import kotlinx.coroutines.CoroutineScope
31 import kotlinx.coroutines.DisposableHandle
32 import kotlinx.coroutines.test.runBlockingTest
33 import org.junit.Before
34 import org.junit.Rule
35 import org.junit.Test
36 import org.junit.runner.RunWith
37 import org.junit.runners.JUnit4
38 import org.mockito.Mock
39 import org.mockito.Mockito.any
40 import org.mockito.Mockito.verify
41 import org.mockito.Mockito.`when` as whenever
42 import org.mockito.junit.MockitoJUnit
43 
44 @SmallTest
45 @RunWith(JUnit4::class)
46 @RunWithLooper
47 class RepeatWhenAttachedTest : SysuiTestCase() {
48 
49     @JvmField @Rule val mockito = MockitoJUnit.rule()
50     @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule()
51 
52     @Mock private lateinit var view: View
53     @Mock private lateinit var viewTreeObserver: ViewTreeObserver
54 
55     private lateinit var block: Block
56     private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener>
57 
58     @Before
setUpnull59     fun setUp() {
60         Assert.setTestThread(Thread.currentThread())
61         whenever(view.viewTreeObserver).thenReturn(viewTreeObserver)
62         whenever(view.windowVisibility).thenReturn(View.GONE)
63         whenever(view.hasWindowFocus()).thenReturn(false)
64         attachListeners = mutableListOf()
65         whenever(view.addOnAttachStateChangeListener(any())).then {
66             attachListeners.add(it.arguments[0] as View.OnAttachStateChangeListener)
67         }
68         whenever(view.removeOnAttachStateChangeListener(any())).then {
69             attachListeners.remove(it.arguments[0] as View.OnAttachStateChangeListener)
70         }
71         block = Block()
72     }
73 
74     @Test(expected = IllegalStateException::class)
<lambda>null75     fun `repeatWhenAttached - enforces main thread`() = runBlockingTest {
76         Assert.setTestThread(null)
77 
78         repeatWhenAttached()
79     }
80 
81     @Test(expected = IllegalStateException::class)
<lambda>null82     fun `repeatWhenAttached - dispose enforces main thread`() = runBlockingTest {
83         val disposableHandle = repeatWhenAttached()
84         Assert.setTestThread(null)
85 
86         disposableHandle.dispose()
87     }
88 
89     @Test
<lambda>null90     fun `repeatWhenAttached - view starts detached - runs block when attached`() = runBlockingTest {
91         whenever(view.isAttachedToWindow).thenReturn(false)
92         repeatWhenAttached()
93         assertThat(block.invocationCount).isEqualTo(0)
94 
95         whenever(view.isAttachedToWindow).thenReturn(true)
96         attachListeners.last().onViewAttachedToWindow(view)
97 
98         assertThat(block.invocationCount).isEqualTo(1)
99         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
100     }
101 
102     @Test
<lambda>null103     fun `repeatWhenAttached - view already attached - immediately runs block`() = runBlockingTest {
104         whenever(view.isAttachedToWindow).thenReturn(true)
105 
106         repeatWhenAttached()
107 
108         assertThat(block.invocationCount).isEqualTo(1)
109         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
110     }
111 
112     @Test
<lambda>null113     fun `repeatWhenAttached - starts visible without focus - STARTED`() = runBlockingTest {
114         whenever(view.isAttachedToWindow).thenReturn(true)
115         whenever(view.windowVisibility).thenReturn(View.VISIBLE)
116 
117         repeatWhenAttached()
118 
119         assertThat(block.invocationCount).isEqualTo(1)
120         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
121     }
122 
123     @Test
<lambda>null124     fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = runBlockingTest {
125         whenever(view.isAttachedToWindow).thenReturn(true)
126         whenever(view.hasWindowFocus()).thenReturn(true)
127 
128         repeatWhenAttached()
129 
130         assertThat(block.invocationCount).isEqualTo(1)
131         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
132     }
133 
134     @Test
<lambda>null135     fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = runBlockingTest {
136         whenever(view.isAttachedToWindow).thenReturn(true)
137         whenever(view.windowVisibility).thenReturn(View.VISIBLE)
138         whenever(view.hasWindowFocus()).thenReturn(true)
139 
140         repeatWhenAttached()
141 
142         assertThat(block.invocationCount).isEqualTo(1)
143         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
144     }
145 
146     @Test
<lambda>null147     fun `repeatWhenAttached - becomes visible without focus - STARTED`() = runBlockingTest {
148         whenever(view.isAttachedToWindow).thenReturn(true)
149         repeatWhenAttached()
150         val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
151         verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture())
152 
153         whenever(view.windowVisibility).thenReturn(View.VISIBLE)
154         listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
155 
156         assertThat(block.invocationCount).isEqualTo(1)
157         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
158     }
159 
160     @Test
<lambda>null161     fun `repeatWhenAttached - gains focus but invisible - CREATED`() = runBlockingTest {
162         whenever(view.isAttachedToWindow).thenReturn(true)
163         repeatWhenAttached()
164         val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
165         verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture())
166 
167         whenever(view.hasWindowFocus()).thenReturn(true)
168         listenerCaptor.value.onWindowFocusChanged(true)
169 
170         assertThat(block.invocationCount).isEqualTo(1)
171         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
172     }
173 
174     @Test
<lambda>null175     fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = runBlockingTest {
176         whenever(view.isAttachedToWindow).thenReturn(true)
177         repeatWhenAttached()
178         val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
179         verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture())
180         val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
181         verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture())
182 
183         whenever(view.windowVisibility).thenReturn(View.VISIBLE)
184         visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
185         whenever(view.hasWindowFocus()).thenReturn(true)
186         focusCaptor.value.onWindowFocusChanged(true)
187 
188         assertThat(block.invocationCount).isEqualTo(1)
189         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
190     }
191 
192     @Test
<lambda>null193     fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = runBlockingTest {
194         whenever(view.isAttachedToWindow).thenReturn(true)
195         repeatWhenAttached()
196 
197         whenever(view.isAttachedToWindow).thenReturn(false)
198         attachListeners.last().onViewDetachedFromWindow(view)
199 
200         assertThat(block.invocationCount).isEqualTo(1)
201         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
202     }
203 
204     @Test
<lambda>null205     fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = runBlockingTest {
206         whenever(view.isAttachedToWindow).thenReturn(true)
207         repeatWhenAttached()
208         whenever(view.isAttachedToWindow).thenReturn(false)
209         attachListeners.last().onViewDetachedFromWindow(view)
210 
211         whenever(view.isAttachedToWindow).thenReturn(true)
212         attachListeners.last().onViewAttachedToWindow(view)
213 
214         assertThat(block.invocationCount).isEqualTo(2)
215         assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
216         assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED)
217     }
218 
219     @Test
<lambda>null220     fun `repeatWhenAttached - dispose attached`() = runBlockingTest {
221         whenever(view.isAttachedToWindow).thenReturn(true)
222         val handle = repeatWhenAttached()
223 
224         handle.dispose()
225 
226         assertThat(attachListeners).isEmpty()
227         assertThat(block.invocationCount).isEqualTo(1)
228         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
229     }
230 
231     @Test
<lambda>null232     fun `repeatWhenAttached - dispose never attached`() = runBlockingTest {
233         whenever(view.isAttachedToWindow).thenReturn(false)
234         val handle = repeatWhenAttached()
235 
236         handle.dispose()
237 
238         assertThat(attachListeners).isEmpty()
239         assertThat(block.invocationCount).isEqualTo(0)
240     }
241 
242     @Test
<lambda>null243     fun `repeatWhenAttached - dispose previously attached now detached`() = runBlockingTest {
244         whenever(view.isAttachedToWindow).thenReturn(true)
245         val handle = repeatWhenAttached()
246         attachListeners.last().onViewDetachedFromWindow(view)
247 
248         handle.dispose()
249 
250         assertThat(attachListeners).isEmpty()
251         assertThat(block.invocationCount).isEqualTo(1)
252         assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
253     }
254 
CoroutineScopenull255     private fun CoroutineScope.repeatWhenAttached(): DisposableHandle {
256         return view.repeatWhenAttached(
257             coroutineContext = coroutineContext,
258             block = block,
259         )
260     }
261 
262     private class Block : suspend LifecycleOwner.(View) -> Unit {
263         data class Invocation(
264             val lifecycleOwner: LifecycleOwner,
265         ) {
266             val lifecycleState: Lifecycle.State
267                 get() = lifecycleOwner.lifecycle.currentState
268         }
269 
270         private val _invocations = mutableListOf<Invocation>()
271         val invocations: List<Invocation> = _invocations
272         val invocationCount: Int
273             get() = _invocations.size
274         val latestLifecycleState: Lifecycle.State
275             get() = _invocations.last().lifecycleState
276 
invokenull277         override suspend fun invoke(lifecycleOwner: LifecycleOwner, view: View) {
278             _invocations.add(Invocation(lifecycleOwner))
279         }
280     }
281 }
282