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