Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0c97e288 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "Window-added View LifecycleOwner" into tm-qpr-dev

parents dbdc3922 21efc103
Loading
Loading
Loading
Loading
+114 −0
Original line number Diff line number Diff line
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
        }
    }
}
+150 −0
Original line number Diff line number Diff line
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())
    }
}