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.util.Slog; 29 import android.view.accessibility.AccessibilityEvent; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 import android.widget.RemoteViews.RemoteView; 32 33 import java.util.Formatter; 34 import java.util.IllegalFormatException; 35 import java.util.Locale; 36 37 /** 38 * Class that implements a simple timer. 39 * <p> 40 * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, 41 * and it counts up from that, or if you don't give it a base time, it will use the 42 * time at which you call {@link #start}. By default it will display the current 43 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} 44 * to format the timer value into an arbitrary string. 45 * 46 * @attr ref android.R.styleable#Chronometer_format 47 */ 48 @RemoteView 49 public class Chronometer extends TextView { 50 private static final String TAG = "Chronometer"; 51 52 /** 53 * A callback that notifies when the chronometer has incremented on its own. 54 */ 55 public interface OnChronometerTickListener { 56 57 /** 58 * Notification that the chronometer has changed. 59 */ onChronometerTick(Chronometer chronometer)60 void onChronometerTick(Chronometer chronometer); 61 62 } 63 64 private long mBase; 65 private boolean mVisible; 66 private boolean mStarted; 67 private boolean mRunning; 68 private boolean mLogged; 69 private String mFormat; 70 private Formatter mFormatter; 71 private Locale mFormatterLocale; 72 private Object[] mFormatterArgs = new Object[1]; 73 private StringBuilder mFormatBuilder; 74 private OnChronometerTickListener mOnChronometerTickListener; 75 private StringBuilder mRecycle = new StringBuilder(8); 76 77 private static final int TICK_WHAT = 2; 78 79 /** 80 * Initialize this Chronometer object. 81 * Sets the base to the current time. 82 */ Chronometer(Context context)83 public Chronometer(Context context) { 84 this(context, null, 0); 85 } 86 87 /** 88 * Initialize with standard view layout information. 89 * Sets the base to the current time. 90 */ Chronometer(Context context, AttributeSet attrs)91 public Chronometer(Context context, AttributeSet attrs) { 92 this(context, attrs, 0); 93 } 94 95 /** 96 * Initialize with standard view layout information and style. 97 * Sets the base to the current time. 98 */ Chronometer(Context context, AttributeSet attrs, int defStyle)99 public Chronometer(Context context, AttributeSet attrs, int defStyle) { 100 super(context, attrs, defStyle); 101 102 TypedArray a = context.obtainStyledAttributes( 103 attrs, 104 com.android.internal.R.styleable.Chronometer, defStyle, 0); 105 setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); 106 a.recycle(); 107 108 init(); 109 } 110 init()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 setBase(long base)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 */ getBase()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 setFormat(String format)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 */ getFormat()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 */ setOnChronometerTickListener(OnChronometerTickListener listener)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 */ getOnChronometerTickListener()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 */ start()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 */ stop()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 setStarted(boolean started)208 public void setStarted(boolean started) { 209 mStarted = started; 210 updateRunning(); 211 } 212 213 @Override onDetachedFromWindow()214 protected void onDetachedFromWindow() { 215 super.onDetachedFromWindow(); 216 mVisible = false; 217 updateRunning(); 218 } 219 220 @Override onWindowVisibilityChanged(int visibility)221 protected void onWindowVisibilityChanged(int visibility) { 222 super.onWindowVisibilityChanged(visibility); 223 mVisible = visibility == VISIBLE; 224 updateRunning(); 225 } 226 updateText(long now)227 private synchronized void updateText(long now) { 228 long seconds = now - mBase; 229 seconds /= 1000; 230 String text = DateUtils.formatElapsedTime(mRecycle, seconds); 231 232 if (mFormat != null) { 233 Locale loc = Locale.getDefault(); 234 if (mFormatter == null || !loc.equals(mFormatterLocale)) { 235 mFormatterLocale = loc; 236 mFormatter = new Formatter(mFormatBuilder, loc); 237 } 238 mFormatBuilder.setLength(0); 239 mFormatterArgs[0] = text; 240 try { 241 mFormatter.format(mFormat, mFormatterArgs); 242 text = mFormatBuilder.toString(); 243 } catch (IllegalFormatException ex) { 244 if (!mLogged) { 245 Log.w(TAG, "Illegal format string: " + mFormat); 246 mLogged = true; 247 } 248 } 249 } 250 setText(text); 251 } 252 updateRunning()253 private void updateRunning() { 254 boolean running = mVisible && mStarted; 255 if (running != mRunning) { 256 if (running) { 257 updateText(SystemClock.elapsedRealtime()); 258 dispatchChronometerTick(); 259 mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000); 260 } else { 261 mHandler.removeMessages(TICK_WHAT); 262 } 263 mRunning = running; 264 } 265 } 266 267 private Handler mHandler = new Handler() { 268 public void handleMessage(Message m) { 269 if (mRunning) { 270 updateText(SystemClock.elapsedRealtime()); 271 dispatchChronometerTick(); 272 sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); 273 } 274 } 275 }; 276 dispatchChronometerTick()277 void dispatchChronometerTick() { 278 if (mOnChronometerTickListener != null) { 279 mOnChronometerTickListener.onChronometerTick(this); 280 } 281 } 282 283 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)284 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 285 super.onInitializeAccessibilityEvent(event); 286 event.setClassName(Chronometer.class.getName()); 287 } 288 289 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)290 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 291 super.onInitializeAccessibilityNodeInfo(info); 292 info.setClassName(Chronometer.class.getName()); 293 } 294 } 295