Merge "Window-added View LifecycleOwner" into tm-qpr-dev am: 0c97e28823

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19264427

Change-Id: I5b376ae2efe03eac2c706d8e13b38c4648ffcdb0
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Alejandro Nijamkin
2022-07-14 19:54:04 +00:00
committed by Automerger Merge Worker
2 changed files with 264 additions and 0 deletions

View File

@@ -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
}
}
}

View File

@@ -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())
}
}