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