blob: 73e05e8d49329c072852ec1f15542461df0fc0c4 [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
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070019import android.content.Context;
Alan Viverette60727e02014-07-28 16:56:32 -070020import android.content.res.ColorStateList;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070021import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.TextUtils;
27import android.text.format.DateFormat;
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -080028import android.text.format.DateUtils;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070029import android.util.AttributeSet;
30import android.util.Log;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070031import android.view.HapticFeedbackConstants;
32import android.view.KeyCharacterMap;
33import android.view.KeyEvent;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
39
40import com.android.internal.R;
41
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070042import java.util.ArrayList;
43import java.util.Calendar;
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -080044import java.util.Locale;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070045
46/**
47 * A view for selecting the time of day, in either 24 hour or AM/PM mode.
48 */
Chet Haase3053b2f2014-08-06 07:51:50 -070049class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate implements
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070050 RadialTimePickerView.OnValueSelectedListener {
51
52 private static final String TAG = "TimePickerDelegate";
53
54 // Index used by RadialPickerLayout
55 private static final int HOUR_INDEX = 0;
56 private static final int MINUTE_INDEX = 1;
57
58 // NOT a real index for the purpose of what's showing.
59 private static final int AMPM_INDEX = 2;
60
61 // Also NOT a real index, just used for keyboard mode.
62 private static final int ENABLE_PICKER_INDEX = 3;
63
64 private static final int AM = 0;
65 private static final int PM = 1;
66
67 private static final boolean DEFAULT_ENABLED_STATE = true;
68 private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
69
70 private static final int HOURS_IN_HALF_DAY = 12;
71
Alan Viveretteba9bf412014-09-03 20:14:21 -070072 private View mHeaderView;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070073 private TextView mHourView;
74 private TextView mMinuteView;
75 private TextView mAmPmTextView;
76 private RadialTimePickerView mRadialTimePickerView;
77 private TextView mSeparatorView;
78
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070079 private String mAmText;
80 private String mPmText;
81
82 private boolean mAllowAutoAdvance;
83 private int mInitialHourOfDay;
84 private int mInitialMinute;
85 private boolean mIs24HourView;
86
87 // For hardware IME input.
88 private char mPlaceholderText;
89 private String mDoublePlaceholderText;
90 private String mDeletedKeyFormat;
91 private boolean mInKbMode;
92 private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>();
93 private Node mLegalTimesTree;
94 private int mAmKeyCode;
95 private int mPmKeyCode;
96
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070097 // Accessibility strings.
98 private String mHourPickerDescription;
99 private String mSelectHours;
100 private String mMinutePickerDescription;
101 private String mSelectMinutes;
102
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -0800103 private Calendar mTempCalendar;
104
Chet Haase3053b2f2014-08-06 07:51:50 -0700105 public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
106 int defStyleAttr, int defStyleRes) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700107 super(delegator, context);
108
109 // process style attributes
110 final TypedArray a = mContext.obtainStyledAttributes(attrs,
111 R.styleable.TimePicker, defStyleAttr, defStyleRes);
Alan Viverette60727e02014-07-28 16:56:32 -0700112 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
113 Context.LAYOUT_INFLATER_SERVICE);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700114 final Resources res = mContext.getResources();
115
116 mHourPickerDescription = res.getString(R.string.hour_picker_description);
117 mSelectHours = res.getString(R.string.select_hours);
118 mMinutePickerDescription = res.getString(R.string.minute_picker_description);
119 mSelectMinutes = res.getString(R.string.select_minutes);
Elliott Hughes1cc51a62014-08-21 16:21:30 -0700120
121 String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(context);
122 mAmText = amPmStrings[0];
123 mPmText = amPmStrings[1];
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700124
Alan Viverette60727e02014-07-28 16:56:32 -0700125 final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout,
126 R.layout.time_picker_holo);
127 final View mainView = inflater.inflate(layoutResourceId, null);
128 mDelegator.addView(mainView);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700129
Alan Viverette60727e02014-07-28 16:56:32 -0700130 mHourView = (TextView) mainView.findViewById(R.id.hours);
131 mSeparatorView = (TextView) mainView.findViewById(R.id.separator);
132 mMinuteView = (TextView) mainView.findViewById(R.id.minutes);
133 mAmPmTextView = (TextView) mainView.findViewById(R.id.ampm_label);
Alan Viverette51344782014-07-16 17:39:27 -0700134
135 // Set up text appearances from style.
Alan Viverette60727e02014-07-28 16:56:32 -0700136 final int headerTimeTextAppearance = a.getResourceId(
137 R.styleable.TimePicker_headerTimeTextAppearance, 0);
Alan Viverette51344782014-07-16 17:39:27 -0700138 if (headerTimeTextAppearance != 0) {
139 mHourView.setTextAppearance(context, headerTimeTextAppearance);
140 mSeparatorView.setTextAppearance(context, headerTimeTextAppearance);
141 mMinuteView.setTextAppearance(context, headerTimeTextAppearance);
142 }
143
Alan Viverette60727e02014-07-28 16:56:32 -0700144 final int headerSelectedTextColor = a.getColor(
145 R.styleable.TimePicker_headerSelectedTextColor,
146 res.getColor(R.color.timepicker_default_selector_color_material));
147 mHourView.setTextColor(ColorStateList.addFirstIfMissing(mHourView.getTextColors(),
148 R.attr.state_selected, headerSelectedTextColor));
149 mMinuteView.setTextColor(ColorStateList.addFirstIfMissing(mMinuteView.getTextColors(),
150 R.attr.state_selected, headerSelectedTextColor));
151
152 final int headerAmPmTextAppearance = a.getResourceId(
153 R.styleable.TimePicker_headerAmPmTextAppearance, 0);
Alan Viverette51344782014-07-16 17:39:27 -0700154 if (headerAmPmTextAppearance != 0) {
155 mAmPmTextView.setTextAppearance(context, headerAmPmTextAppearance);
156 }
157
Alan Viveretteba9bf412014-09-03 20:14:21 -0700158 mHeaderView = mainView.findViewById(R.id.time_header);
159 mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground));
Alan Viverette51344782014-07-16 17:39:27 -0700160
Alan Viverette60727e02014-07-28 16:56:32 -0700161 a.recycle();
Alan Viverette51344782014-07-16 17:39:27 -0700162
Alan Viverette60727e02014-07-28 16:56:32 -0700163 mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById(
164 R.id.radial_picker);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700165
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700166 setupListeners();
167
168 mAllowAutoAdvance = true;
169
170 // Set up for keyboard mode.
171 mDoublePlaceholderText = res.getString(R.string.time_placeholder);
172 mDeletedKeyFormat = res.getString(R.string.deleted_key);
173 mPlaceholderText = mDoublePlaceholderText.charAt(0);
174 mAmKeyCode = mPmKeyCode = -1;
175 generateLegalTimesTree();
176
177 // Initialize with current time
178 final Calendar calendar = Calendar.getInstance(mCurrentLocale);
179 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
180 final int currentMinute = calendar.get(Calendar.MINUTE);
Alan Viverette518ff0d2014-08-15 14:20:35 -0700181 initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700182 }
183
Alan Viverette518ff0d2014-08-15 14:20:35 -0700184 private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700185 mInitialHourOfDay = hourOfDay;
186 mInitialMinute = minute;
187 mIs24HourView = is24HourView;
188 mInKbMode = false;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700189 updateUI(index);
190 }
191
192 private void setupListeners() {
Alan Viveretteba9bf412014-09-03 20:14:21 -0700193 mHeaderView.setOnKeyListener(mKeyListener);
194 mHeaderView.setOnFocusChangeListener(mFocusListener);
195 mHeaderView.setFocusable(true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700196
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700197 mRadialTimePickerView.setOnValueSelectedListener(this);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700198
199 mHourView.setOnClickListener(new View.OnClickListener() {
200 @Override
201 public void onClick(View v) {
Alan Viverette7119d0d2014-08-25 17:27:02 -0700202 setCurrentItemShowing(HOUR_INDEX, true, true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700203 tryVibrate();
204 }
205 });
206 mMinuteView.setOnClickListener(new View.OnClickListener() {
207 @Override
208 public void onClick(View v) {
Alan Viverette7119d0d2014-08-25 17:27:02 -0700209 setCurrentItemShowing(MINUTE_INDEX, true, true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700210 tryVibrate();
211 }
212 });
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700213 }
214
215 private void updateUI(int index) {
216 // Update RadialPicker values
217 updateRadialPicker(index);
218 // Enable or disable the AM/PM view.
219 updateHeaderAmPm();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700220 // Update Hour and Minutes
221 updateHeaderHour(mInitialHourOfDay, true);
222 // Update time separator
223 updateHeaderSeparator();
224 // Update Minutes
225 updateHeaderMinute(mInitialMinute);
226 // Invalidate everything
227 mDelegator.invalidate();
228 }
229
230 private void updateRadialPicker(int index) {
231 mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView);
Alan Viverette7119d0d2014-08-25 17:27:02 -0700232 setCurrentItemShowing(index, false, true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700233 }
234
235 private int computeMaxWidthOfNumbers(int max) {
236 TextView tempView = new TextView(mContext);
Alan Viverette51344782014-07-16 17:39:27 -0700237 tempView.setTextAppearance(mContext, R.style.TextAppearance_Material_TimePicker_TimeLabel);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700238 ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
239 ViewGroup.LayoutParams.WRAP_CONTENT);
240 tempView.setLayoutParams(lp);
241 int maxWidth = 0;
242 for (int minutes = 0; minutes < max; minutes++) {
243 final String text = String.format("%02d", minutes);
244 tempView.setText(text);
245 tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
246 maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth());
247 }
248 return maxWidth;
249 }
250
251 private void updateHeaderAmPm() {
252 if (mIs24HourView) {
253 mAmPmTextView.setVisibility(View.GONE);
254 } else {
255 mAmPmTextView.setVisibility(View.VISIBLE);
256 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
257 "hm");
258
259 boolean amPmOnLeft = bestDateTimePattern.startsWith("a");
260 if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) ==
261 View.LAYOUT_DIRECTION_RTL) {
262 amPmOnLeft = !amPmOnLeft;
263 }
264
265 RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)
266 mAmPmTextView.getLayoutParams();
267
268 if (amPmOnLeft) {
269 layoutParams.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */);
270 layoutParams.removeRule(RelativeLayout.RIGHT_OF);
271 layoutParams.addRule(RelativeLayout.LEFT_OF, R.id.separator);
272 } else {
273 layoutParams.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */);
274 layoutParams.removeRule(RelativeLayout.LEFT_OF);
275 layoutParams.addRule(RelativeLayout.RIGHT_OF, R.id.separator);
276 }
277
278 updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM);
279 mAmPmTextView.setOnClickListener(new View.OnClickListener() {
280 @Override
281 public void onClick(View v) {
282 tryVibrate();
283 int amOrPm = mRadialTimePickerView.getAmOrPm();
284 if (amOrPm == AM) {
285 amOrPm = PM;
286 } else if (amOrPm == PM){
287 amOrPm = AM;
288 }
289 updateAmPmDisplay(amOrPm);
290 mRadialTimePickerView.setAmOrPm(amOrPm);
291 }
292 });
293 }
294 }
295
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700296 /**
297 * Set the current hour.
298 */
299 @Override
300 public void setCurrentHour(Integer currentHour) {
301 if (mInitialHourOfDay == currentHour) {
302 return;
303 }
304 mInitialHourOfDay = currentHour;
305 updateHeaderHour(currentHour, true /* accessibility announce */);
306 updateHeaderAmPm();
307 mRadialTimePickerView.setCurrentHour(currentHour);
308 mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM);
309 mDelegator.invalidate();
310 onTimeChanged();
311 }
312
313 /**
314 * @return The current hour in the range (0-23).
315 */
316 @Override
317 public Integer getCurrentHour() {
318 int currentHour = mRadialTimePickerView.getCurrentHour();
319 if (mIs24HourView) {
320 return currentHour;
321 } else {
322 switch(mRadialTimePickerView.getAmOrPm()) {
323 case PM:
324 return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
325 case AM:
326 default:
327 return currentHour % HOURS_IN_HALF_DAY;
328 }
329 }
330 }
331
332 /**
333 * Set the current minute (0-59).
334 */
335 @Override
336 public void setCurrentMinute(Integer currentMinute) {
337 if (mInitialMinute == currentMinute) {
338 return;
339 }
340 mInitialMinute = currentMinute;
341 updateHeaderMinute(currentMinute);
342 mRadialTimePickerView.setCurrentMinute(currentMinute);
343 mDelegator.invalidate();
344 onTimeChanged();
345 }
346
347 /**
348 * @return The current minute.
349 */
350 @Override
351 public Integer getCurrentMinute() {
352 return mRadialTimePickerView.getCurrentMinute();
353 }
354
355 /**
356 * Set whether in 24 hour or AM/PM mode.
357 *
358 * @param is24HourView True = 24 hour mode. False = AM/PM.
359 */
360 @Override
361 public void setIs24HourView(Boolean is24HourView) {
362 if (is24HourView == mIs24HourView) {
363 return;
364 }
365 mIs24HourView = is24HourView;
366 generateLegalTimesTree();
367 int hour = mRadialTimePickerView.getCurrentHour();
368 mInitialHourOfDay = hour;
369 updateHeaderHour(hour, false /* no accessibility announce */);
370 updateHeaderAmPm();
371 updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing());
372 mDelegator.invalidate();
373 }
374
375 /**
376 * @return true if this is in 24 hour view else false.
377 */
378 @Override
379 public boolean is24HourView() {
380 return mIs24HourView;
381 }
382
383 @Override
384 public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) {
385 mOnTimeChangedListener = callback;
386 }
387
388 @Override
389 public void setEnabled(boolean enabled) {
390 mHourView.setEnabled(enabled);
391 mMinuteView.setEnabled(enabled);
392 mAmPmTextView.setEnabled(enabled);
393 mRadialTimePickerView.setEnabled(enabled);
394 mIsEnabled = enabled;
395 }
396
397 @Override
398 public boolean isEnabled() {
399 return mIsEnabled;
400 }
401
402 @Override
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700403 public int getBaseline() {
404 // does not support baseline alignment
405 return -1;
406 }
407
408 @Override
409 public void onConfigurationChanged(Configuration newConfig) {
410 updateUI(mRadialTimePickerView.getCurrentItemShowing());
411 }
412
413 @Override
414 public Parcelable onSaveInstanceState(Parcelable superState) {
415 return new SavedState(superState, getCurrentHour(), getCurrentMinute(),
Alan Viverette518ff0d2014-08-15 14:20:35 -0700416 is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700417 }
418
419 @Override
420 public void onRestoreInstanceState(Parcelable state) {
421 SavedState ss = (SavedState) state;
422 setInKbMode(ss.inKbMode());
423 setTypedTimes(ss.getTypesTimes());
Alan Viverette518ff0d2014-08-15 14:20:35 -0700424 initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700425 mRadialTimePickerView.invalidate();
426 if (mInKbMode) {
427 tryStartingKbMode(-1);
428 mHourView.invalidate();
429 }
430 }
431
432 @Override
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -0800433 public void setCurrentLocale(Locale locale) {
434 super.setCurrentLocale(locale);
435 mTempCalendar = Calendar.getInstance(locale);
436 }
437
438 @Override
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700439 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -0800440 onPopulateAccessibilityEvent(event);
441 return true;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700442 }
443
444 @Override
445 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -0800446 int flags = DateUtils.FORMAT_SHOW_TIME;
447 if (mIs24HourView) {
448 flags |= DateUtils.FORMAT_24HOUR;
449 } else {
450 flags |= DateUtils.FORMAT_12HOUR;
451 }
452 mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
453 mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
454 String selectedDate = DateUtils.formatDateTime(mContext,
455 mTempCalendar.getTimeInMillis(), flags);
456 event.getText().add(selectedDate);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700457 }
458
459 @Override
460 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -0800461 event.setClassName(TimePicker.class.getName());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700462 }
463
464 @Override
465 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
Fabrice Di Meglio014b8cf2013-12-18 12:51:22 -0800466 info.setClassName(TimePicker.class.getName());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700467 }
468
469 /**
470 * Set whether in keyboard mode or not.
471 *
472 * @param inKbMode True means in keyboard mode.
473 */
474 private void setInKbMode(boolean inKbMode) {
475 mInKbMode = inKbMode;
476 }
477
478 /**
479 * @return true if in keyboard mode
480 */
481 private boolean inKbMode() {
482 return mInKbMode;
483 }
484
485 private void setTypedTimes(ArrayList<Integer> typeTimes) {
486 mTypedTimes = typeTimes;
487 }
488
489 /**
490 * @return an array of typed times
491 */
492 private ArrayList<Integer> getTypedTimes() {
493 return mTypedTimes;
494 }
495
496 /**
497 * @return the index of the current item showing
498 */
499 private int getCurrentItemShowing() {
500 return mRadialTimePickerView.getCurrentItemShowing();
501 }
502
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700503 /**
504 * Propagate the time change
505 */
506 private void onTimeChanged() {
507 mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
508 if (mOnTimeChangedListener != null) {
509 mOnTimeChangedListener.onTimeChanged(mDelegator,
510 getCurrentHour(), getCurrentMinute());
511 }
512 }
513
514 /**
515 * Used to save / restore state of time picker
516 */
517 private static class SavedState extends View.BaseSavedState {
518
519 private final int mHour;
520 private final int mMinute;
521 private final boolean mIs24HourMode;
522 private final boolean mInKbMode;
523 private final ArrayList<Integer> mTypedTimes;
524 private final int mCurrentItemShowing;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700525
526 private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
527 boolean isKbMode, ArrayList<Integer> typedTimes,
Alan Viverette518ff0d2014-08-15 14:20:35 -0700528 int currentItemShowing) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700529 super(superState);
530 mHour = hour;
531 mMinute = minute;
532 mIs24HourMode = is24HourMode;
533 mInKbMode = isKbMode;
534 mTypedTimes = typedTimes;
535 mCurrentItemShowing = currentItemShowing;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700536 }
537
538 private SavedState(Parcel in) {
539 super(in);
540 mHour = in.readInt();
541 mMinute = in.readInt();
542 mIs24HourMode = (in.readInt() == 1);
543 mInKbMode = (in.readInt() == 1);
544 mTypedTimes = in.readArrayList(getClass().getClassLoader());
545 mCurrentItemShowing = in.readInt();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700546 }
547
548 public int getHour() {
549 return mHour;
550 }
551
552 public int getMinute() {
553 return mMinute;
554 }
555
556 public boolean is24HourMode() {
557 return mIs24HourMode;
558 }
559
560 public boolean inKbMode() {
561 return mInKbMode;
562 }
563
564 public ArrayList<Integer> getTypesTimes() {
565 return mTypedTimes;
566 }
567
568 public int getCurrentItemShowing() {
569 return mCurrentItemShowing;
570 }
571
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700572 @Override
573 public void writeToParcel(Parcel dest, int flags) {
574 super.writeToParcel(dest, flags);
575 dest.writeInt(mHour);
576 dest.writeInt(mMinute);
577 dest.writeInt(mIs24HourMode ? 1 : 0);
578 dest.writeInt(mInKbMode ? 1 : 0);
579 dest.writeList(mTypedTimes);
580 dest.writeInt(mCurrentItemShowing);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700581 }
582
583 @SuppressWarnings({"unused", "hiding"})
584 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
585 public SavedState createFromParcel(Parcel in) {
586 return new SavedState(in);
587 }
588
589 public SavedState[] newArray(int size) {
590 return new SavedState[size];
591 }
592 };
593 }
594
595 private void tryVibrate() {
596 mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
597 }
598
599 private void updateAmPmDisplay(int amOrPm) {
600 if (amOrPm == AM) {
601 mAmPmTextView.setText(mAmText);
602 mRadialTimePickerView.announceForAccessibility(mAmText);
603 } else if (amOrPm == PM){
604 mAmPmTextView.setText(mPmText);
605 mRadialTimePickerView.announceForAccessibility(mPmText);
606 } else {
607 mAmPmTextView.setText(mDoublePlaceholderText);
608 }
609 }
610
611 /**
612 * Called by the picker for updating the header display.
613 */
614 @Override
615 public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
616 if (pickerIndex == HOUR_INDEX) {
617 updateHeaderHour(newValue, false);
618 String announcement = String.format("%d", newValue);
619 if (mAllowAutoAdvance && autoAdvance) {
Alan Viverette7119d0d2014-08-25 17:27:02 -0700620 setCurrentItemShowing(MINUTE_INDEX, true, false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700621 announcement += ". " + mSelectMinutes;
622 } else {
623 mRadialTimePickerView.setContentDescription(
624 mHourPickerDescription + ": " + newValue);
625 }
626
627 mRadialTimePickerView.announceForAccessibility(announcement);
628 } else if (pickerIndex == MINUTE_INDEX){
629 updateHeaderMinute(newValue);
630 mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue);
631 } else if (pickerIndex == AMPM_INDEX) {
632 updateAmPmDisplay(newValue);
633 } else if (pickerIndex == ENABLE_PICKER_INDEX) {
634 if (!isTypedTimeFullyLegal()) {
635 mTypedTimes.clear();
636 }
Alan Viveretteba9bf412014-09-03 20:14:21 -0700637 finishKbMode();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700638 }
639 }
640
641 private void updateHeaderHour(int value, boolean announce) {
642 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
643 (mIs24HourView) ? "Hm" : "hm");
644 final int lengthPattern = bestDateTimePattern.length();
645 boolean hourWithTwoDigit = false;
646 char hourFormat = '\0';
647 // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
648 // the hour format that we found.
649 for (int i = 0; i < lengthPattern; i++) {
650 final char c = bestDateTimePattern.charAt(i);
651 if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
652 hourFormat = c;
653 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
654 hourWithTwoDigit = true;
655 }
656 break;
657 }
658 }
659 final String format;
660 if (hourWithTwoDigit) {
661 format = "%02d";
662 } else {
663 format = "%d";
664 }
665 if (mIs24HourView) {
666 // 'k' means 1-24 hour
667 if (hourFormat == 'k' && value == 0) {
668 value = 24;
669 }
670 } else {
671 // 'K' means 0-11 hour
672 value = modulo12(value, hourFormat == 'K');
673 }
674 CharSequence text = String.format(format, value);
675 mHourView.setText(text);
676 if (announce) {
677 mRadialTimePickerView.announceForAccessibility(text);
678 }
679 }
680
681 private static int modulo12(int n, boolean startWithZero) {
682 int value = n % 12;
683 if (value == 0 && !startWithZero) {
684 value = 12;
685 }
686 return value;
687 }
688
689 /**
690 * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
691 *
692 * See http://unicode.org/cldr/trac/browser/trunk/common/main
693 *
694 * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
695 * separator as the character which is just after the hour marker in the returned pattern.
696 */
697 private void updateHeaderSeparator() {
698 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
699 (mIs24HourView) ? "Hm" : "hm");
700 final String separatorText;
701 // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats
702 final char[] hourFormats = {'H', 'h', 'K', 'k'};
703 int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats);
704 if (hIndex == -1) {
705 // Default case
706 separatorText = ":";
707 } else {
708 separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1));
709 }
710 mSeparatorView.setText(separatorText);
711 }
712
713 static private int lastIndexOfAny(String str, char[] any) {
714 final int lengthAny = any.length;
715 if (lengthAny > 0) {
716 for (int i = str.length() - 1; i >= 0; i--) {
717 char c = str.charAt(i);
718 for (int j = 0; j < lengthAny; j++) {
719 if (c == any[j]) {
720 return i;
721 }
722 }
723 }
724 }
725 return -1;
726 }
727
728 private void updateHeaderMinute(int value) {
729 if (value == 60) {
730 value = 0;
731 }
732 CharSequence text = String.format(mCurrentLocale, "%02d", value);
733 mRadialTimePickerView.announceForAccessibility(text);
734 mMinuteView.setText(text);
735 }
736
737 /**
738 * Show either Hours or Minutes.
739 */
Alan Viverette7119d0d2014-08-25 17:27:02 -0700740 private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700741 mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
742
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700743 if (index == HOUR_INDEX) {
744 int hours = mRadialTimePickerView.getCurrentHour();
745 if (!mIs24HourView) {
746 hours = hours % 12;
747 }
748 mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours);
749 if (announce) {
750 mRadialTimePickerView.announceForAccessibility(mSelectHours);
751 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700752 } else {
753 int minutes = mRadialTimePickerView.getCurrentMinute();
754 mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes);
755 if (announce) {
756 mRadialTimePickerView.announceForAccessibility(mSelectMinutes);
757 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700758 }
759
Alan Viverette60727e02014-07-28 16:56:32 -0700760 mHourView.setSelected(index == HOUR_INDEX);
761 mMinuteView.setSelected(index == MINUTE_INDEX);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700762 }
763
764 /**
765 * For keyboard mode, processes key events.
766 *
767 * @param keyCode the pressed key.
768 *
769 * @return true if the key was successfully processed, false otherwise.
770 */
771 private boolean processKeyUp(int keyCode) {
Alan Viveretteba9bf412014-09-03 20:14:21 -0700772 if (keyCode == KeyEvent.KEYCODE_DEL) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700773 if (mInKbMode) {
774 if (!mTypedTimes.isEmpty()) {
775 int deleted = deleteLastTypedKey();
776 String deletedKeyStr;
777 if (deleted == getAmOrPmKeyCode(AM)) {
778 deletedKeyStr = mAmText;
779 } else if (deleted == getAmOrPmKeyCode(PM)) {
780 deletedKeyStr = mPmText;
781 } else {
782 deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
783 }
784 mRadialTimePickerView.announceForAccessibility(
785 String.format(mDeletedKeyFormat, deletedKeyStr));
786 updateDisplay(true);
787 }
788 }
789 } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1
790 || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3
791 || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5
792 || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
793 || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
794 || (!mIs24HourView &&
795 (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
796 if (!mInKbMode) {
797 if (mRadialTimePickerView == null) {
798 // Something's wrong, because time picker should definitely not be null.
799 Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null.");
800 return true;
801 }
802 mTypedTimes.clear();
803 tryStartingKbMode(keyCode);
804 return true;
805 }
806 // We're already in keyboard mode.
807 if (addKeyIfLegal(keyCode)) {
808 updateDisplay(false);
809 }
810 return true;
811 }
812 return false;
813 }
814
815 /**
816 * Try to start keyboard mode with the specified key.
817 *
818 * @param keyCode The key to use as the first press. Keyboard mode will not be started if the
819 * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
820 * key.
821 */
822 private void tryStartingKbMode(int keyCode) {
823 if (keyCode == -1 || addKeyIfLegal(keyCode)) {
824 mInKbMode = true;
Alan Viverette518ff0d2014-08-15 14:20:35 -0700825 onValidationChanged(false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700826 updateDisplay(false);
827 mRadialTimePickerView.setInputEnabled(false);
828 }
829 }
830
831 private boolean addKeyIfLegal(int keyCode) {
832 // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
833 // we'll need to see if AM/PM have been typed.
834 if ((mIs24HourView && mTypedTimes.size() == 4) ||
835 (!mIs24HourView && isTypedTimeFullyLegal())) {
836 return false;
837 }
838
839 mTypedTimes.add(keyCode);
840 if (!isTypedTimeLegalSoFar()) {
841 deleteLastTypedKey();
842 return false;
843 }
844
845 int val = getValFromKeyCode(keyCode);
846 mRadialTimePickerView.announceForAccessibility(String.format("%d", val));
847 // Automatically fill in 0's if AM or PM was legally entered.
848 if (isTypedTimeFullyLegal()) {
849 if (!mIs24HourView && mTypedTimes.size() <= 3) {
850 mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
851 mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
852 }
Alan Viverette518ff0d2014-08-15 14:20:35 -0700853 onValidationChanged(true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700854 }
855
856 return true;
857 }
858
859 /**
860 * Traverse the tree to see if the keys that have been typed so far are legal as is,
861 * or may become legal as more keys are typed (excluding backspace).
862 */
863 private boolean isTypedTimeLegalSoFar() {
864 Node node = mLegalTimesTree;
865 for (int keyCode : mTypedTimes) {
866 node = node.canReach(keyCode);
867 if (node == null) {
868 return false;
869 }
870 }
871 return true;
872 }
873
874 /**
875 * Check if the time that has been typed so far is completely legal, as is.
876 */
877 private boolean isTypedTimeFullyLegal() {
878 if (mIs24HourView) {
879 // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note:
880 // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
881 int[] values = getEnteredTime(null);
882 return (values[0] >= 0 && values[1] >= 0 && values[1] < 60);
883 } else {
884 // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be
885 // legally added at specific times based on the tree's algorithm.
886 return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) ||
887 mTypedTimes.contains(getAmOrPmKeyCode(PM)));
888 }
889 }
890
891 private int deleteLastTypedKey() {
892 int deleted = mTypedTimes.remove(mTypedTimes.size() - 1);
893 if (!isTypedTimeFullyLegal()) {
Alan Viverette518ff0d2014-08-15 14:20:35 -0700894 onValidationChanged(false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700895 }
896 return deleted;
897 }
898
899 /**
900 * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700901 */
Alan Viveretteba9bf412014-09-03 20:14:21 -0700902 private void finishKbMode() {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700903 mInKbMode = false;
904 if (!mTypedTimes.isEmpty()) {
905 int values[] = getEnteredTime(null);
906 mRadialTimePickerView.setCurrentHour(values[0]);
907 mRadialTimePickerView.setCurrentMinute(values[1]);
908 if (!mIs24HourView) {
909 mRadialTimePickerView.setAmOrPm(values[2]);
910 }
911 mTypedTimes.clear();
912 }
Alan Viveretteba9bf412014-09-03 20:14:21 -0700913 updateDisplay(false);
914 mRadialTimePickerView.setInputEnabled(true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700915 }
916
917 /**
918 * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is
919 * empty, either show an empty display (filled with the placeholder text), or update from the
920 * timepicker's values.
921 *
922 * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text.
923 * Otherwise, revert to the timepicker's values.
924 */
925 private void updateDisplay(boolean allowEmptyDisplay) {
926 if (!allowEmptyDisplay && mTypedTimes.isEmpty()) {
927 int hour = mRadialTimePickerView.getCurrentHour();
928 int minute = mRadialTimePickerView.getCurrentMinute();
929 updateHeaderHour(hour, true);
930 updateHeaderMinute(minute);
931 if (!mIs24HourView) {
932 updateAmPmDisplay(hour < 12 ? AM : PM);
933 }
Alan Viverette7119d0d2014-08-25 17:27:02 -0700934 setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true);
Alan Viverette518ff0d2014-08-15 14:20:35 -0700935 onValidationChanged(true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700936 } else {
937 boolean[] enteredZeros = {false, false};
938 int[] values = getEnteredTime(enteredZeros);
939 String hourFormat = enteredZeros[0] ? "%02d" : "%2d";
940 String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d";
941 String hourStr = (values[0] == -1) ? mDoublePlaceholderText :
942 String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
943 String minuteStr = (values[1] == -1) ? mDoublePlaceholderText :
944 String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
945 mHourView.setText(hourStr);
Alan Viverette60727e02014-07-28 16:56:32 -0700946 mHourView.setSelected(false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700947 mMinuteView.setText(minuteStr);
Alan Viverette60727e02014-07-28 16:56:32 -0700948 mMinuteView.setSelected(false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700949 if (!mIs24HourView) {
950 updateAmPmDisplay(values[2]);
951 }
952 }
953 }
954
955 private int getValFromKeyCode(int keyCode) {
956 switch (keyCode) {
957 case KeyEvent.KEYCODE_0:
958 return 0;
959 case KeyEvent.KEYCODE_1:
960 return 1;
961 case KeyEvent.KEYCODE_2:
962 return 2;
963 case KeyEvent.KEYCODE_3:
964 return 3;
965 case KeyEvent.KEYCODE_4:
966 return 4;
967 case KeyEvent.KEYCODE_5:
968 return 5;
969 case KeyEvent.KEYCODE_6:
970 return 6;
971 case KeyEvent.KEYCODE_7:
972 return 7;
973 case KeyEvent.KEYCODE_8:
974 return 8;
975 case KeyEvent.KEYCODE_9:
976 return 9;
977 default:
978 return -1;
979 }
980 }
981
982 /**
983 * Get the currently-entered time, as integer values of the hours and minutes typed.
984 *
985 * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which
986 * may then be used for the caller to know whether zeros had been explicitly entered as either
987 * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
988 *
989 * @return A size-3 int array. The first value will be the hours, the second value will be the
990 * minutes, and the third will be either AM or PM.
991 */
992 private int[] getEnteredTime(boolean[] enteredZeros) {
993 int amOrPm = -1;
994 int startIndex = 1;
995 if (!mIs24HourView && isTypedTimeFullyLegal()) {
996 int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
997 if (keyCode == getAmOrPmKeyCode(AM)) {
998 amOrPm = AM;
999 } else if (keyCode == getAmOrPmKeyCode(PM)){
1000 amOrPm = PM;
1001 }
1002 startIndex = 2;
1003 }
1004 int minute = -1;
1005 int hour = -1;
1006 for (int i = startIndex; i <= mTypedTimes.size(); i++) {
1007 int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
1008 if (i == startIndex) {
1009 minute = val;
1010 } else if (i == startIndex+1) {
1011 minute += 10 * val;
1012 if (enteredZeros != null && val == 0) {
1013 enteredZeros[1] = true;
1014 }
1015 } else if (i == startIndex+2) {
1016 hour = val;
1017 } else if (i == startIndex+3) {
1018 hour += 10 * val;
1019 if (enteredZeros != null && val == 0) {
1020 enteredZeros[0] = true;
1021 }
1022 }
1023 }
1024
Alan Viverette60727e02014-07-28 16:56:32 -07001025 return new int[] { hour, minute, amOrPm };
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001026 }
1027
1028 /**
1029 * Get the keycode value for AM and PM in the current language.
1030 */
1031 private int getAmOrPmKeyCode(int amOrPm) {
1032 // Cache the codes.
1033 if (mAmKeyCode == -1 || mPmKeyCode == -1) {
1034 // Find the first character in the AM/PM text that is unique.
1035 KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
1036 char amChar;
1037 char pmChar;
1038 for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) {
1039 amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i);
1040 pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i);
1041 if (amChar != pmChar) {
1042 KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar});
1043 // There should be 4 events: a down and up for both AM and PM.
1044 if (events != null && events.length == 4) {
1045 mAmKeyCode = events[0].getKeyCode();
1046 mPmKeyCode = events[2].getKeyCode();
1047 } else {
1048 Log.e(TAG, "Unable to find keycodes for AM and PM.");
1049 }
1050 break;
1051 }
1052 }
1053 }
1054 if (amOrPm == AM) {
1055 return mAmKeyCode;
1056 } else if (amOrPm == PM) {
1057 return mPmKeyCode;
1058 }
1059
1060 return -1;
1061 }
1062
1063 /**
1064 * Create a tree for deciding what keys can legally be typed.
1065 */
1066 private void generateLegalTimesTree() {
1067 // Create a quick cache of numbers to their keycodes.
1068 final int k0 = KeyEvent.KEYCODE_0;
1069 final int k1 = KeyEvent.KEYCODE_1;
1070 final int k2 = KeyEvent.KEYCODE_2;
1071 final int k3 = KeyEvent.KEYCODE_3;
1072 final int k4 = KeyEvent.KEYCODE_4;
1073 final int k5 = KeyEvent.KEYCODE_5;
1074 final int k6 = KeyEvent.KEYCODE_6;
1075 final int k7 = KeyEvent.KEYCODE_7;
1076 final int k8 = KeyEvent.KEYCODE_8;
1077 final int k9 = KeyEvent.KEYCODE_9;
1078
1079 // The root of the tree doesn't contain any numbers.
1080 mLegalTimesTree = new Node();
1081 if (mIs24HourView) {
1082 // We'll be re-using these nodes, so we'll save them.
1083 Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5);
1084 Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1085 // The first digit must be followed by the second digit.
1086 minuteFirstDigit.addChild(minuteSecondDigit);
1087
1088 // The first digit may be 0-1.
1089 Node firstDigit = new Node(k0, k1);
1090 mLegalTimesTree.addChild(firstDigit);
1091
1092 // When the first digit is 0-1, the second digit may be 0-5.
1093 Node secondDigit = new Node(k0, k1, k2, k3, k4, k5);
1094 firstDigit.addChild(secondDigit);
1095 // We may now be followed by the first minute digit. E.g. 00:09, 15:58.
1096 secondDigit.addChild(minuteFirstDigit);
1097
1098 // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9.
1099 Node thirdDigit = new Node(k6, k7, k8, k9);
1100 // The time must now be finished. E.g. 0:55, 1:08.
1101 secondDigit.addChild(thirdDigit);
1102
1103 // When the first digit is 0-1, the second digit may be 6-9.
1104 secondDigit = new Node(k6, k7, k8, k9);
1105 firstDigit.addChild(secondDigit);
1106 // We must now be followed by the first minute digit. E.g. 06:50, 18:20.
1107 secondDigit.addChild(minuteFirstDigit);
1108
1109 // The first digit may be 2.
1110 firstDigit = new Node(k2);
1111 mLegalTimesTree.addChild(firstDigit);
1112
1113 // When the first digit is 2, the second digit may be 0-3.
1114 secondDigit = new Node(k0, k1, k2, k3);
1115 firstDigit.addChild(secondDigit);
1116 // We must now be followed by the first minute digit. E.g. 20:50, 23:09.
1117 secondDigit.addChild(minuteFirstDigit);
1118
1119 // When the first digit is 2, the second digit may be 4-5.
1120 secondDigit = new Node(k4, k5);
1121 firstDigit.addChild(secondDigit);
1122 // We must now be followd by the last minute digit. E.g. 2:40, 2:53.
1123 secondDigit.addChild(minuteSecondDigit);
1124
1125 // The first digit may be 3-9.
1126 firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9);
1127 mLegalTimesTree.addChild(firstDigit);
1128 // We must now be followed by the first minute digit. E.g. 3:57, 8:12.
1129 firstDigit.addChild(minuteFirstDigit);
1130 } else {
1131 // We'll need to use the AM/PM node a lot.
1132 // Set up AM and PM to respond to "a" and "p".
1133 Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM));
1134
1135 // The first hour digit may be 1.
1136 Node firstDigit = new Node(k1);
1137 mLegalTimesTree.addChild(firstDigit);
1138 // We'll allow quick input of on-the-hour times. E.g. 1pm.
1139 firstDigit.addChild(ampm);
1140
1141 // When the first digit is 1, the second digit may be 0-2.
1142 Node secondDigit = new Node(k0, k1, k2);
1143 firstDigit.addChild(secondDigit);
1144 // Also for quick input of on-the-hour times. E.g. 10pm, 12am.
1145 secondDigit.addChild(ampm);
1146
1147 // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5.
1148 Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5);
1149 secondDigit.addChild(thirdDigit);
1150 // The time may be finished now. E.g. 1:02pm, 1:25am.
1151 thirdDigit.addChild(ampm);
1152
1153 // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5,
1154 // the fourth digit may be 0-9.
1155 Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1156 thirdDigit.addChild(fourthDigit);
1157 // The time must be finished now. E.g. 10:49am, 12:40pm.
1158 fourthDigit.addChild(ampm);
1159
1160 // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9.
1161 thirdDigit = new Node(k6, k7, k8, k9);
1162 secondDigit.addChild(thirdDigit);
1163 // The time must be finished now. E.g. 1:08am, 1:26pm.
1164 thirdDigit.addChild(ampm);
1165
1166 // When the first digit is 1, the second digit may be 3-5.
1167 secondDigit = new Node(k3, k4, k5);
1168 firstDigit.addChild(secondDigit);
1169
1170 // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9.
1171 thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1172 secondDigit.addChild(thirdDigit);
1173 // The time must be finished now. E.g. 1:39am, 1:50pm.
1174 thirdDigit.addChild(ampm);
1175
1176 // The hour digit may be 2-9.
1177 firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9);
1178 mLegalTimesTree.addChild(firstDigit);
1179 // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm.
1180 firstDigit.addChild(ampm);
1181
1182 // When the first digit is 2-9, the second digit may be 0-5.
1183 secondDigit = new Node(k0, k1, k2, k3, k4, k5);
1184 firstDigit.addChild(secondDigit);
1185
1186 // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9.
1187 thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1188 secondDigit.addChild(thirdDigit);
1189 // The time must be finished now. E.g. 2:57am, 9:30pm.
1190 thirdDigit.addChild(ampm);
1191 }
1192 }
1193
1194 /**
1195 * Simple node class to be used for traversal to check for legal times.
1196 * mLegalKeys represents the keys that can be typed to get to the node.
1197 * mChildren are the children that can be reached from this node.
1198 */
1199 private class Node {
1200 private int[] mLegalKeys;
1201 private ArrayList<Node> mChildren;
1202
1203 public Node(int... legalKeys) {
1204 mLegalKeys = legalKeys;
1205 mChildren = new ArrayList<Node>();
1206 }
1207
1208 public void addChild(Node child) {
1209 mChildren.add(child);
1210 }
1211
1212 public boolean containsKey(int key) {
1213 for (int i = 0; i < mLegalKeys.length; i++) {
1214 if (mLegalKeys[i] == key) {
1215 return true;
1216 }
1217 }
1218 return false;
1219 }
1220
1221 public Node canReach(int key) {
1222 if (mChildren == null) {
1223 return null;
1224 }
1225 for (Node child : mChildren) {
1226 if (child.containsKey(key)) {
1227 return child;
1228 }
1229 }
1230 return null;
1231 }
1232 }
1233
Alan Viveretteba9bf412014-09-03 20:14:21 -07001234 private final View.OnKeyListener mKeyListener = new View.OnKeyListener() {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001235 @Override
1236 public boolean onKey(View v, int keyCode, KeyEvent event) {
1237 if (event.getAction() == KeyEvent.ACTION_UP) {
1238 return processKeyUp(keyCode);
1239 }
1240 return false;
1241 }
Alan Viveretteba9bf412014-09-03 20:14:21 -07001242 };
1243
1244 private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() {
1245 @Override
1246 public void onFocusChange(View v, boolean hasFocus) {
1247 if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) {
1248 finishKbMode();
1249
1250 if (mOnTimeChangedListener != null) {
1251 mOnTimeChangedListener.onTimeChanged(mDelegator,
1252 mRadialTimePickerView.getCurrentHour(),
1253 mRadialTimePickerView.getCurrentMinute());
1254 }
1255 }
1256 }
1257 };
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001258}