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