1/*
2 * Copyright (C) 2007 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.annotation.IntDef;
20import android.annotation.IntRange;
21import android.annotation.NonNull;
22import android.annotation.TestApi;
23import android.annotation.Widget;
24import android.content.Context;
25import android.content.res.TypedArray;
26import android.icu.util.Calendar;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.util.MathUtils;
32import android.view.View;
33import android.view.ViewStructure;
34import android.view.accessibility.AccessibilityEvent;
35import android.view.autofill.AutofillManager;
36import android.view.autofill.AutofillValue;
37
38import com.android.internal.R;
39
40import libcore.icu.LocaleData;
41
42import java.lang.annotation.Retention;
43import java.lang.annotation.RetentionPolicy;
44import java.util.Locale;
45
46/**
47 * A widget for selecting the time of day, in either 24-hour or AM/PM mode.
48 * <p>
49 * For a dialog using this view, see {@link android.app.TimePickerDialog}. See
50 * the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
51 * guide for more information.
52 *
53 * @attr ref android.R.styleable#TimePicker_timePickerMode
54 */
55@Widget
56public class TimePicker extends FrameLayout {
57    private static final String LOG_TAG = TimePicker.class.getSimpleName();
58
59    /**
60     * Presentation mode for the Holo-style time picker that uses a set of
61     * {@link android.widget.NumberPicker}s.
62     *
63     * @see #getMode()
64     * @hide Visible for testing only.
65     */
66    @TestApi
67    public static final int MODE_SPINNER = 1;
68
69    /**
70     * Presentation mode for the Material-style time picker that uses a clock
71     * face.
72     *
73     * @see #getMode()
74     * @hide Visible for testing only.
75     */
76    @TestApi
77    public static final int MODE_CLOCK = 2;
78
79    /** @hide */
80    @IntDef(prefix = { "MODE_" }, value = {
81            MODE_SPINNER,
82            MODE_CLOCK
83    })
84    @Retention(RetentionPolicy.SOURCE)
85    public @interface TimePickerMode {}
86
87    private final TimePickerDelegate mDelegate;
88
89    @TimePickerMode
90    private final int mMode;
91
92    /**
93     * The callback interface used to indicate the time has been adjusted.
94     */
95    public interface OnTimeChangedListener {
96
97        /**
98         * @param view The view associated with this listener.
99         * @param hourOfDay The current hour.
100         * @param minute The current minute.
101         */
102        void onTimeChanged(TimePicker view, int hourOfDay, int minute);
103    }
104
105    public TimePicker(Context context) {
106        this(context, null);
107    }
108
109    public TimePicker(Context context, AttributeSet attrs) {
110        this(context, attrs, R.attr.timePickerStyle);
111    }
112
113    public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
114        this(context, attrs, defStyleAttr, 0);
115    }
116
117    public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
118        super(context, attrs, defStyleAttr, defStyleRes);
119
120        // DatePicker is important by default, unless app developer overrode attribute.
121        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
122            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
123        }
124
125        final TypedArray a = context.obtainStyledAttributes(
126                attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
127        final boolean isDialogMode = a.getBoolean(R.styleable.TimePicker_dialogMode, false);
128        final int requestedMode = a.getInt(R.styleable.TimePicker_timePickerMode, MODE_SPINNER);
129        a.recycle();
130
131        if (requestedMode == MODE_CLOCK && isDialogMode) {
132            // You want MODE_CLOCK? YOU CAN'T HANDLE MODE_CLOCK! Well, maybe
133            // you can depending on your screen size. Let's check...
134            mMode = context.getResources().getInteger(R.integer.time_picker_mode);
135        } else {
136            mMode = requestedMode;
137        }
138
139        switch (mMode) {
140            case MODE_CLOCK:
141                mDelegate = new TimePickerClockDelegate(
142                        this, context, attrs, defStyleAttr, defStyleRes);
143                break;
144            case MODE_SPINNER:
145            default:
146                mDelegate = new TimePickerSpinnerDelegate(
147                        this, context, attrs, defStyleAttr, defStyleRes);
148                break;
149        }
150        mDelegate.setAutoFillChangeListener((v, h, m) -> {
151            final AutofillManager afm = context.getSystemService(AutofillManager.class);
152            if (afm != null) {
153                afm.notifyValueChanged(this);
154            }
155        });
156    }
157
158    /**
159     * @return the picker's presentation mode, one of {@link #MODE_CLOCK} or
160     *         {@link #MODE_SPINNER}
161     * @attr ref android.R.styleable#TimePicker_timePickerMode
162     * @hide Visible for testing only.
163     */
164    @TimePickerMode
165    @TestApi
166    public int getMode() {
167        return mMode;
168    }
169
170    /**
171     * Sets the currently selected hour using 24-hour time.
172     *
173     * @param hour the hour to set, in the range (0-23)
174     * @see #getHour()
175     */
176    public void setHour(@IntRange(from = 0, to = 23) int hour) {
177        mDelegate.setHour(MathUtils.constrain(hour, 0, 23));
178    }
179
180    /**
181     * Returns the currently selected hour using 24-hour time.
182     *
183     * @return the currently selected hour, in the range (0-23)
184     * @see #setHour(int)
185     */
186    public int getHour() {
187        return mDelegate.getHour();
188    }
189
190    /**
191     * Sets the currently selected minute.
192     *
193     * @param minute the minute to set, in the range (0-59)
194     * @see #getMinute()
195     */
196    public void setMinute(@IntRange(from = 0, to = 59) int minute) {
197        mDelegate.setMinute(MathUtils.constrain(minute, 0, 59));
198    }
199
200    /**
201     * Returns the currently selected minute.
202     *
203     * @return the currently selected minute, in the range (0-59)
204     * @see #setMinute(int)
205     */
206    public int getMinute() {
207        return mDelegate.getMinute();
208    }
209
210    /**
211     * Sets the currently selected hour using 24-hour time.
212     *
213     * @param currentHour the hour to set, in the range (0-23)
214     * @deprecated Use {@link #setHour(int)}
215     */
216    @Deprecated
217    public void setCurrentHour(@NonNull Integer currentHour) {
218        setHour(currentHour);
219    }
220
221    /**
222     * @return the currently selected hour, in the range (0-23)
223     * @deprecated Use {@link #getHour()}
224     */
225    @NonNull
226    @Deprecated
227    public Integer getCurrentHour() {
228        return getHour();
229    }
230
231    /**
232     * Sets the currently selected minute.
233     *
234     * @param currentMinute the minute to set, in the range (0-59)
235     * @deprecated Use {@link #setMinute(int)}
236     */
237    @Deprecated
238    public void setCurrentMinute(@NonNull Integer currentMinute) {
239        setMinute(currentMinute);
240    }
241
242    /**
243     * @return the currently selected minute, in the range (0-59)
244     * @deprecated Use {@link #getMinute()}
245     */
246    @NonNull
247    @Deprecated
248    public Integer getCurrentMinute() {
249        return getMinute();
250    }
251
252    /**
253     * Sets whether this widget displays time in 24-hour mode or 12-hour mode
254     * with an AM/PM picker.
255     *
256     * @param is24HourView {@code true} to display in 24-hour mode,
257     *                     {@code false} for 12-hour mode with AM/PM
258     * @see #is24HourView()
259     */
260    public void setIs24HourView(@NonNull Boolean is24HourView) {
261        if (is24HourView == null) {
262            return;
263        }
264
265        mDelegate.setIs24Hour(is24HourView);
266    }
267
268    /**
269     * @return {@code true} if this widget displays time in 24-hour mode,
270     *         {@code false} otherwise}
271     * @see #setIs24HourView(Boolean)
272     */
273    public boolean is24HourView() {
274        return mDelegate.is24Hour();
275    }
276
277    /**
278     * Set the callback that indicates the time has been adjusted by the user.
279     *
280     * @param onTimeChangedListener the callback, should not be null.
281     */
282    public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
283        mDelegate.setOnTimeChangedListener(onTimeChangedListener);
284    }
285
286    @Override
287    public void setEnabled(boolean enabled) {
288        super.setEnabled(enabled);
289        mDelegate.setEnabled(enabled);
290    }
291
292    @Override
293    public boolean isEnabled() {
294        return mDelegate.isEnabled();
295    }
296
297    @Override
298    public int getBaseline() {
299        return mDelegate.getBaseline();
300    }
301
302    /**
303     * Validates whether current input by the user is a valid time based on the locale. TimePicker
304     * will show an error message to the user if the time is not valid.
305     *
306     * @return {@code true} if the input is valid, {@code false} otherwise
307     */
308    public boolean validateInput() {
309        return mDelegate.validateInput();
310    }
311
312    @Override
313    protected Parcelable onSaveInstanceState() {
314        Parcelable superState = super.onSaveInstanceState();
315        return mDelegate.onSaveInstanceState(superState);
316    }
317
318    @Override
319    protected void onRestoreInstanceState(Parcelable state) {
320        BaseSavedState ss = (BaseSavedState) state;
321        super.onRestoreInstanceState(ss.getSuperState());
322        mDelegate.onRestoreInstanceState(ss);
323    }
324
325    @Override
326    public CharSequence getAccessibilityClassName() {
327        return TimePicker.class.getName();
328    }
329
330    /** @hide */
331    @Override
332    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
333        return mDelegate.dispatchPopulateAccessibilityEvent(event);
334    }
335
336    /** @hide */
337    @TestApi
338    public View getHourView() {
339        return mDelegate.getHourView();
340    }
341
342    /** @hide */
343    @TestApi
344    public View getMinuteView() {
345        return mDelegate.getMinuteView();
346    }
347
348    /** @hide */
349    @TestApi
350    public View getAmView() {
351        return mDelegate.getAmView();
352    }
353
354    /** @hide */
355    @TestApi
356    public View getPmView() {
357        return mDelegate.getPmView();
358    }
359
360    /**
361     * A delegate interface that defined the public API of the TimePicker. Allows different
362     * TimePicker implementations. This would need to be implemented by the TimePicker delegates
363     * for the real behavior.
364     */
365    interface TimePickerDelegate {
366        void setHour(@IntRange(from = 0, to = 23) int hour);
367        int getHour();
368
369        void setMinute(@IntRange(from = 0, to = 59) int minute);
370        int getMinute();
371
372        void setDate(@IntRange(from = 0, to = 23) int hour,
373                @IntRange(from = 0, to = 59) int minute);
374
375        void autofill(AutofillValue value);
376        AutofillValue getAutofillValue();
377
378        void setIs24Hour(boolean is24Hour);
379        boolean is24Hour();
380
381        boolean validateInput();
382
383        void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener);
384        void setAutoFillChangeListener(OnTimeChangedListener autoFillChangeListener);
385
386        void setEnabled(boolean enabled);
387        boolean isEnabled();
388
389        int getBaseline();
390
391        Parcelable onSaveInstanceState(Parcelable superState);
392        void onRestoreInstanceState(Parcelable state);
393
394        boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
395        void onPopulateAccessibilityEvent(AccessibilityEvent event);
396
397        /** @hide */
398        @TestApi View getHourView();
399
400        /** @hide */
401        @TestApi View getMinuteView();
402
403        /** @hide */
404        @TestApi View getAmView();
405
406        /** @hide */
407        @TestApi View getPmView();
408    }
409
410    static String[] getAmPmStrings(Context context) {
411        final Locale locale = context.getResources().getConfiguration().locale;
412        final LocaleData d = LocaleData.get(locale);
413
414        final String[] result = new String[2];
415        result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
416        result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
417        return result;
418    }
419
420    /**
421     * An abstract class which can be used as a start for TimePicker implementations
422     */
423    abstract static class AbstractTimePickerDelegate implements TimePickerDelegate {
424        protected final TimePicker mDelegator;
425        protected final Context mContext;
426        protected final Locale mLocale;
427
428        protected OnTimeChangedListener mOnTimeChangedListener;
429        protected OnTimeChangedListener mAutoFillChangeListener;
430
431        // The value that was passed to autofill() - it must be stored because it getAutofillValue()
432        // must return the exact same value that was autofilled, otherwise the widget will not be
433        // properly highlighted after autofill().
434        private long mAutofilledValue;
435
436        public AbstractTimePickerDelegate(@NonNull TimePicker delegator, @NonNull Context context) {
437            mDelegator = delegator;
438            mContext = context;
439            mLocale = context.getResources().getConfiguration().locale;
440        }
441
442        @Override
443        public void setOnTimeChangedListener(OnTimeChangedListener callback) {
444            mOnTimeChangedListener = callback;
445        }
446
447        @Override
448        public void setAutoFillChangeListener(OnTimeChangedListener callback) {
449            mAutoFillChangeListener = callback;
450        }
451
452        @Override
453        public final void autofill(AutofillValue value) {
454            if (value == null || !value.isDate()) {
455                Log.w(LOG_TAG, value + " could not be autofilled into " + this);
456                return;
457            }
458
459            final long time = value.getDateValue();
460
461            final Calendar cal = Calendar.getInstance(mLocale);
462            cal.setTimeInMillis(time);
463            setDate(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
464
465            // Must set mAutofilledValue *after* calling subclass method to make sure the value
466            // returned by getAutofillValue() matches it.
467            mAutofilledValue = time;
468        }
469
470        @Override
471        public final AutofillValue getAutofillValue() {
472            if (mAutofilledValue != 0) {
473                return AutofillValue.forDate(mAutofilledValue);
474            }
475
476            final Calendar cal = Calendar.getInstance(mLocale);
477            cal.set(Calendar.HOUR_OF_DAY, getHour());
478            cal.set(Calendar.MINUTE, getMinute());
479            return AutofillValue.forDate(cal.getTimeInMillis());
480        }
481
482        /**
483         * This method must be called every time the value of the hour and/or minute is changed by
484         * a subclass method.
485         */
486        protected void resetAutofilledValue() {
487            mAutofilledValue = 0;
488        }
489
490        protected static class SavedState extends View.BaseSavedState {
491            private final int mHour;
492            private final int mMinute;
493            private final boolean mIs24HourMode;
494            private final int mCurrentItemShowing;
495
496            public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode) {
497                this(superState, hour, minute, is24HourMode, 0);
498            }
499
500            public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
501                    int currentItemShowing) {
502                super(superState);
503                mHour = hour;
504                mMinute = minute;
505                mIs24HourMode = is24HourMode;
506                mCurrentItemShowing = currentItemShowing;
507            }
508
509            private SavedState(Parcel in) {
510                super(in);
511                mHour = in.readInt();
512                mMinute = in.readInt();
513                mIs24HourMode = (in.readInt() == 1);
514                mCurrentItemShowing = in.readInt();
515            }
516
517            public int getHour() {
518                return mHour;
519            }
520
521            public int getMinute() {
522                return mMinute;
523            }
524
525            public boolean is24HourMode() {
526                return mIs24HourMode;
527            }
528
529            public int getCurrentItemShowing() {
530                return mCurrentItemShowing;
531            }
532
533            @Override
534            public void writeToParcel(Parcel dest, int flags) {
535                super.writeToParcel(dest, flags);
536                dest.writeInt(mHour);
537                dest.writeInt(mMinute);
538                dest.writeInt(mIs24HourMode ? 1 : 0);
539                dest.writeInt(mCurrentItemShowing);
540            }
541
542            @SuppressWarnings({"unused", "hiding"})
543            public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
544                public SavedState createFromParcel(Parcel in) {
545                    return new SavedState(in);
546                }
547
548                public SavedState[] newArray(int size) {
549                    return new SavedState[size];
550                }
551            };
552        }
553    }
554
555    @Override
556    public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
557        // This view is self-sufficient for autofill, so it needs to call
558        // onProvideAutoFillStructure() to fill itself, but it does not need to call
559        // dispatchProvideAutoFillStructure() to fill its children.
560        structure.setAutofillId(getAutofillId());
561        onProvideAutofillStructure(structure, flags);
562    }
563
564    @Override
565    public void autofill(AutofillValue value) {
566        if (!isEnabled()) return;
567
568        mDelegate.autofill(value);
569    }
570
571    @Override
572    public @AutofillType int getAutofillType() {
573        return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
574    }
575
576    @Override
577    public AutofillValue getAutofillValue() {
578        return isEnabled() ? mDelegate.getAutofillValue() : null;
579    }
580}
581