TimePickerDialog.java revision 1f129e23db2dc5837a856f7734b15a5a8be6be94
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.animation.ObjectAnimator;
20import android.app.ActionBar.LayoutParams;
21import android.app.DialogFragment;
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.os.Bundle;
27import android.util.Log;
28import android.view.KeyCharacterMap;
29import android.view.KeyEvent;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.View.OnClickListener;
33import android.view.View.OnKeyListener;
34import android.view.ViewGroup;
35import android.view.Window;
36import android.widget.RelativeLayout;
37import android.widget.TextView;
38
39import com.android.datetimepicker.HapticFeedbackController;
40import com.android.datetimepicker.R;
41import com.android.datetimepicker.Utils;
42import com.android.datetimepicker.time.RadialPickerLayout.OnValueSelectedListener;
43
44import java.text.DateFormatSymbols;
45import java.util.ArrayList;
46import java.util.Locale;
47
48/**
49 * Dialog to set a time.
50 */
51public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{
52    private static final String TAG = "TimePickerDialog";
53
54    private static final String KEY_HOUR_OF_DAY = "hour_of_day";
55    private static final String KEY_MINUTE = "minute";
56    private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view";
57    private static final String KEY_CURRENT_ITEM_SHOWING = "current_item_showing";
58    private static final String KEY_IN_KB_MODE = "in_kb_mode";
59    private static final String KEY_TYPED_TIMES = "typed_times";
60    private static final String KEY_DARK_THEME = "dark_theme";
61
62    public static final int HOUR_INDEX = 0;
63    public static final int MINUTE_INDEX = 1;
64    // NOT a real index for the purpose of what's showing.
65    public static final int AMPM_INDEX = 2;
66    // Also NOT a real index, just used for keyboard mode.
67    public static final int ENABLE_PICKER_INDEX = 3;
68    public static final int AM = 0;
69    public static final int PM = 1;
70
71    // Delay before starting the pulse animation, in ms.
72    private static final int PULSE_ANIMATOR_DELAY = 300;
73
74    private OnTimeSetListener mCallback;
75
76    private HapticFeedbackController mHapticFeedbackController;
77
78    private TextView mDoneButton;
79    private TextView mHourView;
80    private TextView mHourSpaceView;
81    private TextView mMinuteView;
82    private TextView mMinuteSpaceView;
83    private TextView mAmPmTextView;
84    private View mAmPmHitspace;
85    private RadialPickerLayout mTimePicker;
86
87    private int mSelectedColor;
88    private int mUnselectedColor;
89    private String mAmText;
90    private String mPmText;
91
92    private boolean mAllowAutoAdvance;
93    private int mInitialHourOfDay;
94    private int mInitialMinute;
95    private boolean mIs24HourMode;
96    private boolean mThemeDark;
97
98    // For hardware IME input.
99    private char mPlaceholderText;
100    private String mDoublePlaceholderText;
101    private String mDeletedKeyFormat;
102    private boolean mInKbMode;
103    private ArrayList<Integer> mTypedTimes;
104    private Node mLegalTimesTree;
105    private int mAmKeyCode;
106    private int mPmKeyCode;
107
108    // Accessibility strings.
109    private String mHourPickerDescription;
110    private String mSelectHours;
111    private String mMinutePickerDescription;
112    private String mSelectMinutes;
113
114    /**
115     * The callback interface used to indicate the user is done filling in
116     * the time (they clicked on the 'Set' button).
117     */
118    public interface OnTimeSetListener {
119
120        /**
121         * @param view The view associated with this listener.
122         * @param hourOfDay The hour that was set.
123         * @param minute The minute that was set.
124         */
125        void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute);
126    }
127
128    public TimePickerDialog() {
129        // Empty constructor required for dialog fragment.
130    }
131
132    public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
133            int hourOfDay, int minute, boolean is24HourMode) {
134        // Empty constructor required for dialog fragment.
135    }
136
137    public static TimePickerDialog newInstance(OnTimeSetListener callback,
138            int hourOfDay, int minute, boolean is24HourMode) {
139        TimePickerDialog ret = new TimePickerDialog();
140        ret.initialize(callback, hourOfDay, minute, is24HourMode);
141        return ret;
142    }
143
144    public void initialize(OnTimeSetListener callback,
145            int hourOfDay, int minute, boolean is24HourMode) {
146        mCallback = callback;
147
148        mInitialHourOfDay = hourOfDay;
149        mInitialMinute = minute;
150        mIs24HourMode = is24HourMode;
151        mInKbMode = false;
152        mThemeDark = false;
153    }
154
155    /**
156     * Set a dark or light theme. NOTE: this will only take effect for the next onCreateView.
157     */
158    public void setThemeDark(boolean dark) {
159        mThemeDark = dark;
160    }
161
162    public boolean isThemeDark() {
163        return mThemeDark;
164    }
165
166    public void setOnTimeSetListener(OnTimeSetListener callback) {
167        mCallback = callback;
168    }
169
170    public void setStartTime(int hourOfDay, int minute) {
171        mInitialHourOfDay = hourOfDay;
172        mInitialMinute = minute;
173        mInKbMode = false;
174    }
175
176    @Override
177    public void onCreate(Bundle savedInstanceState) {
178        super.onCreate(savedInstanceState);
179        if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
180                    && savedInstanceState.containsKey(KEY_MINUTE)
181                    && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
182            mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
183            mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
184            mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
185            mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE);
186            mThemeDark = savedInstanceState.getBoolean(KEY_DARK_THEME);
187        }
188    }
189
190    @Override
191    public View onCreateView(LayoutInflater inflater, ViewGroup container,
192            Bundle savedInstanceState) {
193        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
194
195        View view = inflater.inflate(R.layout.time_picker_dialog, null);
196        KeyboardListener keyboardListener = new KeyboardListener();
197        view.findViewById(R.id.time_picker_dialog).setOnKeyListener(keyboardListener);
198
199        Resources res = getResources();
200        mHourPickerDescription = res.getString(R.string.hour_picker_description);
201        mSelectHours = res.getString(R.string.select_hours);
202        mMinutePickerDescription = res.getString(R.string.minute_picker_description);
203        mSelectMinutes = res.getString(R.string.select_minutes);
204        mSelectedColor = res.getColor(mThemeDark? R.color.red : R.color.blue);
205        mUnselectedColor = res.getColor(mThemeDark? R.color.white : R.color.numbers_text_color);
206
207        mHourView = (TextView) view.findViewById(R.id.hours);
208        mHourView.setOnKeyListener(keyboardListener);
209        mHourSpaceView = (TextView) view.findViewById(R.id.hour_space);
210        mMinuteSpaceView = (TextView) view.findViewById(R.id.minutes_space);
211        mMinuteView = (TextView) view.findViewById(R.id.minutes);
212        mMinuteView.setOnKeyListener(keyboardListener);
213        mAmPmTextView = (TextView) view.findViewById(R.id.ampm_label);
214        mAmPmTextView.setOnKeyListener(keyboardListener);
215        String[] amPmTexts = new DateFormatSymbols().getAmPmStrings();
216        mAmText = amPmTexts[0];
217        mPmText = amPmTexts[1];
218
219        mHapticFeedbackController = new HapticFeedbackController(getActivity());
220
221        mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker);
222        mTimePicker.setOnValueSelectedListener(this);
223        mTimePicker.setOnKeyListener(keyboardListener);
224        mTimePicker.initialize(getActivity(), mHapticFeedbackController, mInitialHourOfDay,
225            mInitialMinute, mIs24HourMode);
226
227        int currentItemShowing = HOUR_INDEX;
228        if (savedInstanceState != null &&
229                savedInstanceState.containsKey(KEY_CURRENT_ITEM_SHOWING)) {
230            currentItemShowing = savedInstanceState.getInt(KEY_CURRENT_ITEM_SHOWING);
231        }
232        setCurrentItemShowing(currentItemShowing, false, true, true);
233        mTimePicker.invalidate();
234
235        mHourView.setOnClickListener(new OnClickListener() {
236            @Override
237            public void onClick(View v) {
238                setCurrentItemShowing(HOUR_INDEX, true, false, true);
239                tryVibrate();
240            }
241        });
242        mMinuteView.setOnClickListener(new OnClickListener() {
243            @Override
244            public void onClick(View v) {
245                setCurrentItemShowing(MINUTE_INDEX, true, false, true);
246                tryVibrate();
247            }
248        });
249
250        mDoneButton = (TextView) view.findViewById(R.id.done_button);
251        mDoneButton.setOnClickListener(new OnClickListener() {
252            @Override
253            public void onClick(View v) {
254                if (mInKbMode && isTypedTimeFullyLegal()) {
255                    finishKbMode(false);
256                } else {
257                    tryVibrate();
258                }
259                if (mCallback != null) {
260                    mCallback.onTimeSet(mTimePicker,
261                            mTimePicker.getHours(), mTimePicker.getMinutes());
262                }
263                dismiss();
264            }
265        });
266        mDoneButton.setOnKeyListener(keyboardListener);
267
268        // Enable or disable the AM/PM view.
269        mAmPmHitspace = view.findViewById(R.id.ampm_hitspace);
270        if (mIs24HourMode) {
271            mAmPmTextView.setVisibility(View.GONE);
272
273            RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams(
274                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
275            paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT);
276            TextView separatorView = (TextView) view.findViewById(R.id.separator);
277            separatorView.setLayoutParams(paramsSeparator);
278        } else {
279            mAmPmTextView.setVisibility(View.VISIBLE);
280            updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM);
281            mAmPmHitspace.setOnClickListener(new OnClickListener() {
282                @Override
283                public void onClick(View v) {
284                    tryVibrate();
285                    int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
286                    if (amOrPm == AM) {
287                        amOrPm = PM;
288                    } else if (amOrPm == PM){
289                        amOrPm = AM;
290                    }
291                    updateAmPmDisplay(amOrPm);
292                    mTimePicker.setAmOrPm(amOrPm);
293                }
294            });
295        }
296
297        mAllowAutoAdvance = true;
298        setHour(mInitialHourOfDay, true);
299        setMinute(mInitialMinute);
300
301        // Set up for keyboard mode.
302        mDoublePlaceholderText = res.getString(R.string.time_placeholder);
303        mDeletedKeyFormat = res.getString(R.string.deleted_key);
304        mPlaceholderText = mDoublePlaceholderText.charAt(0);
305        mAmKeyCode = mPmKeyCode = -1;
306        generateLegalTimesTree();
307        if (mInKbMode) {
308            mTypedTimes = savedInstanceState.getIntegerArrayList(KEY_TYPED_TIMES);
309            tryStartingKbMode(-1);
310            mHourView.invalidate();
311        } else if (mTypedTimes == null) {
312            mTypedTimes = new ArrayList<Integer>();
313        }
314
315        // Set the theme at the end so that the initialize()s above don't counteract the theme.
316        mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
317        // Prepare some colors to use.
318        int white = res.getColor(R.color.white);
319        int circleBackground = res.getColor(R.color.circle_background);
320        int line = res.getColor(R.color.line_background);
321        int timeDisplay = res.getColor(R.color.numbers_text_color);
322        ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
323        int doneBackground = R.drawable.done_background_color;
324
325        int darkGray = res.getColor(R.color.dark_gray);
326        int lightGray = res.getColor(R.color.light_gray);
327        int darkLine = res.getColor(R.color.line_dark);
328        ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark);
329        int darkDoneBackground = R.drawable.done_background_color_dark;
330
331        // Set the colors for each view based on the theme.
332        view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white);
333        view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white);
334        ((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay);
335        ((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay);
336        view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line);
337        mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor);
338        mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground);
339        mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground);
340        return view;
341    }
342
343    @Override
344    public void onResume() {
345        super.onResume();
346        mHapticFeedbackController.start();
347    }
348
349    @Override
350    public void onPause() {
351        super.onPause();
352        mHapticFeedbackController.stop();
353    }
354
355    public void tryVibrate() {
356        mHapticFeedbackController.tryVibrate();
357    }
358
359    private void updateAmPmDisplay(int amOrPm) {
360        if (amOrPm == AM) {
361            mAmPmTextView.setText(mAmText);
362            Utils.tryAccessibilityAnnounce(mTimePicker, mAmText);
363            mAmPmHitspace.setContentDescription(mAmText);
364        } else if (amOrPm == PM){
365            mAmPmTextView.setText(mPmText);
366            Utils.tryAccessibilityAnnounce(mTimePicker, mPmText);
367            mAmPmHitspace.setContentDescription(mPmText);
368        } else {
369            mAmPmTextView.setText(mDoublePlaceholderText);
370        }
371    }
372
373    @Override
374    public void onSaveInstanceState(Bundle outState) {
375        if (mTimePicker != null) {
376            outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
377            outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
378            outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode);
379            outState.putInt(KEY_CURRENT_ITEM_SHOWING, mTimePicker.getCurrentItemShowing());
380            outState.putBoolean(KEY_IN_KB_MODE, mInKbMode);
381            if (mInKbMode) {
382                outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes);
383            }
384            outState.putBoolean(KEY_DARK_THEME, mThemeDark);
385        }
386    }
387
388    /**
389     * Called by the picker for updating the header display.
390     */
391    @Override
392    public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
393        if (pickerIndex == HOUR_INDEX) {
394            setHour(newValue, false);
395            String announcement = String.format("%d", newValue);
396            if (mAllowAutoAdvance && autoAdvance) {
397                setCurrentItemShowing(MINUTE_INDEX, true, true, false);
398                announcement += ". " + mSelectMinutes;
399            } else {
400                mTimePicker.setContentDescription(mHourPickerDescription + ": " + newValue);
401            }
402
403            Utils.tryAccessibilityAnnounce(mTimePicker, announcement);
404        } else if (pickerIndex == MINUTE_INDEX){
405            setMinute(newValue);
406            mTimePicker.setContentDescription(mMinutePickerDescription + ": " + newValue);
407        } else if (pickerIndex == AMPM_INDEX) {
408            updateAmPmDisplay(newValue);
409        } else if (pickerIndex == ENABLE_PICKER_INDEX) {
410            if (!isTypedTimeFullyLegal()) {
411                mTypedTimes.clear();
412            }
413            finishKbMode(true);
414        }
415    }
416
417    private void setHour(int value, boolean announce) {
418        String format;
419        if (mIs24HourMode) {
420            format = "%02d";
421        } else {
422            format = "%d";
423            value = value % 12;
424            if (value == 0) {
425                value = 12;
426            }
427        }
428
429        CharSequence text = String.format(format, value);
430        mHourView.setText(text);
431        mHourSpaceView.setText(text);
432        if (announce) {
433            Utils.tryAccessibilityAnnounce(mTimePicker, text);
434        }
435    }
436
437    private void setMinute(int value) {
438        if (value == 60) {
439            value = 0;
440        }
441        CharSequence text = String.format(Locale.getDefault(), "%02d", value);
442        Utils.tryAccessibilityAnnounce(mTimePicker, text);
443        mMinuteView.setText(text);
444        mMinuteSpaceView.setText(text);
445    }
446
447    // Show either Hours or Minutes.
448    private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate,
449            boolean announce) {
450        mTimePicker.setCurrentItemShowing(index, animateCircle);
451
452        TextView labelToAnimate;
453        if (index == HOUR_INDEX) {
454            int hours = mTimePicker.getHours();
455            if (!mIs24HourMode) {
456                hours = hours % 12;
457            }
458            mTimePicker.setContentDescription(mHourPickerDescription + ": " + hours);
459            if (announce) {
460                Utils.tryAccessibilityAnnounce(mTimePicker, mSelectHours);
461            }
462            labelToAnimate = mHourView;
463        } else {
464            int minutes = mTimePicker.getMinutes();
465            mTimePicker.setContentDescription(mMinutePickerDescription + ": " + minutes);
466            if (announce) {
467                Utils.tryAccessibilityAnnounce(mTimePicker, mSelectMinutes);
468            }
469            labelToAnimate = mMinuteView;
470        }
471
472        int hourColor = (index == HOUR_INDEX)? mSelectedColor : mUnselectedColor;
473        int minuteColor = (index == MINUTE_INDEX)? mSelectedColor : mUnselectedColor;
474        mHourView.setTextColor(hourColor);
475        mMinuteView.setTextColor(minuteColor);
476
477        ObjectAnimator pulseAnimator = Utils.getPulseAnimator(labelToAnimate, 0.85f, 1.1f);
478        if (delayLabelAnimate) {
479            pulseAnimator.setStartDelay(PULSE_ANIMATOR_DELAY);
480        }
481        pulseAnimator.start();
482    }
483
484    /**
485     * For keyboard mode, processes key events.
486     * @param keyCode the pressed key.
487     * @return true if the key was successfully processed, false otherwise.
488     */
489    private boolean processKeyUp(int keyCode) {
490        if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
491            dismiss();
492            return true;
493        } else if (keyCode == KeyEvent.KEYCODE_TAB) {
494            if(mInKbMode) {
495                if (isTypedTimeFullyLegal()) {
496                    finishKbMode(true);
497                }
498                return true;
499            }
500        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
501            if (mInKbMode) {
502                if (!isTypedTimeFullyLegal()) {
503                    return true;
504                }
505                finishKbMode(false);
506            }
507            if (mCallback != null) {
508                mCallback.onTimeSet(mTimePicker,
509                        mTimePicker.getHours(), mTimePicker.getMinutes());
510            }
511            dismiss();
512            return true;
513        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
514            if (mInKbMode) {
515                if (!mTypedTimes.isEmpty()) {
516                    int deleted = deleteLastTypedKey();
517                    String deletedKeyStr;
518                    if (deleted == getAmOrPmKeyCode(AM)) {
519                        deletedKeyStr = mAmText;
520                    } else if (deleted == getAmOrPmKeyCode(PM)) {
521                        deletedKeyStr = mPmText;
522                    } else {
523                        deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
524                    }
525                    Utils.tryAccessibilityAnnounce(mTimePicker,
526                            String.format(mDeletedKeyFormat, deletedKeyStr));
527                    updateDisplay(true);
528                }
529            }
530        } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1
531                || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3
532                || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5
533                || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
534                || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
535                || (!mIs24HourMode &&
536                        (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
537            if (!mInKbMode) {
538                if (mTimePicker == null) {
539                    // Something's wrong, because time picker should definitely not be null.
540                    Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null.");
541                    return true;
542                }
543                mTypedTimes.clear();
544                tryStartingKbMode(keyCode);
545                return true;
546            }
547            // We're already in keyboard mode.
548            if (addKeyIfLegal(keyCode)) {
549                updateDisplay(false);
550            }
551            return true;
552        }
553        return false;
554    }
555
556    /**
557     * Try to start keyboard mode with the specified key, as long as the timepicker is not in the
558     * middle of a touch-event.
559     * @param keyCode The key to use as the first press. Keyboard mode will not be started if the
560     * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
561     * key.
562     */
563    private void tryStartingKbMode(int keyCode) {
564        if (mTimePicker.trySettingInputEnabled(false) &&
565                (keyCode == -1 || addKeyIfLegal(keyCode))) {
566            mInKbMode = true;
567            mDoneButton.setEnabled(false);
568            updateDisplay(false);
569        }
570    }
571
572    private boolean addKeyIfLegal(int keyCode) {
573        // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
574        // we'll need to see if AM/PM have been typed.
575        if ((mIs24HourMode && mTypedTimes.size() == 4) ||
576                (!mIs24HourMode && isTypedTimeFullyLegal())) {
577            return false;
578        }
579
580        mTypedTimes.add(keyCode);
581        if (!isTypedTimeLegalSoFar()) {
582            deleteLastTypedKey();
583            return false;
584        }
585
586        int val = getValFromKeyCode(keyCode);
587        Utils.tryAccessibilityAnnounce(mTimePicker, String.format("%d", val));
588        // Automatically fill in 0's if AM or PM was legally entered.
589        if (isTypedTimeFullyLegal()) {
590            if (!mIs24HourMode && mTypedTimes.size() <= 3) {
591                mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
592                mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
593            }
594            mDoneButton.setEnabled(true);
595        }
596
597        return true;
598    }
599
600    /**
601     * Traverse the tree to see if the keys that have been typed so far are legal as is,
602     * or may become legal as more keys are typed (excluding backspace).
603     */
604    private boolean isTypedTimeLegalSoFar() {
605        Node node = mLegalTimesTree;
606        for (int keyCode : mTypedTimes) {
607            node = node.canReach(keyCode);
608            if (node == null) {
609                return false;
610            }
611        }
612        return true;
613    }
614
615    /**
616     * Check if the time that has been typed so far is completely legal, as is.
617     */
618    private boolean isTypedTimeFullyLegal() {
619        if (mIs24HourMode) {
620            // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note:
621            // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
622            int[] values = getEnteredTime(null);
623            return (values[0] >= 0 && values[1] >= 0 && values[1] < 60);
624        } else {
625            // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be
626            // legally added at specific times based on the tree's algorithm.
627            return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) ||
628                    mTypedTimes.contains(getAmOrPmKeyCode(PM)));
629        }
630    }
631
632    private int deleteLastTypedKey() {
633        int deleted = mTypedTimes.remove(mTypedTimes.size() - 1);
634        if (!isTypedTimeFullyLegal()) {
635            mDoneButton.setEnabled(false);
636        }
637        return deleted;
638    }
639
640    /**
641     * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
642     * @param changeDisplays If true, update the displays with the relevant time.
643     */
644    private void finishKbMode(boolean updateDisplays) {
645        mInKbMode = false;
646        if (!mTypedTimes.isEmpty()) {
647            int values[] = getEnteredTime(null);
648            mTimePicker.setTime(values[0], values[1]);
649            if (!mIs24HourMode) {
650                mTimePicker.setAmOrPm(values[2]);
651            }
652            mTypedTimes.clear();
653        }
654        if (updateDisplays) {
655            updateDisplay(false);
656            mTimePicker.trySettingInputEnabled(true);
657        }
658    }
659
660    /**
661     * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is
662     * empty, either show an empty display (filled with the placeholder text), or update from the
663     * timepicker's values.
664     * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text.
665     * Otherwise, revert to the timepicker's values.
666     */
667    private void updateDisplay(boolean allowEmptyDisplay) {
668        if (!allowEmptyDisplay && mTypedTimes.isEmpty()) {
669            int hour = mTimePicker.getHours();
670            int minute = mTimePicker.getMinutes();
671            setHour(hour, true);
672            setMinute(minute);
673            if (!mIs24HourMode) {
674                updateAmPmDisplay(hour < 12? AM : PM);
675            }
676            setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true, true);
677            mDoneButton.setEnabled(true);
678        } else {
679            Boolean[] enteredZeros = {false, false};
680            int[] values = getEnteredTime(enteredZeros);
681            String hourFormat = enteredZeros[0]? "%02d" : "%2d";
682            String minuteFormat = (enteredZeros[1])? "%02d" : "%2d";
683            String hourStr = (values[0] == -1)? mDoublePlaceholderText :
684                String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
685            String minuteStr = (values[1] == -1)? mDoublePlaceholderText :
686                String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
687            mHourView.setText(hourStr);
688            mHourSpaceView.setText(hourStr);
689            mHourView.setTextColor(mUnselectedColor);
690            mMinuteView.setText(minuteStr);
691            mMinuteSpaceView.setText(minuteStr);
692            mMinuteView.setTextColor(mUnselectedColor);
693            if (!mIs24HourMode) {
694                updateAmPmDisplay(values[2]);
695            }
696        }
697    }
698
699    private int getValFromKeyCode(int keyCode) {
700        switch (keyCode) {
701            case KeyEvent.KEYCODE_0:
702                return 0;
703            case KeyEvent.KEYCODE_1:
704                return 1;
705            case KeyEvent.KEYCODE_2:
706                return 2;
707            case KeyEvent.KEYCODE_3:
708                return 3;
709            case KeyEvent.KEYCODE_4:
710                return 4;
711            case KeyEvent.KEYCODE_5:
712                return 5;
713            case KeyEvent.KEYCODE_6:
714                return 6;
715            case KeyEvent.KEYCODE_7:
716                return 7;
717            case KeyEvent.KEYCODE_8:
718                return 8;
719            case KeyEvent.KEYCODE_9:
720                return 9;
721            default:
722                return -1;
723        }
724    }
725
726    /**
727     * Get the currently-entered time, as integer values of the hours and minutes typed.
728     * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which
729     * may then be used for the caller to know whether zeros had been explicitly entered as either
730     * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
731     * @return A size-3 int array. The first value will be the hours, the second value will be the
732     * minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM.
733     */
734    private int[] getEnteredTime(Boolean[] enteredZeros) {
735        int amOrPm = -1;
736        int startIndex = 1;
737        if (!mIs24HourMode && isTypedTimeFullyLegal()) {
738            int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
739            if (keyCode == getAmOrPmKeyCode(AM)) {
740                amOrPm = AM;
741            } else if (keyCode == getAmOrPmKeyCode(PM)){
742                amOrPm = PM;
743            }
744            startIndex = 2;
745        }
746        int minute = -1;
747        int hour = -1;
748        for (int i = startIndex; i <= mTypedTimes.size(); i++) {
749            int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
750            if (i == startIndex) {
751                minute = val;
752            } else if (i == startIndex+1) {
753                minute += 10*val;
754                if (enteredZeros != null && val == 0) {
755                    enteredZeros[1] = true;
756                }
757            } else if (i == startIndex+2) {
758                hour = val;
759            } else if (i == startIndex+3) {
760                hour += 10*val;
761                if (enteredZeros != null && val == 0) {
762                    enteredZeros[0] = true;
763                }
764            }
765        }
766
767        int[] ret = {hour, minute, amOrPm};
768        return ret;
769    }
770
771    /**
772     * Get the keycode value for AM and PM in the current language.
773     */
774    private int getAmOrPmKeyCode(int amOrPm) {
775        // Cache the codes.
776        if (mAmKeyCode == -1 || mPmKeyCode == -1) {
777            // Find the first character in the AM/PM text that is unique.
778            KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
779            char amChar;
780            char pmChar;
781            for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) {
782                amChar = mAmText.toLowerCase(Locale.getDefault()).charAt(i);
783                pmChar = mPmText.toLowerCase(Locale.getDefault()).charAt(i);
784                if (amChar != pmChar) {
785                    KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar});
786                    // There should be 4 events: a down and up for both AM and PM.
787                    if (events != null && events.length == 4) {
788                        mAmKeyCode = events[0].getKeyCode();
789                        mPmKeyCode = events[2].getKeyCode();
790                    } else {
791                        Log.e(TAG, "Unable to find keycodes for AM and PM.");
792                    }
793                    break;
794                }
795            }
796        }
797        if (amOrPm == AM) {
798            return mAmKeyCode;
799        } else if (amOrPm == PM) {
800            return mPmKeyCode;
801        }
802
803        return -1;
804    }
805
806    /**
807     * Create a tree for deciding what keys can legally be typed.
808     */
809    private void generateLegalTimesTree() {
810        // Create a quick cache of numbers to their keycodes.
811        int k0 = KeyEvent.KEYCODE_0;
812        int k1 = KeyEvent.KEYCODE_1;
813        int k2 = KeyEvent.KEYCODE_2;
814        int k3 = KeyEvent.KEYCODE_3;
815        int k4 = KeyEvent.KEYCODE_4;
816        int k5 = KeyEvent.KEYCODE_5;
817        int k6 = KeyEvent.KEYCODE_6;
818        int k7 = KeyEvent.KEYCODE_7;
819        int k8 = KeyEvent.KEYCODE_8;
820        int k9 = KeyEvent.KEYCODE_9;
821
822        // The root of the tree doesn't contain any numbers.
823        mLegalTimesTree = new Node();
824        if (mIs24HourMode) {
825            // We'll be re-using these nodes, so we'll save them.
826            Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5);
827            Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
828            // The first digit must be followed by the second digit.
829            minuteFirstDigit.addChild(minuteSecondDigit);
830
831            // The first digit may be 0-1.
832            Node firstDigit = new Node(k0, k1);
833            mLegalTimesTree.addChild(firstDigit);
834
835            // When the first digit is 0-1, the second digit may be 0-5.
836            Node secondDigit = new Node(k0, k1, k2, k3, k4, k5);
837            firstDigit.addChild(secondDigit);
838            // We may now be followed by the first minute digit. E.g. 00:09, 15:58.
839            secondDigit.addChild(minuteFirstDigit);
840
841            // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9.
842            Node thirdDigit = new Node(k6, k7, k8, k9);
843            // The time must now be finished. E.g. 0:55, 1:08.
844            secondDigit.addChild(thirdDigit);
845
846            // When the first digit is 0-1, the second digit may be 6-9.
847            secondDigit = new Node(k6, k7, k8, k9);
848            firstDigit.addChild(secondDigit);
849            // We must now be followed by the first minute digit. E.g. 06:50, 18:20.
850            secondDigit.addChild(minuteFirstDigit);
851
852            // The first digit may be 2.
853            firstDigit = new Node(k2);
854            mLegalTimesTree.addChild(firstDigit);
855
856            // When the first digit is 2, the second digit may be 0-3.
857            secondDigit = new Node(k0, k1, k2, k3);
858            firstDigit.addChild(secondDigit);
859            // We must now be followed by the first minute digit. E.g. 20:50, 23:09.
860            secondDigit.addChild(minuteFirstDigit);
861
862            // When the first digit is 2, the second digit may be 4-5.
863            secondDigit = new Node(k4, k5);
864            firstDigit.addChild(secondDigit);
865            // We must now be followd by the last minute digit. E.g. 2:40, 2:53.
866            secondDigit.addChild(minuteSecondDigit);
867
868            // The first digit may be 3-9.
869            firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9);
870            mLegalTimesTree.addChild(firstDigit);
871            // We must now be followed by the first minute digit. E.g. 3:57, 8:12.
872            firstDigit.addChild(minuteFirstDigit);
873        } else {
874            // We'll need to use the AM/PM node a lot.
875            // Set up AM and PM to respond to "a" and "p".
876            Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM));
877
878            // The first hour digit may be 1.
879            Node firstDigit = new Node(k1);
880            mLegalTimesTree.addChild(firstDigit);
881            // We'll allow quick input of on-the-hour times. E.g. 1pm.
882            firstDigit.addChild(ampm);
883
884            // When the first digit is 1, the second digit may be 0-2.
885            Node secondDigit = new Node(k0, k1, k2);
886            firstDigit.addChild(secondDigit);
887            // Also for quick input of on-the-hour times. E.g. 10pm, 12am.
888            secondDigit.addChild(ampm);
889
890            // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5.
891            Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5);
892            secondDigit.addChild(thirdDigit);
893            // The time may be finished now. E.g. 1:02pm, 1:25am.
894            thirdDigit.addChild(ampm);
895
896            // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5,
897            // the fourth digit may be 0-9.
898            Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
899            thirdDigit.addChild(fourthDigit);
900            // The time must be finished now. E.g. 10:49am, 12:40pm.
901            fourthDigit.addChild(ampm);
902
903            // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9.
904            thirdDigit = new Node(k6, k7, k8, k9);
905            secondDigit.addChild(thirdDigit);
906            // The time must be finished now. E.g. 1:08am, 1:26pm.
907            thirdDigit.addChild(ampm);
908
909            // When the first digit is 1, the second digit may be 3-5.
910            secondDigit = new Node(k3, k4, k5);
911            firstDigit.addChild(secondDigit);
912
913            // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9.
914            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
915            secondDigit.addChild(thirdDigit);
916            // The time must be finished now. E.g. 1:39am, 1:50pm.
917            thirdDigit.addChild(ampm);
918
919            // The hour digit may be 2-9.
920            firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9);
921            mLegalTimesTree.addChild(firstDigit);
922            // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm.
923            firstDigit.addChild(ampm);
924
925            // When the first digit is 2-9, the second digit may be 0-5.
926            secondDigit = new Node(k0, k1, k2, k3, k4, k5);
927            firstDigit.addChild(secondDigit);
928
929            // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9.
930            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
931            secondDigit.addChild(thirdDigit);
932            // The time must be finished now. E.g. 2:57am, 9:30pm.
933            thirdDigit.addChild(ampm);
934        }
935    }
936
937    /**
938     * Simple node class to be used for traversal to check for legal times.
939     * mLegalKeys represents the keys that can be typed to get to the node.
940     * mChildren are the children that can be reached from this node.
941     */
942    private class Node {
943        private int[] mLegalKeys;
944        private ArrayList<Node> mChildren;
945
946        public Node(int... legalKeys) {
947            mLegalKeys = legalKeys;
948            mChildren = new ArrayList<Node>();
949        }
950
951        public void addChild(Node child) {
952            mChildren.add(child);
953        }
954
955        public boolean containsKey(int key) {
956            for (int i = 0; i < mLegalKeys.length; i++) {
957                if (mLegalKeys[i] == key) {
958                    return true;
959                }
960            }
961            return false;
962        }
963
964        public Node canReach(int key) {
965            if (mChildren == null) {
966                return null;
967            }
968            for (Node child : mChildren) {
969                if (child.containsKey(key)) {
970                    return child;
971                }
972            }
973            return null;
974        }
975    }
976
977    private class KeyboardListener implements OnKeyListener {
978        @Override
979        public boolean onKey(View v, int keyCode, KeyEvent event) {
980            if (event.getAction() == KeyEvent.ACTION_UP) {
981                return processKeyUp(keyCode);
982            }
983            return false;
984        }
985    }
986}
987