DatePickerCalendarDelegate.java revision 50eb025c2fea7e364e0be951ce8ba6ca605f901a
1/*
2 * Copyright (C) 2014 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.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.format.DateFormat;
27import android.text.format.DateUtils;
28import android.util.AttributeSet;
29import android.view.HapticFeedbackConstants;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.accessibility.AccessibilityNodeInfo;
34import android.view.animation.AlphaAnimation;
35import android.view.animation.Animation;
36
37import com.android.internal.R;
38import com.android.internal.widget.AccessibleDateAnimator;
39
40import java.text.SimpleDateFormat;
41import java.util.Calendar;
42import java.util.HashSet;
43import java.util.Locale;
44
45/**
46 * A delegate for picking up a date (day / month / year).
47 */
48class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate implements
49        View.OnClickListener, DatePickerController {
50    private static final int USE_LOCALE = 0;
51
52    private static final int UNINITIALIZED = -1;
53    private static final int MONTH_AND_DAY_VIEW = 0;
54    private static final int YEAR_VIEW = 1;
55
56    private static final int DEFAULT_START_YEAR = 1900;
57    private static final int DEFAULT_END_YEAR = 2100;
58
59    private static final int ANIMATION_DURATION = 300;
60
61    private static final int MONTH_INDEX = 0;
62    private static final int DAY_INDEX = 1;
63    private static final int YEAR_INDEX = 2;
64
65    private SimpleDateFormat mYearFormat = new SimpleDateFormat("y", Locale.getDefault());
66    private SimpleDateFormat mDayFormat = new SimpleDateFormat("d", Locale.getDefault());
67
68    private TextView mDayOfWeekView;
69
70    /** Layout that contains the current month, day, and year. */
71    private LinearLayout mMonthDayYearLayout;
72
73    /** Clickable layout that contains the current day and year. */
74    private LinearLayout mMonthAndDayLayout;
75
76    private TextView mHeaderMonthTextView;
77    private TextView mHeaderDayOfMonthTextView;
78    private TextView mHeaderYearTextView;
79    private DayPickerView mDayPickerView;
80    private YearPickerView mYearPickerView;
81
82    private boolean mIsEnabled = true;
83
84    // Accessibility strings.
85    private String mDayPickerDescription;
86    private String mSelectDay;
87    private String mYearPickerDescription;
88    private String mSelectYear;
89
90    private AccessibleDateAnimator mAnimator;
91
92    private DatePicker.OnDateChangedListener mDateChangedListener;
93
94    private int mCurrentView = UNINITIALIZED;
95
96    private Calendar mCurrentDate;
97    private Calendar mTempDate;
98    private Calendar mMinDate;
99    private Calendar mMaxDate;
100
101    private int mFirstDayOfWeek = USE_LOCALE;
102
103    private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
104
105    public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
106            int defStyleAttr, int defStyleRes) {
107        super(delegator, context);
108
109        final Locale locale = Locale.getDefault();
110        mMinDate = getCalendarForLocale(mMinDate, locale);
111        mMaxDate = getCalendarForLocale(mMaxDate, locale);
112        mTempDate = getCalendarForLocale(mMaxDate, locale);
113        mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
114
115        mMinDate.set(DEFAULT_START_YEAR, 1, 1);
116        mMaxDate.set(DEFAULT_END_YEAR, 12, 31);
117
118        final Resources res = mDelegator.getResources();
119        final TypedArray a = mContext.obtainStyledAttributes(attrs,
120                R.styleable.DatePicker, defStyleAttr, defStyleRes);
121        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
122                Context.LAYOUT_INFLATER_SERVICE);
123        final int layoutResourceId = a.getResourceId(
124                R.styleable.DatePicker_internalLayout, R.layout.date_picker_holo);
125        final View mainView = inflater.inflate(layoutResourceId, null);
126        mDelegator.addView(mainView);
127
128        mDayOfWeekView = (TextView) mainView.findViewById(R.id.date_picker_header);
129
130        // Layout that contains the current date and day name header.
131        final LinearLayout dateLayout = (LinearLayout) mainView.findViewById(
132                R.id.day_picker_selector_layout);
133        mMonthDayYearLayout = (LinearLayout) mainView.findViewById(
134                R.id.date_picker_month_day_year_layout);
135        mMonthAndDayLayout = (LinearLayout) mainView.findViewById(
136                R.id.date_picker_month_and_day_layout);
137        mMonthAndDayLayout.setOnClickListener(this);
138        mHeaderMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_month);
139        mHeaderDayOfMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_day);
140        mHeaderYearTextView = (TextView) mainView.findViewById(R.id.date_picker_year);
141        mHeaderYearTextView.setOnClickListener(this);
142
143        // Obtain default highlight color from the theme.
144        final int defaultHighlightColor = mHeaderYearTextView.getHighlightColor();
145
146        // Use Theme attributes if possible
147        final int dayOfWeekTextAppearanceResId = a.getResourceId(
148                R.styleable.DatePicker_dayOfWeekTextAppearance, -1);
149        if (dayOfWeekTextAppearanceResId != -1) {
150            mDayOfWeekView.setTextAppearance(context, dayOfWeekTextAppearanceResId);
151        }
152
153        mDayOfWeekView.setBackground(a.getDrawable(R.styleable.DatePicker_dayOfWeekBackground));
154
155        dateLayout.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
156
157        final int headerSelectedTextColor = a.getColor(
158                R.styleable.DatePicker_headerSelectedTextColor, defaultHighlightColor);
159        final int monthTextAppearanceResId = a.getResourceId(
160                R.styleable.DatePicker_headerMonthTextAppearance, -1);
161        if (monthTextAppearanceResId != -1) {
162            mHeaderMonthTextView.setTextAppearance(context, monthTextAppearanceResId);
163        }
164        mHeaderMonthTextView.setTextColor(ColorStateList.addFirstIfMissing(
165                mHeaderMonthTextView.getTextColors(), R.attr.state_selected,
166                headerSelectedTextColor));
167
168        final int dayOfMonthTextAppearanceResId = a.getResourceId(
169                R.styleable.DatePicker_headerDayOfMonthTextAppearance, -1);
170        if (dayOfMonthTextAppearanceResId != -1) {
171            mHeaderDayOfMonthTextView.setTextAppearance(context, dayOfMonthTextAppearanceResId);
172        }
173        mHeaderDayOfMonthTextView.setTextColor(ColorStateList.addFirstIfMissing(
174                mHeaderDayOfMonthTextView.getTextColors(), R.attr.state_selected,
175                headerSelectedTextColor));
176
177        final int yearTextAppearanceResId = a.getResourceId(
178                R.styleable.DatePicker_headerYearTextAppearance, -1);
179        if (yearTextAppearanceResId != -1) {
180            mHeaderYearTextView.setTextAppearance(context, yearTextAppearanceResId);
181        }
182        mHeaderYearTextView.setTextColor(ColorStateList.addFirstIfMissing(
183                mHeaderYearTextView.getTextColors(), R.attr.state_selected,
184                headerSelectedTextColor));
185
186        mDayPickerView = new DayPickerView(mContext, this);
187        mDayPickerView.setRange(mMinDate, mMaxDate);
188
189        mYearPickerView = new YearPickerView(mContext);
190        mYearPickerView.init(this);
191
192        final int yearSelectedCircleColor = a.getColor(R.styleable.DatePicker_yearListSelectorColor,
193                defaultHighlightColor);
194        mYearPickerView.setYearSelectedCircleColor(yearSelectedCircleColor);
195
196        final ColorStateList calendarTextColor = a.getColorStateList(
197                R.styleable.DatePicker_calendarTextColor);
198        final int calendarSelectedTextColor = a.getColor(
199                R.styleable.DatePicker_calendarSelectedTextColor, defaultHighlightColor);
200        mDayPickerView.setCalendarTextColor(ColorStateList.addFirstIfMissing(
201                calendarTextColor, R.attr.state_selected, calendarSelectedTextColor));
202
203        mDayPickerDescription = res.getString(R.string.day_picker_description);
204        mSelectDay = res.getString(R.string.select_day);
205        mYearPickerDescription = res.getString(R.string.year_picker_description);
206        mSelectYear = res.getString(R.string.select_year);
207
208        mAnimator = (AccessibleDateAnimator) mainView.findViewById(R.id.animator);
209        mAnimator.addView(mDayPickerView);
210        mAnimator.addView(mYearPickerView);
211        mAnimator.setDateMillis(mCurrentDate.getTimeInMillis());
212
213        final Animation animation = new AlphaAnimation(0.0f, 1.0f);
214        animation.setDuration(ANIMATION_DURATION);
215        mAnimator.setInAnimation(animation);
216
217        final Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
218        animation2.setDuration(ANIMATION_DURATION);
219        mAnimator.setOutAnimation(animation2);
220
221        updateDisplay(false);
222        setCurrentView(MONTH_AND_DAY_VIEW);
223    }
224
225    /**
226     * Gets a calendar for locale bootstrapped with the value of a given calendar.
227     *
228     * @param oldCalendar The old calendar.
229     * @param locale The locale.
230     */
231    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
232        if (oldCalendar == null) {
233            return Calendar.getInstance(locale);
234        } else {
235            final long currentTimeMillis = oldCalendar.getTimeInMillis();
236            Calendar newCalendar = Calendar.getInstance(locale);
237            newCalendar.setTimeInMillis(currentTimeMillis);
238            return newCalendar;
239        }
240    }
241
242    /**
243     * Compute the array representing the order of Month / Day / Year views in their layout.
244     * Will be used for I18N purpose as the order of them depends on the Locale.
245     */
246    private int[] getMonthDayYearIndexes(String pattern) {
247        int[] result = new int[3];
248
249        final String filteredPattern = pattern.replaceAll("'.*?'", "");
250
251        final int dayIndex = filteredPattern.indexOf('d');
252        final int monthMIndex = filteredPattern.indexOf("M");
253        final int monthIndex = (monthMIndex != -1) ? monthMIndex : filteredPattern.indexOf("L");
254        final int yearIndex = filteredPattern.indexOf("y");
255
256        if (yearIndex < monthIndex) {
257            result[YEAR_INDEX] = 0;
258
259            if (monthIndex < dayIndex) {
260                result[MONTH_INDEX] = 1;
261                result[DAY_INDEX] = 2;
262            } else {
263                result[MONTH_INDEX] = 2;
264                result[DAY_INDEX] = 1;
265            }
266        } else {
267            result[YEAR_INDEX] = 2;
268
269            if (monthIndex < dayIndex) {
270                result[MONTH_INDEX] = 0;
271                result[DAY_INDEX] = 1;
272            } else {
273                result[MONTH_INDEX] = 1;
274                result[DAY_INDEX] = 0;
275            }
276        }
277        return result;
278    }
279
280    private void updateDisplay(boolean announce) {
281        if (mDayOfWeekView != null) {
282            mDayOfWeekView.setText(mCurrentDate.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
283                    Locale.getDefault()));
284        }
285
286        // Compute indices of Month, Day and Year views
287        final String bestDateTimePattern =
288                DateFormat.getBestDateTimePattern(mCurrentLocale, "yMMMd");
289        final int[] viewIndices = getMonthDayYearIndexes(bestDateTimePattern);
290
291        // Position the Year and MonthAndDay views within the header.
292        mMonthDayYearLayout.removeAllViews();
293        if (viewIndices[YEAR_INDEX] == 0) {
294            mMonthDayYearLayout.addView(mHeaderYearTextView);
295            mMonthDayYearLayout.addView(mMonthAndDayLayout);
296        } else {
297            mMonthDayYearLayout.addView(mMonthAndDayLayout);
298            mMonthDayYearLayout.addView(mHeaderYearTextView);
299        }
300
301        // Position Day and Month views within the MonthAndDay view.
302        mMonthAndDayLayout.removeAllViews();
303        if (viewIndices[MONTH_INDEX] > viewIndices[DAY_INDEX]) {
304            mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView);
305            mMonthAndDayLayout.addView(mHeaderMonthTextView);
306        } else {
307            mMonthAndDayLayout.addView(mHeaderMonthTextView);
308            mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView);
309        }
310
311        mHeaderMonthTextView.setText(mCurrentDate.getDisplayName(Calendar.MONTH, Calendar.SHORT,
312                Locale.getDefault()).toUpperCase(Locale.getDefault()));
313        mHeaderDayOfMonthTextView.setText(mDayFormat.format(mCurrentDate.getTime()));
314        mHeaderYearTextView.setText(mYearFormat.format(mCurrentDate.getTime()));
315
316        // Accessibility.
317        long millis = mCurrentDate.getTimeInMillis();
318        mAnimator.setDateMillis(millis);
319        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
320        String monthAndDayText = DateUtils.formatDateTime(mContext, millis, flags);
321        mMonthAndDayLayout.setContentDescription(monthAndDayText);
322
323        if (announce) {
324            flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
325            String fullDateText = DateUtils.formatDateTime(mContext, millis, flags);
326            mAnimator.announceForAccessibility(fullDateText);
327        }
328        updatePickers();
329    }
330
331    private void setCurrentView(final int viewIndex) {
332        long millis = mCurrentDate.getTimeInMillis();
333
334        switch (viewIndex) {
335            case MONTH_AND_DAY_VIEW:
336                mDayPickerView.onDateChanged();
337                if (mCurrentView != viewIndex) {
338                    mMonthAndDayLayout.setSelected(true);
339                    mHeaderYearTextView.setSelected(false);
340                    mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
341                    mCurrentView = viewIndex;
342                }
343
344                final int flags = DateUtils.FORMAT_SHOW_DATE;
345                final String dayString = DateUtils.formatDateTime(mContext, millis, flags);
346                mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString);
347                mAnimator.announceForAccessibility(mSelectDay);
348                break;
349            case YEAR_VIEW:
350                mYearPickerView.onDateChanged();
351                if (mCurrentView != viewIndex) {
352                    mMonthAndDayLayout.setSelected(false);
353                    mHeaderYearTextView.setSelected(true);
354                    mAnimator.setDisplayedChild(YEAR_VIEW);
355                    mCurrentView = viewIndex;
356                }
357
358                final CharSequence yearString = mYearFormat.format(millis);
359                mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString);
360                mAnimator.announceForAccessibility(mSelectYear);
361                break;
362        }
363    }
364
365    @Override
366    public void init(int year, int monthOfYear, int dayOfMonth,
367            DatePicker.OnDateChangedListener callBack) {
368        mDateChangedListener = callBack;
369        mCurrentDate.set(Calendar.YEAR, year);
370        mCurrentDate.set(Calendar.MONTH, monthOfYear);
371        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
372        updateDisplay(false);
373    }
374
375    @Override
376    public void updateDate(int year, int month, int dayOfMonth) {
377        mCurrentDate.set(Calendar.YEAR, year);
378        mCurrentDate.set(Calendar.MONTH, month);
379        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
380        if (mDateChangedListener != null) {
381            mDateChangedListener.onDateChanged(mDelegator, year, month, dayOfMonth);
382        }
383        updateDisplay(false);
384    }
385
386    @Override
387    public int getYear() {
388        return mCurrentDate.get(Calendar.YEAR);
389    }
390
391    @Override
392    public int getMonth() {
393        return mCurrentDate.get(Calendar.MONTH);
394    }
395
396    @Override
397    public int getDayOfMonth() {
398        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
399    }
400
401    @Override
402    public void setMinDate(long minDate) {
403        mTempDate.setTimeInMillis(minDate);
404        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
405                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
406            return;
407        }
408        if (mCurrentDate.before(mTempDate)) {
409            mCurrentDate.setTimeInMillis(minDate);
410            updatePickers();
411            updateDisplay(false);
412        }
413        mMinDate.setTimeInMillis(minDate);
414        mDayPickerView.setRange(mMinDate, mMaxDate);
415        mYearPickerView.setRange(mMinDate, mMaxDate);
416    }
417
418    @Override
419    public Calendar getMinDate() {
420        return mMinDate;
421    }
422
423    @Override
424    public void setMaxDate(long maxDate) {
425        mTempDate.setTimeInMillis(maxDate);
426        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
427                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
428            return;
429        }
430        if (mCurrentDate.after(mTempDate)) {
431            mCurrentDate.setTimeInMillis(maxDate);
432            updatePickers();
433            updateDisplay(false);
434        }
435        mMaxDate.setTimeInMillis(maxDate);
436        mDayPickerView.setRange(mMinDate, mMaxDate);
437        mYearPickerView.setRange(mMinDate, mMaxDate);
438    }
439
440    @Override
441    public Calendar getMaxDate() {
442        return mMaxDate;
443    }
444
445    @Override
446    public void setFirstDayOfWeek(int firstDayOfWeek) {
447        mFirstDayOfWeek = firstDayOfWeek;
448    }
449
450    @Override
451    public int getFirstDayOfWeek() {
452        if (mFirstDayOfWeek != USE_LOCALE) {
453            return mFirstDayOfWeek;
454        }
455        return mCurrentDate.getFirstDayOfWeek();
456    }
457
458    @Override
459    public void setEnabled(boolean enabled) {
460        mMonthAndDayLayout.setEnabled(enabled);
461        mHeaderYearTextView.setEnabled(enabled);
462        mAnimator.setEnabled(enabled);
463        mIsEnabled = enabled;
464    }
465
466    @Override
467    public boolean isEnabled() {
468        return mIsEnabled;
469    }
470
471    @Override
472    public CalendarView getCalendarView() {
473        throw new UnsupportedOperationException(
474                "CalendarView does not exists for the new DatePicker");
475    }
476
477    @Override
478    public void setCalendarViewShown(boolean shown) {
479        // No-op for compatibility with the old DatePicker.
480    }
481
482    @Override
483    public boolean getCalendarViewShown() {
484        return false;
485    }
486
487    @Override
488    public void setSpinnersShown(boolean shown) {
489        // No-op for compatibility with the old DatePicker.
490    }
491
492    @Override
493    public boolean getSpinnersShown() {
494        return false;
495    }
496
497    @Override
498    public void onConfigurationChanged(Configuration newConfig) {
499        mYearFormat = new SimpleDateFormat("y", newConfig.locale);
500        mDayFormat = new SimpleDateFormat("d", newConfig.locale);
501    }
502
503    @Override
504    public Parcelable onSaveInstanceState(Parcelable superState) {
505        final int year = mCurrentDate.get(Calendar.YEAR);
506        final int month = mCurrentDate.get(Calendar.MONTH);
507        final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
508
509        int listPosition = -1;
510        int listPositionOffset = -1;
511
512        if (mCurrentView == MONTH_AND_DAY_VIEW) {
513            listPosition = mDayPickerView.getMostVisiblePosition();
514        } else if (mCurrentView == YEAR_VIEW) {
515            listPosition = mYearPickerView.getFirstVisiblePosition();
516            listPositionOffset = mYearPickerView.getFirstPositionOffset();
517        }
518
519        return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
520                mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
521    }
522
523    @Override
524    public void onRestoreInstanceState(Parcelable state) {
525        SavedState ss = (SavedState) state;
526
527        mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
528        mCurrentView = ss.getCurrentView();
529        mMinDate.setTimeInMillis(ss.getMinDate());
530        mMaxDate.setTimeInMillis(ss.getMaxDate());
531
532        updateDisplay(false);
533        setCurrentView(mCurrentView);
534
535        final int listPosition = ss.getListPosition();
536        if (listPosition != -1) {
537            if (mCurrentView == MONTH_AND_DAY_VIEW) {
538                mDayPickerView.postSetSelection(listPosition);
539            } else if (mCurrentView == YEAR_VIEW) {
540                mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset());
541            }
542        }
543    }
544
545    @Override
546    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
547        onPopulateAccessibilityEvent(event);
548        return true;
549    }
550
551    @Override
552    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
553        event.getText().add(mCurrentDate.getTime().toString());
554    }
555
556    @Override
557    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
558        event.setClassName(DatePicker.class.getName());
559    }
560
561    @Override
562    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
563        info.setClassName(DatePicker.class.getName());
564    }
565
566    @Override
567    public void onYearSelected(int year) {
568        adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year);
569        mCurrentDate.set(Calendar.YEAR, year);
570        updatePickers();
571        setCurrentView(MONTH_AND_DAY_VIEW);
572        updateDisplay(true);
573    }
574
575    // If the newly selected month / year does not contain the currently selected day number,
576    // change the selected day number to the last day of the selected month or year.
577    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
578    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
579    private void adjustDayInMonthIfNeeded(int month, int year) {
580        int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
581        int daysInMonth = getDaysInMonth(month, year);
582        if (day > daysInMonth) {
583            mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
584        }
585    }
586
587    public static int getDaysInMonth(int month, int year) {
588        switch (month) {
589            case Calendar.JANUARY:
590            case Calendar.MARCH:
591            case Calendar.MAY:
592            case Calendar.JULY:
593            case Calendar.AUGUST:
594            case Calendar.OCTOBER:
595            case Calendar.DECEMBER:
596                return 31;
597            case Calendar.APRIL:
598            case Calendar.JUNE:
599            case Calendar.SEPTEMBER:
600            case Calendar.NOVEMBER:
601                return 30;
602            case Calendar.FEBRUARY:
603                return (year % 4 == 0) ? 29 : 28;
604            default:
605                throw new IllegalArgumentException("Invalid Month");
606        }
607    }
608
609    @Override
610    public void onDayOfMonthSelected(int year, int month, int day) {
611        mCurrentDate.set(Calendar.YEAR, year);
612        mCurrentDate.set(Calendar.MONTH, month);
613        mCurrentDate.set(Calendar.DAY_OF_MONTH, day);
614        updatePickers();
615        updateDisplay(true);
616    }
617
618    private void updatePickers() {
619        for (OnDateChangedListener listener : mListeners) {
620            listener.onDateChanged();
621        }
622    }
623
624    @Override
625    public void registerOnDateChangedListener(OnDateChangedListener listener) {
626        mListeners.add(listener);
627    }
628
629    @Override
630    public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
631        mListeners.remove(listener);
632    }
633
634    @Override
635    public Calendar getSelectedDay() {
636        return mCurrentDate;
637    }
638
639    @Override
640    public void tryVibrate() {
641        mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
642    }
643
644    @Override
645    public void onClick(View v) {
646        tryVibrate();
647        if (v.getId() == R.id.date_picker_year) {
648            setCurrentView(YEAR_VIEW);
649        } else if (v.getId() == R.id.date_picker_month_and_day_layout) {
650            setCurrentView(MONTH_AND_DAY_VIEW);
651        }
652    }
653
654    /**
655     * Class for managing state storing/restoring.
656     */
657    private static class SavedState extends View.BaseSavedState {
658
659        private final int mSelectedYear;
660        private final int mSelectedMonth;
661        private final int mSelectedDay;
662        private final long mMinDate;
663        private final long mMaxDate;
664        private final int mCurrentView;
665        private final int mListPosition;
666        private final int mListPositionOffset;
667
668        /**
669         * Constructor called from {@link DatePicker#onSaveInstanceState()}
670         */
671        private SavedState(Parcelable superState, int year, int month, int day,
672                long minDate, long maxDate, int currentView, int listPosition,
673                int listPositionOffset) {
674            super(superState);
675            mSelectedYear = year;
676            mSelectedMonth = month;
677            mSelectedDay = day;
678            mMinDate = minDate;
679            mMaxDate = maxDate;
680            mCurrentView = currentView;
681            mListPosition = listPosition;
682            mListPositionOffset = listPositionOffset;
683        }
684
685        /**
686         * Constructor called from {@link #CREATOR}
687         */
688        private SavedState(Parcel in) {
689            super(in);
690            mSelectedYear = in.readInt();
691            mSelectedMonth = in.readInt();
692            mSelectedDay = in.readInt();
693            mMinDate = in.readLong();
694            mMaxDate = in.readLong();
695            mCurrentView = in.readInt();
696            mListPosition = in.readInt();
697            mListPositionOffset = in.readInt();
698        }
699
700        @Override
701        public void writeToParcel(Parcel dest, int flags) {
702            super.writeToParcel(dest, flags);
703            dest.writeInt(mSelectedYear);
704            dest.writeInt(mSelectedMonth);
705            dest.writeInt(mSelectedDay);
706            dest.writeLong(mMinDate);
707            dest.writeLong(mMaxDate);
708            dest.writeInt(mCurrentView);
709            dest.writeInt(mListPosition);
710            dest.writeInt(mListPositionOffset);
711        }
712
713        public int getSelectedDay() {
714            return mSelectedDay;
715        }
716
717        public int getSelectedMonth() {
718            return mSelectedMonth;
719        }
720
721        public int getSelectedYear() {
722            return mSelectedYear;
723        }
724
725        public long getMinDate() {
726            return mMinDate;
727        }
728
729        public long getMaxDate() {
730            return mMaxDate;
731        }
732
733        public int getCurrentView() {
734            return mCurrentView;
735        }
736
737        public int getListPosition() {
738            return mListPosition;
739        }
740
741        public int getListPositionOffset() {
742            return mListPositionOffset;
743        }
744
745        @SuppressWarnings("all")
746        // suppress unused and hiding
747        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
748
749            public SavedState createFromParcel(Parcel in) {
750                return new SavedState(in);
751            }
752
753            public SavedState[] newArray(int size) {
754                return new SavedState[size];
755            }
756        };
757    }
758}
759