Chronometer.java revision 99441c5d7da45c10b729185852be97cbb0bdc8d5
1/* 2 * Copyright (C) 2008 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.Context; 20import android.content.res.Resources; 21import android.content.res.TypedArray; 22import android.os.SystemClock; 23import android.text.format.DateUtils; 24import android.util.AttributeSet; 25import android.util.Log; 26import android.view.View; 27import android.widget.RemoteViews.RemoteView; 28 29import com.android.internal.R; 30 31import java.util.Formatter; 32import java.util.IllegalFormatException; 33import java.util.Locale; 34 35/** 36 * Class that implements a simple timer. 37 * <p> 38 * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, 39 * and it counts up from that, or if you don't give it a base time, it will use the 40 * time at which you call {@link #start}. 41 * 42 * <p>The timer can also count downward towards the base time by 43 * setting {@link #setCountDown(boolean)} to true. 44 * 45 * <p>By default it will display the current 46 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} 47 * to format the timer value into an arbitrary string. 48 * 49 * @attr ref android.R.styleable#Chronometer_format 50 * @attr ref android.R.styleable#Chronometer_countDown 51 */ 52@RemoteView 53public class Chronometer extends TextView { 54 private static final String TAG = "Chronometer"; 55 56 /** 57 * A callback that notifies when the chronometer has incremented on its own. 58 */ 59 public interface OnChronometerTickListener { 60 61 /** 62 * Notification that the chronometer has changed. 63 */ 64 void onChronometerTick(Chronometer chronometer); 65 66 } 67 68 private long mBase; 69 private long mNow; // the currently displayed time 70 private boolean mVisible; 71 private boolean mStarted; 72 private boolean mRunning; 73 private boolean mLogged; 74 private String mFormat; 75 private Formatter mFormatter; 76 private Locale mFormatterLocale; 77 private Object[] mFormatterArgs = new Object[1]; 78 private StringBuilder mFormatBuilder; 79 private OnChronometerTickListener mOnChronometerTickListener; 80 private StringBuilder mRecycle = new StringBuilder(8); 81 private boolean mCountDown; 82 83 /** 84 * Initialize this Chronometer object. 85 * Sets the base to the current time. 86 */ 87 public Chronometer(Context context) { 88 this(context, null, 0); 89 } 90 91 /** 92 * Initialize with standard view layout information. 93 * Sets the base to the current time. 94 */ 95 public Chronometer(Context context, AttributeSet attrs) { 96 this(context, attrs, 0); 97 } 98 99 /** 100 * Initialize with standard view layout information and style. 101 * Sets the base to the current time. 102 */ 103 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { 104 this(context, attrs, defStyleAttr, 0); 105 } 106 107 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 108 super(context, attrs, defStyleAttr, defStyleRes); 109 110 final TypedArray a = context.obtainStyledAttributes( 111 attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); 112 setFormat(a.getString(R.styleable.Chronometer_format)); 113 setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false)); 114 a.recycle(); 115 116 init(); 117 } 118 119 private void init() { 120 mBase = SystemClock.elapsedRealtime(); 121 updateText(mBase); 122 } 123 124 /** 125 * Set this view to count down to the base instead of counting up from it. 126 * 127 * @param countDown whether this view should count down 128 * 129 * @see #setBase(long) 130 */ 131 @android.view.RemotableViewMethod 132 public void setCountDown(boolean countDown) { 133 mCountDown = countDown; 134 updateText(SystemClock.elapsedRealtime()); 135 } 136 137 /** 138 * @return whether this view counts down 139 * 140 * @see #setCountDown(boolean) 141 */ 142 public boolean isCountDown() { 143 return mCountDown; 144 } 145 146 /** 147 * Set the time that the count-up timer is in reference to. 148 * 149 * @param base Use the {@link SystemClock#elapsedRealtime} time base. 150 */ 151 @android.view.RemotableViewMethod 152 public void setBase(long base) { 153 mBase = base; 154 dispatchChronometerTick(); 155 updateText(SystemClock.elapsedRealtime()); 156 } 157 158 /** 159 * Return the base time as set through {@link #setBase}. 160 */ 161 public long getBase() { 162 return mBase; 163 } 164 165 /** 166 * Sets the format string used for display. The Chronometer will display 167 * this string, with the first "%s" replaced by the current timer value in 168 * "MM:SS" or "H:MM:SS" form. 169 * 170 * If the format string is null, or if you never call setFormat(), the 171 * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" 172 * form. 173 * 174 * @param format the format string. 175 */ 176 @android.view.RemotableViewMethod 177 public void setFormat(String format) { 178 mFormat = format; 179 if (format != null && mFormatBuilder == null) { 180 mFormatBuilder = new StringBuilder(format.length() * 2); 181 } 182 } 183 184 /** 185 * Returns the current format string as set through {@link #setFormat}. 186 */ 187 public String getFormat() { 188 return mFormat; 189 } 190 191 /** 192 * Sets the listener to be called when the chronometer changes. 193 * 194 * @param listener The listener. 195 */ 196 public void setOnChronometerTickListener(OnChronometerTickListener listener) { 197 mOnChronometerTickListener = listener; 198 } 199 200 /** 201 * @return The listener (may be null) that is listening for chronometer change 202 * events. 203 */ 204 public OnChronometerTickListener getOnChronometerTickListener() { 205 return mOnChronometerTickListener; 206 } 207 208 /** 209 * Start counting up. This does not affect the base as set from {@link #setBase}, just 210 * the view display. 211 * 212 * Chronometer works by regularly scheduling messages to the handler, even when the 213 * Widget is not visible. To make sure resource leaks do not occur, the user should 214 * make sure that each start() call has a reciprocal call to {@link #stop}. 215 */ 216 public void start() { 217 mStarted = true; 218 updateRunning(); 219 } 220 221 /** 222 * Stop counting up. This does not affect the base as set from {@link #setBase}, just 223 * the view display. 224 * 225 * This stops the messages to the handler, effectively releasing resources that would 226 * be held as the chronometer is running, via {@link #start}. 227 */ 228 public void stop() { 229 mStarted = false; 230 updateRunning(); 231 } 232 233 /** 234 * The same as calling {@link #start} or {@link #stop}. 235 * @hide pending API council approval 236 */ 237 @android.view.RemotableViewMethod 238 public void setStarted(boolean started) { 239 mStarted = started; 240 updateRunning(); 241 } 242 243 @Override 244 protected void onDetachedFromWindow() { 245 super.onDetachedFromWindow(); 246 mVisible = false; 247 updateRunning(); 248 } 249 250 @Override 251 protected void onWindowVisibilityChanged(int visibility) { 252 super.onWindowVisibilityChanged(visibility); 253 mVisible = visibility == VISIBLE; 254 updateRunning(); 255 } 256 257 @Override 258 protected void onVisibilityChanged(View changedView, int visibility) { 259 super.onVisibilityChanged(changedView, visibility); 260 updateRunning(); 261 } 262 263 private synchronized void updateText(long now) { 264 mNow = now; 265 long seconds = mCountDown ? mBase - now : now - mBase; 266 seconds /= 1000; 267 boolean negative = false; 268 if (seconds < 0) { 269 seconds = -seconds; 270 negative = true; 271 } 272 String text = DateUtils.formatElapsedTime(mRecycle, seconds); 273 if (negative) { 274 text = getResources().getString(R.string.negative_duration, text); 275 } 276 277 if (mFormat != null) { 278 Locale loc = Locale.getDefault(); 279 if (mFormatter == null || !loc.equals(mFormatterLocale)) { 280 mFormatterLocale = loc; 281 mFormatter = new Formatter(mFormatBuilder, loc); 282 } 283 mFormatBuilder.setLength(0); 284 mFormatterArgs[0] = text; 285 try { 286 mFormatter.format(mFormat, mFormatterArgs); 287 text = mFormatBuilder.toString(); 288 } catch (IllegalFormatException ex) { 289 if (!mLogged) { 290 Log.w(TAG, "Illegal format string: " + mFormat); 291 mLogged = true; 292 } 293 } 294 } 295 setText(text); 296 } 297 298 private void updateRunning() { 299 boolean running = mVisible && mStarted && isShown(); 300 if (running != mRunning) { 301 if (running) { 302 updateText(SystemClock.elapsedRealtime()); 303 dispatchChronometerTick(); 304 postDelayed(mTickRunnable, 1000); 305 } else { 306 removeCallbacks(mTickRunnable); 307 } 308 mRunning = running; 309 } 310 } 311 312 private final Runnable mTickRunnable = new Runnable() { 313 @Override 314 public void run() { 315 if (mRunning) { 316 updateText(SystemClock.elapsedRealtime()); 317 dispatchChronometerTick(); 318 postDelayed(mTickRunnable, 1000); 319 } 320 } 321 }; 322 323 void dispatchChronometerTick() { 324 if (mOnChronometerTickListener != null) { 325 mOnChronometerTickListener.onChronometerTick(this); 326 } 327 } 328 329 private static final int MIN_IN_SEC = 60; 330 private static final int HOUR_IN_SEC = MIN_IN_SEC*60; 331 private static String formatDuration(long ms) { 332 final Resources res = Resources.getSystem(); 333 final StringBuilder text = new StringBuilder(); 334 335 int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS); 336 if (duration < 0) { 337 duration = -duration; 338 } 339 340 int h = 0; 341 int m = 0; 342 343 if (duration >= HOUR_IN_SEC) { 344 h = duration / HOUR_IN_SEC; 345 duration -= h * HOUR_IN_SEC; 346 } 347 if (duration >= MIN_IN_SEC) { 348 m = duration / MIN_IN_SEC; 349 duration -= m * MIN_IN_SEC; 350 } 351 int s = duration; 352 353 try { 354 if (h > 0) { 355 text.append(res.getQuantityString( 356 com.android.internal.R.plurals.duration_hours, h, h)); 357 } 358 if (m > 0) { 359 if (text.length() > 0) { 360 text.append(' '); 361 } 362 text.append(res.getQuantityString( 363 com.android.internal.R.plurals.duration_minutes, m, m)); 364 } 365 366 if (text.length() > 0) { 367 text.append(' '); 368 } 369 text.append(res.getQuantityString( 370 com.android.internal.R.plurals.duration_seconds, s, s)); 371 } catch (Resources.NotFoundException e) { 372 // Ignore; plurals throws an exception for an untranslated quantity for a given locale. 373 return null; 374 } 375 return text.toString(); 376 } 377 378 @Override 379 public CharSequence getContentDescription() { 380 return formatDuration(mNow - mBase); 381 } 382 383 @Override 384 public CharSequence getAccessibilityClassName() { 385 return Chronometer.class.getName(); 386 } 387} 388