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