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