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