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 android.annotation.Nullable;
     20 import android.content.Context;
     21 import android.content.res.ColorStateList;
     22 import android.content.res.Configuration;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.icu.text.DisplayContext;
     26 import android.icu.text.SimpleDateFormat;
     27 import android.icu.util.Calendar;
     28 import android.os.Parcelable;
     29 import android.text.format.DateFormat;
     30 import android.util.AttributeSet;
     31 import android.util.StateSet;
     32 import android.view.HapticFeedbackConstants;
     33 import android.view.LayoutInflater;
     34 import android.view.View;
     35 import android.view.View.OnClickListener;
     36 import android.view.ViewGroup;
     37 import android.view.accessibility.AccessibilityEvent;
     38 import android.widget.DayPickerView.OnDaySelectedListener;
     39 import android.widget.YearPickerView.OnYearSelectedListener;
     40 
     41 import com.android.internal.R;
     42 
     43 import java.util.Locale;
     44 
     45 /**
     46  * A delegate for picking up a date (day / month / year).
     47  */
     48 class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
     49     private static final int USE_LOCALE = 0;
     50 
     51     private static final int UNINITIALIZED = -1;
     52     private static final int VIEW_MONTH_DAY = 0;
     53     private static final int VIEW_YEAR = 1;
     54 
     55     private static final int DEFAULT_START_YEAR = 1900;
     56     private static final int DEFAULT_END_YEAR = 2100;
     57 
     58     private static final int ANIMATION_DURATION = 300;
     59 
     60     private static final int[] ATTRS_TEXT_COLOR = new int[] {
     61             com.android.internal.R.attr.textColor};
     62     private static final int[] ATTRS_DISABLED_ALPHA = new int[] {
     63             com.android.internal.R.attr.disabledAlpha};
     64 
     65     private SimpleDateFormat mYearFormat;
     66     private SimpleDateFormat mMonthDayFormat;
     67 
     68     // Top-level container.
     69     private ViewGroup mContainer;
     70 
     71     // Header views.
     72     private TextView mHeaderYear;
     73     private TextView mHeaderMonthDay;
     74 
     75     // Picker views.
     76     private ViewAnimator mAnimator;
     77     private DayPickerView mDayPickerView;
     78     private YearPickerView mYearPickerView;
     79 
     80     // Accessibility strings.
     81     private String mSelectDay;
     82     private String mSelectYear;
     83 
     84     private int mCurrentView = UNINITIALIZED;
     85 
     86     private final Calendar mTempDate;
     87     private final Calendar mMinDate;
     88     private final Calendar mMaxDate;
     89 
     90     private int mFirstDayOfWeek = USE_LOCALE;
     91 
     92     public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
     93             int defStyleAttr, int defStyleRes) {
     94         super(delegator, context);
     95 
     96         final Locale locale = mCurrentLocale;
     97         mCurrentDate = Calendar.getInstance(locale);
     98         mTempDate = Calendar.getInstance(locale);
     99         mMinDate = Calendar.getInstance(locale);
    100         mMaxDate = Calendar.getInstance(locale);
    101 
    102         mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
    103         mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
    104 
    105         final Resources res = mDelegator.getResources();
    106         final TypedArray a = mContext.obtainStyledAttributes(attrs,
    107                 R.styleable.DatePicker, defStyleAttr, defStyleRes);
    108         final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
    109                 Context.LAYOUT_INFLATER_SERVICE);
    110         final int layoutResourceId = a.getResourceId(
    111                 R.styleable.DatePicker_internalLayout, R.layout.date_picker_material);
    112 
    113         // Set up and attach container.
    114         mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false);
    115         mContainer.setSaveFromParentEnabled(false);
    116         mDelegator.addView(mContainer);
    117 
    118         // Set up header views.
    119         final ViewGroup header = mContainer.findViewById(R.id.date_picker_header);
    120         mHeaderYear = header.findViewById(R.id.date_picker_header_year);
    121         mHeaderYear.setOnClickListener(mOnHeaderClickListener);
    122         mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date);
    123         mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
    124 
    125         // For the sake of backwards compatibility, attempt to extract the text
    126         // color from the header month text appearance. If it's set, we'll let
    127         // that override the "real" header text color.
    128         ColorStateList headerTextColor = null;
    129 
    130         @SuppressWarnings("deprecation")
    131         final int monthHeaderTextAppearance = a.getResourceId(
    132                 R.styleable.DatePicker_headerMonthTextAppearance, 0);
    133         if (monthHeaderTextAppearance != 0) {
    134             final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
    135                     ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance);
    136             final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
    137             headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
    138             textAppearance.recycle();
    139         }
    140 
    141         if (headerTextColor == null) {
    142             headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor);
    143         }
    144 
    145         if (headerTextColor != null) {
    146             mHeaderYear.setTextColor(headerTextColor);
    147             mHeaderMonthDay.setTextColor(headerTextColor);
    148         }
    149 
    150         // Set up header background, if available.
    151         if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) {
    152             header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
    153         }
    154 
    155         a.recycle();
    156 
    157         // Set up picker container.
    158         mAnimator = mContainer.findViewById(R.id.animator);
    159 
    160         // Set up day picker view.
    161         mDayPickerView = mAnimator.findViewById(R.id.date_picker_day_picker);
    162         mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
    163         mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
    164         mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
    165         mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
    166         mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
    167 
    168         // Set up year picker view.
    169         mYearPickerView = mAnimator.findViewById(R.id.date_picker_year_picker);
    170         mYearPickerView.setRange(mMinDate, mMaxDate);
    171         mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
    172         mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
    173 
    174         // Set up content descriptions.
    175         mSelectDay = res.getString(R.string.select_day);
    176         mSelectYear = res.getString(R.string.select_year);
    177 
    178         // Initialize for current locale. This also initializes the date, so no
    179         // need to call onDateChanged.
    180         onLocaleChanged(mCurrentLocale);
    181 
    182         setCurrentView(VIEW_MONTH_DAY);
    183     }
    184 
    185     /**
    186      * The legacy text color might have been poorly defined. Ensures that it
    187      * has an appropriate activated state, using the selected state if one
    188      * exists or modifying the default text color otherwise.
    189      *
    190      * @param color a legacy text color, or {@code null}
    191      * @return a color state list with an appropriate activated state, or
    192      *         {@code null} if a valid activated state could not be generated
    193      */
    194     @Nullable
    195     private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
    196         if (color == null || color.hasState(R.attr.state_activated)) {
    197             return color;
    198         }
    199 
    200         final int activatedColor;
    201         final int defaultColor;
    202         if (color.hasState(R.attr.state_selected)) {
    203             activatedColor = color.getColorForState(StateSet.get(
    204                     StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
    205             defaultColor = color.getColorForState(StateSet.get(
    206                     StateSet.VIEW_STATE_ENABLED), 0);
    207         } else {
    208             activatedColor = color.getDefaultColor();
    209 
    210             // Generate a non-activated color using the disabled alpha.
    211             final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
    212             final float disabledAlpha = ta.getFloat(0, 0.30f);
    213             defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
    214         }
    215 
    216         if (activatedColor == 0 || defaultColor == 0) {
    217             // We somehow failed to obtain the colors.
    218             return null;
    219         }
    220 
    221         final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
    222         final int[] colors = new int[] { activatedColor, defaultColor };
    223         return new ColorStateList(stateSet, colors);
    224     }
    225 
    226     private int multiplyAlphaComponent(int color, float alphaMod) {
    227         final int srcRgb = color & 0xFFFFFF;
    228         final int srcAlpha = (color >> 24) & 0xFF;
    229         final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
    230         return srcRgb | (dstAlpha << 24);
    231     }
    232 
    233     /**
    234      * Listener called when the user selects a day in the day picker view.
    235      */
    236     private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
    237         @Override
    238         public void onDaySelected(DayPickerView view, Calendar day) {
    239             mCurrentDate.setTimeInMillis(day.getTimeInMillis());
    240             onDateChanged(true, true);
    241         }
    242     };
    243 
    244     /**
    245      * Listener called when the user selects a year in the year picker view.
    246      */
    247     private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() {
    248         @Override
    249         public void onYearChanged(YearPickerView view, int year) {
    250             // If the newly selected month / year does not contain the
    251             // currently selected day number, change the selected day number
    252             // to the last day of the selected month or year.
    253             // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
    254             // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
    255             final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
    256             final int month = mCurrentDate.get(Calendar.MONTH);
    257             final int daysInMonth = getDaysInMonth(month, year);
    258             if (day > daysInMonth) {
    259                 mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
    260             }
    261 
    262             mCurrentDate.set(Calendar.YEAR, year);
    263             onDateChanged(true, true);
    264 
    265             // Automatically switch to day picker.
    266             setCurrentView(VIEW_MONTH_DAY);
    267 
    268             // Switch focus back to the year text.
    269             mHeaderYear.requestFocus();
    270         }
    271     };
    272 
    273     /**
    274      * Listener called when the user clicks on a header item.
    275      */
    276     private final OnClickListener mOnHeaderClickListener = new OnClickListener() {
    277         @Override
    278         public void onClick(View v) {
    279             tryVibrate();
    280 
    281             switch (v.getId()) {
    282                 case R.id.date_picker_header_year:
    283                     setCurrentView(VIEW_YEAR);
    284                     break;
    285                 case R.id.date_picker_header_date:
    286                     setCurrentView(VIEW_MONTH_DAY);
    287                     break;
    288             }
    289         }
    290     };
    291 
    292     @Override
    293     protected void onLocaleChanged(Locale locale) {
    294         final TextView headerYear = mHeaderYear;
    295         if (headerYear == null) {
    296             // Abort, we haven't initialized yet. This method will get called
    297             // again later after everything has been set up.
    298             return;
    299         }
    300 
    301         // Update the date formatter.
    302         final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd");
    303         mMonthDayFormat = new SimpleDateFormat(datePattern, locale);
    304         mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
    305         mYearFormat = new SimpleDateFormat("y", locale);
    306 
    307         // Update the header text.
    308         onCurrentDateChanged(false);
    309     }
    310 
    311     private void onCurrentDateChanged(boolean announce) {
    312         if (mHeaderYear == null) {
    313             // Abort, we haven't initialized yet. This method will get called
    314             // again later after everything has been set up.
    315             return;
    316         }
    317 
    318         final String year = mYearFormat.format(mCurrentDate.getTime());
    319         mHeaderYear.setText(year);
    320 
    321         final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
    322         mHeaderMonthDay.setText(monthDay);
    323 
    324         // TODO: This should use live regions.
    325         if (announce) {
    326             mAnimator.announceForAccessibility(getFormattedCurrentDate());
    327         }
    328     }
    329 
    330     private void setCurrentView(final int viewIndex) {
    331         switch (viewIndex) {
    332             case VIEW_MONTH_DAY:
    333                 mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
    334 
    335                 if (mCurrentView != viewIndex) {
    336                     mHeaderMonthDay.setActivated(true);
    337                     mHeaderYear.setActivated(false);
    338                     mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
    339                     mCurrentView = viewIndex;
    340                 }
    341 
    342                 mAnimator.announceForAccessibility(mSelectDay);
    343                 break;
    344             case VIEW_YEAR:
    345                 final int year = mCurrentDate.get(Calendar.YEAR);
    346                 mYearPickerView.setYear(year);
    347                 mYearPickerView.post(new Runnable() {
    348                     @Override
    349                     public void run() {
    350                         mYearPickerView.requestFocus();
    351                         final View selected = mYearPickerView.getSelectedView();
    352                         if (selected != null) {
    353                             selected.requestFocus();
    354                         }
    355                     }
    356                 });
    357 
    358                 if (mCurrentView != viewIndex) {
    359                     mHeaderMonthDay.setActivated(false);
    360                     mHeaderYear.setActivated(true);
    361                     mAnimator.setDisplayedChild(VIEW_YEAR);
    362                     mCurrentView = viewIndex;
    363                 }
    364 
    365                 mAnimator.announceForAccessibility(mSelectYear);
    366                 break;
    367         }
    368     }
    369 
    370     @Override
    371     public void init(int year, int monthOfYear, int dayOfMonth,
    372             DatePicker.OnDateChangedListener callBack) {
    373         mCurrentDate.set(Calendar.YEAR, year);
    374         mCurrentDate.set(Calendar.MONTH, monthOfYear);
    375         mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
    376 
    377         onDateChanged(false, false);
    378 
    379         mOnDateChangedListener = callBack;
    380     }
    381 
    382     @Override
    383     public void updateDate(int year, int month, int dayOfMonth) {
    384         mCurrentDate.set(Calendar.YEAR, year);
    385         mCurrentDate.set(Calendar.MONTH, month);
    386         mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
    387 
    388         onDateChanged(false, true);
    389     }
    390 
    391     private void onDateChanged(boolean fromUser, boolean callbackToClient) {
    392         final int year = mCurrentDate.get(Calendar.YEAR);
    393 
    394         if (callbackToClient
    395                 && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) {
    396             final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
    397             final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
    398             if (mOnDateChangedListener != null) {
    399                 mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
    400             }
    401             if (mAutoFillChangeListener != null) {
    402                 mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
    403             }
    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     public CharSequence getAccessibilityClassName() {
    588         return DatePicker.class.getName();
    589     }
    590 
    591     public static int getDaysInMonth(int month, int year) {
    592         switch (month) {
    593             case Calendar.JANUARY:
    594             case Calendar.MARCH:
    595             case Calendar.MAY:
    596             case Calendar.JULY:
    597             case Calendar.AUGUST:
    598             case Calendar.OCTOBER:
    599             case Calendar.DECEMBER:
    600                 return 31;
    601             case Calendar.APRIL:
    602             case Calendar.JUNE:
    603             case Calendar.SEPTEMBER:
    604             case Calendar.NOVEMBER:
    605                 return 30;
    606             case Calendar.FEBRUARY:
    607                 return (year % 4 == 0) ? 29 : 28;
    608             default:
    609                 throw new IllegalArgumentException("Invalid Month");
    610         }
    611     }
    612 
    613     private void tryVibrate() {
    614         mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
    615     }
    616 }
    617