• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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