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