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