diff options
2 files changed, 264 insertions, 0 deletions
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 000000000000..55c7ac9fb0cc --- /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 <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/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt new file mode 100644 index 000000000000..4f5c570ee812 --- /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<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()) + } +} |