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

Commit c47ad8df authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[kairos] fix mobile icon bg binding" into main

parents 63e969a3 9eaace52
Loading
Loading
Loading
Loading
+135 −159
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import com.android.systemui.kairos.BuildSpec
import com.android.systemui.kairos.ExperimentalKairosApi
import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.kairos.MutableState
import com.android.systemui.kairos.combine
import com.android.systemui.kairos.effect
import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
import com.android.systemui.lifecycle.repeatWhenWindowIsVisible
@@ -44,6 +45,8 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBased
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants
import com.android.systemui.util.lifecycle.kairos.repeatWhenAttachedToWindow
import com.android.systemui.util.lifecycle.kairos.repeatWhenWindowIsVisible
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
@@ -64,7 +67,6 @@ object MobileIconBinderKairos {
        val binding = ModernStatusBarViewBindingKairosImpl(kairosNetwork, initialVisibilityState)
        return binding to
            scope.launch {
                view.repeatWhenAttachedToWindow {
                kairosNetwork.activateSpec {
                    bind(
                        view = view,
@@ -75,7 +77,6 @@ object MobileIconBinderKairos {
                }
            }
    }
    }

    @ExperimentalKairosApi
    private class ModernStatusBarViewBindingKairosImpl(
@@ -140,12 +141,12 @@ object MobileIconBinderKairos {
        effect {
            view.isVisible = isVisible
            iconView.isVisible = true
            launch {
                view.repeatWhenAttachedToWindow {
        }

        repeatWhenAttachedToWindow(view) {
            // isVisible controls the visibility state of the outer group, and thus it needs
            // to run in the CREATED lifecycle so it can continue to watch while invisible
            // See (b/291031862) for details
                    kairosNetwork.activateSpec {
            viewModel.isVisible.observe { isVisible ->
                viewModel.verboseLogger?.logBinderReceivedVisibility(
                    view,
@@ -158,13 +159,19 @@ object MobileIconBinderKairos {
                view.requestLayout()
            }
        }
                }
            }
            launch {
                view.repeatWhenWindowIsVisible {

        repeatWhenWindowIsVisible(view) {
            logger.logCollectionStarted(view, viewModel)
            binding.isCollecting = true
                    kairosNetwork.activateSpec {
            effect {
                try {
                    awaitCancellation()
                } finally {
                    binding.isCollecting = false
                    logger.logCollectionStopped(view, viewModel)
                }
            }

            binding.visibility.observe { state ->
                ModernStatusBarViewVisibilityHelper.setVisibilityState(
                    state,
@@ -175,8 +182,7 @@ object MobileIconBinderKairos {
            }

            // Set the icon for the triangle
                        viewModel.icon.pairwise(initialPreviousValue = null).observe {
                            (oldIcon, newIcon) ->
            viewModel.icon.pairwise(initialPreviousValue = null).observe { (oldIcon, newIcon) ->
                val shouldRequestLayout =
                    when {
                        oldIcon == null -> true
@@ -223,23 +229,28 @@ object MobileIconBinderKairos {
                }
            }

                        // Set the network type background

                        viewModel.networkTypeIcon
                            .mapTransactionally { it to binding.iconTint.sample() }
                            .observe { (background, iconTintColors) ->
            // Set the network type background and tint
            viewModel.networkTypeBackground.observe { background ->
                networkTypeContainer.setBackgroundResource(background?.res ?: 0)
            }

            combine(viewModel.networkTypeBackground, binding.iconTint) { background, colors ->
                    Pair(background != null, colors)
                }
                .observe { (hasBackground, colors) ->
                    // Tint will invert when this bit changes
                                if (background?.res != null) {
                                    networkTypeContainer.backgroundTintList =
                                        ColorStateList.valueOf(iconTintColors.tint)
                                    networkTypeView.imageTintList =
                                        ColorStateList.valueOf(iconTintColors.contrast)
                    val tint = ColorStateList.valueOf(colors.tint)
                    if (hasBackground) {
                        networkTypeContainer.backgroundTintList = tint
                        networkTypeView.imageTintList = ColorStateList.valueOf(colors.contrast)
                    } else {
                                    networkTypeView.imageTintList =
                                        ColorStateList.valueOf(iconTintColors.tint)
                        networkTypeView.imageTintList = tint
                    }

                    roamingView.imageTintList = tint
                    activityIn.imageTintList = tint
                    activityOut.imageTintList = tint
                    dotView.setDecorColor(colors.tint)
                }

            // Set the roaming indicator
@@ -266,44 +277,9 @@ object MobileIconBinderKairos {
                viewModel.activityOutVisible.observe { activityOut.isVisible = it }
            }

                        viewModel.activityContainerVisible.observe {
                            activityContainer.isVisible = it
                        }

                        // Set the tint
                        binding.iconTint
                            .mapTransactionally { it to viewModel.networkTypeBackground.sample() }
                            .observe { (colors, networkTypeBackground) ->
                                val tint = ColorStateList.valueOf(colors.tint)
                                val contrast = ColorStateList.valueOf(colors.contrast)

                                iconView.imageTintList = tint

                                // If the bg is visible, tint it and use the contrast for the fg
                                if (networkTypeBackground != null) {
                                    networkTypeContainer.backgroundTintList = tint
                                    networkTypeView.imageTintList = contrast
                                } else {
                                    networkTypeView.imageTintList = tint
                                }

                                roamingView.imageTintList = tint
                                activityIn.imageTintList = tint
                                activityOut.imageTintList = tint
                                dotView.setDecorColor(colors.tint)
                            }
            viewModel.activityContainerVisible.observe { activityContainer.isVisible = it }

            binding.decorTint.observe { tint -> dotView.setDecorColor(tint) }
        }

                    try {
                        awaitCancellation()
                    } finally {
                        binding.isCollecting = false
                        logger.logCollectionStopped(view, viewModel)
                    }
                }
            }
        }
    }
}
+169 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.util.lifecycle.kairos

import android.view.View
import android.view.ViewTreeObserver
import com.android.systemui.coroutines.newTracingContext
import com.android.systemui.kairos.BuildScope
import com.android.systemui.kairos.ExperimentalKairosApi
import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.util.Assert
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart

/**
 * Runs the given [block] in a new [BuildScope] when `this` [View]'s [Window][android.view.Window]'s
 * [WindowLifecycleState] is at least at [state] (or immediately after calling this function if the
 * window is already at least at [state]), automatically canceling the work when the window is no
 * longer at least at that state.
 *
 * [block] may be run multiple times, running once per every time the [WindowLifecycleState] becomes
 * at least at [state].
 */
@ExperimentalKairosApi
fun BuildScope.repeatOnWindowLifecycle(
    view: View,
    state: WindowLifecycleState,
    block: BuildScope.() -> Unit,
) {
    when (state) {
        WindowLifecycleState.ATTACHED -> repeatWhenAttachedToWindow(view, block)
        WindowLifecycleState.VISIBLE -> repeatWhenWindowIsVisible(view, block)
        WindowLifecycleState.FOCUSED -> repeatWhenWindowHasFocus(view, block)
    }
}

/**
 * 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.
 *
 * [block] may be run multiple times, running once per every time the view is attached.
 */
@ExperimentalKairosApi
fun BuildScope.repeatWhenAttachedToWindow(view: View, block: BuildScope.() -> Unit) {
    view.isAttached.flowOn(MAIN_DISPATCHER_SINGLETON).toState(false).observeLatestBuild {
        if (it) {
            block()
        }
    }
}

/**
 * Runs the given [block] every time the [Window][android.view.Window] this [View] is attached to
 * becomes visible (or immediately after calling this function, if the window is already visible),
 * automatically canceling the work when the window becomes invisible.
 *
 * [block] may be run multiple times, running once per every time the window becomes visible.
 */
@ExperimentalKairosApi
fun BuildScope.repeatWhenWindowIsVisible(view: View, block: BuildScope.() -> Unit) {
    view.isWindowVisible.flowOn(MAIN_DISPATCHER_SINGLETON).toState(false).observeLatestBuild {
        if (it) {
            block()
        }
    }
}

/**
 * Runs the given [block] every time the [Window][android.view.Window] this [View] is attached to
 * has focus (or immediately after calling this function, if the window is already focused),
 * automatically canceling the work when the window loses focus.
 *
 * [block] may be run multiple times, running once per every time the window is focused.
 */
@ExperimentalKairosApi
fun BuildScope.repeatWhenWindowHasFocus(view: View, block: BuildScope.() -> Unit) {
    view.isWindowFocused.flowOn(MAIN_DISPATCHER_SINGLETON).toState(false).observeLatestBuild {
        if (it) {
            block()
        }
    }
}

private val View.isAttached
    get() = conflatedCallbackFlow {
        val onAttachListener =
            object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {
                    Assert.isMainThread()
                    trySend(true)
                }

                override fun onViewDetachedFromWindow(v: View) {
                    trySend(false)
                }
            }
        addOnAttachStateChangeListener(onAttachListener)
        trySend(isAttachedToWindow)
        awaitClose { removeOnAttachStateChangeListener(onAttachListener) }
    }

private val View.currentViewTreeObserver: Flow<ViewTreeObserver?>
    get() = isAttached.map { if (it) viewTreeObserver else null }

private val View.isWindowVisible
    get() =
        currentViewTreeObserver
            .flatMapLatestConflated { vto ->
                vto?.isWindowVisible?.onStart { emit(windowVisibility == View.VISIBLE) }
                    ?: emptyFlow()
            }
            .flowOn(MAIN_DISPATCHER_SINGLETON)

private val View.isWindowFocused
    get() =
        currentViewTreeObserver
            .flatMapLatestConflated { vto ->
                vto?.isWindowFocused?.onStart { emit(hasWindowFocus()) } ?: emptyFlow()
            }
            .flowOn(MAIN_DISPATCHER_SINGLETON)

private val ViewTreeObserver.isWindowFocused
    get() = conflatedCallbackFlow {
        val listener = ViewTreeObserver.OnWindowFocusChangeListener { trySend(it) }
        addOnWindowFocusChangeListener(listener)
        awaitClose { removeOnWindowFocusChangeListener(listener) }
    }

private val ViewTreeObserver.isWindowVisible
    get() = conflatedCallbackFlow {
        val listener =
            ViewTreeObserver.OnWindowVisibilityChangeListener { v -> trySend(v == View.VISIBLE) }
        addOnWindowVisibilityChangeListener(listener)
        awaitClose { removeOnWindowVisibilityChangeListener(listener) }
    }

/**
 * Cache dispatcher in a top-level property so that we do not unnecessarily create new
 * `CoroutineContext` objects for tracing on each call to `repeatWhen-`. It is okay to reuse a
 * single instance of the tracing context because it is copied for its children.
 *
 * Also, ideally, we would use the injected `@Main CoroutineDispatcher`, but `repeatWhen-` functions
 * are extension functions, and plumbing dagger-injected instances for static usage has little
 * benefit.
 */
private val MAIN_DISPATCHER_SINGLETON =
    Dispatchers.Main.immediate + newTracingContext("RepeatWhenAttachedKairos")
+11 −25
Original line number Diff line number Diff line
@@ -638,13 +638,8 @@ interface BuildScope : HasNetwork, StateScope {
     *
     * With each invocation of [block], running effects from the previous invocation are cancelled.
     */
    fun <A> Events<A>.observeLatest(block: suspend EffectScope.(A) -> Unit): DisposableHandle {
        var innerJob: Job? = null
        return observeBuild {
            innerJob?.cancel()
            innerJob = effect { block(it) }
        }
    }
    fun <A> Events<A>.observeLatest(block: suspend EffectScope.(A) -> Unit): DisposableHandle =
        mapLatestBuild { effect { block(it) } }.observeSync()

    /**
     * Invokes [block] with the value held by this [State], allowing side-effects to be safely
@@ -652,13 +647,9 @@ interface BuildScope : HasNetwork, StateScope {
     *
     * With each invocation of [block], running effects from the previous invocation are cancelled.
     */
    fun <A> State<A>.observeLatest(block: TransactionEffectScope.(A) -> Unit): Job = launchScope {
        var innerJob = effectSync { block(sample()) }
        changes.observeBuild {
            innerJob.cancel()
            innerJob = effectSync { block(it) }
        }
    }
    fun <A> State<A>.observeLatestSync(
        block: TransactionEffectScope.(A) -> Unit
    ): DisposableHandle = mapLatestBuild { effectSync { block(it) } }.observeSync()

    /**
     * Applies [block] to the value held by this [State]. [block] receives an [BuildScope] that can
@@ -669,13 +660,8 @@ interface BuildScope : HasNetwork, StateScope {
     * each invocation of [block], changes from the previous invocation are undone (any registered
     * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled).
     */
    fun <A> State<A>.observeLatestBuild(block: BuildScope.(A) -> Unit): Job = launchScope {
        var innerJob: Job = launchScope { block(sample()) }
        changes.observeBuild {
            innerJob.cancel()
            innerJob = launchScope { block(it) }
        }
    }
    fun <A> State<A>.observeLatestBuild(block: BuildScope.(A) -> Unit): DisposableHandle =
        mapLatestBuild(block).observeSync()

    /** Applies the [BuildSpec] within this [BuildScope]. */
    fun <A> BuildSpec<A>.applySpec(): A = this()
@@ -691,15 +677,15 @@ interface BuildScope : HasNetwork, StateScope {
     * [effect].
     *
     * ```
     *     fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit): Job = launchScope {
     *     fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit): DisposableHandle {
     *         block(sample())
     *         changes.observeBuild(block)
     *         return changes.observeBuild(block)
     *     }
     * ```
     */
    fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit): Job = launchScope {
    fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit): DisposableHandle {
        block(sample())
        changes.observeBuild(block)
        return changes.observeBuild(block)
    }

    /**