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