1 /* 2 * Copyright (C) 2012 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 static android.view.ViewDebug.ExportedProperty; 20 import static android.widget.RemoteViews.RemoteView; 21 22 import android.annotation.NonNull; 23 import android.annotation.TestApi; 24 import android.app.ActivityManager; 25 import android.compat.annotation.UnsupportedAppUsage; 26 import android.content.BroadcastReceiver; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.res.TypedArray; 32 import android.database.ContentObserver; 33 import android.icu.text.DateTimePatternGenerator; 34 import android.net.Uri; 35 import android.os.Build; 36 import android.os.Handler; 37 import android.os.UserHandle; 38 import android.provider.Settings; 39 import android.text.format.DateFormat; 40 import android.util.AttributeSet; 41 import android.view.RemotableViewMethod; 42 import android.view.ViewHierarchyEncoder; 43 import android.view.inspector.InspectableProperty; 44 45 import com.android.internal.R; 46 47 import java.time.Duration; 48 import java.time.Instant; 49 import java.time.ZoneId; 50 import java.time.ZonedDateTime; 51 import java.util.Calendar; 52 import java.util.TimeZone; 53 54 /** 55 * <p><code>TextClock</code> can display the current date and/or time as 56 * a formatted string.</p> 57 * 58 * <p>This view honors the 24-hour format system setting. As such, it is 59 * possible and recommended to provide two different formatting patterns: 60 * one to display the date/time in 24-hour mode and one to display the 61 * date/time in 12-hour mode. Most callers will want to use the defaults, 62 * though, which will be appropriate for the user's locale.</p> 63 * 64 * <p>It is possible to determine whether the system is currently in 65 * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p> 66 * 67 * <p>The rules used by this widget to decide how to format the date and 68 * time are the following:</p> 69 * <ul> 70 * <li>In 24-hour mode: 71 * <ul> 72 * <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li> 73 * <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li> 74 * <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li> 75 * </ul> 76 * </li> 77 * <li>In 12-hour mode: 78 * <ul> 79 * <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li> 80 * <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li> 81 * <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li> 82 * </ul> 83 * </li> 84 * </ul> 85 * 86 * <p>The {@link CharSequence} instances used as formatting patterns when calling either 87 * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can 88 * contain styling information. To do so, use a {@link android.text.Spanned} object. 89 * Note that if you customize these strings, it is your responsibility to supply strings 90 * appropriate for formatting dates and/or times in the user's locale.</p> 91 * 92 * @attr ref android.R.styleable#TextClock_format12Hour 93 * @attr ref android.R.styleable#TextClock_format24Hour 94 * @attr ref android.R.styleable#TextClock_timeZone 95 */ 96 @RemoteView 97 public class TextClock extends TextView { 98 /** 99 * The default formatting pattern in 12-hour mode. This pattern is used 100 * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern 101 * or if no pattern was specified when creating an instance of this class. 102 * 103 * This default pattern shows only the time, hours and minutes, and an am/pm 104 * indicator. 105 * 106 * @see #setFormat12Hour(CharSequence) 107 * @see #getFormat12Hour() 108 * 109 * @deprecated Let the system use locale-appropriate defaults instead. 110 */ 111 @Deprecated 112 public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a"; 113 114 /** 115 * The default formatting pattern in 24-hour mode. This pattern is used 116 * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern 117 * or if no pattern was specified when creating an instance of this class. 118 * 119 * This default pattern shows only the time, hours and minutes. 120 * 121 * @see #setFormat24Hour(CharSequence) 122 * @see #getFormat24Hour() 123 * 124 * @deprecated Let the system use locale-appropriate defaults instead. 125 */ 126 @Deprecated 127 public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm"; 128 129 private CharSequence mFormat12; 130 private CharSequence mFormat24; 131 private CharSequence mDescFormat12; 132 private CharSequence mDescFormat24; 133 134 @ExportedProperty 135 private CharSequence mFormat; 136 @ExportedProperty 137 private boolean mHasSeconds; 138 139 private CharSequence mDescFormat; 140 141 private boolean mRegistered; 142 private boolean mShouldRunTicker; 143 144 private Calendar mTime; 145 private String mTimeZone; 146 147 private boolean mShowCurrentUserTime; 148 149 private ContentObserver mFormatChangeObserver; 150 // Used by tests to stop time change events from triggering the text update 151 private boolean mStopTicking; 152 153 private class FormatChangeObserver extends ContentObserver { 154 FormatChangeObserver(Handler handler)155 public FormatChangeObserver(Handler handler) { 156 super(handler); 157 } 158 159 @Override onChange(boolean selfChange)160 public void onChange(boolean selfChange) { 161 chooseFormat(); 162 onTimeChanged(); 163 } 164 165 @Override onChange(boolean selfChange, Uri uri)166 public void onChange(boolean selfChange, Uri uri) { 167 chooseFormat(); 168 onTimeChanged(); 169 } 170 }; 171 172 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 173 @Override 174 public void onReceive(Context context, Intent intent) { 175 if (mStopTicking) { 176 return; // Test disabled the clock ticks 177 } 178 if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { 179 final String timeZone = intent.getStringExtra(Intent.EXTRA_TIMEZONE); 180 createTime(timeZone); 181 } else if (!mShouldRunTicker && (Intent.ACTION_TIME_TICK.equals(intent.getAction()) 182 || Intent.ACTION_TIME_CHANGED.equals(intent.getAction()))) { 183 return; 184 } 185 onTimeChanged(); 186 } 187 }; 188 189 private final Runnable mTicker = new Runnable() { 190 public void run() { 191 removeCallbacks(this); 192 if (mStopTicking || !mShouldRunTicker) { 193 return; // Test disabled the clock ticks 194 } 195 onTimeChanged(); 196 197 Instant now = mTime.toInstant(); 198 ZoneId zone = mTime.getTimeZone().toZoneId(); 199 200 ZonedDateTime nextTick; 201 if (mHasSeconds) { 202 nextTick = now.atZone(zone).plusSeconds(1).withNano(0); 203 } else { 204 nextTick = now.atZone(zone).plusMinutes(1).withSecond(0).withNano(0); 205 } 206 207 long millisUntilNextTick = Duration.between(now, nextTick.toInstant()).toMillis(); 208 if (millisUntilNextTick <= 0) { 209 // This should never happen, but if it does, then tick again in a second. 210 millisUntilNextTick = 1000; 211 } 212 213 postDelayed(this, millisUntilNextTick); 214 } 215 }; 216 217 /** 218 * Creates a new clock using the default patterns for the current locale. 219 * 220 * @param context The Context the view is running in, through which it can 221 * access the current theme, resources, etc. 222 */ 223 @SuppressWarnings("UnusedDeclaration") TextClock(Context context)224 public TextClock(Context context) { 225 super(context); 226 init(); 227 } 228 229 /** 230 * Creates a new clock inflated from XML. This object's properties are 231 * intialized from the attributes specified in XML. 232 * 233 * This constructor uses a default style of 0, so the only attribute values 234 * applied are those in the Context's Theme and the given AttributeSet. 235 * 236 * @param context The Context the view is running in, through which it can 237 * access the current theme, resources, etc. 238 * @param attrs The attributes of the XML tag that is inflating the view 239 */ 240 @SuppressWarnings("UnusedDeclaration") TextClock(Context context, AttributeSet attrs)241 public TextClock(Context context, AttributeSet attrs) { 242 this(context, attrs, 0); 243 } 244 245 /** 246 * Creates a new clock inflated from XML. This object's properties are 247 * intialized from the attributes specified in XML. 248 * 249 * @param context The Context the view is running in, through which it can 250 * access the current theme, resources, etc. 251 * @param attrs The attributes of the XML tag that is inflating the view 252 * @param defStyleAttr An attribute in the current theme that contains a 253 * reference to a style resource that supplies default values for 254 * the view. Can be 0 to not look for defaults. 255 */ TextClock(Context context, AttributeSet attrs, int defStyleAttr)256 public TextClock(Context context, AttributeSet attrs, int defStyleAttr) { 257 this(context, attrs, defStyleAttr, 0); 258 } 259 TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)260 public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 261 super(context, attrs, defStyleAttr, defStyleRes); 262 263 final TypedArray a = context.obtainStyledAttributes( 264 attrs, R.styleable.TextClock, defStyleAttr, defStyleRes); 265 saveAttributeDataForStyleable(context, R.styleable.TextClock, 266 attrs, a, defStyleAttr, defStyleRes); 267 try { 268 mFormat12 = a.getText(R.styleable.TextClock_format12Hour); 269 mFormat24 = a.getText(R.styleable.TextClock_format24Hour); 270 mTimeZone = a.getString(R.styleable.TextClock_timeZone); 271 } finally { 272 a.recycle(); 273 } 274 275 init(); 276 } 277 init()278 private void init() { 279 if (mFormat12 == null) { 280 mFormat12 = getBestDateTimePattern("hm"); 281 } 282 if (mFormat24 == null) { 283 mFormat24 = getBestDateTimePattern("Hm"); 284 } 285 286 createTime(mTimeZone); 287 chooseFormat(); 288 } 289 createTime(String timeZone)290 private void createTime(String timeZone) { 291 if (timeZone != null) { 292 mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); 293 } else { 294 mTime = Calendar.getInstance(); 295 } 296 } 297 298 /** 299 * Returns the formatting pattern used to display the date and/or time 300 * in 12-hour mode. The formatting pattern syntax is described in 301 * {@link DateFormat}. 302 * 303 * @return A {@link CharSequence} or null. 304 * 305 * @see #setFormat12Hour(CharSequence) 306 * @see #is24HourModeEnabled() 307 */ 308 @InspectableProperty 309 @ExportedProperty getFormat12Hour()310 public CharSequence getFormat12Hour() { 311 return mFormat12; 312 } 313 314 /** 315 * <p>Specifies the formatting pattern used to display the date and/or time 316 * in 12-hour mode. The formatting pattern syntax is described in 317 * {@link DateFormat}.</p> 318 * 319 * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used 320 * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns 321 * are set to null, the default pattern for the current locale will be used 322 * instead.</p> 323 * 324 * <p><strong>Note:</strong> if styling is not needed, it is highly recommended 325 * you supply a format string generated by 326 * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method 327 * takes care of generating a format string adapted to the desired locale.</p> 328 * 329 * 330 * @param format A date/time formatting pattern as described in {@link DateFormat} 331 * 332 * @see #getFormat12Hour() 333 * @see #is24HourModeEnabled() 334 * @see DateFormat#getBestDateTimePattern(java.util.Locale, String) 335 * @see DateFormat 336 * 337 * @attr ref android.R.styleable#TextClock_format12Hour 338 */ 339 @RemotableViewMethod setFormat12Hour(CharSequence format)340 public void setFormat12Hour(CharSequence format) { 341 mFormat12 = format; 342 343 chooseFormat(); 344 onTimeChanged(); 345 } 346 347 /** 348 * Like setFormat12Hour, but for the content description. 349 * @hide 350 */ setContentDescriptionFormat12Hour(CharSequence format)351 public void setContentDescriptionFormat12Hour(CharSequence format) { 352 mDescFormat12 = format; 353 354 chooseFormat(); 355 onTimeChanged(); 356 } 357 358 /** 359 * Returns the formatting pattern used to display the date and/or time 360 * in 24-hour mode. The formatting pattern syntax is described in 361 * {@link DateFormat}. 362 * 363 * @return A {@link CharSequence} or null. 364 * 365 * @see #setFormat24Hour(CharSequence) 366 * @see #is24HourModeEnabled() 367 */ 368 @InspectableProperty 369 @ExportedProperty getFormat24Hour()370 public CharSequence getFormat24Hour() { 371 return mFormat24; 372 } 373 374 /** 375 * <p>Specifies the formatting pattern used to display the date and/or time 376 * in 24-hour mode. The formatting pattern syntax is described in 377 * {@link DateFormat}.</p> 378 * 379 * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used 380 * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns 381 * are set to null, the default pattern for the current locale will be used 382 * instead.</p> 383 * 384 * <p><strong>Note:</strong> if styling is not needed, it is highly recommended 385 * you supply a format string generated by 386 * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method 387 * takes care of generating a format string adapted to the desired locale.</p> 388 * 389 * @param format A date/time formatting pattern as described in {@link DateFormat} 390 * 391 * @see #getFormat24Hour() 392 * @see #is24HourModeEnabled() 393 * @see DateFormat#getBestDateTimePattern(java.util.Locale, String) 394 * @see DateFormat 395 * 396 * @attr ref android.R.styleable#TextClock_format24Hour 397 */ 398 @RemotableViewMethod setFormat24Hour(CharSequence format)399 public void setFormat24Hour(CharSequence format) { 400 mFormat24 = format; 401 402 chooseFormat(); 403 onTimeChanged(); 404 } 405 406 /** 407 * Like setFormat24Hour, but for the content description. 408 * @hide 409 */ setContentDescriptionFormat24Hour(CharSequence format)410 public void setContentDescriptionFormat24Hour(CharSequence format) { 411 mDescFormat24 = format; 412 413 chooseFormat(); 414 onTimeChanged(); 415 } 416 417 /** 418 * Sets whether this clock should always track the current user and not the user of the 419 * current process. This is used for single instance processes like the systemUI who need 420 * to display time for different users. 421 * 422 * @hide 423 */ setShowCurrentUserTime(boolean showCurrentUserTime)424 public void setShowCurrentUserTime(boolean showCurrentUserTime) { 425 mShowCurrentUserTime = showCurrentUserTime; 426 427 chooseFormat(); 428 onTimeChanged(); 429 unregisterObserver(); 430 registerObserver(); 431 } 432 433 /** 434 * Update the displayed time if necessary and invalidate the view. 435 */ refreshTime()436 public void refreshTime() { 437 onTimeChanged(); 438 invalidate(); 439 } 440 441 /** 442 * Indicates whether the system is currently using the 24-hour mode. 443 * 444 * When the system is in 24-hour mode, this view will use the pattern 445 * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern 446 * returned by {@link #getFormat12Hour()} is used instead. 447 * 448 * If either one of the formats is null, the other format is used. If 449 * both formats are null, the default formats for the current locale are used. 450 * 451 * @return true if time should be displayed in 24-hour format, false if it 452 * should be displayed in 12-hour format. 453 * 454 * @see #setFormat12Hour(CharSequence) 455 * @see #getFormat12Hour() 456 * @see #setFormat24Hour(CharSequence) 457 * @see #getFormat24Hour() 458 */ 459 @InspectableProperty(hasAttributeId = false) is24HourModeEnabled()460 public boolean is24HourModeEnabled() { 461 if (mShowCurrentUserTime) { 462 return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser()); 463 } else { 464 return DateFormat.is24HourFormat(getContext()); 465 } 466 } 467 468 /** 469 * Indicates which time zone is currently used by this view. 470 * 471 * @return The ID of the current time zone or null if the default time zone, 472 * as set by the user, must be used 473 * 474 * @see TimeZone 475 * @see java.util.TimeZone#getAvailableIDs() 476 * @see #setTimeZone(String) 477 */ 478 @InspectableProperty getTimeZone()479 public String getTimeZone() { 480 return mTimeZone; 481 } 482 483 /** 484 * Sets the specified time zone to use in this clock. When the time zone 485 * is set through this method, system time zone changes (when the user 486 * sets the time zone in settings for instance) will be ignored. 487 * 488 * @param timeZone The desired time zone's ID as specified in {@link TimeZone} 489 * or null to user the time zone specified by the user 490 * (system time zone) 491 * 492 * @see #getTimeZone() 493 * @see java.util.TimeZone#getAvailableIDs() 494 * @see TimeZone#getTimeZone(String) 495 * 496 * @attr ref android.R.styleable#TextClock_timeZone 497 */ 498 @RemotableViewMethod setTimeZone(String timeZone)499 public void setTimeZone(String timeZone) { 500 mTimeZone = timeZone; 501 502 createTime(timeZone); 503 onTimeChanged(); 504 } 505 506 /** 507 * Returns the current format string. Always valid after constructor has 508 * finished, and will never be {@code null}. 509 * 510 * @hide 511 */ 512 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getFormat()513 public CharSequence getFormat() { 514 return mFormat; 515 } 516 517 /** 518 * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} 519 * depending on whether the user has selected 24-hour format. 520 */ chooseFormat()521 private void chooseFormat() { 522 final boolean format24Requested = is24HourModeEnabled(); 523 524 if (format24Requested) { 525 mFormat = abc(mFormat24, mFormat12, getBestDateTimePattern("Hm")); 526 mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat); 527 } else { 528 mFormat = abc(mFormat12, mFormat24, getBestDateTimePattern("hm")); 529 mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat); 530 } 531 532 boolean hadSeconds = mHasSeconds; 533 mHasSeconds = DateFormat.hasSeconds(mFormat); 534 535 if (mShouldRunTicker && hadSeconds != mHasSeconds) { 536 mTicker.run(); 537 } 538 } 539 getBestDateTimePattern(String skeleton)540 private String getBestDateTimePattern(String skeleton) { 541 DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance( 542 getContext().getResources().getConfiguration().locale); 543 return dtpg.getBestPattern(skeleton); 544 } 545 546 /** 547 * Returns a if not null, else return b if not null, else return c. 548 */ abc(CharSequence a, CharSequence b, CharSequence c)549 private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) { 550 return a == null ? (b == null ? c : b) : a; 551 } 552 553 @Override onAttachedToWindow()554 protected void onAttachedToWindow() { 555 super.onAttachedToWindow(); 556 557 if (!mRegistered) { 558 mRegistered = true; 559 560 registerReceiver(); 561 registerObserver(); 562 563 createTime(mTimeZone); 564 } 565 } 566 567 @Override onVisibilityAggregated(boolean isVisible)568 public void onVisibilityAggregated(boolean isVisible) { 569 super.onVisibilityAggregated(isVisible); 570 571 if (!mShouldRunTicker && isVisible) { 572 mShouldRunTicker = true; 573 mTicker.run(); 574 } else if (mShouldRunTicker && !isVisible) { 575 mShouldRunTicker = false; 576 removeCallbacks(mTicker); 577 } 578 } 579 580 @Override onDetachedFromWindow()581 protected void onDetachedFromWindow() { 582 super.onDetachedFromWindow(); 583 584 if (mRegistered) { 585 unregisterReceiver(); 586 unregisterObserver(); 587 588 mRegistered = false; 589 } 590 } 591 592 /** 593 * Used by tests to stop the clock tick from updating the text. 594 * @hide 595 */ 596 @TestApi disableClockTick()597 public void disableClockTick() { 598 mStopTicking = true; 599 } 600 registerReceiver()601 private void registerReceiver() { 602 final IntentFilter filter = new IntentFilter(); 603 604 filter.addAction(Intent.ACTION_TIME_CHANGED); 605 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 606 607 // OK, this is gross but needed. This class is supported by the 608 // remote views mechanism and as a part of that the remote views 609 // can be inflated by a context for another user without the app 610 // having interact users permission - just for loading resources. 611 // For example, when adding widgets from a managed profile to the 612 // home screen. Therefore, we register the receiver as the user 613 // the app is running as not the one the context is for. 614 getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(), 615 filter, null, getHandler()); 616 } 617 registerObserver()618 private void registerObserver() { 619 if (mRegistered) { 620 if (mFormatChangeObserver == null) { 621 mFormatChangeObserver = new FormatChangeObserver(getHandler()); 622 } 623 final ContentResolver resolver = getContext().getContentResolver(); 624 Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24); 625 if (mShowCurrentUserTime) { 626 resolver.registerContentObserver(uri, true, 627 mFormatChangeObserver, UserHandle.USER_ALL); 628 } else { 629 // UserHandle.myUserId() is needed. This class is supported by the 630 // remote views mechanism and as a part of that the remote views 631 // can be inflated by a context for another user without the app 632 // having interact users permission - just for loading resources. 633 // For example, when adding widgets from a managed profile to the 634 // home screen. Therefore, we register the ContentObserver with the user 635 // the app is running (e.g. the launcher) and not the user of the 636 // context (e.g. the widget's profile). 637 resolver.registerContentObserver(uri, true, 638 mFormatChangeObserver, UserHandle.myUserId()); 639 } 640 } 641 } 642 unregisterReceiver()643 private void unregisterReceiver() { 644 getContext().unregisterReceiver(mIntentReceiver); 645 } 646 unregisterObserver()647 private void unregisterObserver() { 648 if (mFormatChangeObserver != null) { 649 final ContentResolver resolver = getContext().getContentResolver(); 650 resolver.unregisterContentObserver(mFormatChangeObserver); 651 } 652 } 653 654 /** 655 * Update the displayed time if this view and its ancestors and window is visible 656 */ 657 @UnsupportedAppUsage onTimeChanged()658 private void onTimeChanged() { 659 mTime.setTimeInMillis(System.currentTimeMillis()); 660 setText(DateFormat.format(mFormat, mTime)); 661 setContentDescription(DateFormat.format(mDescFormat, mTime)); 662 } 663 664 /** @hide */ 665 @Override encodeProperties(@onNull ViewHierarchyEncoder stream)666 protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) { 667 super.encodeProperties(stream); 668 669 CharSequence s = getFormat12Hour(); 670 stream.addProperty("format12Hour", s == null ? null : s.toString()); 671 672 s = getFormat24Hour(); 673 stream.addProperty("format24Hour", s == null ? null : s.toString()); 674 stream.addProperty("format", mFormat == null ? null : mFormat.toString()); 675 stream.addProperty("hasSeconds", mHasSeconds); 676 } 677 } 678