TimePickerClockDelegate.java revision f5b02eb7c43a96d3f70a3ecc81738b0dff86df13
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.format.DateFormat;
27import android.text.format.DateUtils;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.util.TypedValue;
31import android.view.HapticFeedbackConstants;
32import android.view.KeyCharacterMap;
33import android.view.KeyEvent;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.View.AccessibilityDelegate;
37import android.view.ViewGroup;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityNodeInfo;
40import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
41
42import com.android.internal.R;
43
44import java.util.ArrayList;
45import java.util.Calendar;
46import java.util.Locale;
47
48/**
49 * A delegate implementing the radial clock-based TimePicker.
50 */
51class 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            KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
1057            char amChar;
1058            char pmChar;
1059            for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) {
1060                amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i);
1061                pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i);
1062                if (amChar != pmChar) {
1063                    KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar});
1064                    // There should be 4 events: a down and up for both AM and PM.
1065                    if (events != null && events.length == 4) {
1066                        mAmKeyCode = events[0].getKeyCode();
1067                        mPmKeyCode = events[2].getKeyCode();
1068                    } else {
1069                        Log.e(TAG, "Unable to find keycodes for AM and PM.");
1070                    }
1071                    break;
1072                }
1073            }
1074        }
1075        if (amOrPm == AM) {
1076            return mAmKeyCode;
1077        } else if (amOrPm == PM) {
1078            return mPmKeyCode;
1079        }
1080
1081        return -1;
1082    }
1083
1084    /**
1085     * Create a tree for deciding what keys can legally be typed.
1086     */
1087    private void generateLegalTimesTree() {
1088        // Create a quick cache of numbers to their keycodes.
1089        final int k0 = KeyEvent.KEYCODE_0;
1090        final int k1 = KeyEvent.KEYCODE_1;
1091        final int k2 = KeyEvent.KEYCODE_2;
1092        final int k3 = KeyEvent.KEYCODE_3;
1093        final int k4 = KeyEvent.KEYCODE_4;
1094        final int k5 = KeyEvent.KEYCODE_5;
1095        final int k6 = KeyEvent.KEYCODE_6;
1096        final int k7 = KeyEvent.KEYCODE_7;
1097        final int k8 = KeyEvent.KEYCODE_8;
1098        final int k9 = KeyEvent.KEYCODE_9;
1099
1100        // The root of the tree doesn't contain any numbers.
1101        mLegalTimesTree = new Node();
1102        if (mIs24HourView) {
1103            // We'll be re-using these nodes, so we'll save them.
1104            Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5);
1105            Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1106            // The first digit must be followed by the second digit.
1107            minuteFirstDigit.addChild(minuteSecondDigit);
1108
1109            // The first digit may be 0-1.
1110            Node firstDigit = new Node(k0, k1);
1111            mLegalTimesTree.addChild(firstDigit);
1112
1113            // When the first digit is 0-1, the second digit may be 0-5.
1114            Node secondDigit = new Node(k0, k1, k2, k3, k4, k5);
1115            firstDigit.addChild(secondDigit);
1116            // We may now be followed by the first minute digit. E.g. 00:09, 15:58.
1117            secondDigit.addChild(minuteFirstDigit);
1118
1119            // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9.
1120            Node thirdDigit = new Node(k6, k7, k8, k9);
1121            // The time must now be finished. E.g. 0:55, 1:08.
1122            secondDigit.addChild(thirdDigit);
1123
1124            // When the first digit is 0-1, the second digit may be 6-9.
1125            secondDigit = new Node(k6, k7, k8, k9);
1126            firstDigit.addChild(secondDigit);
1127            // We must now be followed by the first minute digit. E.g. 06:50, 18:20.
1128            secondDigit.addChild(minuteFirstDigit);
1129
1130            // The first digit may be 2.
1131            firstDigit = new Node(k2);
1132            mLegalTimesTree.addChild(firstDigit);
1133
1134            // When the first digit is 2, the second digit may be 0-3.
1135            secondDigit = new Node(k0, k1, k2, k3);
1136            firstDigit.addChild(secondDigit);
1137            // We must now be followed by the first minute digit. E.g. 20:50, 23:09.
1138            secondDigit.addChild(minuteFirstDigit);
1139
1140            // When the first digit is 2, the second digit may be 4-5.
1141            secondDigit = new Node(k4, k5);
1142            firstDigit.addChild(secondDigit);
1143            // We must now be followd by the last minute digit. E.g. 2:40, 2:53.
1144            secondDigit.addChild(minuteSecondDigit);
1145
1146            // The first digit may be 3-9.
1147            firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9);
1148            mLegalTimesTree.addChild(firstDigit);
1149            // We must now be followed by the first minute digit. E.g. 3:57, 8:12.
1150            firstDigit.addChild(minuteFirstDigit);
1151        } else {
1152            // We'll need to use the AM/PM node a lot.
1153            // Set up AM and PM to respond to "a" and "p".
1154            Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM));
1155
1156            // The first hour digit may be 1.
1157            Node firstDigit = new Node(k1);
1158            mLegalTimesTree.addChild(firstDigit);
1159            // We'll allow quick input of on-the-hour times. E.g. 1pm.
1160            firstDigit.addChild(ampm);
1161
1162            // When the first digit is 1, the second digit may be 0-2.
1163            Node secondDigit = new Node(k0, k1, k2);
1164            firstDigit.addChild(secondDigit);
1165            // Also for quick input of on-the-hour times. E.g. 10pm, 12am.
1166            secondDigit.addChild(ampm);
1167
1168            // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5.
1169            Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5);
1170            secondDigit.addChild(thirdDigit);
1171            // The time may be finished now. E.g. 1:02pm, 1:25am.
1172            thirdDigit.addChild(ampm);
1173
1174            // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5,
1175            // the fourth digit may be 0-9.
1176            Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1177            thirdDigit.addChild(fourthDigit);
1178            // The time must be finished now. E.g. 10:49am, 12:40pm.
1179            fourthDigit.addChild(ampm);
1180
1181            // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9.
1182            thirdDigit = new Node(k6, k7, k8, k9);
1183            secondDigit.addChild(thirdDigit);
1184            // The time must be finished now. E.g. 1:08am, 1:26pm.
1185            thirdDigit.addChild(ampm);
1186
1187            // When the first digit is 1, the second digit may be 3-5.
1188            secondDigit = new Node(k3, k4, k5);
1189            firstDigit.addChild(secondDigit);
1190
1191            // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9.
1192            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1193            secondDigit.addChild(thirdDigit);
1194            // The time must be finished now. E.g. 1:39am, 1:50pm.
1195            thirdDigit.addChild(ampm);
1196
1197            // The hour digit may be 2-9.
1198            firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9);
1199            mLegalTimesTree.addChild(firstDigit);
1200            // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm.
1201            firstDigit.addChild(ampm);
1202
1203            // When the first digit is 2-9, the second digit may be 0-5.
1204            secondDigit = new Node(k0, k1, k2, k3, k4, k5);
1205            firstDigit.addChild(secondDigit);
1206
1207            // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9.
1208            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
1209            secondDigit.addChild(thirdDigit);
1210            // The time must be finished now. E.g. 2:57am, 9:30pm.
1211            thirdDigit.addChild(ampm);
1212        }
1213    }
1214
1215    /**
1216     * Simple node class to be used for traversal to check for legal times.
1217     * mLegalKeys represents the keys that can be typed to get to the node.
1218     * mChildren are the children that can be reached from this node.
1219     */
1220    private class Node {
1221        private int[] mLegalKeys;
1222        private ArrayList<Node> mChildren;
1223
1224        public Node(int... legalKeys) {
1225            mLegalKeys = legalKeys;
1226            mChildren = new ArrayList<Node>();
1227        }
1228
1229        public void addChild(Node child) {
1230            mChildren.add(child);
1231        }
1232
1233        public boolean containsKey(int key) {
1234            for (int i = 0; i < mLegalKeys.length; i++) {
1235                if (mLegalKeys[i] == key) {
1236                    return true;
1237                }
1238            }
1239            return false;
1240        }
1241
1242        public Node canReach(int key) {
1243            if (mChildren == null) {
1244                return null;
1245            }
1246            for (Node child : mChildren) {
1247                if (child.containsKey(key)) {
1248                    return child;
1249                }
1250            }
1251            return null;
1252        }
1253    }
1254
1255    private final View.OnClickListener mClickListener = new View.OnClickListener() {
1256        @Override
1257        public void onClick(View v) {
1258
1259            final int amOrPm;
1260            switch (v.getId()) {
1261                case R.id.am_label:
1262                    setAmOrPm(AM);
1263                    break;
1264                case R.id.pm_label:
1265                    setAmOrPm(PM);
1266                    break;
1267                case R.id.hours:
1268                    setCurrentItemShowing(HOUR_INDEX, true, true);
1269                    break;
1270                case R.id.minutes:
1271                    setCurrentItemShowing(MINUTE_INDEX, true, true);
1272                    break;
1273                default:
1274                    // Failed to handle this click, don't vibrate.
1275                    return;
1276            }
1277
1278            tryVibrate();
1279        }
1280    };
1281
1282    private final View.OnKeyListener mKeyListener = new View.OnKeyListener() {
1283        @Override
1284        public boolean onKey(View v, int keyCode, KeyEvent event) {
1285            if (event.getAction() == KeyEvent.ACTION_UP) {
1286                return processKeyUp(keyCode);
1287            }
1288            return false;
1289        }
1290    };
1291
1292    private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() {
1293        @Override
1294        public void onFocusChange(View v, boolean hasFocus) {
1295            if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) {
1296                finishKbMode();
1297
1298                if (mOnTimeChangedListener != null) {
1299                    mOnTimeChangedListener.onTimeChanged(mDelegator,
1300                            mRadialTimePickerView.getCurrentHour(),
1301                            mRadialTimePickerView.getCurrentMinute());
1302                }
1303            }
1304        }
1305    };
1306}
1307