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