Home | History | Annotate | Download | only in widget
      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 
     17 package android.widget;
     18 
     19 import com.android.internal.R;
     20 
     21 import android.annotation.Nullable;
     22 import android.content.Context;
     23 import android.content.res.ColorStateList;
     24 import android.content.res.Configuration;
     25 import android.content.res.Resources;
     26 import android.content.res.TypedArray;
     27 import android.icu.text.DisplayContext;
     28 import android.icu.text.SimpleDateFormat;
     29 import android.icu.util.Calendar;
     30 import android.os.Parcelable;
     31 import android.text.format.DateFormat;
     32 import android.text.format.DateUtils;
     33 import android.util.AttributeSet;
     34 import android.util.StateSet;
     35 import android.view.HapticFeedbackConstants;
     36 import android.view.LayoutInflater;
     37 import android.view.View;
     38 import android.view.View.OnClickListener;
     39 import android.view.ViewGroup;
     40 import android.view.accessibility.AccessibilityEvent;
     41 import android.widget.DayPickerView.OnDaySelectedListener;
     42 import android.widget.YearPickerView.OnYearSelectedListener;
     43 
     44 import java.util.Locale;
     45 
     46 /**
     47  * A delegate for picking up a date (day / month / year).
     48  */
     49 class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
     50     private static final int USE_LOCALE = 0;
     51 
     52     private static final int UNINITIALIZED = -1;
     53     private static final int VIEW_MONTH_DAY = 0;
     54     private static final int VIEW_YEAR = 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[] ATTRS_TEXT_COLOR = new int[] {
     62             com.android.internal.R.attr.textColor};
     63     private static final int[] ATTRS_DISABLED_ALPHA = new int[] {
     64             com.android.internal.R.attr.disabledAlpha};
     65 
     66     private SimpleDateFormat mYearFormat;
     67     private SimpleDateFormat mMonthDayFormat;
     68 
     69     // Top-level container.
     70     private ViewGroup mContainer;
     71 
     72     // Header views.
     73     private TextView mHeaderYear;
     74     private TextView mHeaderMonthDay;
     75 
     76     // Picker views.
     77     private ViewAnimator mAnimator;
     78     private DayPickerView mDayPickerView;
     79     private YearPickerView mYearPickerView;
     80 
     81     // Accessibility strings.
     82     private String mSelectDay;
     83     private String mSelectYear;
     84 
     85     private DatePicker.OnDateChangedListener mDateChangedListener;
     86 
     87     private int mCurrentView = UNINITIALIZED;
     88 
     89     private final Calendar mCurrentDate;
     90     private final Calendar mTempDate;
     91     private final Calendar mMinDate;
     92     private final Calendar mMaxDate;
     93 
     94     private int mFirstDayOfWeek = USE_LOCALE;
     95 
     96     public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
     97             int defStyleAttr, int defStyleRes) {
     98         super(delegator, context);
     99 
    100         final Locale locale = mCurrentLocale;
    101         mCurrentDate = Calendar.getInstance(locale);
    102         mTempDate = Calendar.getInstance(locale);
    103         mMinDate = Calendar.getInstance(locale);
    104         mMaxDate = Calendar.getInstance(locale);
    105 
    106         mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
    107         mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
    108 
    109         final Resources res = mDelegator.getResources();
    110         final TypedArray a = mContext.obtainStyledAttributes(attrs,
    111                 R.styleable.DatePicker, defStyleAttr, defStyleRes);
    112         final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
    113                 Context.LAYOUT_INFLATER_SERVICE);
    114         final int layoutResourceId = a.getResourceId(
    115                 R.styleable.DatePicker_internalLayout, R.layout.date_picker_material);
    116 
    117         // Set up and attach container.
    118         mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false);
    119         mDelegator.addView(mContainer);
    120 
    121         // Set up header views.
    122         final ViewGroup header = (ViewGroup) mContainer.findViewById(R.id.date_picker_header);
    123         mHeaderYear = (TextView) header.findViewById(R.id.date_picker_header_year);
    124         mHeaderYear.setOnClickListener(mOnHeaderClickListener);
    125         mHeaderMonthDay = (TextView) header.findViewById(R.id.date_picker_header_date);
    126         mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
    127 
    128         // For the sake of backwards compatibility, attempt to extract the text
    129         // color from the header month text appearance. If it's set, we'll let
    130         // that override the "real" header text color.
    131         ColorStateList headerTextColor = null;
    132 
    133         @SuppressWarnings("deprecation")
    134         final int monthHeaderTextAppearance = a.getResourceId(
    135                 R.styleable.DatePicker_headerMonthTextAppearance, 0);
    136         if (monthHeaderTextAppearance != 0) {
    137             final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
    138                     ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance);
    139             final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
    140             headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
    141             textAppearance.recycle();
    142         }
    143 
    144         if (headerTextColor == null) {
    145             headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor);
    146         }
    147 
    148         if (headerTextColor != null) {
    149             mHeaderYear.setTextColor(headerTextColor);
    150             mHeaderMonthDay.setTextColor(headerTextColor);
    151         }
    152 
    153         // Set up header background, if available.
    154         if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) {
    155             header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
    156         }
    157 
    158         a.recycle();
    159 
    160         // Set up picker container.
    161         mAnimator = (ViewAnimator) mContainer.findViewById(R.id.animator);
    162 
    163         // Set up day picker view.
    164         mDayPickerView = (DayPickerView) mAnimator.findViewById(R.id.date_picker_day_picker);
    165         mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
    166         mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
    167         mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
    168         mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
    169         mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
    170 
    171         // Set up year picker view.
    172         mYearPickerView = (YearPickerView) mAnimator.findViewById(R.id.date_picker_year_picker);
    173         mYearPickerView.setRange(mMinDate, mMaxDate);
    174         mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
    175         mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
    176 
    177         // Set up content descriptions.
    178         mSelectDay = res.getString(R.string.select_day);
    179         mSelectYear = res.getString(R.string.select_year);
    180 
    181         // Initialize for current locale. This also initializes the date, so no
    182         // need to call onDateChanged.
    183         onLocaleChanged(mCurrentLocale);
    184 
    185         setCurrentView(VIEW_MONTH_DAY);
    186     }
    187 
    188     /**
    189      * The legacy text color might have been poorly defined. Ensures that it
    190      * has an appropriate activated state, using the selected state if one
    191      * exists or modifying the default text color otherwise.
    192      *
    193      * @param color a legacy text color, or {@code null}
    194      * @return a color state list with an appropriate activated state, or
    195      *         {@code null} if a valid activated state could not be generated
    196      */
    197     @Nullable
    198     private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
    199         if (color == null || color.hasState(R.attr.state_activated)) {
    200             return color;
    201         }
    202 
    203         final int activatedColor;
    204         final int defaultColor;
    205         if (color.hasState(R.attr.state_selected)) {
    206             activatedColor = color.getColorForState(StateSet.get(
    207                     StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
    208             defaultColor = color.getColorForState(StateSet.get(
    209                     StateSet.VIEW_STATE_ENABLED), 0);
    210         } else {
    211             activatedColor = color.getDefaultColor();
    212 
    213             // Generate a non-activated color using the disabled alpha.
    214             final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
    215             final float disabledAlpha = ta.getFloat(0, 0.30f);
    216             defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
    217         }
    218 
    219         if (activatedColor == 0 || defaultColor == 0) {
    220             // We somehow failed to obtain the colors.
    221             return null;
    222         }
    223 
    224         final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
    225         final int[] colors = new int[] { activatedColor, defaultColor };
    226         return new ColorStateList(stateSet, colors);
    227     }
    228 
    229     private int multiplyAlphaComponent(int color, float alphaMod) {
    230         final int srcRgb = color & 0xFFFFFF;
    231         final int srcAlpha = (color >> 24) & 0xFF;
    232         final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
    233         return srcRgb | (dstAlpha << 24);
    234     }
    235 
    236     /**
    237      * Listener called when the user selects a day in the day picker view.
    238      */
    239     private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
    240         @Override
    241         public void onDaySelected(DayPickerView view, Calendar day) {
    242             mCurrentDate.setTimeInMillis(day.getTimeInMillis());
    243             onDateChanged(true, true);
    244         }
    245     };
    246 
    247     /**
    248      * Listener called when the user selects a year in the year picker view.
    249      */
    250     private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() {
    251         @Override
    252         public void onYearChanged(YearPickerView view, int year) {
    253             // If the newly selected month / year does not contain the
    254             // currently selected day number, change the selected day number
    255             // to the last day of the selected month or year.
    256             // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
    257             // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
    258             final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
    259             final int month = mCurrentDate.get(Calendar.MONTH);
    260             final int daysInMonth = getDaysInMonth(month, year);
    261             if (day > daysInMonth) {
    262                 mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
    263             }
    264 
    265             mCurrentDate.set(Calendar.YEAR, year);
    266             onDateChanged(true, true);
    267 
    268             // Automatically switch to day picker.
    269             setCurrentView(VIEW_MONTH_DAY);
    270 
    271             // Switch focus back to the year text.
    272             mHeaderYear.requestFocus();
    273         }
    274     };
    275 
    276     /**
    277      * Listener called when the user clicks on a header item.
    278      */
    279     private final OnClickListener mOnHeaderClickListener = new OnClickListener() {
    280         @Override
    281         public void onClick(View v) {
    282             tryVibrate();
    283 
    284             switch (v.getId()) {
    285                 case R.id.date_picker_header_year:
    286                     setCurrentView(VIEW_YEAR);
    287                     break;
    288                 case R.id.date_picker_header_date:
    289                     setCurrentView(VIEW_MONTH_DAY);
    290                     break;
    291             }
    292         }
    293     };
    294 
    295     @Override
    296     protected void onLocaleChanged(Locale locale) {
    297         final TextView headerYear = mHeaderYear;
    298         if (headerYear == null) {
    299             // Abort, we haven't initialized yet. This method will get called
    300             // again later after everything has been set up.
    301             return;
    302         }
    303 
    304         // Update the date formatter.
    305         final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd");
    306         mMonthDayFormat = new SimpleDateFormat(datePattern, locale);
    307         mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
    308         mYearFormat = new SimpleDateFormat("y", locale);
    309 
    310         // Update the header text.
    311         onCurrentDateChanged(false);
    312     }
    313 
    314     private void onCurrentDateChanged(boolean announce) {
    315         if (mHeaderYear == null) {
    316             // Abort, we haven't initialized yet. This method will get called
    317             // again later after everything has been set up.
    318             return;
    319         }
    320 
    321         final String year = mYearFormat.format(mCurrentDate.getTime());
    322         mHeaderYear.setText(year);
    323 
    324         final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
    325         mHeaderMonthDay.setText(monthDay);
    326 
    327         // TODO: This should use live regions.
    328         if (announce) {
    329             final long millis = mCurrentDate.getTimeInMillis();
    330             final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
    331             final String fullDateText = DateUtils.formatDateTime(mContext, millis, flags);
    332             mAnimator.announceForAccessibility(fullDateText);
    333         }
    334     }
    335 
    336     private void setCurrentView(final int viewIndex) {
    337         switch (viewIndex) {
    338             case VIEW_MONTH_DAY:
    339                 mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
    340 
    341                 if (mCurrentView != viewIndex) {
    342                     mHeaderMonthDay.setActivated(true);
    343                     mHeaderYear.setActivated(false);
    344                     mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
    345                     mCurrentView = viewIndex;
    346                 }
    347 
    348                 mAnimator.announceForAccessibility(mSelectDay);
    349                 break;
    350             case VIEW_YEAR:
    351                 final int year = mCurrentDate.get(Calendar.YEAR);
    352                 mYearPickerView.setYear(year);
    353                 mYearPickerView.post(new Runnable() {
    354                     @Override
    355                     public void run() {
    356                         mYearPickerView.requestFocus();
    357                         final View selected = mYearPickerView.getSelectedView();
    358                         if (selected != null) {
    359                             selected.requestFocus();
    360                         }
    361                     }
    362                 });
    363 
    364                 if (mCurrentView != viewIndex) {
    365                     mHeaderMonthDay.setActivated(false);
    366                     mHeaderYear.setActivated(true);
    367                     mAnimator.setDisplayedChild(VIEW_YEAR);
    368                     mCurrentView = viewIndex;
    369                 }
    370 
    371                 mAnimator.announceForAccessibility(mSelectYear);
    372                 break;
    373         }
    374     }
    375 
    376     @Override
    377     public void init(int year, int monthOfYear, int dayOfMonth,
    378             DatePicker.OnDateChangedListener callBack) {
    379         mCurrentDate.set(Calendar.YEAR, year);
    380         mCurrentDate.set(Calendar.MONTH, monthOfYear);
    381         mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
    382 
    383         onDateChanged(false, false);
    384 
    385         mDateChangedListener = callBack;
    386     }
    387 
    388     @Override
    389     public void updateDate(int year, int month, int dayOfMonth) {
    390         mCurrentDate.set(Calendar.YEAR, year);
    391         mCurrentDate.set(Calendar.MONTH, month);
    392         mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
    393 
    394         onDateChanged(false, true);
    395     }
    396 
    397     private void onDateChanged(boolean fromUser, boolean callbackToClient) {
    398         final int year = mCurrentDate.get(Calendar.YEAR);
    399 
    400         if (callbackToClient && mDateChangedListener != null) {
    401             final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
    402             final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
    403             mDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
    404         }
    405 
    406         mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
    407         mYearPickerView.setYear(year);
    408 
    409         onCurrentDateChanged(fromUser);
    410 
    411         if (fromUser) {
    412             tryVibrate();
    413         }
    414     }
    415 
    416     @Override
    417     public int getYear() {
    418         return mCurrentDate.get(Calendar.YEAR);
    419     }
    420 
    421     @Override
    422     public int getMonth() {
    423         return mCurrentDate.get(Calendar.MONTH);
    424     }
    425 
    426     @Override
    427     public int getDayOfMonth() {
    428         return mCurrentDate.get(Calendar.DAY_OF_MONTH);
    429     }
    430 
    431     @Override
    432     public void setMinDate(long minDate) {
    433         mTempDate.setTimeInMillis(minDate);
    434         if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
    435                 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
    436             // Same day, no-op.
    437             return;
    438         }
    439         if (mCurrentDate.before(mTempDate)) {
    440             mCurrentDate.setTimeInMillis(minDate);
    441             onDateChanged(false, true);
    442         }
    443         mMinDate.setTimeInMillis(minDate);
    444         mDayPickerView.setMinDate(minDate);
    445         mYearPickerView.setRange(mMinDate, mMaxDate);
    446     }
    447 
    448     @Override
    449     public Calendar getMinDate() {
    450         return mMinDate;
    451     }
    452 
    453     @Override
    454     public void setMaxDate(long maxDate) {
    455         mTempDate.setTimeInMillis(maxDate);
    456         if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
    457                 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
    458             // Same day, no-op.
    459             return;
    460         }
    461         if (mCurrentDate.after(mTempDate)) {
    462             mCurrentDate.setTimeInMillis(maxDate);
    463             onDateChanged(false, true);
    464         }
    465         mMaxDate.setTimeInMillis(maxDate);
    466         mDayPickerView.setMaxDate(maxDate);
    467         mYearPickerView.setRange(mMinDate, mMaxDate);
    468     }
    469 
    470     @Override
    471     public Calendar getMaxDate() {
    472         return mMaxDate;
    473     }
    474 
    475     @Override
    476     public void setFirstDayOfWeek(int firstDayOfWeek) {
    477         mFirstDayOfWeek = firstDayOfWeek;
    478 
    479         mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
    480     }
    481 
    482     @Override
    483     public int getFirstDayOfWeek() {
    484         if (mFirstDayOfWeek != USE_LOCALE) {
    485             return mFirstDayOfWeek;
    486         }
    487         return mCurrentDate.getFirstDayOfWeek();
    488     }
    489 
    490     @Override
    491     public void setEnabled(boolean enabled) {
    492         mContainer.setEnabled(enabled);
    493         mDayPickerView.setEnabled(enabled);
    494         mYearPickerView.setEnabled(enabled);
    495         mHeaderYear.setEnabled(enabled);
    496         mHeaderMonthDay.setEnabled(enabled);
    497     }
    498 
    499     @Override
    500     public boolean isEnabled() {
    501         return mContainer.isEnabled();
    502     }
    503 
    504     @Override
    505     public CalendarView getCalendarView() {
    506         throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker");
    507     }
    508 
    509     @Override
    510     public void setCalendarViewShown(boolean shown) {
    511         // No-op for compatibility with the old DatePicker.
    512     }
    513 
    514     @Override
    515     public boolean getCalendarViewShown() {
    516         return false;
    517     }
    518 
    519     @Override
    520     public void setSpinnersShown(boolean shown) {
    521         // No-op for compatibility with the old DatePicker.
    522     }
    523 
    524     @Override
    525     public boolean getSpinnersShown() {
    526         return false;
    527     }
    528 
    529     @Override
    530     public void onConfigurationChanged(Configuration newConfig) {
    531         setCurrentLocale(newConfig.locale);
    532     }
    533 
    534     @Override
    535     public Parcelable onSaveInstanceState(Parcelable superState) {
    536         final int year = mCurrentDate.get(Calendar.YEAR);
    537         final int month = mCurrentDate.get(Calendar.MONTH);
    538         final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
    539 
    540         int listPosition = -1;
    541         int listPositionOffset = -1;
    542 
    543         if (mCurrentView == VIEW_MONTH_DAY) {
    544             listPosition = mDayPickerView.getMostVisiblePosition();
    545         } else if (mCurrentView == VIEW_YEAR) {
    546             listPosition = mYearPickerView.getFirstVisiblePosition();
    547             listPositionOffset = mYearPickerView.getFirstPositionOffset();
    548         }
    549 
    550         return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
    551                 mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
    552     }
    553 
    554     @Override
    555     public void onRestoreInstanceState(Parcelable state) {
    556         if (state instanceof SavedState) {
    557             final SavedState ss = (SavedState) state;
    558 
    559             // TODO: Move instance state into DayPickerView, YearPickerView.
    560             mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
    561             mMinDate.setTimeInMillis(ss.getMinDate());
    562             mMaxDate.setTimeInMillis(ss.getMaxDate());
    563 
    564             onCurrentDateChanged(false);
    565 
    566             final int currentView = ss.getCurrentView();
    567             setCurrentView(currentView);
    568 
    569             final int listPosition = ss.getListPosition();
    570             if (listPosition != -1) {
    571                 if (currentView == VIEW_MONTH_DAY) {
    572                     mDayPickerView.setPosition(listPosition);
    573                 } else if (currentView == VIEW_YEAR) {
    574                     final int listPositionOffset = ss.getListPositionOffset();
    575                     mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset);
    576                 }
    577             }
    578         }
    579     }
    580 
    581     @Override
    582     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    583         onPopulateAccessibilityEvent(event);
    584         return true;
    585     }
    586 
    587     @Override
    588     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    589         event.getText().add(mCurrentDate.getTime().toString());
    590     }
    591 
    592     public CharSequence getAccessibilityClassName() {
    593         return DatePicker.class.getName();
    594     }
    595 
    596     public static int getDaysInMonth(int month, int year) {
    597         switch (month) {
    598             case Calendar.JANUARY:
    599             case Calendar.MARCH:
    600             case Calendar.MAY:
    601             case Calendar.JULY:
    602             case Calendar.AUGUST:
    603             case Calendar.OCTOBER:
    604             case Calendar.DECEMBER:
    605                 return 31;
    606             case Calendar.APRIL:
    607             case Calendar.JUNE:
    608             case Calendar.SEPTEMBER:
    609             case Calendar.NOVEMBER:
    610                 return 30;
    611             case Calendar.FEBRUARY:
    612                 return (year % 4 == 0) ? 29 : 28;
    613             default:
    614                 throw new IllegalArgumentException("Invalid Month");
    615         }
    616     }
    617 
    618     private void tryVibrate() {
    619         mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
    620     }
    621 }
    622