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