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