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