diff options
| author | 2025-01-06 19:06:34 -0800 | |
|---|---|---|
| committer | 2025-01-06 19:06:34 -0800 | |
| commit | 44c25771cfa7f0fe376f8b876cc1072d982afb9d (patch) | |
| tree | 635a5c85cdbd8d8f30e7d93b815a6de05853c059 | |
| parent | f492c93e40d43f23947eb0f01d7bee1d38425cc3 (diff) | |
| parent | c15aa61c2683a0a73d118f37d8dad0d8cd9606fd (diff) | |
Merge "New BytesFormatter to unify all bytes formats" into main
2 files changed, 261 insertions, 0 deletions
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt new file mode 100644 index 000000000000..5b7e2a86135a --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt @@ -0,0 +1,83 @@ +/* + * 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.settingslib.spaprivileged.framework.common + +import android.content.Context +import android.content.res.Resources +import android.icu.text.DecimalFormat +import android.icu.text.MeasureFormat +import android.icu.text.NumberFormat +import android.icu.text.UnicodeSet +import android.icu.text.UnicodeSetSpanner +import android.icu.util.Measure +import android.text.format.Formatter +import android.text.format.Formatter.RoundedBytesResult +import java.math.BigDecimal + +class BytesFormatter(resources: Resources) { + + enum class UseCase(val flag: Int) { + FileSize(Formatter.FLAG_SI_UNITS), + DataUsage(Formatter.FLAG_IEC_UNITS), + } + + data class Result(val number: String, val units: String) + + constructor(context: Context) : this(context.resources) + + private val locale = resources.configuration.locales[0] + + fun format(bytes: Long, useCase: UseCase): String { + val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag) + val numberFormatter = getNumberFormatter(rounded.fractionDigits) + return numberFormatter.formatRoundedBytesResult(rounded) + } + + fun formatWithUnits(bytes: Long, useCase: UseCase): Result { + val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag) + val numberFormatter = getNumberFormatter(rounded.fractionDigits) + val formattedString = numberFormatter.formatRoundedBytesResult(rounded) + val formattedNumber = numberFormatter.format(rounded.value) + return Result( + number = formattedNumber, + units = formattedString.removeFirst(formattedNumber), + ) + } + + private fun NumberFormat.formatRoundedBytesResult(rounded: RoundedBytesResult): String { + val measureFormatter = + MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT, this) + return measureFormatter.format(Measure(rounded.value, rounded.units)) + } + + private fun getNumberFormatter(fractionDigits: Int) = + NumberFormat.getInstance(locale).apply { + minimumFractionDigits = fractionDigits + maximumFractionDigits = fractionDigits + isGroupingUsed = false + if (this is DecimalFormat) { + setRoundingMode(BigDecimal.ROUND_HALF_UP) + } + } + + private companion object { + fun String.removeFirst(removed: String): String = + SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString() + + val SPACES_AND_CONTROLS = UnicodeSetSpanner(UnicodeSet("[[:Zs:][:Cf:]]").freeze()) + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt new file mode 100644 index 000000000000..7220848eebff --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt @@ -0,0 +1,178 @@ +/* + * 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.settingslib.spaprivileged.framework.common + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BytesFormatterTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val formatter = BytesFormatter(context) + + @Test + fun `Zero bytes`() { + // Given a byte value of 0, the formatted output should be "0 byte" for both FileSize + // and DataUsage UseCases. This verifies special handling of zero values. + + val fileSizeResult = formatter.format(0, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("0 byte") + + val dataUsageResult = formatter.format(0, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("0 byte") + } + + @Test + fun `Positive bytes`() { + // Given a positive byte value (e.g., 1000), the formatted output should be correctly + // displayed with appropriate units (e.g., '1.00 kB') for both UseCases. + + val fileSizeResult = formatter.format(1000, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("1.00 kB") + + val dataUsageResult = formatter.format(1024, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("1.00 kB") + } + + @Test + fun `Large bytes`() { + // Given a very large byte value (e.g., Long.MAX_VALUE), the formatted output should be + // correctly displayed with the largest unit (e.g., 'PB') for both UseCases. + + val fileSizeResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("9223 PB") + + val dataUsageResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("8192 PB") + } + + @Test + fun `Bytes requiring rounding`() { + // Given byte values that require rounding (e.g., 1512), the formatted output should be + // rounded to the appropriate number of decimal places (e.g., '1.51 kB'). + + val fileSizeResult = formatter.format(1512, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("1.51 kB") + + val dataUsageResult = formatter.format(1512, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("1.48 kB") + } + + @Test + fun `FileSize UseCase`() { + // When the UseCase is FileSize, the correct units (byte, KB, kB, GB, TB, PB) should + // be used. + val values = + listOf( + 1L, + 1024L, + 1024L * 1024L, + 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L * 1024L, + ) + val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB") + + values.zip(expectedUnits).forEach { (value, expectedUnit) -> + val result = formatter.format(value, BytesFormatter.UseCase.FileSize) + assertThat(result).contains(expectedUnit) + } + } + + @Test + fun `DataUsage UseCase`() { + // When the UseCase is DataUsage, the correct units (byte, kB, MB, GB, TB, PB) should + // be used. + val values = + listOf( + 1L, + 1024L, + 1024L * 1024L, + 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L * 1024L, + ) + val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB") + + values.zip(expectedUnits).forEach { (value, expectedUnit) -> + val result = formatter.format(value, BytesFormatter.UseCase.DataUsage) + assertThat(result).contains(expectedUnit) + } + } + + @Test + fun `Fraction digits`() { + // The number of fraction digits in the output should be correctly determined based on + // the rounded byte value. + + assertThat(formatter.format(1500, BytesFormatter.UseCase.FileSize)).isEqualTo("1.50 kB") + assertThat(formatter.format(1050, BytesFormatter.UseCase.FileSize)).isEqualTo("1.05 kB") + assertThat(formatter.format(999, BytesFormatter.UseCase.FileSize)).isEqualTo("1.00 kB") + } + + @Test + fun `Rounding mode`() { + // The rounding mode used for formatting should be ROUND_HALF_UP. + + val result = formatter.format(1006, BytesFormatter.UseCase.FileSize) + + assertThat(result).isEqualTo("1.01 kB") // Ensure rounding mode is effective + } + + @Test + fun `Grouping separator`() { + // Grouping separators should not be used in the formatted output. + + val result = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize) + + assertThat(result).isEqualTo("9223 PB") + } + + @Test + fun `Format with units`() { + // Verify that the `formatWithUnits` method correctly formats the given bytes with the + // specified units. + + val resultByte = formatter.formatWithUnits(0, BytesFormatter.UseCase.FileSize) + assertThat(resultByte).isEqualTo(BytesFormatter.Result("0", "byte")) + + val resultKb = formatter.formatWithUnits(1000, BytesFormatter.UseCase.FileSize) + assertThat(resultKb).isEqualTo(BytesFormatter.Result("1.00", "kB")) + + val resultMb = formatter.formatWithUnits(479_999_999, BytesFormatter.UseCase.FileSize) + assertThat(resultMb).isEqualTo(BytesFormatter.Result("480", "MB")) + + val resultGb = formatter.formatWithUnits(20_100_000_000, BytesFormatter.UseCase.FileSize) + assertThat(resultGb).isEqualTo(BytesFormatter.Result("20.10", "GB")) + + val resultTb = + formatter.formatWithUnits(300_100_000_000_000, BytesFormatter.UseCase.FileSize) + assertThat(resultTb).isEqualTo(BytesFormatter.Result("300", "TB")) + + val resultPb = + formatter.formatWithUnits(1000_000_000_000_000, BytesFormatter.UseCase.FileSize) + assertThat(resultPb).isEqualTo(BytesFormatter.Result("1.00", "PB")) + } +} |