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