Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +121 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,9 @@ 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.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.display.data.repository.displayStateRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue Loading @@ -50,6 +53,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip 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.MultipleOngoingActivityChipsModelLegacy import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel Loading Loading @@ -1954,6 +1958,123 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(latest).isEqualTo(OngoingActivityChipModel.Inactive(shouldAnimate = false)) } @Test @EnableChipsModernization fun chips_singleRefiner_hidesSpecificChip() = kosmos.runTest { chipsRefinerSet.add(RemoveChipRefiner(ShareToAppChipViewModel.KEY)) val latestChips by collectLastValue(underTest.chips) val callNotificationKey = "call" addOngoingCallState(key = callNotificationKey, isAppVisible = false) mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) val activeChips = latestChips?.active assertThat(activeChips?.size).isEqualTo(1) assertThat(activeChips?.any { it.key == ShareToAppChipViewModel.KEY }).isFalse() assertThat(activeChips?.any { it.key.startsWith(CallChipViewModel.KEY_PREFIX) }) .isTrue() val inactiveChips = latestChips?.inactive assertThat( inactiveChips?.any { it == OngoingActivityChipModel.Inactive(shouldAnimate = false) } ) .isTrue() } @Test @EnableChipsModernization fun chips_multipleRefiners_bothApplied() = kosmos.runTest { chipsRefinerSet.add(RemoveChipRefiner(ShareToAppChipViewModel.KEY)) val changeFirstChipIconRefiner = ChangeFirstChipIconRefiner() chipsRefinerSet.add(changeFirstChipIconRefiner) val latestChips by collectLastValue(underTest.chips) val callNotificationKey = "callChip" addOngoingCallState(key = callNotificationKey, isAppVisible = false) mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) screenRecordState.value = ScreenRecordModel.DoingNothing val activeChips = latestChips?.active assertThat(activeChips).isNotNull() assertThat(activeChips).isNotEmpty() assertThat(activeChips?.any { it.key == ShareToAppChipViewModel.KEY }).isFalse() val firstChip = activeChips?.firstOrNull() assertThat(firstChip!!.key).startsWith(CallChipViewModel.KEY_PREFIX) val icon = firstChip.icon assertThat(icon) .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) val singleColorIcon = icon as OngoingActivityChipModel.ChipIcon.SingleColorIcon val resourceIcon = singleColorIcon.impl as Icon.Resource assertThat(resourceIcon.res).isEqualTo(changeFirstChipIconRefiner.newIconRes) assertThat(resourceIcon.contentDescription?.loadContentDescription(context)) .isEqualTo(changeFirstChipIconRefiner.newIconDesc) } companion object { class RemoveChipRefiner(private val keyToRemove: String) : OngoingActivityChipsRefiner { override fun transform( input: MultipleOngoingActivityChipsModel ): MultipleOngoingActivityChipsModel { val chipWasInActive = input.active.any { it.key == keyToRemove } val chipWasInOverflow = input.overflow.any { it.key == keyToRemove } val newActive = input.active.filterNot { it.key == keyToRemove } val newOverflow = input.overflow.filterNot { it.key == keyToRemove } val currentInactive = input.inactive.toMutableList() if (chipWasInActive || chipWasInOverflow) { // Add an Inactive model if the chip key was found in active or overflow lists. // This signifies that the refiner made this chip inactive. currentInactive.add(OngoingActivityChipModel.Inactive(shouldAnimate = false)) } return input.copy( active = newActive, overflow = newOverflow, inactive = currentInactive.toList(), ) } } class ChangeFirstChipIconRefiner : OngoingActivityChipsRefiner { val newIconRes = android.R.drawable.ic_dialog_info // A standard Android resource val newIconDesc = "Test Changed Icon" override fun transform( input: MultipleOngoingActivityChipsModel ): MultipleOngoingActivityChipsModel { if (input.active.isNotEmpty()) { val firstChip = input.active.first() val modifiedFirstChip = firstChip.copy( icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( newIconRes, ContentDescription.Loaded(newIconDesc), ) ) ) val newActive = listOf(modifiedFirstChip) + input.active.drop(1) return input.copy(active = newActive) } return input } } } private fun setNotifs(notifs: List<ActiveNotificationModel>) { activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() Loading packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt +6 −0 Original line number Diff line number Diff line Loading @@ -21,12 +21,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import dagger.Lazy import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.Multibinds @Module abstract class StatusBarChipsModule { Loading @@ -52,4 +54,8 @@ abstract class StatusBarChipsModule { } } } /** Provides the base empty set for [OngoingActivityChipsRefiner]. */ @Multibinds abstract fun provideOngoingActivityChipRefinerSet(): Set<OngoingActivityChipsRefiner> } packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsRefiner.kt 0 → 100644 +28 −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.statusbar.chips.ui.viewmodel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel /** * An [OngoingActivityChipsRefiner] takes the entire set of chips produced by the * [OngoingActivityChipsViewModel] and can apply a transform on them to produce a modified set of * chips. */ interface OngoingActivityChipsRefiner { fun transform(input: MultipleOngoingActivityChipsModel): MultipleOngoingActivityChipsModel } packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +40 −31 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ constructor( callChipViewModel: CallChipViewModel, notifChipsViewModel: NotifChipsViewModel, displayStateInteractor: DisplayStateInteractor, private val chipsRefiners: Set<@JvmSuppressWildcards OngoingActivityChipsRefiner>, @StatusBarChipsLog private val logger: LogBuffer, ) { private enum class ChipType { Loading Loading @@ -255,7 +256,7 @@ constructor( * A flow modeling the active and inactive chips as well as which should be shown in the status * bar after accounting for possibly multiple ongoing activities and animation requirements. */ val chips: StateFlow<MultipleOngoingActivityChipsModel> = private val unrefinedChips = if (StatusBarChipsModernization.isEnabled) { combine( incomingChipBundle.map { bundle -> rankChips(bundle) }, Loading Loading @@ -289,11 +290,19 @@ constructor( rankedChips } } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) } else { MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow() MutableStateFlow(MultipleOngoingActivityChipsModel()) } val chips: StateFlow<MultipleOngoingActivityChipsModel> = unrefinedChips .map { unrefinedChips -> chipsRefiners.fold(unrefinedChips) { currentOutput, refiner -> refiner.transform(currentOutput) } } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) /** * A flow modeling the primary chip that should be shown in the status bar after accounting for * possibly multiple ongoing activities and animation requirements. Loading packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt +4 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,10 @@ val Kosmos.ongoingActivityChipsViewModel: OngoingActivityChipsViewModel by callChipViewModel = callChipViewModel, notifChipsViewModel = notifChipsViewModel, displayStateInteractor = displayStateInteractor, chipsRefiners = chipsRefinerSet, logger = statusBarChipsLogger, ) } val Kosmos.chipsRefinerSet: MutableSet<OngoingActivityChipsRefiner> by Kosmos.Fixture { mutableSetOf() } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +121 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,9 @@ 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.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.display.data.repository.displayStateRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue Loading @@ -50,6 +53,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip 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.MultipleOngoingActivityChipsModelLegacy import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel Loading Loading @@ -1954,6 +1958,123 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(latest).isEqualTo(OngoingActivityChipModel.Inactive(shouldAnimate = false)) } @Test @EnableChipsModernization fun chips_singleRefiner_hidesSpecificChip() = kosmos.runTest { chipsRefinerSet.add(RemoveChipRefiner(ShareToAppChipViewModel.KEY)) val latestChips by collectLastValue(underTest.chips) val callNotificationKey = "call" addOngoingCallState(key = callNotificationKey, isAppVisible = false) mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) val activeChips = latestChips?.active assertThat(activeChips?.size).isEqualTo(1) assertThat(activeChips?.any { it.key == ShareToAppChipViewModel.KEY }).isFalse() assertThat(activeChips?.any { it.key.startsWith(CallChipViewModel.KEY_PREFIX) }) .isTrue() val inactiveChips = latestChips?.inactive assertThat( inactiveChips?.any { it == OngoingActivityChipModel.Inactive(shouldAnimate = false) } ) .isTrue() } @Test @EnableChipsModernization fun chips_multipleRefiners_bothApplied() = kosmos.runTest { chipsRefinerSet.add(RemoveChipRefiner(ShareToAppChipViewModel.KEY)) val changeFirstChipIconRefiner = ChangeFirstChipIconRefiner() chipsRefinerSet.add(changeFirstChipIconRefiner) val latestChips by collectLastValue(underTest.chips) val callNotificationKey = "callChip" addOngoingCallState(key = callNotificationKey, isAppVisible = false) mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) screenRecordState.value = ScreenRecordModel.DoingNothing val activeChips = latestChips?.active assertThat(activeChips).isNotNull() assertThat(activeChips).isNotEmpty() assertThat(activeChips?.any { it.key == ShareToAppChipViewModel.KEY }).isFalse() val firstChip = activeChips?.firstOrNull() assertThat(firstChip!!.key).startsWith(CallChipViewModel.KEY_PREFIX) val icon = firstChip.icon assertThat(icon) .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) val singleColorIcon = icon as OngoingActivityChipModel.ChipIcon.SingleColorIcon val resourceIcon = singleColorIcon.impl as Icon.Resource assertThat(resourceIcon.res).isEqualTo(changeFirstChipIconRefiner.newIconRes) assertThat(resourceIcon.contentDescription?.loadContentDescription(context)) .isEqualTo(changeFirstChipIconRefiner.newIconDesc) } companion object { class RemoveChipRefiner(private val keyToRemove: String) : OngoingActivityChipsRefiner { override fun transform( input: MultipleOngoingActivityChipsModel ): MultipleOngoingActivityChipsModel { val chipWasInActive = input.active.any { it.key == keyToRemove } val chipWasInOverflow = input.overflow.any { it.key == keyToRemove } val newActive = input.active.filterNot { it.key == keyToRemove } val newOverflow = input.overflow.filterNot { it.key == keyToRemove } val currentInactive = input.inactive.toMutableList() if (chipWasInActive || chipWasInOverflow) { // Add an Inactive model if the chip key was found in active or overflow lists. // This signifies that the refiner made this chip inactive. currentInactive.add(OngoingActivityChipModel.Inactive(shouldAnimate = false)) } return input.copy( active = newActive, overflow = newOverflow, inactive = currentInactive.toList(), ) } } class ChangeFirstChipIconRefiner : OngoingActivityChipsRefiner { val newIconRes = android.R.drawable.ic_dialog_info // A standard Android resource val newIconDesc = "Test Changed Icon" override fun transform( input: MultipleOngoingActivityChipsModel ): MultipleOngoingActivityChipsModel { if (input.active.isNotEmpty()) { val firstChip = input.active.first() val modifiedFirstChip = firstChip.copy( icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( newIconRes, ContentDescription.Loaded(newIconDesc), ) ) ) val newActive = listOf(modifiedFirstChip) + input.active.drop(1) return input.copy(active = newActive) } return input } } } private fun setNotifs(notifs: List<ActiveNotificationModel>) { activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() Loading
packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt +6 −0 Original line number Diff line number Diff line Loading @@ -21,12 +21,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import dagger.Lazy import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.Multibinds @Module abstract class StatusBarChipsModule { Loading @@ -52,4 +54,8 @@ abstract class StatusBarChipsModule { } } } /** Provides the base empty set for [OngoingActivityChipsRefiner]. */ @Multibinds abstract fun provideOngoingActivityChipRefinerSet(): Set<OngoingActivityChipsRefiner> }
packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsRefiner.kt 0 → 100644 +28 −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.statusbar.chips.ui.viewmodel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel /** * An [OngoingActivityChipsRefiner] takes the entire set of chips produced by the * [OngoingActivityChipsViewModel] and can apply a transform on them to produce a modified set of * chips. */ interface OngoingActivityChipsRefiner { fun transform(input: MultipleOngoingActivityChipsModel): MultipleOngoingActivityChipsModel }
packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +40 −31 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ constructor( callChipViewModel: CallChipViewModel, notifChipsViewModel: NotifChipsViewModel, displayStateInteractor: DisplayStateInteractor, private val chipsRefiners: Set<@JvmSuppressWildcards OngoingActivityChipsRefiner>, @StatusBarChipsLog private val logger: LogBuffer, ) { private enum class ChipType { Loading Loading @@ -255,7 +256,7 @@ constructor( * A flow modeling the active and inactive chips as well as which should be shown in the status * bar after accounting for possibly multiple ongoing activities and animation requirements. */ val chips: StateFlow<MultipleOngoingActivityChipsModel> = private val unrefinedChips = if (StatusBarChipsModernization.isEnabled) { combine( incomingChipBundle.map { bundle -> rankChips(bundle) }, Loading Loading @@ -289,11 +290,19 @@ constructor( rankedChips } } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) } else { MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow() MutableStateFlow(MultipleOngoingActivityChipsModel()) } val chips: StateFlow<MultipleOngoingActivityChipsModel> = unrefinedChips .map { unrefinedChips -> chipsRefiners.fold(unrefinedChips) { currentOutput, refiner -> refiner.transform(currentOutput) } } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) /** * A flow modeling the primary chip that should be shown in the status bar after accounting for * possibly multiple ongoing activities and animation requirements. Loading
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt +4 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,10 @@ val Kosmos.ongoingActivityChipsViewModel: OngoingActivityChipsViewModel by callChipViewModel = callChipViewModel, notifChipsViewModel = notifChipsViewModel, displayStateInteractor = displayStateInteractor, chipsRefiners = chipsRefinerSet, logger = statusBarChipsLogger, ) } val Kosmos.chipsRefinerSet: MutableSet<OngoingActivityChipsRefiner> by Kosmos.Fixture { mutableSetOf() }