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

Commit 988cb5a3 authored by Roozbeh Pournader's avatar Roozbeh Pournader Committed by Victor Chang
Browse files

Re-land "Switch file size formatters to use ICU's MeasureFormat"

Re-land http://ag/2443141 because the over-translation issue should be
resolved now.

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

Bug: 36994779
Bug: 71580745
Bug: 217592956
Test: atest FrameworksCoreTests:android.text
Test: atest CtsTextTestCases
Change-Id: If3416ec38cf18c0441576643bfab850148e18c8e
parent 92891b4b
Loading
Loading
Loading
Loading
+164 −75
Original line number Original line Diff line number Diff line
@@ -21,7 +21,11 @@ import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.compat.annotation.UnsupportedAppUsage;
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.text.BidiFormatter;
import android.text.BidiFormatter;
@@ -30,6 +34,7 @@ import android.view.View;


import com.android.net.module.util.Inet4AddressUtils;
import com.android.net.module.util.Inet4AddressUtils;


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


/**
/**
@@ -64,7 +69,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) {
@@ -101,9 +108,8 @@ public final class Formatter {
        if (context == null) {
        if (context == null) {
            return "";
            return "";
        }
        }
        final BytesResult res = formatBytes(context.getResources(), sizeBytes, flags);
        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));
    }
    }


    /**
    /**
@@ -111,91 +117,174 @@ 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) {
        if (context == null) {
        return formatFileSize(context, sizeBytes, FLAG_SI_UNITS | FLAG_SHORTER);
            return "";
    }

    private static String getByteSuffixOverride(@NonNull Resources res) {
        return res.getString(com.android.internal.R.string.byteShort);
    }

    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) {
            // ICU spells out "byte" instead of "B".
            final String formattedNumber = numberFormatter.format(input.value);
            return context.getString(com.android.internal.R.string.fileSizeSuffix,
                    formattedNumber, getByteSuffixOverride(context.getResources()));
        } else {
            return formatMeasureShort(locale, numberFormatter, input.value, input.units);
        }
        }
        final BytesResult res = formatBytes(context.getResources(), sizeBytes,
                FLAG_SI_UNITS | FLAG_SHORTER);
        return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
                res.value, res.units));
    }
    }


    /** {@hide} */
    /** {@hide} */
    @UnsupportedAppUsage
    public static class RoundedBytesResult {
    public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
        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 int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000;
            final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000;
            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 = unit;
                mult = unit;
                result = result / unit;
                result = result / unit;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.megabyteShort;
                units = MeasureUnit.MEGABYTE;
                mult *= unit;
                mult *= unit;
                result = result / unit;
                result = result / unit;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.gigabyteShort;
                units = MeasureUnit.GIGABYTE;
                mult *= unit;
                mult *= unit;
                result = result / unit;
                result = result / unit;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.terabyteShort;
                units = MeasureUnit.TERABYTE;
                mult *= unit;
                mult *= unit;
                result = result / unit;
                result = result / unit;
            }
            }
            if (result > 900) {
            if (result > 900) {
            suffix = com.android.internal.R.string.petabyteShort;
                units = MeasureUnit.PETABYTE;
                mult *= unit;
                mult *= unit;
                result = result / unit;
                result = result / unit;
            }
            }
        // 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);
    /** {@hide} */
    @UnsupportedAppUsage
    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) {
            // ICU spells out "byte" instead of "B".
            units = getByteSuffixOverride(res);
        } 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);
    }
    }


    /**
    /**
+53 −0
Original line number Original line Diff line number Diff line
@@ -36,6 +36,8 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.RunWith;


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


@Presubmit
@Presubmit
@@ -216,6 +218,57 @@ public class FormatterTest {
                mContext, 1 * SECOND));
                mContext, 1 * SECOND));
    }
    }


    /**
     * Regression test for http://b/71580745 and https://unicode-org.atlassian.net/browse/CLDR-10831
     */
    @Test
    public void testFormatFileSize_zhCN() {
        setLocale(Locale.forLanguageTag("zh-CN"));

        assertFormatFileSize_englishOutput();
    }

    @Test
    public void testFormatFileSize_enUS() {
        setLocale(Locale.US);

        assertFormatFileSize_englishOutput();
    }

    private void assertFormatFileSize_englishOutput() {
        final MathContext mc = MathContext.DECIMAL64;
        final BigDecimal bd = new BigDecimal((long) 1000, mc);
        // test null Context
        assertEquals("", Formatter.formatFileSize(null, 0));
        // test different long values with various length
        assertEquals("0 B", Formatter.formatFileSize(mContext, 0));
        assertEquals("1 B", Formatter.formatFileSize(mContext, 1));
        assertEquals("9 B", Formatter.formatFileSize(mContext, 9));
        assertEquals("10 B", Formatter.formatFileSize(mContext, 10));
        assertEquals("99 B", Formatter.formatFileSize(mContext, 99));
        assertEquals("100 B", Formatter.formatFileSize(mContext, 100));
        assertEquals("900 B", Formatter.formatFileSize(mContext, 900));
        assertEquals("0.90 kB", Formatter.formatFileSize(mContext, 901));

        assertEquals("1.00 kB", Formatter.formatFileSize(mContext, bd.pow(1).longValue()));
        assertEquals("1.50 kB", Formatter.formatFileSize(mContext, bd.pow(1).longValue() * 3 / 2));
        assertEquals("12.50 kB", Formatter.formatFileSize(mContext,
                bd.pow(1).longValue() * 25 / 2));

        assertEquals("1.00 MB", Formatter.formatFileSize(mContext, bd.pow(2).longValue()));

        assertEquals("1.00 GB", Formatter.formatFileSize(mContext, bd.pow(3).longValue()));

        assertEquals("1.00 TB", Formatter.formatFileSize(mContext, bd.pow(4).longValue()));

        assertEquals("1.00 PB", Formatter.formatFileSize(mContext, bd.pow(5).longValue()));

        assertEquals("1000 PB", Formatter.formatFileSize(mContext, bd.pow(6).longValue()));

        // test Negative value
        assertEquals("-1 B", Formatter.formatFileSize(mContext, -1));
    }

    private void checkFormatBytes(long bytes, boolean useShort,
    private void checkFormatBytes(long bytes, boolean useShort,
            String expectedString, long expectedRounded) {
            String expectedString, long expectedRounded) {
        checkFormatBytes(bytes, (useShort ? Formatter.FLAG_SHORTER : 0),
        checkFormatBytes(bytes, (useShort ? Formatter.FLAG_SHORTER : 0),