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.annotation.UnsupportedAppUsage; 25 import android.app.ActivityManager; 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("time-zone"); 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 getHandler().postAtTime(mTicker, next); 197 } 198 }; 199 200 /** 201 * Creates a new clock using the default patterns for the current locale. 202 * 203 * @param context The Context the view is running in, through which it can 204 * access the current theme, resources, etc. 205 */ 206 @SuppressWarnings("UnusedDeclaration") TextClock(Context context)207 public TextClock(Context context) { 208 super(context); 209 init(); 210 } 211 212 /** 213 * Creates a new clock inflated from XML. This object's properties are 214 * intialized from the attributes specified in XML. 215 * 216 * This constructor uses a default style of 0, so the only attribute values 217 * applied are those in the Context's Theme and the given AttributeSet. 218 * 219 * @param context The Context the view is running in, through which it can 220 * access the current theme, resources, etc. 221 * @param attrs The attributes of the XML tag that is inflating the view 222 */ 223 @SuppressWarnings("UnusedDeclaration") TextClock(Context context, AttributeSet attrs)224 public TextClock(Context context, AttributeSet attrs) { 225 this(context, attrs, 0); 226 } 227 228 /** 229 * Creates a new clock inflated from XML. This object's properties are 230 * intialized from the attributes specified in XML. 231 * 232 * @param context The Context the view is running in, through which it can 233 * access the current theme, resources, etc. 234 * @param attrs The attributes of the XML tag that is inflating the view 235 * @param defStyleAttr An attribute in the current theme that contains a 236 * reference to a style resource that supplies default values for 237 * the view. Can be 0 to not look for defaults. 238 */ TextClock(Context context, AttributeSet attrs, int defStyleAttr)239 public TextClock(Context context, AttributeSet attrs, int defStyleAttr) { 240 this(context, attrs, defStyleAttr, 0); 241 } 242 TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)243 public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 244 super(context, attrs, defStyleAttr, defStyleRes); 245 246 final TypedArray a = context.obtainStyledAttributes( 247 attrs, R.styleable.TextClock, defStyleAttr, defStyleRes); 248 saveAttributeDataForStyleable(context, R.styleable.TextClock, 249 attrs, a, defStyleAttr, defStyleRes); 250 try { 251 mFormat12 = a.getText(R.styleable.TextClock_format12Hour); 252 mFormat24 = a.getText(R.styleable.TextClock_format24Hour); 253 mTimeZone = a.getString(R.styleable.TextClock_timeZone); 254 } finally { 255 a.recycle(); 256 } 257 258 init(); 259 } 260 init()261 private void init() { 262 if (mFormat12 == null || mFormat24 == null) { 263 LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale); 264 if (mFormat12 == null) { 265 mFormat12 = ld.timeFormat_hm; 266 } 267 if (mFormat24 == null) { 268 mFormat24 = ld.timeFormat_Hm; 269 } 270 } 271 272 createTime(mTimeZone); 273 chooseFormat(); 274 } 275 createTime(String timeZone)276 private void createTime(String timeZone) { 277 if (timeZone != null) { 278 mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); 279 } else { 280 mTime = Calendar.getInstance(); 281 } 282 } 283 284 /** 285 * Returns the formatting pattern used to display the date and/or time 286 * in 12-hour mode. The formatting pattern syntax is described in 287 * {@link DateFormat}. 288 * 289 * @return A {@link CharSequence} or null. 290 * 291 * @see #setFormat12Hour(CharSequence) 292 * @see #is24HourModeEnabled() 293 */ 294 @InspectableProperty 295 @ExportedProperty getFormat12Hour()296 public CharSequence getFormat12Hour() { 297 return mFormat12; 298 } 299 300 /** 301 * <p>Specifies the formatting pattern used to display the date and/or time 302 * in 12-hour mode. The formatting pattern syntax is described in 303 * {@link DateFormat}.</p> 304 * 305 * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used 306 * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns 307 * are set to null, the default pattern for the current locale will be used 308 * instead.</p> 309 * 310 * <p><strong>Note:</strong> if styling is not needed, it is highly recommended 311 * you supply a format string generated by 312 * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method 313 * takes care of generating a format string adapted to the desired locale.</p> 314 * 315 * 316 * @param format A date/time formatting pattern as described in {@link DateFormat} 317 * 318 * @see #getFormat12Hour() 319 * @see #is24HourModeEnabled() 320 * @see DateFormat#getBestDateTimePattern(java.util.Locale, String) 321 * @see DateFormat 322 * 323 * @attr ref android.R.styleable#TextClock_format12Hour 324 */ 325 @RemotableViewMethod setFormat12Hour(CharSequence format)326 public void setFormat12Hour(CharSequence format) { 327 mFormat12 = format; 328 329 chooseFormat(); 330 onTimeChanged(); 331 } 332 333 /** 334 * Like setFormat12Hour, but for the content description. 335 * @hide 336 */ setContentDescriptionFormat12Hour(CharSequence format)337 public void setContentDescriptionFormat12Hour(CharSequence format) { 338 mDescFormat12 = format; 339 340 chooseFormat(); 341 onTimeChanged(); 342 } 343 344 /** 345 * Returns the formatting pattern used to display the date and/or time 346 * in 24-hour mode. The formatting pattern syntax is described in 347 * {@link DateFormat}. 348 * 349 * @return A {@link CharSequence} or null. 350 * 351 * @see #setFormat24Hour(CharSequence) 352 * @see #is24HourModeEnabled() 353 */ 354 @InspectableProperty 355 @ExportedProperty getFormat24Hour()356 public CharSequence getFormat24Hour() { 357 return mFormat24; 358 } 359 360 /** 361 * <p>Specifies the formatting pattern used to display the date and/or time 362 * in 24-hour mode. The formatting pattern syntax is described in 363 * {@link DateFormat}.</p> 364 * 365 * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used 366 * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns 367 * are set to null, the default pattern for the current locale will be used 368 * instead.</p> 369 * 370 * <p><strong>Note:</strong> if styling is not needed, it is highly recommended 371 * you supply a format string generated by 372 * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method 373 * takes care of generating a format string adapted to the desired locale.</p> 374 * 375 * @param format A date/time formatting pattern as described in {@link DateFormat} 376 * 377 * @see #getFormat24Hour() 378 * @see #is24HourModeEnabled() 379 * @see DateFormat#getBestDateTimePattern(java.util.Locale, String) 380 * @see DateFormat 381 * 382 * @attr ref android.R.styleable#TextClock_format24Hour 383 */ 384 @RemotableViewMethod setFormat24Hour(CharSequence format)385 public void setFormat24Hour(CharSequence format) { 386 mFormat24 = format; 387 388 chooseFormat(); 389 onTimeChanged(); 390 } 391 392 /** 393 * Like setFormat24Hour, but for the content description. 394 * @hide 395 */ setContentDescriptionFormat24Hour(CharSequence format)396 public void setContentDescriptionFormat24Hour(CharSequence format) { 397 mDescFormat24 = format; 398 399 chooseFormat(); 400 onTimeChanged(); 401 } 402 403 /** 404 * Sets whether this clock should always track the current user and not the user of the 405 * current process. This is used for single instance processes like the systemUI who need 406 * to display time for different users. 407 * 408 * @hide 409 */ setShowCurrentUserTime(boolean showCurrentUserTime)410 public void setShowCurrentUserTime(boolean showCurrentUserTime) { 411 mShowCurrentUserTime = showCurrentUserTime; 412 413 chooseFormat(); 414 onTimeChanged(); 415 unregisterObserver(); 416 registerObserver(); 417 } 418 419 /** 420 * Update the displayed time if necessary and invalidate the view. 421 * @hide 422 */ refresh()423 public void refresh() { 424 onTimeChanged(); 425 invalidate(); 426 } 427 428 /** 429 * Indicates whether the system is currently using the 24-hour mode. 430 * 431 * When the system is in 24-hour mode, this view will use the pattern 432 * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern 433 * returned by {@link #getFormat12Hour()} is used instead. 434 * 435 * If either one of the formats is null, the other format is used. If 436 * both formats are null, the default formats for the current locale are used. 437 * 438 * @return true if time should be displayed in 24-hour format, false if it 439 * should be displayed in 12-hour format. 440 * 441 * @see #setFormat12Hour(CharSequence) 442 * @see #getFormat12Hour() 443 * @see #setFormat24Hour(CharSequence) 444 * @see #getFormat24Hour() 445 */ 446 @InspectableProperty(hasAttributeId = false) is24HourModeEnabled()447 public boolean is24HourModeEnabled() { 448 if (mShowCurrentUserTime) { 449 return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser()); 450 } else { 451 return DateFormat.is24HourFormat(getContext()); 452 } 453 } 454 455 /** 456 * Indicates which time zone is currently used by this view. 457 * 458 * @return The ID of the current time zone or null if the default time zone, 459 * as set by the user, must be used 460 * 461 * @see TimeZone 462 * @see java.util.TimeZone#getAvailableIDs() 463 * @see #setTimeZone(String) 464 */ 465 @InspectableProperty getTimeZone()466 public String getTimeZone() { 467 return mTimeZone; 468 } 469 470 /** 471 * Sets the specified time zone to use in this clock. When the time zone 472 * is set through this method, system time zone changes (when the user 473 * sets the time zone in settings for instance) will be ignored. 474 * 475 * @param timeZone The desired time zone's ID as specified in {@link TimeZone} 476 * or null to user the time zone specified by the user 477 * (system time zone) 478 * 479 * @see #getTimeZone() 480 * @see java.util.TimeZone#getAvailableIDs() 481 * @see TimeZone#getTimeZone(String) 482 * 483 * @attr ref android.R.styleable#TextClock_timeZone 484 */ 485 @RemotableViewMethod setTimeZone(String timeZone)486 public void setTimeZone(String timeZone) { 487 mTimeZone = timeZone; 488 489 createTime(timeZone); 490 onTimeChanged(); 491 } 492 493 /** 494 * Returns the current format string. Always valid after constructor has 495 * finished, and will never be {@code null}. 496 * 497 * @hide 498 */ 499 @UnsupportedAppUsage getFormat()500 public CharSequence getFormat() { 501 return mFormat; 502 } 503 504 /** 505 * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} 506 * depending on whether the user has selected 24-hour format. 507 */ chooseFormat()508 private void chooseFormat() { 509 final boolean format24Requested = is24HourModeEnabled(); 510 511 LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale); 512 513 if (format24Requested) { 514 mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm); 515 mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat); 516 } else { 517 mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm); 518 mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat); 519 } 520 521 boolean hadSeconds = mHasSeconds; 522 mHasSeconds = DateFormat.hasSeconds(mFormat); 523 524 if (mShouldRunTicker && hadSeconds != mHasSeconds) { 525 if (hadSeconds) getHandler().removeCallbacks(mTicker); 526 else mTicker.run(); 527 } 528 } 529 530 /** 531 * Returns a if not null, else return b if not null, else return c. 532 */ abc(CharSequence a, CharSequence b, CharSequence c)533 private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) { 534 return a == null ? (b == null ? c : b) : a; 535 } 536 537 @Override onAttachedToWindow()538 protected void onAttachedToWindow() { 539 super.onAttachedToWindow(); 540 541 if (!mRegistered) { 542 mRegistered = true; 543 544 registerReceiver(); 545 registerObserver(); 546 547 createTime(mTimeZone); 548 } 549 } 550 551 @Override onVisibilityAggregated(boolean isVisible)552 public void onVisibilityAggregated(boolean isVisible) { 553 super.onVisibilityAggregated(isVisible); 554 555 if (!mShouldRunTicker && isVisible) { 556 mShouldRunTicker = true; 557 if (mHasSeconds) { 558 mTicker.run(); 559 } else { 560 onTimeChanged(); 561 } 562 } else if (mShouldRunTicker && !isVisible) { 563 mShouldRunTicker = false; 564 getHandler().removeCallbacks(mTicker); 565 } 566 } 567 568 @Override onDetachedFromWindow()569 protected void onDetachedFromWindow() { 570 super.onDetachedFromWindow(); 571 572 if (mRegistered) { 573 unregisterReceiver(); 574 unregisterObserver(); 575 576 mRegistered = false; 577 } 578 } 579 580 /** 581 * Used by tests to stop the clock tick from updating the text. 582 * @hide 583 */ 584 @TestApi disableClockTick()585 public void disableClockTick() { 586 mStopTicking = true; 587 } 588 registerReceiver()589 private void registerReceiver() { 590 final IntentFilter filter = new IntentFilter(); 591 592 filter.addAction(Intent.ACTION_TIME_TICK); 593 filter.addAction(Intent.ACTION_TIME_CHANGED); 594 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 595 596 // OK, this is gross but needed. This class is supported by the 597 // remote views mechanism and as a part of that the remote views 598 // can be inflated by a context for another user without the app 599 // having interact users permission - just for loading resources. 600 // For example, when adding widgets from a managed profile to the 601 // home screen. Therefore, we register the receiver as the user 602 // the app is running as not the one the context is for. 603 getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(), 604 filter, null, getHandler()); 605 } 606 registerObserver()607 private void registerObserver() { 608 if (mRegistered) { 609 if (mFormatChangeObserver == null) { 610 mFormatChangeObserver = new FormatChangeObserver(getHandler()); 611 } 612 final ContentResolver resolver = getContext().getContentResolver(); 613 Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24); 614 if (mShowCurrentUserTime) { 615 resolver.registerContentObserver(uri, true, 616 mFormatChangeObserver, UserHandle.USER_ALL); 617 } else { 618 // UserHandle.myUserId() is needed. This class is supported by the 619 // remote views mechanism and as a part of that the remote views 620 // can be inflated by a context for another user without the app 621 // having interact users permission - just for loading resources. 622 // For example, when adding widgets from a managed profile to the 623 // home screen. Therefore, we register the ContentObserver with the user 624 // the app is running (e.g. the launcher) and not the user of the 625 // context (e.g. the widget's profile). 626 resolver.registerContentObserver(uri, true, 627 mFormatChangeObserver, UserHandle.myUserId()); 628 } 629 } 630 } 631 unregisterReceiver()632 private void unregisterReceiver() { 633 getContext().unregisterReceiver(mIntentReceiver); 634 } 635 unregisterObserver()636 private void unregisterObserver() { 637 if (mFormatChangeObserver != null) { 638 final ContentResolver resolver = getContext().getContentResolver(); 639 resolver.unregisterContentObserver(mFormatChangeObserver); 640 } 641 } 642 643 /** 644 * Update the displayed time if this view and its ancestors and window is visible 645 */ 646 @UnsupportedAppUsage onTimeChanged()647 private void onTimeChanged() { 648 mTime.setTimeInMillis(System.currentTimeMillis()); 649 setText(DateFormat.format(mFormat, mTime)); 650 setContentDescription(DateFormat.format(mDescFormat, mTime)); 651 } 652 653 /** @hide */ 654 @Override encodeProperties(@onNull ViewHierarchyEncoder stream)655 protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) { 656 super.encodeProperties(stream); 657 658 CharSequence s = getFormat12Hour(); 659 stream.addProperty("format12Hour", s == null ? null : s.toString()); 660 661 s = getFormat24Hour(); 662 stream.addProperty("format24Hour", s == null ? null : s.toString()); 663 stream.addProperty("format", mFormat == null ? null : mFormat.toString()); 664 stream.addProperty("hasSeconds", mHasSeconds); 665 } 666 } 667