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