TimePickerClockDelegate.java revision 646a0f8e0d0a9f94a5969d09a498f5de5fe40a88
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.os.Parcel; 24import android.os.Parcelable; 25import android.text.format.DateFormat; 26import android.text.format.DateUtils; 27import android.util.AttributeSet; 28import android.util.Log; 29import android.util.TypedValue; 30import android.view.HapticFeedbackConstants; 31import android.view.KeyCharacterMap; 32import android.view.KeyEvent; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.View.AccessibilityDelegate; 36import android.view.accessibility.AccessibilityEvent; 37import android.view.accessibility.AccessibilityNodeInfo; 38import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 39 40import com.android.internal.R; 41 42import java.util.ArrayList; 43import java.util.Calendar; 44import java.util.Locale; 45 46/** 47 * A delegate implementing the radial clock-based TimePicker. 48 */ 49class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate implements 50 RadialTimePickerView.OnValueSelectedListener { 51 52 private static final String TAG = "TimePickerClockDelegate"; 53 54 // Index used by RadialPickerLayout 55 private static final int HOUR_INDEX = 0; 56 private static final int MINUTE_INDEX = 1; 57 58 // NOT a real index for the purpose of what's showing. 59 private static final int AMPM_INDEX = 2; 60 61 // Also NOT a real index, just used for keyboard mode. 62 private static final int ENABLE_PICKER_INDEX = 3; 63 64 // LayoutLib relies on these constants. Change TimePickerClockDelegate_Delegate if 65 // modifying these. 66 static final int AM = 0; 67 static final int PM = 1; 68 69 private static final boolean DEFAULT_ENABLED_STATE = true; 70 private boolean mIsEnabled = DEFAULT_ENABLED_STATE; 71 72 private static final int HOURS_IN_HALF_DAY = 12; 73 74 private final View mHeaderView; 75 private final TextView mHourView; 76 private final TextView mMinuteView; 77 private final View mAmPmLayout; 78 private final CheckedTextView mAmLabel; 79 private final CheckedTextView mPmLabel; 80 private final RadialTimePickerView mRadialTimePickerView; 81 private final TextView mSeparatorView; 82 83 private final String mAmText; 84 private final String mPmText; 85 86 private final float mDisabledAlpha; 87 88 private boolean mAllowAutoAdvance; 89 private int mInitialHourOfDay; 90 private int mInitialMinute; 91 private boolean mIs24HourView; 92 private boolean mIsAmPmAtStart; 93 94 // For hardware IME input. 95 private char mPlaceholderText; 96 private String mDoublePlaceholderText; 97 private String mDeletedKeyFormat; 98 private boolean mInKbMode; 99 private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); 100 private Node mLegalTimesTree; 101 private int mAmKeyCode; 102 private int mPmKeyCode; 103 104 // Accessibility strings. 105 private String mSelectHours; 106 private String mSelectMinutes; 107 108 // Most recent time announcement values for accessibility. 109 private CharSequence mLastAnnouncedText; 110 private boolean mLastAnnouncedIsHour; 111 112 private Calendar mTempCalendar; 113 114 public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs, 115 int defStyleAttr, int defStyleRes) { 116 super(delegator, context); 117 118 // process style attributes 119 final TypedArray a = mContext.obtainStyledAttributes(attrs, 120 R.styleable.TimePicker, defStyleAttr, defStyleRes); 121 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 122 Context.LAYOUT_INFLATER_SERVICE); 123 final Resources res = mContext.getResources(); 124 125 mSelectHours = res.getString(R.string.select_hours); 126 mSelectMinutes = res.getString(R.string.select_minutes); 127 128 String[] amPmStrings = TimePickerSpinnerDelegate.getAmPmStrings(context); 129 mAmText = amPmStrings[0]; 130 mPmText = amPmStrings[1]; 131 132 final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, 133 R.layout.time_picker_material); 134 final View mainView = inflater.inflate(layoutResourceId, delegator); 135 136 mHeaderView = mainView.findViewById(R.id.time_header); 137 mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); 138 139 // Set up hour/minute labels. 140 mHourView = (TextView) mainView.findViewById(R.id.hours); 141 mHourView.setOnClickListener(mClickListener); 142 mHourView.setAccessibilityDelegate( 143 new ClickActionDelegate(context, R.string.select_hours)); 144 mSeparatorView = (TextView) mainView.findViewById(R.id.separator); 145 mMinuteView = (TextView) mainView.findViewById(R.id.minutes); 146 mMinuteView.setOnClickListener(mClickListener); 147 mMinuteView.setAccessibilityDelegate( 148 new ClickActionDelegate(context, R.string.select_minutes)); 149 150 final int headerTimeTextAppearance = a.getResourceId( 151 R.styleable.TimePicker_headerTimeTextAppearance, 0); 152 if (headerTimeTextAppearance != 0) { 153 mHourView.setTextAppearance(context, headerTimeTextAppearance); 154 mSeparatorView.setTextAppearance(context, headerTimeTextAppearance); 155 mMinuteView.setTextAppearance(context, headerTimeTextAppearance); 156 } 157 158 // Now that we have text appearances out of the way, make sure the hour 159 // and minute views are correctly sized. 160 mHourView.setMinWidth(computeStableWidth(mHourView, 24)); 161 mMinuteView.setMinWidth(computeStableWidth(mMinuteView, 60)); 162 163 // Set up AM/PM labels. 164 mAmPmLayout = mainView.findViewById(R.id.ampm_layout); 165 mAmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.am_label); 166 mAmLabel.setText(amPmStrings[0]); 167 mAmLabel.setOnClickListener(mClickListener); 168 mPmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.pm_label); 169 mPmLabel.setText(amPmStrings[1]); 170 mPmLabel.setOnClickListener(mClickListener); 171 172 final int headerAmPmTextAppearance = a.getResourceId( 173 R.styleable.TimePicker_headerAmPmTextAppearance, 0); 174 if (headerAmPmTextAppearance != 0) { 175 mAmLabel.setTextAppearance(context, headerAmPmTextAppearance); 176 mPmLabel.setTextAppearance(context, headerAmPmTextAppearance); 177 } 178 179 a.recycle(); 180 181 // Pull disabled alpha from theme. 182 final TypedValue outValue = new TypedValue(); 183 context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); 184 mDisabledAlpha = outValue.getFloat(); 185 186 mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById( 187 R.id.radial_picker); 188 189 setupListeners(); 190 191 mAllowAutoAdvance = true; 192 193 // Set up for keyboard mode. 194 mDoublePlaceholderText = res.getString(R.string.time_placeholder); 195 mDeletedKeyFormat = res.getString(R.string.deleted_key); 196 mPlaceholderText = mDoublePlaceholderText.charAt(0); 197 mAmKeyCode = mPmKeyCode = -1; 198 generateLegalTimesTree(); 199 200 // Initialize with current time 201 final Calendar calendar = Calendar.getInstance(mCurrentLocale); 202 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); 203 final int currentMinute = calendar.get(Calendar.MINUTE); 204 initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX); 205 } 206 207 private static class ClickActionDelegate extends AccessibilityDelegate { 208 private final AccessibilityAction mClickAction; 209 210 public ClickActionDelegate(Context context, int resId) { 211 mClickAction = new AccessibilityAction( 212 AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId)); 213 } 214 215 @Override 216 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 217 super.onInitializeAccessibilityNodeInfo(host, info); 218 219 info.addAction(mClickAction); 220 } 221 } 222 223 private int computeStableWidth(TextView v, int maxNumber) { 224 int maxWidth = 0; 225 226 for (int i = 0; i < maxNumber; i++) { 227 final String text = String.format("%02d", i); 228 v.setText(text); 229 v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 230 231 final int width = v.getMeasuredWidth(); 232 if (width > maxWidth) { 233 maxWidth = width; 234 } 235 } 236 237 return maxWidth; 238 } 239 240 private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) { 241 mInitialHourOfDay = hourOfDay; 242 mInitialMinute = minute; 243 mIs24HourView = is24HourView; 244 mInKbMode = false; 245 updateUI(index); 246 } 247 248 private void setupListeners() { 249 mHeaderView.setOnKeyListener(mKeyListener); 250 mHeaderView.setOnFocusChangeListener(mFocusListener); 251 mHeaderView.setFocusable(true); 252 253 mRadialTimePickerView.setOnValueSelectedListener(this); 254 } 255 256 private void updateUI(int index) { 257 // Update RadialPicker values 258 updateRadialPicker(index); 259 // Enable or disable the AM/PM view. 260 updateHeaderAmPm(); 261 // Update Hour and Minutes 262 updateHeaderHour(mInitialHourOfDay, false); 263 // Update time separator 264 updateHeaderSeparator(); 265 // Update Minutes 266 updateHeaderMinute(mInitialMinute, false); 267 // Invalidate everything 268 mDelegator.invalidate(); 269 } 270 271 private void updateRadialPicker(int index) { 272 mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); 273 setCurrentItemShowing(index, false, true); 274 } 275 276 private void updateHeaderAmPm() { 277 278 if (mIs24HourView) { 279 mAmPmLayout.setVisibility(View.GONE); 280 } else { 281 // Ensure that AM/PM layout is in the correct position. 282 final String dateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, "hm"); 283 final boolean isAmPmAtStart = dateTimePattern.startsWith("a"); 284 setAmPmAtStart(isAmPmAtStart); 285 286 updateAmPmLabelStates(mInitialHourOfDay < 12 ? AM : PM); 287 } 288 } 289 290 private void setAmPmAtStart(boolean isAmPmAtStart) { 291 if (mIsAmPmAtStart != isAmPmAtStart) { 292 mIsAmPmAtStart = isAmPmAtStart; 293 294 final RelativeLayout.LayoutParams params = 295 (RelativeLayout.LayoutParams) mAmPmLayout.getLayoutParams(); 296 if (params.getRule(RelativeLayout.RIGHT_OF) != 0 || 297 params.getRule(RelativeLayout.LEFT_OF) != 0) { 298 if (isAmPmAtStart) { 299 params.removeRule(RelativeLayout.RIGHT_OF); 300 params.addRule(RelativeLayout.LEFT_OF, mHourView.getId()); 301 } else { 302 params.removeRule(RelativeLayout.LEFT_OF); 303 params.addRule(RelativeLayout.RIGHT_OF, mMinuteView.getId()); 304 } 305 } 306 307 mAmPmLayout.setLayoutParams(params); 308 } 309 } 310 311 /** 312 * Set the current hour. 313 */ 314 @Override 315 public void setCurrentHour(int currentHour) { 316 if (mInitialHourOfDay == currentHour) { 317 return; 318 } 319 mInitialHourOfDay = currentHour; 320 updateHeaderHour(currentHour, true); 321 updateHeaderAmPm(); 322 mRadialTimePickerView.setCurrentHour(currentHour); 323 mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); 324 mDelegator.invalidate(); 325 onTimeChanged(); 326 } 327 328 /** 329 * @return The current hour in the range (0-23). 330 */ 331 @Override 332 public int getCurrentHour() { 333 int currentHour = mRadialTimePickerView.getCurrentHour(); 334 if (mIs24HourView) { 335 return currentHour; 336 } else { 337 switch(mRadialTimePickerView.getAmOrPm()) { 338 case PM: 339 return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; 340 case AM: 341 default: 342 return currentHour % HOURS_IN_HALF_DAY; 343 } 344 } 345 } 346 347 /** 348 * Set the current minute (0-59). 349 */ 350 @Override 351 public void setCurrentMinute(int currentMinute) { 352 if (mInitialMinute == currentMinute) { 353 return; 354 } 355 mInitialMinute = currentMinute; 356 updateHeaderMinute(currentMinute, true); 357 mRadialTimePickerView.setCurrentMinute(currentMinute); 358 mDelegator.invalidate(); 359 onTimeChanged(); 360 } 361 362 /** 363 * @return The current minute. 364 */ 365 @Override 366 public int getCurrentMinute() { 367 return mRadialTimePickerView.getCurrentMinute(); 368 } 369 370 /** 371 * Set whether in 24 hour or AM/PM mode. 372 * 373 * @param is24HourView True = 24 hour mode. False = AM/PM. 374 */ 375 @Override 376 public void setIs24HourView(boolean is24HourView) { 377 if (is24HourView == mIs24HourView) { 378 return; 379 } 380 mIs24HourView = is24HourView; 381 generateLegalTimesTree(); 382 int hour = mRadialTimePickerView.getCurrentHour(); 383 mInitialHourOfDay = hour; 384 updateHeaderHour(hour, false); 385 updateHeaderAmPm(); 386 updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); 387 mDelegator.invalidate(); 388 } 389 390 /** 391 * @return true if this is in 24 hour view else false. 392 */ 393 @Override 394 public boolean is24HourView() { 395 return mIs24HourView; 396 } 397 398 @Override 399 public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { 400 mOnTimeChangedListener = callback; 401 } 402 403 @Override 404 public void setEnabled(boolean enabled) { 405 mHourView.setEnabled(enabled); 406 mMinuteView.setEnabled(enabled); 407 mAmLabel.setEnabled(enabled); 408 mPmLabel.setEnabled(enabled); 409 mRadialTimePickerView.setEnabled(enabled); 410 mIsEnabled = enabled; 411 } 412 413 @Override 414 public boolean isEnabled() { 415 return mIsEnabled; 416 } 417 418 @Override 419 public int getBaseline() { 420 // does not support baseline alignment 421 return -1; 422 } 423 424 @Override 425 public void onConfigurationChanged(Configuration newConfig) { 426 updateUI(mRadialTimePickerView.getCurrentItemShowing()); 427 } 428 429 @Override 430 public Parcelable onSaveInstanceState(Parcelable superState) { 431 return new SavedState(superState, getCurrentHour(), getCurrentMinute(), 432 is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing()); 433 } 434 435 @Override 436 public void onRestoreInstanceState(Parcelable state) { 437 SavedState ss = (SavedState) state; 438 setInKbMode(ss.inKbMode()); 439 setTypedTimes(ss.getTypesTimes()); 440 initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing()); 441 mRadialTimePickerView.invalidate(); 442 if (mInKbMode) { 443 tryStartingKbMode(-1); 444 mHourView.invalidate(); 445 } 446 } 447 448 @Override 449 public void setCurrentLocale(Locale locale) { 450 super.setCurrentLocale(locale); 451 mTempCalendar = Calendar.getInstance(locale); 452 } 453 454 @Override 455 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 456 onPopulateAccessibilityEvent(event); 457 return true; 458 } 459 460 @Override 461 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 462 int flags = DateUtils.FORMAT_SHOW_TIME; 463 if (mIs24HourView) { 464 flags |= DateUtils.FORMAT_24HOUR; 465 } else { 466 flags |= DateUtils.FORMAT_12HOUR; 467 } 468 mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); 469 mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); 470 String selectedDate = DateUtils.formatDateTime(mContext, 471 mTempCalendar.getTimeInMillis(), flags); 472 event.getText().add(selectedDate); 473 } 474 475 /** 476 * Set whether in keyboard mode or not. 477 * 478 * @param inKbMode True means in keyboard mode. 479 */ 480 private void setInKbMode(boolean inKbMode) { 481 mInKbMode = inKbMode; 482 } 483 484 /** 485 * @return true if in keyboard mode 486 */ 487 private boolean inKbMode() { 488 return mInKbMode; 489 } 490 491 private void setTypedTimes(ArrayList<Integer> typeTimes) { 492 mTypedTimes = typeTimes; 493 } 494 495 /** 496 * @return an array of typed times 497 */ 498 private ArrayList<Integer> getTypedTimes() { 499 return mTypedTimes; 500 } 501 502 /** 503 * @return the index of the current item showing 504 */ 505 private int getCurrentItemShowing() { 506 return mRadialTimePickerView.getCurrentItemShowing(); 507 } 508 509 /** 510 * Propagate the time change 511 */ 512 private void onTimeChanged() { 513 mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 514 if (mOnTimeChangedListener != null) { 515 mOnTimeChangedListener.onTimeChanged(mDelegator, 516 getCurrentHour(), getCurrentMinute()); 517 } 518 } 519 520 /** 521 * Used to save / restore state of time picker 522 */ 523 private static class SavedState extends View.BaseSavedState { 524 525 private final int mHour; 526 private final int mMinute; 527 private final boolean mIs24HourMode; 528 private final boolean mInKbMode; 529 private final ArrayList<Integer> mTypedTimes; 530 private final int mCurrentItemShowing; 531 532 private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, 533 boolean isKbMode, ArrayList<Integer> typedTimes, 534 int currentItemShowing) { 535 super(superState); 536 mHour = hour; 537 mMinute = minute; 538 mIs24HourMode = is24HourMode; 539 mInKbMode = isKbMode; 540 mTypedTimes = typedTimes; 541 mCurrentItemShowing = currentItemShowing; 542 } 543 544 private SavedState(Parcel in) { 545 super(in); 546 mHour = in.readInt(); 547 mMinute = in.readInt(); 548 mIs24HourMode = (in.readInt() == 1); 549 mInKbMode = (in.readInt() == 1); 550 mTypedTimes = in.readArrayList(getClass().getClassLoader()); 551 mCurrentItemShowing = in.readInt(); 552 } 553 554 public int getHour() { 555 return mHour; 556 } 557 558 public int getMinute() { 559 return mMinute; 560 } 561 562 public boolean is24HourMode() { 563 return mIs24HourMode; 564 } 565 566 public boolean inKbMode() { 567 return mInKbMode; 568 } 569 570 public ArrayList<Integer> getTypesTimes() { 571 return mTypedTimes; 572 } 573 574 public int getCurrentItemShowing() { 575 return mCurrentItemShowing; 576 } 577 578 @Override 579 public void writeToParcel(Parcel dest, int flags) { 580 super.writeToParcel(dest, flags); 581 dest.writeInt(mHour); 582 dest.writeInt(mMinute); 583 dest.writeInt(mIs24HourMode ? 1 : 0); 584 dest.writeInt(mInKbMode ? 1 : 0); 585 dest.writeList(mTypedTimes); 586 dest.writeInt(mCurrentItemShowing); 587 } 588 589 @SuppressWarnings({"unused", "hiding"}) 590 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 591 public SavedState createFromParcel(Parcel in) { 592 return new SavedState(in); 593 } 594 595 public SavedState[] newArray(int size) { 596 return new SavedState[size]; 597 } 598 }; 599 } 600 601 private void tryVibrate() { 602 mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 603 } 604 605 private void updateAmPmLabelStates(int amOrPm) { 606 final boolean isAm = amOrPm == AM; 607 mAmLabel.setChecked(isAm); 608 mAmLabel.setSelected(isAm); 609 610 final boolean isPm = amOrPm == PM; 611 mPmLabel.setChecked(isPm); 612 mPmLabel.setSelected(isPm); 613 } 614 615 /** 616 * Called by the picker for updating the header display. 617 */ 618 @Override 619 public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { 620 switch (pickerIndex) { 621 case HOUR_INDEX: 622 if (mAllowAutoAdvance && autoAdvance) { 623 updateHeaderHour(newValue, false); 624 setCurrentItemShowing(MINUTE_INDEX, true, false); 625 mDelegator.announceForAccessibility(newValue + ". " + mSelectMinutes); 626 } else { 627 updateHeaderHour(newValue, true); 628 } 629 break; 630 case MINUTE_INDEX: 631 updateHeaderMinute(newValue, true); 632 break; 633 case AMPM_INDEX: 634 updateAmPmLabelStates(newValue); 635 break; 636 case ENABLE_PICKER_INDEX: 637 if (!isTypedTimeFullyLegal()) { 638 mTypedTimes.clear(); 639 } 640 finishKbMode(); 641 break; 642 } 643 644 if (mOnTimeChangedListener != null) { 645 mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), getCurrentMinute()); 646 } 647 } 648 649 private void updateHeaderHour(int value, boolean announce) { 650 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 651 (mIs24HourView) ? "Hm" : "hm"); 652 final int lengthPattern = bestDateTimePattern.length(); 653 boolean hourWithTwoDigit = false; 654 char hourFormat = '\0'; 655 // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save 656 // the hour format that we found. 657 for (int i = 0; i < lengthPattern; i++) { 658 final char c = bestDateTimePattern.charAt(i); 659 if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { 660 hourFormat = c; 661 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { 662 hourWithTwoDigit = true; 663 } 664 break; 665 } 666 } 667 final String format; 668 if (hourWithTwoDigit) { 669 format = "%02d"; 670 } else { 671 format = "%d"; 672 } 673 if (mIs24HourView) { 674 // 'k' means 1-24 hour 675 if (hourFormat == 'k' && value == 0) { 676 value = 24; 677 } 678 } else { 679 // 'K' means 0-11 hour 680 value = modulo12(value, hourFormat == 'K'); 681 } 682 CharSequence text = String.format(format, value); 683 mHourView.setText(text); 684 if (announce) { 685 tryAnnounceForAccessibility(text, true); 686 } 687 } 688 689 private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) { 690 if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) { 691 // TODO: Find a better solution, potentially live regions? 692 mDelegator.announceForAccessibility(text); 693 mLastAnnouncedText = text; 694 mLastAnnouncedIsHour = isHour; 695 } 696 } 697 698 private static int modulo12(int n, boolean startWithZero) { 699 int value = n % 12; 700 if (value == 0 && !startWithZero) { 701 value = 12; 702 } 703 return value; 704 } 705 706 /** 707 * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". 708 * 709 * See http://unicode.org/cldr/trac/browser/trunk/common/main 710 * 711 * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the 712 * separator as the character which is just after the hour marker in the returned pattern. 713 */ 714 private void updateHeaderSeparator() { 715 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 716 (mIs24HourView) ? "Hm" : "hm"); 717 final String separatorText; 718 // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats 719 final char[] hourFormats = {'H', 'h', 'K', 'k'}; 720 int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); 721 if (hIndex == -1) { 722 // Default case 723 separatorText = ":"; 724 } else { 725 separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); 726 } 727 mSeparatorView.setText(separatorText); 728 } 729 730 static private int lastIndexOfAny(String str, char[] any) { 731 final int lengthAny = any.length; 732 if (lengthAny > 0) { 733 for (int i = str.length() - 1; i >= 0; i--) { 734 char c = str.charAt(i); 735 for (int j = 0; j < lengthAny; j++) { 736 if (c == any[j]) { 737 return i; 738 } 739 } 740 } 741 } 742 return -1; 743 } 744 745 private void updateHeaderMinute(int value, boolean announceForAccessibility) { 746 if (value == 60) { 747 value = 0; 748 } 749 final CharSequence text = String.format(mCurrentLocale, "%02d", value); 750 mMinuteView.setText(text); 751 if (announceForAccessibility) { 752 tryAnnounceForAccessibility(text, false); 753 } 754 } 755 756 /** 757 * Show either Hours or Minutes. 758 */ 759 private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { 760 mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); 761 762 if (index == HOUR_INDEX) { 763 if (announce) { 764 mDelegator.announceForAccessibility(mSelectHours); 765 } 766 } else { 767 if (announce) { 768 mDelegator.announceForAccessibility(mSelectMinutes); 769 } 770 } 771 772 mHourView.setSelected(index == HOUR_INDEX); 773 mMinuteView.setSelected(index == MINUTE_INDEX); 774 } 775 776 private void setAmOrPm(int amOrPm) { 777 updateAmPmLabelStates(amOrPm); 778 mRadialTimePickerView.setAmOrPm(amOrPm); 779 } 780 781 /** 782 * For keyboard mode, processes key events. 783 * 784 * @param keyCode the pressed key. 785 * 786 * @return true if the key was successfully processed, false otherwise. 787 */ 788 private boolean processKeyUp(int keyCode) { 789 if (keyCode == KeyEvent.KEYCODE_DEL) { 790 if (mInKbMode) { 791 if (!mTypedTimes.isEmpty()) { 792 int deleted = deleteLastTypedKey(); 793 String deletedKeyStr; 794 if (deleted == getAmOrPmKeyCode(AM)) { 795 deletedKeyStr = mAmText; 796 } else if (deleted == getAmOrPmKeyCode(PM)) { 797 deletedKeyStr = mPmText; 798 } else { 799 deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); 800 } 801 mDelegator.announceForAccessibility( 802 String.format(mDeletedKeyFormat, deletedKeyStr)); 803 updateDisplay(true); 804 } 805 } 806 } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 807 || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 808 || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 809 || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 810 || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 811 || (!mIs24HourView && 812 (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { 813 if (!mInKbMode) { 814 if (mRadialTimePickerView == null) { 815 // Something's wrong, because time picker should definitely not be null. 816 Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); 817 return true; 818 } 819 mTypedTimes.clear(); 820 tryStartingKbMode(keyCode); 821 return true; 822 } 823 // We're already in keyboard mode. 824 if (addKeyIfLegal(keyCode)) { 825 updateDisplay(false); 826 } 827 return true; 828 } 829 return false; 830 } 831 832 /** 833 * Try to start keyboard mode with the specified key. 834 * 835 * @param keyCode The key to use as the first press. Keyboard mode will not be started if the 836 * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting 837 * key. 838 */ 839 private void tryStartingKbMode(int keyCode) { 840 if (keyCode == -1 || addKeyIfLegal(keyCode)) { 841 mInKbMode = true; 842 onValidationChanged(false); 843 updateDisplay(false); 844 mRadialTimePickerView.setInputEnabled(false); 845 } 846 } 847 848 private boolean addKeyIfLegal(int keyCode) { 849 // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, 850 // we'll need to see if AM/PM have been typed. 851 if ((mIs24HourView && mTypedTimes.size() == 4) || 852 (!mIs24HourView && isTypedTimeFullyLegal())) { 853 return false; 854 } 855 856 mTypedTimes.add(keyCode); 857 if (!isTypedTimeLegalSoFar()) { 858 deleteLastTypedKey(); 859 return false; 860 } 861 862 int val = getValFromKeyCode(keyCode); 863 mDelegator.announceForAccessibility(String.format("%d", val)); 864 // Automatically fill in 0's if AM or PM was legally entered. 865 if (isTypedTimeFullyLegal()) { 866 if (!mIs24HourView && mTypedTimes.size() <= 3) { 867 mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); 868 mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); 869 } 870 onValidationChanged(true); 871 } 872 873 return true; 874 } 875 876 /** 877 * Traverse the tree to see if the keys that have been typed so far are legal as is, 878 * or may become legal as more keys are typed (excluding backspace). 879 */ 880 private boolean isTypedTimeLegalSoFar() { 881 Node node = mLegalTimesTree; 882 for (int keyCode : mTypedTimes) { 883 node = node.canReach(keyCode); 884 if (node == null) { 885 return false; 886 } 887 } 888 return true; 889 } 890 891 /** 892 * Check if the time that has been typed so far is completely legal, as is. 893 */ 894 private boolean isTypedTimeFullyLegal() { 895 if (mIs24HourView) { 896 // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: 897 // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. 898 int[] values = getEnteredTime(null); 899 return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); 900 } else { 901 // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be 902 // legally added at specific times based on the tree's algorithm. 903 return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || 904 mTypedTimes.contains(getAmOrPmKeyCode(PM))); 905 } 906 } 907 908 private int deleteLastTypedKey() { 909 int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); 910 if (!isTypedTimeFullyLegal()) { 911 onValidationChanged(false); 912 } 913 return deleted; 914 } 915 916 /** 917 * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. 918 */ 919 private void finishKbMode() { 920 mInKbMode = false; 921 if (!mTypedTimes.isEmpty()) { 922 int values[] = getEnteredTime(null); 923 mRadialTimePickerView.setCurrentHour(values[0]); 924 mRadialTimePickerView.setCurrentMinute(values[1]); 925 if (!mIs24HourView) { 926 mRadialTimePickerView.setAmOrPm(values[2]); 927 } 928 mTypedTimes.clear(); 929 } 930 updateDisplay(false); 931 mRadialTimePickerView.setInputEnabled(true); 932 } 933 934 /** 935 * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is 936 * empty, either show an empty display (filled with the placeholder text), or update from the 937 * timepicker's values. 938 * 939 * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. 940 * Otherwise, revert to the timepicker's values. 941 */ 942 private void updateDisplay(boolean allowEmptyDisplay) { 943 if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { 944 int hour = mRadialTimePickerView.getCurrentHour(); 945 int minute = mRadialTimePickerView.getCurrentMinute(); 946 updateHeaderHour(hour, false); 947 updateHeaderMinute(minute, false); 948 if (!mIs24HourView) { 949 updateAmPmLabelStates(hour < 12 ? AM : PM); 950 } 951 setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true); 952 onValidationChanged(true); 953 } else { 954 boolean[] enteredZeros = {false, false}; 955 int[] values = getEnteredTime(enteredZeros); 956 String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; 957 String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; 958 String hourStr = (values[0] == -1) ? mDoublePlaceholderText : 959 String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); 960 String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : 961 String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); 962 mHourView.setText(hourStr); 963 mHourView.setSelected(false); 964 mMinuteView.setText(minuteStr); 965 mMinuteView.setSelected(false); 966 if (!mIs24HourView) { 967 updateAmPmLabelStates(values[2]); 968 } 969 } 970 } 971 972 private int getValFromKeyCode(int keyCode) { 973 switch (keyCode) { 974 case KeyEvent.KEYCODE_0: 975 return 0; 976 case KeyEvent.KEYCODE_1: 977 return 1; 978 case KeyEvent.KEYCODE_2: 979 return 2; 980 case KeyEvent.KEYCODE_3: 981 return 3; 982 case KeyEvent.KEYCODE_4: 983 return 4; 984 case KeyEvent.KEYCODE_5: 985 return 5; 986 case KeyEvent.KEYCODE_6: 987 return 6; 988 case KeyEvent.KEYCODE_7: 989 return 7; 990 case KeyEvent.KEYCODE_8: 991 return 8; 992 case KeyEvent.KEYCODE_9: 993 return 9; 994 default: 995 return -1; 996 } 997 } 998 999 /** 1000 * Get the currently-entered time, as integer values of the hours and minutes typed. 1001 * 1002 * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which 1003 * may then be used for the caller to know whether zeros had been explicitly entered as either 1004 * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. 1005 * 1006 * @return A size-3 int array. The first value will be the hours, the second value will be the 1007 * minutes, and the third will be either AM or PM. 1008 */ 1009 private int[] getEnteredTime(boolean[] enteredZeros) { 1010 int amOrPm = -1; 1011 int startIndex = 1; 1012 if (!mIs24HourView && isTypedTimeFullyLegal()) { 1013 int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); 1014 if (keyCode == getAmOrPmKeyCode(AM)) { 1015 amOrPm = AM; 1016 } else if (keyCode == getAmOrPmKeyCode(PM)){ 1017 amOrPm = PM; 1018 } 1019 startIndex = 2; 1020 } 1021 int minute = -1; 1022 int hour = -1; 1023 for (int i = startIndex; i <= mTypedTimes.size(); i++) { 1024 int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); 1025 if (i == startIndex) { 1026 minute = val; 1027 } else if (i == startIndex+1) { 1028 minute += 10 * val; 1029 if (enteredZeros != null && val == 0) { 1030 enteredZeros[1] = true; 1031 } 1032 } else if (i == startIndex+2) { 1033 hour = val; 1034 } else if (i == startIndex+3) { 1035 hour += 10 * val; 1036 if (enteredZeros != null && val == 0) { 1037 enteredZeros[0] = true; 1038 } 1039 } 1040 } 1041 1042 return new int[] { hour, minute, amOrPm }; 1043 } 1044 1045 /** 1046 * Get the keycode value for AM and PM in the current language. 1047 */ 1048 private int getAmOrPmKeyCode(int amOrPm) { 1049 // Cache the codes. 1050 if (mAmKeyCode == -1 || mPmKeyCode == -1) { 1051 // Find the first character in the AM/PM text that is unique. 1052 final KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); 1053 final CharSequence amText = mAmText.toLowerCase(mCurrentLocale); 1054 final CharSequence pmText = mPmText.toLowerCase(mCurrentLocale); 1055 final int N = Math.min(amText.length(), pmText.length()); 1056 for (int i = 0; i < N; i++) { 1057 final char amChar = amText.charAt(i); 1058 final char pmChar = pmText.charAt(i); 1059 if (amChar != pmChar) { 1060 // There should be 4 events: a down and up for both AM and PM. 1061 final KeyEvent[] events = kcm.getEvents(new char[] { amChar, pmChar }); 1062 if (events != null && events.length == 4) { 1063 mAmKeyCode = events[0].getKeyCode(); 1064 mPmKeyCode = events[2].getKeyCode(); 1065 } else { 1066 Log.e(TAG, "Unable to find keycodes for AM and PM."); 1067 } 1068 break; 1069 } 1070 } 1071 } 1072 1073 if (amOrPm == AM) { 1074 return mAmKeyCode; 1075 } else if (amOrPm == PM) { 1076 return mPmKeyCode; 1077 } 1078 1079 return -1; 1080 } 1081 1082 /** 1083 * Create a tree for deciding what keys can legally be typed. 1084 */ 1085 private void generateLegalTimesTree() { 1086 // Create a quick cache of numbers to their keycodes. 1087 final int k0 = KeyEvent.KEYCODE_0; 1088 final int k1 = KeyEvent.KEYCODE_1; 1089 final int k2 = KeyEvent.KEYCODE_2; 1090 final int k3 = KeyEvent.KEYCODE_3; 1091 final int k4 = KeyEvent.KEYCODE_4; 1092 final int k5 = KeyEvent.KEYCODE_5; 1093 final int k6 = KeyEvent.KEYCODE_6; 1094 final int k7 = KeyEvent.KEYCODE_7; 1095 final int k8 = KeyEvent.KEYCODE_8; 1096 final int k9 = KeyEvent.KEYCODE_9; 1097 1098 // The root of the tree doesn't contain any numbers. 1099 mLegalTimesTree = new Node(); 1100 if (mIs24HourView) { 1101 // We'll be re-using these nodes, so we'll save them. 1102 Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); 1103 Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1104 // The first digit must be followed by the second digit. 1105 minuteFirstDigit.addChild(minuteSecondDigit); 1106 1107 // The first digit may be 0-1. 1108 Node firstDigit = new Node(k0, k1); 1109 mLegalTimesTree.addChild(firstDigit); 1110 1111 // When the first digit is 0-1, the second digit may be 0-5. 1112 Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); 1113 firstDigit.addChild(secondDigit); 1114 // We may now be followed by the first minute digit. E.g. 00:09, 15:58. 1115 secondDigit.addChild(minuteFirstDigit); 1116 1117 // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. 1118 Node thirdDigit = new Node(k6, k7, k8, k9); 1119 // The time must now be finished. E.g. 0:55, 1:08. 1120 secondDigit.addChild(thirdDigit); 1121 1122 // When the first digit is 0-1, the second digit may be 6-9. 1123 secondDigit = new Node(k6, k7, k8, k9); 1124 firstDigit.addChild(secondDigit); 1125 // We must now be followed by the first minute digit. E.g. 06:50, 18:20. 1126 secondDigit.addChild(minuteFirstDigit); 1127 1128 // The first digit may be 2. 1129 firstDigit = new Node(k2); 1130 mLegalTimesTree.addChild(firstDigit); 1131 1132 // When the first digit is 2, the second digit may be 0-3. 1133 secondDigit = new Node(k0, k1, k2, k3); 1134 firstDigit.addChild(secondDigit); 1135 // We must now be followed by the first minute digit. E.g. 20:50, 23:09. 1136 secondDigit.addChild(minuteFirstDigit); 1137 1138 // When the first digit is 2, the second digit may be 4-5. 1139 secondDigit = new Node(k4, k5); 1140 firstDigit.addChild(secondDigit); 1141 // We must now be followd by the last minute digit. E.g. 2:40, 2:53. 1142 secondDigit.addChild(minuteSecondDigit); 1143 1144 // The first digit may be 3-9. 1145 firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); 1146 mLegalTimesTree.addChild(firstDigit); 1147 // We must now be followed by the first minute digit. E.g. 3:57, 8:12. 1148 firstDigit.addChild(minuteFirstDigit); 1149 } else { 1150 // We'll need to use the AM/PM node a lot. 1151 // Set up AM and PM to respond to "a" and "p". 1152 Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); 1153 1154 // The first hour digit may be 1. 1155 Node firstDigit = new Node(k1); 1156 mLegalTimesTree.addChild(firstDigit); 1157 // We'll allow quick input of on-the-hour times. E.g. 1pm. 1158 firstDigit.addChild(ampm); 1159 1160 // When the first digit is 1, the second digit may be 0-2. 1161 Node secondDigit = new Node(k0, k1, k2); 1162 firstDigit.addChild(secondDigit); 1163 // Also for quick input of on-the-hour times. E.g. 10pm, 12am. 1164 secondDigit.addChild(ampm); 1165 1166 // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. 1167 Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); 1168 secondDigit.addChild(thirdDigit); 1169 // The time may be finished now. E.g. 1:02pm, 1:25am. 1170 thirdDigit.addChild(ampm); 1171 1172 // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, 1173 // the fourth digit may be 0-9. 1174 Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1175 thirdDigit.addChild(fourthDigit); 1176 // The time must be finished now. E.g. 10:49am, 12:40pm. 1177 fourthDigit.addChild(ampm); 1178 1179 // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. 1180 thirdDigit = new Node(k6, k7, k8, k9); 1181 secondDigit.addChild(thirdDigit); 1182 // The time must be finished now. E.g. 1:08am, 1:26pm. 1183 thirdDigit.addChild(ampm); 1184 1185 // When the first digit is 1, the second digit may be 3-5. 1186 secondDigit = new Node(k3, k4, k5); 1187 firstDigit.addChild(secondDigit); 1188 1189 // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. 1190 thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1191 secondDigit.addChild(thirdDigit); 1192 // The time must be finished now. E.g. 1:39am, 1:50pm. 1193 thirdDigit.addChild(ampm); 1194 1195 // The hour digit may be 2-9. 1196 firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); 1197 mLegalTimesTree.addChild(firstDigit); 1198 // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. 1199 firstDigit.addChild(ampm); 1200 1201 // When the first digit is 2-9, the second digit may be 0-5. 1202 secondDigit = new Node(k0, k1, k2, k3, k4, k5); 1203 firstDigit.addChild(secondDigit); 1204 1205 // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. 1206 thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1207 secondDigit.addChild(thirdDigit); 1208 // The time must be finished now. E.g. 2:57am, 9:30pm. 1209 thirdDigit.addChild(ampm); 1210 } 1211 } 1212 1213 /** 1214 * Simple node class to be used for traversal to check for legal times. 1215 * mLegalKeys represents the keys that can be typed to get to the node. 1216 * mChildren are the children that can be reached from this node. 1217 */ 1218 private class Node { 1219 private int[] mLegalKeys; 1220 private ArrayList<Node> mChildren; 1221 1222 public Node(int... legalKeys) { 1223 mLegalKeys = legalKeys; 1224 mChildren = new ArrayList<Node>(); 1225 } 1226 1227 public void addChild(Node child) { 1228 mChildren.add(child); 1229 } 1230 1231 public boolean containsKey(int key) { 1232 for (int i = 0; i < mLegalKeys.length; i++) { 1233 if (mLegalKeys[i] == key) { 1234 return true; 1235 } 1236 } 1237 return false; 1238 } 1239 1240 public Node canReach(int key) { 1241 if (mChildren == null) { 1242 return null; 1243 } 1244 for (Node child : mChildren) { 1245 if (child.containsKey(key)) { 1246 return child; 1247 } 1248 } 1249 return null; 1250 } 1251 } 1252 1253 private final View.OnClickListener mClickListener = new View.OnClickListener() { 1254 @Override 1255 public void onClick(View v) { 1256 1257 final int amOrPm; 1258 switch (v.getId()) { 1259 case R.id.am_label: 1260 setAmOrPm(AM); 1261 break; 1262 case R.id.pm_label: 1263 setAmOrPm(PM); 1264 break; 1265 case R.id.hours: 1266 setCurrentItemShowing(HOUR_INDEX, true, true); 1267 break; 1268 case R.id.minutes: 1269 setCurrentItemShowing(MINUTE_INDEX, true, true); 1270 break; 1271 default: 1272 // Failed to handle this click, don't vibrate. 1273 return; 1274 } 1275 1276 tryVibrate(); 1277 } 1278 }; 1279 1280 private final View.OnKeyListener mKeyListener = new View.OnKeyListener() { 1281 @Override 1282 public boolean onKey(View v, int keyCode, KeyEvent event) { 1283 if (event.getAction() == KeyEvent.ACTION_UP) { 1284 return processKeyUp(keyCode); 1285 } 1286 return false; 1287 } 1288 }; 1289 1290 private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() { 1291 @Override 1292 public void onFocusChange(View v, boolean hasFocus) { 1293 if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) { 1294 finishKbMode(); 1295 1296 if (mOnTimeChangedListener != null) { 1297 mOnTimeChangedListener.onTimeChanged(mDelegator, 1298 mRadialTimePickerView.getCurrentHour(), 1299 mRadialTimePickerView.getCurrentMinute()); 1300 } 1301 } 1302 } 1303 }; 1304} 1305