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