1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.text.format; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.icu.text.DecimalFormat; 25 import android.icu.text.MeasureFormat; 26 import android.icu.text.NumberFormat; 27 import android.icu.text.UnicodeSet; 28 import android.icu.text.UnicodeSetSpanner; 29 import android.icu.util.Measure; 30 import android.icu.util.MeasureUnit; 31 import android.text.BidiFormatter; 32 import android.text.TextUtils; 33 import android.view.View; 34 35 import com.android.net.module.util.Inet4AddressUtils; 36 37 import java.math.BigDecimal; 38 import java.util.Locale; 39 40 /** 41 * Utility class to aid in formatting common values that are not covered 42 * by the {@link java.util.Formatter} class in {@link java.util} 43 */ 44 public final class Formatter { 45 46 /** {@hide} */ 47 public static final int FLAG_SHORTER = 1 << 0; 48 /** {@hide} */ 49 public static final int FLAG_CALCULATE_ROUNDED = 1 << 1; 50 /** {@hide} */ 51 public static final int FLAG_SI_UNITS = 1 << 2; 52 /** {@hide} */ 53 public static final int FLAG_IEC_UNITS = 1 << 3; 54 55 /** {@hide} */ 56 public static class BytesResult { 57 public final String value; 58 public final String units; 59 /** 60 * Content description of the {@link #units}. 61 * See {@link View#setContentDescription(CharSequence)} 62 */ 63 public final String unitsContentDescription; 64 public final long roundedBytes; 65 BytesResult(String value, String units, String unitsContentDescription, long roundedBytes)66 public BytesResult(String value, String units, String unitsContentDescription, 67 long roundedBytes) { 68 this.value = value; 69 this.units = units; 70 this.unitsContentDescription = unitsContentDescription; 71 this.roundedBytes = roundedBytes; 72 } 73 } 74 localeFromContext(@onNull Context context)75 private static Locale localeFromContext(@NonNull Context context) { 76 return context.getResources().getConfiguration().getLocales().get(0); 77 } 78 79 /** 80 * Wraps the source string in bidi formatting characters in RTL locales. 81 */ bidiWrap(@onNull Context context, String source)82 private static String bidiWrap(@NonNull Context context, String source) { 83 final Locale locale = localeFromContext(context); 84 if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) { 85 return BidiFormatter.getInstance(true /* RTL*/).unicodeWrap(source); 86 } else { 87 return source; 88 } 89 } 90 91 /** 92 * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc. 93 * 94 * <p>As of O, the prefixes are used in their standard meanings in the SI system, so kB = 1000 95 * bytes, MB = 1,000,000 bytes, etc.</p> 96 * 97 * <p class="note">In {@link android.os.Build.VERSION_CODES#N} and earlier, powers of 1024 are 98 * used instead, with KB = 1024 bytes, MB = 1,048,576 bytes, etc.</p> 99 * 100 * <p>If the context has a right-to-left locale, the returned string is wrapped in bidi 101 * formatting characters to make sure it's displayed correctly if inserted inside a 102 * right-to-left string. (This is useful in cases where the unit strings, like "MB", are 103 * left-to-right, but the locale is right-to-left.)</p> 104 * 105 * @param context Context to use to load the localized units 106 * @param sizeBytes size value to be formatted, in bytes 107 * @return formatted string with the number 108 */ formatFileSize(@ullable Context context, long sizeBytes)109 public static String formatFileSize(@Nullable Context context, long sizeBytes) { 110 return formatFileSize(context, sizeBytes, FLAG_SI_UNITS); 111 } 112 113 /** @hide */ formatFileSize(@ullable Context context, long sizeBytes, int flags)114 public static String formatFileSize(@Nullable Context context, long sizeBytes, int flags) { 115 if (context == null) { 116 return ""; 117 } 118 final RoundedBytesResult res = RoundedBytesResult.roundBytes(sizeBytes, flags); 119 return bidiWrap(context, formatRoundedBytesResult(context, res)); 120 } 121 122 /** 123 * Like {@link #formatFileSize}, but trying to generate shorter numbers 124 * (showing fewer digits of precision). 125 */ formatShortFileSize(@ullable Context context, long sizeBytes)126 public static String formatShortFileSize(@Nullable Context context, long sizeBytes) { 127 return formatFileSize(context, sizeBytes, FLAG_SI_UNITS | FLAG_SHORTER); 128 } 129 getByteSuffixOverride(@onNull Resources res)130 private static String getByteSuffixOverride(@NonNull Resources res) { 131 return res.getString(com.android.internal.R.string.byteShort); 132 } 133 getNumberFormatter(Locale locale, int fractionDigits)134 private static NumberFormat getNumberFormatter(Locale locale, int fractionDigits) { 135 final NumberFormat numberFormatter = NumberFormat.getInstance(locale); 136 numberFormatter.setMinimumFractionDigits(fractionDigits); 137 numberFormatter.setMaximumFractionDigits(fractionDigits); 138 numberFormatter.setGroupingUsed(false); 139 if (numberFormatter instanceof DecimalFormat) { 140 // We do this only for DecimalFormat, since in the general NumberFormat case, calling 141 // setRoundingMode may throw an exception. 142 numberFormatter.setRoundingMode(BigDecimal.ROUND_HALF_UP); 143 } 144 return numberFormatter; 145 } 146 deleteFirstFromString(String source, String toDelete)147 private static String deleteFirstFromString(String source, String toDelete) { 148 final int location = source.indexOf(toDelete); 149 if (location == -1) { 150 return source; 151 } else { 152 return source.substring(0, location) 153 + source.substring(location + toDelete.length(), source.length()); 154 } 155 } 156 formatMeasureShort(Locale locale, NumberFormat numberFormatter, float value, MeasureUnit units)157 private static String formatMeasureShort(Locale locale, NumberFormat numberFormatter, 158 float value, MeasureUnit units) { 159 final MeasureFormat measureFormatter = MeasureFormat.getInstance( 160 locale, MeasureFormat.FormatWidth.SHORT, numberFormatter); 161 return measureFormatter.format(new Measure(value, units)); 162 } 163 164 private static final UnicodeSetSpanner SPACES_AND_CONTROLS = 165 new UnicodeSetSpanner(new UnicodeSet("[[:Zs:][:Cf:]]").freeze()); 166 formatRoundedBytesResult( @onNull Context context, @NonNull RoundedBytesResult input)167 private static String formatRoundedBytesResult( 168 @NonNull Context context, @NonNull RoundedBytesResult input) { 169 final Locale locale = localeFromContext(context); 170 final NumberFormat numberFormatter = getNumberFormatter(locale, input.fractionDigits); 171 if (input.units == MeasureUnit.BYTE) { 172 // ICU spells out "byte" instead of "B". 173 final String formattedNumber = numberFormatter.format(input.value); 174 return context.getString(com.android.internal.R.string.fileSizeSuffix, 175 formattedNumber, getByteSuffixOverride(context.getResources())); 176 } else { 177 return formatMeasureShort(locale, numberFormatter, input.value, input.units); 178 } 179 } 180 181 /** {@hide} */ 182 public static class RoundedBytesResult { 183 public final float value; 184 public final MeasureUnit units; 185 public final int fractionDigits; 186 public final long roundedBytes; 187 RoundedBytesResult( float value, MeasureUnit units, int fractionDigits, long roundedBytes)188 private RoundedBytesResult( 189 float value, MeasureUnit units, int fractionDigits, long roundedBytes) { 190 this.value = value; 191 this.units = units; 192 this.fractionDigits = fractionDigits; 193 this.roundedBytes = roundedBytes; 194 } 195 196 /** 197 * Returns a RoundedBytesResult object based on the input size in bytes and the rounding 198 * flags. The result can be used for formatting. 199 */ roundBytes(long sizeBytes, int flags)200 public static RoundedBytesResult roundBytes(long sizeBytes, int flags) { 201 final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000; 202 final boolean isNegative = (sizeBytes < 0); 203 float result = isNegative ? -sizeBytes : sizeBytes; 204 MeasureUnit units = MeasureUnit.BYTE; 205 long mult = 1; 206 if (result > 900) { 207 units = MeasureUnit.KILOBYTE; 208 mult = unit; 209 result = result / unit; 210 } 211 if (result > 900) { 212 units = MeasureUnit.MEGABYTE; 213 mult *= unit; 214 result = result / unit; 215 } 216 if (result > 900) { 217 units = MeasureUnit.GIGABYTE; 218 mult *= unit; 219 result = result / unit; 220 } 221 if (result > 900) { 222 units = MeasureUnit.TERABYTE; 223 mult *= unit; 224 result = result / unit; 225 } 226 if (result > 900) { 227 units = MeasureUnit.PETABYTE; 228 mult *= unit; 229 result = result / unit; 230 } 231 // Note we calculate the rounded long by ourselves, but still let NumberFormat compute 232 // the rounded value. NumberFormat.format(0.1) might not return "0.1" due to floating 233 // point errors. 234 final int roundFactor; 235 final int roundDigits; 236 if (mult == 1 || result >= 100) { 237 roundFactor = 1; 238 roundDigits = 0; 239 } else if (result < 1) { 240 roundFactor = 100; 241 roundDigits = 2; 242 } else if (result < 10) { 243 if ((flags & FLAG_SHORTER) != 0) { 244 roundFactor = 10; 245 roundDigits = 1; 246 } else { 247 roundFactor = 100; 248 roundDigits = 2; 249 } 250 } else { // 10 <= result < 100 251 if ((flags & FLAG_SHORTER) != 0) { 252 roundFactor = 1; 253 roundDigits = 0; 254 } else { 255 roundFactor = 100; 256 roundDigits = 2; 257 } 258 } 259 260 if (isNegative) { 261 result = -result; 262 } 263 264 // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 265 // 80PB so it's okay (for now)... 266 final long roundedBytes = 267 (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0 268 : (((long) Math.round(result * roundFactor)) * mult / roundFactor); 269 270 return new RoundedBytesResult(result, units, roundDigits, roundedBytes); 271 } 272 } 273 274 /** {@hide} */ 275 @UnsupportedAppUsage formatBytes(Resources res, long sizeBytes, int flags)276 public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) { 277 final RoundedBytesResult rounded = RoundedBytesResult.roundBytes(sizeBytes, flags); 278 final Locale locale = res.getConfiguration().getLocales().get(0); 279 final NumberFormat numberFormatter = getNumberFormatter(locale, rounded.fractionDigits); 280 final String formattedNumber = numberFormatter.format(rounded.value); 281 // Since ICU does not give us access to the pattern, we need to extract the unit string 282 // from ICU, which we do by taking out the formatted number out of the formatted string 283 // and trimming the result of spaces and controls. 284 final String formattedMeasure = formatMeasureShort( 285 locale, numberFormatter, rounded.value, rounded.units); 286 final String numberRemoved = deleteFirstFromString(formattedMeasure, formattedNumber); 287 String units = SPACES_AND_CONTROLS.trim(numberRemoved).toString(); 288 String unitsContentDescription = units; 289 if (rounded.units == MeasureUnit.BYTE) { 290 // ICU spells out "byte" instead of "B". 291 units = getByteSuffixOverride(res); 292 } 293 return new BytesResult(formattedNumber, units, unitsContentDescription, 294 rounded.roundedBytes); 295 } 296 297 /** 298 * Returns a string in the canonical IPv4 format ###.###.###.### from a packed integer 299 * containing the IP address. The IPv4 address is expected to be in little-endian 300 * format (LSB first). That is, 0x01020304 will return "4.3.2.1". 301 * 302 * @deprecated Use {@link java.net.InetAddress#getHostAddress()}, which supports both IPv4 and 303 * IPv6 addresses. This method does not support IPv6 addresses. 304 */ 305 @Deprecated formatIpAddress(int ipv4Address)306 public static String formatIpAddress(int ipv4Address) { 307 return Inet4AddressUtils.intToInet4AddressHTL(ipv4Address).getHostAddress(); 308 } 309 310 private static final int SECONDS_PER_MINUTE = 60; 311 private static final int SECONDS_PER_HOUR = 60 * 60; 312 private static final int SECONDS_PER_DAY = 24 * 60 * 60; 313 private static final int MILLIS_PER_MINUTE = 1000 * 60; 314 315 /** 316 * Returns elapsed time for the given millis, in the following format: 317 * 1 day, 5 hr; will include at most two units, can go down to seconds precision. 318 * @param context the application context 319 * @param millis the elapsed time in milli seconds 320 * @return the formatted elapsed time 321 * @hide 322 */ 323 @UnsupportedAppUsage formatShortElapsedTime(Context context, long millis)324 public static String formatShortElapsedTime(Context context, long millis) { 325 long secondsLong = millis / 1000; 326 327 int days = 0, hours = 0, minutes = 0; 328 if (secondsLong >= SECONDS_PER_DAY) { 329 days = (int)(secondsLong / SECONDS_PER_DAY); 330 secondsLong -= days * SECONDS_PER_DAY; 331 } 332 if (secondsLong >= SECONDS_PER_HOUR) { 333 hours = (int)(secondsLong / SECONDS_PER_HOUR); 334 secondsLong -= hours * SECONDS_PER_HOUR; 335 } 336 if (secondsLong >= SECONDS_PER_MINUTE) { 337 minutes = (int)(secondsLong / SECONDS_PER_MINUTE); 338 secondsLong -= minutes * SECONDS_PER_MINUTE; 339 } 340 int seconds = (int)secondsLong; 341 342 final Locale locale = localeFromContext(context); 343 final MeasureFormat measureFormat = MeasureFormat.getInstance( 344 locale, MeasureFormat.FormatWidth.SHORT); 345 if (days >= 2 || (days > 0 && hours == 0)) { 346 days += (hours+12)/24; 347 return measureFormat.format(new Measure(days, MeasureUnit.DAY)); 348 } else if (days > 0) { 349 return measureFormat.formatMeasures( 350 new Measure(days, MeasureUnit.DAY), 351 new Measure(hours, MeasureUnit.HOUR)); 352 } else if (hours >= 2 || (hours > 0 && minutes == 0)) { 353 hours += (minutes+30)/60; 354 return measureFormat.format(new Measure(hours, MeasureUnit.HOUR)); 355 } else if (hours > 0) { 356 return measureFormat.formatMeasures( 357 new Measure(hours, MeasureUnit.HOUR), 358 new Measure(minutes, MeasureUnit.MINUTE)); 359 } else if (minutes >= 2 || (minutes > 0 && seconds == 0)) { 360 minutes += (seconds+30)/60; 361 return measureFormat.format(new Measure(minutes, MeasureUnit.MINUTE)); 362 } else if (minutes > 0) { 363 return measureFormat.formatMeasures( 364 new Measure(minutes, MeasureUnit.MINUTE), 365 new Measure(seconds, MeasureUnit.SECOND)); 366 } else { 367 return measureFormat.format(new Measure(seconds, MeasureUnit.SECOND)); 368 } 369 } 370 371 /** 372 * Returns elapsed time for the given millis, in the following format: 373 * 1 day, 5 hr; will include at most two units, can go down to minutes precision. 374 * @param context the application context 375 * @param millis the elapsed time in milli seconds 376 * @return the formatted elapsed time 377 * @hide 378 */ 379 @UnsupportedAppUsage formatShortElapsedTimeRoundingUpToMinutes(Context context, long millis)380 public static String formatShortElapsedTimeRoundingUpToMinutes(Context context, long millis) { 381 long minutesRoundedUp = (millis + MILLIS_PER_MINUTE - 1) / MILLIS_PER_MINUTE; 382 383 if (minutesRoundedUp == 0 || minutesRoundedUp == 1) { 384 final Locale locale = localeFromContext(context); 385 final MeasureFormat measureFormat = MeasureFormat.getInstance( 386 locale, MeasureFormat.FormatWidth.SHORT); 387 return measureFormat.format(new Measure(minutesRoundedUp, MeasureUnit.MINUTE)); 388 } 389 390 return formatShortElapsedTime(context, minutesRoundedUp * MILLIS_PER_MINUTE); 391 } 392 } 393