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

Commit 391a1fc3 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Notif] If there's 2 chips, make both chips as compact as possible.

The new POR is that if we have 2 active chips, we prefer seeing both
chips as icon-only (or text-only) chips instead of seeing the first chip
with icon+text and having the second chip completely hidden.

Most chips will hide their text and become icon-only. The only exception
to this is the 3-2-1 screen record countdown chip, which always shows a
single digit without an icon and will continue to do that even when
there's 2 chips.

Caveats:
 - The icon-only squishing will only happen for phones in portrait; for
   all other configurations, there's enough room to show 2 full-sized
   chips.
 - We also need to re-implement this in the Compose chips. That'll
   happen in a separate CL.

Bug: 392895330
Bug: 364653005
Flag: com.android.systemui.status_bar_notification_chips
Test: Have 3-2-1 countdown chip + other chip -> verify see countdown
number & other chip with icon only
Test: Have 2 RON chips in portrait phone -> verify both are icon only
Test: Have call chip + RON chip in portrait phone -> verify both are icon only
Test: Have screen record chip + other chip in portrait phone -> verify both are icon only
Test: Have 2 chips then remove one in portrait phone -> verify first chip re-expands to
icon+text

Test: with status_bar_notification_chips OFF, have both a call and
screen record active -> verify screen record chip shows, and shows with
a timer (basically, verify squishing doesn't happen if we don't support
multiple chips)
Test: Have 2 RON chips in portrait phone then rotate -> verify chips
update to show text
Test: Have 2 RON chips on a large screen -> verify chips show text

Test: atest OngoingActivityChipsViewModelTest
Change-Id: I283d4c38b8d4f25e369fe6ab7d7fbf5a9017633c
parent ea55a5ff
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -404,7 +404,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
        }

        fun assertIsCallChip(latest: OngoingActivityChipModel?, notificationKey: String) {
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
            if (StatusBarConnectedDisplays.isEnabled) {
                assertNotificationIcon(latest, notificationKey)
                return
+154 −6
Original line number Diff line number Diff line
@@ -18,15 +18,20 @@ package com.android.systemui.statusbar.chips.ui.viewmodel

import android.content.DialogInterface
import android.content.packageManager
import android.content.res.Configuration
import android.content.res.mainResources
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.display.data.repository.displayStateRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
@@ -48,21 +53,21 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsVie
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
import com.android.systemui.statusbar.commandline.commandRegistry
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -85,15 +90,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val systemClock = kosmos.fakeSystemClock
    private val commandRegistry = kosmos.commandRegistry

    private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
    private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
    private val callRepo = kosmos.ongoingCallRepository
    private val activeNotificationListRepository = kosmos.activeNotificationListRepository

    private val pw = PrintWriter(StringWriter())

    private val mockSystemUIDialog = mock<SystemUIDialog>()
    private val chipBackgroundView = mock<ChipBackgroundContainer>()
    private val chipView =
@@ -201,6 +203,152 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
            assertIsCallChip(latest!!.secondary, callNotificationKey)
        }

    @Test
    fun chips_oneChip_notSquished() =
        testScope.runTest {
            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))

            val latest by collectLastValue(underTest.chips)

            // The call chip isn't squished (squished chips would be icon only)
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
        }

    @Test
    @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
    fun chips_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() =
        testScope.runTest {
            screenRecordState.value = ScreenRecordModel.Recording
            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))

            val latest by collectLastValue(underTest.chips)

            // Squished chips are icon only
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
            assertThat(latest!!.secondary)
                .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
        }

    @Test
    @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
    fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
        testScope.runTest {
            screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))

            val latest by collectLastValue(underTest.chips)

            // The screen record countdown isn't squished to icon-only
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Countdown::class.java)
            // But the call chip *is* squished
            assertThat(latest!!.secondary)
                .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
        }

    @Test
    @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
    fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chips)

            // WHEN there's only one chip
            screenRecordState.value = ScreenRecordModel.Recording
            callRepo.setOngoingCallState(OngoingCallModel.NoCall)

            // The screen record isn't squished because it's the only one
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)

            // WHEN there's 2 chips
            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))

            // THEN they both become squished
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
            // But the call chip *is* squished
            assertThat(latest!!.secondary)
                .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)

            // WHEN we go back down to 1 chip
            screenRecordState.value = ScreenRecordModel.DoingNothing

            // THEN the remaining chip unsquishes
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
            assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
        }

    @Test
    @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
    fun chips_twoChips_isLandscape_notSquished() =
        testScope.runTest {
            screenRecordState.value = ScreenRecordModel.Recording
            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))

            // WHEN we're in landscape
            val config =
                Configuration(kosmos.mainResources.configuration).apply {
                    orientation = Configuration.ORIENTATION_LANDSCAPE
                }
            kosmos.fakeConfigurationRepository.onConfigurationChange(config)

            val latest by collectLastValue(underTest.chips)

            // THEN the chips aren't squished (squished chips would be icon only)
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
            assertThat(latest!!.secondary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
        }

    @Test
    @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
    fun chips_twoChips_isLargeScreen_notSquished() =
        testScope.runTest {
            screenRecordState.value = ScreenRecordModel.Recording
            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))

            // WHEN we're on a large screen
            kosmos.displayStateRepository.setIsLargeScreen(true)

            val latest by collectLastValue(underTest.chips)

            // THEN the chips aren't squished (squished chips would be icon only)
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
            assertThat(latest!!.secondary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
        }

    @Test
    @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
    fun chips_twoChips_chipsModernizationEnabled_notSquished() =
        testScope.runTest {
            screenRecordState.value = ScreenRecordModel.Recording
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "call",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        callType = CallType.Ongoing,
                        whenTime = 499,
                    )
                )
            )

            val latest by collectLastValue(underTest.chips)

            // Squished chips would be icon only
            assertThat(latest!!.primary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
            assertThat(latest!!.secondary)
                .isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
        }

    @Test
    fun primaryChip_screenRecordShowAndShareToAppShow_screenRecordShown() =
        testScope.runTest {
+92 −11
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

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

import android.content.res.Configuration
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -30,6 +33,7 @@ import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenReco
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.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -37,7 +41,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn

/**
@@ -56,8 +62,30 @@ constructor(
    castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel,
    callChipViewModel: CallChipViewModel,
    notifChipsViewModel: NotifChipsViewModel,
    displayStateInteractor: DisplayStateInteractor,
    configurationInteractor: ConfigurationInteractor,
    @StatusBarChipsLog private val logger: LogBuffer,
) {
    private val isLandscape: Flow<Boolean> =
        configurationInteractor.configurationValues
            .map { it.isLandscape }
            .stateIn(scope, SharingStarted.WhileSubscribed(), false)

    private val isScreenReasonablyLarge: Flow<Boolean> =
        combine(isLandscape, displayStateInteractor.isLargeScreen) { isLandscape, isLargeScreen ->
                isLandscape || isLargeScreen
            }
            .distinctUntilChanged()
            .onEach {
                logger.log(
                    TAG,
                    LogLevel.DEBUG,
                    { bool1 = it },
                    { "isScreenReasonablyLarge: $bool1" },
                )
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), false)

    private enum class ChipType {
        ScreenRecord,
        ShareToApp,
@@ -165,21 +193,71 @@ constructor(
    )

    private val internalChips: Flow<InternalMultipleOngoingActivityChipsModel> =
        incomingChipBundle.map { bundle ->
        combine(incomingChipBundle, isScreenReasonablyLarge) { bundle, isScreenReasonablyLarge ->
            // 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.
            when (val primaryChip = primaryChipResult.mostImportantChip) {
                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.
                }
                is InternalChipModel.Shown -> {
                    // Otherwise: Find the next most important chip.
                    val secondaryChip =
                        pickMostImportantChip(primaryChipResult.remainingChips).mostImportantChip
                    if (
                        secondaryChip is InternalChipModel.Shown &&
                            StatusBarNotifChips.isEnabled &&
                            !StatusBarChipsModernization.isEnabled &&
                            !isScreenReasonablyLarge
                    ) {
                        // If we have two showing chips and we don't have a ton of room
                        // (!isScreenReasonablyLarge), then we want to make both of them as small as
                        // possible so that we have the highest chance of showing both chips (as
                        // opposed to showing the primary chip with a lot of text and completely
                        // hiding the secondary chip).
                        // Also: If StatusBarChipsModernization is enabled, then we'll do the
                        // squishing in Compose instead.
                        InternalMultipleOngoingActivityChipsModel(
                            primaryChip.squish(),
                            secondaryChip.squish(),
                        )
                    } else {
                        InternalMultipleOngoingActivityChipsModel(primaryChip, secondaryChip)
                    }
                }
            }
        }

    /** Squishes the chip down to the smallest content possible. */
    private fun InternalChipModel.Shown.squish(): InternalChipModel.Shown {
        return when (model) {
            // Icon-only is already maximum squished
            is OngoingActivityChipModel.Shown.IconOnly -> this
            // Countdown shows just a single digit, so already maximum squished
            is OngoingActivityChipModel.Shown.Countdown -> this
            // The other chips have icon+text, so we should hide the text
            is OngoingActivityChipModel.Shown.Timer,
            is OngoingActivityChipModel.Shown.ShortTimeDelta,
            is OngoingActivityChipModel.Shown.Text ->
                InternalChipModel.Shown(this.type, this.model.toIconOnly())
        }
    }

    private fun OngoingActivityChipModel.Shown.toIconOnly(): OngoingActivityChipModel.Shown {
        // If this chip doesn't have an icon, then it only has text and we should continue showing
        // its text. (This is theoretically impossible because
        // [OngoingActivityChipModel.Shown.Countdown] is the only chip without an icon, but protect
        // against it just in case.)
        val currentIcon = icon ?: return this
        return OngoingActivityChipModel.Shown.IconOnly(
            currentIcon,
            colors,
            onClickListenerLegacy,
            clickBehavior,
        )
    }

    /**
     * A flow modeling the primary chip that should be shown in the status bar after accounting for
@@ -327,6 +405,9 @@ constructor(
        }
    }

    private val Configuration.isLandscape: Boolean
        get() = orientation == Configuration.ORIENTATION_LANDSCAPE

    companion object {
        private val TAG = "ChipsViewModel".pad()

+4 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

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

import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.chips.call.ui.viewmodel.callChipViewModel
@@ -34,6 +36,8 @@ val Kosmos.ongoingActivityChipsViewModel: OngoingActivityChipsViewModel by
            castToOtherDeviceChipViewModel = castToOtherDeviceChipViewModel,
            callChipViewModel = callChipViewModel,
            notifChipsViewModel = notifChipsViewModel,
            displayStateInteractor = displayStateInteractor,
            configurationInteractor = configurationInteractor,
            logger = statusBarChipsLogger,
        )
    }