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