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.content.Context;
22 import android.content.res.Resources;
23 import android.net.NetworkUtils;
24 import android.text.BidiFormatter;
25 import android.text.TextUtils;
26 import android.view.View;
27 
28 import java.util.Locale;
29 
30 /**
31  * Utility class to aid in formatting common values that are not covered
32  * by the {@link java.util.Formatter} class in {@link java.util}
33  */
34 public final class Formatter {
35 
36     /** {@hide} */
37     public static final int FLAG_SHORTER = 1 << 0;
38     /** {@hide} */
39     public static final int FLAG_CALCULATE_ROUNDED = 1 << 1;
40 
41     /** {@hide} */
42     public static class BytesResult {
43         public final String value;
44         public final String units;
45         public final long roundedBytes;
46 
BytesResult(String value, String units, long roundedBytes)47         public BytesResult(String value, String units, long roundedBytes) {
48             this.value = value;
49             this.units = units;
50             this.roundedBytes = roundedBytes;
51         }
52     }
53 
54     /* Wraps the source string in bidi formatting characters in RTL locales */
bidiWrap(@onNull Context context, String source)55     private static String bidiWrap(@NonNull Context context, String source) {
56         final Locale locale = context.getResources().getConfiguration().locale;
57         if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
58             return BidiFormatter.getInstance(true /* RTL*/).unicodeWrap(source);
59         } else {
60             return source;
61         }
62     }
63 
64     /**
65      * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
66      *
67      * <p>As of O, the prefixes are used in their standard meanings in the SI system, so kB = 1000
68      * bytes, MB = 1,000,000 bytes, etc.</p>
69      *
70      * <p class="note">In {@link android.os.Build.VERSION_CODES#N} and earlier, powers of 1024 are
71      * used instead, with KB = 1024 bytes, MB = 1,048,576 bytes, etc.</p>
72      *
73      * <p>If the context has a right-to-left locale, the returned string is wrapped in bidi
74      * formatting characters to make sure it's displayed correctly if inserted inside a
75      * right-to-left string. (This is useful in cases where the unit strings, like "MB", are
76      * left-to-right, but the locale is right-to-left.)</p>
77      *
78      * @param context Context to use to load the localized units
79      * @param sizeBytes size value to be formatted, in bytes
80      * @return formatted string with the number
81      */
formatFileSize(@ullable Context context, long sizeBytes)82     public static String formatFileSize(@Nullable Context context, long sizeBytes) {
83         if (context == null) {
84             return "";
85         }
86         final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
87         return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
88                 res.value, res.units));
89     }
90 
91     /**
92      * Like {@link #formatFileSize}, but trying to generate shorter numbers
93      * (showing fewer digits of precision).
94      */
formatShortFileSize(@ullable Context context, long sizeBytes)95     public static String formatShortFileSize(@Nullable Context context, long sizeBytes) {
96         if (context == null) {
97             return "";
98         }
99         final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
100         return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
101                 res.value, res.units));
102     }
103 
104     /** {@hide} */
formatBytes(Resources res, long sizeBytes, int flags)105     public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
106         final boolean isNegative = (sizeBytes < 0);
107         float result = isNegative ? -sizeBytes : sizeBytes;
108         int suffix = com.android.internal.R.string.byteShort;
109         long mult = 1;
110         if (result > 900) {
111             suffix = com.android.internal.R.string.kilobyteShort;
112             mult = 1000;
113             result = result / 1000;
114         }
115         if (result > 900) {
116             suffix = com.android.internal.R.string.megabyteShort;
117             mult *= 1000;
118             result = result / 1000;
119         }
120         if (result > 900) {
121             suffix = com.android.internal.R.string.gigabyteShort;
122             mult *= 1000;
123             result = result / 1000;
124         }
125         if (result > 900) {
126             suffix = com.android.internal.R.string.terabyteShort;
127             mult *= 1000;
128             result = result / 1000;
129         }
130         if (result > 900) {
131             suffix = com.android.internal.R.string.petabyteShort;
132             mult *= 1000;
133             result = result / 1000;
134         }
135         // Note we calculate the rounded long by ourselves, but still let String.format()
136         // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
137         // floating point errors.
138         final int roundFactor;
139         final String roundFormat;
140         if (mult == 1 || result >= 100) {
141             roundFactor = 1;
142             roundFormat = "%.0f";
143         } else if (result < 1) {
144             roundFactor = 100;
145             roundFormat = "%.2f";
146         } else if (result < 10) {
147             if ((flags & FLAG_SHORTER) != 0) {
148                 roundFactor = 10;
149                 roundFormat = "%.1f";
150             } else {
151                 roundFactor = 100;
152                 roundFormat = "%.2f";
153             }
154         } else { // 10 <= result < 100
155             if ((flags & FLAG_SHORTER) != 0) {
156                 roundFactor = 1;
157                 roundFormat = "%.0f";
158             } else {
159                 roundFactor = 100;
160                 roundFormat = "%.2f";
161             }
162         }
163 
164         if (isNegative) {
165             result = -result;
166         }
167         final String roundedString = String.format(roundFormat, result);
168 
169         // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
170         // it's okay (for now)...
171         final long roundedBytes =
172                 (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
173                 : (((long) Math.round(result * roundFactor)) * mult / roundFactor);
174 
175         final String units = res.getString(suffix);
176 
177         return new BytesResult(roundedString, units, roundedBytes);
178     }
179 
180     /**
181      * Returns a string in the canonical IPv4 format ###.###.###.### from a packed integer
182      * containing the IP address. The IPv4 address is expected to be in little-endian
183      * format (LSB first). That is, 0x01020304 will return "4.3.2.1".
184      *
185      * @deprecated Use {@link java.net.InetAddress#getHostAddress()}, which supports both IPv4 and
186      *     IPv6 addresses. This method does not support IPv6 addresses.
187      */
188     @Deprecated
formatIpAddress(int ipv4Address)189     public static String formatIpAddress(int ipv4Address) {
190         return NetworkUtils.intToInetAddress(ipv4Address).getHostAddress();
191     }
192 
193     private static final int SECONDS_PER_MINUTE = 60;
194     private static final int SECONDS_PER_HOUR = 60 * 60;
195     private static final int SECONDS_PER_DAY = 24 * 60 * 60;
196     private static final int MILLIS_PER_MINUTE = 1000 * 60;
197 
198     /**
199      * Returns elapsed time for the given millis, in the following format:
200      * 1 day 5 hrs; will include at most two units, can go down to seconds precision.
201      * @param context the application context
202      * @param millis the elapsed time in milli seconds
203      * @return the formatted elapsed time
204      * @hide
205      */
formatShortElapsedTime(Context context, long millis)206     public static String formatShortElapsedTime(Context context, long millis) {
207         long secondsLong = millis / 1000;
208 
209         int days = 0, hours = 0, minutes = 0;
210         if (secondsLong >= SECONDS_PER_DAY) {
211             days = (int)(secondsLong / SECONDS_PER_DAY);
212             secondsLong -= days * SECONDS_PER_DAY;
213         }
214         if (secondsLong >= SECONDS_PER_HOUR) {
215             hours = (int)(secondsLong / SECONDS_PER_HOUR);
216             secondsLong -= hours * SECONDS_PER_HOUR;
217         }
218         if (secondsLong >= SECONDS_PER_MINUTE) {
219             minutes = (int)(secondsLong / SECONDS_PER_MINUTE);
220             secondsLong -= minutes * SECONDS_PER_MINUTE;
221         }
222         int seconds = (int)secondsLong;
223 
224         if (days >= 2) {
225             days += (hours+12)/24;
226             return context.getString(com.android.internal.R.string.durationDays, days);
227         } else if (days > 0) {
228             if (hours == 1) {
229                 return context.getString(com.android.internal.R.string.durationDayHour, days, hours);
230             }
231             return context.getString(com.android.internal.R.string.durationDayHours, days, hours);
232         } else if (hours >= 2) {
233             hours += (minutes+30)/60;
234             return context.getString(com.android.internal.R.string.durationHours, hours);
235         } else if (hours > 0) {
236             if (minutes == 1) {
237                 return context.getString(com.android.internal.R.string.durationHourMinute, hours,
238                         minutes);
239             }
240             return context.getString(com.android.internal.R.string.durationHourMinutes, hours,
241                     minutes);
242         } else if (minutes >= 2) {
243             minutes += (seconds+30)/60;
244             return context.getString(com.android.internal.R.string.durationMinutes, minutes);
245         } else if (minutes > 0) {
246             if (seconds == 1) {
247                 return context.getString(com.android.internal.R.string.durationMinuteSecond, minutes,
248                         seconds);
249             }
250             return context.getString(com.android.internal.R.string.durationMinuteSeconds, minutes,
251                     seconds);
252         } else if (seconds == 1) {
253             return context.getString(com.android.internal.R.string.durationSecond, seconds);
254         } else {
255             return context.getString(com.android.internal.R.string.durationSeconds, seconds);
256         }
257     }
258 
259     /**
260      * Returns elapsed time for the given millis, in the following format:
261      * 1 day 5 hrs; will include at most two units, can go down to minutes precision.
262      * @param context the application context
263      * @param millis the elapsed time in milli seconds
264      * @return the formatted elapsed time
265      * @hide
266      */
formatShortElapsedTimeRoundingUpToMinutes(Context context, long millis)267     public static String formatShortElapsedTimeRoundingUpToMinutes(Context context, long millis) {
268         long minutesRoundedUp = (millis + MILLIS_PER_MINUTE - 1) / MILLIS_PER_MINUTE;
269 
270         if (minutesRoundedUp == 0) {
271             return context.getString(com.android.internal.R.string.durationMinutes, 0);
272         } else if (minutesRoundedUp == 1) {
273             return context.getString(com.android.internal.R.string.durationMinute, 1);
274         }
275 
276         return formatShortElapsedTime(context, minutesRoundedUp * MILLIS_PER_MINUTE);
277     }
278 }
279