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