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