diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt new file mode 100644 index 0000000000000..55c7ac9fb0cc7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt @@ -0,0 +1,114 @@ +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 CREATED `Lifecycle.State` when the view gets attached to the window but the window + * is not yet visible + * * The STARTED `Lifecycle.State` when the view is attached to the window and the window is + * visible + * * The RESUMED `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. + * + *
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/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt
new file mode 100644
index 0000000000000..4f5c570ee812b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt
@@ -0,0 +1,150 @@
+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