DatePickerCalendarDelegate.java revision e6ec07fc5d7ce7732f63b6da73d3cc9e207df69f
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        onDateChanged(false);
376
377        // Set the listener last so that we don't call it.
378        mDateChangedListener = callBack;
379    }
380
381    @Override
382    public void updateDate(int year, int month, int dayOfMonth) {
383        mCurrentDate.set(Calendar.YEAR, year);
384        mCurrentDate.set(Calendar.MONTH, month);
385        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
386
387        onDateChanged(false);
388    }
389
390    private void onDateChanged(boolean fromUser) {
391        if (mDateChangedListener != null) {
392            final int year = mCurrentDate.get(Calendar.YEAR);
393            final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
394            final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
395            mDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
396        }
397
398        for (OnDateChangedListener listener : mListeners) {
399            listener.onDateChanged();
400        }
401
402        mDayPickerView.setDate(getSelectedDay().getTimeInMillis());
403
404        updateDisplay(fromUser);
405
406        if (fromUser) {
407            tryVibrate();
408        }
409    }
410
411    @Override
412    public int getYear() {
413        return mCurrentDate.get(Calendar.YEAR);
414    }
415
416    @Override
417    public int getMonth() {
418        return mCurrentDate.get(Calendar.MONTH);
419    }
420
421    @Override
422    public int getDayOfMonth() {
423        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
424    }
425
426    @Override
427    public void setMinDate(long minDate) {
428        mTempDate.setTimeInMillis(minDate);
429        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
430                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
431            return;
432        }
433        if (mCurrentDate.before(mTempDate)) {
434            mCurrentDate.setTimeInMillis(minDate);
435            onDateChanged(false);
436        }
437        mMinDate.setTimeInMillis(minDate);
438        mDayPickerView.setMinDate(minDate);
439        mYearPickerView.setRange(mMinDate, mMaxDate);
440    }
441
442    @Override
443    public Calendar getMinDate() {
444        return mMinDate;
445    }
446
447    @Override
448    public void setMaxDate(long maxDate) {
449        mTempDate.setTimeInMillis(maxDate);
450        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
451                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
452            return;
453        }
454        if (mCurrentDate.after(mTempDate)) {
455            mCurrentDate.setTimeInMillis(maxDate);
456            onDateChanged(false);
457        }
458        mMaxDate.setTimeInMillis(maxDate);
459        mDayPickerView.setMaxDate(maxDate);
460        mYearPickerView.setRange(mMinDate, mMaxDate);
461    }
462
463    @Override
464    public Calendar getMaxDate() {
465        return mMaxDate;
466    }
467
468    @Override
469    public void setFirstDayOfWeek(int firstDayOfWeek) {
470        mFirstDayOfWeek = firstDayOfWeek;
471
472        mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
473    }
474
475    @Override
476    public int getFirstDayOfWeek() {
477        if (mFirstDayOfWeek != USE_LOCALE) {
478            return mFirstDayOfWeek;
479        }
480        return mCurrentDate.getFirstDayOfWeek();
481    }
482
483    @Override
484    public void setEnabled(boolean enabled) {
485        mMonthAndDayLayout.setEnabled(enabled);
486        mHeaderYearTextView.setEnabled(enabled);
487        mAnimator.setEnabled(enabled);
488        mIsEnabled = enabled;
489    }
490
491    @Override
492    public boolean isEnabled() {
493        return mIsEnabled;
494    }
495
496    @Override
497    public CalendarView getCalendarView() {
498        throw new UnsupportedOperationException(
499                "CalendarView does not exists for the new DatePicker");
500    }
501
502    @Override
503    public void setCalendarViewShown(boolean shown) {
504        // No-op for compatibility with the old DatePicker.
505    }
506
507    @Override
508    public boolean getCalendarViewShown() {
509        return false;
510    }
511
512    @Override
513    public void setSpinnersShown(boolean shown) {
514        // No-op for compatibility with the old DatePicker.
515    }
516
517    @Override
518    public boolean getSpinnersShown() {
519        return false;
520    }
521
522    @Override
523    public void onConfigurationChanged(Configuration newConfig) {
524        mYearFormat = new SimpleDateFormat("y", newConfig.locale);
525        mDayFormat = new SimpleDateFormat("d", newConfig.locale);
526    }
527
528    @Override
529    public Parcelable onSaveInstanceState(Parcelable superState) {
530        final int year = mCurrentDate.get(Calendar.YEAR);
531        final int month = mCurrentDate.get(Calendar.MONTH);
532        final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
533
534        int listPosition = -1;
535        int listPositionOffset = -1;
536
537        if (mCurrentView == MONTH_AND_DAY_VIEW) {
538            listPosition = mDayPickerView.getMostVisiblePosition();
539        } else if (mCurrentView == YEAR_VIEW) {
540            listPosition = mYearPickerView.getFirstVisiblePosition();
541            listPositionOffset = mYearPickerView.getFirstPositionOffset();
542        }
543
544        return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
545                mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
546    }
547
548    @Override
549    public void onRestoreInstanceState(Parcelable state) {
550        SavedState ss = (SavedState) state;
551
552        mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
553        mCurrentView = ss.getCurrentView();
554        mMinDate.setTimeInMillis(ss.getMinDate());
555        mMaxDate.setTimeInMillis(ss.getMaxDate());
556
557        updateDisplay(false);
558        setCurrentView(mCurrentView);
559
560        final int listPosition = ss.getListPosition();
561        if (listPosition != -1) {
562            if (mCurrentView == MONTH_AND_DAY_VIEW) {
563                mDayPickerView.postSetSelection(listPosition);
564            } else if (mCurrentView == YEAR_VIEW) {
565                mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset());
566            }
567        }
568    }
569
570    @Override
571    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
572        onPopulateAccessibilityEvent(event);
573        return true;
574    }
575
576    @Override
577    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
578        event.getText().add(mCurrentDate.getTime().toString());
579    }
580
581    @Override
582    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
583        event.setClassName(DatePicker.class.getName());
584    }
585
586    @Override
587    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
588        info.setClassName(DatePicker.class.getName());
589    }
590
591    @Override
592    public void onYearSelected(int year) {
593        adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year);
594        mCurrentDate.set(Calendar.YEAR, year);
595        onDateChanged(true);
596
597        // Auto-advance to month and day view.
598        setCurrentView(MONTH_AND_DAY_VIEW);
599    }
600
601    // If the newly selected month / year does not contain the currently selected day number,
602    // change the selected day number to the last day of the selected month or year.
603    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
604    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
605    private void adjustDayInMonthIfNeeded(int month, int year) {
606        int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
607        int daysInMonth = getDaysInMonth(month, year);
608        if (day > daysInMonth) {
609            mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
610        }
611    }
612
613    public static int getDaysInMonth(int month, int year) {
614        switch (month) {
615            case Calendar.JANUARY:
616            case Calendar.MARCH:
617            case Calendar.MAY:
618            case Calendar.JULY:
619            case Calendar.AUGUST:
620            case Calendar.OCTOBER:
621            case Calendar.DECEMBER:
622                return 31;
623            case Calendar.APRIL:
624            case Calendar.JUNE:
625            case Calendar.SEPTEMBER:
626            case Calendar.NOVEMBER:
627                return 30;
628            case Calendar.FEBRUARY:
629                return (year % 4 == 0) ? 29 : 28;
630            default:
631                throw new IllegalArgumentException("Invalid Month");
632        }
633    }
634
635    @Override
636    public void registerOnDateChangedListener(OnDateChangedListener listener) {
637        mListeners.add(listener);
638    }
639
640    @Override
641    public Calendar getSelectedDay() {
642        return mCurrentDate;
643    }
644
645    @Override
646    public void tryVibrate() {
647        mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
648    }
649
650    @Override
651    public void onClick(View v) {
652        tryVibrate();
653        if (v.getId() == R.id.date_picker_year) {
654            setCurrentView(YEAR_VIEW);
655        } else if (v.getId() == R.id.date_picker_month_and_day_layout) {
656            setCurrentView(MONTH_AND_DAY_VIEW);
657        }
658    }
659
660    /**
661     * Listener called when the user selects a day in the day picker view.
662     */
663    private final DayPickerView.OnDaySelectedListener
664            mOnDaySelectedListener = new DayPickerView.OnDaySelectedListener() {
665        @Override
666        public void onDaySelected(DayPickerView view, Calendar day) {
667            mCurrentDate.setTimeInMillis(day.getTimeInMillis());
668            onDateChanged(true);
669        }
670    };
671
672    /**
673     * Class for managing state storing/restoring.
674     */
675    private static class SavedState extends View.BaseSavedState {
676
677        private final int mSelectedYear;
678        private final int mSelectedMonth;
679        private final int mSelectedDay;
680        private final long mMinDate;
681        private final long mMaxDate;
682        private final int mCurrentView;
683        private final int mListPosition;
684        private final int mListPositionOffset;
685
686        /**
687         * Constructor called from {@link DatePicker#onSaveInstanceState()}
688         */
689        private SavedState(Parcelable superState, int year, int month, int day,
690                long minDate, long maxDate, int currentView, int listPosition,
691                int listPositionOffset) {
692            super(superState);
693            mSelectedYear = year;
694            mSelectedMonth = month;
695            mSelectedDay = day;
696            mMinDate = minDate;
697            mMaxDate = maxDate;
698            mCurrentView = currentView;
699            mListPosition = listPosition;
700            mListPositionOffset = listPositionOffset;
701        }
702
703        /**
704         * Constructor called from {@link #CREATOR}
705         */
706        private SavedState(Parcel in) {
707            super(in);
708            mSelectedYear = in.readInt();
709            mSelectedMonth = in.readInt();
710            mSelectedDay = in.readInt();
711            mMinDate = in.readLong();
712            mMaxDate = in.readLong();
713            mCurrentView = in.readInt();
714            mListPosition = in.readInt();
715            mListPositionOffset = in.readInt();
716        }
717
718        @Override
719        public void writeToParcel(Parcel dest, int flags) {
720            super.writeToParcel(dest, flags);
721            dest.writeInt(mSelectedYear);
722            dest.writeInt(mSelectedMonth);
723            dest.writeInt(mSelectedDay);
724            dest.writeLong(mMinDate);
725            dest.writeLong(mMaxDate);
726            dest.writeInt(mCurrentView);
727            dest.writeInt(mListPosition);
728            dest.writeInt(mListPositionOffset);
729        }
730
731        public int getSelectedDay() {
732            return mSelectedDay;
733        }
734
735        public int getSelectedMonth() {
736            return mSelectedMonth;
737        }
738
739        public int getSelectedYear() {
740            return mSelectedYear;
741        }
742
743        public long getMinDate() {
744            return mMinDate;
745        }
746
747        public long getMaxDate() {
748            return mMaxDate;
749        }
750
751        public int getCurrentView() {
752            return mCurrentView;
753        }
754
755        public int getListPosition() {
756            return mListPosition;
757        }
758
759        public int getListPositionOffset() {
760            return mListPositionOffset;
761        }
762
763        @SuppressWarnings("all")
764        // suppress unused and hiding
765        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
766
767            public SavedState createFromParcel(Parcel in) {
768                return new SavedState(in);
769            }
770
771            public SavedState[] newArray(int size) {
772                return new SavedState[size];
773            }
774        };
775    }
776}
777