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.view.accessibility.AccessibilityNodeInfo; 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 DateFormat mYearFormat; 66 private DateFormat 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 private int mCurrentView = UNINITIALIZED; 81 82 private final Calendar mTempDate; 83 private final Calendar mMinDate; 84 private final Calendar mMaxDate; 85 86 private int mFirstDayOfWeek = USE_LOCALE; 87 DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)88 public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs, 89 int defStyleAttr, int defStyleRes) { 90 super(delegator, context); 91 92 final Locale locale = mCurrentLocale; 93 mCurrentDate = Calendar.getInstance(locale); 94 mTempDate = Calendar.getInstance(locale); 95 mMinDate = Calendar.getInstance(locale); 96 mMaxDate = Calendar.getInstance(locale); 97 98 mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); 99 mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); 100 101 final Resources res = mDelegator.getResources(); 102 final TypedArray a = mContext.obtainStyledAttributes(attrs, 103 R.styleable.DatePicker, defStyleAttr, defStyleRes); 104 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 105 Context.LAYOUT_INFLATER_SERVICE); 106 final int layoutResourceId = a.getResourceId( 107 R.styleable.DatePicker_internalLayout, R.layout.date_picker_material); 108 109 // Set up and attach container. 110 mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false); 111 mContainer.setSaveFromParentEnabled(false); 112 mDelegator.addView(mContainer); 113 114 // Set up header views. 115 final ViewGroup header = mContainer.findViewById(R.id.date_picker_header); 116 mHeaderYear = header.findViewById(R.id.date_picker_header_year); 117 mHeaderYear.setOnClickListener(mOnHeaderClickListener); 118 mHeaderYear.setAccessibilityDelegate( 119 new ClickActionDelegate(context, R.string.select_year)); 120 mHeaderYear.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 121 122 mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date); 123 mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener); 124 mHeaderMonthDay.setAccessibilityDelegate( 125 new ClickActionDelegate(context, R.string.select_day)); 126 mHeaderMonthDay.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 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 = mContainer.findViewById(R.id.animator); 162 163 // Set up day picker view. 164 mDayPickerView = 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 = 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 // 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 private static class ClickActionDelegate extends View.AccessibilityDelegate { 234 private final AccessibilityNodeInfo.AccessibilityAction mClickAction; 235 ClickActionDelegate(Context context, int resId)236 ClickActionDelegate(Context context, int resId) { 237 mClickAction = new AccessibilityNodeInfo.AccessibilityAction( 238 AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId)); 239 } 240 241 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)242 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 243 super.onInitializeAccessibilityNodeInfo(host, info); 244 245 info.addAction(mClickAction); 246 } 247 } 248 249 /** 250 * Listener called when the user selects a day in the day picker view. 251 */ 252 private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() { 253 @Override 254 public void onDaySelected(DayPickerView view, Calendar day) { 255 mCurrentDate.setTimeInMillis(day.getTimeInMillis()); 256 onDateChanged(true, true); 257 } 258 }; 259 260 /** 261 * Listener called when the user selects a year in the year picker view. 262 */ 263 private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() { 264 @Override 265 public void onYearChanged(YearPickerView view, int year) { 266 // If the newly selected month / year does not contain the 267 // currently selected day number, change the selected day number 268 // to the last day of the selected month or year. 269 // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 270 // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 271 final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); 272 final int month = mCurrentDate.get(Calendar.MONTH); 273 final int daysInMonth = getDaysInMonth(month, year); 274 if (day > daysInMonth) { 275 mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth); 276 } 277 278 mCurrentDate.set(Calendar.YEAR, year); 279 if (mCurrentDate.compareTo(mMinDate) < 0) { 280 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 281 } else if (mCurrentDate.compareTo(mMaxDate) > 0) { 282 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 283 } 284 onDateChanged(true, true); 285 286 // Automatically switch to day picker. 287 setCurrentView(VIEW_MONTH_DAY); 288 289 // Switch focus back to the year text. 290 mHeaderYear.requestFocus(); 291 } 292 }; 293 294 /** 295 * Listener called when the user clicks on a header item. 296 */ 297 private final OnClickListener mOnHeaderClickListener = v -> { 298 tryVibrate(); 299 300 switch (v.getId()) { 301 case R.id.date_picker_header_year: 302 setCurrentView(VIEW_YEAR); 303 break; 304 case R.id.date_picker_header_date: 305 setCurrentView(VIEW_MONTH_DAY); 306 break; 307 } 308 }; 309 310 @Override onLocaleChanged(Locale locale)311 protected void onLocaleChanged(Locale locale) { 312 final TextView headerYear = mHeaderYear; 313 if (headerYear == null) { 314 // Abort, we haven't initialized yet. This method will get called 315 // again later after everything has been set up. 316 return; 317 } 318 319 // Update the date formatter. 320 mMonthDayFormat = DateFormat.getInstanceForSkeleton("EMMMd", locale); 321 // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of 322 // CAPITALIZATION_FOR_STANDALONE is to address 323 // https://unicode-org.atlassian.net/browse/ICU-21631 324 // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE 325 mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE); 326 mYearFormat = DateFormat.getInstanceForSkeleton("y", locale); 327 328 // Update the header text. 329 onCurrentDateChanged(); 330 } 331 onCurrentDateChanged()332 private void onCurrentDateChanged() { 333 if (mHeaderYear == null) { 334 // Abort, we haven't initialized yet. This method will get called 335 // again later after everything has been set up. 336 return; 337 } 338 339 final String year = mYearFormat.format(mCurrentDate.getTime()); 340 mHeaderYear.setText(year); 341 342 final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime()); 343 mHeaderMonthDay.setText(monthDay); 344 } 345 setCurrentView(final int viewIndex)346 private void setCurrentView(final int viewIndex) { 347 switch (viewIndex) { 348 case VIEW_MONTH_DAY: 349 mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); 350 351 if (mCurrentView != viewIndex) { 352 mHeaderMonthDay.setActivated(true); 353 mHeaderYear.setActivated(false); 354 mAnimator.setDisplayedChild(VIEW_MONTH_DAY); 355 mCurrentView = viewIndex; 356 } 357 break; 358 case VIEW_YEAR: 359 final int year = mCurrentDate.get(Calendar.YEAR); 360 mYearPickerView.setYear(year); 361 mYearPickerView.post(() -> { 362 mYearPickerView.requestFocus(); 363 final View selected = mYearPickerView.getSelectedView(); 364 if (selected != null) { 365 selected.requestFocus(); 366 } 367 }); 368 369 if (mCurrentView != viewIndex) { 370 mHeaderMonthDay.setActivated(false); 371 mHeaderYear.setActivated(true); 372 mAnimator.setDisplayedChild(VIEW_YEAR); 373 mCurrentView = viewIndex; 374 } 375 376 break; 377 } 378 } 379 380 @Override init(int year, int month, int dayOfMonth, DatePicker.OnDateChangedListener callBack)381 public void init(int year, int month, int dayOfMonth, 382 DatePicker.OnDateChangedListener callBack) { 383 setDate(year, month, dayOfMonth); 384 onDateChanged(false, false); 385 386 mOnDateChangedListener = callBack; 387 } 388 389 @Override updateDate(int year, int month, int dayOfMonth)390 public void updateDate(int year, int month, int dayOfMonth) { 391 setDate(year, month, dayOfMonth); 392 onDateChanged(false, true); 393 } 394 setDate(int year, int month, int dayOfMonth)395 private void setDate(int year, int month, int dayOfMonth) { 396 mCurrentDate.set(Calendar.YEAR, year); 397 mCurrentDate.set(Calendar.MONTH, month); 398 mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); 399 resetAutofilledValue(); 400 } 401 onDateChanged(boolean fromUser, boolean callbackToClient)402 private void onDateChanged(boolean fromUser, boolean callbackToClient) { 403 final int year = mCurrentDate.get(Calendar.YEAR); 404 405 if (callbackToClient 406 && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) { 407 final int monthOfYear = mCurrentDate.get(Calendar.MONTH); 408 final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH); 409 if (mOnDateChangedListener != null) { 410 mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth); 411 } 412 if (mAutoFillChangeListener != null) { 413 mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth); 414 } 415 } 416 417 mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); 418 mYearPickerView.setYear(year); 419 420 onCurrentDateChanged(); 421 422 if (fromUser) { 423 tryVibrate(); 424 } 425 } 426 427 @Override getYear()428 public int getYear() { 429 return mCurrentDate.get(Calendar.YEAR); 430 } 431 432 @Override getMonth()433 public int getMonth() { 434 return mCurrentDate.get(Calendar.MONTH); 435 } 436 437 @Override getDayOfMonth()438 public int getDayOfMonth() { 439 return mCurrentDate.get(Calendar.DAY_OF_MONTH); 440 } 441 442 @Override setMinDate(long minDate)443 public void setMinDate(long minDate) { 444 mTempDate.setTimeInMillis(minDate); 445 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 446 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) { 447 // Same day, no-op. 448 return; 449 } 450 if (mCurrentDate.before(mTempDate)) { 451 mCurrentDate.setTimeInMillis(minDate); 452 onDateChanged(false, true); 453 } 454 mMinDate.setTimeInMillis(minDate); 455 mDayPickerView.setMinDate(minDate); 456 mYearPickerView.setRange(mMinDate, mMaxDate); 457 } 458 459 @Override getMinDate()460 public Calendar getMinDate() { 461 return mMinDate; 462 } 463 464 @Override setMaxDate(long maxDate)465 public void setMaxDate(long maxDate) { 466 mTempDate.setTimeInMillis(maxDate); 467 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 468 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) { 469 // Same day, no-op. 470 return; 471 } 472 if (mCurrentDate.after(mTempDate)) { 473 mCurrentDate.setTimeInMillis(maxDate); 474 onDateChanged(false, true); 475 } 476 mMaxDate.setTimeInMillis(maxDate); 477 mDayPickerView.setMaxDate(maxDate); 478 mYearPickerView.setRange(mMinDate, mMaxDate); 479 } 480 481 @Override getMaxDate()482 public Calendar getMaxDate() { 483 return mMaxDate; 484 } 485 486 @Override setFirstDayOfWeek(int firstDayOfWeek)487 public void setFirstDayOfWeek(int firstDayOfWeek) { 488 mFirstDayOfWeek = firstDayOfWeek; 489 490 mDayPickerView.setFirstDayOfWeek(firstDayOfWeek); 491 } 492 493 @Override getFirstDayOfWeek()494 public int getFirstDayOfWeek() { 495 if (mFirstDayOfWeek != USE_LOCALE) { 496 return mFirstDayOfWeek; 497 } 498 return mCurrentDate.getFirstDayOfWeek(); 499 } 500 501 @Override setEnabled(boolean enabled)502 public void setEnabled(boolean enabled) { 503 mContainer.setEnabled(enabled); 504 mDayPickerView.setEnabled(enabled); 505 mYearPickerView.setEnabled(enabled); 506 mHeaderYear.setEnabled(enabled); 507 mHeaderMonthDay.setEnabled(enabled); 508 } 509 510 @Override isEnabled()511 public boolean isEnabled() { 512 return mContainer.isEnabled(); 513 } 514 515 @Override getCalendarView()516 public CalendarView getCalendarView() { 517 throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker"); 518 } 519 520 @Override setCalendarViewShown(boolean shown)521 public void setCalendarViewShown(boolean shown) { 522 // No-op for compatibility with the old DatePicker. 523 } 524 525 @Override getCalendarViewShown()526 public boolean getCalendarViewShown() { 527 return false; 528 } 529 530 @Override setSpinnersShown(boolean shown)531 public void setSpinnersShown(boolean shown) { 532 // No-op for compatibility with the old DatePicker. 533 } 534 535 @Override getSpinnersShown()536 public boolean getSpinnersShown() { 537 return false; 538 } 539 540 @Override onConfigurationChanged(Configuration newConfig)541 public void onConfigurationChanged(Configuration newConfig) { 542 setCurrentLocale(newConfig.locale); 543 } 544 545 @Override onSaveInstanceState(Parcelable superState)546 public Parcelable onSaveInstanceState(Parcelable superState) { 547 final int year = mCurrentDate.get(Calendar.YEAR); 548 final int month = mCurrentDate.get(Calendar.MONTH); 549 final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); 550 551 int listPosition = -1; 552 int listPositionOffset = -1; 553 554 if (mCurrentView == VIEW_MONTH_DAY) { 555 listPosition = mDayPickerView.getMostVisiblePosition(); 556 } else if (mCurrentView == VIEW_YEAR) { 557 listPosition = mYearPickerView.getFirstVisiblePosition(); 558 listPositionOffset = mYearPickerView.getFirstPositionOffset(); 559 } 560 561 return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(), 562 mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset); 563 } 564 565 @Override onRestoreInstanceState(Parcelable state)566 public void onRestoreInstanceState(Parcelable state) { 567 if (state instanceof SavedState) { 568 final SavedState ss = (SavedState) state; 569 570 // TODO: Move instance state into DayPickerView, YearPickerView. 571 mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay()); 572 mMinDate.setTimeInMillis(ss.getMinDate()); 573 mMaxDate.setTimeInMillis(ss.getMaxDate()); 574 575 onCurrentDateChanged(); 576 577 final int currentView = ss.getCurrentView(); 578 setCurrentView(currentView); 579 580 final int listPosition = ss.getListPosition(); 581 if (listPosition != -1) { 582 if (currentView == VIEW_MONTH_DAY) { 583 mDayPickerView.setPosition(listPosition); 584 } else if (currentView == VIEW_YEAR) { 585 final int listPositionOffset = ss.getListPositionOffset(); 586 mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset); 587 } 588 } 589 } 590 } 591 592 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)593 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 594 onPopulateAccessibilityEvent(event); 595 return true; 596 } 597 getAccessibilityClassName()598 public CharSequence getAccessibilityClassName() { 599 return DatePicker.class.getName(); 600 } 601 getDaysInMonth(int month, int year)602 private static int getDaysInMonth(int month, int year) { 603 switch (month) { 604 case Calendar.JANUARY: 605 case Calendar.MARCH: 606 case Calendar.MAY: 607 case Calendar.JULY: 608 case Calendar.AUGUST: 609 case Calendar.OCTOBER: 610 case Calendar.DECEMBER: 611 return 31; 612 case Calendar.APRIL: 613 case Calendar.JUNE: 614 case Calendar.SEPTEMBER: 615 case Calendar.NOVEMBER: 616 return 30; 617 case Calendar.FEBRUARY: 618 return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28; 619 default: 620 throw new IllegalArgumentException("Invalid Month"); 621 } 622 } 623 tryVibrate()624 private void tryVibrate() { 625 mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE); 626 } 627 } 628