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
This commit is contained in:
@@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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<String> adapter = new ArrayAdapter<String>(
|
||||
getContext(), android.R.layout.simple_spinner_item, unitNames);
|
||||
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
|
@@ -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);
|
||||
|
64
src/com/android/settings/datausage/lib/DataUsageFormatter.kt
Normal file
64
src/com/android/settings/datausage/lib/DataUsageFormatter.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
@@ -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(
|
||||
|
@@ -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<String?, Boolean> {
|
||||
private fun getDataUsageSummaryAndEnabled(): Pair<FormattedDataUsage?, Boolean> {
|
||||
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)
|
||||
|
@@ -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),
|
||||
)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
Reference in New Issue
Block a user