1/*
2 * Copyright (C) 2007 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
17package android.widget;
18
19import android.annotation.Nullable;
20import android.annotation.Widget;
21import android.content.Context;
22import android.content.res.Configuration;
23import android.content.res.TypedArray;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.TextUtils;
27import android.text.InputType;
28import android.text.format.DateFormat;
29import android.text.format.DateUtils;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.util.SparseArray;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.accessibility.AccessibilityEvent;
36import android.view.accessibility.AccessibilityNodeInfo;
37import android.view.inputmethod.EditorInfo;
38import android.view.inputmethod.InputMethodManager;
39import android.widget.NumberPicker.OnValueChangeListener;
40
41import com.android.internal.R;
42
43import java.text.DateFormatSymbols;
44import java.text.ParseException;
45import java.text.SimpleDateFormat;
46import java.util.Arrays;
47import java.util.Calendar;
48import java.util.Locale;
49import java.util.TimeZone;
50
51import libcore.icu.ICU;
52
53/**
54 * This class is a widget for selecting a date. The date can be selected by a
55 * year, month, and day spinners or a {@link CalendarView}. The set of spinners
56 * and the calendar view are automatically synchronized. The client can
57 * customize whether only the spinners, or only the calendar view, or both to be
58 * displayed. Also the minimal and maximal date from which dates to be selected
59 * can be customized.
60 * <p>
61 * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
62 * guide.
63 * </p>
64 * <p>
65 * For a dialog using this view, see {@link android.app.DatePickerDialog}.
66 * </p>
67 *
68 * @attr ref android.R.styleable#DatePicker_startYear
69 * @attr ref android.R.styleable#DatePicker_endYear
70 * @attr ref android.R.styleable#DatePicker_maxDate
71 * @attr ref android.R.styleable#DatePicker_minDate
72 * @attr ref android.R.styleable#DatePicker_spinnersShown
73 * @attr ref android.R.styleable#DatePicker_calendarViewShown
74 * @attr ref android.R.styleable#DatePicker_dayOfWeekBackground
75 * @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance
76 * @attr ref android.R.styleable#DatePicker_headerBackground
77 * @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance
78 * @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance
79 * @attr ref android.R.styleable#DatePicker_headerYearTextAppearance
80 * @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance
81 * @attr ref android.R.styleable#DatePicker_yearListSelectorColor
82 * @attr ref android.R.styleable#DatePicker_calendarTextColor
83 */
84@Widget
85public class DatePicker extends FrameLayout {
86    private static final String LOG_TAG = DatePicker.class.getSimpleName();
87
88    private static final int MODE_SPINNER = 1;
89    private static final int MODE_CALENDAR = 2;
90
91    private final DatePickerDelegate mDelegate;
92
93    /**
94     * The callback used to indicate the user changes\d the date.
95     */
96    public interface OnDateChangedListener {
97
98        /**
99         * Called upon a date change.
100         *
101         * @param view The view associated with this listener.
102         * @param year The year that was set.
103         * @param monthOfYear The month that was set (0-11) for compatibility
104         *            with {@link java.util.Calendar}.
105         * @param dayOfMonth The day of the month that was set.
106         */
107        void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
108    }
109
110    public DatePicker(Context context) {
111        this(context, null);
112    }
113
114    public DatePicker(Context context, AttributeSet attrs) {
115        this(context, attrs, R.attr.datePickerStyle);
116    }
117
118    public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) {
119        this(context, attrs, defStyleAttr, 0);
120    }
121
122    public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
123        super(context, attrs, defStyleAttr, defStyleRes);
124
125        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
126                defStyleAttr, defStyleRes);
127        final int mode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER);
128        final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0);
129        a.recycle();
130
131        switch (mode) {
132            case MODE_CALENDAR:
133                mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes);
134                break;
135            case MODE_SPINNER:
136            default:
137                mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes);
138                break;
139        }
140
141        if (firstDayOfWeek != 0) {
142            setFirstDayOfWeek(firstDayOfWeek);
143        }
144    }
145
146    private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs,
147            int defStyleAttr, int defStyleRes) {
148        return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
149    }
150
151    private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs,
152            int defStyleAttr, int defStyleRes) {
153        return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr,
154                defStyleRes);
155    }
156
157    /**
158     * Initialize the state. If the provided values designate an inconsistent
159     * date the values are normalized before updating the spinners.
160     *
161     * @param year The initial year.
162     * @param monthOfYear The initial month <strong>starting from zero</strong>.
163     * @param dayOfMonth The initial day of the month.
164     * @param onDateChangedListener How user is notified date is changed by
165     *            user, can be null.
166     */
167    public void init(int year, int monthOfYear, int dayOfMonth,
168                     OnDateChangedListener onDateChangedListener) {
169        mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener);
170    }
171
172    /**
173     * Update the current date.
174     *
175     * @param year The year.
176     * @param month The month which is <strong>starting from zero</strong>.
177     * @param dayOfMonth The day of the month.
178     */
179    public void updateDate(int year, int month, int dayOfMonth) {
180        mDelegate.updateDate(year, month, dayOfMonth);
181    }
182
183    /**
184     * @return The selected year.
185     */
186    public int getYear() {
187        return mDelegate.getYear();
188    }
189
190    /**
191     * @return The selected month.
192     */
193    public int getMonth() {
194        return mDelegate.getMonth();
195    }
196
197    /**
198     * @return The selected day of month.
199     */
200    public int getDayOfMonth() {
201        return mDelegate.getDayOfMonth();
202    }
203
204    /**
205     * Gets the minimal date supported by this {@link DatePicker} in
206     * milliseconds since January 1, 1970 00:00:00 in
207     * {@link TimeZone#getDefault()} time zone.
208     * <p>
209     * Note: The default minimal date is 01/01/1900.
210     * <p>
211     *
212     * @return The minimal supported date.
213     */
214    public long getMinDate() {
215        return mDelegate.getMinDate().getTimeInMillis();
216    }
217
218    /**
219     * Sets the minimal date supported by this {@link NumberPicker} in
220     * milliseconds since January 1, 1970 00:00:00 in
221     * {@link TimeZone#getDefault()} time zone.
222     *
223     * @param minDate The minimal supported date.
224     */
225    public void setMinDate(long minDate) {
226        mDelegate.setMinDate(minDate);
227    }
228
229    /**
230     * Gets the maximal date supported by this {@link DatePicker} in
231     * milliseconds since January 1, 1970 00:00:00 in
232     * {@link TimeZone#getDefault()} time zone.
233     * <p>
234     * Note: The default maximal date is 12/31/2100.
235     * <p>
236     *
237     * @return The maximal supported date.
238     */
239    public long getMaxDate() {
240        return mDelegate.getMaxDate().getTimeInMillis();
241    }
242
243    /**
244     * Sets the maximal date supported by this {@link DatePicker} in
245     * milliseconds since January 1, 1970 00:00:00 in
246     * {@link TimeZone#getDefault()} time zone.
247     *
248     * @param maxDate The maximal supported date.
249     */
250    public void setMaxDate(long maxDate) {
251        mDelegate.setMaxDate(maxDate);
252    }
253
254    /**
255     * Sets the callback that indicates the current date is valid.
256     *
257     * @param callback the callback, may be null
258     * @hide
259     */
260    public void setValidationCallback(@Nullable ValidationCallback callback) {
261        mDelegate.setValidationCallback(callback);
262    }
263
264    @Override
265    public void setEnabled(boolean enabled) {
266        if (mDelegate.isEnabled() == enabled) {
267            return;
268        }
269        super.setEnabled(enabled);
270        mDelegate.setEnabled(enabled);
271    }
272
273    @Override
274    public boolean isEnabled() {
275        return mDelegate.isEnabled();
276    }
277
278    @Override
279    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
280        return mDelegate.dispatchPopulateAccessibilityEvent(event);
281    }
282
283    @Override
284    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
285        super.onPopulateAccessibilityEvent(event);
286        mDelegate.onPopulateAccessibilityEvent(event);
287    }
288
289    @Override
290    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
291        super.onInitializeAccessibilityEvent(event);
292        mDelegate.onInitializeAccessibilityEvent(event);
293    }
294
295    @Override
296    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
297        super.onInitializeAccessibilityNodeInfo(info);
298        mDelegate.onInitializeAccessibilityNodeInfo(info);
299    }
300
301    @Override
302    protected void onConfigurationChanged(Configuration newConfig) {
303        super.onConfigurationChanged(newConfig);
304        mDelegate.onConfigurationChanged(newConfig);
305    }
306
307    /**
308     * Sets the first day of week.
309     *
310     * @param firstDayOfWeek The first day of the week conforming to the
311     *            {@link CalendarView} APIs.
312     * @see Calendar#SUNDAY
313     * @see Calendar#MONDAY
314     * @see Calendar#TUESDAY
315     * @see Calendar#WEDNESDAY
316     * @see Calendar#THURSDAY
317     * @see Calendar#FRIDAY
318     * @see Calendar#SATURDAY
319     *
320     * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
321     */
322    public void setFirstDayOfWeek(int firstDayOfWeek) {
323        if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) {
324            throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7");
325        }
326        mDelegate.setFirstDayOfWeek(firstDayOfWeek);
327    }
328
329    /**
330     * Gets the first day of week.
331     *
332     * @return The first day of the week conforming to the {@link CalendarView}
333     *         APIs.
334     * @see Calendar#SUNDAY
335     * @see Calendar#MONDAY
336     * @see Calendar#TUESDAY
337     * @see Calendar#WEDNESDAY
338     * @see Calendar#THURSDAY
339     * @see Calendar#FRIDAY
340     * @see Calendar#SATURDAY
341     *
342     * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
343     */
344    public int getFirstDayOfWeek() {
345        return mDelegate.getFirstDayOfWeek();
346    }
347
348    /**
349     * Gets whether the {@link CalendarView} is shown.
350     *
351     * @return True if the calendar view is shown.
352     * @see #getCalendarView()
353     */
354    public boolean getCalendarViewShown() {
355        return mDelegate.getCalendarViewShown();
356    }
357
358    /**
359     * Gets the {@link CalendarView}.
360     *
361     * @return The calendar view.
362     * @see #getCalendarViewShown()
363     */
364    public CalendarView getCalendarView() {
365        return mDelegate.getCalendarView();
366    }
367
368    /**
369     * Sets whether the {@link CalendarView} is shown.
370     *
371     * @param shown True if the calendar view is to be shown.
372     */
373    public void setCalendarViewShown(boolean shown) {
374        mDelegate.setCalendarViewShown(shown);
375    }
376
377    /**
378     * Gets whether the spinners are shown.
379     *
380     * @return True if the spinners are shown.
381     */
382    public boolean getSpinnersShown() {
383        return mDelegate.getSpinnersShown();
384    }
385
386    /**
387     * Sets whether the spinners are shown.
388     *
389     * @param shown True if the spinners are to be shown.
390     */
391    public void setSpinnersShown(boolean shown) {
392        mDelegate.setSpinnersShown(shown);
393    }
394
395    @Override
396    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
397        dispatchThawSelfOnly(container);
398    }
399
400    @Override
401    protected Parcelable onSaveInstanceState() {
402        Parcelable superState = super.onSaveInstanceState();
403        return mDelegate.onSaveInstanceState(superState);
404    }
405
406    @Override
407    protected void onRestoreInstanceState(Parcelable state) {
408        BaseSavedState ss = (BaseSavedState) state;
409        super.onRestoreInstanceState(ss.getSuperState());
410        mDelegate.onRestoreInstanceState(ss);
411    }
412
413    /**
414     * A delegate interface that defined the public API of the DatePicker. Allows different
415     * DatePicker implementations. This would need to be implemented by the DatePicker delegates
416     * for the real behavior.
417     *
418     * @hide
419     */
420    interface DatePickerDelegate {
421        void init(int year, int monthOfYear, int dayOfMonth,
422                  OnDateChangedListener onDateChangedListener);
423
424        void updateDate(int year, int month, int dayOfMonth);
425
426        int getYear();
427        int getMonth();
428        int getDayOfMonth();
429
430        void setFirstDayOfWeek(int firstDayOfWeek);
431        int getFirstDayOfWeek();
432
433        void setMinDate(long minDate);
434        Calendar getMinDate();
435
436        void setMaxDate(long maxDate);
437        Calendar getMaxDate();
438
439        void setEnabled(boolean enabled);
440        boolean isEnabled();
441
442        CalendarView getCalendarView();
443
444        void setCalendarViewShown(boolean shown);
445        boolean getCalendarViewShown();
446
447        void setSpinnersShown(boolean shown);
448        boolean getSpinnersShown();
449
450        void setValidationCallback(ValidationCallback callback);
451
452        void onConfigurationChanged(Configuration newConfig);
453
454        Parcelable onSaveInstanceState(Parcelable superState);
455        void onRestoreInstanceState(Parcelable state);
456
457        boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
458        void onPopulateAccessibilityEvent(AccessibilityEvent event);
459        void onInitializeAccessibilityEvent(AccessibilityEvent event);
460        void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info);
461    }
462
463    /**
464     * An abstract class which can be used as a start for DatePicker implementations
465     */
466    abstract static class AbstractDatePickerDelegate implements DatePickerDelegate {
467        // The delegator
468        protected DatePicker mDelegator;
469
470        // The context
471        protected Context mContext;
472
473        // The current locale
474        protected Locale mCurrentLocale;
475
476        // Callbacks
477        protected OnDateChangedListener mOnDateChangedListener;
478        protected ValidationCallback mValidationCallback;
479
480        public AbstractDatePickerDelegate(DatePicker delegator, Context context) {
481            mDelegator = delegator;
482            mContext = context;
483
484            // initialization based on locale
485            setCurrentLocale(Locale.getDefault());
486        }
487
488        protected void setCurrentLocale(Locale locale) {
489            if (locale.equals(mCurrentLocale)) {
490                return;
491            }
492            mCurrentLocale = locale;
493        }
494
495        @Override
496        public void setValidationCallback(ValidationCallback callback) {
497            mValidationCallback = callback;
498        }
499
500        protected void onValidationChanged(boolean valid) {
501            if (mValidationCallback != null) {
502                mValidationCallback.onValidationChanged(valid);
503            }
504        }
505    }
506
507    /**
508     * A callback interface for updating input validity when the date picker
509     * when included into a dialog.
510     *
511     * @hide
512     */
513    public static interface ValidationCallback {
514        void onValidationChanged(boolean valid);
515    }
516
517    /**
518     * A delegate implementing the basic DatePicker
519     */
520    private static class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate {
521
522        private static final String DATE_FORMAT = "MM/dd/yyyy";
523
524        private static final int DEFAULT_START_YEAR = 1900;
525
526        private static final int DEFAULT_END_YEAR = 2100;
527
528        private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
529
530        private static final boolean DEFAULT_SPINNERS_SHOWN = true;
531
532        private static final boolean DEFAULT_ENABLED_STATE = true;
533
534        private final LinearLayout mSpinners;
535
536        private final NumberPicker mDaySpinner;
537
538        private final NumberPicker mMonthSpinner;
539
540        private final NumberPicker mYearSpinner;
541
542        private final EditText mDaySpinnerInput;
543
544        private final EditText mMonthSpinnerInput;
545
546        private final EditText mYearSpinnerInput;
547
548        private final CalendarView mCalendarView;
549
550        private String[] mShortMonths;
551
552        private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
553
554        private int mNumberOfMonths;
555
556        private Calendar mTempDate;
557
558        private Calendar mMinDate;
559
560        private Calendar mMaxDate;
561
562        private Calendar mCurrentDate;
563
564        private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
565
566        DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
567                int defStyleAttr, int defStyleRes) {
568            super(delegator, context);
569
570            mDelegator = delegator;
571            mContext = context;
572
573            // initialization based on locale
574            setCurrentLocale(Locale.getDefault());
575
576            final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
577                    R.styleable.DatePicker, defStyleAttr, defStyleRes);
578            boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown,
579                    DEFAULT_SPINNERS_SHOWN);
580            boolean calendarViewShown = attributesArray.getBoolean(
581                    R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
582            int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear,
583                    DEFAULT_START_YEAR);
584            int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
585            String minDate = attributesArray.getString(R.styleable.DatePicker_minDate);
586            String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate);
587            int layoutResourceId = attributesArray.getResourceId(
588                    R.styleable.DatePicker_legacyLayout, R.layout.date_picker_legacy);
589            attributesArray.recycle();
590
591            LayoutInflater inflater = (LayoutInflater) context
592                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
593            inflater.inflate(layoutResourceId, mDelegator, true);
594
595            OnValueChangeListener onChangeListener = new OnValueChangeListener() {
596                public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
597                    updateInputState();
598                    mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
599                    // take care of wrapping of days and months to update greater fields
600                    if (picker == mDaySpinner) {
601                        int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
602                        if (oldVal == maxDayOfMonth && newVal == 1) {
603                            mTempDate.add(Calendar.DAY_OF_MONTH, 1);
604                        } else if (oldVal == 1 && newVal == maxDayOfMonth) {
605                            mTempDate.add(Calendar.DAY_OF_MONTH, -1);
606                        } else {
607                            mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
608                        }
609                    } else if (picker == mMonthSpinner) {
610                        if (oldVal == 11 && newVal == 0) {
611                            mTempDate.add(Calendar.MONTH, 1);
612                        } else if (oldVal == 0 && newVal == 11) {
613                            mTempDate.add(Calendar.MONTH, -1);
614                        } else {
615                            mTempDate.add(Calendar.MONTH, newVal - oldVal);
616                        }
617                    } else if (picker == mYearSpinner) {
618                        mTempDate.set(Calendar.YEAR, newVal);
619                    } else {
620                        throw new IllegalArgumentException();
621                    }
622                    // now set the date to the adjusted one
623                    setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
624                            mTempDate.get(Calendar.DAY_OF_MONTH));
625                    updateSpinners();
626                    updateCalendarView();
627                    notifyDateChanged();
628                }
629            };
630
631            mSpinners = (LinearLayout) mDelegator.findViewById(R.id.pickers);
632
633            // calendar view day-picker
634            mCalendarView = (CalendarView) mDelegator.findViewById(R.id.calendar_view);
635            mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
636                public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
637                    setDate(year, month, monthDay);
638                    updateSpinners();
639                    notifyDateChanged();
640                }
641            });
642
643            // day
644            mDaySpinner = (NumberPicker) mDelegator.findViewById(R.id.day);
645            mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
646            mDaySpinner.setOnLongPressUpdateInterval(100);
647            mDaySpinner.setOnValueChangedListener(onChangeListener);
648            mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input);
649
650            // month
651            mMonthSpinner = (NumberPicker) mDelegator.findViewById(R.id.month);
652            mMonthSpinner.setMinValue(0);
653            mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
654            mMonthSpinner.setDisplayedValues(mShortMonths);
655            mMonthSpinner.setOnLongPressUpdateInterval(200);
656            mMonthSpinner.setOnValueChangedListener(onChangeListener);
657            mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input);
658
659            // year
660            mYearSpinner = (NumberPicker) mDelegator.findViewById(R.id.year);
661            mYearSpinner.setOnLongPressUpdateInterval(100);
662            mYearSpinner.setOnValueChangedListener(onChangeListener);
663            mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input);
664
665            // show only what the user required but make sure we
666            // show something and the spinners have higher priority
667            if (!spinnersShown && !calendarViewShown) {
668                setSpinnersShown(true);
669            } else {
670                setSpinnersShown(spinnersShown);
671                setCalendarViewShown(calendarViewShown);
672            }
673
674            // set the min date giving priority of the minDate over startYear
675            mTempDate.clear();
676            if (!TextUtils.isEmpty(minDate)) {
677                if (!parseDate(minDate, mTempDate)) {
678                    mTempDate.set(startYear, 0, 1);
679                }
680            } else {
681                mTempDate.set(startYear, 0, 1);
682            }
683            setMinDate(mTempDate.getTimeInMillis());
684
685            // set the max date giving priority of the maxDate over endYear
686            mTempDate.clear();
687            if (!TextUtils.isEmpty(maxDate)) {
688                if (!parseDate(maxDate, mTempDate)) {
689                    mTempDate.set(endYear, 11, 31);
690                }
691            } else {
692                mTempDate.set(endYear, 11, 31);
693            }
694            setMaxDate(mTempDate.getTimeInMillis());
695
696            // initialize to current date
697            mCurrentDate.setTimeInMillis(System.currentTimeMillis());
698            init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
699                    .get(Calendar.DAY_OF_MONTH), null);
700
701            // re-order the number spinners to match the current date format
702            reorderSpinners();
703
704            // accessibility
705            setContentDescriptions();
706
707            // If not explicitly specified this view is important for accessibility.
708            if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
709                mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
710            }
711        }
712
713        @Override
714        public void init(int year, int monthOfYear, int dayOfMonth,
715                         OnDateChangedListener onDateChangedListener) {
716            setDate(year, monthOfYear, dayOfMonth);
717            updateSpinners();
718            updateCalendarView();
719            mOnDateChangedListener = onDateChangedListener;
720        }
721
722        @Override
723        public void updateDate(int year, int month, int dayOfMonth) {
724            if (!isNewDate(year, month, dayOfMonth)) {
725                return;
726            }
727            setDate(year, month, dayOfMonth);
728            updateSpinners();
729            updateCalendarView();
730            notifyDateChanged();
731        }
732
733        @Override
734        public int getYear() {
735            return mCurrentDate.get(Calendar.YEAR);
736        }
737
738        @Override
739        public int getMonth() {
740            return mCurrentDate.get(Calendar.MONTH);
741        }
742
743        @Override
744        public int getDayOfMonth() {
745            return mCurrentDate.get(Calendar.DAY_OF_MONTH);
746        }
747
748        @Override
749        public void setFirstDayOfWeek(int firstDayOfWeek) {
750            mCalendarView.setFirstDayOfWeek(firstDayOfWeek);
751        }
752
753        @Override
754        public int getFirstDayOfWeek() {
755            return mCalendarView.getFirstDayOfWeek();
756        }
757
758        @Override
759        public void setMinDate(long minDate) {
760            mTempDate.setTimeInMillis(minDate);
761            if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
762                    && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
763                return;
764            }
765            mMinDate.setTimeInMillis(minDate);
766            mCalendarView.setMinDate(minDate);
767            if (mCurrentDate.before(mMinDate)) {
768                mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
769                updateCalendarView();
770            }
771            updateSpinners();
772        }
773
774        @Override
775        public Calendar getMinDate() {
776            final Calendar minDate = Calendar.getInstance();
777            minDate.setTimeInMillis(mCalendarView.getMinDate());
778            return minDate;
779        }
780
781        @Override
782        public void setMaxDate(long maxDate) {
783            mTempDate.setTimeInMillis(maxDate);
784            if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
785                    && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
786                return;
787            }
788            mMaxDate.setTimeInMillis(maxDate);
789            mCalendarView.setMaxDate(maxDate);
790            if (mCurrentDate.after(mMaxDate)) {
791                mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
792                updateCalendarView();
793            }
794            updateSpinners();
795        }
796
797        @Override
798        public Calendar getMaxDate() {
799            final Calendar maxDate = Calendar.getInstance();
800            maxDate.setTimeInMillis(mCalendarView.getMaxDate());
801            return maxDate;
802        }
803
804        @Override
805        public void setEnabled(boolean enabled) {
806            mDaySpinner.setEnabled(enabled);
807            mMonthSpinner.setEnabled(enabled);
808            mYearSpinner.setEnabled(enabled);
809            mCalendarView.setEnabled(enabled);
810            mIsEnabled = enabled;
811        }
812
813        @Override
814        public boolean isEnabled() {
815            return mIsEnabled;
816        }
817
818        @Override
819        public CalendarView getCalendarView() {
820            return mCalendarView;
821        }
822
823        @Override
824        public void setCalendarViewShown(boolean shown) {
825            mCalendarView.setVisibility(shown ? VISIBLE : GONE);
826        }
827
828        @Override
829        public boolean getCalendarViewShown() {
830            return (mCalendarView.getVisibility() == View.VISIBLE);
831        }
832
833        @Override
834        public void setSpinnersShown(boolean shown) {
835            mSpinners.setVisibility(shown ? VISIBLE : GONE);
836        }
837
838        @Override
839        public boolean getSpinnersShown() {
840            return mSpinners.isShown();
841        }
842
843        @Override
844        public void onConfigurationChanged(Configuration newConfig) {
845            setCurrentLocale(newConfig.locale);
846        }
847
848        @Override
849        public Parcelable onSaveInstanceState(Parcelable superState) {
850            return new SavedState(superState, getYear(), getMonth(), getDayOfMonth());
851        }
852
853        @Override
854        public void onRestoreInstanceState(Parcelable state) {
855            SavedState ss = (SavedState) state;
856            setDate(ss.mYear, ss.mMonth, ss.mDay);
857            updateSpinners();
858            updateCalendarView();
859        }
860
861        @Override
862        public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
863            onPopulateAccessibilityEvent(event);
864            return true;
865        }
866
867        @Override
868        public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
869            final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
870            String selectedDateUtterance = DateUtils.formatDateTime(mContext,
871                    mCurrentDate.getTimeInMillis(), flags);
872            event.getText().add(selectedDateUtterance);
873        }
874
875        @Override
876        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
877            event.setClassName(DatePicker.class.getName());
878        }
879
880        @Override
881        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
882            info.setClassName(DatePicker.class.getName());
883        }
884
885        /**
886         * Sets the current locale.
887         *
888         * @param locale The current locale.
889         */
890        @Override
891        protected void setCurrentLocale(Locale locale) {
892            super.setCurrentLocale(locale);
893
894            mTempDate = getCalendarForLocale(mTempDate, locale);
895            mMinDate = getCalendarForLocale(mMinDate, locale);
896            mMaxDate = getCalendarForLocale(mMaxDate, locale);
897            mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
898
899            mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
900            mShortMonths = new DateFormatSymbols().getShortMonths();
901
902            if (usingNumericMonths()) {
903                // We're in a locale where a date should either be all-numeric, or all-text.
904                // All-text would require custom NumberPicker formatters for day and year.
905                mShortMonths = new String[mNumberOfMonths];
906                for (int i = 0; i < mNumberOfMonths; ++i) {
907                    mShortMonths[i] = String.format("%d", i + 1);
908                }
909            }
910        }
911
912        /**
913         * Tests whether the current locale is one where there are no real month names,
914         * such as Chinese, Japanese, or Korean locales.
915         */
916        private boolean usingNumericMonths() {
917            return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
918        }
919
920        /**
921         * Gets a calendar for locale bootstrapped with the value of a given calendar.
922         *
923         * @param oldCalendar The old calendar.
924         * @param locale The locale.
925         */
926        private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
927            if (oldCalendar == null) {
928                return Calendar.getInstance(locale);
929            } else {
930                final long currentTimeMillis = oldCalendar.getTimeInMillis();
931                Calendar newCalendar = Calendar.getInstance(locale);
932                newCalendar.setTimeInMillis(currentTimeMillis);
933                return newCalendar;
934            }
935        }
936
937        /**
938         * Reorders the spinners according to the date format that is
939         * explicitly set by the user and if no such is set fall back
940         * to the current locale's default format.
941         */
942        private void reorderSpinners() {
943            mSpinners.removeAllViews();
944            // We use numeric spinners for year and day, but textual months. Ask icu4c what
945            // order the user's locale uses for that combination. http://b/7207103.
946            String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");
947            char[] order = ICU.getDateFormatOrder(pattern);
948            final int spinnerCount = order.length;
949            for (int i = 0; i < spinnerCount; i++) {
950                switch (order[i]) {
951                    case 'd':
952                        mSpinners.addView(mDaySpinner);
953                        setImeOptions(mDaySpinner, spinnerCount, i);
954                        break;
955                    case 'M':
956                        mSpinners.addView(mMonthSpinner);
957                        setImeOptions(mMonthSpinner, spinnerCount, i);
958                        break;
959                    case 'y':
960                        mSpinners.addView(mYearSpinner);
961                        setImeOptions(mYearSpinner, spinnerCount, i);
962                        break;
963                    default:
964                        throw new IllegalArgumentException(Arrays.toString(order));
965                }
966            }
967        }
968
969        /**
970         * Parses the given <code>date</code> and in case of success sets the result
971         * to the <code>outDate</code>.
972         *
973         * @return True if the date was parsed.
974         */
975        private boolean parseDate(String date, Calendar outDate) {
976            try {
977                outDate.setTime(mDateFormat.parse(date));
978                return true;
979            } catch (ParseException e) {
980                Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
981                return false;
982            }
983        }
984
985        private boolean isNewDate(int year, int month, int dayOfMonth) {
986            return (mCurrentDate.get(Calendar.YEAR) != year
987                    || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
988                    || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
989        }
990
991        private void setDate(int year, int month, int dayOfMonth) {
992            mCurrentDate.set(year, month, dayOfMonth);
993            if (mCurrentDate.before(mMinDate)) {
994                mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
995            } else if (mCurrentDate.after(mMaxDate)) {
996                mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
997            }
998        }
999
1000        private void updateSpinners() {
1001            // set the spinner ranges respecting the min and max dates
1002            if (mCurrentDate.equals(mMinDate)) {
1003                mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
1004                mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
1005                mDaySpinner.setWrapSelectorWheel(false);
1006                mMonthSpinner.setDisplayedValues(null);
1007                mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
1008                mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
1009                mMonthSpinner.setWrapSelectorWheel(false);
1010            } else if (mCurrentDate.equals(mMaxDate)) {
1011                mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
1012                mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
1013                mDaySpinner.setWrapSelectorWheel(false);
1014                mMonthSpinner.setDisplayedValues(null);
1015                mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
1016                mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
1017                mMonthSpinner.setWrapSelectorWheel(false);
1018            } else {
1019                mDaySpinner.setMinValue(1);
1020                mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
1021                mDaySpinner.setWrapSelectorWheel(true);
1022                mMonthSpinner.setDisplayedValues(null);
1023                mMonthSpinner.setMinValue(0);
1024                mMonthSpinner.setMaxValue(11);
1025                mMonthSpinner.setWrapSelectorWheel(true);
1026            }
1027
1028            // make sure the month names are a zero based array
1029            // with the months in the month spinner
1030            String[] displayedValues = Arrays.copyOfRange(mShortMonths,
1031                    mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
1032            mMonthSpinner.setDisplayedValues(displayedValues);
1033
1034            // year spinner range does not change based on the current date
1035            mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
1036            mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
1037            mYearSpinner.setWrapSelectorWheel(false);
1038
1039            // set the spinner values
1040            mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
1041            mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
1042            mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
1043
1044            if (usingNumericMonths()) {
1045                mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1046            }
1047        }
1048
1049        /**
1050         * Updates the calendar view with the current date.
1051         */
1052        private void updateCalendarView() {
1053            mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
1054        }
1055
1056
1057        /**
1058         * Notifies the listener, if such, for a change in the selected date.
1059         */
1060        private void notifyDateChanged() {
1061            mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1062            if (mOnDateChangedListener != null) {
1063                mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(),
1064                        getDayOfMonth());
1065            }
1066        }
1067
1068        /**
1069         * Sets the IME options for a spinner based on its ordering.
1070         *
1071         * @param spinner The spinner.
1072         * @param spinnerCount The total spinner count.
1073         * @param spinnerIndex The index of the given spinner.
1074         */
1075        private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
1076            final int imeOptions;
1077            if (spinnerIndex < spinnerCount - 1) {
1078                imeOptions = EditorInfo.IME_ACTION_NEXT;
1079            } else {
1080                imeOptions = EditorInfo.IME_ACTION_DONE;
1081            }
1082            TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input);
1083            input.setImeOptions(imeOptions);
1084        }
1085
1086        private void setContentDescriptions() {
1087            // Day
1088            trySetContentDescription(mDaySpinner, R.id.increment,
1089                    R.string.date_picker_increment_day_button);
1090            trySetContentDescription(mDaySpinner, R.id.decrement,
1091                    R.string.date_picker_decrement_day_button);
1092            // Month
1093            trySetContentDescription(mMonthSpinner, R.id.increment,
1094                    R.string.date_picker_increment_month_button);
1095            trySetContentDescription(mMonthSpinner, R.id.decrement,
1096                    R.string.date_picker_decrement_month_button);
1097            // Year
1098            trySetContentDescription(mYearSpinner, R.id.increment,
1099                    R.string.date_picker_increment_year_button);
1100            trySetContentDescription(mYearSpinner, R.id.decrement,
1101                    R.string.date_picker_decrement_year_button);
1102        }
1103
1104        private void trySetContentDescription(View root, int viewId, int contDescResId) {
1105            View target = root.findViewById(viewId);
1106            if (target != null) {
1107                target.setContentDescription(mContext.getString(contDescResId));
1108            }
1109        }
1110
1111        private void updateInputState() {
1112            // Make sure that if the user changes the value and the IME is active
1113            // for one of the inputs if this widget, the IME is closed. If the user
1114            // changed the value via the IME and there is a next input the IME will
1115            // be shown, otherwise the user chose another means of changing the
1116            // value and having the IME up makes no sense.
1117            InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
1118            if (inputMethodManager != null) {
1119                if (inputMethodManager.isActive(mYearSpinnerInput)) {
1120                    mYearSpinnerInput.clearFocus();
1121                    inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
1122                } else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
1123                    mMonthSpinnerInput.clearFocus();
1124                    inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
1125                } else if (inputMethodManager.isActive(mDaySpinnerInput)) {
1126                    mDaySpinnerInput.clearFocus();
1127                    inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
1128                }
1129            }
1130        }
1131    }
1132
1133    /**
1134     * Class for managing state storing/restoring.
1135     */
1136    private static class SavedState extends BaseSavedState {
1137
1138        private final int mYear;
1139
1140        private final int mMonth;
1141
1142        private final int mDay;
1143
1144        /**
1145         * Constructor called from {@link DatePicker#onSaveInstanceState()}
1146         */
1147        private SavedState(Parcelable superState, int year, int month, int day) {
1148            super(superState);
1149            mYear = year;
1150            mMonth = month;
1151            mDay = day;
1152        }
1153
1154        /**
1155         * Constructor called from {@link #CREATOR}
1156         */
1157        private SavedState(Parcel in) {
1158            super(in);
1159            mYear = in.readInt();
1160            mMonth = in.readInt();
1161            mDay = in.readInt();
1162        }
1163
1164        @Override
1165        public void writeToParcel(Parcel dest, int flags) {
1166            super.writeToParcel(dest, flags);
1167            dest.writeInt(mYear);
1168            dest.writeInt(mMonth);
1169            dest.writeInt(mDay);
1170        }
1171
1172        @SuppressWarnings("all")
1173        // suppress unused and hiding
1174        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
1175
1176            public SavedState createFromParcel(Parcel in) {
1177                return new SavedState(in);
1178            }
1179
1180            public SavedState[] newArray(int size) {
1181                return new SavedState[size];
1182            }
1183        };
1184    }
1185}
1186