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