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