From 7ca1ceb722c031bdfcf6d052dec375424749c2bb Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 9 Apr 2024 14:03:04 +0800 Subject: [PATCH] Use DataUsageFormatter to format app data usage Use the new unitsContentDescription from Formatter.formatBytes() Fix: 318780411 Test: manual - on AppDataUsage Test: unit test Change-Id: I55079c83db2e46a48f49f746f2371825ec0bb029 --- .../AppDataUsageSummaryController.kt | 27 +++++--- .../datausage/BillingCycleSettings.java | 5 +- .../settings/datausage/DataUsageFormatter.kt | 32 ---------- .../settings/datausage/DataUsageList.kt | 2 +- .../settings/datausage/DataUsageUtils.java | 3 + .../datausage/lib/DataUsageFormatter.kt | 64 +++++++++++++++++++ .../datausage/lib/NetworkUsageData.kt | 9 +-- .../DataUsagePreferenceController.kt | 14 ++-- .../spa/app/appinfo/AppDataUsagePreference.kt | 2 +- .../AppDataUsageSummaryControllerTest.kt | 34 ++++++++-- .../{ => lib}/DataUsageFormatterTest.kt | 34 ++++++++-- 11 files changed, 161 insertions(+), 65 deletions(-) delete mode 100644 src/com/android/settings/datausage/DataUsageFormatter.kt create mode 100644 src/com/android/settings/datausage/lib/DataUsageFormatter.kt rename tests/spa_unit/src/com/android/settings/datausage/{ => lib}/DataUsageFormatterTest.kt (59%) diff --git a/src/com/android/settings/datausage/AppDataUsageSummaryController.kt b/src/com/android/settings/datausage/AppDataUsageSummaryController.kt index a764c1d6cfe..233e1078812 100644 --- a/src/com/android/settings/datausage/AppDataUsageSummaryController.kt +++ b/src/com/android/settings/datausage/AppDataUsageSummaryController.kt @@ -23,11 +23,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R +import com.android.settings.datausage.lib.DataUsageFormatter import com.android.settings.datausage.lib.NetworkUsageDetailsData import com.android.settings.spa.preference.ComposePreferenceController import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spaprivileged.framework.compose.placeholder +import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -35,17 +36,20 @@ class AppDataUsageSummaryController(context: Context, preferenceKey: String) : ComposePreferenceController(context, preferenceKey) { private val dataFlow = MutableStateFlow(NetworkUsageDetailsData.AllZero) + private val dataUsageFormatter = DataUsageFormatter(context) + private val emptyDataUsage = + DataUsageFormatter.FormattedDataUsage(context.getPlaceholder(), context.getPlaceholder()) private val totalUsageFlow = dataFlow.map { - DataUsageUtils.formatDataUsage(mContext, it.totalUsage).toString() + dataUsageFormatter.formatDataUsage(it.totalUsage) } private val foregroundUsageFlow = dataFlow.map { - DataUsageUtils.formatDataUsage(mContext, it.foregroundUsage).toString() + dataUsageFormatter.formatDataUsage(it.foregroundUsage) } private val backgroundUsageFlow = dataFlow.map { - DataUsageUtils.formatDataUsage(mContext, it.backgroundUsage).toString() + dataUsageFormatter.formatDataUsage(it.backgroundUsage) } override fun getAvailabilityStatus() = AVAILABLE @@ -57,20 +61,23 @@ class AppDataUsageSummaryController(context: Context, preferenceKey: String) : @Composable override fun Content() { Column { - val totalUsage by totalUsageFlow.collectAsStateWithLifecycle(placeholder()) - val foregroundUsage by foregroundUsageFlow.collectAsStateWithLifecycle(placeholder()) - val backgroundUsage by backgroundUsageFlow.collectAsStateWithLifecycle(placeholder()) + val totalUsage by totalUsageFlow.collectAsStateWithLifecycle(emptyDataUsage) + val foregroundUsage by foregroundUsageFlow.collectAsStateWithLifecycle(emptyDataUsage) + val backgroundUsage by backgroundUsageFlow.collectAsStateWithLifecycle(emptyDataUsage) Preference(object : PreferenceModel { override val title = stringResource(R.string.total_size_label) - override val summary = { totalUsage } + override val summary = { totalUsage.displayText } + override val summaryContentDescription = { totalUsage.contentDescription } }) Preference(object : PreferenceModel { override val title = stringResource(R.string.data_usage_label_foreground) - override val summary = { foregroundUsage } + override val summary = { foregroundUsage.displayText } + override val summaryContentDescription = { foregroundUsage.contentDescription } }) Preference(object : PreferenceModel { override val title = stringResource(R.string.data_usage_label_background) - override val summary = { backgroundUsage } + override val summary = { backgroundUsage.displayText } + override val summaryContentDescription = { backgroundUsage.contentDescription } }) } } diff --git a/src/com/android/settings/datausage/BillingCycleSettings.java b/src/com/android/settings/datausage/BillingCycleSettings.java index 9a7411a7347..69577a85574 100644 --- a/src/com/android/settings/datausage/BillingCycleSettings.java +++ b/src/com/android/settings/datausage/BillingCycleSettings.java @@ -44,6 +44,7 @@ import androidx.preference.TwoStatePreference; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.datausage.lib.DataUsageFormatter; import com.android.settings.datausage.lib.NetworkTemplates; import com.android.settings.network.SubscriptionUtil; import com.android.settings.network.telephony.MobileNetworkUtils; @@ -322,8 +323,8 @@ public class BillingCycleSettings extends DataUsageBaseFragment implements : editor.getPolicyWarningBytes(template); final String[] unitNames = new String[] { - DataUsageFormatter.INSTANCE.getBytesDisplayUnit(getResources(), MIB_IN_BYTES), - DataUsageFormatter.INSTANCE.getBytesDisplayUnit(getResources(), GIB_IN_BYTES), + DataUsageFormatter.Companion.getBytesDisplayUnit(getResources(), MIB_IN_BYTES), + DataUsageFormatter.Companion.getBytesDisplayUnit(getResources(), GIB_IN_BYTES), }; final ArrayAdapter adapter = new ArrayAdapter( getContext(), android.R.layout.simple_spinner_item, unitNames); diff --git a/src/com/android/settings/datausage/DataUsageFormatter.kt b/src/com/android/settings/datausage/DataUsageFormatter.kt deleted file mode 100644 index 16a9ae8b6b0..00000000000 --- a/src/com/android/settings/datausage/DataUsageFormatter.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.settings.datausage - -import android.content.res.Resources -import android.text.format.Formatter - -object DataUsageFormatter { - - /** - * Gets the display unit of the given bytes. - * - * Similar to MeasureFormat.getUnitDisplayName(), but with the expected result for the bytes in - * Settings, and align with other places in Settings. - */ - fun Resources.getBytesDisplayUnit(bytes: Long): String = - Formatter.formatBytes(this, bytes, Formatter.FLAG_IEC_UNITS).units -} \ No newline at end of file diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt index a8f5460a18c..af115d9d370 100644 --- a/src/com/android/settings/datausage/DataUsageList.kt +++ b/src/com/android/settings/datausage/DataUsageList.kt @@ -178,7 +178,7 @@ open class DataUsageList : DashboardFragment() { private fun updateSelectedCycle(usageData: NetworkUsageData) { Log.d(TAG, "showing cycle $usageData") - usageAmount?.title = usageData.getDataUsedString(requireContext()) + usageAmount?.title = usageData.getDataUsedString(requireContext()).displayText viewModel.selectedCycleFlow.value = usageData updateApps(usageData) diff --git a/src/com/android/settings/datausage/DataUsageUtils.java b/src/com/android/settings/datausage/DataUsageUtils.java index 2bbf3e2a462..b73da1c3ada 100644 --- a/src/com/android/settings/datausage/DataUsageUtils.java +++ b/src/com/android/settings/datausage/DataUsageUtils.java @@ -56,7 +56,10 @@ public final class DataUsageUtils { /** * Format byte value to readable string using IEC units. + * + * @deprecated Use {@link com.android.settings.datausage.lib.DataUsageFormatter} instead. */ + @Deprecated public static CharSequence formatDataUsage(Context context, long byteValue) { final BytesResult res = Formatter.formatBytes(context.getResources(), byteValue, Formatter.FLAG_IEC_UNITS); diff --git a/src/com/android/settings/datausage/lib/DataUsageFormatter.kt b/src/com/android/settings/datausage/lib/DataUsageFormatter.kt new file mode 100644 index 00000000000..0a4c06b264b --- /dev/null +++ b/src/com/android/settings/datausage/lib/DataUsageFormatter.kt @@ -0,0 +1,64 @@ +/* + * 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.settings.datausage.lib + +import android.annotation.StringRes +import android.content.Context +import android.content.res.Resources +import android.icu.text.UnicodeSet +import android.icu.text.UnicodeSetSpanner +import android.text.BidiFormatter +import android.text.format.Formatter +import com.android.internal.R + +class DataUsageFormatter(private val context: Context) { + + data class FormattedDataUsage( + val displayText: String, + val contentDescription: String, + ) { + fun format(context: Context, @StringRes resId: Int, vararg formatArgs: Any?) = + FormattedDataUsage( + displayText = context.getString(resId, displayText, *formatArgs), + contentDescription = context.getString(resId, contentDescription, *formatArgs), + ) + } + + /** Formats the data usage. */ + fun formatDataUsage(sizeBytes: Long): FormattedDataUsage { + val result = Formatter.formatBytes(context.resources, sizeBytes, Formatter.FLAG_IEC_UNITS) + return FormattedDataUsage( + displayText = BidiFormatter.getInstance().unicodeWrap( + context.getString(R.string.fileSizeSuffix, result.value, result.units) + ), + contentDescription = context.getString( + R.string.fileSizeSuffix, result.value, result.unitsContentDescription + ), + ) + } + + companion object { + /** + * Gets the display unit of the given bytes. + * + * Similar to MeasureFormat.getUnitDisplayName(), but with the expected result for the bytes + * in Settings, and align with other places in Settings. + */ + fun Resources.getBytesDisplayUnit(bytes: Long): String = + Formatter.formatBytes(this, bytes, Formatter.FLAG_IEC_UNITS).units + } +} diff --git a/src/com/android/settings/datausage/lib/NetworkUsageData.kt b/src/com/android/settings/datausage/lib/NetworkUsageData.kt index f9d83d52659..26578e325b4 100644 --- a/src/com/android/settings/datausage/lib/NetworkUsageData.kt +++ b/src/com/android/settings/datausage/lib/NetworkUsageData.kt @@ -20,7 +20,7 @@ import android.content.Context import android.text.format.DateUtils import android.util.Range import com.android.settings.R -import com.android.settings.datausage.DataUsageUtils +import com.android.settings.datausage.lib.DataUsageFormatter.FormattedDataUsage /** * Base data structure representing usage data in a period. @@ -38,10 +38,11 @@ data class NetworkUsageData( fun formatDateRange(context: Context): String = DateUtils.formatDateRange(context, startTime, endTime, DATE_FORMAT) - fun formatUsage(context: Context): CharSequence = DataUsageUtils.formatDataUsage(context, usage) + fun formatUsage(context: Context): FormattedDataUsage = + DataUsageFormatter(context).formatDataUsage(usage) - fun getDataUsedString(context: Context): String = - context.getString(R.string.data_used_template, formatUsage(context)) + fun getDataUsedString(context: Context): FormattedDataUsage = + formatUsage(context).format(context, R.string.data_used_template) companion object { val AllZero = NetworkUsageData( diff --git a/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt b/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt index 1cf770b3d40..d47a246644d 100644 --- a/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt +++ b/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt @@ -30,6 +30,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceScreen import com.android.settings.R import com.android.settings.datausage.DataUsageUtils +import com.android.settings.datausage.lib.DataUsageFormatter.FormattedDataUsage import com.android.settings.datausage.lib.DataUsageLib import com.android.settings.datausage.lib.NetworkCycleDataRepository import com.android.settings.datausage.lib.NetworkStatsRepository.Companion.AllTimeRange @@ -89,7 +90,7 @@ class DataUsagePreferenceController(context: Context, key: String) : getDataUsageSummaryAndEnabled() } preference.isEnabled = enabled - preference.summary = summary + preference.summary = summary?.displayText } private fun getNetworkTemplate(): NetworkTemplate? = when { @@ -104,15 +105,14 @@ class DataUsagePreferenceController(context: Context, key: String) : fun createNetworkCycleDataRepository(): NetworkCycleDataRepository? = networkTemplate?.let { NetworkCycleDataRepository(mContext, it) } - private fun getDataUsageSummaryAndEnabled(): Pair { + private fun getDataUsageSummaryAndEnabled(): Pair { val repository = createNetworkCycleDataRepository() ?: return null to false repository.loadFirstCycle()?.let { usageData -> - return mContext.getString( - R.string.data_usage_template, - usageData.formatUsage(mContext), - usageData.formatDateRange(mContext), - ) to (usageData.usage > 0 || repository.queryUsage(AllTimeRange).usage > 0) + val formattedDataUsage = usageData.formatUsage(mContext) + .format(mContext, R.string.data_usage_template, usageData.formatDateRange(mContext)) + val hasUsage = usageData.usage > 0 || repository.queryUsage(AllTimeRange).usage > 0 + return formattedDataUsage to hasUsage } val allTimeUsage = repository.queryUsage(AllTimeRange) diff --git a/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt b/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt index 7e6e72613b7..7b8cf8c8b55 100644 --- a/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt +++ b/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt @@ -113,7 +113,7 @@ private class AppDataUsagePresenter( } else { context.getString( R.string.data_summary_format, - appUsageData.formatUsage(context), + appUsageData.formatUsage(context).displayText, appUsageData.formatStartDate(context), ) } diff --git a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageSummaryControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageSummaryControllerTest.kt index 584295649aa..be839dd416f 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageSummaryControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageSummaryControllerTest.kt @@ -19,8 +19,9 @@ package com.android.settings.datausage import android.content.Context import android.util.Range import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTextExactly import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.datausage.lib.NetworkUsageDetailsData @@ -52,9 +53,34 @@ class AppDataUsageSummaryControllerTest { controller.Content() } - composeTestRule.onNodeWithText("6.75 kB").assertIsDisplayed() - composeTestRule.onNodeWithText("5.54 kB").assertIsDisplayed() - composeTestRule.onNodeWithText("1.21 kB").assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Total", "6.75 kB")).assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Foreground", "5.54 kB")).assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Background", "1.21 kB")).assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("6.75 kB").assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("5.54 kB").assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("1.21 kB").assertIsDisplayed() + } + + @Test + fun summary_zero() { + val appUsage = NetworkUsageDetailsData( + range = Range(1L, 2L), + totalUsage = 3, + foregroundUsage = 1, + backgroundUsage = 2, + ) + + controller.update(appUsage) + composeTestRule.setContent { + controller.Content() + } + + composeTestRule.onNode(hasTextExactly("Total", "3 B")).assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Foreground", "1 B")).assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Background", "2 B")).assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("3 byte").assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("1 byte").assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("2 byte").assertIsDisplayed() } private companion object { diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageFormatterTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/DataUsageFormatterTest.kt similarity index 59% rename from tests/spa_unit/src/com/android/settings/datausage/DataUsageFormatterTest.kt rename to tests/spa_unit/src/com/android/settings/datausage/lib/DataUsageFormatterTest.kt index dc6a421b940..071234dcd65 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/DataUsageFormatterTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/lib/DataUsageFormatterTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.settings.datausage +package com.android.settings.datausage.lib import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settings.datausage.DataUsageFormatter.getBytesDisplayUnit +import com.android.settings.datausage.lib.DataUsageFormatter.Companion.getBytesDisplayUnit import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -29,6 +29,32 @@ import org.junit.runner.RunWith class DataUsageFormatterTest { private val context: Context = ApplicationProvider.getApplicationContext() + private val dataUsageFormatter = DataUsageFormatter(context) + + @Test + fun formatDataUsage_0() { + val (displayText, contentDescription) = dataUsageFormatter.formatDataUsage(0) + + assertThat(displayText).isEqualTo("0 B") + assertThat(contentDescription).isEqualTo("0 byte") + } + + @Test + fun formatDataUsage_1000() { + val (displayText, contentDescription) = dataUsageFormatter.formatDataUsage(1000) + + assertThat(displayText).isEqualTo("0.98 kB") + assertThat(contentDescription).isEqualTo("0.98 kB") + } + + @Test + fun formatDataUsage_2000000() { + val (displayText, contentDescription) = dataUsageFormatter.formatDataUsage(2000000) + + assertThat(displayText).isEqualTo("1.91 MB") + assertThat(contentDescription).isEqualTo("1.91 MB") + } + @Test fun getUnitDisplayName_megaByte() { val displayName = context.resources.getBytesDisplayUnit(ONE_MEGA_BYTE_IN_BYTES) @@ -47,4 +73,4 @@ class DataUsageFormatterTest { const val ONE_MEGA_BYTE_IN_BYTES = 1024L * 1024 const val ONE_GIGA_BYTE_IN_BYTES = 1024L * 1024 * 1024 } -} \ No newline at end of file +}