1 /*
2  * Copyright (C) 2018 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.settingslib.utils;
18 
19 import android.content.Context;
20 import android.icu.text.MeasureFormat;
21 import android.icu.text.MeasureFormat.FormatWidth;
22 import android.icu.text.MessageFormat;
23 import android.icu.text.RelativeDateTimeFormatter;
24 import android.icu.text.RelativeDateTimeFormatter.RelativeUnit;
25 import android.icu.util.Measure;
26 import android.icu.util.MeasureUnit;
27 import android.icu.util.ULocale;
28 import android.text.SpannableStringBuilder;
29 import android.text.Spanned;
30 import android.text.style.TtsSpan;
31 
32 import com.android.settingslib.R;
33 
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.Locale;
37 import java.util.Map;
38 
39 /** Utility class for generally useful string methods **/
40 public class StringUtil {
41 
42     public static final int SECONDS_PER_MINUTE = 60;
43     public static final int SECONDS_PER_HOUR = 60 * 60;
44     public static final int SECONDS_PER_DAY = 24 * 60 * 60;
45 
46     private static final int LIMITED_TIME_UNIT_COUNT = 2;
47 
48     /**
49      * Returns elapsed time for the given millis, in the following format:
50      * 2 days, 5 hr, 40 min, 29 sec
51      *
52      * @param context     the application context
53      * @param millis      the elapsed time in milli seconds
54      * @param withSeconds include seconds?
55      * @param collapseTimeUnit limit the output to top 2 time unit
56      *                         e.g 2 days, 5 hr, 40 min, 29 sec will convert to 2 days, 5 hr
57      * @return the formatted elapsed time
58      */
formatElapsedTime(Context context, double millis, boolean withSeconds, boolean collapseTimeUnit)59     public static CharSequence formatElapsedTime(Context context, double millis,
60             boolean withSeconds, boolean collapseTimeUnit) {
61         SpannableStringBuilder sb = new SpannableStringBuilder();
62         int seconds = (int) Math.floor(millis / 1000);
63         if (!withSeconds) {
64             // Round up.
65             seconds += 30;
66         }
67 
68         int days = 0, hours = 0, minutes = 0;
69         if (seconds >= SECONDS_PER_DAY) {
70             days = seconds / SECONDS_PER_DAY;
71             seconds -= days * SECONDS_PER_DAY;
72         }
73         if (seconds >= SECONDS_PER_HOUR) {
74             hours = seconds / SECONDS_PER_HOUR;
75             seconds -= hours * SECONDS_PER_HOUR;
76         }
77         if (seconds >= SECONDS_PER_MINUTE) {
78             minutes = seconds / SECONDS_PER_MINUTE;
79             seconds -= minutes * SECONDS_PER_MINUTE;
80         }
81 
82         final ArrayList<Measure> measureList = new ArrayList(4);
83         if (days > 0) {
84             measureList.add(new Measure(days, MeasureUnit.DAY));
85         }
86         if (hours > 0) {
87             measureList.add(new Measure(hours, MeasureUnit.HOUR));
88         }
89         if (minutes > 0) {
90             measureList.add(new Measure(minutes, MeasureUnit.MINUTE));
91         }
92         if (withSeconds && seconds > 0) {
93             measureList.add(new Measure(seconds, MeasureUnit.SECOND));
94         }
95         if (measureList.size() == 0) {
96             // Everything addable was zero, so nothing was added. We add a zero.
97             measureList.add(new Measure(0, withSeconds ? MeasureUnit.SECOND : MeasureUnit.MINUTE));
98         }
99 
100         if (collapseTimeUnit && measureList.size() > LIMITED_TIME_UNIT_COUNT) {
101             // Limit the output to top 2 time unit.
102             measureList.subList(LIMITED_TIME_UNIT_COUNT, measureList.size()).clear();
103         }
104 
105         final Measure[] measureArray = measureList.toArray(new Measure[measureList.size()]);
106 
107         final Locale locale = context.getResources().getConfiguration().locale;
108         final MeasureFormat measureFormat = MeasureFormat.getInstance(
109                 locale, FormatWidth.SHORT);
110         sb.append(measureFormat.formatMeasures(measureArray));
111 
112         if (measureArray.length == 1 && MeasureUnit.MINUTE.equals(measureArray[0].getUnit())) {
113             // Add ttsSpan if it only have minute value, because it will be read as "meters"
114             final TtsSpan ttsSpan = new TtsSpan.MeasureBuilder().setNumber(minutes)
115                     .setUnit("minute").build();
116             sb.setSpan(ttsSpan, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
117         }
118 
119         return sb;
120     }
121 
122     /**
123      * Returns relative time for the given millis in the past with different format style.
124      * In a short format such as "2 days ago", "5 hr. ago", "40 min. ago", or "29 sec. ago".
125      * In a long format such as "2 days ago", "5 hours ago",  "40 minutes ago" or "29 seconds ago".
126      *
127      * <p>The unit is chosen to have good information value while only using one unit. So 27 hours
128      * and 50 minutes would be formatted as "28 hr. ago", while 50 hours would be formatted as
129      * "2 days ago".
130      *
131      * @param context     the application context
132      * @param millis      the elapsed time in milli seconds
133      * @param withSeconds include seconds?
134      * @param formatStyle format style
135      * @return the formatted elapsed time
136      */
formatRelativeTime(Context context, double millis, boolean withSeconds, RelativeDateTimeFormatter.Style formatStyle)137     public static CharSequence formatRelativeTime(Context context, double millis,
138             boolean withSeconds, RelativeDateTimeFormatter.Style formatStyle) {
139         final int seconds = (int) Math.floor(millis / 1000);
140         final RelativeUnit unit;
141         final int value;
142         if (withSeconds && seconds < 2 * SECONDS_PER_MINUTE) {
143             return context.getResources().getString(R.string.time_unit_just_now);
144         } else if (seconds < 2 * SECONDS_PER_HOUR) {
145             unit = RelativeUnit.MINUTES;
146             value = (seconds + SECONDS_PER_MINUTE / 2)
147                     / SECONDS_PER_MINUTE;
148         } else if (seconds < 2 * SECONDS_PER_DAY) {
149             unit = RelativeUnit.HOURS;
150             value = (seconds + SECONDS_PER_HOUR / 2)
151                     / SECONDS_PER_HOUR;
152         } else {
153             unit = RelativeUnit.DAYS;
154             value = (seconds + SECONDS_PER_DAY / 2)
155                     / SECONDS_PER_DAY;
156         }
157 
158         final Locale locale = context.getResources().getConfiguration().locale;
159         final RelativeDateTimeFormatter formatter = RelativeDateTimeFormatter.getInstance(
160                 ULocale.forLocale(locale),
161                 null /* default NumberFormat */,
162                 formatStyle,
163                 android.icu.text.DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE);
164 
165         return formatter.format(value, RelativeDateTimeFormatter.Direction.LAST, unit);
166     }
167 
168     /**
169      * Returns relative time for the given millis in the past, in a long format such as "2 days
170      * ago", "5 hours ago",  "40 minutes ago" or "29 seconds ago".
171      *
172      * <p>The unit is chosen to have good information value while only using one unit. So 27 hours
173      * and 50 minutes would be formatted as "28 hr. ago", while 50 hours would be formatted as
174      * "2 days ago".
175      *
176      * @param context     the application context
177      * @param millis      the elapsed time in milli seconds
178      * @param withSeconds include seconds?
179      * @return the formatted elapsed time
180      * @deprecated use {@link #formatRelativeTime(Context, double, boolean,
181      * RelativeDateTimeFormatter.Style)} instead.
182      */
183     @Deprecated
formatRelativeTime(Context context, double millis, boolean withSeconds)184     public static CharSequence formatRelativeTime(Context context, double millis,
185             boolean withSeconds) {
186         return formatRelativeTime(context, millis, withSeconds,
187                 RelativeDateTimeFormatter.Style.LONG);
188     }
189 
190     /**
191      * Get ICU plural string without additional arguments
192      *
193      * @param context Context used to get the string
194      * @param count The number used to get the correct string for the current language's plural
195      *              rules.
196      * @param resId Resource id of the string
197      *
198      * @return Formatted plural string
199      */
getIcuPluralsString(Context context, int count, int resId)200     public static String getIcuPluralsString(Context context, int count, int resId) {
201         MessageFormat msgFormat = new MessageFormat(context.getResources().getString(resId),
202                 Locale.getDefault());
203         Map<String, Object> arguments = new HashMap<>();
204         arguments.put("count", count);
205         return msgFormat.format(arguments);
206     }
207 
208     /**
209      * Get ICU plural string with additional arguments
210      *
211      * @param context Context used to get the string
212      * @param args String arguments
213      * @param resId Resource id of the string
214      *
215      * @return Formatted plural string
216      */
getIcuPluralsString(Context context, Map<String, Object> args, int resId)217     public static String getIcuPluralsString(Context context, Map<String, Object> args, int resId) {
218         MessageFormat msgFormat = new MessageFormat(context.getResources().getString(resId),
219                 Locale.getDefault());
220         return msgFormat.format(args);
221     }
222 }
223