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