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

Commit 9bfd66eb authored by Alejandro Nijamkin's avatar Alejandro Nijamkin Committed by Automerger Merge Worker
Browse files

Merge "repeatWhenAttached" into tm-qpr-dev am: 768d1483 am: b9cf8d6a

parents e6add331 b9cf8d6a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -226,6 +226,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