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 Original line Diff line number Diff line
@@ -20,7 +20,11 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources;
import android.icu.text.DecimalFormat;
import android.icu.text.MeasureFormat;
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.Measure;
import android.icu.util.MeasureUnit;
import android.icu.util.MeasureUnit;
import android.net.NetworkUtils;
import android.net.NetworkUtils;
@@ -28,6 +32,7 @@ import android.text.BidiFormatter;
import android.text.TextUtils;
import android.text.TextUtils;
import android.view.View;
import android.view.View;


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


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


    /** {@hide} */
    public static final int FLAG_DEFAULT = 0;
    /** {@hide} */
    /** {@hide} */
    public static final int FLAG_SHORTER = 1 << 0;
    public static final int FLAG_SHORTER = 1 << 0;
    /** {@hide} */
    /** {@hide} */
@@ -58,7 +65,9 @@ public final class Formatter {
        return context.getResources().getConfiguration().getLocales().get(0);
        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) {
    private static String bidiWrap(@NonNull Context context, String source) {
        final Locale locale = localeFromContext(context);
        final Locale locale = localeFromContext(context);
        if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
        if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
@@ -87,12 +96,7 @@ public final class Formatter {
     * @return formatted string with the number
     * @return formatted string with the number
     */
     */
    public static String formatFileSize(@Nullable Context context, long sizeBytes) {
    public static String formatFileSize(@Nullable Context context, long sizeBytes) {
        if (context == null) {
        return formatFileSize(context, sizeBytes, FLAG_DEFAULT);
            return "";
        }
        final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
        return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
                res.value, res.units));
    }
    }


    /**
    /**
@@ -100,88 +104,191 @@ public final class Formatter {
     * (showing fewer digits of precision).
     * (showing fewer digits of precision).
     */
     */
    public static String formatShortFileSize(@Nullable Context context, long sizeBytes) {
    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) {
        if (context == null) {
            return "";
            return "";
        }
        }
        final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
        final RoundedBytesResult res = RoundedBytesResult.roundBytes(sizeBytes, flags);
        return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
        return bidiWrap(context, formatRoundedBytesResult(context, res));
                res.value, res.units));
    }

    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} */
    /** {@hide} */
    public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
    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);
            final boolean isNegative = (sizeBytes < 0);
            float result = isNegative ? -sizeBytes : sizeBytes;
            float result = isNegative ? -sizeBytes : sizeBytes;
        int suffix = com.android.internal.R.string.byteShort;
            MeasureUnit units = MeasureUnit.BYTE;
            long mult = 1;
            long mult = 1;
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.kilobyteShort;
                units = MeasureUnit.KILOBYTE;
                mult = 1000;
                mult = 1000;
                result = result / 1000;
                result = result / 1000;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.megabyteShort;
                units = MeasureUnit.MEGABYTE;
                mult *= 1000;
                mult *= 1000;
                result = result / 1000;
                result = result / 1000;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.gigabyteShort;
                units = MeasureUnit.GIGABYTE;
                mult *= 1000;
                mult *= 1000;
                result = result / 1000;
                result = result / 1000;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.terabyteShort;
                units = MeasureUnit.TERABYTE;
                mult *= 1000;
                mult *= 1000;
                result = result / 1000;
                result = result / 1000;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.petabyteShort;
                units = PETABYTE;
                mult *= 1000;
                mult *= 1000;
                result = result / 1000;
                result = result / 1000;
            }
            }
        // Note we calculate the rounded long by ourselves, but still let String.format()
            // Note we calculate the rounded long by ourselves, but still let NumberFormat compute
        // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
            // the rounded value. NumberFormat.format(0.1) might not return "0.1" due to floating
        // floating point errors.
            // point errors.
            final int roundFactor;
            final int roundFactor;
        final String roundFormat;
            final int roundDigits;
            if (mult == 1 || result >= 100) {
            if (mult == 1 || result >= 100) {
                roundFactor = 1;
                roundFactor = 1;
            roundFormat = "%.0f";
                roundDigits = 0;
            } else if (result < 1) {
            } else if (result < 1) {
                roundFactor = 100;
                roundFactor = 100;
            roundFormat = "%.2f";
                roundDigits = 2;
            } else if (result < 10) {
            } else if (result < 10) {
                if ((flags & FLAG_SHORTER) != 0) {
                if ((flags & FLAG_SHORTER) != 0) {
                    roundFactor = 10;
                    roundFactor = 10;
                roundFormat = "%.1f";
                    roundDigits = 1;
                } else {
                } else {
                    roundFactor = 100;
                    roundFactor = 100;
                roundFormat = "%.2f";
                    roundDigits = 2;
                }
                }
            } else { // 10 <= result < 100
            } else { // 10 <= result < 100
                if ((flags & FLAG_SHORTER) != 0) {
                if ((flags & FLAG_SHORTER) != 0) {
                    roundFactor = 1;
                    roundFactor = 1;
                roundFormat = "%.0f";
                    roundDigits = 0;
                } else {
                } else {
                    roundFactor = 100;
                    roundFactor = 100;
                roundFormat = "%.2f";
                    roundDigits = 2;
                }
                }
            }
            }


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


        final String units = res.getString(suffix);
            return new RoundedBytesResult(result, units, roundDigits, roundedBytes);

        }
        return new BytesResult(roundedString, units, roundedBytes);
    }
    }


    /**
    /**
+5 −15
Original line number Original line Diff line number Diff line
@@ -20,23 +20,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <!-- Suffix added to a number to signify size in bytes. -->
    <!-- Suffix added to a number to signify size in bytes. -->
    <string name="byteShort">B</string>
    <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. -->
    <!-- Suffix added to a number to signify size in petabytes. -->
    <string name="petabyteShort">PB</string>
    <string name="petabyteShort">PB</string>
    <!-- Format string used to add a suffix like "kB" or "MB" to a number
    <!-- Format string used to add a suffix like "B" or "PB" to a number
         to display a size in kilobytes, megabytes, or other size units.
         to display a size in bytes or petabytes.
         Some languages (like French) will want to add a space between
         Some languages may want to remove the space between the placeholders
         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="MB">%2$s</xliff:g></string>
    <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
    <!-- Used in Contacts for a field that has no label and in Note Pad
         for a note with no name. -->
         for a note with no name. -->
+0 −4
Original line number Original line Diff line number Diff line
@@ -675,7 +675,6 @@
  <java-symbol type="string" name="fileSizeSuffix" />
  <java-symbol type="string" name="fileSizeSuffix" />
  <java-symbol type="string" name="force_close" />
  <java-symbol type="string" name="force_close" />
  <java-symbol type="string" name="gadget_host_error_inflating" />
  <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="gpsNotifMessage" />
  <java-symbol type="string" name="gpsNotifTicker" />
  <java-symbol type="string" name="gpsNotifTicker" />
  <java-symbol type="string" name="gpsNotifTitle" />
  <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_enter" />
  <java-symbol type="string" name="keyboardview_keycode_mode_change" />
  <java-symbol type="string" name="keyboardview_keycode_mode_change" />
  <java-symbol type="string" name="keyboardview_keycode_shift" />
  <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="last_month" />
  <java-symbol type="string" name="launchBrowserDefault" />
  <java-symbol type="string" name="launchBrowserDefault" />
  <java-symbol type="string" name="lock_to_app_toast" />
  <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_emergency_call" />
  <java-symbol type="string" name="lockscreen_return_to_call" />
  <java-symbol type="string" name="lockscreen_return_to_call" />
  <java-symbol type="string" name="low_memory" />
  <java-symbol type="string" name="low_memory" />
  <java-symbol type="string" name="megabyteShort" />
  <java-symbol type="string" name="midnight" />
  <java-symbol type="string" name="midnight" />
  <java-symbol type="string" name="mismatchPin" />
  <java-symbol type="string" name="mismatchPin" />
  <java-symbol type="string" name="mmiComplete" />
  <java-symbol type="string" name="mmiComplete" />
@@ -955,7 +952,6 @@
  <java-symbol type="string" name="sync_really_delete" />
  <java-symbol type="string" name="sync_really_delete" />
  <java-symbol type="string" name="sync_too_many_deletes_desc" />
  <java-symbol type="string" name="sync_too_many_deletes_desc" />
  <java-symbol type="string" name="sync_undo_deletes" />
  <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="text_copied" />
  <java-symbol type="string" name="time_of_day" />
  <java-symbol type="string" name="time_of_day" />
  <java-symbol type="string" name="time_picker_decrement_hour_button" />
  <java-symbol type="string" name="time_picker_decrement_hour_button" />