1 /* 2 * Copyright (C) 2017 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.dialer.calllogutils; 18 19 import android.content.Context; 20 import android.icu.lang.UCharacter; 21 import android.icu.text.BreakIterator; 22 import android.text.format.DateUtils; 23 import java.util.Calendar; 24 import java.util.Locale; 25 import java.util.concurrent.TimeUnit; 26 27 /** Static methods for formatting dates in the call log. */ 28 public final class CallLogDates { 29 30 /** 31 * Uses the new date formatting rules to format dates in the new call log. 32 * 33 * <p>Rules: 34 * 35 * <pre> 36 * if < 1 minute ago: "Just now"; 37 * else if < 1 hour ago: time relative to now (e.g., "8 min ago"); 38 * else if today: time (e.g., "12:15 PM"); 39 * else if < 7 days: day of week (e.g., "Wed"); 40 * else if < 1 year: date with month, day, but no year (e.g., "Jan 15"); 41 * else: date with month, day, and year (e.g., "Jan 15, 2018"). 42 * </pre> 43 * 44 * <p>Callers can decide whether to abbreviate date/time by specifying flag {@code 45 * abbreviateDateTime}. 46 */ newCallLogTimestampLabel( Context context, long nowMillis, long timestampMillis, boolean abbreviateDateTime)47 public static CharSequence newCallLogTimestampLabel( 48 Context context, long nowMillis, long timestampMillis, boolean abbreviateDateTime) { 49 // For calls logged less than 1 minute ago, display "Just now". 50 if (nowMillis - timestampMillis < TimeUnit.MINUTES.toMillis(1)) { 51 return context.getString(R.string.just_now); 52 } 53 54 // For calls logged less than 1 hour ago, display time relative to now (e.g., "8 min ago"). 55 if (nowMillis - timestampMillis < TimeUnit.HOURS.toMillis(1)) { 56 return abbreviateDateTime 57 ? DateUtils.getRelativeTimeSpanString( 58 timestampMillis, 59 nowMillis, 60 DateUtils.MINUTE_IN_MILLIS, 61 DateUtils.FORMAT_ABBREV_RELATIVE) 62 .toString() 63 // The platform method DateUtils#getRelativeTimeSpanString adds a dot ('.') after the 64 // abbreviated time unit for some languages (e.g., "8 min. ago") but we prefer not to 65 // have the dot. 66 .replace(".", "") 67 : DateUtils.getRelativeTimeSpanString( 68 timestampMillis, nowMillis, DateUtils.MINUTE_IN_MILLIS); 69 } 70 71 int dayDifference = getDayDifference(nowMillis, timestampMillis); 72 73 // For calls logged today, display time (e.g., "12:15 PM"). 74 if (dayDifference == 0) { 75 return DateUtils.formatDateTime(context, timestampMillis, DateUtils.FORMAT_SHOW_TIME); 76 } 77 78 // For calls logged within a week, display the day of week (e.g., "Wed"). 79 if (dayDifference < 7) { 80 return formatDayOfWeek(context, timestampMillis, abbreviateDateTime); 81 } 82 83 // For calls logged within a year, display month, day, but no year (e.g., "Jan 15"). 84 if (isWithinOneYear(nowMillis, timestampMillis)) { 85 return formatDate(context, timestampMillis, /* showYear = */ false, abbreviateDateTime); 86 } 87 88 // For calls logged no less than one year ago, display month, day, and year 89 // (e.g., "Jan 15, 2018"). 90 return formatDate(context, timestampMillis, /* showYear = */ true, abbreviateDateTime); 91 } 92 93 /** 94 * Formats the provided timestamp (in milliseconds) into date and time suitable for display in the 95 * current locale. 96 * 97 * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016 98 * may 25,20:02". 99 * 100 * <p>For pre-N devices, the returned value may not start with a capital if the local convention 101 * is to not capitalize day names. On N+ devices, the returned value is always capitalized. 102 */ formatDate(Context context, long timestamp)103 public static CharSequence formatDate(Context context, long timestamp) { 104 return toTitleCase( 105 DateUtils.formatDateTime( 106 context, 107 timestamp, 108 DateUtils.FORMAT_SHOW_TIME 109 | DateUtils.FORMAT_SHOW_DATE 110 | DateUtils.FORMAT_SHOW_WEEKDAY 111 | DateUtils.FORMAT_SHOW_YEAR)); 112 } 113 114 /** 115 * Formats the provided timestamp (in milliseconds) into the month, day, and optionally, year. 116 * 117 * <p>For example, returns a string like "Jan 15" or "Jan 15, 2018". 118 * 119 * <p>For pre-N devices, the returned value may not start with a capital if the local convention 120 * is to not capitalize day names. On N+ devices, the returned value is always capitalized. 121 */ formatDate( Context context, long timestamp, boolean showYear, boolean abbreviateDateTime)122 private static CharSequence formatDate( 123 Context context, long timestamp, boolean showYear, boolean abbreviateDateTime) { 124 int formatFlags = 0; 125 if (abbreviateDateTime) { 126 formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; 127 } 128 if (!showYear) { 129 formatFlags |= DateUtils.FORMAT_NO_YEAR; 130 } 131 132 return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags)); 133 } 134 135 /** 136 * Formats the provided timestamp (in milliseconds) into day of week. 137 * 138 * <p>For example, returns a string like "Wed" or "Chor". 139 * 140 * <p>For pre-N devices, the returned value may not start with a capital if the local convention 141 * is to not capitalize day names. On N+ devices, the returned value is always capitalized. 142 */ formatDayOfWeek( Context context, long timestamp, boolean abbreviateDateTime)143 private static CharSequence formatDayOfWeek( 144 Context context, long timestamp, boolean abbreviateDateTime) { 145 int formatFlags = 146 abbreviateDateTime 147 ? (DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY) 148 : DateUtils.FORMAT_SHOW_WEEKDAY; 149 return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags)); 150 } 151 toTitleCase(CharSequence value)152 private static CharSequence toTitleCase(CharSequence value) { 153 // We want the beginning of the date string to be capitalized, even if the word at the beginning 154 // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba” 155 // (not capitalized). To handle this issue we apply title casing to the start of the sentence so 156 // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02". 157 158 // Using the ICU library is safer than just applying toUpperCase() on the first letter of the 159 // word because in some languages, there can be multiple starting characters which should be 160 // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be 161 // capitalized together. 162 163 // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized are not 164 // lower-cased as part of the conversion. 165 return UCharacter.toTitleCase( 166 Locale.getDefault(), 167 value.toString(), 168 BreakIterator.getSentenceInstance(), 169 UCharacter.TITLECASE_NO_LOWERCASE); 170 } 171 172 /** 173 * Returns the absolute difference in days between two timestamps. It is the caller's 174 * responsibility to ensure both timestamps are in milliseconds. Failure to do so will result in 175 * undefined behavior. 176 * 177 * <p>Note that the difference is based on day boundaries, not 24-hour periods. 178 * 179 * <p>Examples: 180 * 181 * <ul> 182 * <li>The difference between 01/19/2018 00:00 and 01/19/2018 23:59 is 0. 183 * <li>The difference between 01/18/2018 23:59 and 01/19/2018 23:59 is 1. 184 * <li>The difference between 01/18/2018 00:00 and 01/19/2018 23:59 is 1. 185 * <li>The difference between 01/17/2018 23:59 and 01/19/2018 00:00 is 2. 186 * </ul> 187 */ getDayDifference(long firstTimestamp, long secondTimestamp)188 public static int getDayDifference(long firstTimestamp, long secondTimestamp) { 189 // Ensure secondTimestamp is no less than firstTimestamp 190 if (secondTimestamp < firstTimestamp) { 191 long t = firstTimestamp; 192 firstTimestamp = secondTimestamp; 193 secondTimestamp = t; 194 } 195 196 // Use secondTimestamp as reference 197 Calendar startOfReferenceDay = Calendar.getInstance(); 198 startOfReferenceDay.setTimeInMillis(secondTimestamp); 199 200 // This is attempting to find the start of the reference day, but it's not quite right due to 201 // daylight savings. Unfortunately there doesn't seem to be a way to get the correct start of 202 // the day without using Joda or Java8, both of which are disallowed. This means that the wrong 203 // formatting may be applied on days with time changes (though the displayed values will be 204 // correct). 205 startOfReferenceDay.add(Calendar.HOUR_OF_DAY, -startOfReferenceDay.get(Calendar.HOUR_OF_DAY)); 206 startOfReferenceDay.add(Calendar.MINUTE, -startOfReferenceDay.get(Calendar.MINUTE)); 207 startOfReferenceDay.add(Calendar.SECOND, -startOfReferenceDay.get(Calendar.SECOND)); 208 startOfReferenceDay.add(Calendar.MILLISECOND, -startOfReferenceDay.get(Calendar.MILLISECOND)); 209 210 Calendar other = Calendar.getInstance(); 211 other.setTimeInMillis(firstTimestamp); 212 213 int dayDifference = 0; 214 while (other.before(startOfReferenceDay)) { 215 startOfReferenceDay.add(Calendar.DATE, -1); 216 dayDifference++; 217 } 218 219 return dayDifference; 220 } 221 222 /** 223 * Returns true if the two timestamps are within one year. It is the caller's responsibility to 224 * ensure both timestamps are in milliseconds. Failure to do so will result in undefined behavior. 225 * 226 * <p>Note that the difference is based on 365/366-day periods. 227 * 228 * <p>Examples: 229 * 230 * <ul> 231 * <li>01/01/2018 00:00 and 12/31/2018 23:59 is within one year. 232 * <li>12/31/2017 23:59 and 12/31/2018 23:59 is not within one year. 233 * <li>12/31/2017 23:59 and 01/01/2018 00:00 is within one year. 234 * </ul> 235 */ isWithinOneYear(long firstTimestamp, long secondTimestamp)236 private static boolean isWithinOneYear(long firstTimestamp, long secondTimestamp) { 237 // Ensure secondTimestamp is no less than firstTimestamp 238 if (secondTimestamp < firstTimestamp) { 239 long t = firstTimestamp; 240 firstTimestamp = secondTimestamp; 241 secondTimestamp = t; 242 } 243 244 // Use secondTimestamp as reference 245 Calendar reference = Calendar.getInstance(); 246 reference.setTimeInMillis(secondTimestamp); 247 reference.add(Calendar.YEAR, -1); 248 249 Calendar other = Calendar.getInstance(); 250 other.setTimeInMillis(firstTimestamp); 251 252 return reference.before(other); 253 } 254 } 255