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.text.DateFormatSymbols;
28 import android.icu.util.Calendar;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.text.format.DateFormat;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.util.MathUtils;
35 import android.view.View;
36 import android.view.ViewStructure;
37 import android.view.accessibility.AccessibilityEvent;
38 import android.view.autofill.AutofillManager;
39 import android.view.autofill.AutofillValue;
40 import android.view.inspector.InspectableProperty;
41 
42 import com.android.internal.R;
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         DateFormatSymbols dfs = DateFormat.getIcuDateFormatSymbols(locale);
425         String[] amPm = dfs.getAmPmStrings();
426         String[] narrowAmPm = dfs.getAmpmNarrowStrings();
427 
428         final String[] result = new String[2];
429         result[0] = amPm[0].length() > 4 ? narrowAmPm[0] : amPm[0];
430         result[1] = amPm[1].length() > 4 ? narrowAmPm[1] : amPm[1];
431         return result;
432     }
433 
434     /**
435      * An abstract class which can be used as a start for TimePicker implementations
436      */
437     abstract static class AbstractTimePickerDelegate implements TimePickerDelegate {
438         protected final TimePicker mDelegator;
439         protected final Context mContext;
440         protected final Locale mLocale;
441 
442         protected OnTimeChangedListener mOnTimeChangedListener;
443         protected OnTimeChangedListener mAutoFillChangeListener;
444 
445         // The value that was passed to autofill() - it must be stored because it getAutofillValue()
446         // must return the exact same value that was autofilled, otherwise the widget will not be
447         // properly highlighted after autofill().
448         private long mAutofilledValue;
449 
AbstractTimePickerDelegate(@onNull TimePicker delegator, @NonNull Context context)450         public AbstractTimePickerDelegate(@NonNull TimePicker delegator, @NonNull Context context) {
451             mDelegator = delegator;
452             mContext = context;
453             mLocale = context.getResources().getConfiguration().locale;
454         }
455 
456         @Override
setOnTimeChangedListener(OnTimeChangedListener callback)457         public void setOnTimeChangedListener(OnTimeChangedListener callback) {
458             mOnTimeChangedListener = callback;
459         }
460 
461         @Override
setAutoFillChangeListener(OnTimeChangedListener callback)462         public void setAutoFillChangeListener(OnTimeChangedListener callback) {
463             mAutoFillChangeListener = callback;
464         }
465 
466         @Override
autofill(AutofillValue value)467         public final void autofill(AutofillValue value) {
468             if (value == null || !value.isDate()) {
469                 Log.w(LOG_TAG, value + " could not be autofilled into " + this);
470                 return;
471             }
472 
473             final long time = value.getDateValue();
474 
475             final Calendar cal = Calendar.getInstance(mLocale);
476             cal.setTimeInMillis(time);
477             setDate(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
478 
479             // Must set mAutofilledValue *after* calling subclass method to make sure the value
480             // returned by getAutofillValue() matches it.
481             mAutofilledValue = time;
482         }
483 
484         @Override
getAutofillValue()485         public final AutofillValue getAutofillValue() {
486             if (mAutofilledValue != 0) {
487                 return AutofillValue.forDate(mAutofilledValue);
488             }
489 
490             final Calendar cal = Calendar.getInstance(mLocale);
491             cal.set(Calendar.HOUR_OF_DAY, getHour());
492             cal.set(Calendar.MINUTE, getMinute());
493             return AutofillValue.forDate(cal.getTimeInMillis());
494         }
495 
496         /**
497          * This method must be called every time the value of the hour and/or minute is changed by
498          * a subclass method.
499          */
resetAutofilledValue()500         protected void resetAutofilledValue() {
501             mAutofilledValue = 0;
502         }
503 
504         protected static class SavedState extends View.BaseSavedState {
505             private final int mHour;
506             private final int mMinute;
507             private final boolean mIs24HourMode;
508             private final int mCurrentItemShowing;
509 
SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode)510             public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode) {
511                 this(superState, hour, minute, is24HourMode, 0);
512             }
513 
SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, int currentItemShowing)514             public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
515                     int currentItemShowing) {
516                 super(superState);
517                 mHour = hour;
518                 mMinute = minute;
519                 mIs24HourMode = is24HourMode;
520                 mCurrentItemShowing = currentItemShowing;
521             }
522 
SavedState(Parcel in)523             private SavedState(Parcel in) {
524                 super(in);
525                 mHour = in.readInt();
526                 mMinute = in.readInt();
527                 mIs24HourMode = (in.readInt() == 1);
528                 mCurrentItemShowing = in.readInt();
529             }
530 
getHour()531             public int getHour() {
532                 return mHour;
533             }
534 
getMinute()535             public int getMinute() {
536                 return mMinute;
537             }
538 
is24HourMode()539             public boolean is24HourMode() {
540                 return mIs24HourMode;
541             }
542 
getCurrentItemShowing()543             public int getCurrentItemShowing() {
544                 return mCurrentItemShowing;
545             }
546 
547             @Override
writeToParcel(Parcel dest, int flags)548             public void writeToParcel(Parcel dest, int flags) {
549                 super.writeToParcel(dest, flags);
550                 dest.writeInt(mHour);
551                 dest.writeInt(mMinute);
552                 dest.writeInt(mIs24HourMode ? 1 : 0);
553                 dest.writeInt(mCurrentItemShowing);
554             }
555 
556             @SuppressWarnings({"unused", "hiding"})
557             public static final @android.annotation.NonNull Creator<SavedState> CREATOR = new Creator<SavedState>() {
558                 public SavedState createFromParcel(Parcel in) {
559                     return new SavedState(in);
560                 }
561 
562                 public SavedState[] newArray(int size) {
563                     return new SavedState[size];
564                 }
565             };
566         }
567     }
568 
569     @Override
dispatchProvideAutofillStructure(ViewStructure structure, int flags)570     public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
571         // This view is self-sufficient for autofill, so it needs to call
572         // onProvideAutoFillStructure() to fill itself, but it does not need to call
573         // dispatchProvideAutoFillStructure() to fill its children.
574         structure.setAutofillId(getAutofillId());
575         onProvideAutofillStructure(structure, flags);
576     }
577 
578     @Override
autofill(AutofillValue value)579     public void autofill(AutofillValue value) {
580         if (!isEnabled()) return;
581 
582         mDelegate.autofill(value);
583     }
584 
585     @Override
getAutofillType()586     public @AutofillType int getAutofillType() {
587         return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
588     }
589 
590     @Override
getAutofillValue()591     public AutofillValue getAutofillValue() {
592         return isEnabled() ? mDelegate.getAutofillValue() : null;
593     }
594 }
595