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