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.Widget;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.TypedArray;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.text.TextUtils;
26import android.text.format.DateFormat;
27import android.text.format.DateUtils;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.util.SparseArray;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.accessibility.AccessibilityEvent;
34import android.view.accessibility.AccessibilityNodeInfo;
35import android.view.inputmethod.EditorInfo;
36import android.view.inputmethod.InputMethodManager;
37import android.widget.NumberPicker.OnValueChangeListener;
38
39import com.android.internal.R;
40
41import java.text.ParseException;
42import java.text.SimpleDateFormat;
43import java.util.Arrays;
44import java.util.Calendar;
45import java.util.Locale;
46import java.util.TimeZone;
47
48/**
49 * This class is a widget for selecting a date. The date can be selected by a
50 * year, month, and day spinners or a {@link CalendarView}. The set of spinners
51 * and the calendar view are automatically synchronized. The client can
52 * customize whether only the spinners, or only the calendar view, or both to be
53 * displayed. Also the minimal and maximal date from which dates to be selected
54 * can be customized.
55 * <p>
56 * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
57 * guide.
58 * </p>
59 * <p>
60 * For a dialog using this view, see {@link android.app.DatePickerDialog}.
61 * </p>
62 *
63 * @attr ref android.R.styleable#DatePicker_startYear
64 * @attr ref android.R.styleable#DatePicker_endYear
65 * @attr ref android.R.styleable#DatePicker_maxDate
66 * @attr ref android.R.styleable#DatePicker_minDate
67 * @attr ref android.R.styleable#DatePicker_spinnersShown
68 * @attr ref android.R.styleable#DatePicker_calendarViewShown
69 */
70@Widget
71public class DatePicker extends FrameLayout {
72
73    private static final String LOG_TAG = DatePicker.class.getSimpleName();
74
75    private static final String DATE_FORMAT = "MM/dd/yyyy";
76
77    private static final int DEFAULT_START_YEAR = 1900;
78
79    private static final int DEFAULT_END_YEAR = 2100;
80
81    private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
82
83    private static final boolean DEFAULT_SPINNERS_SHOWN = true;
84
85    private static final boolean DEFAULT_ENABLED_STATE = true;
86
87    private final LinearLayout mSpinners;
88
89    private final NumberPicker mDaySpinner;
90
91    private final NumberPicker mMonthSpinner;
92
93    private final NumberPicker mYearSpinner;
94
95    private final EditText mDaySpinnerInput;
96
97    private final EditText mMonthSpinnerInput;
98
99    private final EditText mYearSpinnerInput;
100
101    private final CalendarView mCalendarView;
102
103    private Locale mCurrentLocale;
104
105    private OnDateChangedListener mOnDateChangedListener;
106
107    private String[] mShortMonths;
108
109    private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
110
111    private int mNumberOfMonths;
112
113    private Calendar mTempDate;
114
115    private Calendar mMinDate;
116
117    private Calendar mMaxDate;
118
119    private Calendar mCurrentDate;
120
121    private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
122
123    /**
124     * The callback used to indicate the user changes\d the date.
125     */
126    public interface OnDateChangedListener {
127
128        /**
129         * Called upon a date change.
130         *
131         * @param view The view associated with this listener.
132         * @param year The year that was set.
133         * @param monthOfYear The month that was set (0-11) for compatibility
134         *            with {@link java.util.Calendar}.
135         * @param dayOfMonth The day of the month that was set.
136         */
137        void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
138    }
139
140    public DatePicker(Context context) {
141        this(context, null);
142    }
143
144    public DatePicker(Context context, AttributeSet attrs) {
145        this(context, attrs, R.attr.datePickerStyle);
146    }
147
148    public DatePicker(Context context, AttributeSet attrs, int defStyle) {
149        super(context, attrs, defStyle);
150
151        // initialization based on locale
152        setCurrentLocale(Locale.getDefault());
153
154        TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
155                defStyle, 0);
156        boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown,
157                DEFAULT_SPINNERS_SHOWN);
158        boolean calendarViewShown = attributesArray.getBoolean(
159                R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
160        int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear,
161                DEFAULT_START_YEAR);
162        int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
163        String minDate = attributesArray.getString(R.styleable.DatePicker_minDate);
164        String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate);
165        int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_internalLayout,
166                R.layout.date_picker);
167        attributesArray.recycle();
168
169        LayoutInflater inflater = (LayoutInflater) context
170                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
171        inflater.inflate(layoutResourceId, this, true);
172
173        OnValueChangeListener onChangeListener = new OnValueChangeListener() {
174            public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
175                updateInputState();
176                mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
177                // take care of wrapping of days and months to update greater fields
178                if (picker == mDaySpinner) {
179                    int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
180                    if (oldVal == maxDayOfMonth && newVal == 1) {
181                        mTempDate.add(Calendar.DAY_OF_MONTH, 1);
182                    } else if (oldVal == 1 && newVal == maxDayOfMonth) {
183                        mTempDate.add(Calendar.DAY_OF_MONTH, -1);
184                    } else {
185                        mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
186                    }
187                } else if (picker == mMonthSpinner) {
188                    if (oldVal == 11 && newVal == 0) {
189                        mTempDate.add(Calendar.MONTH, 1);
190                    } else if (oldVal == 0 && newVal == 11) {
191                        mTempDate.add(Calendar.MONTH, -1);
192                    } else {
193                        mTempDate.add(Calendar.MONTH, newVal - oldVal);
194                    }
195                } else if (picker == mYearSpinner) {
196                    mTempDate.set(Calendar.YEAR, newVal);
197                } else {
198                    throw new IllegalArgumentException();
199                }
200                // now set the date to the adjusted one
201                setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
202                        mTempDate.get(Calendar.DAY_OF_MONTH));
203                updateSpinners();
204                updateCalendarView();
205                notifyDateChanged();
206            }
207        };
208
209        mSpinners = (LinearLayout) findViewById(R.id.pickers);
210
211        // calendar view day-picker
212        mCalendarView = (CalendarView) findViewById(R.id.calendar_view);
213        mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
214            public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
215                setDate(year, month, monthDay);
216                updateSpinners();
217                notifyDateChanged();
218            }
219        });
220
221        // day
222        mDaySpinner = (NumberPicker) findViewById(R.id.day);
223        mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
224        mDaySpinner.setOnLongPressUpdateInterval(100);
225        mDaySpinner.setOnValueChangedListener(onChangeListener);
226        mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input);
227
228        // month
229        mMonthSpinner = (NumberPicker) findViewById(R.id.month);
230        mMonthSpinner.setMinValue(0);
231        mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
232        mMonthSpinner.setDisplayedValues(mShortMonths);
233        mMonthSpinner.setOnLongPressUpdateInterval(200);
234        mMonthSpinner.setOnValueChangedListener(onChangeListener);
235        mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input);
236
237        // year
238        mYearSpinner = (NumberPicker) findViewById(R.id.year);
239        mYearSpinner.setOnLongPressUpdateInterval(100);
240        mYearSpinner.setOnValueChangedListener(onChangeListener);
241        mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input);
242
243        // show only what the user required but make sure we
244        // show something and the spinners have higher priority
245        if (!spinnersShown && !calendarViewShown) {
246            setSpinnersShown(true);
247        } else {
248            setSpinnersShown(spinnersShown);
249            setCalendarViewShown(calendarViewShown);
250        }
251
252        // set the min date giving priority of the minDate over startYear
253        mTempDate.clear();
254        if (!TextUtils.isEmpty(minDate)) {
255            if (!parseDate(minDate, mTempDate)) {
256                mTempDate.set(startYear, 0, 1);
257            }
258        } else {
259            mTempDate.set(startYear, 0, 1);
260        }
261        setMinDate(mTempDate.getTimeInMillis());
262
263        // set the max date giving priority of the maxDate over endYear
264        mTempDate.clear();
265        if (!TextUtils.isEmpty(maxDate)) {
266            if (!parseDate(maxDate, mTempDate)) {
267                mTempDate.set(endYear, 11, 31);
268            }
269        } else {
270            mTempDate.set(endYear, 11, 31);
271        }
272        setMaxDate(mTempDate.getTimeInMillis());
273
274        // initialize to current date
275        mCurrentDate.setTimeInMillis(System.currentTimeMillis());
276        init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
277                .get(Calendar.DAY_OF_MONTH), null);
278
279        // re-order the number spinners to match the current date format
280        reorderSpinners();
281
282        // accessibility
283        setContentDescriptions();
284
285        // If not explicitly specified this view is important for accessibility.
286        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
287            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
288        }
289    }
290
291    /**
292     * Gets the minimal date supported by this {@link DatePicker} in
293     * milliseconds since January 1, 1970 00:00:00 in
294     * {@link TimeZone#getDefault()} time zone.
295     * <p>
296     * Note: The default minimal date is 01/01/1900.
297     * <p>
298     *
299     * @return The minimal supported date.
300     */
301    public long getMinDate() {
302        return mCalendarView.getMinDate();
303    }
304
305    /**
306     * Sets the minimal date supported by this {@link NumberPicker} in
307     * milliseconds since January 1, 1970 00:00:00 in
308     * {@link TimeZone#getDefault()} time zone.
309     *
310     * @param minDate The minimal supported date.
311     */
312    public void setMinDate(long minDate) {
313        mTempDate.setTimeInMillis(minDate);
314        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
315                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
316            return;
317        }
318        mMinDate.setTimeInMillis(minDate);
319        mCalendarView.setMinDate(minDate);
320        if (mCurrentDate.before(mMinDate)) {
321            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
322            updateCalendarView();
323        }
324        updateSpinners();
325    }
326
327    /**
328     * Gets the maximal date supported by this {@link DatePicker} in
329     * milliseconds since January 1, 1970 00:00:00 in
330     * {@link TimeZone#getDefault()} time zone.
331     * <p>
332     * Note: The default maximal date is 12/31/2100.
333     * <p>
334     *
335     * @return The maximal supported date.
336     */
337    public long getMaxDate() {
338        return mCalendarView.getMaxDate();
339    }
340
341    /**
342     * Sets the maximal date supported by this {@link DatePicker} in
343     * milliseconds since January 1, 1970 00:00:00 in
344     * {@link TimeZone#getDefault()} time zone.
345     *
346     * @param maxDate The maximal supported date.
347     */
348    public void setMaxDate(long maxDate) {
349        mTempDate.setTimeInMillis(maxDate);
350        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
351                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
352            return;
353        }
354        mMaxDate.setTimeInMillis(maxDate);
355        mCalendarView.setMaxDate(maxDate);
356        if (mCurrentDate.after(mMaxDate)) {
357            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
358            updateCalendarView();
359        }
360        updateSpinners();
361    }
362
363    @Override
364    public void setEnabled(boolean enabled) {
365        if (mIsEnabled == enabled) {
366            return;
367        }
368        super.setEnabled(enabled);
369        mDaySpinner.setEnabled(enabled);
370        mMonthSpinner.setEnabled(enabled);
371        mYearSpinner.setEnabled(enabled);
372        mCalendarView.setEnabled(enabled);
373        mIsEnabled = enabled;
374    }
375
376    @Override
377    public boolean isEnabled() {
378        return mIsEnabled;
379    }
380
381    @Override
382    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
383        onPopulateAccessibilityEvent(event);
384        return true;
385    }
386
387    @Override
388    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
389        super.onPopulateAccessibilityEvent(event);
390
391        final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
392        String selectedDateUtterance = DateUtils.formatDateTime(mContext,
393                mCurrentDate.getTimeInMillis(), flags);
394        event.getText().add(selectedDateUtterance);
395    }
396
397    @Override
398    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
399        super.onInitializeAccessibilityEvent(event);
400        event.setClassName(DatePicker.class.getName());
401    }
402
403    @Override
404    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
405        super.onInitializeAccessibilityNodeInfo(info);
406        info.setClassName(DatePicker.class.getName());
407    }
408
409    @Override
410    protected void onConfigurationChanged(Configuration newConfig) {
411        super.onConfigurationChanged(newConfig);
412        setCurrentLocale(newConfig.locale);
413    }
414
415    /**
416     * Gets whether the {@link CalendarView} is shown.
417     *
418     * @return True if the calendar view is shown.
419     * @see #getCalendarView()
420     */
421    public boolean getCalendarViewShown() {
422        return mCalendarView.isShown();
423    }
424
425    /**
426     * Gets the {@link CalendarView}.
427     *
428     * @return The calendar view.
429     * @see #getCalendarViewShown()
430     */
431    public CalendarView getCalendarView () {
432        return mCalendarView;
433    }
434
435    /**
436     * Sets whether the {@link CalendarView} is shown.
437     *
438     * @param shown True if the calendar view is to be shown.
439     */
440    public void setCalendarViewShown(boolean shown) {
441        mCalendarView.setVisibility(shown ? VISIBLE : GONE);
442    }
443
444    /**
445     * Gets whether the spinners are shown.
446     *
447     * @return True if the spinners are shown.
448     */
449    public boolean getSpinnersShown() {
450        return mSpinners.isShown();
451    }
452
453    /**
454     * Sets whether the spinners are shown.
455     *
456     * @param shown True if the spinners are to be shown.
457     */
458    public void setSpinnersShown(boolean shown) {
459        mSpinners.setVisibility(shown ? VISIBLE : GONE);
460    }
461
462    /**
463     * Sets the current locale.
464     *
465     * @param locale The current locale.
466     */
467    private void setCurrentLocale(Locale locale) {
468        if (locale.equals(mCurrentLocale)) {
469            return;
470        }
471
472        mCurrentLocale = locale;
473
474        mTempDate = getCalendarForLocale(mTempDate, locale);
475        mMinDate = getCalendarForLocale(mMinDate, locale);
476        mMaxDate = getCalendarForLocale(mMaxDate, locale);
477        mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
478
479        mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
480        mShortMonths = new String[mNumberOfMonths];
481        for (int i = 0; i < mNumberOfMonths; i++) {
482            mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
483                    DateUtils.LENGTH_MEDIUM);
484        }
485    }
486
487    /**
488     * Gets a calendar for locale bootstrapped with the value of a given calendar.
489     *
490     * @param oldCalendar The old calendar.
491     * @param locale The locale.
492     */
493    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
494        if (oldCalendar == null) {
495            return Calendar.getInstance(locale);
496        } else {
497            final long currentTimeMillis = oldCalendar.getTimeInMillis();
498            Calendar newCalendar = Calendar.getInstance(locale);
499            newCalendar.setTimeInMillis(currentTimeMillis);
500            return newCalendar;
501        }
502    }
503
504    /**
505     * Reorders the spinners according to the date format that is
506     * explicitly set by the user and if no such is set fall back
507     * to the current locale's default format.
508     */
509    private void reorderSpinners() {
510        mSpinners.removeAllViews();
511        char[] order = DateFormat.getDateFormatOrder(getContext());
512        final int spinnerCount = order.length;
513        for (int i = 0; i < spinnerCount; i++) {
514            switch (order[i]) {
515                case DateFormat.DATE:
516                    mSpinners.addView(mDaySpinner);
517                    setImeOptions(mDaySpinner, spinnerCount, i);
518                    break;
519                case DateFormat.MONTH:
520                    mSpinners.addView(mMonthSpinner);
521                    setImeOptions(mMonthSpinner, spinnerCount, i);
522                    break;
523                case DateFormat.YEAR:
524                    mSpinners.addView(mYearSpinner);
525                    setImeOptions(mYearSpinner, spinnerCount, i);
526                    break;
527                default:
528                    throw new IllegalArgumentException();
529            }
530        }
531    }
532
533    /**
534     * Updates the current date.
535     *
536     * @param year The year.
537     * @param month The month which is <strong>starting from zero</strong>.
538     * @param dayOfMonth The day of the month.
539     */
540    public void updateDate(int year, int month, int dayOfMonth) {
541        if (!isNewDate(year, month, dayOfMonth)) {
542            return;
543        }
544        setDate(year, month, dayOfMonth);
545        updateSpinners();
546        updateCalendarView();
547        notifyDateChanged();
548    }
549
550    // Override so we are in complete control of save / restore for this widget.
551    @Override
552    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
553        dispatchThawSelfOnly(container);
554    }
555
556    @Override
557    protected Parcelable onSaveInstanceState() {
558        Parcelable superState = super.onSaveInstanceState();
559        return new SavedState(superState, getYear(), getMonth(), getDayOfMonth());
560    }
561
562    @Override
563    protected void onRestoreInstanceState(Parcelable state) {
564        SavedState ss = (SavedState) state;
565        super.onRestoreInstanceState(ss.getSuperState());
566        setDate(ss.mYear, ss.mMonth, ss.mDay);
567        updateSpinners();
568        updateCalendarView();
569    }
570
571    /**
572     * Initialize the state. If the provided values designate an inconsistent
573     * date the values are normalized before updating the spinners.
574     *
575     * @param year The initial year.
576     * @param monthOfYear The initial month <strong>starting from zero</strong>.
577     * @param dayOfMonth The initial day of the month.
578     * @param onDateChangedListener How user is notified date is changed by
579     *            user, can be null.
580     */
581    public void init(int year, int monthOfYear, int dayOfMonth,
582            OnDateChangedListener onDateChangedListener) {
583        setDate(year, monthOfYear, dayOfMonth);
584        updateSpinners();
585        updateCalendarView();
586        mOnDateChangedListener = onDateChangedListener;
587    }
588
589    /**
590     * Parses the given <code>date</code> and in case of success sets the result
591     * to the <code>outDate</code>.
592     *
593     * @return True if the date was parsed.
594     */
595    private boolean parseDate(String date, Calendar outDate) {
596        try {
597            outDate.setTime(mDateFormat.parse(date));
598            return true;
599        } catch (ParseException e) {
600            Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
601            return false;
602        }
603    }
604
605    private boolean isNewDate(int year, int month, int dayOfMonth) {
606        return (mCurrentDate.get(Calendar.YEAR) != year
607                || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
608                || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
609    }
610
611    private void setDate(int year, int month, int dayOfMonth) {
612        mCurrentDate.set(year, month, dayOfMonth);
613        if (mCurrentDate.before(mMinDate)) {
614            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
615        } else if (mCurrentDate.after(mMaxDate)) {
616            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
617        }
618    }
619
620    private void updateSpinners() {
621        // set the spinner ranges respecting the min and max dates
622        if (mCurrentDate.equals(mMinDate)) {
623            mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
624            mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
625            mDaySpinner.setWrapSelectorWheel(false);
626            mMonthSpinner.setDisplayedValues(null);
627            mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
628            mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
629            mMonthSpinner.setWrapSelectorWheel(false);
630        } else if (mCurrentDate.equals(mMaxDate)) {
631            mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
632            mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
633            mDaySpinner.setWrapSelectorWheel(false);
634            mMonthSpinner.setDisplayedValues(null);
635            mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
636            mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
637            mMonthSpinner.setWrapSelectorWheel(false);
638        } else {
639            mDaySpinner.setMinValue(1);
640            mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
641            mDaySpinner.setWrapSelectorWheel(true);
642            mMonthSpinner.setDisplayedValues(null);
643            mMonthSpinner.setMinValue(0);
644            mMonthSpinner.setMaxValue(11);
645            mMonthSpinner.setWrapSelectorWheel(true);
646        }
647
648        // make sure the month names are a zero based array
649        // with the months in the month spinner
650        String[] displayedValues = Arrays.copyOfRange(mShortMonths,
651                mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
652        mMonthSpinner.setDisplayedValues(displayedValues);
653
654        // year spinner range does not change based on the current date
655        mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
656        mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
657        mYearSpinner.setWrapSelectorWheel(false);
658
659        // set the spinner values
660        mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
661        mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
662        mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
663    }
664
665    /**
666     * Updates the calendar view with the current date.
667     */
668    private void updateCalendarView() {
669         mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
670    }
671
672    /**
673     * @return The selected year.
674     */
675    public int getYear() {
676        return mCurrentDate.get(Calendar.YEAR);
677    }
678
679    /**
680     * @return The selected month.
681     */
682    public int getMonth() {
683        return mCurrentDate.get(Calendar.MONTH);
684    }
685
686    /**
687     * @return The selected day of month.
688     */
689    public int getDayOfMonth() {
690        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
691    }
692
693    /**
694     * Notifies the listener, if such, for a change in the selected date.
695     */
696    private void notifyDateChanged() {
697        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
698        if (mOnDateChangedListener != null) {
699            mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth());
700        }
701    }
702
703    /**
704     * Sets the IME options for a spinner based on its ordering.
705     *
706     * @param spinner The spinner.
707     * @param spinnerCount The total spinner count.
708     * @param spinnerIndex The index of the given spinner.
709     */
710    private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
711        final int imeOptions;
712        if (spinnerIndex < spinnerCount - 1) {
713            imeOptions = EditorInfo.IME_ACTION_NEXT;
714        } else {
715            imeOptions = EditorInfo.IME_ACTION_DONE;
716        }
717        TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input);
718        input.setImeOptions(imeOptions);
719    }
720
721    private void setContentDescriptions() {
722        // Day
723        trySetContentDescription(mDaySpinner, R.id.increment,
724                R.string.date_picker_increment_day_button);
725        trySetContentDescription(mDaySpinner, R.id.decrement,
726                R.string.date_picker_decrement_day_button);
727        // Month
728        trySetContentDescription(mMonthSpinner, R.id.increment,
729                R.string.date_picker_increment_month_button);
730        trySetContentDescription(mMonthSpinner, R.id.decrement,
731                R.string.date_picker_decrement_month_button);
732        // Year
733        trySetContentDescription(mYearSpinner, R.id.increment,
734                R.string.date_picker_increment_year_button);
735        trySetContentDescription(mYearSpinner, R.id.decrement,
736                R.string.date_picker_decrement_year_button);
737    }
738
739    private void trySetContentDescription(View root, int viewId, int contDescResId) {
740        View target = root.findViewById(viewId);
741        if (target != null) {
742            target.setContentDescription(mContext.getString(contDescResId));
743        }
744    }
745
746    private void updateInputState() {
747        // Make sure that if the user changes the value and the IME is active
748        // for one of the inputs if this widget, the IME is closed. If the user
749        // changed the value via the IME and there is a next input the IME will
750        // be shown, otherwise the user chose another means of changing the
751        // value and having the IME up makes no sense.
752        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
753        if (inputMethodManager != null) {
754            if (inputMethodManager.isActive(mYearSpinnerInput)) {
755                mYearSpinnerInput.clearFocus();
756                inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
757            } else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
758                mMonthSpinnerInput.clearFocus();
759                inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
760            } else if (inputMethodManager.isActive(mDaySpinnerInput)) {
761                mDaySpinnerInput.clearFocus();
762                inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
763            }
764        }
765    }
766
767    /**
768     * Class for managing state storing/restoring.
769     */
770    private static class SavedState extends BaseSavedState {
771
772        private final int mYear;
773
774        private final int mMonth;
775
776        private final int mDay;
777
778        /**
779         * Constructor called from {@link DatePicker#onSaveInstanceState()}
780         */
781        private SavedState(Parcelable superState, int year, int month, int day) {
782            super(superState);
783            mYear = year;
784            mMonth = month;
785            mDay = day;
786        }
787
788        /**
789         * Constructor called from {@link #CREATOR}
790         */
791        private SavedState(Parcel in) {
792            super(in);
793            mYear = in.readInt();
794            mMonth = in.readInt();
795            mDay = in.readInt();
796        }
797
798        @Override
799        public void writeToParcel(Parcel dest, int flags) {
800            super.writeToParcel(dest, flags);
801            dest.writeInt(mYear);
802            dest.writeInt(mMonth);
803            dest.writeInt(mDay);
804        }
805
806        @SuppressWarnings("all")
807        // suppress unused and hiding
808        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
809
810            public SavedState createFromParcel(Parcel in) {
811                return new SavedState(in);
812            }
813
814            public SavedState[] newArray(int size) {
815                return new SavedState[size];
816            }
817        };
818    }
819}
820