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

Commit c738a8cc authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

repeatWhenAttached

This new View extension function replaces WindowAddedViewLifecycleOwner and is
intended for use with views that are not part of an activity.

It is more correct because it properly disposes itself and stops
all previously launched coroutines/jobs when the view is detached from
its view hierarchy.

Test: Extensive unit tests included. Also tested manually making sure that there are no
crashes and that jobs scheduled by a view-binder are properly cleaned up
when the view is detached and replaced by a different view when changing
device configuration using:

$ adb shell wm density 1000

Bug: 235403546
Change-Id: Ied36c9e1735333c482fc82cfbe28e665083795ae
parent f0734a24
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -225,6 +225,7 @@ android_library {
        "androidx.exifinterface_exifinterface",
        "kotlinx-coroutines-android",
        "kotlinx-coroutines-core",
        "kotlinx_coroutines_test",
        "iconloader_base",
        "SystemUI-tags",
        "SystemUI-proto",
+183 −0
Original line number Diff line number Diff line
/*
 *  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
            }
    }
}
+0 −114
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
        }
    }
}
+0 −12
Original line number Diff line number Diff line
@@ -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();

+319 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading