• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }