DatePickerCalendarDelegate.java revision 0cecbc9c7208f918ddf9d117057649f6e132069f
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);
187        mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
188        mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
189        mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
190        mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
191        mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
192
193        mYearPickerView = new YearPickerView(mContext);
194        mYearPickerView.init(this);
195
196        final int yearSelectedCircleColor = a.getColor(R.styleable.DatePicker_yearListSelectorColor,
197                defaultHighlightColor);
198        mYearPickerView.setYearSelectedCircleColor(yearSelectedCircleColor);
199
200        final ColorStateList calendarTextColor = a.getColorStateList(
201                R.styleable.DatePicker_calendarTextColor);
202        final int calendarSelectedTextColor = a.getColor(
203                R.styleable.DatePicker_calendarSelectedTextColor, defaultHighlightColor);
204        mDayPickerView.setCalendarTextColor(ColorStateList.addFirstIfMissing(
205                calendarTextColor, R.attr.state_selected, calendarSelectedTextColor));
206
207        mDayPickerDescription = res.getString(R.string.day_picker_description);
208        mSelectDay = res.getString(R.string.select_day);
209        mYearPickerDescription = res.getString(R.string.year_picker_description);
210        mSelectYear = res.getString(R.string.select_year);
211
212        mAnimator = (AccessibleDateAnimator) mainView.findViewById(R.id.animator);
213        mAnimator.addView(mDayPickerView);
214        mAnimator.addView(mYearPickerView);
215        mAnimator.setDateMillis(mCurrentDate.getTimeInMillis());
216
217        final Animation animation = new AlphaAnimation(0.0f, 1.0f);
218        animation.setDuration(ANIMATION_DURATION);
219        mAnimator.setInAnimation(animation);
220
221        final Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
222        animation2.setDuration(ANIMATION_DURATION);
223        mAnimator.setOutAnimation(animation2);
224
225        updateDisplay(false);
226        setCurrentView(MONTH_AND_DAY_VIEW);
227    }
228
229    /**
230     * Gets a calendar for locale bootstrapped with the value of a given calendar.
231     *
232     * @param oldCalendar The old calendar.
233     * @param locale The locale.
234     */
235    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
236        if (oldCalendar == null) {
237            return Calendar.getInstance(locale);
238        } else {
239            final long currentTimeMillis = oldCalendar.getTimeInMillis();
240            Calendar newCalendar = Calendar.getInstance(locale);
241            newCalendar.setTimeInMillis(currentTimeMillis);
242            return newCalendar;
243        }
244    }
245
246    /**
247     * Compute the array representing the order of Month / Day / Year views in their layout.
248     * Will be used for I18N purpose as the order of them depends on the Locale.
249     */
250    private int[] getMonthDayYearIndexes(String pattern) {
251        int[] result = new int[3];
252
253        final String filteredPattern = pattern.replaceAll("'.*?'", "");
254
255        final int dayIndex = filteredPattern.indexOf('d');
256        final int monthMIndex = filteredPattern.indexOf("M");
257        final int monthIndex = (monthMIndex != -1) ? monthMIndex : filteredPattern.indexOf("L");
258        final int yearIndex = filteredPattern.indexOf("y");
259
260        if (yearIndex < monthIndex) {
261            result[YEAR_INDEX] = 0;
262
263            if (monthIndex < dayIndex) {
264                result[MONTH_INDEX] = 1;
265                result[DAY_INDEX] = 2;
266            } else {
267                result[MONTH_INDEX] = 2;
268                result[DAY_INDEX] = 1;
269            }
270        } else {
271            result[YEAR_INDEX] = 2;
272
273            if (monthIndex < dayIndex) {
274                result[MONTH_INDEX] = 0;
275                result[DAY_INDEX] = 1;
276            } else {
277                result[MONTH_INDEX] = 1;
278                result[DAY_INDEX] = 0;
279            }
280        }
281        return result;
282    }
283
284    private void updateDisplay(boolean announce) {
285        if (mDayOfWeekView != null) {
286            mDayOfWeekView.setText(mCurrentDate.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
287                    Locale.getDefault()));
288        }
289
290        // Compute indices of Month, Day and Year views
291        final String bestDateTimePattern =
292                DateFormat.getBestDateTimePattern(mCurrentLocale, "yMMMd");
293        final int[] viewIndices = getMonthDayYearIndexes(bestDateTimePattern);
294
295        // Position the Year and MonthAndDay views within the header.
296        mMonthDayYearLayout.removeAllViews();
297        if (viewIndices[YEAR_INDEX] == 0) {
298            mMonthDayYearLayout.addView(mHeaderYearTextView);
299            mMonthDayYearLayout.addView(mMonthAndDayLayout);
300        } else {
301            mMonthDayYearLayout.addView(mMonthAndDayLayout);
302            mMonthDayYearLayout.addView(mHeaderYearTextView);
303        }
304
305        // Position Day and Month views within the MonthAndDay view.
306        mMonthAndDayLayout.removeAllViews();
307        if (viewIndices[MONTH_INDEX] > viewIndices[DAY_INDEX]) {
308            mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView);
309            mMonthAndDayLayout.addView(mHeaderMonthTextView);
310        } else {
311            mMonthAndDayLayout.addView(mHeaderMonthTextView);
312            mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView);
313        }
314
315        mHeaderMonthTextView.setText(mCurrentDate.getDisplayName(Calendar.MONTH, Calendar.SHORT,
316                Locale.getDefault()).toUpperCase(Locale.getDefault()));
317        mHeaderDayOfMonthTextView.setText(mDayFormat.format(mCurrentDate.getTime()));
318        mHeaderYearTextView.setText(mYearFormat.format(mCurrentDate.getTime()));
319
320        // Accessibility.
321        long millis = mCurrentDate.getTimeInMillis();
322        mAnimator.setDateMillis(millis);
323        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
324        String monthAndDayText = DateUtils.formatDateTime(mContext, millis, flags);
325        mMonthAndDayLayout.setContentDescription(monthAndDayText);
326
327        if (announce) {
328            flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
329            String fullDateText = DateUtils.formatDateTime(mContext, millis, flags);
330            mAnimator.announceForAccessibility(fullDateText);
331        }
332    }
333
334    private void setCurrentView(final int viewIndex) {
335        long millis = mCurrentDate.getTimeInMillis();
336
337        switch (viewIndex) {
338            case MONTH_AND_DAY_VIEW:
339                mDayPickerView.setDate(getSelectedDay().getTimeInMillis());
340                if (mCurrentView != viewIndex) {
341                    mMonthAndDayLayout.setSelected(true);
342                    mHeaderYearTextView.setSelected(false);
343                    mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
344                    mCurrentView = viewIndex;
345                }
346
347                final int flags = DateUtils.FORMAT_SHOW_DATE;
348                final String dayString = DateUtils.formatDateTime(mContext, millis, flags);
349                mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString);
350                mAnimator.announceForAccessibility(mSelectDay);
351                break;
352            case YEAR_VIEW:
353                mYearPickerView.onDateChanged();
354                if (mCurrentView != viewIndex) {
355                    mMonthAndDayLayout.setSelected(false);
356                    mHeaderYearTextView.setSelected(true);
357                    mAnimator.setDisplayedChild(YEAR_VIEW);
358                    mCurrentView = viewIndex;
359                }
360
361                final CharSequence yearString = mYearFormat.format(millis);
362                mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString);
363                mAnimator.announceForAccessibility(mSelectYear);
364                break;
365        }
366    }
367
368    @Override
369    public void init(int year, int monthOfYear, int dayOfMonth,
370            DatePicker.OnDateChangedListener callBack) {
371        mCurrentDate.set(Calendar.YEAR, year);
372        mCurrentDate.set(Calendar.MONTH, monthOfYear);
373        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
374
375        mDateChangedListener = callBack;
376
377        onDateChanged(false, false);
378    }
379
380    @Override
381    public void updateDate(int year, int month, int dayOfMonth) {
382        mCurrentDate.set(Calendar.YEAR, year);
383        mCurrentDate.set(Calendar.MONTH, month);
384        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
385
386        onDateChanged(false, true);
387    }
388
389    private void onDateChanged(boolean fromUser, boolean callbackToClient) {
390        if (callbackToClient && mDateChangedListener != null) {
391            final int year = mCurrentDate.get(Calendar.YEAR);
392            final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
393            final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
394            mDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
395        }
396
397        for (OnDateChangedListener listener : mListeners) {
398            listener.onDateChanged();
399        }
400
401        mDayPickerView.setDate(getSelectedDay().getTimeInMillis());
402
403        updateDisplay(fromUser);
404
405        if (fromUser) {
406            tryVibrate();
407        }
408    }
409
410    @Override
411    public int getYear() {
412        return mCurrentDate.get(Calendar.YEAR);
413    }
414
415    @Override
416    public int getMonth() {
417        return mCurrentDate.get(Calendar.MONTH);
418    }
419
420    @Override
421    public int getDayOfMonth() {
422        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
423    }
424
425    @Override
426    public void setMinDate(long minDate) {
427        mTempDate.setTimeInMillis(minDate);
428        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
429                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
430            return;
431        }
432        if (mCurrentDate.before(mTempDate)) {
433            mCurrentDate.setTimeInMillis(minDate);
434            onDateChanged(false, true);
435        }
436        mMinDate.setTimeInMillis(minDate);
437        mDayPickerView.setMinDate(minDate);
438        mYearPickerView.setRange(mMinDate, mMaxDate);
439    }
440
441    @Override
442    public Calendar getMinDate() {
443        return mMinDate;
444    }
445
446    @Override
447    public void setMaxDate(long maxDate) {
448        mTempDate.setTimeInMillis(maxDate);
449        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
450                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
451            return;
452        }
453        if (mCurrentDate.after(mTempDate)) {
454            mCurrentDate.setTimeInMillis(maxDate);
455            onDateChanged(false, true);
456        }
457        mMaxDate.setTimeInMillis(maxDate);
458        mDayPickerView.setMaxDate(maxDate);
459        mYearPickerView.setRange(mMinDate, mMaxDate);
460    }
461
462    @Override
463    public Calendar getMaxDate() {
464        return mMaxDate;
465    }
466
467    @Override
468    public void setFirstDayOfWeek(int firstDayOfWeek) {
469        mFirstDayOfWeek = firstDayOfWeek;
470
471        mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
472    }
473
474    @Override
475    public int getFirstDayOfWeek() {
476        if (mFirstDayOfWeek != USE_LOCALE) {
477            return mFirstDayOfWeek;
478        }
479        return mCurrentDate.getFirstDayOfWeek();
480    }
481
482    @Override
483    public void setEnabled(boolean enabled) {
484        mMonthAndDayLayout.setEnabled(enabled);
485        mHeaderYearTextView.setEnabled(enabled);
486        mAnimator.setEnabled(enabled);
487        mIsEnabled = enabled;
488    }
489
490    @Override
491    public boolean isEnabled() {
492        return mIsEnabled;
493    }
494
495    @Override
496    public CalendarView getCalendarView() {
497        throw new UnsupportedOperationException(
498                "CalendarView does not exists for the new DatePicker");
499    }
500
501    @Override
502    public void setCalendarViewShown(boolean shown) {
503        // No-op for compatibility with the old DatePicker.
504    }
505
506    @Override
507    public boolean getCalendarViewShown() {
508        return false;
509    }
510
511    @Override
512    public void setSpinnersShown(boolean shown) {
513        // No-op for compatibility with the old DatePicker.
514    }
515
516    @Override
517    public boolean getSpinnersShown() {
518        return false;
519    }
520
521    @Override
522    public void onConfigurationChanged(Configuration newConfig) {
523        mYearFormat = new SimpleDateFormat("y", newConfig.locale);
524        mDayFormat = new SimpleDateFormat("d", newConfig.locale);
525    }
526
527    @Override
528    public Parcelable onSaveInstanceState(Parcelable superState) {
529        final int year = mCurrentDate.get(Calendar.YEAR);
530        final int month = mCurrentDate.get(Calendar.MONTH);
531        final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
532
533        int listPosition = -1;
534        int listPositionOffset = -1;
535
536        if (mCurrentView == MONTH_AND_DAY_VIEW) {
537            listPosition = mDayPickerView.getMostVisiblePosition();
538        } else if (mCurrentView == YEAR_VIEW) {
539            listPosition = mYearPickerView.getFirstVisiblePosition();
540            listPositionOffset = mYearPickerView.getFirstPositionOffset();
541        }
542
543        return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
544                mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
545    }
546
547    @Override
548    public void onRestoreInstanceState(Parcelable state) {
549        SavedState ss = (SavedState) state;
550
551        mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
552        mCurrentView = ss.getCurrentView();
553        mMinDate.setTimeInMillis(ss.getMinDate());
554        mMaxDate.setTimeInMillis(ss.getMaxDate());
555
556        updateDisplay(false);
557        setCurrentView(mCurrentView);
558
559        final int listPosition = ss.getListPosition();
560        if (listPosition != -1) {
561            if (mCurrentView == MONTH_AND_DAY_VIEW) {
562                mDayPickerView.postSetSelection(listPosition);
563            } else if (mCurrentView == YEAR_VIEW) {
564                mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset());
565            }
566        }
567    }
568
569    @Override
570    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
571        onPopulateAccessibilityEvent(event);
572        return true;
573    }
574
575    @Override
576    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
577        event.getText().add(mCurrentDate.getTime().toString());
578    }
579
580    @Override
581    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
582        event.setClassName(DatePicker.class.getName());
583    }
584
585    @Override
586    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
587        info.setClassName(DatePicker.class.getName());
588    }
589
590    @Override
591    public void onYearSelected(int year) {
592        adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year);
593        mCurrentDate.set(Calendar.YEAR, year);
594        onDateChanged(true, true);
595
596        // Auto-advance to month and day view.
597        setCurrentView(MONTH_AND_DAY_VIEW);
598    }
599
600    // If the newly selected month / year does not contain the currently selected day number,
601    // change the selected day number to the last day of the selected month or year.
602    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
603    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
604    private void adjustDayInMonthIfNeeded(int month, int year) {
605        int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
606        int daysInMonth = getDaysInMonth(month, year);
607        if (day > daysInMonth) {
608            mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
609        }
610    }
611
612    public static int getDaysInMonth(int month, int year) {
613        switch (month) {
614            case Calendar.JANUARY:
615            case Calendar.MARCH:
616            case Calendar.MAY:
617            case Calendar.JULY:
618            case Calendar.AUGUST:
619            case Calendar.OCTOBER:
620            case Calendar.DECEMBER:
621                return 31;
622            case Calendar.APRIL:
623            case Calendar.JUNE:
624            case Calendar.SEPTEMBER:
625            case Calendar.NOVEMBER:
626                return 30;
627            case Calendar.FEBRUARY:
628                return (year % 4 == 0) ? 29 : 28;
629            default:
630                throw new IllegalArgumentException("Invalid Month");
631        }
632    }
633
634    @Override
635    public void registerOnDateChangedListener(OnDateChangedListener listener) {
636        mListeners.add(listener);
637    }
638
639    @Override
640    public Calendar getSelectedDay() {
641        return mCurrentDate;
642    }
643
644    @Override
645    public void tryVibrate() {
646        mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
647    }
648
649    @Override
650    public void onClick(View v) {
651        tryVibrate();
652        if (v.getId() == R.id.date_picker_year) {
653            setCurrentView(YEAR_VIEW);
654        } else if (v.getId() == R.id.date_picker_month_and_day_layout) {
655            setCurrentView(MONTH_AND_DAY_VIEW);
656        }
657    }
658
659    /**
660     * Listener called when the user selects a day in the day picker view.
661     */
662    private final DayPickerView.OnDaySelectedListener
663            mOnDaySelectedListener = new DayPickerView.OnDaySelectedListener() {
664        @Override
665        public void onDaySelected(DayPickerView view, Calendar day) {
666            mCurrentDate.setTimeInMillis(day.getTimeInMillis());
667            onDateChanged(true, true);
668        }
669    };
670
671    /**
672     * Class for managing state storing/restoring.
673     */
674    private static class SavedState extends View.BaseSavedState {
675
676        private final int mSelectedYear;
677        private final int mSelectedMonth;
678        private final int mSelectedDay;
679        private final long mMinDate;
680        private final long mMaxDate;
681        private final int mCurrentView;
682        private final int mListPosition;
683        private final int mListPositionOffset;
684
685        /**
686         * Constructor called from {@link DatePicker#onSaveInstanceState()}
687         */
688        private SavedState(Parcelable superState, int year, int month, int day,
689                long minDate, long maxDate, int currentView, int listPosition,
690                int listPositionOffset) {
691            super(superState);
692            mSelectedYear = year;
693            mSelectedMonth = month;
694            mSelectedDay = day;
695            mMinDate = minDate;
696            mMaxDate = maxDate;
697            mCurrentView = currentView;
698            mListPosition = listPosition;
699            mListPositionOffset = listPositionOffset;
700        }
701
702        /**
703         * Constructor called from {@link #CREATOR}
704         */
705        private SavedState(Parcel in) {
706            super(in);
707            mSelectedYear = in.readInt();
708            mSelectedMonth = in.readInt();
709            mSelectedDay = in.readInt();
710            mMinDate = in.readLong();
711            mMaxDate = in.readLong();
712            mCurrentView = in.readInt();
713            mListPosition = in.readInt();
714            mListPositionOffset = in.readInt();
715        }
716
717        @Override
718        public void writeToParcel(Parcel dest, int flags) {
719            super.writeToParcel(dest, flags);
720            dest.writeInt(mSelectedYear);
721            dest.writeInt(mSelectedMonth);
722            dest.writeInt(mSelectedDay);
723            dest.writeLong(mMinDate);
724            dest.writeLong(mMaxDate);
725            dest.writeInt(mCurrentView);
726            dest.writeInt(mListPosition);
727            dest.writeInt(mListPositionOffset);
728        }
729
730        public int getSelectedDay() {
731            return mSelectedDay;
732        }
733
734        public int getSelectedMonth() {
735            return mSelectedMonth;
736        }
737
738        public int getSelectedYear() {
739            return mSelectedYear;
740        }
741
742        public long getMinDate() {
743            return mMinDate;
744        }
745
746        public long getMaxDate() {
747            return mMaxDate;
748        }
749
750        public int getCurrentView() {
751            return mCurrentView;
752        }
753
754        public int getListPosition() {
755            return mListPosition;
756        }
757
758        public int getListPositionOffset() {
759            return mListPositionOffset;
760        }
761
762        @SuppressWarnings("all")
763        // suppress unused and hiding
764        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
765
766            public SavedState createFromParcel(Parcel in) {
767                return new SavedState(in);
768            }
769
770            public SavedState[] newArray(int size) {
771                return new SavedState[size];
772            }
773        };
774    }
775}
776