Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt +59 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,65 @@ class StackedMobileIconViewModelKairosTest : SysuiTestCase() { assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_requiresBothIcons() = kosmos.runKairosTest { mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf()) assertThat(underTest.contentDescription).isNull() mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) assertThat(underTest.contentDescription).isNull() mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( listOf(SUB_1, SUB_2, SUB_3) ) assertThat(underTest.contentDescription).isNull() mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) assertThat(underTest.contentDescription).isNotNull() } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_tracksBars() = kosmos.runKairosTest { mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("default name, one bar. default name, two bars.") // Change signal bars setIconLevel(SUB_1.subscriptionId, 3) setIconLevel(SUB_2.subscriptionId, 1) assertThat(underTest.contentDescription!!) .isEqualTo("default name, three bars. default name, one bar.") } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_hasActiveIconFirst() = kosmos.runKairosTest { // Active sub id is null, order is unchanged mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("default name, one bar. default name, two bars.") // Active sub is 2, order is swapped mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId( SUB_2.subscriptionId ) assertThat(underTest.contentDescription!!) .isEqualTo("default name, two bars. default name, one bar.") } private suspend fun KairosTestScope.setIconLevel(subId: Int, level: Int) { mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[subId]!!.apply { isNonTerrestrial.setValue(false) Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt +72 −4 Original line number Diff line number Diff line Loading @@ -43,15 +43,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class StackedMobileIconViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val Kosmos.underTest: StackedMobileIconViewModelImpl by Fixture { stackedMobileIconViewModelImpl } @Before fun setUp() { kosmos.underTest.activateIn(testScope) fun setUp() = kosmos.run { // Set prerequisites for the stacked icon fakeMobileIconsInteractor.isStackable.value = true underTest.activateIn(testScope) } @Test Loading Loading @@ -109,6 +111,72 @@ class StackedMobileIconViewModelTest : SysuiTestCase() { assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_requiresBothIcons() = kosmos.runTest { fakeMobileIconsInteractor.filteredSubscriptions.value = listOf() assertThat(underTest.contentDescription).isNull() fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) assertThat(underTest.contentDescription).isNull() fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) assertThat(underTest.contentDescription).isNull() fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) assertThat(underTest.contentDescription).isNotNull() } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_tracksBars() = kosmos.runTest { fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, one bar. demo mode, two bars.") // Change signal bars setIconLevel(SUB_1.subscriptionId, 3) setIconLevel(SUB_2.subscriptionId, 1) assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, three bars. demo mode, one bar.") } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_hasActiveIconFirst() = kosmos.runTest { // Active sub id is null, order is unchanged fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, one bar. demo mode, two bars.") // Active sub is 2, order is swapped fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, two bars. demo mode, one bar.") } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_tracksVisibility() = kosmos.runTest { fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) assertThat(underTest.contentDescription).isNotNull() fakeMobileIconsInteractor.isStackable.value = false assertThat(underTest.contentDescription).isNull() } private fun setIconLevel(subId: Int, level: Int) { with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) { signalLevelIcon.value = Loading packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt +67 −23 Original line number Diff line number Diff line Loading @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.compose.runtime.derivedStateOf import android.content.Context import androidx.compose.runtime.getValue import com.android.systemui.common.shared.model.Icon import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel.DualSim import dagger.assisted.AssistedFactory Loading @@ -30,9 +31,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map interface StackedMobileIconViewModel { val dualSim: DualSim? val contentDescription: String? val networkTypeIcon: Icon.Resource? val isIconVisible: Boolean Loading @@ -45,17 +48,12 @@ interface StackedMobileIconViewModel { @OptIn(ExperimentalCoroutinesApi::class) class StackedMobileIconViewModelImpl @AssistedInject constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable(), StackedMobileIconViewModel { constructor( mobileIconsViewModel: MobileIconsViewModel, @ShadeDisplayAware private val context: Context, ) : ExclusiveActivatable(), StackedMobileIconViewModel { private val hydrator = Hydrator("StackedMobileIconViewModel") private val isStackable: Boolean by hydrator.hydratedStateOf( traceName = "isStackable", source = mobileIconsViewModel.isStackable, initialValue = false, ) private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> = combine( mobileIconsViewModel.mobileSubViewModels, Loading @@ -65,10 +63,7 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : viewModels.sortedByDescending { it.subscriptionId == activeSubId } } override val dualSim: DualSim? by hydrator.hydratedStateOf( traceName = "dualSim", source = private val _dualSim: Flow<DualSim?> = iconViewModelFlow.flatMapLatest { viewModels -> combine(viewModels.map { it.icon }) { icons -> icons Loading @@ -77,7 +72,39 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : .takeIf { it.size == 2 } ?.let { DualSim(it[0], it[1]) } } }, } private val _isIconVisible: Flow<Boolean> = combine(_dualSim, mobileIconsViewModel.isStackable) { dualSim, isStackable -> dualSim != null && isStackable } override val dualSim: DualSim? by hydrator.hydratedStateOf(traceName = "dualSim", source = _dualSim, initialValue = null) /** Content description of both icons, starting with the active connection. */ override val contentDescription: String? by hydrator.hydratedStateOf( traceName = "contentDescription", source = flowIfIconIsVisible( iconViewModelFlow.flatMapLatest { viewModels -> combine(viewModels.map { it.contentDescription }) { contentDescriptions -> contentDescriptions.map { it?.loadContentDescription(context) } } .map { loadedStrings -> // Only provide the content description if both icons have one if (loadedStrings.any { it == null }) { null } else { // The content description of each icon has the format: // "[Carrier name], N bars." // To combine, we simply join them with a space loadedStrings.joinToString(" ") } } } ), initialValue = null, ) Loading @@ -85,13 +112,30 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : hydrator.hydratedStateOf( traceName = "networkTypeIcon", source = flowIfIconIsVisible( iconViewModelFlow.flatMapLatest { viewModels -> viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null) }, } ), initialValue = null, ) override val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } override val isIconVisible: Boolean by hydrator.hydratedStateOf( traceName = "isIconVisible", source = _isIconVisible, initialValue = false, ) private fun <T> flowIfIconIsVisible(flow: Flow<T>): Flow<T?> { return _isIconVisible.flatMapLatest { isVisible -> if (isVisible) { flow } else { flowOf(null) } } } override suspend fun onActivated(): Nothing { hydrator.activate() Loading packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt +27 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.content.Context import androidx.compose.runtime.getValue import com.android.systemui.KairosBuilder import com.android.systemui.common.shared.model.Icon Loading @@ -25,7 +26,9 @@ import com.android.systemui.kairos.combine import com.android.systemui.kairos.flatMap import com.android.systemui.kairos.stateOf import com.android.systemui.kairosBuilder import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel.DualSim import com.android.systemui.util.composable.kairos.hydratedComposeStateOf import dagger.assisted.AssistedFactory Loading @@ -34,8 +37,10 @@ import dagger.assisted.AssistedInject @OptIn(ExperimentalKairosApi::class) class StackedMobileIconViewModelKairos @AssistedInject constructor(mobileIcons: MobileIconsViewModelKairos) : KairosBuilder by kairosBuilder(), StackedMobileIconViewModel { constructor( mobileIcons: MobileIconsViewModelKairos, @ShadeDisplayAware private val context: Context, ) : KairosBuilder by kairosBuilder(), StackedMobileIconViewModel { private val isStackable: Boolean by hydratedComposeStateOf(mobileIcons.isStackable, initialValue = false) Loading @@ -56,6 +61,18 @@ constructor(mobileIcons: MobileIconsViewModelKairos) : initialValue = null, ) override val contentDescription: String? by hydratedComposeStateOf( iconList.flatMap { icons -> icons .map { it.contentDescription } .combine { contentDescriptions -> tryParseContentDescriptions(contentDescriptions) } }, initialValue = null, ) override val networkTypeIcon: Icon.Resource? by hydratedComposeStateOf( iconList.flatMap { icons -> icons.firstOrNull()?.networkTypeIcon ?: stateOf(null) }, Loading @@ -79,6 +96,14 @@ constructor(mobileIcons: MobileIconsViewModelKairos) : return first?.let { second?.let { DualSim(first, second) } } } private fun tryParseContentDescriptions( contentDescriptions: List<MobileContentDescription?> ): String? { if (contentDescriptions.size != 2 || null in contentDescriptions) return null return contentDescriptions.joinToString(" ") { it?.loadContentDescription(context) ?: "" } } @AssistedFactory interface Factory { fun create(): StackedMobileIconViewModelKairos Loading packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt +13 −2 Original line number Diff line number Diff line Loading @@ -34,6 +34,8 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import com.android.systemui.common.ui.compose.Icon Loading Loading @@ -79,7 +81,11 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier Icon(it, tint = contentColor, modifier = Modifier.height(height).wrapContentWidth()) } StackedMobileIcon(dualSim, contentColor) StackedMobileIcon( viewModel = dualSim, color = contentColor, contentDescription = viewModel.contentDescription, ) } } Loading @@ -87,6 +93,7 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier private fun StackedMobileIcon( viewModel: StackedMobileIconViewModel.DualSim, color: Color, contentDescription: String?, modifier: Modifier = Modifier, ) { // Removing 1 to get the real number of bars Loading @@ -95,7 +102,11 @@ private fun StackedMobileIcon( val iconSize = with(LocalDensity.current) { dimensions.totalWidth.toDp() to IconHeightSp.toDp() } Canvas(modifier.width(iconSize.first).height(iconSize.second)) { Canvas( modifier.width(iconSize.first).height(iconSize.second).semantics { contentDescription?.let { this.contentDescription = it } } ) { val verticalPaddingPx = BarsVerticalPaddingSp.roundToPx() val horizontalPaddingPx = dimensions.barsHorizontalPadding.roundToPx() val totalPaddingWidthPx = horizontalPaddingPx * (numberOfBars - 1) Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt +59 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,65 @@ class StackedMobileIconViewModelKairosTest : SysuiTestCase() { assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_requiresBothIcons() = kosmos.runKairosTest { mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf()) assertThat(underTest.contentDescription).isNull() mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) assertThat(underTest.contentDescription).isNull() mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( listOf(SUB_1, SUB_2, SUB_3) ) assertThat(underTest.contentDescription).isNull() mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) assertThat(underTest.contentDescription).isNotNull() } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_tracksBars() = kosmos.runKairosTest { mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("default name, one bar. default name, two bars.") // Change signal bars setIconLevel(SUB_1.subscriptionId, 3) setIconLevel(SUB_2.subscriptionId, 1) assertThat(underTest.contentDescription!!) .isEqualTo("default name, three bars. default name, one bar.") } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_hasActiveIconFirst() = kosmos.runKairosTest { // Active sub id is null, order is unchanged mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("default name, one bar. default name, two bars.") // Active sub is 2, order is swapped mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId( SUB_2.subscriptionId ) assertThat(underTest.contentDescription!!) .isEqualTo("default name, two bars. default name, one bar.") } private suspend fun KairosTestScope.setIconLevel(subId: Int, level: Int) { mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[subId]!!.apply { isNonTerrestrial.setValue(false) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt +72 −4 Original line number Diff line number Diff line Loading @@ -43,15 +43,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class StackedMobileIconViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val Kosmos.underTest: StackedMobileIconViewModelImpl by Fixture { stackedMobileIconViewModelImpl } @Before fun setUp() { kosmos.underTest.activateIn(testScope) fun setUp() = kosmos.run { // Set prerequisites for the stacked icon fakeMobileIconsInteractor.isStackable.value = true underTest.activateIn(testScope) } @Test Loading Loading @@ -109,6 +111,72 @@ class StackedMobileIconViewModelTest : SysuiTestCase() { assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_requiresBothIcons() = kosmos.runTest { fakeMobileIconsInteractor.filteredSubscriptions.value = listOf() assertThat(underTest.contentDescription).isNull() fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) assertThat(underTest.contentDescription).isNull() fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) assertThat(underTest.contentDescription).isNull() fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) assertThat(underTest.contentDescription).isNotNull() } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_tracksBars() = kosmos.runTest { fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, one bar. demo mode, two bars.") // Change signal bars setIconLevel(SUB_1.subscriptionId, 3) setIconLevel(SUB_2.subscriptionId, 1) assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, three bars. demo mode, one bar.") } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_hasActiveIconFirst() = kosmos.runTest { // Active sub id is null, order is unchanged fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, one bar. demo mode, two bars.") // Active sub is 2, order is swapped fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId assertThat(underTest.contentDescription!!) .isEqualTo("demo mode, two bars. demo mode, one bar.") } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun contentDescription_tracksVisibility() = kosmos.runTest { fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) assertThat(underTest.contentDescription).isNotNull() fakeMobileIconsInteractor.isStackable.value = false assertThat(underTest.contentDescription).isNull() } private fun setIconLevel(subId: Int, level: Int) { with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) { signalLevelIcon.value = Loading
packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt +67 −23 Original line number Diff line number Diff line Loading @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.compose.runtime.derivedStateOf import android.content.Context import androidx.compose.runtime.getValue import com.android.systemui.common.shared.model.Icon import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel.DualSim import dagger.assisted.AssistedFactory Loading @@ -30,9 +31,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map interface StackedMobileIconViewModel { val dualSim: DualSim? val contentDescription: String? val networkTypeIcon: Icon.Resource? val isIconVisible: Boolean Loading @@ -45,17 +48,12 @@ interface StackedMobileIconViewModel { @OptIn(ExperimentalCoroutinesApi::class) class StackedMobileIconViewModelImpl @AssistedInject constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable(), StackedMobileIconViewModel { constructor( mobileIconsViewModel: MobileIconsViewModel, @ShadeDisplayAware private val context: Context, ) : ExclusiveActivatable(), StackedMobileIconViewModel { private val hydrator = Hydrator("StackedMobileIconViewModel") private val isStackable: Boolean by hydrator.hydratedStateOf( traceName = "isStackable", source = mobileIconsViewModel.isStackable, initialValue = false, ) private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> = combine( mobileIconsViewModel.mobileSubViewModels, Loading @@ -65,10 +63,7 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : viewModels.sortedByDescending { it.subscriptionId == activeSubId } } override val dualSim: DualSim? by hydrator.hydratedStateOf( traceName = "dualSim", source = private val _dualSim: Flow<DualSim?> = iconViewModelFlow.flatMapLatest { viewModels -> combine(viewModels.map { it.icon }) { icons -> icons Loading @@ -77,7 +72,39 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : .takeIf { it.size == 2 } ?.let { DualSim(it[0], it[1]) } } }, } private val _isIconVisible: Flow<Boolean> = combine(_dualSim, mobileIconsViewModel.isStackable) { dualSim, isStackable -> dualSim != null && isStackable } override val dualSim: DualSim? by hydrator.hydratedStateOf(traceName = "dualSim", source = _dualSim, initialValue = null) /** Content description of both icons, starting with the active connection. */ override val contentDescription: String? by hydrator.hydratedStateOf( traceName = "contentDescription", source = flowIfIconIsVisible( iconViewModelFlow.flatMapLatest { viewModels -> combine(viewModels.map { it.contentDescription }) { contentDescriptions -> contentDescriptions.map { it?.loadContentDescription(context) } } .map { loadedStrings -> // Only provide the content description if both icons have one if (loadedStrings.any { it == null }) { null } else { // The content description of each icon has the format: // "[Carrier name], N bars." // To combine, we simply join them with a space loadedStrings.joinToString(" ") } } } ), initialValue = null, ) Loading @@ -85,13 +112,30 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : hydrator.hydratedStateOf( traceName = "networkTypeIcon", source = flowIfIconIsVisible( iconViewModelFlow.flatMapLatest { viewModels -> viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null) }, } ), initialValue = null, ) override val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } override val isIconVisible: Boolean by hydrator.hydratedStateOf( traceName = "isIconVisible", source = _isIconVisible, initialValue = false, ) private fun <T> flowIfIconIsVisible(flow: Flow<T>): Flow<T?> { return _isIconVisible.flatMapLatest { isVisible -> if (isVisible) { flow } else { flowOf(null) } } } override suspend fun onActivated(): Nothing { hydrator.activate() Loading
packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt +27 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.content.Context import androidx.compose.runtime.getValue import com.android.systemui.KairosBuilder import com.android.systemui.common.shared.model.Icon Loading @@ -25,7 +26,9 @@ import com.android.systemui.kairos.combine import com.android.systemui.kairos.flatMap import com.android.systemui.kairos.stateOf import com.android.systemui.kairosBuilder import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel.DualSim import com.android.systemui.util.composable.kairos.hydratedComposeStateOf import dagger.assisted.AssistedFactory Loading @@ -34,8 +37,10 @@ import dagger.assisted.AssistedInject @OptIn(ExperimentalKairosApi::class) class StackedMobileIconViewModelKairos @AssistedInject constructor(mobileIcons: MobileIconsViewModelKairos) : KairosBuilder by kairosBuilder(), StackedMobileIconViewModel { constructor( mobileIcons: MobileIconsViewModelKairos, @ShadeDisplayAware private val context: Context, ) : KairosBuilder by kairosBuilder(), StackedMobileIconViewModel { private val isStackable: Boolean by hydratedComposeStateOf(mobileIcons.isStackable, initialValue = false) Loading @@ -56,6 +61,18 @@ constructor(mobileIcons: MobileIconsViewModelKairos) : initialValue = null, ) override val contentDescription: String? by hydratedComposeStateOf( iconList.flatMap { icons -> icons .map { it.contentDescription } .combine { contentDescriptions -> tryParseContentDescriptions(contentDescriptions) } }, initialValue = null, ) override val networkTypeIcon: Icon.Resource? by hydratedComposeStateOf( iconList.flatMap { icons -> icons.firstOrNull()?.networkTypeIcon ?: stateOf(null) }, Loading @@ -79,6 +96,14 @@ constructor(mobileIcons: MobileIconsViewModelKairos) : return first?.let { second?.let { DualSim(first, second) } } } private fun tryParseContentDescriptions( contentDescriptions: List<MobileContentDescription?> ): String? { if (contentDescriptions.size != 2 || null in contentDescriptions) return null return contentDescriptions.joinToString(" ") { it?.loadContentDescription(context) ?: "" } } @AssistedFactory interface Factory { fun create(): StackedMobileIconViewModelKairos Loading
packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt +13 −2 Original line number Diff line number Diff line Loading @@ -34,6 +34,8 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import com.android.systemui.common.ui.compose.Icon Loading Loading @@ -79,7 +81,11 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier Icon(it, tint = contentColor, modifier = Modifier.height(height).wrapContentWidth()) } StackedMobileIcon(dualSim, contentColor) StackedMobileIcon( viewModel = dualSim, color = contentColor, contentDescription = viewModel.contentDescription, ) } } Loading @@ -87,6 +93,7 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier private fun StackedMobileIcon( viewModel: StackedMobileIconViewModel.DualSim, color: Color, contentDescription: String?, modifier: Modifier = Modifier, ) { // Removing 1 to get the real number of bars Loading @@ -95,7 +102,11 @@ private fun StackedMobileIcon( val iconSize = with(LocalDensity.current) { dimensions.totalWidth.toDp() to IconHeightSp.toDp() } Canvas(modifier.width(iconSize.first).height(iconSize.second)) { Canvas( modifier.width(iconSize.first).height(iconSize.second).semantics { contentDescription?.let { this.contentDescription = it } } ) { val verticalPaddingPx = BarsVerticalPaddingSp.roundToPx() val horizontalPaddingPx = dimensions.barsHorizontalPadding.roundToPx() val totalPaddingWidthPx = horizontalPaddingPx * (numberOfBars - 1) Loading