diff options
6 files changed, 256 insertions, 60 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 038722cd9608..bf1fbad074cd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -20,8 +20,6 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH -import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons.G import com.android.settingslib.mobile.TelephonyIcons.THREE_G @@ -40,12 +38,15 @@ import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesF import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository.Companion.DEFAULT_NETWORK_NAME import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl 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.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -255,59 +256,146 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun contentDescription_notInService_usesNoPhone() = testScope.runTest { - var latest: ContentDescription? = null - val job = underTest.contentDescription.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.contentDescription) repository.isInService.value = false - assertThat((latest as ContentDescription.Resource).res) - .isEqualTo(PHONE_SIGNAL_STRENGTH_NONE) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } - job.cancel() + @Test + fun contentDescription_includesNetworkName() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.isInService.value = true + repository.networkName.value = NetworkNameModel.SubscriptionDerived("Test Network Name") + repository.numberOfLevels.value = 5 + repository.setAllLevels(3) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular("Test Network Name", THREE_BARS)) } @Test fun contentDescription_inService_usesLevel() = testScope.runTest { - var latest: ContentDescription? = null - val job = underTest.contentDescription.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.contentDescription) repository.setAllLevels(2) - assertThat((latest as ContentDescription.Resource).res) - .isEqualTo(PHONE_SIGNAL_STRENGTH[2]) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) repository.setAllLevels(0) - assertThat((latest as ContentDescription.Resource).res) - .isEqualTo(PHONE_SIGNAL_STRENGTH[0]) - job.cancel() + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) } @Test - fun contentDescription_nonInflated_invalidLevelIsNull() = + fun contentDescription_nonInflated_invalidLevelUsesNoSignalText() = testScope.runTest { val latest by collectLastValue(underTest.contentDescription) repository.inflateSignalStrength.value = false repository.setAllLevels(-1) - assertThat(latest).isNull() + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) repository.setAllLevels(100) - assertThat(latest).isNull() + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } + + @Test + fun contentDescription_nonInflated_levelStrings() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = false + repository.setAllLevels(0) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + + repository.setAllLevels(1) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) + + repository.setAllLevels(2) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + + repository.setAllLevels(3) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) + + repository.setAllLevels(4) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) } @Test - fun contentDescription_inflated_invalidLevelIsNull() = + fun contentDescription_inflated_invalidLevelUsesNoSignalText() = testScope.runTest { val latest by collectLastValue(underTest.contentDescription) repository.inflateSignalStrength.value = true repository.numberOfLevels.value = 6 + repository.setAllLevels(-2) - assertThat(latest).isNull() + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) repository.setAllLevels(100) - assertThat(latest).isNull() + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } + + @Test + fun contentDescription_inflated_levelStrings() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = true + repository.numberOfLevels.value = 6 + + // Note that the _repo_ level is 1 lower than the reported level through the interactor + + repository.setAllLevels(0) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) + + repository.setAllLevels(1) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + + repository.setAllLevels(2) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) + + repository.setAllLevels(3) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FOUR_BARS)) + + repository.setAllLevels(4) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) } @Test @@ -323,7 +411,10 @@ class MobileIconViewModelTest : SysuiTestCase() { repository.setAllLevels(i) when (i) { -1, - 5 -> assertWithMessage("Level $i is expected to be null").that(latest).isNull() + 5 -> + assertWithMessage("Level $i is expected to be 'no signal'") + .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) + .isEqualTo(NO_SIGNAL) else -> assertWithMessage("Level $i is expected not to be null") .that(latest) @@ -344,7 +435,10 @@ class MobileIconViewModelTest : SysuiTestCase() { repository.setAllLevels(i) when (i) { -2, - 5 -> assertWithMessage("Level $i is expected to be null").that(latest).isNull() + 5 -> + assertWithMessage("Level $i is expected to be 'no signal'") + .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) + .isEqualTo(NO_SIGNAL) else -> assertWithMessage("Level $i is not expected to be null") .that(latest) @@ -967,5 +1061,13 @@ class MobileIconViewModelTest : SysuiTestCase() { companion object { private const val SUB_1_ID = 1 + + // For convenience, just define these as constants + private val NO_SIGNAL = R.string.accessibility_no_signal + private val ONE_BAR = R.string.accessibility_one_bar + private val TWO_BARS = R.string.accessibility_two_bars + private val THREE_BARS = R.string.accessibility_three_bars + private val FOUR_BARS = R.string.accessibility_four_bars + private val FULL_BARS = R.string.accessibility_signal_full } } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index fac2c4a5a9c2..c78350dfac77 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1945,6 +1945,21 @@ <!-- Text displayed indicating that the user might be able to use satellite SOS. --> <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS</string> + <!-- Content description skeleton. Input strings should be carrier name and signal bar description [CHAR LIMIT=NONE]--> + <string name="accessibility_phone_string_format"><xliff:g id="carrier_name" example="Carrier Name">%1$s</xliff:g>, <xliff:g id="signal_strength_description" example="two bars">%2$s</xliff:g>.</string> + <!-- Content description describing 0 signal bars. [CHAR LIMIT=NONE] --> + <string name="accessibility_no_signal">no signal</string> + <!-- Content description describing 1 signal bar. [CHAR LIMIT=NONE] --> + <string name="accessibility_one_bar">one bar</string> + <!-- Content description describing 2 signal bars. [CHAR LIMIT=NONE] --> + <string name="accessibility_two_bars">two bars</string> + <!-- Content description describing 3 signal bars. [CHAR LIMIT=NONE] --> + <string name="accessibility_three_bars">three bars</string> + <!-- Content description describing 4 signal bars. [CHAR LIMIT=NONE] --> + <string name="accessibility_four_bars">four bars</string> + <!-- Content description describing full signal bars. [CHAR LIMIT=NONE] --> + <string name="accessibility_signal_full">signal full</string> + <!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] --> <string name="accessibility_managed_profile">Work profile</string> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileContentDescriptionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileContentDescriptionViewBinder.kt new file mode 100644 index 000000000000..c720b1df1e62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileContentDescriptionViewBinder.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.binder + +import android.view.View +import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription + +object MobileContentDescriptionViewBinder { + fun bind(contentDescription: MobileContentDescription?, view: View) { + view.contentDescription = + when (contentDescription) { + null -> null + else -> contentDescription.loadContentDescription(view.context) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 31d349eb4cca..788f041b38c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -29,9 +29,9 @@ import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.settingslib.graph.SignalDrawable import com.android.systemui.Flags.statusBarStaticInoutIndicators -import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.DarkIconDispatcher @@ -48,12 +48,8 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBin import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import com.android.app.tracing.coroutines.launchTraced as launch -private data class Colors( - @ColorInt val tint: Int, - @ColorInt val contrast: Int, -) +private data class Colors(@ColorInt val tint: Int, @ColorInt val contrast: Int) object MobileIconBinder { /** Binds the view to the view-model, continuing to update the former based on the latter */ @@ -87,7 +83,7 @@ object MobileIconBinder { MutableStateFlow( Colors( tint = DarkIconDispatcher.DEFAULT_ICON_TINT, - contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT + contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT, ) ) val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) @@ -105,7 +101,7 @@ object MobileIconBinder { viewModel.verboseLogger?.logBinderReceivedVisibility( view, viewModel.subscriptionId, - isVisible + isVisible, ) view.isVisible = isVisible // [StatusIconContainer] can get out of sync sometimes. Make sure to @@ -152,7 +148,7 @@ object MobileIconBinder { launch { viewModel.contentDescription.distinctUntilChanged().collect { - ContentDescriptionViewBinder.bind(it, view) + MobileContentDescriptionViewBinder.bind(it, view) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/MobileContentDescription.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/MobileContentDescription.kt new file mode 100644 index 000000000000..84fa07379a49 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/MobileContentDescription.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.model + +import android.annotation.StringRes +import android.content.Context +import com.android.systemui.res.R + +sealed interface MobileContentDescription { + fun loadContentDescription(context: Context): String + + /** + * Content description for cellular parameterizes the [networkName] which comes from the system + */ + data class Cellular(val networkName: String, @StringRes val levelDescriptionRes: Int) : + MobileContentDescription { + override fun loadContentDescription(context: Context): String = + context.getString( + R.string.accessibility_phone_string_format, + networkName, + context.getString(levelDescriptionRes), + ) + } + + data class SatelliteContentDescription(@StringRes val resId: Int) : MobileContentDescription { + override fun loadContentDescription(context: Context): String = + context.getString(this.resId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 103b0e3a6f27..0bd3426712bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import com.android.settingslib.AccessibilityContentDescriptions import com.android.systemui.Flags.statusBarStaticInoutIndicators import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -28,6 +27,7 @@ import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.Airpla import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor 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.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.CoroutineScope @@ -50,7 +50,7 @@ interface MobileIconViewModelCommon { /** True if this view should be visible at all. */ val isVisible: StateFlow<Boolean> val icon: Flow<SignalIconModel> - val contentDescription: Flow<ContentDescription?> + val contentDescription: Flow<MobileContentDescription?> val roaming: Flow<Boolean> /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: Flow<Icon.Resource?> @@ -95,10 +95,7 @@ class MobileIconViewModel( } private val satelliteProvider by lazy { - CarrierBasedSatelliteViewModelImpl( - subscriptionId, - iconInteractor, - ) + CarrierBasedSatelliteViewModelImpl(subscriptionId, iconInteractor) } /** @@ -123,7 +120,7 @@ class MobileIconViewModel( override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon } - override val contentDescription: Flow<ContentDescription?> = + override val contentDescription: Flow<MobileContentDescription?> = vmProvider.flatMapLatest { it.contentDescription } override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming } @@ -154,8 +151,7 @@ private class CarrierBasedSatelliteViewModelImpl( override val isVisible: StateFlow<Boolean> = MutableStateFlow(true) override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon - override val contentDescription: Flow<ContentDescription> = - MutableStateFlow(ContentDescription.Loaded("")) + override val contentDescription: Flow<MobileContentDescription?> = MutableStateFlow(null) /** These fields are not used for satellite icons currently */ override val roaming: Flow<Boolean> = flowOf(false) @@ -206,27 +202,42 @@ private class CellularIconViewModel( override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon - override val contentDescription: Flow<ContentDescription?> = - iconInteractor.signalLevelIcon - .map { - // We expect the signal icon to be cellular here since this is the cellular vm - if (it !is SignalIconModel.Cellular) { - null - } else { - val resId = - AccessibilityContentDescriptions.getDescriptionForLevel( - it.level, - it.numberOfLevels + override val contentDescription: Flow<MobileContentDescription?> = + combine(iconInteractor.signalLevelIcon, iconInteractor.networkName) { icon, nameModel -> + when (icon) { + is SignalIconModel.Cellular -> + MobileContentDescription.Cellular( + nameModel.name, + icon.levelDescriptionRes(), ) - if (resId != 0) { - ContentDescription.Resource(resId) - } else { - null - } + else -> null } } .stateIn(scope, SharingStarted.WhileSubscribed(), null) + private fun SignalIconModel.Cellular.levelDescriptionRes() = + when (level) { + 0 -> R.string.accessibility_no_signal + 1 -> R.string.accessibility_one_bar + 2 -> R.string.accessibility_two_bars + 3 -> R.string.accessibility_three_bars + 4 -> { + if (numberOfLevels == 6) { + R.string.accessibility_four_bars + } else { + R.string.accessibility_signal_full + } + } + 5 -> { + if (numberOfLevels == 6) { + R.string.accessibility_signal_full + } else { + R.string.accessibility_no_signal + } + } + else -> R.string.accessibility_no_signal + } + private val showNetworkTypeIcon: Flow<Boolean> = combine( iconInteractor.isDataConnected, @@ -248,10 +259,9 @@ private class CellularIconViewModel( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val networkTypeIcon: Flow<Icon.Resource?> = - combine( - iconInteractor.networkTypeIconGroup, - showNetworkTypeIcon, - ) { networkTypeIconGroup, shouldShow -> + combine(iconInteractor.networkTypeIconGroup, showNetworkTypeIcon) { + networkTypeIconGroup, + shouldShow -> val desc = if (networkTypeIconGroup.contentDescription != 0) ContentDescription.Resource(networkTypeIconGroup.contentDescription) |