From 488f2028edb93b3f9b520ac57bdca76550b78306 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 20 Nov 2024 11:20:00 +0100 Subject: [PATCH] fix: Stop using strings reflection to get suffixes for Nougat and after --- .../lib/core/utils/FormatterFileSize.kt | 93 ++++++++++++++++++- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/infomaniak/lib/core/utils/FormatterFileSize.kt b/src/main/java/com/infomaniak/lib/core/utils/FormatterFileSize.kt index 58cfb6ec..dbcd2fd9 100644 --- a/src/main/java/com/infomaniak/lib/core/utils/FormatterFileSize.kt +++ b/src/main/java/com/infomaniak/lib/core/utils/FormatterFileSize.kt @@ -18,9 +18,19 @@ package com.infomaniak.lib.core.utils import android.content.Context +import android.icu.text.DecimalFormat +import android.icu.text.MeasureFormat +import android.icu.text.NumberFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit +import android.os.Build import android.text.format.Formatter +import androidx.annotation.RequiresApi +import java.math.BigDecimal +import java.util.Locale import kotlin.math.abs + object FormatterFileSize { private const val KIBI_BYTE = 1_024L @@ -31,17 +41,22 @@ object FormatterFileSize { private const val FLAG_SHORTER = 1 shl 0 fun Context.formatShortFileSize(bytes: Long, valueOnly: Boolean = false): String = runCatching { - val (value, unit) = formatFileSize(bytes, FLAG_IEC_UNITS or FLAG_SHORTER, valueOnly) - if (unit == null) { - value + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + val (value, unit) = formatFileSizeBeforeNougat(bytes, FLAG_IEC_UNITS or FLAG_SHORTER, valueOnly) + if (unit == null) { + value + } else { + getString(resources.getIdentifier("fileSizeSuffix", "string", "android"), value, unit) + } } else { - getString(resources.getIdentifier("fileSizeSuffix", "string", "android"), value, unit) + val (value, formattedValue) = formatFileSize(bytes, FLAG_IEC_UNITS or FLAG_SHORTER, valueOnly) + formattedValue ?: value } }.getOrElse { Formatter.formatShortFileSize(this, bytes) } - private fun Context.formatFileSize(bytes: Long, flags: Int, valueOnly: Boolean): Pair { + private fun Context.formatFileSizeBeforeNougat(bytes: Long, flags: Int, valueOnly: Boolean): Pair { fun getSuffix(suffixes: MutableList): Int? { return if (valueOnly) null else resources.getIdentifier(suffixes.removeFirstOrNull(), "string", "android") @@ -83,4 +98,72 @@ object FormatterFileSize { return resultValue to resultUnit } + + @RequiresApi(Build.VERSION_CODES.N) + private fun Context.formatFileSize(bytes: Long, flags: Int, valueOnly: Boolean): Pair { + + fun getSuffix(suffixes: MutableList): MeasureUnit? { + return if (valueOnly) null else suffixes.removeFirstOrNull() + } + + val suffixes = mutableListOf( + MeasureUnit.BYTE, + MeasureUnit.KILOBYTE, + MeasureUnit.MEGABYTE, + MeasureUnit.GIGABYTE, + MeasureUnit.TERABYTE, + ) + val unit = if (flags and FLAG_IEC_UNITS != 0) KIBI_BYTE else KILO_BYTE + var multiplier = 1L + + var result = abs(bytes).toFloat() + val suffixesCount = suffixes.count() - 1 + var suffix = getSuffix(suffixes) + + repeat(suffixesCount) { + if (result > 900) { + suffix = getSuffix(suffixes) + multiplier *= unit + result /= unit + } + } + + val (roundFormat, roundedBytes) = when { + multiplier == 1L || result >= 100 -> "%.0f" to 0 + result < 1 -> "%.2f" to 2 + result < 10 -> if (flags and FLAG_SHORTER != 0) "%.1f" to 1 else "%.2f" to 2 + else -> if (flags and FLAG_SHORTER != 0) "%.0f" to 0 else "%.2f" to 2 // 10 <= result < 100 + } + + result = abs(result) + val resultValue = String.format(roundFormat, result) + + val resultValueFormatted = suffix?.let { + val locale = currentLocale() + formatMeasureShort(locale, getNumberFormatter(locale, roundedBytes), result, it) + } + + return resultValue to resultValueFormatted + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun Context.currentLocale(): Locale = resources.configuration.locales[0] + + @RequiresApi(Build.VERSION_CODES.N) + private fun getNumberFormatter(locale: Locale, fractionDigits: Int): NumberFormat { + return NumberFormat.getInstance(locale).apply { + minimumFractionDigits = fractionDigits + maximumFractionDigits = fractionDigits + isGroupingUsed = false + // We do this only for DecimalFormat, since in the general NumberFormat case, + // calling setRoundingMode may throw an exception. + if (this is DecimalFormat) setRoundingMode(BigDecimal.ROUND_HALF_UP) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun formatMeasureShort(locale: Locale, numberFormatter: NumberFormat, value: Float, units: MeasureUnit): String { + val measureFormatter = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT, numberFormatter) + return measureFormatter.format(Measure(value, units)) + } }