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