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.graphics.drawable.Drawable;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.text.format.DateFormat;
28import android.text.format.DateUtils;
29import android.util.AttributeSet;
30import android.util.SparseArray;
31import android.view.HapticFeedbackConstants;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.accessibility.AccessibilityEvent;
35import android.view.accessibility.AccessibilityNodeInfo;
36import android.view.animation.AlphaAnimation;
37import android.view.animation.Animation;
38
39import com.android.internal.R;
40import com.android.internal.widget.AccessibleDateAnimator;
41
42import java.text.SimpleDateFormat;
43import java.util.Calendar;
44import java.util.HashSet;
45import java.util.Locale;
46
47/**
48 * A delegate for picking up a date (day / month / year).
49 */
50class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate implements
51        View.OnClickListener, DatePickerController {
52    private static final int USE_LOCALE = 0;
53
54    private static final int UNINITIALIZED = -1;
55    private static final int MONTH_AND_DAY_VIEW = 0;
56    private static final int YEAR_VIEW = 1;
57
58    private static final int DEFAULT_START_YEAR = 1900;
59    private static final int DEFAULT_END_YEAR = 2100;
60
61    private static final int ANIMATION_DURATION = 300;
62
63    private static final int MONTH_INDEX = 0;
64    private static final int DAY_INDEX = 1;
65    private static final int YEAR_INDEX = 2;
66
67    private SimpleDateFormat mYearFormat = new SimpleDateFormat("y", Locale.getDefault());
68    private SimpleDateFormat mDayFormat = new SimpleDateFormat("d", Locale.getDefault());
69
70    private TextView mDayOfWeekView;
71
72    /** Layout that contains the current month, day, and year. */
73    private LinearLayout mMonthDayYearLayout;
74
75    /** Clickable layout that contains the current day and year. */
76    private LinearLayout mMonthAndDayLayout;
77
78    private TextView mHeaderMonthTextView;
79    private TextView mHeaderDayOfMonthTextView;
80    private TextView mHeaderYearTextView;
81    private DayPickerView mDayPickerView;
82    private YearPickerView mYearPickerView;
83
84    private boolean mIsEnabled = true;
85
86    // Accessibility strings.
87    private String mDayPickerDescription;
88    private String mSelectDay;
89    private String mYearPickerDescription;
90    private String mSelectYear;
91
92    private AccessibleDateAnimator mAnimator;
93
94    private DatePicker.OnDateChangedListener mDateChangedListener;
95
96    private int mCurrentView = UNINITIALIZED;
97
98    private Calendar mCurrentDate;
99    private Calendar mTempDate;
100    private Calendar mMinDate;
101    private Calendar mMaxDate;
102
103    private int mFirstDayOfWeek = USE_LOCALE;
104
105    private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
106
107    public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
108            int defStyleAttr, int defStyleRes) {
109        super(delegator, context);
110
111        final Locale locale = Locale.getDefault();
112        mMinDate = getCalendarForLocale(mMinDate, locale);
113        mMaxDate = getCalendarForLocale(mMaxDate, locale);
114        mTempDate = getCalendarForLocale(mMaxDate, locale);
115        mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
116
117        mMinDate.set(DEFAULT_START_YEAR, 1, 1);
118        mMaxDate.set(DEFAULT_END_YEAR, 12, 31);
119
120        final Resources res = mDelegator.getResources();
121        final TypedArray a = mContext.obtainStyledAttributes(attrs,
122                R.styleable.DatePicker, defStyleAttr, defStyleRes);
123        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
124                Context.LAYOUT_INFLATER_SERVICE);
125        final int layoutResourceId = a.getResourceId(
126                R.styleable.DatePicker_internalLayout, R.layout.date_picker_holo);
127        final View mainView = inflater.inflate(layoutResourceId, null);
128        mDelegator.addView(mainView);
129
130        mDayOfWeekView = (TextView) mainView.findViewById(R.id.date_picker_header);
131
132        // Layout that contains the current date and day name header.
133        final LinearLayout dateLayout = (LinearLayout) mainView.findViewById(
134                R.id.day_picker_selector_layout);
135        mMonthDayYearLayout = (LinearLayout) mainView.findViewById(
136                R.id.date_picker_month_day_year_layout);
137        mMonthAndDayLayout = (LinearLayout) mainView.findViewById(
138                R.id.date_picker_month_and_day_layout);
139        mMonthAndDayLayout.setOnClickListener(this);
140        mHeaderMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_month);
141        mHeaderDayOfMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_day);
142        mHeaderYearTextView = (TextView) mainView.findViewById(R.id.date_picker_year);
143        mHeaderYearTextView.setOnClickListener(this);
144
145        // Obtain default highlight color from the theme.
146        final int defaultHighlightColor = mHeaderYearTextView.getHighlightColor();
147
148        // Use Theme attributes if possible
149        final int dayOfWeekTextAppearanceResId = a.getResourceId(
150                R.styleable.DatePicker_dayOfWeekTextAppearance, -1);
151        if (dayOfWeekTextAppearanceResId != -1) {
152            mDayOfWeekView.setTextAppearance(context, dayOfWeekTextAppearanceResId);
153        }
154
155        mDayOfWeekView.setBackground(a.getDrawable(R.styleable.DatePicker_dayOfWeekBackground));
156
157        dateLayout.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
158
159        final int headerSelectedTextColor = a.getColor(
160                R.styleable.DatePicker_headerSelectedTextColor, defaultHighlightColor);
161        final int monthTextAppearanceResId = a.getResourceId(
162                R.styleable.DatePicker_headerMonthTextAppearance, -1);
163        if (monthTextAppearanceResId != -1) {
164            mHeaderMonthTextView.setTextAppearance(context, monthTextAppearanceResId);
165        }
166        mHeaderMonthTextView.setTextColor(ColorStateList.addFirstIfMissing(
167                mHeaderMonthTextView.getTextColors(), R.attr.state_selected,
168                headerSelectedTextColor));
169
170        final int dayOfMonthTextAppearanceResId = a.getResourceId(
171                R.styleable.DatePicker_headerDayOfMonthTextAppearance, -1);
172        if (dayOfMonthTextAppearanceResId != -1) {
173            mHeaderDayOfMonthTextView.setTextAppearance(context, dayOfMonthTextAppearanceResId);
174        }
175        mHeaderDayOfMonthTextView.setTextColor(ColorStateList.addFirstIfMissing(
176                mHeaderDayOfMonthTextView.getTextColors(), R.attr.state_selected,
177                headerSelectedTextColor));
178
179        final int yearTextAppearanceResId = a.getResourceId(
180                R.styleable.DatePicker_headerYearTextAppearance, -1);
181        if (yearTextAppearanceResId != -1) {
182            mHeaderYearTextView.setTextAppearance(context, yearTextAppearanceResId);
183        }
184        mHeaderYearTextView.setTextColor(ColorStateList.addFirstIfMissing(
185                mHeaderYearTextView.getTextColors(), R.attr.state_selected,
186                headerSelectedTextColor));
187
188        mDayPickerView = new DayPickerView(mContext, this);
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.goTo(getSelectedDay(), false, true, true);
415    }
416
417    @Override
418    public Calendar getMinDate() {
419        return mMinDate;
420    }
421
422    @Override
423    public void setMaxDate(long maxDate) {
424        mTempDate.setTimeInMillis(maxDate);
425        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
426                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
427            return;
428        }
429        if (mCurrentDate.after(mTempDate)) {
430            mCurrentDate.setTimeInMillis(maxDate);
431            updatePickers();
432            updateDisplay(false);
433        }
434        mMaxDate.setTimeInMillis(maxDate);
435        mDayPickerView.goTo(getSelectedDay(), false, true, true);
436    }
437
438    @Override
439    public Calendar getMaxDate() {
440        return mMaxDate;
441    }
442
443    @Override
444    public void setFirstDayOfWeek(int firstDayOfWeek) {
445        mFirstDayOfWeek = firstDayOfWeek;
446    }
447
448    @Override
449    public int getFirstDayOfWeek() {
450        if (mFirstDayOfWeek != USE_LOCALE) {
451            return mFirstDayOfWeek;
452        }
453        return mCurrentDate.getFirstDayOfWeek();
454    }
455
456    @Override
457    public int getMinYear() {
458        return mMinDate.get(Calendar.YEAR);
459    }
460
461    @Override
462    public int getMaxYear() {
463        return mMaxDate.get(Calendar.YEAR);
464    }
465
466    @Override
467    public int getMinMonth() {
468        return mMinDate.get(Calendar.MONTH);
469    }
470
471    @Override
472    public int getMaxMonth() {
473        return mMaxDate.get(Calendar.MONTH);
474    }
475
476    @Override
477    public int getMinDay() {
478        return mMinDate.get(Calendar.DAY_OF_MONTH);
479    }
480
481    @Override
482    public int getMaxDay() {
483        return mMaxDate.get(Calendar.DAY_OF_MONTH);
484    }
485
486    @Override
487    public void setEnabled(boolean enabled) {
488        mMonthAndDayLayout.setEnabled(enabled);
489        mHeaderYearTextView.setEnabled(enabled);
490        mAnimator.setEnabled(enabled);
491        mIsEnabled = enabled;
492    }
493
494    @Override
495    public boolean isEnabled() {
496        return mIsEnabled;
497    }
498
499    @Override
500    public CalendarView getCalendarView() {
501        throw new UnsupportedOperationException(
502                "CalendarView does not exists for the new DatePicker");
503    }
504
505    @Override
506    public void setCalendarViewShown(boolean shown) {
507        // No-op for compatibility with the old DatePicker.
508    }
509
510    @Override
511    public boolean getCalendarViewShown() {
512        return false;
513    }
514
515    @Override
516    public void setSpinnersShown(boolean shown) {
517        // No-op for compatibility with the old DatePicker.
518    }
519
520    @Override
521    public boolean getSpinnersShown() {
522        return false;
523    }
524
525    @Override
526    public void onConfigurationChanged(Configuration newConfig) {
527        mYearFormat = new SimpleDateFormat("y", newConfig.locale);
528        mDayFormat = new SimpleDateFormat("d", newConfig.locale);
529    }
530
531    @Override
532    public Parcelable onSaveInstanceState(Parcelable superState) {
533        final int year = mCurrentDate.get(Calendar.YEAR);
534        final int month = mCurrentDate.get(Calendar.MONTH);
535        final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
536
537        int listPosition = -1;
538        int listPositionOffset = -1;
539
540        if (mCurrentView == MONTH_AND_DAY_VIEW) {
541            listPosition = mDayPickerView.getMostVisiblePosition();
542        } else if (mCurrentView == YEAR_VIEW) {
543            listPosition = mYearPickerView.getFirstVisiblePosition();
544            listPositionOffset = mYearPickerView.getFirstPositionOffset();
545        }
546
547        return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
548                mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
549    }
550
551    @Override
552    public void onRestoreInstanceState(Parcelable state) {
553        SavedState ss = (SavedState) state;
554
555        mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
556        mCurrentView = ss.getCurrentView();
557        mMinDate.setTimeInMillis(ss.getMinDate());
558        mMaxDate.setTimeInMillis(ss.getMaxDate());
559
560        updateDisplay(false);
561        setCurrentView(mCurrentView);
562
563        final int listPosition = ss.getListPosition();
564        if (listPosition != -1) {
565            if (mCurrentView == MONTH_AND_DAY_VIEW) {
566                mDayPickerView.postSetSelection(listPosition);
567            } else if (mCurrentView == YEAR_VIEW) {
568                mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset());
569            }
570        }
571    }
572
573    @Override
574    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
575        onPopulateAccessibilityEvent(event);
576        return true;
577    }
578
579    @Override
580    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
581        event.getText().add(mCurrentDate.getTime().toString());
582    }
583
584    @Override
585    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
586        event.setClassName(DatePicker.class.getName());
587    }
588
589    @Override
590    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
591        info.setClassName(DatePicker.class.getName());
592    }
593
594    @Override
595    public void onYearSelected(int year) {
596        adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year);
597        mCurrentDate.set(Calendar.YEAR, year);
598        updatePickers();
599        setCurrentView(MONTH_AND_DAY_VIEW);
600        updateDisplay(true);
601    }
602
603    // If the newly selected month / year does not contain the currently selected day number,
604    // change the selected day number to the last day of the selected month or year.
605    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
606    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
607    private void adjustDayInMonthIfNeeded(int month, int year) {
608        int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
609        int daysInMonth = getDaysInMonth(month, year);
610        if (day > daysInMonth) {
611            mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
612        }
613    }
614
615    public static int getDaysInMonth(int month, int year) {
616        switch (month) {
617            case Calendar.JANUARY:
618            case Calendar.MARCH:
619            case Calendar.MAY:
620            case Calendar.JULY:
621            case Calendar.AUGUST:
622            case Calendar.OCTOBER:
623            case Calendar.DECEMBER:
624                return 31;
625            case Calendar.APRIL:
626            case Calendar.JUNE:
627            case Calendar.SEPTEMBER:
628            case Calendar.NOVEMBER:
629                return 30;
630            case Calendar.FEBRUARY:
631                return (year % 4 == 0) ? 29 : 28;
632            default:
633                throw new IllegalArgumentException("Invalid Month");
634        }
635    }
636
637    @Override
638    public void onDayOfMonthSelected(int year, int month, int day) {
639        mCurrentDate.set(Calendar.YEAR, year);
640        mCurrentDate.set(Calendar.MONTH, month);
641        mCurrentDate.set(Calendar.DAY_OF_MONTH, day);
642        updatePickers();
643        updateDisplay(true);
644    }
645
646    private void updatePickers() {
647        for (OnDateChangedListener listener : mListeners) {
648            listener.onDateChanged();
649        }
650    }
651
652    @Override
653    public void registerOnDateChangedListener(OnDateChangedListener listener) {
654        mListeners.add(listener);
655    }
656
657    @Override
658    public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
659        mListeners.remove(listener);
660    }
661
662    @Override
663    public Calendar getSelectedDay() {
664        return mCurrentDate;
665    }
666
667    @Override
668    public void tryVibrate() {
669        mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
670    }
671
672    @Override
673    public void onClick(View v) {
674        tryVibrate();
675        if (v.getId() == R.id.date_picker_year) {
676            setCurrentView(YEAR_VIEW);
677        } else if (v.getId() == R.id.date_picker_month_and_day_layout) {
678            setCurrentView(MONTH_AND_DAY_VIEW);
679        }
680    }
681
682    /**
683     * Class for managing state storing/restoring.
684     */
685    private static class SavedState extends View.BaseSavedState {
686
687        private final int mSelectedYear;
688        private final int mSelectedMonth;
689        private final int mSelectedDay;
690        private final long mMinDate;
691        private final long mMaxDate;
692        private final int mCurrentView;
693        private final int mListPosition;
694        private final int mListPositionOffset;
695
696        /**
697         * Constructor called from {@link DatePicker#onSaveInstanceState()}
698         */
699        private SavedState(Parcelable superState, int year, int month, int day,
700                long minDate, long maxDate, int currentView, int listPosition,
701                int listPositionOffset) {
702            super(superState);
703            mSelectedYear = year;
704            mSelectedMonth = month;
705            mSelectedDay = day;
706            mMinDate = minDate;
707            mMaxDate = maxDate;
708            mCurrentView = currentView;
709            mListPosition = listPosition;
710            mListPositionOffset = listPositionOffset;
711        }
712
713        /**
714         * Constructor called from {@link #CREATOR}
715         */
716        private SavedState(Parcel in) {
717            super(in);
718            mSelectedYear = in.readInt();
719            mSelectedMonth = in.readInt();
720            mSelectedDay = in.readInt();
721            mMinDate = in.readLong();
722            mMaxDate = in.readLong();
723            mCurrentView = in.readInt();
724            mListPosition = in.readInt();
725            mListPositionOffset = in.readInt();
726        }
727
728        @Override
729        public void writeToParcel(Parcel dest, int flags) {
730            super.writeToParcel(dest, flags);
731            dest.writeInt(mSelectedYear);
732            dest.writeInt(mSelectedMonth);
733            dest.writeInt(mSelectedDay);
734            dest.writeLong(mMinDate);
735            dest.writeLong(mMaxDate);
736            dest.writeInt(mCurrentView);
737            dest.writeInt(mListPosition);
738            dest.writeInt(mListPositionOffset);
739        }
740
741        public int getSelectedDay() {
742            return mSelectedDay;
743        }
744
745        public int getSelectedMonth() {
746            return mSelectedMonth;
747        }
748
749        public int getSelectedYear() {
750            return mSelectedYear;
751        }
752
753        public long getMinDate() {
754            return mMinDate;
755        }
756
757        public long getMaxDate() {
758            return mMaxDate;
759        }
760
761        public int getCurrentView() {
762            return mCurrentView;
763        }
764
765        public int getListPosition() {
766            return mListPosition;
767        }
768
769        public int getListPositionOffset() {
770            return mListPositionOffset;
771        }
772
773        @SuppressWarnings("all")
774        // suppress unused and hiding
775        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
776
777            public SavedState createFromParcel(Parcel in) {
778                return new SavedState(in);
779            }
780
781            public SavedState[] newArray(int size) {
782                return new SavedState[size];
783            }
784        };
785    }
786}
787