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
17package android.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Handler;
22import android.os.Message;
23import android.os.SystemClock;
24import android.text.format.DateUtils;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.accessibility.AccessibilityEvent;
28import android.view.accessibility.AccessibilityNodeInfo;
29import android.widget.RemoteViews.RemoteView;
30
31import java.util.Formatter;
32import java.util.IllegalFormatException;
33import 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
47public 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