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