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