Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4e5b71f0 authored by Roozbeh Pournader's avatar Roozbeh Pournader
Browse files

Switch file size formatters to use ICU's MeasureFormat

Use ICU's MeasureFormat to the degree possible for formatting file
sizes.

Bug: 36994779
Test: adb shell am instrument -w -e package android.text com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner
Test: bit CtsTextTestCases:*
Change-Id: I4ad3b568ae7585b6ff56fddb79ded7c5b2118176
parent 1137f872
Loading
Loading
Loading
Loading
+179 −72
Original line number Diff line number Diff line
@@ -20,7 +20,11 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
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.icu.util.MeasureUnit;
import android.net.NetworkUtils;
@@ -28,6 +32,7 @@ import android.text.BidiFormatter;
import android.text.TextUtils;
import android.view.View;

import java.math.BigDecimal;
import java.util.Locale;

/**
@@ -36,6 +41,8 @@ import java.util.Locale;
 */
public final class Formatter {

    /** {@hide} */
    public static final int FLAG_DEFAULT = 0;
    /** {@hide} */
    public static final int FLAG_SHORTER = 1 << 0;
    /** {@hide} */
@@ -58,7 +65,9 @@ public final class Formatter {
        return context.getResources().getConfiguration().getLocales().get(0);
    }

    /* Wraps the source string in bidi formatting characters in RTL locales */
    /**
     * Wraps the source string in bidi formatting characters in RTL locales.
     */
    private static String bidiWrap(@NonNull Context context, String source) {
        final Locale locale = localeFromContext(context);
        if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
@@ -87,12 +96,7 @@ public final class Formatter {
     * @return formatted string with the number
     */
    public static String formatFileSize(@Nullable Context context, long sizeBytes) {
        if (context == null) {
            return "";
        }
        final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
        return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
                res.value, res.units));
        return formatFileSize(context, sizeBytes, FLAG_DEFAULT);
    }

    /**
@@ -100,88 +104,191 @@ public final class Formatter {
     * (showing fewer digits of precision).
     */
    public static String formatShortFileSize(@Nullable Context context, long sizeBytes) {
        return formatFileSize(context, sizeBytes, FLAG_SHORTER);
    }

    private static String formatFileSize(@Nullable Context context, long sizeBytes, int flags) {
        if (context == null) {
            return "";
        }
        final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
        return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
                res.value, res.units));
        final RoundedBytesResult res = RoundedBytesResult.roundBytes(sizeBytes, flags);
        return bidiWrap(context, formatRoundedBytesResult(context, res));
    }

    private static String getSuffixOverride(@NonNull Resources res, MeasureUnit unit) {
        if (unit == MeasureUnit.BYTE) {
            return res.getString(com.android.internal.R.string.byteShort);
        } else { // unit == PETABYTE
            return res.getString(com.android.internal.R.string.petabyteShort);
        }
    }

    private static NumberFormat getNumberFormatter(Locale locale, int fractionDigits) {
        final NumberFormat numberFormatter = NumberFormat.getInstance(locale);
        numberFormatter.setMinimumFractionDigits(fractionDigits);
        numberFormatter.setMaximumFractionDigits(fractionDigits);
        numberFormatter.setGroupingUsed(false);
        if (numberFormatter instanceof DecimalFormat) {
            // We do this only for DecimalFormat, since in the general NumberFormat case, calling
            // setRoundingMode may throw an exception.
            numberFormatter.setRoundingMode(BigDecimal.ROUND_HALF_UP);
        }
        return numberFormatter;
    }

    private static String deleteFirstFromString(String source, String toDelete) {
        final int location = source.indexOf(toDelete);
        if (location == -1) {
            return source;
        } else {
            return source.substring(0, location)
                    + source.substring(location + toDelete.length(), source.length());
        }
    }

    private static String formatMeasureShort(Locale locale, NumberFormat numberFormatter,
            float value, MeasureUnit units) {
        final MeasureFormat measureFormatter = MeasureFormat.getInstance(
                locale, MeasureFormat.FormatWidth.SHORT, numberFormatter);
        return measureFormatter.format(new Measure(value, units));
    }

    private static final UnicodeSetSpanner SPACES_AND_CONTROLS =
            new UnicodeSetSpanner(new UnicodeSet("[[:Zs:][:Cf:]]").freeze());

    private static String formatRoundedBytesResult(
            @NonNull Context context, @NonNull RoundedBytesResult input) {
        final Locale locale = localeFromContext(context);
        final NumberFormat numberFormatter = getNumberFormatter(locale, input.fractionDigits);
        if (input.units == MeasureUnit.BYTE || input.units == PETABYTE) {
            // ICU spells out "byte" instead of "B", and can't format petabytes yet.
            final String formattedNumber = numberFormatter.format(input.value);
            return context.getString(com.android.internal.R.string.fileSizeSuffix,
                    formattedNumber, getSuffixOverride(context.getResources(), input.units));
        } else {
            return formatMeasureShort(locale, numberFormatter, input.value, input.units);
        }
    }

    /** {@hide} */
    public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
        final RoundedBytesResult rounded = RoundedBytesResult.roundBytes(sizeBytes, flags);
        final Locale locale = res.getConfiguration().getLocales().get(0);
        final NumberFormat numberFormatter = getNumberFormatter(locale, rounded.fractionDigits);
        final String formattedNumber = numberFormatter.format(rounded.value);
        final String units;
        if (rounded.units == MeasureUnit.BYTE || rounded.units == PETABYTE) {
            // ICU spells out "byte" instead of "B", and can't format petabytes yet.
            units = getSuffixOverride(res, rounded.units);
        } else {
            // Since ICU does not give us access to the pattern, we need to extract the unit string
            // from ICU, which we do by taking out the formatted number out of the formatted string
            // and trimming the result of spaces and controls.
            final String formattedMeasure = formatMeasureShort(
                    locale, numberFormatter, rounded.value, rounded.units);
            final String numberRemoved = deleteFirstFromString(formattedMeasure, formattedNumber);
            units = SPACES_AND_CONTROLS.trim(numberRemoved).toString();
        }
        return new BytesResult(formattedNumber, units, rounded.roundedBytes);
    }

    /**
     * ICU doesn't support PETABYTE yet. Fake it so that we can treat all units the same way.
     * {@hide}
     */
    public static final MeasureUnit PETABYTE = MeasureUnit.internalGetInstance(
            "digital", "petabyte");

    /** {@hide} */
    public static class RoundedBytesResult {
        public final float value;
        public final MeasureUnit units;
        public final int fractionDigits;
        public final long roundedBytes;

        private RoundedBytesResult(
                float value, MeasureUnit units, int fractionDigits, long roundedBytes) {
            this.value = value;
            this.units = units;
            this.fractionDigits = fractionDigits;
            this.roundedBytes = roundedBytes;
        }

        /**
         * Returns a RoundedBytesResult object based on the input size in bytes and the rounding
         * flags. The result can be used for formatting.
         */
        public static RoundedBytesResult roundBytes(long sizeBytes, int flags) {
            final boolean isNegative = (sizeBytes < 0);
            float result = isNegative ? -sizeBytes : sizeBytes;
        int suffix = com.android.internal.R.string.byteShort;
            MeasureUnit units = MeasureUnit.BYTE;
            long mult = 1;
            if (result > 900) {
            suffix = com.android.internal.R.string.kilobyteShort;
                units = MeasureUnit.KILOBYTE;
                mult = 1000;
                result = result / 1000;
            }
            if (result > 900) {
            suffix = com.android.internal.R.string.megabyteShort;
                units = MeasureUnit.MEGABYTE;
                mult *= 1000;
                result = result / 1000;
            }
            if (result > 900) {
            suffix = com.android.internal.R.string.gigabyteShort;
                units = MeasureUnit.GIGABYTE;
                mult *= 1000;
                result = result / 1000;
            }
            if (result > 900) {
            suffix = com.android.internal.R.string.terabyteShort;
                units = MeasureUnit.TERABYTE;
                mult *= 1000;
                result = result / 1000;
            }
            if (result > 900) {
            suffix = com.android.internal.R.string.petabyteShort;
                units = PETABYTE;
                mult *= 1000;
                result = result / 1000;
            }
        // Note we calculate the rounded long by ourselves, but still let String.format()
        // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
        // floating point errors.
            // Note we calculate the rounded long by ourselves, but still let NumberFormat compute
            // the rounded value. NumberFormat.format(0.1) might not return "0.1" due to floating
            // point errors.
            final int roundFactor;
        final String roundFormat;
            final int roundDigits;
            if (mult == 1 || result >= 100) {
                roundFactor = 1;
            roundFormat = "%.0f";
                roundDigits = 0;
            } else if (result < 1) {
                roundFactor = 100;
            roundFormat = "%.2f";
                roundDigits = 2;
            } else if (result < 10) {
                if ((flags & FLAG_SHORTER) != 0) {
                    roundFactor = 10;
                roundFormat = "%.1f";
                    roundDigits = 1;
                } else {
                    roundFactor = 100;
                roundFormat = "%.2f";
                    roundDigits = 2;
                }
            } else { // 10 <= result < 100
                if ((flags & FLAG_SHORTER) != 0) {
                    roundFactor = 1;
                roundFormat = "%.0f";
                    roundDigits = 0;
                } else {
                    roundFactor = 100;
                roundFormat = "%.2f";
                    roundDigits = 2;
                }
            }

            if (isNegative) {
                result = -result;
            }
        final String roundedString = String.format(roundFormat, result);

        // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
        // it's okay (for now)...
            // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like
            // 80PB so it's okay (for now)...
            final long roundedBytes =
                    (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
                    : (((long) Math.round(result * roundFactor)) * mult / roundFactor);

        final String units = res.getString(suffix);

        return new BytesResult(roundedString, units, roundedBytes);
            return new RoundedBytesResult(result, units, roundDigits, roundedBytes);
        }
    }

    /**
+5 −15
Original line number Diff line number Diff line
@@ -20,23 +20,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <!-- Suffix added to a number to signify size in bytes. -->
    <string name="byteShort">B</string>
    <!-- Suffix added to a number to signify size in kilobytes (1000 bytes).
        If you retain the Latin script for the localization, please use the lowercase
        'k', as it signifies 1000 bytes as opposed to 1024 bytes. -->
    <string name="kilobyteShort">kB</string>
    <!-- Suffix added to a number to signify size in megabytes. -->
    <string name="megabyteShort">MB</string>
    <!-- Suffix added to a number to signify size in gigabytes. -->
    <string name="gigabyteShort">GB</string>
    <!-- Suffix added to a number to signify size in terabytes. -->
    <string name="terabyteShort">TB</string>
    <!-- Suffix added to a number to signify size in petabytes. -->
    <string name="petabyteShort">PB</string>
    <!-- Format string used to add a suffix like "kB" or "MB" to a number
         to display a size in kilobytes, megabytes, or other size units.
         Some languages (like French) will want to add a space between
         the placeholders. -->
    <string name="fileSizeSuffix"><xliff:g id="number" example="123">%1$s</xliff:g> <xliff:g id="unit" example="MB">%2$s</xliff:g></string>
    <!-- Format string used to add a suffix like "B" or "PB" to a number
         to display a size in bytes or petabytes.
         Some languages may want to remove the space between the placeholders
         or replace it with a non-breaking space. -->
    <string name="fileSizeSuffix"><xliff:g id="number" example="123">%1$s</xliff:g> <xliff:g id="unit" example="B">%2$s</xliff:g></string>

    <!-- Used in Contacts for a field that has no label and in Note Pad
         for a note with no name. -->
+0 −4
Original line number Diff line number Diff line
@@ -675,7 +675,6 @@
  <java-symbol type="string" name="fileSizeSuffix" />
  <java-symbol type="string" name="force_close" />
  <java-symbol type="string" name="gadget_host_error_inflating" />
  <java-symbol type="string" name="gigabyteShort" />
  <java-symbol type="string" name="gpsNotifMessage" />
  <java-symbol type="string" name="gpsNotifTicker" />
  <java-symbol type="string" name="gpsNotifTitle" />
@@ -731,7 +730,6 @@
  <java-symbol type="string" name="keyboardview_keycode_enter" />
  <java-symbol type="string" name="keyboardview_keycode_mode_change" />
  <java-symbol type="string" name="keyboardview_keycode_shift" />
  <java-symbol type="string" name="kilobyteShort" />
  <java-symbol type="string" name="last_month" />
  <java-symbol type="string" name="launchBrowserDefault" />
  <java-symbol type="string" name="lock_to_app_toast" />
@@ -752,7 +750,6 @@
  <java-symbol type="string" name="lockscreen_emergency_call" />
  <java-symbol type="string" name="lockscreen_return_to_call" />
  <java-symbol type="string" name="low_memory" />
  <java-symbol type="string" name="megabyteShort" />
  <java-symbol type="string" name="midnight" />
  <java-symbol type="string" name="mismatchPin" />
  <java-symbol type="string" name="mmiComplete" />
@@ -955,7 +952,6 @@
  <java-symbol type="string" name="sync_really_delete" />
  <java-symbol type="string" name="sync_too_many_deletes_desc" />
  <java-symbol type="string" name="sync_undo_deletes" />
  <java-symbol type="string" name="terabyteShort" />
  <java-symbol type="string" name="text_copied" />
  <java-symbol type="string" name="time_of_day" />
  <java-symbol type="string" name="time_picker_decrement_hour_button" />