| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * Copyright (C) 2023 The LineageOS Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.dialer.calllogutils; |
| |
| import android.content.Context; |
| import android.icu.lang.UCharacter; |
| import android.icu.text.BreakIterator; |
| import android.text.format.DateUtils; |
| |
| import java.util.Calendar; |
| import java.util.Locale; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Static methods for formatting dates in the call log. */ |
| public final class CallLogDates { |
| |
| /** |
| * Uses the new date formatting rules to format dates in the new call log. |
| * |
| * <p>Rules: |
| * |
| * <pre> |
| * if < 1 minute ago: "Just now"; |
| * else if < 1 hour ago: time relative to now (e.g., "8 min ago"); |
| * else if today: time (e.g., "12:15 PM"); |
| * else if < 7 days: day of week (e.g., "Wed"); |
| * else if < 1 year: date with month, day, but no year (e.g., "Jan 15"); |
| * else: date with month, day, and year (e.g., "Jan 15, 2018"). |
| * </pre> |
| * |
| * <p>Callers can decide whether to abbreviate date/time by specifying flag {@code |
| * abbreviateDateTime}. |
| */ |
| public static CharSequence newCallLogTimestampLabel( |
| Context context, long nowMillis, long timestampMillis, boolean abbreviateDateTime) { |
| // For calls logged less than 1 minute ago, display "Just now". |
| if (nowMillis - timestampMillis < TimeUnit.MINUTES.toMillis(1)) { |
| return context.getString(R.string.just_now); |
| } |
| |
| // For calls logged less than 1 hour ago, display time relative to now (e.g., "8 min ago"). |
| if (nowMillis - timestampMillis < TimeUnit.HOURS.toMillis(1)) { |
| return abbreviateDateTime |
| ? DateUtils.getRelativeTimeSpanString( |
| timestampMillis, |
| nowMillis, |
| DateUtils.MINUTE_IN_MILLIS, |
| DateUtils.FORMAT_ABBREV_RELATIVE) |
| .toString() |
| // The platform method DateUtils#getRelativeTimeSpanString adds a dot ('.') after the |
| // abbreviated time unit for some languages (e.g., "8 min. ago") but we prefer not to |
| // have the dot. |
| .replace(".", "") |
| : DateUtils.getRelativeTimeSpanString( |
| timestampMillis, nowMillis, DateUtils.MINUTE_IN_MILLIS); |
| } |
| |
| int dayDifference = getDayDifference(nowMillis, timestampMillis); |
| |
| // For calls logged today, display time (e.g., "12:15 PM"). |
| if (dayDifference == 0) { |
| return DateUtils.formatDateTime(context, timestampMillis, DateUtils.FORMAT_SHOW_TIME); |
| } |
| |
| // For calls logged within a week, display the day of week (e.g., "Wed"). |
| if (dayDifference < 7) { |
| return formatDayOfWeek(context, timestampMillis, abbreviateDateTime); |
| } |
| |
| // For calls logged within a year, display month, day, but no year (e.g., "Jan 15"). |
| if (isWithinOneYear(nowMillis, timestampMillis)) { |
| return formatDate(context, timestampMillis, /* showYear = */ false, abbreviateDateTime); |
| } |
| |
| // For calls logged no less than one year ago, display month, day, and year |
| // (e.g., "Jan 15, 2018"). |
| return formatDate(context, timestampMillis, /* showYear = */ true, abbreviateDateTime); |
| } |
| |
| /** |
| * Formats the provided timestamp (in milliseconds) into date and time suitable for display in the |
| * current locale. |
| * |
| * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016 |
| * may 25,20:02". |
| * |
| * <p>For pre-N devices, the returned value may not start with a capital if the local convention |
| * is to not capitalize day names. On N+ devices, the returned value is always capitalized. |
| */ |
| public static CharSequence formatDate(Context context, long timestamp) { |
| return toTitleCase( |
| DateUtils.formatDateTime( |
| context, |
| timestamp, |
| DateUtils.FORMAT_SHOW_TIME |
| | DateUtils.FORMAT_SHOW_DATE |
| | DateUtils.FORMAT_SHOW_WEEKDAY |
| | DateUtils.FORMAT_SHOW_YEAR)); |
| } |
| |
| /** |
| * Formats the provided timestamp (in milliseconds) into the month, day, and optionally, year. |
| * |
| * <p>For example, returns a string like "Jan 15" or "Jan 15, 2018". |
| * |
| * <p>For pre-N devices, the returned value may not start with a capital if the local convention |
| * is to not capitalize day names. On N+ devices, the returned value is always capitalized. |
| */ |
| private static CharSequence formatDate( |
| Context context, long timestamp, boolean showYear, boolean abbreviateDateTime) { |
| int formatFlags = 0; |
| if (abbreviateDateTime) { |
| formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; |
| } |
| if (!showYear) { |
| formatFlags |= DateUtils.FORMAT_NO_YEAR; |
| } |
| |
| return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags)); |
| } |
| |
| /** |
| * Formats the provided timestamp (in milliseconds) into day of week. |
| * |
| * <p>For example, returns a string like "Wed" or "Chor". |
| * |
| * <p>For pre-N devices, the returned value may not start with a capital if the local convention |
| * is to not capitalize day names. On N+ devices, the returned value is always capitalized. |
| */ |
| private static CharSequence formatDayOfWeek( |
| Context context, long timestamp, boolean abbreviateDateTime) { |
| int formatFlags = |
| abbreviateDateTime |
| ? (DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY) |
| : DateUtils.FORMAT_SHOW_WEEKDAY; |
| return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags)); |
| } |
| |
| private static CharSequence toTitleCase(CharSequence value) { |
| // We want the beginning of the date string to be capitalized, even if the word at the beginning |
| // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba” |
| // (not capitalized). To handle this issue we apply title casing to the start of the sentence so |
| // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02". |
| |
| // Using the ICU library is safer than just applying toUpperCase() on the first letter of the |
| // word because in some languages, there can be multiple starting characters which should be |
| // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be |
| // capitalized together. |
| |
| // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized are not |
| // lower-cased as part of the conversion. |
| return UCharacter.toTitleCase( |
| Locale.getDefault(), |
| value.toString(), |
| BreakIterator.getSentenceInstance(), |
| UCharacter.TITLECASE_NO_LOWERCASE); |
| } |
| |
| /** |
| * Returns the absolute difference in days between two timestamps. It is the caller's |
| * responsibility to ensure both timestamps are in milliseconds. Failure to do so will result in |
| * undefined behavior. |
| * |
| * <p>Note that the difference is based on day boundaries, not 24-hour periods. |
| * |
| * <p>Examples: |
| * |
| * <ul> |
| * <li>The difference between 01/19/2018 00:00 and 01/19/2018 23:59 is 0. |
| * <li>The difference between 01/18/2018 23:59 and 01/19/2018 23:59 is 1. |
| * <li>The difference between 01/18/2018 00:00 and 01/19/2018 23:59 is 1. |
| * <li>The difference between 01/17/2018 23:59 and 01/19/2018 00:00 is 2. |
| * </ul> |
| */ |
| public static int getDayDifference(long firstTimestamp, long secondTimestamp) { |
| // Ensure secondTimestamp is no less than firstTimestamp |
| if (secondTimestamp < firstTimestamp) { |
| long t = firstTimestamp; |
| firstTimestamp = secondTimestamp; |
| secondTimestamp = t; |
| } |
| |
| // Use secondTimestamp as reference |
| Calendar startOfReferenceDay = Calendar.getInstance(); |
| startOfReferenceDay.setTimeInMillis(secondTimestamp); |
| |
| // This is attempting to find the start of the reference day, but it's not quite right due to |
| // daylight savings. Unfortunately there doesn't seem to be a way to get the correct start of |
| // the day without using Joda or Java8, both of which are disallowed. This means that the wrong |
| // formatting may be applied on days with time changes (though the displayed values will be |
| // correct). |
| startOfReferenceDay.add(Calendar.HOUR_OF_DAY, -startOfReferenceDay.get(Calendar.HOUR_OF_DAY)); |
| startOfReferenceDay.add(Calendar.MINUTE, -startOfReferenceDay.get(Calendar.MINUTE)); |
| startOfReferenceDay.add(Calendar.SECOND, -startOfReferenceDay.get(Calendar.SECOND)); |
| startOfReferenceDay.add(Calendar.MILLISECOND, -startOfReferenceDay.get(Calendar.MILLISECOND)); |
| |
| Calendar other = Calendar.getInstance(); |
| other.setTimeInMillis(firstTimestamp); |
| |
| int dayDifference = 0; |
| while (other.before(startOfReferenceDay)) { |
| startOfReferenceDay.add(Calendar.DATE, -1); |
| dayDifference++; |
| } |
| |
| return dayDifference; |
| } |
| |
| /** |
| * Returns true if the two timestamps are within one year. It is the caller's responsibility to |
| * ensure both timestamps are in milliseconds. Failure to do so will result in undefined behavior. |
| * |
| * <p>Note that the difference is based on 365/366-day periods. |
| * |
| * <p>Examples: |
| * |
| * <ul> |
| * <li>01/01/2018 00:00 and 12/31/2018 23:59 is within one year. |
| * <li>12/31/2017 23:59 and 12/31/2018 23:59 is not within one year. |
| * <li>12/31/2017 23:59 and 01/01/2018 00:00 is within one year. |
| * </ul> |
| */ |
| private static boolean isWithinOneYear(long firstTimestamp, long secondTimestamp) { |
| // Ensure secondTimestamp is no less than firstTimestamp |
| if (secondTimestamp < firstTimestamp) { |
| long t = firstTimestamp; |
| firstTimestamp = secondTimestamp; |
| secondTimestamp = t; |
| } |
| |
| // Use secondTimestamp as reference |
| Calendar reference = Calendar.getInstance(); |
| reference.setTimeInMillis(secondTimestamp); |
| reference.add(Calendar.YEAR, -1); |
| |
| Calendar other = Calendar.getInstance(); |
| other.setTimeInMillis(firstTimestamp); |
| |
| return reference.before(other); |
| } |
| } |