Loading packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +107 −0 Original line number Diff line number Diff line Loading @@ -30,10 +30,21 @@ import com.android.app.tracing.coroutines.launch import com.android.systemui.Flags.coroutineTracing import com.android.systemui.util.Assert import com.android.systemui.util.Compile import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart /** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this Loading Loading @@ -215,6 +226,102 @@ private fun inferTraceSectionName(): String { } } /** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this * function, if the view was already attached), automatically canceling the work when the view * becomes detached. * * Only use from the main thread. * * The [block] may be run multiple times, running once per every time the view is attached. */ @MainThread suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing { Assert.isMainThread() isAttached.collectLatest { if (it) coroutineScope { block() } } awaitCancellation() // satisfies return type of Nothing } /** * Runs the given [block] every time the [Window] this [View] is attached to becomes visible (or * immediately after calling this function, if the window is already visible), automatically * canceling the work when the window becomes invisible. * * Only use from the main thread. * * The [block] may be run multiple times, running once per every time the window becomes visible. */ @MainThread suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing { Assert.isMainThread() isWindowVisible.collectLatest { if (it) coroutineScope { block() } } awaitCancellation() // satisfies return type of Nothing } /** * Runs the given [block] every time the [Window] this [View] is attached to has focus (or * immediately after calling this function, if the window is already focused), automatically * canceling the work when the window loses focus. * * Only use from the main thread. * * The [block] may be run multiple times, running once per every time the window is focused. */ @MainThread suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing { Assert.isMainThread() isWindowFocused.collectLatest { if (it) coroutineScope { block() } } awaitCancellation() // satisfies return type of Nothing } private val View.isAttached get() = conflatedCallbackFlow { val onAttachListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { Assert.isMainThread() trySend(true) } override fun onViewDetachedFromWindow(v: View) { trySend(false) } } addOnAttachStateChangeListener(onAttachListener) trySend(isAttachedToWindow) awaitClose { removeOnAttachStateChangeListener(onAttachListener) } } private val View.currentViewTreeObserver: Flow<ViewTreeObserver?> get() = isAttached.map { if (it) viewTreeObserver else null } private val View.isWindowVisible get() = currentViewTreeObserver.flatMapLatestConflated { vto -> vto?.isWindowVisible?.onStart { emit(windowVisibility == View.VISIBLE) } ?: emptyFlow() } private val View.isWindowFocused get() = currentViewTreeObserver.flatMapLatestConflated { vto -> vto?.isWindowFocused?.onStart { emit(hasWindowFocus()) } ?: emptyFlow() } private val ViewTreeObserver.isWindowFocused get() = conflatedCallbackFlow { val listener = ViewTreeObserver.OnWindowFocusChangeListener { trySend(it) } addOnWindowFocusChangeListener(listener) awaitClose { removeOnWindowFocusChangeListener(listener) } } private val ViewTreeObserver.isWindowVisible get() = conflatedCallbackFlow { val listener = ViewTreeObserver.OnWindowVisibilityChangeListener { v -> trySend(v == View.VISIBLE) } addOnWindowVisibilityChangeListener(listener) awaitClose { removeOnWindowVisibilityChangeListener(listener) } } /** * Even though there is only has one usage of `Dispatchers.Main` in this file, we cache it in a * top-level property so that we do not unnecessarily create new `CoroutineContext` objects for Loading packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt +198 −5 Original line number Diff line number Diff line Loading @@ -24,12 +24,14 @@ import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.util.Assert import com.android.systemui.util.mockito.argumentCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.resetMain Loading @@ -47,6 +49,8 @@ import org.mockito.Mockito.any import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.argumentCaptor @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading Loading @@ -95,6 +99,14 @@ class RepeatWhenAttachedTest : SysuiTestCase() { repeatWhenAttached() } @Test(expected = IllegalStateException::class) fun repeatWhenAttachedToWindow_enforcesMainThread() = testScope.runTest { Assert.setTestThread(null) view.repeatWhenAttachedToWindow {} } @Test(expected = IllegalStateException::class) fun repeatWhenAttached_disposeEnforcesMainThread() = testScope.runTest { Loading @@ -119,6 +131,58 @@ class RepeatWhenAttachedTest : SysuiTestCase() { assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) } @Test fun repeatWhenAttachedToWindow_viewAlreadyAttached_immediatelyRunsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenAttachedToWindow_viewStartsDetached_runsBlockWhenAttached() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(false) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) whenever(view.isAttachedToWindow).thenReturn(true) attachListeners.last().onViewAttachedToWindow(view) runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenAttachedToWindow_viewGetsDetached_cancelsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) whenever(view.isAttachedToWindow).thenReturn(false) attachListeners.last().onViewDetachedFromWindow(view) runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) } @Test fun repeatWhenAttached_viewAlreadyAttached_immediatelyRunsBlock() = testScope.runTest { Loading @@ -144,6 +208,65 @@ class RepeatWhenAttachedTest : SysuiTestCase() { assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) } @Test fun repeatWhenWindowIsVisible_startsAlreadyVisible_immediatelyRunsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowIsVisible_startsInvisible_runsBlockWhenVisible() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.INVISIBLE) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) argCaptor { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture()) } .forEach { it.onWindowVisibilityChanged(View.VISIBLE) } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowIsVisible_becomesInvisible_cancelsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) whenever(view.windowVisibility).thenReturn(View.INVISIBLE) argCaptor { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture()) } .forEach { it.onWindowVisibilityChanged(View.INVISIBLE) } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) } @Test fun repeatWhenAttached_startsWithFocusButInvisible_CREATED() = testScope.runTest { Loading Loading @@ -171,6 +294,69 @@ class RepeatWhenAttachedTest : SysuiTestCase() { assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) } @Test fun repeatWhenWindowHasFocus_startsWithFocus_immediatelyRunsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowHasFocus_startsWithoutFocus_runsBlockWhenFocused() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(false) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) whenever(view.hasWindowFocus()).thenReturn(true) argCaptor { verify(viewTreeObserver).addOnWindowFocusChangeListener(capture()) } .forEach { it.onWindowFocusChanged(true) } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowHasFocus_losesFocus_cancelsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) whenever(view.hasWindowFocus()).thenReturn(false) argCaptor { verify(viewTreeObserver).addOnWindowFocusChangeListener(capture()) } .forEach { it.onWindowFocusChanged(false) } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) } @Test fun repeatWhenAttached_becomesVisibleWithoutFocus_STARTED() = testScope.runTest { Loading @@ -180,7 +366,7 @@ class RepeatWhenAttachedTest : SysuiTestCase() { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) whenever(view.windowVisibility).thenReturn(View.VISIBLE) listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) listenerCaptor.lastValue.onWindowVisibilityChanged(View.VISIBLE) runCurrent() assertThat(block.invocationCount).isEqualTo(1) Loading @@ -196,7 +382,7 @@ class RepeatWhenAttachedTest : SysuiTestCase() { verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) whenever(view.hasWindowFocus()).thenReturn(true) listenerCaptor.value.onWindowFocusChanged(true) listenerCaptor.lastValue.onWindowFocusChanged(true) runCurrent() assertThat(block.invocationCount).isEqualTo(1) Loading @@ -214,9 +400,9 @@ class RepeatWhenAttachedTest : SysuiTestCase() { verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) whenever(view.windowVisibility).thenReturn(View.VISIBLE) visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) visibleCaptor.lastValue.onWindowVisibilityChanged(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(true) focusCaptor.value.onWindowFocusChanged(true) focusCaptor.lastValue.onWindowFocusChanged(true) runCurrent() assertThat(block.invocationCount).isEqualTo(1) Loading Loading @@ -314,6 +500,7 @@ class RepeatWhenAttachedTest : SysuiTestCase() { val invocations: List<Invocation> = _invocations val invocationCount: Int get() = _invocations.size val latestLifecycleState: Lifecycle.State get() = _invocations.last().lifecycleState Loading @@ -322,3 +509,9 @@ class RepeatWhenAttachedTest : SysuiTestCase() { } } } private inline fun <reified T : Any> argCaptor(block: KArgumentCaptor<T>.() -> Unit) = argumentCaptor<T>().apply { block() } private inline fun <reified T : Any> KArgumentCaptor<T>.forEach(block: (T) -> Unit): Unit = allValues.forEach(block) Loading
packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +107 −0 Original line number Diff line number Diff line Loading @@ -30,10 +30,21 @@ import com.android.app.tracing.coroutines.launch import com.android.systemui.Flags.coroutineTracing import com.android.systemui.util.Assert import com.android.systemui.util.Compile import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart /** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this Loading Loading @@ -215,6 +226,102 @@ private fun inferTraceSectionName(): String { } } /** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this * function, if the view was already attached), automatically canceling the work when the view * becomes detached. * * Only use from the main thread. * * The [block] may be run multiple times, running once per every time the view is attached. */ @MainThread suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing { Assert.isMainThread() isAttached.collectLatest { if (it) coroutineScope { block() } } awaitCancellation() // satisfies return type of Nothing } /** * Runs the given [block] every time the [Window] this [View] is attached to becomes visible (or * immediately after calling this function, if the window is already visible), automatically * canceling the work when the window becomes invisible. * * Only use from the main thread. * * The [block] may be run multiple times, running once per every time the window becomes visible. */ @MainThread suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing { Assert.isMainThread() isWindowVisible.collectLatest { if (it) coroutineScope { block() } } awaitCancellation() // satisfies return type of Nothing } /** * Runs the given [block] every time the [Window] this [View] is attached to has focus (or * immediately after calling this function, if the window is already focused), automatically * canceling the work when the window loses focus. * * Only use from the main thread. * * The [block] may be run multiple times, running once per every time the window is focused. */ @MainThread suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing { Assert.isMainThread() isWindowFocused.collectLatest { if (it) coroutineScope { block() } } awaitCancellation() // satisfies return type of Nothing } private val View.isAttached get() = conflatedCallbackFlow { val onAttachListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { Assert.isMainThread() trySend(true) } override fun onViewDetachedFromWindow(v: View) { trySend(false) } } addOnAttachStateChangeListener(onAttachListener) trySend(isAttachedToWindow) awaitClose { removeOnAttachStateChangeListener(onAttachListener) } } private val View.currentViewTreeObserver: Flow<ViewTreeObserver?> get() = isAttached.map { if (it) viewTreeObserver else null } private val View.isWindowVisible get() = currentViewTreeObserver.flatMapLatestConflated { vto -> vto?.isWindowVisible?.onStart { emit(windowVisibility == View.VISIBLE) } ?: emptyFlow() } private val View.isWindowFocused get() = currentViewTreeObserver.flatMapLatestConflated { vto -> vto?.isWindowFocused?.onStart { emit(hasWindowFocus()) } ?: emptyFlow() } private val ViewTreeObserver.isWindowFocused get() = conflatedCallbackFlow { val listener = ViewTreeObserver.OnWindowFocusChangeListener { trySend(it) } addOnWindowFocusChangeListener(listener) awaitClose { removeOnWindowFocusChangeListener(listener) } } private val ViewTreeObserver.isWindowVisible get() = conflatedCallbackFlow { val listener = ViewTreeObserver.OnWindowVisibilityChangeListener { v -> trySend(v == View.VISIBLE) } addOnWindowVisibilityChangeListener(listener) awaitClose { removeOnWindowVisibilityChangeListener(listener) } } /** * Even though there is only has one usage of `Dispatchers.Main` in this file, we cache it in a * top-level property so that we do not unnecessarily create new `CoroutineContext` objects for Loading
packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt +198 −5 Original line number Diff line number Diff line Loading @@ -24,12 +24,14 @@ import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.util.Assert import com.android.systemui.util.mockito.argumentCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.resetMain Loading @@ -47,6 +49,8 @@ import org.mockito.Mockito.any import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.argumentCaptor @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading Loading @@ -95,6 +99,14 @@ class RepeatWhenAttachedTest : SysuiTestCase() { repeatWhenAttached() } @Test(expected = IllegalStateException::class) fun repeatWhenAttachedToWindow_enforcesMainThread() = testScope.runTest { Assert.setTestThread(null) view.repeatWhenAttachedToWindow {} } @Test(expected = IllegalStateException::class) fun repeatWhenAttached_disposeEnforcesMainThread() = testScope.runTest { Loading @@ -119,6 +131,58 @@ class RepeatWhenAttachedTest : SysuiTestCase() { assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) } @Test fun repeatWhenAttachedToWindow_viewAlreadyAttached_immediatelyRunsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenAttachedToWindow_viewStartsDetached_runsBlockWhenAttached() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(false) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) whenever(view.isAttachedToWindow).thenReturn(true) attachListeners.last().onViewAttachedToWindow(view) runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenAttachedToWindow_viewGetsDetached_cancelsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenAttachedToWindow { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) whenever(view.isAttachedToWindow).thenReturn(false) attachListeners.last().onViewDetachedFromWindow(view) runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) } @Test fun repeatWhenAttached_viewAlreadyAttached_immediatelyRunsBlock() = testScope.runTest { Loading @@ -144,6 +208,65 @@ class RepeatWhenAttachedTest : SysuiTestCase() { assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) } @Test fun repeatWhenWindowIsVisible_startsAlreadyVisible_immediatelyRunsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowIsVisible_startsInvisible_runsBlockWhenVisible() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.INVISIBLE) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) argCaptor { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture()) } .forEach { it.onWindowVisibilityChanged(View.VISIBLE) } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowIsVisible_becomesInvisible_cancelsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowIsVisible { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) whenever(view.windowVisibility).thenReturn(View.INVISIBLE) argCaptor { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture()) } .forEach { it.onWindowVisibilityChanged(View.INVISIBLE) } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) } @Test fun repeatWhenAttached_startsWithFocusButInvisible_CREATED() = testScope.runTest { Loading Loading @@ -171,6 +294,69 @@ class RepeatWhenAttachedTest : SysuiTestCase() { assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) } @Test fun repeatWhenWindowHasFocus_startsWithFocus_immediatelyRunsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowHasFocus_startsWithoutFocus_runsBlockWhenFocused() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(false) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) whenever(view.hasWindowFocus()).thenReturn(true) argCaptor { verify(viewTreeObserver).addOnWindowFocusChangeListener(capture()) } .forEach { it.onWindowFocusChanged(true) } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) } @Test fun repeatWhenWindowHasFocus_losesFocus_cancelsBlock() = testScope.runTest { whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.windowVisibility).thenReturn(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(true) var innerJob: Job? = null backgroundScope.launch { view.repeatWhenWindowHasFocus { innerJob = launch { awaitCancellation() } } } runCurrent() assertThat(innerJob?.isActive).isEqualTo(true) whenever(view.hasWindowFocus()).thenReturn(false) argCaptor { verify(viewTreeObserver).addOnWindowFocusChangeListener(capture()) } .forEach { it.onWindowFocusChanged(false) } runCurrent() assertThat(innerJob?.isActive).isNotEqualTo(true) } @Test fun repeatWhenAttached_becomesVisibleWithoutFocus_STARTED() = testScope.runTest { Loading @@ -180,7 +366,7 @@ class RepeatWhenAttachedTest : SysuiTestCase() { verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) whenever(view.windowVisibility).thenReturn(View.VISIBLE) listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) listenerCaptor.lastValue.onWindowVisibilityChanged(View.VISIBLE) runCurrent() assertThat(block.invocationCount).isEqualTo(1) Loading @@ -196,7 +382,7 @@ class RepeatWhenAttachedTest : SysuiTestCase() { verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) whenever(view.hasWindowFocus()).thenReturn(true) listenerCaptor.value.onWindowFocusChanged(true) listenerCaptor.lastValue.onWindowFocusChanged(true) runCurrent() assertThat(block.invocationCount).isEqualTo(1) Loading @@ -214,9 +400,9 @@ class RepeatWhenAttachedTest : SysuiTestCase() { verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) whenever(view.windowVisibility).thenReturn(View.VISIBLE) visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) visibleCaptor.lastValue.onWindowVisibilityChanged(View.VISIBLE) whenever(view.hasWindowFocus()).thenReturn(true) focusCaptor.value.onWindowFocusChanged(true) focusCaptor.lastValue.onWindowFocusChanged(true) runCurrent() assertThat(block.invocationCount).isEqualTo(1) Loading Loading @@ -314,6 +500,7 @@ class RepeatWhenAttachedTest : SysuiTestCase() { val invocations: List<Invocation> = _invocations val invocationCount: Int get() = _invocations.size val latestLifecycleState: Lifecycle.State get() = _invocations.last().lifecycleState Loading @@ -322,3 +509,9 @@ class RepeatWhenAttachedTest : SysuiTestCase() { } } } private inline fun <reified T : Any> argCaptor(block: KArgumentCaptor<T>.() -> Unit) = argumentCaptor<T>().apply { block() } private inline fun <reified T : Any> KArgumentCaptor<T>.forEach(block: (T) -> Unit): Unit = allValues.forEach(block)