TimePickerDialog.java revision b8f95646fc0510eebfeaa27864023d630f34090f
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.datetimepicker.time;
18
19import android.annotation.SuppressLint;
20import android.app.ActionBar.LayoutParams;
21import android.app.DialogFragment;
22import android.content.Context;
23import android.content.res.Resources;
24import android.os.Bundle;
25import android.util.Log;
26import android.view.KeyCharacterMap;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.View.OnClickListener;
31import android.view.View.OnKeyListener;
32import android.view.ViewGroup;
33import android.view.Window;
34import android.widget.RelativeLayout;
35import android.widget.TextView;
36
37import com.android.datetimepicker.R;
38
39import com.android.datetimepicker.time.RadialPickerLayout.OnValueSelectedListener;
40import com.android.datetimepicker.Utils;
41
42import java.text.DateFormatSymbols;
43import java.util.ArrayList;
44import java.util.Locale;
45
46/**
47 * Dialog to set a time.
48 */
49public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{
50    private static final String TAG = "TimePickerDialog";
51
52    private static final String KEY_HOUR_OF_DAY = "hour_of_day";
53    private static final String KEY_MINUTE = "minute";
54    private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view";
55    private static final String KEY_CURRENT_ITEM_SHOWING = "current_item_showing";
56    private static final String KEY_IN_KB_MODE = "in_kb_mode";
57    private static final String KEY_TYPED_TIMES = "typed_times";
58
59    public static final int HOUR_INDEX = 0;
60    public static final int MINUTE_INDEX = 1;
61    public static final int AMPM_INDEX = 2; // NOT a real index for the purpose of what's showing.
62    public static final int ENABLE_PICKER_INDEX = 3; // Also NOT a real index, just used for KB mode.
63    public static final int AM = 0;
64    public static final int PM = 1;
65
66    private OnTimeSetListener mCallback;
67
68    private TextView mDoneButton;
69    private TextView mHourView;
70    private TextView mMinuteView;
71    private TextView mAmPmTextView;
72    private View mAmPmHitspace;
73    private RadialPickerLayout mTimePicker;
74
75    private int mBlue;
76    private int mBlack;
77    private String mAmText;
78    private String mPmText;
79
80    private boolean mAllowAutoAdvance;
81    private int mInitialHourOfDay;
82    private int mInitialMinute;
83    private boolean mIs24HourMode;
84
85    // For hardware IME input.
86    private char mPlaceholderText;
87    private String mDoublePlaceholderText;
88    private boolean mInKbMode;
89    private ArrayList<Integer> mTypedTimes;
90    private Node mLegalTimesTree;
91    private int mAmKeyCode;
92    private int mPmKeyCode;
93
94    // Accessibility strings.
95    private String mHourPickerDescription;
96    private String mSelectHours;
97    private String mMinutePickerDescription;
98    private String mSelectMinutes;
99
100    /**
101     * The callback interface used to indicate the user is done filling in
102     * the time (they clicked on the 'Set' button).
103     */
104    public interface OnTimeSetListener {
105
106        /**
107         * @param view The view associated with this listener.
108         * @param hourOfDay The hour that was set.
109         * @param minute The minute that was set.
110         */
111        void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute);
112    }
113
114    public TimePickerDialog() {
115        // Empty constructor required for dialog fragment.
116    }
117
118    public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
119            int hourOfDay, int minute, boolean is24HourMode) {
120        // Empty constructor required for dialog fragment.
121    }
122
123    public static TimePickerDialog newInstance(OnTimeSetListener callback,
124            int hourOfDay, int minute, boolean is24HourMode) {
125        TimePickerDialog ret = new TimePickerDialog();
126        ret.initialize(callback, hourOfDay, minute, is24HourMode);
127        return ret;
128    }
129
130    public void initialize(OnTimeSetListener callback,
131            int hourOfDay, int minute, boolean is24HourMode) {
132        mCallback = callback;
133
134        mInitialHourOfDay = hourOfDay;
135        mInitialMinute = minute;
136        mIs24HourMode = is24HourMode;
137        mInKbMode = false;
138    }
139
140    public void setOnTimeSetListener(OnTimeSetListener callback) {
141        mCallback = callback;
142    }
143
144    @Override
145    public void onCreate(Bundle savedInstanceState) {
146        super.onCreate(savedInstanceState);
147        if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
148                    && savedInstanceState.containsKey(KEY_MINUTE)
149                    && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
150            mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
151            mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
152            mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
153            mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE);
154        }
155    }
156
157    @Override
158    public View onCreateView(LayoutInflater inflater, ViewGroup container,
159            Bundle savedInstanceState) {
160        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
161
162        View view = inflater.inflate(R.layout.time_picker_dialog, null);
163        KeyboardListener keyboardListener = new KeyboardListener();
164        view.findViewById(R.id.time_picker_dialog).setOnKeyListener(keyboardListener);
165
166        Resources res = getResources();
167        mHourPickerDescription = res.getString(R.string.hour_picker_description);
168        mSelectHours = res.getString(R.string.select_hours);
169        mMinutePickerDescription = res.getString(R.string.minute_picker_description);
170        mSelectMinutes = res.getString(R.string.select_minutes);
171        mBlue = res.getColor(R.color.blue);
172        mBlack = res.getColor(R.color.black_80);
173
174        mHourView = (TextView) view.findViewById(R.id.hours);
175        mHourView.setOnKeyListener(keyboardListener);
176        mMinuteView = (TextView) view.findViewById(R.id.minutes);
177        mMinuteView.setOnKeyListener(keyboardListener);
178        mAmPmTextView = (TextView) view.findViewById(R.id.ampm_label);
179        mAmPmTextView.setOnKeyListener(keyboardListener);
180        String[] amPmTexts = new DateFormatSymbols().getAmPmStrings();
181        mAmText = amPmTexts[0];
182        mPmText = amPmTexts[1];
183
184        mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker);
185        mTimePicker.setOnValueSelectedListener(this);
186        mTimePicker.setOnKeyListener(keyboardListener);
187        mTimePicker.initialize(getActivity(), mInitialHourOfDay, mInitialMinute, mIs24HourMode);
188        int currentItemShowing = HOUR_INDEX;
189        if (savedInstanceState != null &&
190                savedInstanceState.containsKey(KEY_CURRENT_ITEM_SHOWING)) {
191            currentItemShowing = savedInstanceState.getInt(KEY_CURRENT_ITEM_SHOWING);
192        }
193        setCurrentItemShowing(currentItemShowing, false);
194        mTimePicker.invalidate();
195
196        mHourView.setOnClickListener(new OnClickListener() {
197            @Override
198            public void onClick(View v) {
199                setCurrentItemShowing(HOUR_INDEX, true);
200                mTimePicker.tryVibrate();
201            }
202        });
203        mMinuteView.setOnClickListener(new OnClickListener() {
204            @Override
205            public void onClick(View v) {
206                setCurrentItemShowing(MINUTE_INDEX, true);
207                mTimePicker.tryVibrate();
208            }
209        });
210
211        mDoneButton = (TextView) view.findViewById(R.id.done_button);
212        mDoneButton.setOnClickListener(new OnClickListener() {
213            @Override
214            public void onClick(View v) {
215                if (mInKbMode && isTypedTimeFullyLegal()) {
216                    finishKbMode(false);
217                } else {
218                    mTimePicker.tryVibrate();
219                }
220                if (mCallback != null) {
221                    mCallback.onTimeSet(mTimePicker,
222                            mTimePicker.getHours(), mTimePicker.getMinutes());
223                }
224                dismiss();
225            }
226        });
227        mDoneButton.setOnKeyListener(keyboardListener);
228
229        mAmPmHitspace = view.findViewById(R.id.ampm_hitspace);
230        if (mIs24HourMode) {
231            mAmPmTextView.setVisibility(View.GONE);
232
233            RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams(
234                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
235            paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT);
236            TextView separatorView = (TextView) view.findViewById(R.id.separator);
237            separatorView.setLayoutParams(paramsSeparator);
238        } else {
239            mAmPmTextView.setVisibility(View.VISIBLE);
240            updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM);
241            mAmPmHitspace.setOnClickListener(new OnClickListener() {
242                @Override
243                public void onClick(View v) {
244                    mTimePicker.tryVibrate();
245                    int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
246                    if (amOrPm == AM) {
247                        amOrPm = PM;
248                    } else if (amOrPm == PM){
249                        amOrPm = AM;
250                    }
251                    updateAmPmDisplay(amOrPm);
252                    mTimePicker.setAmOrPm(amOrPm);
253                }
254            });
255        }
256
257        mAllowAutoAdvance = true;
258        setHour(mInitialHourOfDay);
259        setMinute(mInitialMinute);
260
261        mDoublePlaceholderText = res.getString(R.string.time_placeholder);
262        mPlaceholderText = mDoublePlaceholderText.charAt(0);
263        mAmKeyCode = mPmKeyCode = -1;
264        generateLegalTimesTree();
265        if (mInKbMode) {
266            mTypedTimes = savedInstanceState.getIntegerArrayList(KEY_TYPED_TIMES);
267            tryStartingKbMode(-1);
268            mHourView.invalidate();
269        } else if (mTypedTimes == null) {
270            mTypedTimes = new ArrayList<Integer>();
271        }
272
273        return view;
274    }
275
276    private void updateAmPmDisplay(int amOrPm) {
277        if (amOrPm == AM) {
278            mAmPmTextView.setText(mAmText);
279            tryAccessibilityAnnounce(mAmText);
280            mAmPmHitspace.setContentDescription(mAmText);
281        } else if (amOrPm == PM){
282            mAmPmTextView.setText(mPmText);
283            tryAccessibilityAnnounce(mPmText);
284            mAmPmHitspace.setContentDescription(mPmText);
285        } else {
286            mAmPmTextView.setText(mDoublePlaceholderText);
287        }
288    }
289
290    @Override
291    public void onSaveInstanceState(Bundle outState) {
292        if (mTimePicker != null) {
293            outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
294            outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
295            outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode);
296            outState.putInt(KEY_CURRENT_ITEM_SHOWING, mTimePicker.getCurrentItemShowing());
297            outState.putBoolean(KEY_IN_KB_MODE, mInKbMode);
298            if (mInKbMode) {
299                outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes);
300            }
301        }
302    }
303
304    @Override
305    public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
306        if (pickerIndex == HOUR_INDEX) {
307            setHour(newValue);
308            if (mAllowAutoAdvance && autoAdvance) {
309                setCurrentItemShowing(MINUTE_INDEX, true);
310            }
311        } else if (pickerIndex == MINUTE_INDEX){
312            setMinute(newValue);
313        } else if (pickerIndex == AMPM_INDEX) {
314            updateAmPmDisplay(newValue);
315        } else if (pickerIndex == ENABLE_PICKER_INDEX) {
316            if (!isTypedTimeFullyLegal()) {
317                mTypedTimes.clear();
318            }
319            finishKbMode(true);
320        }
321    }
322
323    private void setHour(int value) {
324        String format;
325        if (mIs24HourMode) {
326            format = "%02d";
327        } else {
328            format = "%d";
329            value = value % 12;
330            if (value == 0) {
331                value = 12;
332            }
333        }
334
335        CharSequence text = String.format(format, value);
336        tryAccessibilityAnnounce(text);
337        mHourView.setText(text);
338    }
339
340    private void setMinute(int value) {
341        if (value == 60) {
342            value = 0;
343        }
344        CharSequence text = String.format(Locale.getDefault(), "%02d", value);
345        tryAccessibilityAnnounce(text);
346        mMinuteView.setText(text);
347    }
348
349    private void setCurrentItemShowing(int index, boolean animate) {
350        mTimePicker.setCurrentItemShowing(index, animate);
351
352        if (index == HOUR_INDEX) {
353            int hours = mTimePicker.getHours();
354            if (!mIs24HourMode) {
355                hours = hours % 12;
356            }
357            mTimePicker.setContentDescription(mHourPickerDescription+": "+hours);
358            tryAccessibilityAnnounce(mSelectHours);
359        } else {
360            int minutes = mTimePicker.getMinutes();
361            mTimePicker.setContentDescription(mMinutePickerDescription+": "+minutes);
362            tryAccessibilityAnnounce(mSelectMinutes);
363        }
364
365        int hourColor = (index == HOUR_INDEX)? mBlue : mBlack;
366        int minuteColor = (index == MINUTE_INDEX)? mBlue : mBlack;
367        mHourView.setTextColor(hourColor);
368        mMinuteView.setTextColor(minuteColor);
369    }
370
371    @SuppressLint("NewApi")
372    private void tryAccessibilityAnnounce(CharSequence text) {
373        if (Utils.isJellybeanOrLater() && mTimePicker != null && text != null) {
374            mTimePicker.announceForAccessibility(text);
375        }
376    }
377
378    private boolean processKeyUp(int keyCode) {
379        if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
380            dismiss();
381            return true;
382        } else if (keyCode == KeyEvent.KEYCODE_TAB) {
383            if(mInKbMode) {
384                if (isTypedTimeFullyLegal()) {
385                    finishKbMode(true);
386                }
387                return true;
388            }
389        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
390            if (mInKbMode) {
391                if (!isTypedTimeFullyLegal()) {
392                    return true;
393                }
394                finishKbMode(false);
395            }
396            if (mCallback != null) {
397                mCallback.onTimeSet(mTimePicker,
398                        mTimePicker.getHours(), mTimePicker.getMinutes());
399            }
400            dismiss();
401            return true;
402        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
403            if (mInKbMode) {
404                if (!mTypedTimes.isEmpty()) {
405                    deleteLastTypedKey();
406                    updateDisplay(true);
407                }
408            }
409        } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1
410                || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3
411                || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5
412                || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
413                || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
414                || (!mIs24HourMode &&
415                        (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
416            if (!mInKbMode) {
417                if (mTimePicker == null) {
418                    // Something's wrong, because time picker should definitely not be null.
419                    Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null.");
420                    return true;
421                }
422                mTypedTimes.clear();
423                tryStartingKbMode(keyCode);
424                return true;
425            }
426            // We're already in keyboard mode.
427            if (addKeyIfLegal(keyCode)) {
428                updateDisplay(false);
429            }
430            return true;
431        }
432        return false;
433    }
434
435    private void tryStartingKbMode(int keyCode) {
436        if (mTimePicker.trySettingInputEnabled(false) && (keyCode == -1 || addKeyIfLegal(keyCode))) {
437            mInKbMode = true;
438            mDoneButton.setEnabled(false);
439            updateDisplay(false);
440        }
441    }
442
443    private boolean addKeyIfLegal(int keyCode) {
444        // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
445        // we'll need to see if AM/PM have been typed.
446        if ((mIs24HourMode && mTypedTimes.size() == 4) ||
447                (!mIs24HourMode && isTypedTimeFullyLegal())) {
448            return false;
449        }
450
451        mTypedTimes.add(keyCode);
452        if (!isTypedTimeLegalSoFar()) {
453            deleteLastTypedKey();
454            return false;
455        }
456
457        // Automatically fill in 0's if AM or PM was legally entered.
458        if (isTypedTimeFullyLegal()) {
459            if (!mIs24HourMode && mTypedTimes.size() <= 3) {
460                mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
461                mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
462            }
463            mDoneButton.setEnabled(true);
464        }
465
466        return true;
467    }
468
469    private boolean isTypedTimeLegalSoFar() {
470        Node node = mLegalTimesTree;
471        for (int keyCode : mTypedTimes) {
472            node = node.canReach(keyCode);
473            if (node == null) {
474                return false;
475            }
476        }
477        return true;
478    }
479
480    private boolean isTypedTimeFullyLegal() {
481        // The time is legal if it contains an AM or PM, as those can only be legally added at
482        // specific times based on the tree's algorithm.
483        if (mIs24HourMode) {
484            // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
485            int[] values = getEnteredTime(null);
486            return (values[0] >= 0 && values[1] >= 0 && values[1] < 60);
487        } else {
488            return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) ||
489                    mTypedTimes.contains(getAmOrPmKeyCode(PM)));
490        }
491    }
492
493    private void deleteLastTypedKey() {
494        mTypedTimes.remove(mTypedTimes.size() - 1);
495        if (!isTypedTimeFullyLegal()) {
496            mDoneButton.setEnabled(false);
497        }
498    }
499
500    private void finishKbMode(boolean changeDisplays) {
501        mInKbMode = false;
502        if (!mTypedTimes.isEmpty()) {
503            int values[] = getEnteredTime(null);
504            mTimePicker.setTime(values[0], values[1]);
505            if (!mIs24HourMode) {
506                mTimePicker.setAmOrPm(values[2]);
507            }
508            mTypedTimes.clear();
509        }
510        if (changeDisplays) {
511            updateDisplay(false);
512            mTimePicker.trySettingInputEnabled(true);
513        }
514    }
515
516    private void updateDisplay(boolean allowEmpty) {
517        if (!allowEmpty && mTypedTimes.isEmpty()) {
518            int hour = mTimePicker.getHours();
519            int minute = mTimePicker.getMinutes();
520            setHour(hour);
521            setMinute(minute);
522            if (!mIs24HourMode) {
523                updateAmPmDisplay(hour < 12? AM : PM);
524            }
525            setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true);
526            mDoneButton.setEnabled(true);
527        } else {
528            Boolean[] enteredZeros = {false, false};
529            int[] values = getEnteredTime(enteredZeros);
530            String hourFormat = enteredZeros[0]? "%02d" : "%2d";
531            String minuteFormat = (enteredZeros[1])? "%02d" : "%2d";
532            String hourStr = (values[0] == -1)? mDoublePlaceholderText :
533                String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
534            String minuteStr = (values[1] == -1)? mDoublePlaceholderText :
535                String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
536            mHourView.setText(hourStr);
537            mHourView.setTextColor(mBlack);
538            mMinuteView.setText(minuteStr);
539            mMinuteView.setTextColor(mBlack);
540            if (!mIs24HourMode) {
541                updateAmPmDisplay(values[2]);
542            }
543        }
544    }
545
546    private int getValFromKeyCode(int keyCode) {
547        switch (keyCode) {
548            case KeyEvent.KEYCODE_0:
549                return 0;
550            case KeyEvent.KEYCODE_1:
551                return 1;
552            case KeyEvent.KEYCODE_2:
553                return 2;
554            case KeyEvent.KEYCODE_3:
555                return 3;
556            case KeyEvent.KEYCODE_4:
557                return 4;
558            case KeyEvent.KEYCODE_5:
559                return 5;
560            case KeyEvent.KEYCODE_6:
561                return 6;
562            case KeyEvent.KEYCODE_7:
563                return 7;
564            case KeyEvent.KEYCODE_8:
565                return 8;
566            case KeyEvent.KEYCODE_9:
567                return 9;
568            default:
569                return -1;
570        }
571    }
572
573    private int[] getEnteredTime(Boolean[] enteredZeros) {
574        int amOrPm = -1;
575        int startIndex = 1;
576        if (!mIs24HourMode && isTypedTimeFullyLegal()) {
577            int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
578            if (keyCode == getAmOrPmKeyCode(AM)) {
579                amOrPm = AM;
580            } else if (keyCode == getAmOrPmKeyCode(PM)){
581                amOrPm = PM;
582            }
583            startIndex = 2;
584        }
585        int minute = -1;
586        int hour = -1;
587        for (int i = startIndex; i <= mTypedTimes.size(); i++) {
588            int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
589            if (i == startIndex) {
590                minute = val;
591            } else if (i == startIndex+1) {
592                minute += 10*val;
593                if (enteredZeros != null && val == 0) {
594                    enteredZeros[1] = true;
595                }
596            } else if (i == startIndex+2) {
597                hour = val;
598            } else if (i == startIndex+3) {
599                hour += 10*val;
600                if (enteredZeros != null && val == 0) {
601                    enteredZeros[0] = true;
602                }
603            }
604        }
605
606        int[] ret = {hour, minute, amOrPm};
607        return ret;
608    }
609
610    private int getAmOrPmKeyCode(int amOrPm) {
611        // Cache the codes.
612        if (mAmKeyCode == -1 || mPmKeyCode == -1) {
613            // Find the first character in the AM/PM text that is unique.
614            KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
615            char amChar;
616            char pmChar;
617            for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) {
618                amChar = mAmText.toLowerCase(Locale.getDefault()).charAt(i);
619                pmChar = mPmText.toLowerCase(Locale.getDefault()).charAt(i);
620                if (amChar != pmChar) {
621                    KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar});
622                    // There should be 4 events: a down and up for both AM and PM.
623                    if (events != null && events.length == 4) {
624                        mAmKeyCode = events[0].getKeyCode();
625                        mPmKeyCode = events[2].getKeyCode();
626                        Log.d(TAG, "am char: "+amChar+" keycode: "+mAmKeyCode);
627                        Log.d(TAG, "pm char: "+pmChar+" keycode: "+mPmKeyCode);
628                    } else {
629                        Log.d(TAG, "am char: "+amChar+" keycode: "+mAmKeyCode);
630                        Log.d(TAG, "pm char: "+pmChar+" keycode: "+mPmKeyCode);
631                        if (events != null) {
632                            for (int j = 0; j < events.length; j++) {
633                                Log.d(TAG, "event code: "+events[j].getKeyCode()+" events: "+events[j]);
634                            }
635                        }
636                        Log.e(TAG, "Unable to find keycodes for AM and PM.");
637                    }
638                    break;
639                }
640            }
641        }
642        if (amOrPm == AM) {
643            return mAmKeyCode;
644        } else if (amOrPm == PM) {
645            return mPmKeyCode;
646        }
647
648        return -1;
649    }
650
651    private void generateLegalTimesTree() {
652        // Create a quick cache of numbers to their keycodes.
653        int k0 = KeyEvent.KEYCODE_0;
654        int k1 = KeyEvent.KEYCODE_1;
655        int k2 = KeyEvent.KEYCODE_2;
656        int k3 = KeyEvent.KEYCODE_3;
657        int k4 = KeyEvent.KEYCODE_4;
658        int k5 = KeyEvent.KEYCODE_5;
659        int k6 = KeyEvent.KEYCODE_6;
660        int k7 = KeyEvent.KEYCODE_7;
661        int k8 = KeyEvent.KEYCODE_8;
662        int k9 = KeyEvent.KEYCODE_9;
663
664        // The root of the tree doesn't contain any numbers.
665        mLegalTimesTree = new Node();
666        if (mIs24HourMode) {
667            // We'll be re-using these nodes, so we'll save them.
668            Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5);
669            Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
670            // The first digit must be followed by the second digit.
671            minuteFirstDigit.addChild(minuteSecondDigit);
672
673            // The first digit may be 0-1.
674            Node firstDigit = new Node(k0, k1);
675            mLegalTimesTree.addChild(firstDigit);
676
677            // When the first digit is 0-1, the second digit may be 0-5.
678            Node secondDigit = new Node(k0, k1, k2, k3, k4, k5);
679            firstDigit.addChild(secondDigit);
680            // We may now be followed by the first minute digit. E.g. 00:09, 15:58.
681            secondDigit.addChild(minuteFirstDigit);
682
683            // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9.
684            Node thirdDigit = new Node(k6, k7, k8, k9);
685            // The time must now be finished. E.g. 0:55, 1:08.
686            secondDigit.addChild(thirdDigit);
687
688            // When the first digit is 0-1, the second digit may be 6-9.
689            secondDigit = new Node(k6, k7, k8, k9);
690            firstDigit.addChild(secondDigit);
691            // We must now be followed by the first minute digit. E.g. 06:50, 18:20.
692            secondDigit.addChild(minuteFirstDigit);
693
694            // The first digit may be 2.
695            firstDigit = new Node(k2);
696            mLegalTimesTree.addChild(firstDigit);
697
698            // When the first digit is 2, the second digit may be 0-3.
699            secondDigit = new Node(k0, k1, k2, k3);
700            firstDigit.addChild(secondDigit);
701            // We must now be followed by the first minute digit. E.g. 20:50, 23:09.
702            secondDigit.addChild(minuteFirstDigit);
703
704            // When the first digit is 2, the second digit may be 4-5.
705            secondDigit = new Node(k4, k5);
706            firstDigit.addChild(secondDigit);
707            // We must now be followd by the last minute digit. E.g. 2:40, 2:53.
708            secondDigit.addChild(minuteSecondDigit);
709
710            // The first digit may be 3-9.
711            firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9);
712            mLegalTimesTree.addChild(firstDigit);
713            // We must now be followed by the first minute digit. E.g. 3:57, 8:12.
714            firstDigit.addChild(minuteFirstDigit);
715        } else {
716            // We'll need to use the AM/PM node a lot.
717            // Set up AM and PM to respond to "a" and "p".
718            Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM));
719
720            // The first hour digit may be 1.
721            Node firstDigit = new Node(k1);
722            mLegalTimesTree.addChild(firstDigit);
723            // We'll allow quick input of on-the-hour times. E.g. 1pm.
724            firstDigit.addChild(ampm);
725
726            // When the first digit is 1, the second digit may be 0-2.
727            Node secondDigit = new Node(k0, k1, k2);
728            firstDigit.addChild(secondDigit);
729            // Also for quick input of on-the-hour times. E.g. 10pm, 12am.
730            secondDigit.addChild(ampm);
731
732            // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5.
733            Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5);
734            secondDigit.addChild(thirdDigit);
735            // The time may be finished now. E.g. 1:02pm, 1:25am.
736            thirdDigit.addChild(ampm);
737
738            // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5,
739            // the fourth digit may be 0-9.
740            Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
741            thirdDigit.addChild(fourthDigit);
742            // The time must be finished now. E.g. 10:49am, 12:40pm.
743            fourthDigit.addChild(ampm);
744
745            // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9.
746            thirdDigit = new Node(k6, k7, k8, k9);
747            secondDigit.addChild(thirdDigit);
748            // The time must be finished now. E.g. 1:08am, 1:26pm.
749            thirdDigit.addChild(ampm);
750
751            // When the first digit is 1, the second digit may be 3-5.
752            secondDigit = new Node(k3, k4, k5);
753            firstDigit.addChild(secondDigit);
754
755            // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9.
756            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
757            secondDigit.addChild(thirdDigit);
758            // The time must be finished now. E.g. 1:39am, 1:50pm.
759            thirdDigit.addChild(ampm);
760
761            // The hour digit may be 2-9.
762            firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9);
763            mLegalTimesTree.addChild(firstDigit);
764            // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm.
765            firstDigit.addChild(ampm);
766
767            // When the first digit is 2-9, the second digit may be 0-5.
768            secondDigit = new Node(k0, k1, k2, k3, k4, k5);
769            firstDigit.addChild(secondDigit);
770
771            // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9.
772            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
773            secondDigit.addChild(thirdDigit);
774            // The time must be finished now. E.g. 2:57am, 9:30pm.
775            thirdDigit.addChild(ampm);
776        }
777    }
778
779    private class Node {
780        private int[] mLegalKeys;
781        private ArrayList<Node> mChildren;
782
783        public Node(int... legalKeys) {
784            mLegalKeys = legalKeys;
785            mChildren = new ArrayList<Node>();
786        }
787
788        public void addChild(Node child) {
789            mChildren.add(child);
790        }
791
792        public boolean containsKey(int key) {
793            for (int i = 0; i < mLegalKeys.length; i++) {
794                if (mLegalKeys[i] == key) {
795                    return true;
796                }
797            }
798            return false;
799        }
800
801        public Node canReach(int key) {
802            if (mChildren == null) {
803                return null;
804            }
805            for (Node child : mChildren) {
806                if (child.containsKey(key)) {
807                    return child;
808                }
809            }
810            return null;
811        }
812    }
813
814    private class KeyboardListener implements OnKeyListener {
815        @Override
816        public boolean onKey(View v, int keyCode, KeyEvent event) {
817            if (event.getAction() == KeyEvent.ACTION_UP) {
818                return processKeyUp(keyCode);
819            }
820            return false;
821        }
822    }
823}
824