| /* |
| * Copyright (C) 2015 The Android Open Source 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.messaging.util; |
| |
| import android.content.Context; |
| import android.text.format.DateUtils; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.R; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.text.SimpleDateFormat; |
| import java.time.Instant; |
| import java.time.LocalDateTime; |
| import java.time.temporal.ChronoUnit; |
| import java.time.ZoneId; |
| import java.util.Date; |
| import java.util.Locale; |
| |
| /** |
| * Collection of date utilities. |
| */ |
| public class Dates { |
| public static final long SECOND_IN_MILLIS = 1000; |
| public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; |
| public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; |
| public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; |
| public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; |
| |
| // Flags to specify whether or not to use 12 or 24 hour mode. |
| // Callers of methods in this class should never have to specify these; this is really |
| // intended only for unit tests. |
| @SuppressWarnings("deprecation") |
| @VisibleForTesting public static final int FORCE_12_HOUR = DateUtils.FORMAT_12HOUR; |
| @SuppressWarnings("deprecation") |
| @VisibleForTesting public static final int FORCE_24_HOUR = DateUtils.FORMAT_24HOUR; |
| |
| /** |
| * Private default constructor |
| */ |
| private Dates() { |
| } |
| |
| private static Context getContext() { |
| return Factory.get().getApplicationContext(); |
| } |
| /** |
| * Get the relative time as a string |
| * |
| * @param time The time |
| * |
| * @return The relative time |
| */ |
| public static CharSequence getRelativeTimeSpanString(final long time) { |
| final long now = System.currentTimeMillis(); |
| if (now - time < DateUtils.MINUTE_IN_MILLIS) { |
| // Also fixes bug where posts appear in the future |
| return getContext().getResources().getText(R.string.posted_just_now); |
| } |
| |
| // Workaround for b/5657035. The platform method {@link DateUtils#getRelativeTimeSpan()} |
| // passes a null context to other platform methods. However, on some devices, this |
| // context is dereferenced when it shouldn't be and an NPE is thrown. We catch that |
| // here and use a slightly less precise time. |
| try { |
| return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS, |
| DateUtils.FORMAT_ABBREV_RELATIVE).toString(); |
| } catch (final NullPointerException npe) { |
| return getShortRelativeTimeSpanString(time); |
| } |
| } |
| |
| public static CharSequence getConversationTimeString(final long time) { |
| return getTimeString(time, true /*abbreviated*/, false /*minPeriodToday*/); |
| } |
| |
| public static CharSequence getMessageTimeString(final long time) { |
| return getTimeString(time, false /*abbreviated*/, false /*minPeriodToday*/); |
| } |
| |
| public static CharSequence getWidgetTimeString(final long time, final boolean abbreviated) { |
| return getTimeString(time, abbreviated, true /*minPeriodToday*/); |
| } |
| |
| public static CharSequence getFastScrollPreviewTimeString(final long time) { |
| return getTimeString(time, true /* abbreviated */, true /* minPeriodToday */); |
| } |
| |
| public static CharSequence getMessageDetailsTimeString(final long time) { |
| final Context context = getContext(); |
| int flags; |
| if (android.text.format.DateFormat.is24HourFormat(context)) { |
| flags = FORCE_24_HOUR; |
| } else { |
| flags = FORCE_12_HOUR; |
| } |
| return getOlderThanAYearTimestamp(time, |
| context.getResources().getConfiguration().locale, false /*abbreviated*/, |
| flags); |
| } |
| |
| private static CharSequence getTimeString(final long time, final boolean abbreviated, |
| final boolean minPeriodToday) { |
| final Context context = getContext(); |
| int flags; |
| if (android.text.format.DateFormat.is24HourFormat(context)) { |
| flags = FORCE_24_HOUR; |
| } else { |
| flags = FORCE_12_HOUR; |
| } |
| return getTimestamp(time, System.currentTimeMillis(), abbreviated, |
| context.getResources().getConfiguration().locale, flags, minPeriodToday); |
| } |
| |
| @VisibleForTesting |
| public static CharSequence getTimestamp(final long time, final long now, |
| final boolean abbreviated, final Locale locale, final int flags, |
| final boolean minPeriodToday) { |
| final long timeDiff = now - time; |
| |
| if (!minPeriodToday && timeDiff < DateUtils.MINUTE_IN_MILLIS) { |
| return getLessThanAMinuteOldTimeString(abbreviated); |
| } else if (!minPeriodToday && timeDiff < DateUtils.HOUR_IN_MILLIS) { |
| return getLessThanAnHourOldTimeString(timeDiff, flags); |
| } else if (getNumberOfDaysPassed(time, now) == 0) { |
| return getTodayTimeStamp(time, flags); |
| } else if (timeDiff < DateUtils.WEEK_IN_MILLIS) { |
| return getThisWeekTimestamp(time, locale, abbreviated, flags); |
| } else if (timeDiff < DateUtils.YEAR_IN_MILLIS) { |
| return getThisYearTimestamp(time, locale, abbreviated, flags); |
| } else { |
| return getOlderThanAYearTimestamp(time, locale, abbreviated, flags); |
| } |
| } |
| |
| private static CharSequence getLessThanAMinuteOldTimeString( |
| final boolean abbreviated) { |
| return getContext().getResources().getText( |
| abbreviated ? R.string.posted_just_now : R.string.posted_now); |
| } |
| |
| private static CharSequence getLessThanAnHourOldTimeString(final long timeDiff, |
| final int flags) { |
| final long count = (timeDiff / MINUTE_IN_MILLIS); |
| final String format = getContext().getResources().getQuantityString( |
| R.plurals.num_minutes_ago, (int) count); |
| return String.format(format, count); |
| } |
| |
| private static CharSequence getTodayTimeStamp(final long time, final int flags) { |
| return DateUtils.formatDateTime(getContext(), time, |
| DateUtils.FORMAT_SHOW_TIME | flags); |
| } |
| |
| private static CharSequence getExplicitFormattedTime(final long time, final int flags, |
| final String format24, final String format12) { |
| SimpleDateFormat formatter; |
| if ((flags & FORCE_24_HOUR) == FORCE_24_HOUR) { |
| formatter = new SimpleDateFormat(format24); |
| } else { |
| formatter = new SimpleDateFormat(format12); |
| } |
| return formatter.format(new Date(time)); |
| } |
| |
| private static CharSequence getThisWeekTimestamp(final long time, |
| final Locale locale, final boolean abbreviated, final int flags) { |
| final Context context = getContext(); |
| if (abbreviated) { |
| return DateUtils.formatDateTime(context, time, |
| DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY | flags); |
| } else { |
| if (locale.equals(Locale.US)) { |
| return getExplicitFormattedTime(time, flags, "EEE HH:mm", "EEE h:mmaa"); |
| } else { |
| return DateUtils.formatDateTime(context, time, |
| DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_TIME |
| | DateUtils.FORMAT_ABBREV_WEEKDAY |
| | flags); |
| } |
| } |
| } |
| |
| private static CharSequence getThisYearTimestamp(final long time, final Locale locale, |
| final boolean abbreviated, final int flags) { |
| final Context context = getContext(); |
| if (abbreviated) { |
| return DateUtils.formatDateTime(context, time, |
| DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH |
| | DateUtils.FORMAT_NO_YEAR | flags); |
| } else { |
| if (locale.equals(Locale.US)) { |
| return getExplicitFormattedTime(time, flags, "MMM d, HH:mm", "MMM d, h:mmaa"); |
| } else { |
| return DateUtils.formatDateTime(context, time, |
| DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME |
| | DateUtils.FORMAT_ABBREV_MONTH |
| | DateUtils.FORMAT_NO_YEAR |
| | flags); |
| } |
| } |
| } |
| |
| private static CharSequence getOlderThanAYearTimestamp(final long time, |
| final Locale locale, final boolean abbreviated, final int flags) { |
| final Context context = getContext(); |
| if (abbreviated) { |
| if (locale.equals(Locale.US)) { |
| return getExplicitFormattedTime(time, flags, "M/d/yy", "M/d/yy"); |
| } else { |
| return DateUtils.formatDateTime(context, time, |
| DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
| | DateUtils.FORMAT_NUMERIC_DATE); |
| } |
| } else { |
| if (locale.equals(Locale.US)) { |
| return getExplicitFormattedTime(time, flags, "M/d/yy, HH:mm", "M/d/yy, h:mmaa"); |
| } else { |
| return DateUtils.formatDateTime(context, time, |
| DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME |
| | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR |
| | flags); |
| } |
| } |
| } |
| |
| public static CharSequence getShortRelativeTimeSpanString(final long time) { |
| final long now = System.currentTimeMillis(); |
| final long duration = Math.abs(now - time); |
| |
| int resId; |
| long count; |
| |
| final Context context = getContext(); |
| |
| if (duration < HOUR_IN_MILLIS) { |
| count = duration / MINUTE_IN_MILLIS; |
| resId = R.plurals.num_minutes_ago; |
| } else if (duration < DAY_IN_MILLIS) { |
| count = duration / HOUR_IN_MILLIS; |
| resId = R.plurals.num_hours_ago; |
| } else if (duration < WEEK_IN_MILLIS) { |
| count = getNumberOfDaysPassed(time, now); |
| resId = R.plurals.num_days_ago; |
| } else { |
| // Although we won't be showing a time, there is a bug on some devices that use |
| // the passed in context. On these devices, passing in a {@code null} context |
| // here will generate an NPE. See b/5657035. |
| return DateUtils.formatDateRange(context, time, time, |
| DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_ABBREV_RELATIVE); |
| } |
| |
| final String format = context.getResources().getQuantityString(resId, (int) count); |
| return String.format(format, count); |
| } |
| |
| private static long getNumberOfDaysPassed(final long date1, final long date2) { |
| LocalDateTime dateTime1 = LocalDateTime.ofInstant(Instant.ofEpochMilli(date1), |
| ZoneId.systemDefault()); |
| LocalDateTime dateTime2 = LocalDateTime.ofInstant(Instant.ofEpochMilli(date2), |
| ZoneId.systemDefault()); |
| return Math.abs(ChronoUnit.DAYS.between(dateTime2, dateTime1)); |
| } |
| } |