Home | History | Annotate | Download | only in widget
      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 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.ColorStateList;
     21 import android.content.res.Configuration;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.os.Parcel;
     25 import android.os.Parcelable;
     26 import android.text.TextUtils;
     27 import android.text.format.DateFormat;
     28 import android.text.format.DateUtils;
     29 import android.util.AttributeSet;
     30 import android.util.Log;
     31 import android.view.HapticFeedbackConstants;
     32 import android.view.KeyCharacterMap;
     33 import android.view.KeyEvent;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.view.accessibility.AccessibilityEvent;
     38 import android.view.accessibility.AccessibilityNodeInfo;
     39 
     40 import com.android.internal.R;
     41 
     42 import java.util.ArrayList;
     43 import java.util.Calendar;
     44 import java.util.Locale;
     45 
     46 /**
     47  * A view for selecting the time of day, in either 24 hour or AM/PM mode.
     48  */
     49 class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate implements
     50         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 
     72     private View mHeaderView;
     73     private TextView mHourView;
     74     private TextView mMinuteView;
     75     private TextView mAmPmTextView;
     76     private RadialTimePickerView mRadialTimePickerView;
     77     private TextView mSeparatorView;
     78 
     79     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 
     97     // Accessibility strings.
     98     private String mHourPickerDescription;
     99     private String mSelectHours;
    100     private String mMinutePickerDescription;
    101     private String mSelectMinutes;
    102 
    103     private Calendar mTempCalendar;
    104 
    105     public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
    106             int defStyleAttr, int defStyleRes) {
    107         super(delegator, context);
    108 
    109         // process style attributes
    110         final TypedArray a = mContext.obtainStyledAttributes(attrs,
    111                 R.styleable.TimePicker, defStyleAttr, defStyleRes);
    112         final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
    113                 Context.LAYOUT_INFLATER_SERVICE);
    114         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);
    120 
    121         String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(context);
    122         mAmText = amPmStrings[0];
    123         mPmText = amPmStrings[1];
    124 
    125         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);
    129 
    130         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);
    134 
    135         // Set up text appearances from style.
    136         final int headerTimeTextAppearance = a.getResourceId(
    137                 R.styleable.TimePicker_headerTimeTextAppearance, 0);
    138         if (headerTimeTextAppearance != 0) {
    139             mHourView.setTextAppearance(context, headerTimeTextAppearance);
    140             mSeparatorView.setTextAppearance(context, headerTimeTextAppearance);
    141             mMinuteView.setTextAppearance(context, headerTimeTextAppearance);
    142         }
    143 
    144         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);
    154         if (headerAmPmTextAppearance != 0) {
    155             mAmPmTextView.setTextAppearance(context, headerAmPmTextAppearance);
    156         }
    157 
    158         mHeaderView = mainView.findViewById(R.id.time_header);
    159         mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground));
    160 
    161         a.recycle();
    162 
    163         mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById(
    164                 R.id.radial_picker);
    165 
    166         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);
    181         initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX);
    182     }
    183 
    184     private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) {
    185         mInitialHourOfDay = hourOfDay;
    186         mInitialMinute = minute;
    187         mIs24HourView = is24HourView;
    188         mInKbMode = false;
    189         updateUI(index);
    190     }
    191 
    192     private void setupListeners() {
    193         mHeaderView.setOnKeyListener(mKeyListener);
    194         mHeaderView.setOnFocusChangeListener(mFocusListener);
    195         mHeaderView.setFocusable(true);
    196 
    197         mRadialTimePickerView.setOnValueSelectedListener(this);
    198 
    199         mHourView.setOnClickListener(new View.OnClickListener() {
    200             @Override
    201             public void onClick(View v) {
    202                 setCurrentItemShowing(HOUR_INDEX, true, true);
    203                 tryVibrate();
    204             }
    205         });
    206         mMinuteView.setOnClickListener(new View.OnClickListener() {
    207             @Override
    208             public void onClick(View v) {
    209                 setCurrentItemShowing(MINUTE_INDEX, true, true);
    210                 tryVibrate();
    211             }
    212         });
    213     }
    214 
    215     private void updateUI(int index) {
    216         // Update RadialPicker values
    217         updateRadialPicker(index);
    218         // Enable or disable the AM/PM view.
    219         updateHeaderAmPm();
    220         // 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);
    232         setCurrentItemShowing(index, false, true);
    233     }
    234 
    235     private int computeMaxWidthOfNumbers(int max) {
    236         TextView tempView = new TextView(mContext);
    237         tempView.setTextAppearance(mContext, R.style.TextAppearance_Material_TimePicker_TimeLabel);
    238         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 
    296     /**
    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
    403     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(),
    416                 is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing());
    417     }
    418 
    419     @Override
    420     public void onRestoreInstanceState(Parcelable state) {
    421         SavedState ss = (SavedState) state;
    422         setInKbMode(ss.inKbMode());
    423         setTypedTimes(ss.getTypesTimes());
    424         initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing());
    425         mRadialTimePickerView.invalidate();
    426         if (mInKbMode) {
    427             tryStartingKbMode(-1);
    428             mHourView.invalidate();
    429         }
    430     }
    431 
    432     @Override
    433     public void setCurrentLocale(Locale locale) {
    434         super.setCurrentLocale(locale);
    435         mTempCalendar = Calendar.getInstance(locale);
    436     }
    437 
    438     @Override
    439     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    440         onPopulateAccessibilityEvent(event);
    441         return true;
    442     }
    443 
    444     @Override
    445     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    446         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);
    457     }
    458 
    459     @Override
    460     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    461         event.setClassName(TimePicker.class.getName());
    462     }
    463 
    464     @Override
    465     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    466         info.setClassName(TimePicker.class.getName());
    467     }
    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 
    503     /**
    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;
    525 
    526         private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
    527                            boolean isKbMode, ArrayList<Integer> typedTimes,
    528                            int currentItemShowing) {
    529             super(superState);
    530             mHour = hour;
    531             mMinute = minute;
    532             mIs24HourMode = is24HourMode;
    533             mInKbMode = isKbMode;
    534             mTypedTimes = typedTimes;
    535             mCurrentItemShowing = currentItemShowing;
    536         }
    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();
    546         }
    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 
    572         @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);
    581         }
    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) {
    620                 setCurrentItemShowing(MINUTE_INDEX, true, false);
    621                 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             }
    637             finishKbMode();
    638         }
    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      */
    740     private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) {
    741         mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
    742 
    743         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             }
    752         } else {
    753             int minutes = mRadialTimePickerView.getCurrentMinute();
    754             mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes);
    755             if (announce) {
    756                 mRadialTimePickerView.announceForAccessibility(mSelectMinutes);
    757             }
    758         }
    759 
    760         mHourView.setSelected(index == HOUR_INDEX);
    761         mMinuteView.setSelected(index == MINUTE_INDEX);
    762     }
    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) {
    772         if (keyCode == KeyEvent.KEYCODE_DEL) {
    773             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;
    825             onValidationChanged(false);
    826             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             }
    853             onValidationChanged(true);
    854         }
    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()) {
    894             onValidationChanged(false);
    895         }
    896         return deleted;
    897     }
    898 
    899     /**
    900      * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
    901      */
    902     private void finishKbMode() {
    903         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         }
    913         updateDisplay(false);
    914         mRadialTimePickerView.setInputEnabled(true);
    915     }
    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             }
    934             setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true);
    935             onValidationChanged(true);
    936         } 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);
    946             mHourView.setSelected(false);
    947             mMinuteView.setText(minuteStr);
    948             mMinuteView.setSelected(false);
    949             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 
   1025         return new int[] { hour, minute, amOrPm };
   1026     }
   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 
   1234     private final View.OnKeyListener mKeyListener = new View.OnKeyListener() {
   1235         @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         }
   1242     };
   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     };
   1258 }
   1259