Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 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.messaging.util;
     18 
     19 import android.content.Context;
     20 import android.text.format.DateUtils;
     21 import android.text.format.Time;
     22 
     23 import com.android.messaging.Factory;
     24 import com.android.messaging.R;
     25 import com.google.common.annotations.VisibleForTesting;
     26 
     27 import java.text.SimpleDateFormat;
     28 import java.util.Date;
     29 import java.util.Locale;
     30 
     31 /**
     32  * Collection of date utilities.
     33  */
     34 public class Dates {
     35     public static final long SECOND_IN_MILLIS = 1000;
     36     public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
     37     public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
     38     public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
     39     public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
     40 
     41     // Flags to specify whether or not to use 12 or 24 hour mode.
     42     // Callers of methods in this class should never have to specify these; this is really
     43     // intended only for unit tests.
     44     @SuppressWarnings("deprecation")
     45     @VisibleForTesting public static final int FORCE_12_HOUR = DateUtils.FORMAT_12HOUR;
     46     @SuppressWarnings("deprecation")
     47     @VisibleForTesting public static final int FORCE_24_HOUR = DateUtils.FORMAT_24HOUR;
     48 
     49     /**
     50      * Private default constructor
     51      */
     52     private Dates() {
     53     }
     54 
     55     private static Context getContext() {
     56         return Factory.get().getApplicationContext();
     57     }
     58     /**
     59      * Get the relative time as a string
     60      *
     61      * @param time The time
     62      *
     63      * @return The relative time
     64      */
     65     public static CharSequence getRelativeTimeSpanString(final long time) {
     66         final long now = System.currentTimeMillis();
     67         if (now - time < DateUtils.MINUTE_IN_MILLIS) {
     68             // Also fixes bug where posts appear in the future
     69             return getContext().getResources().getText(R.string.posted_just_now);
     70         }
     71 
     72         // Workaround for b/5657035. The platform method {@link DateUtils#getRelativeTimeSpan()}
     73         // passes a null context to other platform methods. However, on some devices, this
     74         // context is dereferenced when it shouldn't be and an NPE is thrown. We catch that
     75         // here and use a slightly less precise time.
     76         try {
     77             return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS,
     78                     DateUtils.FORMAT_ABBREV_RELATIVE).toString();
     79         } catch (final NullPointerException npe) {
     80             return getShortRelativeTimeSpanString(time);
     81         }
     82     }
     83 
     84     public static CharSequence getConversationTimeString(final long time) {
     85         return getTimeString(time, true /*abbreviated*/, false /*minPeriodToday*/);
     86     }
     87 
     88     public static CharSequence getMessageTimeString(final long time) {
     89         return getTimeString(time, false /*abbreviated*/, false /*minPeriodToday*/);
     90     }
     91 
     92     public static CharSequence getWidgetTimeString(final long time, final boolean abbreviated) {
     93         return getTimeString(time, abbreviated, true /*minPeriodToday*/);
     94     }
     95 
     96     public static CharSequence getFastScrollPreviewTimeString(final long time) {
     97         return getTimeString(time, true /* abbreviated */, true /* minPeriodToday */);
     98     }
     99 
    100     public static CharSequence getMessageDetailsTimeString(final long time) {
    101         final Context context = getContext();
    102         int flags;
    103         if (android.text.format.DateFormat.is24HourFormat(context)) {
    104             flags = FORCE_24_HOUR;
    105         } else {
    106             flags = FORCE_12_HOUR;
    107         }
    108         return getOlderThanAYearTimestamp(time,
    109                 context.getResources().getConfiguration().locale, false /*abbreviated*/,
    110                 flags);
    111     }
    112 
    113     private static CharSequence getTimeString(final long time, final boolean abbreviated,
    114             final boolean minPeriodToday) {
    115         final Context context = getContext();
    116         int flags;
    117         if (android.text.format.DateFormat.is24HourFormat(context)) {
    118             flags = FORCE_24_HOUR;
    119         } else {
    120             flags = FORCE_12_HOUR;
    121         }
    122         return getTimestamp(time, System.currentTimeMillis(), abbreviated,
    123                 context.getResources().getConfiguration().locale, flags, minPeriodToday);
    124     }
    125 
    126     @VisibleForTesting
    127     public static CharSequence getTimestamp(final long time, final long now,
    128             final boolean abbreviated, final Locale locale, final int flags,
    129             final boolean minPeriodToday) {
    130         final long timeDiff = now - time;
    131 
    132         if (!minPeriodToday && timeDiff < DateUtils.MINUTE_IN_MILLIS) {
    133             return getLessThanAMinuteOldTimeString(abbreviated);
    134         } else if (!minPeriodToday && timeDiff < DateUtils.HOUR_IN_MILLIS) {
    135             return getLessThanAnHourOldTimeString(timeDiff, flags);
    136         } else if (getNumberOfDaysPassed(time, now) == 0) {
    137             return getTodayTimeStamp(time, flags);
    138         } else if (timeDiff < DateUtils.WEEK_IN_MILLIS) {
    139             return getThisWeekTimestamp(time, locale, abbreviated, flags);
    140         } else if (timeDiff < DateUtils.YEAR_IN_MILLIS) {
    141             return getThisYearTimestamp(time, locale, abbreviated, flags);
    142         } else {
    143             return getOlderThanAYearTimestamp(time, locale, abbreviated, flags);
    144         }
    145     }
    146 
    147     private static CharSequence getLessThanAMinuteOldTimeString(
    148             final boolean abbreviated) {
    149         return getContext().getResources().getText(
    150                 abbreviated ? R.string.posted_just_now : R.string.posted_now);
    151     }
    152 
    153     private static CharSequence getLessThanAnHourOldTimeString(final long timeDiff,
    154             final int flags) {
    155         final long count = (timeDiff / MINUTE_IN_MILLIS);
    156         final String format = getContext().getResources().getQuantityString(
    157                 R.plurals.num_minutes_ago, (int) count);
    158         return String.format(format, count);
    159     }
    160 
    161     private static CharSequence getTodayTimeStamp(final long time, final int flags) {
    162         return DateUtils.formatDateTime(getContext(), time,
    163                 DateUtils.FORMAT_SHOW_TIME | flags);
    164     }
    165 
    166     private static CharSequence getExplicitFormattedTime(final long time, final int flags,
    167             final String format24, final String format12) {
    168         SimpleDateFormat formatter;
    169         if ((flags & FORCE_24_HOUR) == FORCE_24_HOUR) {
    170             formatter = new SimpleDateFormat(format24);
    171         } else {
    172             formatter = new SimpleDateFormat(format12);
    173         }
    174         return formatter.format(new Date(time));
    175     }
    176 
    177     private static CharSequence getThisWeekTimestamp(final long time,
    178             final Locale locale, final boolean abbreviated, final int flags) {
    179         final Context context = getContext();
    180         if (abbreviated) {
    181             return DateUtils.formatDateTime(context, time,
    182                     DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY | flags);
    183         } else {
    184             if (locale.equals(Locale.US)) {
    185                 return getExplicitFormattedTime(time, flags, "EEE HH:mm", "EEE h:mmaa");
    186             } else {
    187                 return DateUtils.formatDateTime(context, time,
    188                         DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_TIME
    189                         | DateUtils.FORMAT_ABBREV_WEEKDAY
    190                         | flags);
    191             }
    192         }
    193     }
    194 
    195     private static CharSequence getThisYearTimestamp(final long time, final Locale locale,
    196             final boolean abbreviated, final int flags) {
    197         final Context context = getContext();
    198         if (abbreviated) {
    199             return DateUtils.formatDateTime(context, time,
    200                     DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH
    201                     | DateUtils.FORMAT_NO_YEAR | flags);
    202         } else {
    203             if (locale.equals(Locale.US)) {
    204                 return getExplicitFormattedTime(time, flags, "MMM d, HH:mm", "MMM d, h:mmaa");
    205             } else {
    206                 return DateUtils.formatDateTime(context, time,
    207                         DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME
    208                         | DateUtils.FORMAT_ABBREV_MONTH
    209                         | DateUtils.FORMAT_NO_YEAR
    210                         | flags);
    211             }
    212         }
    213     }
    214 
    215     private static CharSequence getOlderThanAYearTimestamp(final long time,
    216             final Locale locale, final boolean abbreviated, final int flags) {
    217         final Context context = getContext();
    218         if (abbreviated) {
    219             if (locale.equals(Locale.US)) {
    220                 return getExplicitFormattedTime(time, flags, "M/d/yy", "M/d/yy");
    221             } else {
    222                 return DateUtils.formatDateTime(context, time,
    223                         DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
    224                         | DateUtils.FORMAT_NUMERIC_DATE);
    225             }
    226         } else {
    227             if (locale.equals(Locale.US)) {
    228                 return getExplicitFormattedTime(time, flags, "M/d/yy, HH:mm", "M/d/yy, h:mmaa");
    229             } else {
    230                 return DateUtils.formatDateTime(context, time,
    231                         DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME
    232                         | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR
    233                         | flags);
    234             }
    235         }
    236     }
    237 
    238     public static CharSequence getShortRelativeTimeSpanString(final long time) {
    239         final long now = System.currentTimeMillis();
    240         final long duration = Math.abs(now - time);
    241 
    242         int resId;
    243         long count;
    244 
    245         final Context context = getContext();
    246 
    247         if (duration < HOUR_IN_MILLIS) {
    248             count = duration / MINUTE_IN_MILLIS;
    249             resId = R.plurals.num_minutes_ago;
    250         } else if (duration < DAY_IN_MILLIS) {
    251             count = duration / HOUR_IN_MILLIS;
    252             resId = R.plurals.num_hours_ago;
    253         } else if (duration < WEEK_IN_MILLIS) {
    254             count = getNumberOfDaysPassed(time, now);
    255             resId = R.plurals.num_days_ago;
    256         } else {
    257             // Although we won't be showing a time, there is a bug on some devices that use
    258             // the passed in context. On these devices, passing in a {@code null} context
    259             // here will generate an NPE. See b/5657035.
    260             return DateUtils.formatDateRange(context, time, time,
    261                     DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_ABBREV_RELATIVE);
    262         }
    263 
    264         final String format = context.getResources().getQuantityString(resId, (int) count);
    265         return String.format(format, count);
    266     }
    267 
    268     private static synchronized long getNumberOfDaysPassed(final long date1, final long date2) {
    269         if (sThenTime == null) {
    270             sThenTime = new Time();
    271         }
    272         sThenTime.set(date1);
    273         final int day1 = Time.getJulianDay(date1, sThenTime.gmtoff);
    274         sThenTime.set(date2);
    275         final int day2 = Time.getJulianDay(date2, sThenTime.gmtoff);
    276         return Math.abs(day2 - day1);
    277     }
    278 
    279     private static Time sThenTime;
    280 }
    281