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 argCaptornull513private inline fun <reified T : Any> argCaptor(block: KArgumentCaptor<T>.() -> Unit) = 514 argumentCaptor<T>().apply { block() } 515 forEachnull516private inline fun <reified T : Any> KArgumentCaptor<T>.forEach(block: (T) -> Unit): Unit = 517 allValues.forEach(block) 518