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

Commit 80809d80 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov Committed by Android (Google) Code Review
Browse files

Merge "[SB][RONs] Support multiple ongoing activity chips." into main

parents aed83e9b 7f125f08
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -102,7 +102,9 @@
                    <include layout="@layout/ongoing_activity_chip"
                        android:id="@+id/ongoing_activity_chip_primary"/>

                    <!-- TODO(b/364653005): Add a second activity chip. -->
                    <include layout="@layout/ongoing_activity_chip"
                        android:id="@+id/ongoing_activity_chip_secondary"
                        android:visibility="gone"/>

                    <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
                        android:id="@+id/notification_icon_area"
+61 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.statusbar.chips.shared

import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils

/** Helper for reading or using the status bar ron chips flag state. */
@Suppress("NOTHING_TO_INLINE")
object StatusBarRonChips {
    /** The aconfig flag name */
    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS

    /** A token used for dependency declaration */
    val token: FlagToken
        get() = FlagToken(FLAG_NAME, isEnabled)

    /** Is the refactor enabled */
    @JvmStatic
    inline val isEnabled
        get() = Flags.statusBarRonChips()

    /**
     * Called to ensure code is only run when the flag is enabled. This protects users from the
     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
     * build to ensure that the refactor author catches issues in testing.
     */
    @JvmStatic
    inline fun isUnexpectedlyInLegacyMode() =
        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)

    /**
     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
     * the flag is not enabled to ensure that the refactor author catches issues in testing.
     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
     */
    @JvmStatic
    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)

    /**
     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
     * the flag is enabled to ensure that the refactor author catches issues in testing.
     */
    @JvmStatic
    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
}
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.statusbar.chips.ui.model

/** Models multiple active ongoing activity chips at once. */
data class MultipleOngoingActivityChipsModel(
    /** The primary chip to show. This will *always* be shown. */
    val primary: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
    /**
     * The secondary chip to show. If there's not enough room in the status bar, this chip will
     * *not* be shown.
     */
    val secondary: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
) {
    init {
        if (
            primary is OngoingActivityChipModel.Hidden &&
                secondary is OngoingActivityChipModel.Shown
        ) {
            throw IllegalArgumentException("`secondary` cannot be Shown if `primary` is Hidden")
        }
    }
}
+238 −89
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.statusbar.chips.ui.viewmodel

import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -27,6 +28,7 @@ import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipVie
import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
@@ -87,7 +89,16 @@ constructor(
        ) : InternalChipModel
    }

    private val internalChip: Flow<InternalChipModel> =
    private data class ChipBundle(
        val screenRecord: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
        val shareToApp: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
        val castToOtherDevice: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
        val call: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
        val demoRon: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
    )

    /** Bundles all the incoming chips into one object to easily pass to various flows. */
    private val incomingChipBundle =
        combine(
                screenRecordChipViewModel.chip,
                shareToAppChipViewModel.chip,
@@ -114,32 +125,7 @@ constructor(
                    },
                    { "... > Call=$str1 > DemoRon=$str2" }
                )
            // This `when` statement shows the priority order of the chips.
            when {
                // Screen recording also activates the media projection APIs, so whenever the
                // screen recording chip is active, the media projection chip would also be
                // active. We want the screen-recording-specific chip shown in this case, so we
                // give the screen recording chip priority. See b/296461748.
                screenRecord is OngoingActivityChipModel.Shown ->
                    InternalChipModel.Shown(ChipType.ScreenRecord, screenRecord)
                shareToApp is OngoingActivityChipModel.Shown ->
                    InternalChipModel.Shown(ChipType.ShareToApp, shareToApp)
                castToOtherDevice is OngoingActivityChipModel.Shown ->
                    InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice)
                call is OngoingActivityChipModel.Shown ->
                    InternalChipModel.Shown(ChipType.Call, call)
                demoRon is OngoingActivityChipModel.Shown -> {
                    StatusBarRonChips.assertInNewMode()
                    InternalChipModel.Shown(ChipType.DemoRon, demoRon)
                }
                else -> {
                    // We should only get here if all chip types are hidden
                    check(screenRecord is OngoingActivityChipModel.Hidden)
                    check(shareToApp is OngoingActivityChipModel.Hidden)
                    check(castToOtherDevice is OngoingActivityChipModel.Hidden)
                    check(call is OngoingActivityChipModel.Hidden)
                    check(demoRon is OngoingActivityChipModel.Hidden)
                    InternalChipModel.Hidden(
                ChipBundle(
                    screenRecord = screenRecord,
                    shareToApp = shareToApp,
                    castToOtherDevice = castToOtherDevice,
@@ -147,8 +133,14 @@ constructor(
                    demoRon = demoRon,
                )
            }
            }
        }
            // Some of the chips could have timers in them and we don't want the start time
            // for those timers to get reset for any reason. So, as soon as any subscriber has
            // requested the chip information, we maintain it forever by using
            // [SharingStarted.Lazily]. See b/347726238.
            .stateIn(scope, SharingStarted.Lazily, ChipBundle())

    private val internalChip: Flow<InternalChipModel> =
        incomingChipBundle.map { bundle -> pickMostImportantChip(bundle).mostImportantChip }

    /**
     * A flow modeling the primary chip that should be shown in the status bar after accounting for
@@ -160,8 +152,164 @@ constructor(
    val primaryChip: StateFlow<OngoingActivityChipModel> =
        internalChip
            .pairwise(initialValue = DEFAULT_INTERNAL_HIDDEN_MODEL)
            .map { (old, new) -> createOutputModel(old, new) }
            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())

    /**
     * Equivalent to [MultipleOngoingActivityChipsModel] but using the internal models to do some
     * state tracking before we get the final output.
     */
    private data class InternalMultipleOngoingActivityChipsModel(
        val primary: InternalChipModel,
        val secondary: InternalChipModel,
    )

    private val internalChips: Flow<InternalMultipleOngoingActivityChipsModel> =
        incomingChipBundle.map { bundle ->
            // First: Find the most important chip.
            val primaryChipResult = pickMostImportantChip(bundle)
            val primaryChip = primaryChipResult.mostImportantChip
            if (primaryChip is InternalChipModel.Hidden) {
                // If the primary chip is hidden, the secondary chip will also be hidden, so just
                // pass the same Hidden model for both.
                InternalMultipleOngoingActivityChipsModel(primaryChip, primaryChip)
            } else {
                // Then: Find the next most important chip.
                val secondaryChip =
                    pickMostImportantChip(primaryChipResult.remainingChips).mostImportantChip
                InternalMultipleOngoingActivityChipsModel(primaryChip, secondaryChip)
            }
        }

    /**
     * A flow modeling the primary chip that should be shown in the status bar after accounting for
     * possibly multiple ongoing activities and animation requirements.
     *
     * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for
     * actually displaying the chip.
     */
    val chips: StateFlow<MultipleOngoingActivityChipsModel> =
        if (!Flags.statusBarRonChips()) {
            // Multiple chips are only allowed with RONs. If the flag isn't on, use just the
            // primary chip.
            primaryChip
                .map {
                    MultipleOngoingActivityChipsModel(
                        primary = it,
                        secondary = OngoingActivityChipModel.Hidden(),
                    )
                }
                .stateIn(
                    scope,
                    SharingStarted.Lazily,
                    MultipleOngoingActivityChipsModel(),
                )
        } else {
            internalChips
                .pairwise(initialValue = DEFAULT_MULTIPLE_INTERNAL_HIDDEN_MODEL)
                .map { (old, new) ->
                if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) {
                    val correctPrimary = createOutputModel(old.primary, new.primary)
                    val correctSecondary = createOutputModel(old.secondary, new.secondary)
                    MultipleOngoingActivityChipsModel(correctPrimary, correctSecondary)
                }
                .stateIn(
                    scope,
                    SharingStarted.Lazily,
                    MultipleOngoingActivityChipsModel(),
                )
        }

    /** A data class representing the return result of [pickMostImportantChip]. */
    private data class MostImportantChipResult(
        val mostImportantChip: InternalChipModel,
        val remainingChips: ChipBundle,
    )

    /**
     * Finds the most important chip from the given [bundle].
     *
     * This function returns that most important chip, and it also returns any remaining chips that
     * still want to be shown after filtering out the most important chip.
     */
    private fun pickMostImportantChip(bundle: ChipBundle): MostImportantChipResult {
        // This `when` statement shows the priority order of the chips.
        return when {
            bundle.screenRecord is OngoingActivityChipModel.Shown ->
                MostImportantChipResult(
                    mostImportantChip =
                        InternalChipModel.Shown(ChipType.ScreenRecord, bundle.screenRecord),
                    remainingChips =
                        bundle.copy(
                            screenRecord = OngoingActivityChipModel.Hidden(),
                            // Screen recording also activates the media projection APIs, which
                            // means that whenever the screen recording chip is active, the
                            // share-to-app chip would also be active. (Screen recording is a
                            // special case of share-to-app, where the app receiving the share is
                            // specifically System UI.)
                            // We want only the screen-recording-specific chip to be shown in this
                            // case. If we did have screen recording as the primary chip, we need to
                            // suppress the share-to-app chip to make sure they don't both show.
                            // See b/296461748.
                            shareToApp = OngoingActivityChipModel.Hidden(),
                        )
                )
            bundle.shareToApp is OngoingActivityChipModel.Shown ->
                MostImportantChipResult(
                    mostImportantChip =
                        InternalChipModel.Shown(ChipType.ShareToApp, bundle.shareToApp),
                    remainingChips = bundle.copy(shareToApp = OngoingActivityChipModel.Hidden()),
                )
            bundle.castToOtherDevice is OngoingActivityChipModel.Shown ->
                MostImportantChipResult(
                    mostImportantChip =
                        InternalChipModel.Shown(
                            ChipType.CastToOtherDevice,
                            bundle.castToOtherDevice,
                        ),
                    remainingChips =
                        bundle.copy(castToOtherDevice = OngoingActivityChipModel.Hidden()),
                )
            bundle.call is OngoingActivityChipModel.Shown ->
                MostImportantChipResult(
                    mostImportantChip = InternalChipModel.Shown(ChipType.Call, bundle.call),
                    remainingChips = bundle.copy(call = OngoingActivityChipModel.Hidden()),
                )
            bundle.demoRon is OngoingActivityChipModel.Shown -> {
                StatusBarRonChips.assertInNewMode()
                MostImportantChipResult(
                    mostImportantChip = InternalChipModel.Shown(ChipType.DemoRon, bundle.demoRon),
                    remainingChips = bundle.copy(demoRon = OngoingActivityChipModel.Hidden()),
                )
            }
            else -> {
                // We should only get here if all chip types are hidden
                check(bundle.screenRecord is OngoingActivityChipModel.Hidden)
                check(bundle.shareToApp is OngoingActivityChipModel.Hidden)
                check(bundle.castToOtherDevice is OngoingActivityChipModel.Hidden)
                check(bundle.call is OngoingActivityChipModel.Hidden)
                check(bundle.demoRon is OngoingActivityChipModel.Hidden)
                MostImportantChipResult(
                    mostImportantChip =
                        InternalChipModel.Hidden(
                            screenRecord = bundle.screenRecord,
                            shareToApp = bundle.shareToApp,
                            castToOtherDevice = bundle.castToOtherDevice,
                            call = bundle.call,
                            demoRon = bundle.demoRon,
                        ),
                    // All the chips are already hidden, so no need to filter anything out of the
                    // bundle.
                    remainingChips = bundle,
                )
            }
        }
    }

    private fun createOutputModel(
        old: InternalChipModel,
        new: InternalChipModel,
    ): OngoingActivityChipModel {
        return if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) {
            // If we're transitioning from showing the chip to hiding the chip, different
            // chips require different animation behaviors. For example, the screen share
            // chips shouldn't animate if the user stopped the screen share from the dialog
@@ -170,7 +318,7 @@ constructor(
            // This `when` block makes sure that when we're transitioning from Shown to
            // Hidden, we check what chip type was previously showing and we use that chip
            // type's hide animation behavior.
                    when (old.type) {
            return when (old.type) {
                ChipType.ScreenRecord -> new.screenRecord
                ChipType.ShareToApp -> new.shareToApp
                ChipType.CastToOtherDevice -> new.castToOtherDevice
@@ -186,11 +334,6 @@ constructor(
            OngoingActivityChipModel.Hidden()
        }
    }
            // Some of the chips could have timers in them and we don't want the start time
            // for those timers to get reset for any reason. So, as soon as any subscriber has
            // requested the chip information, we maintain it forever by using
            // [SharingStarted.Lazily]. See b/347726238.
            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())

    companion object {
        private const val TAG = "ChipsViewModel"
@@ -203,5 +346,11 @@ constructor(
                call = OngoingActivityChipModel.Hidden(),
                demoRon = OngoingActivityChipModel.Hidden(),
            )

        private val DEFAULT_MULTIPLE_INTERNAL_HIDDEN_MODEL =
            InternalMultipleOngoingActivityChipsModel(
                primary = DEFAULT_INTERNAL_HIDDEN_MODEL,
                secondary = DEFAULT_INTERNAL_HIDDEN_MODEL,
            )
    }
}
+56 −14

File changed.

Preview size limit exceeded, changes collapsed.

Loading