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