1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.lifecycle 19 20 import android.view.View 21 import android.view.ViewTreeObserver 22 import androidx.annotation.MainThread 23 import androidx.lifecycle.Lifecycle 24 import androidx.lifecycle.LifecycleOwner 25 import androidx.lifecycle.LifecycleRegistry 26 import androidx.lifecycle.lifecycleScope 27 import com.android.systemui.util.Assert 28 import kotlin.coroutines.CoroutineContext 29 import kotlin.coroutines.EmptyCoroutineContext 30 import kotlinx.coroutines.Dispatchers 31 import kotlinx.coroutines.DisposableHandle 32 import kotlinx.coroutines.launch 33 34 /** 35 * Runs the given [block] every time the [View] becomes attached (or immediately after calling this 36 * function, if the view was already attached), automatically canceling the work when the `View` 37 * becomes detached. 38 * 39 * Only use from the main thread. 40 * 41 * When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use 42 * to launch jobs, with confidence that the jobs will be properly canceled when the view is 43 * detached. 44 * 45 * The [block] may be run multiple times, running once per every time the view is attached. Each 46 * time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a 47 * fresh one. 48 * 49 * @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is 50 * invoked on. 51 * @param block The block of code that should be run when the view becomes attached. It can end up 52 * being invoked multiple times if the view is reattached after being detached. 53 * @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is 54 * no longer interested in the [block] being run the next time its attached. Calling this is an 55 * optional optimization as the logic will be properly cleaned up and destroyed each time the view 56 * is detached. Using this is not *thread-safe* and should only be used on the main thread. 57 */ 58 @MainThread 59 fun View.repeatWhenAttached( 60 coroutineContext: CoroutineContext = EmptyCoroutineContext, 61 block: suspend LifecycleOwner.(View) -> Unit, 62 ): DisposableHandle { 63 Assert.isMainThread() 64 val view = this 65 // The suspend block will run on the app's main thread unless the caller supplies a different 66 // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as 67 // default behavior. Instead, we want it to run on the view's UI thread since the user will 68 // presumably want to call view methods that require being called from said UI thread. 69 val lifecycleCoroutineContext = Dispatchers.Main + coroutineContext 70 var lifecycleOwner: ViewLifecycleOwner? = null 71 val onAttachListener = 72 object : View.OnAttachStateChangeListener { 73 override fun onViewAttachedToWindow(v: View) { 74 Assert.isMainThread() 75 lifecycleOwner?.onDestroy() 76 lifecycleOwner = 77 createLifecycleOwnerAndRun( 78 view, 79 lifecycleCoroutineContext, 80 block, 81 ) 82 } 83 84 override fun onViewDetachedFromWindow(v: View) { 85 lifecycleOwner?.onDestroy() 86 lifecycleOwner = null 87 } 88 } 89 90 addOnAttachStateChangeListener(onAttachListener) 91 if (view.isAttachedToWindow) { 92 lifecycleOwner = 93 createLifecycleOwnerAndRun( 94 view, 95 lifecycleCoroutineContext, 96 block, 97 ) 98 } 99 100 return object : DisposableHandle { 101 override fun dispose() { 102 Assert.isMainThread() 103 104 lifecycleOwner?.onDestroy() 105 lifecycleOwner = null 106 view.removeOnAttachStateChangeListener(onAttachListener) 107 } 108 } 109 } 110 111 private fun createLifecycleOwnerAndRun( 112 view: View, 113 coroutineContext: CoroutineContext, 114 block: suspend LifecycleOwner.(View) -> Unit, 115 ): ViewLifecycleOwner { 116 return ViewLifecycleOwner(view).apply { 117 onCreate() 118 lifecycleScope.launch(coroutineContext) { block(view) } 119 } 120 } 121 122 /** 123 * A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function. 124 * 125 * The implementation requires the caller to call [onCreate] and [onDestroy] when the view is 126 * attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called, 127 * the implementation monitors window state in the following way 128 * * If the window is not visible, we are in the [Lifecycle.State.CREATED] state 129 * * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state 130 * * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state 131 * 132 * Or in table format: 133 * ``` 134 * ┌───────────────┬───────────────────┬──────────────┬─────────────────┐ 135 * │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │ 136 * ├───────────────┼───────────────────┴──────────────┼─────────────────┤ 137 * │ Not attached │ Any │ N/A │ 138 * ├───────────────┼───────────────────┬──────────────┼─────────────────┤ 139 * │ │ Not visible │ Any │ CREATED │ 140 * │ ├───────────────────┼──────────────┼─────────────────┤ 141 * │ Attached │ │ No focus │ STARTED │ 142 * │ │ Visible ├──────────────┼─────────────────┤ 143 * │ │ │ Has focus │ RESUMED │ 144 * └───────────────┴───────────────────┴──────────────┴─────────────────┘ 145 * ``` 146 */ 147 class ViewLifecycleOwner( 148 private val view: View, 149 ) : LifecycleOwner { 150 151 private val windowVisibleListener = 152 ViewTreeObserver.OnWindowVisibilityChangeListener { updateState() } 153 private val windowFocusListener = ViewTreeObserver.OnWindowFocusChangeListener { updateState() } 154 155 private val registry = LifecycleRegistry(this) 156 157 fun onCreate() { 158 registry.currentState = Lifecycle.State.CREATED 159 view.viewTreeObserver.addOnWindowVisibilityChangeListener(windowVisibleListener) 160 view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusListener) 161 updateState() 162 } 163 164 fun onDestroy() { 165 view.viewTreeObserver.removeOnWindowVisibilityChangeListener(windowVisibleListener) 166 view.viewTreeObserver.removeOnWindowFocusChangeListener(windowFocusListener) 167 registry.currentState = Lifecycle.State.DESTROYED 168 } 169 170 override val lifecycle: Lifecycle 171 get() { 172 return registry 173 } 174 175 private fun updateState() { 176 registry.currentState = 177 when { 178 view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED 179 !view.hasWindowFocus() -> Lifecycle.State.STARTED 180 else -> Lifecycle.State.RESUMED 181 } 182 } 183 } 184