1 /*
2  * Copyright (C) 2013 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 android.text.format;
18 
19 import static android.text.format.DateUtils.FORMAT_SHOW_TIME;
20 import static android.text.format.DateUtils.FORMAT_UTC;
21 
22 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
23 
24 import android.icu.util.Calendar;
25 import android.icu.util.ULocale;
26 import android.util.LruCache;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import java.text.FieldPosition;
31 import java.util.TimeZone;
32 
33 /**
34  * A wrapper of {@link android.icu.text.DateIntervalFormat} used by {@link DateUtilsBridge}.
35  *
36  * @hide
37  */
38 @VisibleForTesting(visibility = PACKAGE)
39 public final class DateIntervalFormat {
40 
41     private static final LruCache<String, android.icu.text.DateIntervalFormat> CACHED_FORMATTERS =
42             new LruCache<>(8);
43 
DateIntervalFormat()44     private DateIntervalFormat() {
45     }
46 
47     /**
48      * Format a date range.
49      */
50     @VisibleForTesting(visibility = PACKAGE)
formatDateRange(long startMs, long endMs, int flags, String olsonId)51     public static String formatDateRange(long startMs, long endMs, int flags, String olsonId) {
52         if ((flags & FORMAT_UTC) != 0) {
53             olsonId = "UTC";
54         }
55         // We create a java.util.TimeZone here to use libcore's data and libcore's olson ID /
56         // pseudo-tz logic.
57         TimeZone tz = (olsonId != null) ? TimeZone.getTimeZone(olsonId) : TimeZone.getDefault();
58         android.icu.util.TimeZone icuTimeZone = DateUtilsBridge.icuTimeZone(tz);
59         ULocale icuLocale = ULocale.getDefault();
60         return formatDateRange(icuLocale, icuTimeZone, startMs, endMs, flags);
61     }
62 
63     /**
64      * Format a date range. This is our slightly more sensible internal API.
65      * A truly reasonable replacement would take a skeleton instead of int flags.
66      */
67     @VisibleForTesting(visibility = PACKAGE)
formatDateRange(ULocale icuLocale, android.icu.util.TimeZone icuTimeZone, long startMs, long endMs, int flags)68     public static String formatDateRange(ULocale icuLocale, android.icu.util.TimeZone icuTimeZone,
69             long startMs, long endMs, int flags) {
70         Calendar startCalendar = DateUtilsBridge.createIcuCalendar(icuTimeZone, icuLocale, startMs);
71         Calendar endCalendar;
72         if (startMs == endMs) {
73             endCalendar = startCalendar;
74         } else {
75             endCalendar = DateUtilsBridge.createIcuCalendar(icuTimeZone, icuLocale, endMs);
76         }
77 
78         // Special handling when the range ends at midnight:
79         // - If we're not showing times, and the range is non-empty, we fudge the end date so we
80         // don't count the day that's about to start.
81         // - If we are showing times, and the range ends at exactly 00:00 of the day following
82         // its start (which can be thought of as 24:00 the same day), we fudge the end date so we
83         // don't show the dates --- unless the start is anything displayed as 00:00, in which case
84         // we include both dates to disambiguate.
85         // This is not the behavior of icu4j's DateIntervalFormat, but it's the required behavior
86         // of Android's DateUtils.formatDateRange.
87         if (isExactlyMidnight(endCalendar)) {
88             boolean showTime = (flags & FORMAT_SHOW_TIME) == FORMAT_SHOW_TIME;
89             boolean endsDayAfterStart = DateUtilsBridge.dayDistance(startCalendar, endCalendar)
90                     == 1;
91             if ((!showTime && startMs != endMs)
92                     || (endsDayAfterStart
93                     && !DateUtilsBridge.isDisplayMidnightUsingSkeleton(startCalendar))) {
94                 endCalendar.add(Calendar.DAY_OF_MONTH, -1);
95             }
96         }
97 
98         String skeleton = DateUtilsBridge.toSkeleton(startCalendar, endCalendar, flags);
99         synchronized (CACHED_FORMATTERS) {
100             android.icu.text.DateIntervalFormat formatter =
101                     getFormatter(skeleton, icuLocale, icuTimeZone);
102             return formatter.format(startCalendar, endCalendar, new StringBuffer(),
103                     new FieldPosition(0)).toString();
104         }
105     }
106 
getFormatter(String skeleton, ULocale locale, android.icu.util.TimeZone icuTimeZone)107     private static android.icu.text.DateIntervalFormat getFormatter(String skeleton, ULocale locale,
108             android.icu.util.TimeZone icuTimeZone) {
109         String key = skeleton + "\t" + locale + "\t" + icuTimeZone;
110         android.icu.text.DateIntervalFormat formatter = CACHED_FORMATTERS.get(key);
111         if (formatter != null) {
112             return formatter;
113         }
114         formatter = android.icu.text.DateIntervalFormat.getInstance(skeleton, locale);
115         formatter.setTimeZone(icuTimeZone);
116         CACHED_FORMATTERS.put(key, formatter);
117         return formatter;
118     }
119 
isExactlyMidnight(Calendar c)120     private static boolean isExactlyMidnight(Calendar c) {
121         return c.get(Calendar.HOUR_OF_DAY) == 0
122                 && c.get(Calendar.MINUTE) == 0
123                 && c.get(Calendar.SECOND) == 0
124                 && c.get(Calendar.MILLISECOND) == 0;
125     }
126 }
127