summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/Android.bp1
-rw-r--r--packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt183
-rw-r--r--packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt114
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt319
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt150
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java21
7 files changed, 503 insertions, 297 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index ffd6b522e394..1344144ee6fc 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -225,6 +225,7 @@ android_library {
"androidx.exifinterface_exifinterface",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
+ "kotlinx_coroutines_test",
"iconloader_base",
"SystemUI-tags",
"SystemUI-proto",
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
new file mode 100644
index 000000000000..e3649187b0a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.lifecycle
+
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.annotation.MainThread
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.util.Assert
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/**
+ * 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.
+ *
+ * When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use
+ * to launch jobs, with confidence that the jobs will be properly canceled when the view is
+ * detached.
+ *
+ * The [block] may be run multiple times, running once per every time the view is attached. Each
+ * time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a
+ * fresh one.
+ *
+ * @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is
+ * invoked on.
+ * @param block The block of code that should be run when the view becomes attached. It can end up
+ * being invoked multiple times if the view is reattached after being detached.
+ * @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is
+ * no longer interested in the [block] being run the next time its attached. Calling this is an
+ * optional optimization as the logic will be properly cleaned up and destroyed each time the view
+ * is detached. Using this is not *thread-safe* and should only be used on the main thread.
+ */
+@MainThread
+fun View.repeatWhenAttached(
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ block: suspend LifecycleOwner.(View) -> Unit,
+): DisposableHandle {
+ Assert.isMainThread()
+ val view = this
+ // The suspend block will run on the app's main thread unless the caller supplies a different
+ // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as
+ // default behavior. Instead, we want it to run on the view's UI thread since the user will
+ // presumably want to call view methods that require being called from said UI thread.
+ val lifecycleCoroutineContext = Dispatchers.Main + coroutineContext
+ var lifecycleOwner: ViewLifecycleOwner? = null
+ val onAttachListener =
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View?) {
+ Assert.isMainThread()
+ lifecycleOwner?.onDestroy()
+ lifecycleOwner =
+ createLifecycleOwnerAndRun(
+ view,
+ lifecycleCoroutineContext,
+ block,
+ )
+ }
+
+ override fun onViewDetachedFromWindow(v: View?) {
+ lifecycleOwner?.onDestroy()
+ lifecycleOwner = null
+ }
+ }
+
+ addOnAttachStateChangeListener(onAttachListener)
+ if (view.isAttachedToWindow) {
+ lifecycleOwner =
+ createLifecycleOwnerAndRun(
+ view,
+ lifecycleCoroutineContext,
+ block,
+ )
+ }
+
+ return object : DisposableHandle {
+ override fun dispose() {
+ Assert.isMainThread()
+
+ lifecycleOwner?.onDestroy()
+ lifecycleOwner = null
+ view.removeOnAttachStateChangeListener(onAttachListener)
+ }
+ }
+}
+
+private fun createLifecycleOwnerAndRun(
+ view: View,
+ coroutineContext: CoroutineContext,
+ block: suspend LifecycleOwner.(View) -> Unit,
+): ViewLifecycleOwner {
+ return ViewLifecycleOwner(view).apply {
+ onCreate()
+ lifecycleScope.launch(coroutineContext) { block(view) }
+ }
+}
+
+/**
+ * A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function.
+ *
+ * The implementation requires the caller to call [onCreate] and [onDestroy] when the view is
+ * attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called,
+ * the implementation monitors window state in the following way
+ *
+ * * If the window is not visible, we are in the [Lifecycle.State.CREATED] state
+ * * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state
+ * * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state
+ *
+ * Or in table format:
+ * ```
+ * ┌───────────────┬───────────────────┬──────────────┬─────────────────┐
+ * │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │
+ * ├───────────────┼───────────────────┴──────────────┼─────────────────┤
+ * │ Not attached │ Any │ N/A │
+ * ├───────────────┼───────────────────┬──────────────┼─────────────────┤
+ * │ │ Not visible │ Any │ CREATED │
+ * │ ├───────────────────┼──────────────┼─────────────────┤
+ * │ Attached │ │ No focus │ STARTED │
+ * │ │ Visible ├──────────────┼─────────────────┤
+ * │ │ │ Has focus │ RESUMED │
+ * └───────────────┴───────────────────┴──────────────┴─────────────────┘
+ * ```
+ */
+private class ViewLifecycleOwner(
+ private val view: View,
+) : LifecycleOwner {
+
+ private val windowVisibleListener =
+ ViewTreeObserver.OnWindowVisibilityChangeListener { updateState() }
+ private val windowFocusListener = ViewTreeObserver.OnWindowFocusChangeListener { updateState() }
+
+ private val registry = LifecycleRegistry(this)
+
+ fun onCreate() {
+ registry.currentState = Lifecycle.State.CREATED
+ view.viewTreeObserver.addOnWindowVisibilityChangeListener(windowVisibleListener)
+ view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusListener)
+ updateState()
+ }
+
+ fun onDestroy() {
+ view.viewTreeObserver.removeOnWindowVisibilityChangeListener(windowVisibleListener)
+ view.viewTreeObserver.removeOnWindowFocusChangeListener(windowFocusListener)
+ registry.currentState = Lifecycle.State.DESTROYED
+ }
+
+ override fun getLifecycle(): Lifecycle {
+ return registry
+ }
+
+ private fun updateState() {
+ registry.currentState =
+ when {
+ view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED
+ !view.hasWindowFocus() -> Lifecycle.State.STARTED
+ else -> Lifecycle.State.RESUMED
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt
deleted file mode 100644
index 55c7ac9fb0cc..000000000000
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.android.systemui.lifecycle
-
-import android.view.View
-import android.view.ViewTreeObserver
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-
-/**
- * [LifecycleOwner] for Window-added Views.
- *
- * These are [View] instances that are added to a `Window` using the `WindowManager` API.
- *
- * This implementation goes to:
- * * The <b>CREATED</b> `Lifecycle.State` when the view gets attached to the window but the window
- * is not yet visible
- * * The <b>STARTED</b> `Lifecycle.State` when the view is attached to the window and the window is
- * visible
- * * The <b>RESUMED</b> `Lifecycle.State` when the view is attached to the window and the window is
- * visible and the window receives focus
- *
- * In table format:
- * ```
- * | ----------------------------------------------------------------------------- |
- * | View attached to window | Window visible | Window has focus | Lifecycle state |
- * | ----------------------------------------------------------------------------- |
- * | not attached | Any | INITIALIZED |
- * | ----------------------------------------------------------------------------- |
- * | | not visible | Any | CREATED |
- * | ----------------------------------------------------- |
- * | attached | | not focused | STARTED |
- * | | is visible |----------------------------------- |
- * | | | has focus | RESUMED |
- * | ----------------------------------------------------------------------------- |
- * ```
- * ### Notes
- * * [dispose] must be invoked when the [LifecycleOwner] is done and won't be reused
- * * It is always better for [LifecycleOwner] implementations to be more explicit than just
- * listening to the state of the `Window`. E.g. if the code that added the `View` to the `Window`
- * already has access to the correct state to know when that `View` should become visible and when
- * it is ready to receive interaction from the user then it already knows when to move to `STARTED`
- * and `RESUMED`, respectively. In that case, it's better to implement your own `LifecycleOwner`
- * instead of relying on the `Window` callbacks.
- */
-class WindowAddedViewLifecycleOwner
-@JvmOverloads
-constructor(
- private val view: View,
- registryFactory: (LifecycleOwner) -> LifecycleRegistry = { LifecycleRegistry(it) },
-) : LifecycleOwner {
-
- private val windowAttachListener =
- object : ViewTreeObserver.OnWindowAttachListener {
- override fun onWindowAttached() {
- updateCurrentState()
- }
-
- override fun onWindowDetached() {
- updateCurrentState()
- }
- }
- private val windowFocusListener =
- ViewTreeObserver.OnWindowFocusChangeListener { updateCurrentState() }
- private val windowVisibilityListener =
- ViewTreeObserver.OnWindowVisibilityChangeListener { updateCurrentState() }
-
- private val registry = registryFactory(this)
-
- init {
- setCurrentState(Lifecycle.State.INITIALIZED)
-
- with(view.viewTreeObserver) {
- addOnWindowAttachListener(windowAttachListener)
- addOnWindowVisibilityChangeListener(windowVisibilityListener)
- addOnWindowFocusChangeListener(windowFocusListener)
- }
-
- updateCurrentState()
- }
-
- override fun getLifecycle(): Lifecycle {
- return registry
- }
-
- /**
- * Disposes of this [LifecycleOwner], performing proper clean-up.
- *
- * <p>Invoke this when the instance is finished and won't be reused.
- */
- fun dispose() {
- with(view.viewTreeObserver) {
- removeOnWindowAttachListener(windowAttachListener)
- removeOnWindowVisibilityChangeListener(windowVisibilityListener)
- removeOnWindowFocusChangeListener(windowFocusListener)
- }
- }
-
- private fun updateCurrentState() {
- val state =
- when {
- !view.isAttachedToWindow -> Lifecycle.State.INITIALIZED
- view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED
- !view.hasWindowFocus() -> Lifecycle.State.STARTED
- else -> Lifecycle.State.RESUMED
- }
- setCurrentState(state)
- }
-
- private fun setCurrentState(state: Lifecycle.State) {
- if (registry.currentState != state) {
- registry.currentState = state
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4b71b2c94f5f..15e1129e43b5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -44,8 +44,6 @@ import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.WindowManagerGlobal;
-import androidx.lifecycle.ViewTreeLifecycleOwner;
-
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
@@ -54,7 +52,6 @@ import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.lifecycle.WindowAddedViewLifecycleOwner;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -251,15 +248,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
mWindowManager.addView(mNotificationShadeView, mLp);
- // Set up and "inject" a LifecycleOwner bound to the Window-View relationship such that all
- // views in the sub-tree rooted under this view can access the LifecycleOwner using
- // ViewTreeLifecycleOwner.get(...).
- if (ViewTreeLifecycleOwner.get(mNotificationShadeView) == null) {
- ViewTreeLifecycleOwner.set(
- mNotificationShadeView,
- new WindowAddedViewLifecycleOwner(mNotificationShadeView));
- }
-
mLpChanged.copyFrom(mLp);
onThemeChanged();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
new file mode 100644
index 000000000000..80f3e46b848f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.lifecycle
+
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
+import androidx.lifecycle.Lifecycle
+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.DisposableHandle
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(JUnit4::class)
+@RunWithLooper
+class RepeatWhenAttachedTest : SysuiTestCase() {
+
+ @JvmField @Rule val mockito = MockitoJUnit.rule()
+ @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule()
+
+ @Mock private lateinit var view: View
+ @Mock private lateinit var viewTreeObserver: ViewTreeObserver
+
+ private lateinit var block: Block
+ private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener>
+
+ @Before
+ fun setUp() {
+ Assert.setTestThread(Thread.currentThread())
+ whenever(view.viewTreeObserver).thenReturn(viewTreeObserver)
+ whenever(view.windowVisibility).thenReturn(View.GONE)
+ whenever(view.hasWindowFocus()).thenReturn(false)
+ attachListeners = mutableListOf()
+ whenever(view.addOnAttachStateChangeListener(any())).then {
+ attachListeners.add(it.arguments[0] as View.OnAttachStateChangeListener)
+ }
+ whenever(view.removeOnAttachStateChangeListener(any())).then {
+ attachListeners.remove(it.arguments[0] as View.OnAttachStateChangeListener)
+ }
+ block = Block()
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `repeatWhenAttached - enforces main thread`() = runBlockingTest {
+ Assert.setTestThread(null)
+
+ repeatWhenAttached()
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `repeatWhenAttached - dispose enforces main thread`() = runBlockingTest {
+ val disposableHandle = repeatWhenAttached()
+ Assert.setTestThread(null)
+
+ disposableHandle.dispose()
+ }
+
+ @Test
+ fun `repeatWhenAttached - view starts detached - runs block when attached`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(false)
+ repeatWhenAttached()
+ assertThat(block.invocationCount).isEqualTo(0)
+
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ attachListeners.last().onViewAttachedToWindow(view)
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - view already attached - immediately runs block`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+
+ repeatWhenAttached()
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - starts visible without focus - STARTED`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ whenever(view.windowVisibility).thenReturn(View.VISIBLE)
+
+ repeatWhenAttached()
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ whenever(view.hasWindowFocus()).thenReturn(true)
+
+ repeatWhenAttached()
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ whenever(view.windowVisibility).thenReturn(View.VISIBLE)
+ whenever(view.hasWindowFocus()).thenReturn(true)
+
+ repeatWhenAttached()
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - becomes visible without focus - STARTED`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ repeatWhenAttached()
+ val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
+ verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture())
+
+ whenever(view.windowVisibility).thenReturn(View.VISIBLE)
+ listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - gains focus but invisible - CREATED`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ repeatWhenAttached()
+ val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
+ verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture())
+
+ whenever(view.hasWindowFocus()).thenReturn(true)
+ listenerCaptor.value.onWindowFocusChanged(true)
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ repeatWhenAttached()
+ val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
+ verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture())
+ val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
+ verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture())
+
+ whenever(view.windowVisibility).thenReturn(View.VISIBLE)
+ visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
+ whenever(view.hasWindowFocus()).thenReturn(true)
+ focusCaptor.value.onWindowFocusChanged(true)
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ repeatWhenAttached()
+
+ whenever(view.isAttachedToWindow).thenReturn(false)
+ attachListeners.last().onViewDetachedFromWindow(view)
+
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ repeatWhenAttached()
+ whenever(view.isAttachedToWindow).thenReturn(false)
+ attachListeners.last().onViewDetachedFromWindow(view)
+
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ attachListeners.last().onViewAttachedToWindow(view)
+
+ assertThat(block.invocationCount).isEqualTo(2)
+ assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
+ assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - dispose attached`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ val handle = repeatWhenAttached()
+
+ handle.dispose()
+
+ assertThat(attachListeners).isEmpty()
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ @Test
+ fun `repeatWhenAttached - dispose never attached`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(false)
+ val handle = repeatWhenAttached()
+
+ handle.dispose()
+
+ assertThat(attachListeners).isEmpty()
+ assertThat(block.invocationCount).isEqualTo(0)
+ }
+
+ @Test
+ fun `repeatWhenAttached - dispose previously attached now detached`() = runBlockingTest {
+ whenever(view.isAttachedToWindow).thenReturn(true)
+ val handle = repeatWhenAttached()
+ attachListeners.last().onViewDetachedFromWindow(view)
+
+ handle.dispose()
+
+ assertThat(attachListeners).isEmpty()
+ assertThat(block.invocationCount).isEqualTo(1)
+ assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ private fun CoroutineScope.repeatWhenAttached(): DisposableHandle {
+ return view.repeatWhenAttached(
+ coroutineContext = coroutineContext,
+ block = block,
+ )
+ }
+
+ private class Block : suspend LifecycleOwner.(View) -> Unit {
+ data class Invocation(
+ val lifecycleOwner: LifecycleOwner,
+ ) {
+ val lifecycleState: Lifecycle.State
+ get() = lifecycleOwner.lifecycle.currentState
+ }
+
+ private val _invocations = mutableListOf<Invocation>()
+ val invocations: List<Invocation> = _invocations
+ val invocationCount: Int
+ get() = _invocations.size
+ val latestLifecycleState: Lifecycle.State
+ get() = _invocations.last().lifecycleState
+
+ override suspend fun invoke(lifecycleOwner: LifecycleOwner, view: View) {
+ _invocations.add(Invocation(lifecycleOwner))
+ }
+ }
+
+ /**
+ * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert
+ * in LifecycleRegistry.
+ */
+ class InstantTaskExecutorRule : TestWatcher() {
+ // TODO(b/240620122): This is a copy of
+ // androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be replaced
+ // with a dependency on the real library once b/ is cleared.
+ override fun starting(description: Description) {
+ super.starting(description)
+ ArchTaskExecutor.getInstance()
+ .setDelegate(
+ object : TaskExecutor() {
+ override fun executeOnDiskIO(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun postToMainThread(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun isMainThread(): Boolean {
+ return true
+ }
+ }
+ )
+ }
+
+ override fun finished(description: Description) {
+ super.finished(description)
+ ArchTaskExecutor.getInstance().setDelegate(null)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt
deleted file mode 100644
index 4f5c570ee812..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package com.android.systemui.lifecycle
-
-import android.view.View
-import android.view.ViewTreeObserver
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleRegistry
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class WindowAddedViewLifecycleOwnerTest : SysuiTestCase() {
-
- @Mock lateinit var view: View
- @Mock lateinit var viewTreeObserver: ViewTreeObserver
-
- private lateinit var underTest: WindowAddedViewLifecycleOwner
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(view.viewTreeObserver).thenReturn(viewTreeObserver)
- whenever(view.isAttachedToWindow).thenReturn(false)
- whenever(view.windowVisibility).thenReturn(View.INVISIBLE)
- whenever(view.hasWindowFocus()).thenReturn(false)
-
- underTest = WindowAddedViewLifecycleOwner(view) { LifecycleRegistry.createUnsafe(it) }
- }
-
- @Test
- fun `detached - invisible - does not have focus -- INITIALIZED`() {
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
- }
-
- @Test
- fun `detached - invisible - has focus -- INITIALIZED`() {
- whenever(view.hasWindowFocus()).thenReturn(true)
- val captor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
- verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(captor))
- captor.value.onWindowFocusChanged(true)
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
- }
-
- @Test
- fun `detached - visible - does not have focus -- INITIALIZED`() {
- whenever(view.windowVisibility).thenReturn(View.VISIBLE)
- val captor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
- verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(captor))
- captor.value.onWindowVisibilityChanged(View.VISIBLE)
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
- }
-
- @Test
- fun `detached - visible - has focus -- INITIALIZED`() {
- whenever(view.hasWindowFocus()).thenReturn(true)
- val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
- verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(focusCaptor))
- focusCaptor.value.onWindowFocusChanged(true)
-
- whenever(view.windowVisibility).thenReturn(View.VISIBLE)
- val visibilityCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
- verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(visibilityCaptor))
- visibilityCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
- }
-
- @Test
- fun `attached - invisible - does not have focus -- CREATED`() {
- whenever(view.isAttachedToWindow).thenReturn(true)
- val captor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>()
- verify(viewTreeObserver).addOnWindowAttachListener(capture(captor))
- captor.value.onWindowAttached()
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
- }
-
- @Test
- fun `attached - invisible - has focus -- CREATED`() {
- whenever(view.isAttachedToWindow).thenReturn(true)
- val attachCaptor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>()
- verify(viewTreeObserver).addOnWindowAttachListener(capture(attachCaptor))
- attachCaptor.value.onWindowAttached()
-
- whenever(view.hasWindowFocus()).thenReturn(true)
- val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
- verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(focusCaptor))
- focusCaptor.value.onWindowFocusChanged(true)
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
- }
-
- @Test
- fun `attached - visible - does not have focus -- STARTED`() {
- whenever(view.isAttachedToWindow).thenReturn(true)
- val attachCaptor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>()
- verify(viewTreeObserver).addOnWindowAttachListener(capture(attachCaptor))
- attachCaptor.value.onWindowAttached()
-
- whenever(view.windowVisibility).thenReturn(View.VISIBLE)
- val visibilityCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
- verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(visibilityCaptor))
- visibilityCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
- }
-
- @Test
- fun `attached - visible - has focus -- RESUMED`() {
- whenever(view.isAttachedToWindow).thenReturn(true)
- val attachCaptor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>()
- verify(viewTreeObserver).addOnWindowAttachListener(capture(attachCaptor))
- attachCaptor.value.onWindowAttached()
-
- whenever(view.hasWindowFocus()).thenReturn(true)
- val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>()
- verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(focusCaptor))
- focusCaptor.value.onWindowFocusChanged(true)
-
- whenever(view.windowVisibility).thenReturn(View.VISIBLE)
- val visibilityCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>()
- verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(visibilityCaptor))
- visibilityCaptor.value.onWindowVisibilityChanged(View.VISIBLE)
-
- assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
- }
-
- @Test
- fun dispose() {
- underTest.dispose()
-
- verify(viewTreeObserver).removeOnWindowAttachListener(any())
- verify(viewTreeObserver).removeOnWindowVisibilityChangeListener(any())
- verify(viewTreeObserver).removeOnWindowFocusChangeListener(any())
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index ec1fa48662b4..ad3d3d2958cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -28,7 +28,6 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -44,8 +43,6 @@ import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.view.WindowManager;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.ViewTreeLifecycleOwner;
import androidx.test.filters.SmallTest;
import com.android.internal.colorextraction.ColorExtractor;
@@ -188,24 +185,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
}
@Test
- public void attach_setsUpLifecycleOwner() {
- mNotificationShadeWindowController.attach();
-
- assertThat(ViewTreeLifecycleOwner.get(mNotificationShadeWindowView)).isNotNull();
- }
-
- @Test
- public void attach_doesNotSetUpLifecycleOwnerIfAlreadySet() {
- final LifecycleOwner previouslySet = mock(LifecycleOwner.class);
- ViewTreeLifecycleOwner.set(mNotificationShadeWindowView, previouslySet);
-
- mNotificationShadeWindowController.attach();
-
- assertThat(ViewTreeLifecycleOwner.get(mNotificationShadeWindowView))
- .isEqualTo(previouslySet);
- }
-
- @Test
public void setScrimsVisibility_earlyReturn() {
clearInvocations(mWindowManager);
mNotificationShadeWindowController.setScrimsVisibility(ScrimController.TRANSPARENT);