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