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