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