Merge "repeatWhenAttached" into tm-qpr-dev am: 768d148394

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

Change-Id: I9d8099324b8df033f49eb4c4d3933e9ef8ae3964
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Alejandro Nijamkin
2022-07-31 21:30:12 +00:00
committed by Automerger Merge Worker
7 changed files with 503 additions and 297 deletions

View File

@@ -225,6 +225,7 @@ android_library {
"androidx.exifinterface_exifinterface",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
"kotlinx_coroutines_test",
"iconloader_base",
"SystemUI-tags",
"SystemUI-proto",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -187,24 +184,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
verify(mWindowManager, never()).updateViewLayout(any(), mLayoutParameters.capture());
}
@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);