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