1 /* 2 * Copyright (C) 2019 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 com.android.car.settings.storage; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.icu.text.DecimalFormat; 23 import android.icu.text.MeasureFormat; 24 import android.icu.text.NumberFormat; 25 import android.icu.util.Measure; 26 import android.icu.util.MeasureUnit; 27 import android.text.BidiFormatter; 28 import android.text.TextUtils; 29 import android.view.View; 30 31 import java.math.BigDecimal; 32 import java.util.Locale; 33 34 /** 35 * Utility class to aid in formatting file sizes always with the same unit. This is modified from 36 * android.text.format.Formatter to fit this purpose. 37 */ 38 public final class FileSizeFormatter { 39 public static final long KILOBYTE_IN_BYTES = 1000; 40 public static final long MEGABYTE_IN_BYTES = KILOBYTE_IN_BYTES * 1000; 41 public static final long GIGABYTE_IN_BYTES = MEGABYTE_IN_BYTES * 1000; 42 43 private static class RoundedBytesResult { 44 public final float value; 45 public final MeasureUnit units; 46 public final int fractionDigits; 47 public final long roundedBytes; 48 RoundedBytesResult(float value, MeasureUnit units, int fractionDigits, long roundedBytes)49 private RoundedBytesResult(float value, MeasureUnit units, int fractionDigits, 50 long roundedBytes) { 51 this.value = value; 52 this.units = units; 53 this.fractionDigits = fractionDigits; 54 this.roundedBytes = roundedBytes; 55 } 56 } 57 localeFromContext(@onNull Context context)58 private static Locale localeFromContext(@NonNull Context context) { 59 return context.getResources().getConfiguration().locale; 60 } 61 bidiWrap(@onNull Context context, String source)62 private static String bidiWrap(@NonNull Context context, String source) { 63 Locale locale = localeFromContext(context); 64 if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) { 65 return BidiFormatter.getInstance(/* RTL= */ true).unicodeWrap(source); 66 } else { 67 return source; 68 } 69 } 70 getNumberFormatter(Locale locale, int fractionDigits)71 private static NumberFormat getNumberFormatter(Locale locale, int fractionDigits) { 72 NumberFormat numberFormatter = NumberFormat.getInstance(locale); 73 numberFormatter.setMinimumFractionDigits(fractionDigits); 74 numberFormatter.setMaximumFractionDigits(fractionDigits); 75 numberFormatter.setGroupingUsed(false); 76 if (numberFormatter instanceof DecimalFormat) { 77 // We do this only for DecimalFormat, since in the general NumberFormat case, calling 78 // setRoundingMode may throw an exception. 79 numberFormatter.setRoundingMode(BigDecimal.ROUND_HALF_UP); 80 } 81 return numberFormatter; 82 } 83 formatMeasureShort(Locale locale, NumberFormat numberFormatter, float value, MeasureUnit units)84 private static String formatMeasureShort(Locale locale, NumberFormat numberFormatter, 85 float value, MeasureUnit units) { 86 MeasureFormat measureFormatter = MeasureFormat.getInstance(locale, 87 MeasureFormat.FormatWidth.SHORT, numberFormatter); 88 return measureFormatter.format(new Measure(value, units)); 89 } 90 formatRoundedBytesResult(@onNull Context context, @NonNull RoundedBytesResult input)91 private static String formatRoundedBytesResult(@NonNull Context context, 92 @NonNull RoundedBytesResult input) { 93 Locale locale = localeFromContext(context); 94 NumberFormat numberFormatter = getNumberFormatter(locale, input.fractionDigits); 95 return formatMeasureShort(locale, numberFormatter, input.value, input.units); 96 } 97 98 /** 99 * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc. 100 * 101 * <p>As of O, the prefixes are used in their standard meanings in the SI system, so kB = 1000 102 * bytes, MB = 1,000,000 bytes, etc. 103 * 104 * <p class="note">In {@link android.os.Build.VERSION_CODES#N} and earlier, powers of 1024 are 105 * used instead, with KB = 1024 bytes, MB = 1,048,576 bytes, etc. 106 * 107 * <p>If the context has a right-to-left locale, the returned string is wrapped in bidi 108 * formatting characters to make sure it's displayed correctly if inserted inside a 109 * right-to-left string. (This is useful in cases where the unit strings, like "MB", are 110 * left-to-right, but the locale is right-to-left.) 111 * 112 * @param context Context to use to load the localized units 113 * @param sizeBytes size value to be formatted, in bytes 114 * @param unit The unit used for formatting. 115 * @param mult Amount of bytes in the unit. 116 * @return formatted string with the number 117 */ formatFileSize(@ullable Context context, long sizeBytes, MeasureUnit unit, long mult)118 public static String formatFileSize(@Nullable Context context, long sizeBytes, 119 MeasureUnit unit, long mult) { 120 if (context == null) { 121 return ""; 122 } 123 RoundedBytesResult res = formatBytes(sizeBytes, unit, mult); 124 return bidiWrap(context, formatRoundedBytesResult(context, res)); 125 } 126 127 /** 128 * A simplified version of the SettingsLib file size formatter. The primary difference is that 129 * this version always assumes it is doing a "short file size" and allows for a suffix to be 130 * provided. 131 * 132 * @param res Resources to fetch strings with. 133 * @param sizeBytes File size in bytes to format. 134 * @param suffix String id for the unit suffix. 135 * @param mult Amount of bytes in the unit. 136 */ formatBytes(long sizeBytes, MeasureUnit unit, long mult)137 private static RoundedBytesResult formatBytes(long sizeBytes, MeasureUnit unit, long mult) { 138 boolean isNegative = (sizeBytes < 0); 139 float result = isNegative ? -sizeBytes : sizeBytes; 140 result = result / mult; 141 // Note we calculate the rounded long by ourselves, but still let String.format() 142 // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to 143 // floating point errors. 144 int roundFactor; 145 int roundDigits; 146 if (mult == 1) { 147 roundFactor = 1; 148 roundDigits = 0; 149 } else if (result < 1) { 150 roundFactor = 100; 151 roundDigits = 2; 152 } else if (result < 10) { 153 roundFactor = 10; 154 roundDigits = 1; 155 } else { // 10 <= result < 100 156 roundFactor = 1; 157 roundDigits = 0; 158 } 159 160 if (isNegative) { 161 result = -result; 162 } 163 164 // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so 165 // it's okay (for now)... 166 long roundedBytes = (((long) Math.round(result * roundFactor)) * mult / roundFactor); 167 168 return new RoundedBytesResult(result, unit, roundDigits, roundedBytes); 169 } 170 }