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