diff options
| author | 2025-02-12 12:52:56 -0800 | |
|---|---|---|
| committer | 2025-02-12 12:52:56 -0800 | |
| commit | 982cee253d6ad96d502ca87c0133c5ba8a2c95e5 (patch) | |
| tree | 3e74c9194cc2c2a8bae5115d2d95abd0f76a8e2d | |
| parent | bec48ec2971cbd67124a2ec0ee120f5e1f70b384 (diff) | |
| parent | 27980c603890502fe9eb70557f77128323670ef1 (diff) | |
Merge "Initial implementation for dual sim icon" into main
14 files changed, 641 insertions, 18 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 17acf9aed278..1a311d572e0b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -60,6 +60,7 @@ <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item> <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item> <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item> + <item><xliff:g id="id">@string/status_bar_stacked_mobile</xliff:g></item> <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item> <item><xliff:g id="id">@string/status_bar_airplane</xliff:g></item> <item><xliff:g id="id">@string/status_bar_battery</xliff:g></item> @@ -94,6 +95,7 @@ <string translatable="false" name="status_bar_secure">secure</string> <string translatable="false" name="status_bar_clock">clock</string> <string translatable="false" name="status_bar_mobile">mobile</string> + <string translatable="false" name="status_bar_stacked_mobile">stacked_mobile</string> <string translatable="false" name="status_bar_vpn">vpn</string> <string translatable="false" name="status_bar_ethernet">ethernet</string> <string translatable="false" name="status_bar_microphone">microphone</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 63db28683b2b..c62732d36038 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3320,6 +3320,7 @@ <java-symbol type="string" name="status_bar_no_calling" /> <java-symbol type="string" name="status_bar_call_strength" /> <java-symbol type="string" name="status_bar_mobile" /> + <java-symbol type="string" name="status_bar_stacked_mobile" /> <java-symbol type="string" name="status_bar_ethernet" /> <java-symbol type="string" name="status_bar_vpn" /> <java-symbol type="string" name="status_bar_microphone" /> diff --git a/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml new file mode 100644 index 000000000000..fa9318bc151c --- /dev/null +++ b/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2023 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. + --> + +<!-- Base layout that provides a single bindable compose view --> +<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + > + + <androidx.compose.ui.platform.ComposeView + android:id="@+id/compose_view" + android:layout_height="@dimen/status_bar_bindable_icon_size" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical" + android:padding="4sp" + /> + +</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt index 8400fb08e147..701dae1594f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.icons.shared import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.mobile.ui.StackedMobileBindableIcon import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon import javax.inject.Inject @@ -40,11 +41,12 @@ class BindableIconsRegistryImpl @Inject constructor( /** Bindables go here */ - oemSatellite: DeviceBasedSatelliteBindableIcon + oemSatellite: DeviceBasedSatelliteBindableIcon, + stackedMobile: StackedMobileBindableIcon, ) : BindableIconsRegistry { /** * Adding the injected bindables to this list will get them registered with * StatusBarIconController */ - override val bindableIcons: List<BindableIcon> = listOf(oemSatellite) + override val bindableIcons: List<BindableIcon> = listOf(oemSatellite, stackedMobile) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index ac3728d9dcaf..c52536d2b312 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -21,6 +21,7 @@ import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.flags.Flags import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -28,6 +29,7 @@ import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository @@ -39,6 +41,7 @@ import com.android.systemui.util.CarrierConfigTracker import java.lang.ref.WeakReference import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -79,6 +82,9 @@ interface MobileIconsInteractor { */ val icons: StateFlow<List<MobileIconInteractor>> + /** Whether the mobile icons can be stacked vertically. */ + val isStackable: StateFlow<Boolean> + /** True if the active mobile data subscription has data enabled */ val activeDataConnectionHasDataEnabled: StateFlow<Boolean> @@ -126,6 +132,7 @@ interface MobileIconsInteractor { fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor } +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SysUISingleton class MobileIconsInteractorImpl @@ -290,6 +297,18 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + override val isStackable = + if (Flags.newStatusBarIcons() && StatusBarRootModernization.isEnabled) { + icons.flatMapLatest { icons -> + combine(icons.map { it.isNonTerrestrial }) { + it.size == 2 && it.none { isNonTerrestrial -> isNonTerrestrial } + } + } + } else { + flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the * validated bit from the old active network (A) while data is changing to the new one (B). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index 30cc2c5da994..abd543d78687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -27,7 +28,7 @@ import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.combine /** * This class is intended to provide a context to collect on the @@ -56,12 +57,23 @@ constructor( // Start notifying the icon controller of subscriptions scope.launch { isCollecting = true - mobileIconsViewModel.subscriptionIdsFlow.collectLatest { - logger.logUiAdapterSubIdsSentToIconController(it) - lastValue = it - iconController.setNewMobileIconSubIds(it) - shadeCarrierGroupController?.updateModernMobileIcons(it) - } + combine( + mobileIconsViewModel.subscriptionIdsFlow, + mobileIconsViewModel.isStackable, + ::Pair, + ) + .collectLatest { (subIds, isStackable) -> + logger.logUiAdapterSubIdsSentToIconController(subIds, isStackable) + lastValue = subIds + if (isStackable) { + // Passing an empty list to remove pre-existing mobile icons. + // StackedMobileBindableIcon will show the stacked icon instead. + iconController.setNewMobileIconSubIds(emptyList()) + } else { + iconController.setNewMobileIconSubIds(subIds) + } + shadeCarrierGroupController?.updateModernMobileIcons(subIds) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt index 2af6795b39c4..4c2849de34ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -31,22 +31,24 @@ import javax.inject.Inject @SysUISingleton class MobileViewLogger @Inject -constructor( - @MobileViewLog private val buffer: LogBuffer, - dumpManager: DumpManager, -) : Dumpable { +constructor(@MobileViewLog private val buffer: LogBuffer, dumpManager: DumpManager) : Dumpable { init { dumpManager.registerNormalDumpable(this) } private val collectionStatuses = mutableMapOf<String, Boolean>() - fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) { + fun logUiAdapterSubIdsSentToIconController(subs: List<Int>, isStackable: Boolean) { buffer.log( TAG, LogLevel.INFO, - { str1 = subs.toString() }, - { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" }, + { + str1 = subs.toString() + bool1 = isStackable + }, + { + "Sub IDs in MobileUiAdapter being sent to icon controller: $str1, isStackable=$bool1" + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt new file mode 100644 index 000000000000..fa9fa4c1366f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt @@ -0,0 +1,52 @@ +/* + * 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.pipeline.mobile.ui + +import android.content.Context +import com.android.settingslib.flags.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.StackedMobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView +import javax.inject.Inject + +@SysUISingleton +class StackedMobileBindableIcon +@Inject +constructor( + context: Context, + mobileIconsViewModel: MobileIconsViewModel, + viewModelFactory: StackedMobileIconViewModel.Factory, +) : BindableIcon { + override val slot: String = + context.getString(com.android.internal.R.string.status_bar_stacked_mobile) + + override val initializer = ModernStatusBarViewCreator { context -> + SingleBindableStatusBarComposeIconView.createView(context).also { view -> + view.initView(slot) { + StackedMobileIconBinder.bind(view, mobileIconsViewModel, viewModelFactory) + } + } + } + + override val shouldBindIcon: Boolean = + Flags.newStatusBarIcons() && StatusBarRootModernization.isEnabled +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt new file mode 100644 index 000000000000..c9fc53ecadc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt @@ -0,0 +1,65 @@ +/* + * 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.pipeline.mobile.ui.binder + +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIcon +import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView + +object StackedMobileIconBinder { + fun bind( + view: SingleBindableStatusBarComposeIconView, + mobileIconsViewModel: MobileIconsViewModel, + viewModelFactory: StackedMobileIconViewModel.Factory, + ): ModernStatusBarViewBinding { + return SingleBindableStatusBarComposeIconView.withDefaultBinding( + view = view, + shouldBeVisible = { mobileIconsViewModel.isStackable.value }, + ) { _, tint -> + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + view.composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + val viewModel = + rememberViewModel("StackedMobileIconBinder") { + viewModelFactory.create() + } + if (viewModel.isIconVisible) { + CompositionLocalProvider(LocalContentColor provides Color(tint())) { + StackedMobileIcon(viewModel) + } + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 22feb7ce77c8..6176a3e9e281 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -70,15 +70,20 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) - private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = + val mobileSubViewModels: StateFlow<List<MobileIconViewModelCommon>> = subscriptionIdsFlow + .map { ids -> ids.map { commonViewModelForSub(it) } } + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + + private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = + mobileSubViewModels .map { if (it.isEmpty()) { null } else { // Mobile icons get reversed by [StatusBarIconController], so the last element // in this list will show up visually first. - commonViewModelForSub(it.last()) + it.last() } } .stateIn(scope, SharingStarted.WhileSubscribed(), null) @@ -94,6 +99,8 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + val isStackable: StateFlow<Boolean> = interactor.isStackable + init { scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt new file mode 100644 index 000000000000..a2c2a3cd1507 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt @@ -0,0 +1,90 @@ +/* + * 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.pipeline.mobile.ui.viewmodel + +import androidx.compose.runtime.derivedStateOf +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.statusbar.pipeline.mobile.domain.model.SignalIconModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +@OptIn(ExperimentalCoroutinesApi::class) +class StackedMobileIconViewModel +@AssistedInject +constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() { + private val hydrator = Hydrator("StackedMobileIconViewModel") + + private val isStackable: Boolean by + hydrator.hydratedStateOf( + traceName = "isStackable", + source = mobileIconsViewModel.isStackable, + initialValue = false, + ) + + private val iconViewModelFlow: StateFlow<List<MobileIconViewModelCommon>> = + mobileIconsViewModel.mobileSubViewModels + + val dualSim: DualSim? by + hydrator.hydratedStateOf( + traceName = "dualSim", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + combine(viewModels.map { it.icon }) { icons -> + icons + .toList() + .filterIsInstance<SignalIconModel.Cellular>() + .takeIf { it.size == 2 } + ?.let { DualSim(it[0], it[1]) } + } + }, + initialValue = null, + ) + + val networkTypeIcon: Icon.Resource? by + hydrator.hydratedStateOf( + traceName = "networkTypeIcon", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null) + }, + initialValue = null, + ) + + val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + @AssistedFactory + interface Factory { + fun create(): StackedMobileIconViewModel + } + + data class DualSim( + val primary: SignalIconModel.Cellular, + val secondary: SignalIconModel.Cellular, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt new file mode 100644 index 000000000000..465a43fbfb9e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt @@ -0,0 +1,180 @@ +/* + * 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.pipeline.shared.ui.composable + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarBaseHeightFiveBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarBaseHeightFourBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarsLevelIncrementSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarsVerticalPaddingSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.HorizontalPaddingFiveBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.HorizontalPaddingFourBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconHeightSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconWidthFiveBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconWidthFourBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.SecondaryBarHeightSp +import kotlin.math.max + +/** + * The dual sim icon that shows both connections stacked vertically with the active connection on + * top + */ +@Composable +fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier = Modifier) { + val dualSim = viewModel.dualSim ?: return + + val contentColor = LocalContentColor.current + + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + viewModel.networkTypeIcon?.let { + Icon( + it, + tint = contentColor, + modifier = + Modifier.height { IconHeightSp.roundToPx() }.padding(start = 1.dp, end = 2.dp), + ) + } + + StackedMobileIcon(dualSim, contentColor) + } +} + +@Composable +private fun StackedMobileIcon( + viewModel: StackedMobileIconViewModel.DualSim, + color: Color, + modifier: Modifier = Modifier, +) { + val maxNumberOfLevels = + max(viewModel.primary.numberOfLevels, viewModel.secondary.numberOfLevels) + val dimensions = if (maxNumberOfLevels == 6) FiveBarsDimensions else FourBarsDimensions + val iconSize = + with(LocalDensity.current) { dimensions.totalWidth.toDp() to IconHeightSp.toDp() } + + Canvas(modifier.size(width = iconSize.first, height = iconSize.second)) { + val verticalPaddingPx = BarsVerticalPaddingSp.roundToPx() + val horizontalPaddingPx = dimensions.barsHorizontalPadding.roundToPx() + val totalPaddingWidthPx = horizontalPaddingPx * (maxNumberOfLevels - 1) + + val barWidthPx = (size.width - totalPaddingWidthPx) / maxNumberOfLevels + val dotHeightPx = SecondaryBarHeightSp.toPx() + val baseBarHeightPx = dimensions.barBaseHeight.toPx() + + var xOffsetPx = 0f + for (bar in 1..maxNumberOfLevels) { + // Bottom dots representing secondary sim + val dotYOffsetPx = size.height - dotHeightPx + if (bar <= viewModel.secondary.numberOfLevels) { + drawMobileIconBar( + level = viewModel.secondary.level, + bar = bar, + topLeft = Offset(xOffsetPx, dotYOffsetPx), + size = Size(barWidthPx, dotHeightPx), + activeColor = color, + ) + } + + // Top bars representing primary sim + if (bar <= viewModel.primary.numberOfLevels) { + val barHeightPx = baseBarHeightPx + (BarsLevelIncrementSp.toPx() * (bar - 1)) + val barYOffsetPx = dotYOffsetPx - verticalPaddingPx - barHeightPx + drawMobileIconBar( + level = viewModel.primary.level, + bar = bar, + topLeft = Offset(xOffsetPx, barYOffsetPx), + size = Size(barWidthPx, barHeightPx), + activeColor = color, + ) + } + + xOffsetPx += barWidthPx + horizontalPaddingPx + } + } +} + +private fun DrawScope.drawMobileIconBar( + level: Int, + bar: Int, + topLeft: Offset, + size: Size, + activeColor: Color, + inactiveColor: Color = activeColor.copy(alpha = .3f), + cornerRadius: CornerRadius = CornerRadius(size.width / 2), +) { + drawRoundRect( + color = if (level >= bar) activeColor else inactiveColor, + topLeft = topLeft, + size = size, + cornerRadius = cornerRadius, + ) +} + +private abstract class BarsDependentDimensions( + val totalWidth: TextUnit, + val barsHorizontalPadding: TextUnit, + val barBaseHeight: TextUnit, +) + +private object FourBarsDimensions : + BarsDependentDimensions( + IconWidthFourBarsSp, + HorizontalPaddingFourBarsSp, + BarBaseHeightFourBarsSp, + ) + +private object FiveBarsDimensions : + BarsDependentDimensions( + IconWidthFiveBarsSp, + HorizontalPaddingFiveBarsSp, + BarBaseHeightFiveBarsSp, + ) + +private object StackedMobileIconDimensions { + // Common dimensions + val IconHeightSp = 12.sp + val BarsVerticalPaddingSp = 1.5.sp + val BarsLevelIncrementSp = 1.sp + val SecondaryBarHeightSp = 3.sp + + // Dimensions dependant on the number of total bars + val IconWidthFiveBarsSp = 18.5.sp + val IconWidthFourBarsSp = 16.sp + val HorizontalPaddingFiveBarsSp = 1.5.sp + val HorizontalPaddingFourBarsSp = 2.sp + val BarBaseHeightFiveBarsSp = 3.5.sp + val BarBaseHeightFourBarsSp = 4.5.sp +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt new file mode 100644 index 000000000000..8076040564fb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 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.pipeline.shared.ui.view + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.MutableStateFlow + +/** Compose view that is bound to bindable_status_bar_compose_icon.xml */ +class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeSet?) : + ModernStatusBarView(context, attrs) { + + internal lateinit var composeView: ComposeView + internal lateinit var dotView: StatusBarIconView + + override fun toString(): String { + return "SingleBindableStatusBarComposeIcon(" + + "slot='$slot', " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + + override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + super.initView(slot, bindingCreator) + + composeView = requireViewById(R.id.compose_view) + dotView = requireViewById(R.id.status_bar_dot) + } + + companion object { + fun createView(context: Context): SingleBindableStatusBarComposeIconView { + return LayoutInflater.from(context) + .inflate(R.layout.bindable_status_bar_compose_icon, null) + as SingleBindableStatusBarComposeIconView + } + + /** + * Using a given binding [block], create the necessary scaffolding to handle the general + * case of a single status bar icon. This includes eliding into a dot view when there is not + * enough space, and handling tint. + * + * [block] should be a simple [launch] call that handles updating the single icon view with + * its new view. Currently there is no simple way to e.g., extend to handle multiple tints + * for dual-layered icons, and any more complex logic should probably find a way to return + * its own version of [ModernStatusBarViewBinding]. + */ + fun withDefaultBinding( + view: SingleBindableStatusBarComposeIconView, + shouldBeVisible: () -> Boolean, + block: suspend LifecycleOwner.(View, () -> Int) -> Unit, + ): ModernStatusBarViewBinding { + @StatusBarIconView.VisibleState + val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) + + val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) + val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) + + var isCollecting: Boolean = false + + view.repeatWhenAttached { + // Child binding + block(view) { iconTint.value } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // 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 + launch { + visibilityState.collect { visibilityState -> + // for b/296864006, we can not hide all the child views if + // visibilityState is STATE_HIDDEN. Because hiding all child views + // would cause the getWidth() of this view return 0, and that would + // cause the translation calculation fails in StatusIconContainer. + // Therefore, like class MobileIconBinder, instead of set the child + // views visibility to View.GONE, we set their visibility to + // View.INVISIBLE to make them invisible but keep the width. + ModernStatusBarViewVisibilityHelper.setVisibilityState( + visibilityState, + view.composeView, + view.dotView, + ) + } + } + + launch { iconTint.collect { tint -> view.dotView.setDecorColor(tint) } } + + launch { + decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) } + } + + try { + awaitCancellation() + } finally { + isCollecting = false + } + } + } + } + + return object : ModernStatusBarViewBinding { + override fun getShouldIconBeVisible(): Boolean { + return shouldBeVisible() + } + + override fun onVisibilityStateChanged(state: Int) { + visibilityState.value = state + } + + override fun onIconTintChanged(newTint: Int, contrastTint: Int) { + iconTint.value = newTint + } + + override fun onDecorTintChanged(newTint: Int) { + decorTint.value = newTint + } + + override fun isCollecting(): Boolean { + return isCollecting + } + } + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 352f6cf011e1..9b6f205fba72 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,6 +26,7 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class FakeMobileIconsInteractor( mobileMappings: MobileMappingsProxy, @@ -73,6 +74,8 @@ class FakeMobileIconsInteractor( override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList()) + override val isStackable: StateFlow<Boolean> = MutableStateFlow(false) + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping |