1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.content.browser.picker;
6 
7 import android.os.Build;
8 import android.widget.DatePicker;
9 import android.widget.DatePicker.OnDateChangedListener;
10 
11 import java.util.Calendar;
12 import java.util.Date;
13 import java.util.GregorianCalendar;
14 import java.util.TimeZone;
15 
16 /**
17  * Sets the current, min, and max values on the given DatePicker.
18  */
19 public class DateDialogNormalizer {
20 
21     /**
22      * Stores a date (year-month-day) and the number of milliseconds corresponding to that date
23      * according to the DatePicker's calendar.
24      */
25     private static class DateAndMillis {
26         /**
27          * Number of milliseconds from the epoch (1970-01-01) to the beginning of year-month-day
28          * in the default time zone (TimeZone.getDefault()) using the Julian/Gregorian split
29          * calendar. This value is interopable with {@link DatePicker#getMinDate} and
30          * {@link DatePicker#setMinDate}.
31          */
32         public final long millisForPicker;
33 
34         public final int year;
35         public final int month;  // 0-based
36         public final int day;
37 
DateAndMillis(long millisForPicker, int year, int month, int day)38         DateAndMillis(long millisForPicker, int year, int month, int day) {
39             this.millisForPicker = millisForPicker;
40             this.year = year;
41             this.month = month;
42             this.day = day;
43         }
44 
create(long millisUtc)45         static DateAndMillis create(long millisUtc) {
46             // millisUtc uses the Gregorian calendar only, so disable the Julian changeover date.
47             GregorianCalendar utcCal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
48             utcCal.setGregorianChange(new Date(Long.MIN_VALUE));
49             utcCal.setTimeInMillis(millisUtc);
50             int year = utcCal.get(Calendar.YEAR);
51             int month = utcCal.get(Calendar.MONTH);
52             int day = utcCal.get(Calendar.DAY_OF_MONTH);
53             return create(year, month, day);
54         }
55 
create(int year, int month, int day)56         static DateAndMillis create(int year, int month, int day) {
57             // By contrast, millisForPicker uses the default Gregorian/Julian changeover date.
58             Calendar defaultTimeZoneCal = Calendar.getInstance(TimeZone.getDefault());
59             defaultTimeZoneCal.clear();
60             defaultTimeZoneCal.set(year, month, day);
61             long millisForPicker = defaultTimeZoneCal.getTimeInMillis();
62             return new DateAndMillis(millisForPicker, year, month, day);
63         }
64     }
65 
setLimits(DatePicker picker, long currentMillisForPicker, long minMillisForPicker, long maxMillisForPicker)66     private static void setLimits(DatePicker picker, long currentMillisForPicker,
67             long minMillisForPicker, long maxMillisForPicker) {
68         // On Lollipop only (not KitKat or Marshmallow), DatePicker has terrible performance for
69         // large date ranges. This causes problems when the min or max date isn't set in HTML, in
70         // which case these values default to the min and max possible values for the JavaScript
71         // Date object (1CE and 275760CE). As a workaround, limit the date range to 5000 years
72         // before and after the current date. In practice, this doesn't limit users since scrolling
73         // through 5000 years in the DatePicker is highly impractical anyway. See
74         // http://crbug.com/441060
75         if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
76                 || Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {
77             final long maxRangeMillis = 5000L * 365 * 24 * 60 * 60 * 1000;
78             minMillisForPicker = Math.max(minMillisForPicker,
79                     currentMillisForPicker - maxRangeMillis);
80             maxMillisForPicker = Math.min(maxMillisForPicker,
81                     currentMillisForPicker + maxRangeMillis);
82         }
83 
84         // On KitKat and earlier, DatePicker requires the minDate is always less than maxDate, even
85         // during the process of setting those values (eek), so set them in an order that preserves
86         // this invariant throughout.
87         if (minMillisForPicker > picker.getMaxDate()) {
88             picker.setMaxDate(maxMillisForPicker);
89             picker.setMinDate(minMillisForPicker);
90         } else {
91             picker.setMinDate(minMillisForPicker);
92             picker.setMaxDate(maxMillisForPicker);
93         }
94     }
95 
96     /**
97      * Sets the current, min, and max values on the given DatePicker and ensures that
98      * min <= current <= max, adjusting current and max if needed.
99      *
100      * @param year The current year to set.
101      * @param month The current month to set. 0-based.
102      * @param day The current day to set.
103      * @param minMillisUtc The minimum allowed date, in milliseconds from the epoch according to a
104      *                     proleptic Gregorian calendar (no Julian switch).
105      * @param maxMillisUtc The maximum allowed date, in milliseconds from the epoch according to a
106      *                     proleptic Gregorian calendar (no Julian switch).
107      */
normalize(DatePicker picker, final OnDateChangedListener listener, int year, int month, int day, long minMillisUtc, long maxMillisUtc)108     public static void normalize(DatePicker picker, final OnDateChangedListener listener,
109             int year, int month, int day, long minMillisUtc, long maxMillisUtc) {
110         DateAndMillis currentDate = DateAndMillis.create(year, month, day);
111         DateAndMillis minDate = DateAndMillis.create(minMillisUtc);
112         DateAndMillis maxDate = DateAndMillis.create(maxMillisUtc);
113 
114         // Ensure min <= current <= max, adjusting current and max if needed.
115         if (maxDate.millisForPicker < minDate.millisForPicker) {
116             maxDate = minDate;
117         }
118         if (currentDate.millisForPicker < minDate.millisForPicker) {
119             currentDate = minDate;
120         } else if (currentDate.millisForPicker > maxDate.millisForPicker) {
121             currentDate = maxDate;
122         }
123 
124         setLimits(picker, currentDate.millisForPicker, minDate.millisForPicker,
125                 maxDate.millisForPicker);
126         picker.init(currentDate.year, currentDate.month, currentDate.day, listener);
127     }
128 }
129