diff options
| -rw-r--r-- | core/java/android/text/format/Formatter.java | 251 | ||||
| -rw-r--r-- | core/res/res/values/strings.xml | 20 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 4 |
3 files changed, 184 insertions, 91 deletions
diff --git a/core/java/android/text/format/Formatter.java b/core/java/android/text/format/Formatter.java index e5bc32bb4f0a..fc56455236a2 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -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; /** @@ -37,6 +42,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} */ public static final int FLAG_CALCULATE_ROUNDED = 1 << 1; @@ -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)); } - /** {@hide} */ - public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) { - final boolean isNegative = (sizeBytes < 0); - float result = isNegative ? -sizeBytes : sizeBytes; - int suffix = com.android.internal.R.string.byteShort; - long mult = 1; - if (result > 900) { - suffix = com.android.internal.R.string.kilobyteShort; - mult = 1000; - result = result / 1000; + 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); } - if (result > 900) { - suffix = com.android.internal.R.string.megabyteShort; - mult *= 1000; - result = result / 1000; + } + + 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); } - if (result > 900) { - suffix = com.android.internal.R.string.gigabyteShort; - mult *= 1000; - result = result / 1000; + 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()); } - if (result > 900) { - suffix = com.android.internal.R.string.terabyteShort; - mult *= 1000; - result = result / 1000; + } + + 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); } - if (result > 900) { - suffix = com.android.internal.R.string.petabyteShort; - mult *= 1000; - result = result / 1000; + } + + /** {@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(); } - // 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. - final int roundFactor; - final String roundFormat; - if (mult == 1 || result >= 100) { - roundFactor = 1; - roundFormat = "%.0f"; - } else if (result < 1) { - roundFactor = 100; - roundFormat = "%.2f"; - } else if (result < 10) { - if ((flags & FLAG_SHORTER) != 0) { - roundFactor = 10; - roundFormat = "%.1f"; - } else { - roundFactor = 100; - roundFormat = "%.2f"; + 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; + MeasureUnit units = MeasureUnit.BYTE; + long mult = 1; + if (result > 900) { + units = MeasureUnit.KILOBYTE; + mult = 1000; + result = result / 1000; + } + if (result > 900) { + units = MeasureUnit.MEGABYTE; + mult *= 1000; + result = result / 1000; + } + if (result > 900) { + units = MeasureUnit.GIGABYTE; + mult *= 1000; + result = result / 1000; + } + if (result > 900) { + units = MeasureUnit.TERABYTE; + mult *= 1000; + result = result / 1000; } - } else { // 10 <= result < 100 - if ((flags & FLAG_SHORTER) != 0) { + if (result > 900) { + units = PETABYTE; + mult *= 1000; + result = result / 1000; + } + // 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 int roundDigits; + if (mult == 1 || result >= 100) { roundFactor = 1; - roundFormat = "%.0f"; - } else { + roundDigits = 0; + } else if (result < 1) { roundFactor = 100; - roundFormat = "%.2f"; + roundDigits = 2; + } else if (result < 10) { + if ((flags & FLAG_SHORTER) != 0) { + roundFactor = 10; + roundDigits = 1; + } else { + roundFactor = 100; + roundDigits = 2; + } + } else { // 10 <= result < 100 + if ((flags & FLAG_SHORTER) != 0) { + roundFactor = 1; + roundDigits = 0; + } else { + roundFactor = 100; + 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)... - final long roundedBytes = - (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0 - : (((long) Math.round(result * roundFactor)) * mult / roundFactor); + if (isNegative) { + result = -result; + } - final String units = res.getString(suffix); + // 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); - return new BytesResult(roundedString, units, roundedBytes); + return new RoundedBytesResult(result, units, roundDigits, roundedBytes); + } } /** diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 18e8af7836d2..8c26db43fc41 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -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. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8a7d8e3e8293..8c81f3f2a1f5 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -677,7 +677,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" /> @@ -733,7 +732,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" /> @@ -754,7 +752,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" /> @@ -957,7 +954,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" /> |