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.content.BroadcastReceiver; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.res.TypedArray; 25 import android.database.ContentObserver; 26 import android.net.Uri; 27 import android.os.Handler; 28 import android.os.SystemClock; 29 import android.provider.Settings; 30 import android.text.format.DateFormat; 31 import android.util.AttributeSet; 32 import android.view.RemotableViewMethod; 33 34 import com.android.internal.R; 35 36 import java.util.Calendar; 37 import java.util.TimeZone; 38 39 import libcore.icu.LocaleData; 40 41 import static android.view.ViewDebug.ExportedProperty; 42 import static android.widget.RemoteViews.*; 43 44 /** 45 * <p><code>TextClock</code> can display the current date and/or time as 46 * a formatted string.</p> 47 * 48 * <p>This view honors the 24-hour format system setting. As such, it is 49 * possible and recommended to provide two different formatting patterns: 50 * one to display the date/time in 24-hour mode and one to display the 51 * date/time in 12-hour mode. Most callers will want to use the defaults, 52 * though, which will be appropriate for the user's locale.</p> 53 * 54 * <p>It is possible to determine whether the system is currently in 55 * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p> 56 * 57 * <p>The rules used by this widget to decide how to format the date and 58 * time are the following:</p> 59 * <ul> 60 * <li>In 24-hour mode: 61 * <ul> 62 * <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li> 63 * <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li> 64 * <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li> 65 * </ul> 66 * </li> 67 * <li>In 12-hour mode: 68 * <ul> 69 * <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li> 70 * <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li> 71 * <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li> 72 * </ul> 73 * </li> 74 * </ul> 75 * 76 * <p>The {@link CharSequence} instances used as formatting patterns when calling either 77 * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can 78 * contain styling information. To do so, use a {@link android.text.Spanned} object. 79 * Note that if you customize these strings, it is your responsibility to supply strings 80 * appropriate for formatting dates and/or times in the user's locale.</p> 81 * 82 * @attr ref android.R.styleable#TextClock_format12Hour 83 * @attr ref android.R.styleable#TextClock_format24Hour 84 * @attr ref android.R.styleable#TextClock_timeZone 85 */ 86 @RemoteView 87 public class TextClock extends TextView { 88 /** 89 * The default formatting pattern in 12-hour mode. This pattern is used 90 * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern 91 * or if no pattern was specified when creating an instance of this class. 92 * 93 * This default pattern shows only the time, hours and minutes, and an am/pm 94 * indicator. 95 * 96 * @see #setFormat12Hour(CharSequence) 97 * @see #getFormat12Hour() 98 * 99 * @deprecated Let the system use locale-appropriate defaults instead. 100 */ 101 public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a"; 102 103 /** 104 * The default formatting pattern in 24-hour mode. This pattern is used 105 * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern 106 * or if no pattern was specified when creating an instance of this class. 107 * 108 * This default pattern shows only the time, hours and minutes. 109 * 110 * @see #setFormat24Hour(CharSequence) 111 * @see #getFormat24Hour() 112 * 113 * @deprecated Let the system use locale-appropriate defaults instead. 114 */ 115 public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm"; 116 117 private CharSequence mFormat12; 118 private CharSequence mFormat24; 119 120 @ExportedProperty 121 private CharSequence mFormat; 122 @ExportedProperty 123 private boolean mHasSeconds; 124 125 private boolean mAttached; 126 127 private Calendar mTime; 128 private String mTimeZone; 129 130 private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) { 131 @Override 132 public void onChange(boolean selfChange) { 133 chooseFormat(); 134 onTimeChanged(); 135 } 136 137 @Override 138 public void onChange(boolean selfChange, Uri uri) { 139 chooseFormat(); 140 onTimeChanged(); 141 } 142 }; 143 144 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 145 @Override 146 public void onReceive(Context context, Intent intent) { 147 if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { 148 final String timeZone = intent.getStringExtra("time-zone"); 149 createTime(timeZone); 150 } 151 onTimeChanged(); 152 } 153 }; 154 155 private final Runnable mTicker = new Runnable() { 156 public void run() { 157 onTimeChanged(); 158 159 long now = SystemClock.uptimeMillis(); 160 long next = now + (1000 - now % 1000); 161 162 getHandler().postAtTime(mTicker, next); 163 } 164 }; 165 166 /** 167 * Creates a new clock using the default patterns for the current locale. 168 * 169 * @param context The Context the view is running in, through which it can 170 * access the current theme, resources, etc. 171 */ 172 @SuppressWarnings("UnusedDeclaration") 173 public TextClock(Context context) { 174 super(context); 175 init(); 176 } 177 178 /** 179 * Creates a new clock inflated from XML. This object's properties are 180 * intialized from the attributes specified in XML. 181 * 182 * This constructor uses a default style of 0, so the only attribute values 183 * applied are those in the Context's Theme and the given AttributeSet. 184 * 185 * @param context The Context the view is running in, through which it can 186 * access the current theme, resources, etc. 187 * @param attrs The attributes of the XML tag that is inflating the view 188 */ 189 @SuppressWarnings("UnusedDeclaration") 190 public TextClock(Context context, AttributeSet attrs) { 191 this(context, attrs, 0); 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 * @param context The Context the view is running in, through which it can 199 * access the current theme, resources, etc. 200 * @param attrs The attributes of the XML tag that is inflating the view 201 * @param defStyle The default style to apply to this view. If 0, no style 202 * will be applied (beyond what is included in the theme). This may 203 * either be an attribute resource, whose value will be retrieved 204 * from the current theme, or an explicit style resource 205 */ 206 public TextClock(Context context, AttributeSet attrs, int defStyle) { 207 super(context, attrs, defStyle); 208 209 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextClock, defStyle, 0); 210 try { 211 mFormat12 = a.getText(R.styleable.TextClock_format12Hour); 212 mFormat24 = a.getText(R.styleable.TextClock_format24Hour); 213 mTimeZone = a.getString(R.styleable.TextClock_timeZone); 214 } finally { 215 a.recycle(); 216 } 217 218 init(); 219 } 220 221 private void init() { 222 if (mFormat12 == null || mFormat24 == null) { 223 LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale); 224 if (mFormat12 == null) { 225 mFormat12 = ld.timeFormat12; 226 } 227 if (mFormat24 == null) { 228 mFormat24 = ld.timeFormat24; 229 } 230 } 231 232 createTime(mTimeZone); 233 // Wait until onAttachedToWindow() to handle the ticker 234 chooseFormat(false); 235 } 236 237 private void createTime(String timeZone) { 238 if (timeZone != null) { 239 mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); 240 } else { 241 mTime = Calendar.getInstance(); 242 } 243 } 244 245 /** 246 * Returns the formatting pattern used to display the date and/or time 247 * in 12-hour mode. The formatting pattern syntax is described in 248 * {@link DateFormat}. 249 * 250 * @return A {@link CharSequence} or null. 251 * 252 * @see #setFormat12Hour(CharSequence) 253 * @see #is24HourModeEnabled() 254 */ 255 @ExportedProperty 256 public CharSequence getFormat12Hour() { 257 return mFormat12; 258 } 259 260 /** 261 * <p>Specifies the formatting pattern used to display the date and/or time 262 * in 12-hour mode. The formatting pattern syntax is described in 263 * {@link DateFormat}.</p> 264 * 265 * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used 266 * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns 267 * are set to null, the default pattern for the current locale will be used 268 * instead.</p> 269 * 270 * <p><strong>Note:</strong> if styling is not needed, it is highly recommended 271 * you supply a format string generated by 272 * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method 273 * takes care of generating a format string adapted to the desired locale.</p> 274 * 275 * 276 * @param format A date/time formatting pattern as described in {@link DateFormat} 277 * 278 * @see #getFormat12Hour() 279 * @see #is24HourModeEnabled() 280 * @see DateFormat#getBestDateTimePattern(java.util.Locale, String) 281 * @see DateFormat 282 * 283 * @attr ref android.R.styleable#TextClock_format12Hour 284 */ 285 @RemotableViewMethod 286 public void setFormat12Hour(CharSequence format) { 287 mFormat12 = format; 288 289 chooseFormat(); 290 onTimeChanged(); 291 } 292 293 /** 294 * Returns the formatting pattern used to display the date and/or time 295 * in 24-hour mode. The formatting pattern syntax is described in 296 * {@link DateFormat}. 297 * 298 * @return A {@link CharSequence} or null. 299 * 300 * @see #setFormat24Hour(CharSequence) 301 * @see #is24HourModeEnabled() 302 */ 303 @ExportedProperty 304 public CharSequence getFormat24Hour() { 305 return mFormat24; 306 } 307 308 /** 309 * <p>Specifies the formatting pattern used to display the date and/or time 310 * in 24-hour mode. The formatting pattern syntax is described in 311 * {@link DateFormat}.</p> 312 * 313 * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used 314 * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns 315 * are set to null, the default pattern for the current locale will be used 316 * instead.</p> 317 * 318 * <p><strong>Note:</strong> if styling is not needed, it is highly recommended 319 * you supply a format string generated by 320 * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method 321 * takes care of generating a format string adapted to the desired locale.</p> 322 * 323 * @param format A date/time formatting pattern as described in {@link DateFormat} 324 * 325 * @see #getFormat24Hour() 326 * @see #is24HourModeEnabled() 327 * @see DateFormat#getBestDateTimePattern(java.util.Locale, String) 328 * @see DateFormat 329 * 330 * @attr ref android.R.styleable#TextClock_format24Hour 331 */ 332 @RemotableViewMethod 333 public void setFormat24Hour(CharSequence format) { 334 mFormat24 = format; 335 336 chooseFormat(); 337 onTimeChanged(); 338 } 339 340 /** 341 * Indicates whether the system is currently using the 24-hour mode. 342 * 343 * When the system is in 24-hour mode, this view will use the pattern 344 * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern 345 * returned by {@link #getFormat12Hour()} is used instead. 346 * 347 * If either one of the formats is null, the other format is used. If 348 * both formats are null, the default formats for the current locale are used. 349 * 350 * @return true if time should be displayed in 24-hour format, false if it 351 * should be displayed in 12-hour format. 352 * 353 * @see #setFormat12Hour(CharSequence) 354 * @see #getFormat12Hour() 355 * @see #setFormat24Hour(CharSequence) 356 * @see #getFormat24Hour() 357 */ 358 public boolean is24HourModeEnabled() { 359 return DateFormat.is24HourFormat(getContext()); 360 } 361 362 /** 363 * Indicates which time zone is currently used by this view. 364 * 365 * @return The ID of the current time zone or null if the default time zone, 366 * as set by the user, must be used 367 * 368 * @see TimeZone 369 * @see java.util.TimeZone#getAvailableIDs() 370 * @see #setTimeZone(String) 371 */ 372 public String getTimeZone() { 373 return mTimeZone; 374 } 375 376 /** 377 * Sets the specified time zone to use in this clock. When the time zone 378 * is set through this method, system time zone changes (when the user 379 * sets the time zone in settings for instance) will be ignored. 380 * 381 * @param timeZone The desired time zone's ID as specified in {@link TimeZone} 382 * or null to user the time zone specified by the user 383 * (system time zone) 384 * 385 * @see #getTimeZone() 386 * @see java.util.TimeZone#getAvailableIDs() 387 * @see TimeZone#getTimeZone(String) 388 * 389 * @attr ref android.R.styleable#TextClock_timeZone 390 */ 391 @RemotableViewMethod 392 public void setTimeZone(String timeZone) { 393 mTimeZone = timeZone; 394 395 createTime(timeZone); 396 onTimeChanged(); 397 } 398 399 /** 400 * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} 401 * depending on whether the user has selected 24-hour format. 402 * 403 * Calling this method does not schedule or unschedule the time ticker. 404 */ 405 private void chooseFormat() { 406 chooseFormat(true); 407 } 408 409 /** 410 * Returns the current format string. Always valid after constructor has 411 * finished, and will never be {@code null}. 412 * 413 * @hide 414 */ 415 public CharSequence getFormat() { 416 return mFormat; 417 } 418 419 /** 420 * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} 421 * depending on whether the user has selected 24-hour format. 422 * 423 * @param handleTicker true if calling this method should schedule/unschedule the 424 * time ticker, false otherwise 425 */ 426 private void chooseFormat(boolean handleTicker) { 427 final boolean format24Requested = is24HourModeEnabled(); 428 429 LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale); 430 431 if (format24Requested) { 432 mFormat = abc(mFormat24, mFormat12, ld.timeFormat24); 433 } else { 434 mFormat = abc(mFormat12, mFormat24, ld.timeFormat12); 435 } 436 437 boolean hadSeconds = mHasSeconds; 438 mHasSeconds = DateFormat.hasSeconds(mFormat); 439 440 if (handleTicker && mAttached && hadSeconds != mHasSeconds) { 441 if (hadSeconds) getHandler().removeCallbacks(mTicker); 442 else mTicker.run(); 443 } 444 } 445 446 /** 447 * Returns a if not null, else return b if not null, else return c. 448 */ 449 private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) { 450 return a == null ? (b == null ? c : b) : a; 451 } 452 453 @Override 454 protected void onAttachedToWindow() { 455 super.onAttachedToWindow(); 456 457 if (!mAttached) { 458 mAttached = true; 459 460 registerReceiver(); 461 registerObserver(); 462 463 createTime(mTimeZone); 464 465 if (mHasSeconds) { 466 mTicker.run(); 467 } else { 468 onTimeChanged(); 469 } 470 } 471 } 472 473 @Override 474 protected void onDetachedFromWindow() { 475 super.onDetachedFromWindow(); 476 477 if (mAttached) { 478 unregisterReceiver(); 479 unregisterObserver(); 480 481 getHandler().removeCallbacks(mTicker); 482 483 mAttached = false; 484 } 485 } 486 487 private void registerReceiver() { 488 final IntentFilter filter = new IntentFilter(); 489 490 filter.addAction(Intent.ACTION_TIME_TICK); 491 filter.addAction(Intent.ACTION_TIME_CHANGED); 492 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 493 494 getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); 495 } 496 497 private void registerObserver() { 498 final ContentResolver resolver = getContext().getContentResolver(); 499 resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver); 500 } 501 502 private void unregisterReceiver() { 503 getContext().unregisterReceiver(mIntentReceiver); 504 } 505 506 private void unregisterObserver() { 507 final ContentResolver resolver = getContext().getContentResolver(); 508 resolver.unregisterContentObserver(mFormatChangeObserver); 509 } 510 511 private void onTimeChanged() { 512 mTime.setTimeInMillis(System.currentTimeMillis()); 513 setText(DateFormat.format(mFormat, mTime)); 514 } 515 } 516