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.graphics.Canvas; 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.widget.RemoteViews.RemoteView; 29 30 import java.util.Formatter; 31 import java.util.IllegalFormatException; 32 import 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}. By default it will display the current 40 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} 41 * to format the timer value into an arbitrary string. 42 * 43 * @attr ref android.R.styleable#Chronometer_format 44 */ 45 @RemoteView 46 public class Chronometer extends TextView { 47 private static final String TAG = "Chronometer"; 48 49 /** 50 * A callback that notifies when the chronometer has incremented on its own. 51 */ 52 public interface OnChronometerTickListener { 53 54 /** 55 * Notification that the chronometer has changed. 56 */ 57 void onChronometerTick(Chronometer chronometer); 58 59 } 60 61 private long mBase; 62 private boolean mVisible; 63 private boolean mStarted; 64 private boolean mRunning; 65 private boolean mLogged; 66 private String mFormat; 67 private Formatter mFormatter; 68 private Locale mFormatterLocale; 69 private Object[] mFormatterArgs = new Object[1]; 70 private StringBuilder mFormatBuilder; 71 private OnChronometerTickListener mOnChronometerTickListener; 72 private StringBuilder mRecycle = new StringBuilder(8); 73 74 private static final int TICK_WHAT = 2; 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 defStyle) { 97 super(context, attrs, defStyle); 98 99 TypedArray a = context.obtainStyledAttributes( 100 attrs, 101 com.android.internal.R.styleable.Chronometer, defStyle, 0); 102 setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); 103 a.recycle(); 104 105 init(); 106 } 107 108 private void init() { 109 mBase = SystemClock.elapsedRealtime(); 110 updateText(mBase); 111 } 112 113 /** 114 * Set the time that the count-up timer is in reference to. 115 * 116 * @param base Use the {@link SystemClock#elapsedRealtime} time base. 117 */ 118 @android.view.RemotableViewMethod 119 public void setBase(long base) { 120 mBase = base; 121 dispatchChronometerTick(); 122 updateText(SystemClock.elapsedRealtime()); 123 } 124 125 /** 126 * Return the base time as set through {@link #setBase}. 127 */ 128 public long getBase() { 129 return mBase; 130 } 131 132 /** 133 * Sets the format string used for display. The Chronometer will display 134 * this string, with the first "%s" replaced by the current timer value in 135 * "MM:SS" or "H:MM:SS" form. 136 * 137 * If the format string is null, or if you never call setFormat(), the 138 * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" 139 * form. 140 * 141 * @param format the format string. 142 */ 143 @android.view.RemotableViewMethod 144 public void setFormat(String format) { 145 mFormat = format; 146 if (format != null && mFormatBuilder == null) { 147 mFormatBuilder = new StringBuilder(format.length() * 2); 148 } 149 } 150 151 /** 152 * Returns the current format string as set through {@link #setFormat}. 153 */ 154 public String getFormat() { 155 return mFormat; 156 } 157 158 /** 159 * Sets the listener to be called when the chronometer changes. 160 * 161 * @param listener The listener. 162 */ 163 public void setOnChronometerTickListener(OnChronometerTickListener listener) { 164 mOnChronometerTickListener = listener; 165 } 166 167 /** 168 * @return The listener (may be null) that is listening for chronometer change 169 * events. 170 */ 171 public OnChronometerTickListener getOnChronometerTickListener() { 172 return mOnChronometerTickListener; 173 } 174 175 /** 176 * Start counting up. This does not affect the base as set from {@link #setBase}, just 177 * the view display. 178 * 179 * Chronometer works by regularly scheduling messages to the handler, even when the 180 * Widget is not visible. To make sure resource leaks do not occur, the user should 181 * make sure that each start() call has a reciprocal call to {@link #stop}. 182 */ 183 public void start() { 184 mStarted = true; 185 updateRunning(); 186 } 187 188 /** 189 * Stop counting up. This does not affect the base as set from {@link #setBase}, just 190 * the view display. 191 * 192 * This stops the messages to the handler, effectively releasing resources that would 193 * be held as the chronometer is running, via {@link #start}. 194 */ 195 public void stop() { 196 mStarted = false; 197 updateRunning(); 198 } 199 200 /** 201 * The same as calling {@link #start} or {@link #stop}. 202 * @hide pending API council approval 203 */ 204 @android.view.RemotableViewMethod 205 public void setStarted(boolean started) { 206 mStarted = started; 207 updateRunning(); 208 } 209 210 @Override 211 protected void onDetachedFromWindow() { 212 super.onDetachedFromWindow(); 213 mVisible = false; 214 updateRunning(); 215 } 216 217 @Override 218 protected void onWindowVisibilityChanged(int visibility) { 219 super.onWindowVisibilityChanged(visibility); 220 mVisible = visibility == VISIBLE; 221 updateRunning(); 222 } 223 224 private synchronized void updateText(long now) { 225 long seconds = now - mBase; 226 seconds /= 1000; 227 String text = DateUtils.formatElapsedTime(mRecycle, seconds); 228 229 if (mFormat != null) { 230 Locale loc = Locale.getDefault(); 231 if (mFormatter == null || !loc.equals(mFormatterLocale)) { 232 mFormatterLocale = loc; 233 mFormatter = new Formatter(mFormatBuilder, loc); 234 } 235 mFormatBuilder.setLength(0); 236 mFormatterArgs[0] = text; 237 try { 238 mFormatter.format(mFormat, mFormatterArgs); 239 text = mFormatBuilder.toString(); 240 } catch (IllegalFormatException ex) { 241 if (!mLogged) { 242 Log.w(TAG, "Illegal format string: " + mFormat); 243 mLogged = true; 244 } 245 } 246 } 247 setText(text); 248 } 249 250 private void updateRunning() { 251 boolean running = mVisible && mStarted; 252 if (running != mRunning) { 253 if (running) { 254 updateText(SystemClock.elapsedRealtime()); 255 dispatchChronometerTick(); 256 mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000); 257 } else { 258 mHandler.removeMessages(TICK_WHAT); 259 } 260 mRunning = running; 261 } 262 } 263 264 private Handler mHandler = new Handler() { 265 public void handleMessage(Message m) { 266 if (mRunning) { 267 updateText(SystemClock.elapsedRealtime()); 268 dispatchChronometerTick(); 269 sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); 270 } 271 } 272 }; 273 274 void dispatchChronometerTick() { 275 if (mOnChronometerTickListener != null) { 276 mOnChronometerTickListener.onChronometerTick(this); 277 } 278 } 279 } 280