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.UnsupportedAppUsage; 24 import android.annotation.Widget; 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 */ 105 void onTimeChanged(TimePicker view, int hourOfDay, int minute); 106 } 107 108 public TimePicker(Context context) { 109 this(context, null); 110 } 111 112 public TimePicker(Context context, AttributeSet attrs) { 113 this(context, attrs, R.attr.timePickerStyle); 114 } 115 116 public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) { 117 this(context, attrs, defStyleAttr, 0); 118 } 119 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 }) 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 */ 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) 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 */ 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) 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 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 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 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 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 */ 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") 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 */ 294 public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { 295 mDelegate.setOnTimeChangedListener(onTimeChangedListener); 296 } 297 298 @Override 299 public void setEnabled(boolean enabled) { 300 super.setEnabled(enabled); 301 mDelegate.setEnabled(enabled); 302 } 303 304 @Override 305 public boolean isEnabled() { 306 return mDelegate.isEnabled(); 307 } 308 309 @Override 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 */ 320 public boolean validateInput() { 321 return mDelegate.validateInput(); 322 } 323 324 @Override 325 protected Parcelable onSaveInstanceState() { 326 Parcelable superState = super.onSaveInstanceState(); 327 return mDelegate.onSaveInstanceState(superState); 328 } 329 330 @Override 331 protected void onRestoreInstanceState(Parcelable state) { 332 BaseSavedState ss = (BaseSavedState) state; 333 super.onRestoreInstanceState(ss.getSuperState()); 334 mDelegate.onRestoreInstanceState(ss); 335 } 336 337 @Override 338 public CharSequence getAccessibilityClassName() { 339 return TimePicker.class.getName(); 340 } 341 342 /** @hide */ 343 @Override 344 public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { 345 return mDelegate.dispatchPopulateAccessibilityEvent(event); 346 } 347 348 /** @hide */ 349 @TestApi 350 public View getHourView() { 351 return mDelegate.getHourView(); 352 } 353 354 /** @hide */ 355 @TestApi 356 public View getMinuteView() { 357 return mDelegate.getMinuteView(); 358 } 359 360 /** @hide */ 361 @TestApi 362 public View getAmView() { 363 return mDelegate.getAmView(); 364 } 365 366 /** @hide */ 367 @TestApi 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 { 378 void setHour(@IntRange(from = 0, to = 23) int hour); 379 int getHour(); 380 381 void setMinute(@IntRange(from = 0, to = 59) int minute); 382 int getMinute(); 383 384 void setDate(@IntRange(from = 0, to = 23) int hour, 385 @IntRange(from = 0, to = 59) int minute); 386 387 void autofill(AutofillValue value); 388 AutofillValue getAutofillValue(); 389 390 void setIs24Hour(boolean is24Hour); 391 boolean is24Hour(); 392 393 boolean validateInput(); 394 395 void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener); 396 void setAutoFillChangeListener(OnTimeChangedListener autoFillChangeListener); 397 398 void setEnabled(boolean enabled); 399 boolean isEnabled(); 400 401 int getBaseline(); 402 403 Parcelable onSaveInstanceState(Parcelable superState); 404 void onRestoreInstanceState(Parcelable state); 405 406 boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); 407 void onPopulateAccessibilityEvent(AccessibilityEvent event); 408 409 /** @hide */ 410 @TestApi View getHourView(); 411 412 /** @hide */ 413 @TestApi View getMinuteView(); 414 415 /** @hide */ 416 @TestApi View getAmView(); 417 418 /** @hide */ 419 @TestApi View getPmView(); 420 } 421 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 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 455 public void setOnTimeChangedListener(OnTimeChangedListener callback) { 456 mOnTimeChangedListener = callback; 457 } 458 459 @Override 460 public void setAutoFillChangeListener(OnTimeChangedListener callback) { 461 mAutoFillChangeListener = callback; 462 } 463 464 @Override 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 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 */ 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 508 public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode) { 509 this(superState, hour, minute, is24HourMode, 0); 510 } 511 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 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 529 public int getHour() { 530 return mHour; 531 } 532 533 public int getMinute() { 534 return mMinute; 535 } 536 537 public boolean is24HourMode() { 538 return mIs24HourMode; 539 } 540 541 public int getCurrentItemShowing() { 542 return mCurrentItemShowing; 543 } 544 545 @Override 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 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 577 public void autofill(AutofillValue value) { 578 if (!isEnabled()) return; 579 580 mDelegate.autofill(value); 581 } 582 583 @Override 584 public @AutofillType int getAutofillType() { 585 return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE; 586 } 587 588 @Override 589 public AutofillValue getAutofillValue() { 590 return isEnabled() ? mDelegate.getAutofillValue() : null; 591 } 592 } 593