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.TypedArray; 21 import android.os.Handler; 22 import android.os.Message; 23 import android.os.SystemClock; 24 import android.text.format.DateUtils; 25 import android.util.AttributeSet; 26 import android.util.Log; 27 import android.view.accessibility.AccessibilityEvent; 28 import android.view.accessibility.AccessibilityNodeInfo; 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 */ 58 void onChronometerTick(Chronometer chronometer); 59 60 } 61 62 private long mBase; 63 private boolean mVisible; 64 private boolean mStarted; 65 private boolean mRunning; 66 private boolean mLogged; 67 private String mFormat; 68 private Formatter mFormatter; 69 private Locale mFormatterLocale; 70 private Object[] mFormatterArgs = new Object[1]; 71 private StringBuilder mFormatBuilder; 72 private OnChronometerTickListener mOnChronometerTickListener; 73 private StringBuilder mRecycle = new StringBuilder(8); 74 75 private static final int TICK_WHAT = 2; 76 77 /** 78 * Initialize this Chronometer object. 79 * Sets the base to the current time. 80 */ 81 public Chronometer(Context context) { 82 this(context, null, 0); 83 } 84 85 /** 86 * Initialize with standard view layout information. 87 * Sets the base to the current time. 88 */ 89 public Chronometer(Context context, AttributeSet attrs) { 90 this(context, attrs, 0); 91 } 92 93 /** 94 * Initialize with standard view layout information and style. 95 * Sets the base to the current time. 96 */ 97 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { 98 this(context, attrs, defStyleAttr, 0); 99 } 100 101 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 102 super(context, attrs, defStyleAttr, defStyleRes); 103 104 final TypedArray a = context.obtainStyledAttributes( 105 attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); 106 setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); 107 a.recycle(); 108 109 init(); 110 } 111 112 private void init() { 113 mBase = SystemClock.elapsedRealtime(); 114 updateText(mBase); 115 } 116 117 /** 118 * Set the time that the count-up timer is in reference to. 119 * 120 * @param base Use the {@link SystemClock#elapsedRealtime} time base. 121 */ 122 @android.view.RemotableViewMethod 123 public void setBase(long base) { 124 mBase = base; 125 dispatchChronometerTick(); 126 updateText(SystemClock.elapsedRealtime()); 127 } 128 129 /** 130 * Return the base time as set through {@link #setBase}. 131 */ 132 public long getBase() { 133 return mBase; 134 } 135 136 /** 137 * Sets the format string used for display. The Chronometer will display 138 * this string, with the first "%s" replaced by the current timer value in 139 * "MM:SS" or "H:MM:SS" form. 140 * 141 * If the format string is null, or if you never call setFormat(), the 142 * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" 143 * form. 144 * 145 * @param format the format string. 146 */ 147 @android.view.RemotableViewMethod 148 public void setFormat(String format) { 149 mFormat = format; 150 if (format != null && mFormatBuilder == null) { 151 mFormatBuilder = new StringBuilder(format.length() * 2); 152 } 153 } 154 155 /** 156 * Returns the current format string as set through {@link #setFormat}. 157 */ 158 public String getFormat() { 159 return mFormat; 160 } 161 162 /** 163 * Sets the listener to be called when the chronometer changes. 164 * 165 * @param listener The listener. 166 */ 167 public void setOnChronometerTickListener(OnChronometerTickListener listener) { 168 mOnChronometerTickListener = listener; 169 } 170 171 /** 172 * @return The listener (may be null) that is listening for chronometer change 173 * events. 174 */ 175 public OnChronometerTickListener getOnChronometerTickListener() { 176 return mOnChronometerTickListener; 177 } 178 179 /** 180 * Start counting up. This does not affect the base as set from {@link #setBase}, just 181 * the view display. 182 * 183 * Chronometer works by regularly scheduling messages to the handler, even when the 184 * Widget is not visible. To make sure resource leaks do not occur, the user should 185 * make sure that each start() call has a reciprocal call to {@link #stop}. 186 */ 187 public void start() { 188 mStarted = true; 189 updateRunning(); 190 } 191 192 /** 193 * Stop counting up. This does not affect the base as set from {@link #setBase}, just 194 * the view display. 195 * 196 * This stops the messages to the handler, effectively releasing resources that would 197 * be held as the chronometer is running, via {@link #start}. 198 */ 199 public void stop() { 200 mStarted = false; 201 updateRunning(); 202 } 203 204 /** 205 * The same as calling {@link #start} or {@link #stop}. 206 * @hide pending API council approval 207 */ 208 @android.view.RemotableViewMethod 209 public void setStarted(boolean started) { 210 mStarted = started; 211 updateRunning(); 212 } 213 214 @Override 215 protected void onDetachedFromWindow() { 216 super.onDetachedFromWindow(); 217 mVisible = false; 218 updateRunning(); 219 } 220 221 @Override 222 protected void onWindowVisibilityChanged(int visibility) { 223 super.onWindowVisibilityChanged(visibility); 224 mVisible = visibility == VISIBLE; 225 updateRunning(); 226 } 227 228 private synchronized void updateText(long 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 mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000); 261 } else { 262 mHandler.removeMessages(TICK_WHAT); 263 } 264 mRunning = running; 265 } 266 } 267 268 private Handler mHandler = new Handler() { 269 public void handleMessage(Message m) { 270 if (mRunning) { 271 updateText(SystemClock.elapsedRealtime()); 272 dispatchChronometerTick(); 273 sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); 274 } 275 } 276 }; 277 278 void dispatchChronometerTick() { 279 if (mOnChronometerTickListener != null) { 280 mOnChronometerTickListener.onChronometerTick(this); 281 } 282 } 283 284 @Override 285 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 286 super.onInitializeAccessibilityEvent(event); 287 event.setClassName(Chronometer.class.getName()); 288 } 289 290 @Override 291 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 292 super.onInitializeAccessibilityNodeInfo(info); 293 info.setClassName(Chronometer.class.getName()); 294 } 295 } 296