1fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian/*
2fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * Copyright (C) 2017 The Android Open Source Project
3fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian *
4fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * Licensed under the Apache License, Version 2.0 (the "License");
5fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * you may not use this file except in compliance with the License.
6fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * You may obtain a copy of the License at
7fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian *
8fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian *      http://www.apache.org/licenses/LICENSE-2.0
9fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian *
10fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * Unless required by applicable law or agreed to in writing, software
11fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * distributed under the License is distributed on an "AS IS" BASIS,
12fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * See the License for the specific language governing permissions and
14fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian * limitations under the License.
15fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian */
16fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
17fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianpackage com.android.dialer.calllogutils;
18fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
19fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.content.Context;
20fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.icu.lang.UCharacter;
21fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.icu.text.BreakIterator;
22fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.os.Build.VERSION;
23fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.os.Build.VERSION_CODES;
24fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport android.text.format.DateUtils;
25fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport java.util.Calendar;
26fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport java.util.Locale;
27fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianimport java.util.concurrent.TimeUnit;
28fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
29fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian/** Static methods for formatting dates in the call log. */
30fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanianpublic final class CallLogDates {
31fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
32fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  /**
33fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * Uses the new date formatting rules to format dates in the new call log.
34fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
35fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * <p>Rules:
36fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
37fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * <pre>
385680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   if < 1 minute ago: "Just now";
39861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh   *   else if < 1 hour ago: time relative to now (e.g., "8 min ago");
405680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   else if today: time (e.g., "12:15 PM");
415680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   else if < 7 days: abbreviated day of week (e.g., "Wed");
425680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   else if < 1 year: date with abbreviated month, day, but no year (e.g., "Jan 15");
435680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   else: date with abbreviated month, day, and year (e.g., "Jan 15, 2018").
44fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * </pre>
45fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   */
46fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  public static CharSequence newCallLogTimestampLabel(
47fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      Context context, long nowMillis, long timestampMillis) {
485680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // For calls logged less than 1 minute ago, display "Just now".
49fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    if (nowMillis - timestampMillis < TimeUnit.MINUTES.toMillis(1)) {
505680b01ecb566e60a63c3a3362ec31f912cef692linyuh      return context.getString(R.string.just_now);
51fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    }
525680b01ecb566e60a63c3a3362ec31f912cef692linyuh
53861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh    // For calls logged less than 1 hour ago, display time relative to now (e.g., "8 min ago").
545680b01ecb566e60a63c3a3362ec31f912cef692linyuh    if (nowMillis - timestampMillis < TimeUnit.HOURS.toMillis(1)) {
555680b01ecb566e60a63c3a3362ec31f912cef692linyuh      return DateUtils.getRelativeTimeSpanString(
56861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh              timestampMillis,
57861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh              nowMillis,
58861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh              DateUtils.MINUTE_IN_MILLIS,
59861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh              DateUtils.FORMAT_ABBREV_RELATIVE)
60861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh          .toString()
61861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh          // The platform method DateUtils#getRelativeTimeSpanString adds a dot ('.') after the
62861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh          // abbreviated time unit for some languages (e.g., "8 min. ago") but we prefer not to have
63861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh          // the dot.
64861d8cd2dc75f80e5a6480145bbe29a1f5156acflinyuh          .replace(".", "");
655680b01ecb566e60a63c3a3362ec31f912cef692linyuh    }
665680b01ecb566e60a63c3a3362ec31f912cef692linyuh
675680b01ecb566e60a63c3a3362ec31f912cef692linyuh    int dayDifference = getDayDifference(nowMillis, timestampMillis);
685680b01ecb566e60a63c3a3362ec31f912cef692linyuh
695680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // For calls logged today, display time (e.g., "12:15 PM").
705680b01ecb566e60a63c3a3362ec31f912cef692linyuh    if (dayDifference == 0) {
715680b01ecb566e60a63c3a3362ec31f912cef692linyuh      return DateUtils.formatDateTime(context, timestampMillis, DateUtils.FORMAT_SHOW_TIME);
72fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    }
735680b01ecb566e60a63c3a3362ec31f912cef692linyuh
745680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // For calls logged within a week, display the abbreviated day of week (e.g., "Wed").
755680b01ecb566e60a63c3a3362ec31f912cef692linyuh    if (dayDifference < 7) {
765680b01ecb566e60a63c3a3362ec31f912cef692linyuh      return formatDayOfWeek(context, timestampMillis);
775680b01ecb566e60a63c3a3362ec31f912cef692linyuh    }
785680b01ecb566e60a63c3a3362ec31f912cef692linyuh
795680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // For calls logged within a year, display abbreviated month, day, but no year (e.g., "Jan 15").
805680b01ecb566e60a63c3a3362ec31f912cef692linyuh    if (isWithinOneYear(nowMillis, timestampMillis)) {
815680b01ecb566e60a63c3a3362ec31f912cef692linyuh      return formatAbbreviatedDate(context, timestampMillis, /* showYear = */ false);
82fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    }
835680b01ecb566e60a63c3a3362ec31f912cef692linyuh
845680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // For calls logged no less than one year ago, display abbreviated month, day, and year
855680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // (e.g., "Jan 15, 2018").
865680b01ecb566e60a63c3a3362ec31f912cef692linyuh    return formatAbbreviatedDate(context, timestampMillis, /* showYear = */ true);
87fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
88fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
89fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  /**
905680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * Formats the provided timestamp (in milliseconds) into date and time suitable for display in the
915680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * current locale.
92fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
93fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
94fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * may 25,20:02".
95fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
96fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * <p>For pre-N devices, the returned value may not start with a capital if the local convention
97fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
98fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   */
995680b01ecb566e60a63c3a3362ec31f912cef692linyuh  public static CharSequence formatDate(Context context, long timestamp) {
100fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    return toTitleCase(
101fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        DateUtils.formatDateTime(
102fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian            context,
1035680b01ecb566e60a63c3a3362ec31f912cef692linyuh            timestamp,
104fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian            DateUtils.FORMAT_SHOW_TIME
105fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                | DateUtils.FORMAT_SHOW_DATE
106fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                | DateUtils.FORMAT_SHOW_WEEKDAY
107fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian                | DateUtils.FORMAT_SHOW_YEAR));
108fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
109fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
110fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  /**
1115680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * Formats the provided timestamp (in milliseconds) into abbreviated day of week.
112fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
1135680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * <p>For example, returns a string like "Wed" or "Chor".
114fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
115fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * <p>For pre-N devices, the returned value may not start with a capital if the local convention
116fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
117fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   */
1185680b01ecb566e60a63c3a3362ec31f912cef692linyuh  private static CharSequence formatDayOfWeek(Context context, long timestamp) {
119fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    return toTitleCase(
1205680b01ecb566e60a63c3a3362ec31f912cef692linyuh        DateUtils.formatDateTime(
1215680b01ecb566e60a63c3a3362ec31f912cef692linyuh            context, timestamp, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY));
122fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
123fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
124fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  /**
1255680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * Formats the provided timestamp (in milliseconds) into the month abbreviation, day, and
1265680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * optionally, year.
127fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
1285680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * <p>For example, returns a string like "Jan 15" or "Jan 15, 2018".
129fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   *
130fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * <p>For pre-N devices, the returned value may not start with a capital if the local convention
131fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
132fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian   */
1335680b01ecb566e60a63c3a3362ec31f912cef692linyuh  private static CharSequence formatAbbreviatedDate(
1345680b01ecb566e60a63c3a3362ec31f912cef692linyuh      Context context, long timestamp, boolean showYear) {
1355680b01ecb566e60a63c3a3362ec31f912cef692linyuh    int flags = DateUtils.FORMAT_ABBREV_MONTH;
1365680b01ecb566e60a63c3a3362ec31f912cef692linyuh    if (!showYear) {
1375680b01ecb566e60a63c3a3362ec31f912cef692linyuh      flags |= DateUtils.FORMAT_NO_YEAR;
1385680b01ecb566e60a63c3a3362ec31f912cef692linyuh    }
1395680b01ecb566e60a63c3a3362ec31f912cef692linyuh
1405680b01ecb566e60a63c3a3362ec31f912cef692linyuh    return toTitleCase(DateUtils.formatDateTime(context, timestamp, flags));
141fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
142fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
143fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  private static CharSequence toTitleCase(CharSequence value) {
144fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // We want the beginning of the date string to be capitalized, even if the word at the beginning
145fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba”
146fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // (not capitalized). To handle this issue we apply title casing to the start of the sentence so
147fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02".
148fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    //
149fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // The ICU library was not available in Android until N, so we can only do this in N+ devices.
150fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // Pre-N devices will still see incorrect capitalization in some languages.
151fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    if (VERSION.SDK_INT < VERSION_CODES.N) {
152fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian      return value;
153fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    }
154fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
155fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // Using the ICU library is safer than just applying toUpperCase() on the first letter of the
156fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // word because in some languages, there can be multiple starting characters which should be
157fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be
158fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // capitalized together.
159fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
160fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized are not
161fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // lower-cased as part of the conversion.
162fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    return UCharacter.toTitleCase(
163fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        Locale.getDefault(),
164fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        value.toString(),
165fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        BreakIterator.getSentenceInstance(),
166fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian        UCharacter.TITLECASE_NO_LOWERCASE);
167fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
168fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
169fdaa46618ce61344bc83a66590863d126c47b05flinyuh  /**
170fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * Returns the absolute difference in days between two timestamps. It is the caller's
171fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * responsibility to ensure both timestamps are in milliseconds. Failure to do so will result in
172fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * undefined behavior.
173fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *
174fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * <p>Note that the difference is based on day boundaries, not 24-hour periods.
175fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *
176fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * <p>Examples:
177fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *
178fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * <ul>
179fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *   <li>The difference between 01/19/2018 00:00 and 01/19/2018 23:59 is 0.
180fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *   <li>The difference between 01/18/2018 23:59 and 01/19/2018 23:59 is 1.
181fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *   <li>The difference between 01/18/2018 00:00 and 01/19/2018 23:59 is 1.
182fdaa46618ce61344bc83a66590863d126c47b05flinyuh   *   <li>The difference between 01/17/2018 23:59 and 01/19/2018 00:00 is 2.
183fdaa46618ce61344bc83a66590863d126c47b05flinyuh   * </ul>
184fdaa46618ce61344bc83a66590863d126c47b05flinyuh   */
185fdaa46618ce61344bc83a66590863d126c47b05flinyuh  public static int getDayDifference(long firstTimestamp, long secondTimestamp) {
1865680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // Ensure secondTimestamp is no less than firstTimestamp
187fdaa46618ce61344bc83a66590863d126c47b05flinyuh    if (secondTimestamp < firstTimestamp) {
188fdaa46618ce61344bc83a66590863d126c47b05flinyuh      long t = firstTimestamp;
189fdaa46618ce61344bc83a66590863d126c47b05flinyuh      firstTimestamp = secondTimestamp;
190fdaa46618ce61344bc83a66590863d126c47b05flinyuh      secondTimestamp = t;
191fdaa46618ce61344bc83a66590863d126c47b05flinyuh    }
192fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
193fdaa46618ce61344bc83a66590863d126c47b05flinyuh    // Use secondTimestamp as reference
194fdaa46618ce61344bc83a66590863d126c47b05flinyuh    Calendar startOfReferenceDay = Calendar.getInstance();
195fdaa46618ce61344bc83a66590863d126c47b05flinyuh    startOfReferenceDay.setTimeInMillis(secondTimestamp);
196fdaa46618ce61344bc83a66590863d126c47b05flinyuh
197fdaa46618ce61344bc83a66590863d126c47b05flinyuh    // This is attempting to find the start of the reference day, but it's not quite right due to
198fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // daylight savings. Unfortunately there doesn't seem to be a way to get the correct start of
199fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // the day without using Joda or Java8, both of which are disallowed. This means that the wrong
200fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // formatting may be applied on days with time changes (though the displayed values will be
201fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian    // correct).
202fdaa46618ce61344bc83a66590863d126c47b05flinyuh    startOfReferenceDay.add(Calendar.HOUR_OF_DAY, -startOfReferenceDay.get(Calendar.HOUR_OF_DAY));
203fdaa46618ce61344bc83a66590863d126c47b05flinyuh    startOfReferenceDay.add(Calendar.MINUTE, -startOfReferenceDay.get(Calendar.MINUTE));
204fdaa46618ce61344bc83a66590863d126c47b05flinyuh    startOfReferenceDay.add(Calendar.SECOND, -startOfReferenceDay.get(Calendar.SECOND));
205fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
206fdaa46618ce61344bc83a66590863d126c47b05flinyuh    Calendar other = Calendar.getInstance();
207fdaa46618ce61344bc83a66590863d126c47b05flinyuh    other.setTimeInMillis(firstTimestamp);
208fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
209fdaa46618ce61344bc83a66590863d126c47b05flinyuh    int dayDifference = 0;
210fdaa46618ce61344bc83a66590863d126c47b05flinyuh    while (other.before(startOfReferenceDay)) {
211fdaa46618ce61344bc83a66590863d126c47b05flinyuh      startOfReferenceDay.add(Calendar.DATE, -1);
212fdaa46618ce61344bc83a66590863d126c47b05flinyuh      dayDifference++;
213fdaa46618ce61344bc83a66590863d126c47b05flinyuh    }
214fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
215fdaa46618ce61344bc83a66590863d126c47b05flinyuh    return dayDifference;
216fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
217fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
2185680b01ecb566e60a63c3a3362ec31f912cef692linyuh  /**
2195680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * Returns true if the two timestamps are within one year. It is the caller's responsibility to
2205680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * ensure both timestamps are in milliseconds. Failure to do so will result in undefined behavior.
2215680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *
2225680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * <p>Note that the difference is based on 365/366-day periods.
2235680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *
2245680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * <p>Examples:
2255680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *
2265680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * <ul>
2275680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   <li>01/01/2018 00:00 and 12/31/2018 23:59 is within one year.
2285680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   <li>12/31/2017 23:59 and 12/31/2018 23:59 is not within one year.
2295680b01ecb566e60a63c3a3362ec31f912cef692linyuh   *   <li>12/31/2017 23:59 and 01/01/2018 00:00 is within one year.
2305680b01ecb566e60a63c3a3362ec31f912cef692linyuh   * </ul>
2315680b01ecb566e60a63c3a3362ec31f912cef692linyuh   */
2325680b01ecb566e60a63c3a3362ec31f912cef692linyuh  private static boolean isWithinOneYear(long firstTimestamp, long secondTimestamp) {
2335680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // Ensure secondTimestamp is no less than firstTimestamp
2345680b01ecb566e60a63c3a3362ec31f912cef692linyuh    if (secondTimestamp < firstTimestamp) {
2355680b01ecb566e60a63c3a3362ec31f912cef692linyuh      long t = firstTimestamp;
2365680b01ecb566e60a63c3a3362ec31f912cef692linyuh      firstTimestamp = secondTimestamp;
2375680b01ecb566e60a63c3a3362ec31f912cef692linyuh      secondTimestamp = t;
2385680b01ecb566e60a63c3a3362ec31f912cef692linyuh    }
2395680b01ecb566e60a63c3a3362ec31f912cef692linyuh
2405680b01ecb566e60a63c3a3362ec31f912cef692linyuh    // Use secondTimestamp as reference
2415680b01ecb566e60a63c3a3362ec31f912cef692linyuh    Calendar reference = Calendar.getInstance();
2425680b01ecb566e60a63c3a3362ec31f912cef692linyuh    reference.setTimeInMillis(secondTimestamp);
2435680b01ecb566e60a63c3a3362ec31f912cef692linyuh    reference.add(Calendar.YEAR, -1);
244fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
2455680b01ecb566e60a63c3a3362ec31f912cef692linyuh    Calendar other = Calendar.getInstance();
2465680b01ecb566e60a63c3a3362ec31f912cef692linyuh    other.setTimeInMillis(firstTimestamp);
247fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian
2485680b01ecb566e60a63c3a3362ec31f912cef692linyuh    return reference.before(other);
249fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian  }
250fc0eb8ccebcc7846db5e8b5c5430070055679bfaEric Erfanian}
251