blob: b9224f36a922854c8b2cf98c9ffff6600eba5da1 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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;
Dan Sandlera79a7472015-06-05 16:52:22 -040020import android.content.res.Resources;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.content.res.TypedArray;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.os.SystemClock;
23import android.text.format.DateUtils;
24import android.util.AttributeSet;
25import android.util.Log;
Simon Dubray814e1f52015-11-05 11:38:40 +010026import android.view.View;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.widget.RemoteViews.RemoteView;
28
Selim Cineked1a33c2016-02-18 17:12:57 -080029import com.android.internal.R;
30
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031import 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
Selim Cineked1a33c2016-02-18 17:12:57 -080040 * time at which you call {@link #start}.
41 *
42 * <p>The timer can also count downward towards the base time by
43 * setting {@link #setCountDown(boolean)} to true.
44 *
45 * <p>By default it will display the current
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
47 * to format the timer value into an arbitrary string.
48 *
49 * @attr ref android.R.styleable#Chronometer_format
Selim Cineked1a33c2016-02-18 17:12:57 -080050 * @attr ref android.R.styleable#Chronometer_countDown
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051 */
52@RemoteView
53public class Chronometer extends TextView {
54 private static final String TAG = "Chronometer";
55
56 /**
57 * A callback that notifies when the chronometer has incremented on its own.
58 */
59 public interface OnChronometerTickListener {
60
61 /**
62 * Notification that the chronometer has changed.
63 */
64 void onChronometerTick(Chronometer chronometer);
65
66 }
67
68 private long mBase;
Dan Sandlera79a7472015-06-05 16:52:22 -040069 private long mNow; // the currently displayed time
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070 private boolean mVisible;
71 private boolean mStarted;
72 private boolean mRunning;
73 private boolean mLogged;
74 private String mFormat;
75 private Formatter mFormatter;
76 private Locale mFormatterLocale;
77 private Object[] mFormatterArgs = new Object[1];
78 private StringBuilder mFormatBuilder;
79 private OnChronometerTickListener mOnChronometerTickListener;
80 private StringBuilder mRecycle = new StringBuilder(8);
Selim Cineked1a33c2016-02-18 17:12:57 -080081 private boolean mCountDown;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070082
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080083 /**
84 * Initialize this Chronometer object.
85 * Sets the base to the current time.
86 */
87 public Chronometer(Context context) {
88 this(context, null, 0);
89 }
90
91 /**
92 * Initialize with standard view layout information.
93 * Sets the base to the current time.
94 */
95 public Chronometer(Context context, AttributeSet attrs) {
96 this(context, attrs, 0);
97 }
98
99 /**
100 * Initialize with standard view layout information and style.
101 * Sets the base to the current time.
102 */
Alan Viverette617feb92013-09-09 18:09:13 -0700103 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) {
104 this(context, attrs, defStyleAttr, 0);
105 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800106
Alan Viverette617feb92013-09-09 18:09:13 -0700107 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
108 super(context, attrs, defStyleAttr, defStyleRes);
109
110 final TypedArray a = context.obtainStyledAttributes(
111 attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
Selim Cineked1a33c2016-02-18 17:12:57 -0800112 setFormat(a.getString(R.styleable.Chronometer_format));
113 setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800114 a.recycle();
115
116 init();
117 }
118
119 private void init() {
120 mBase = SystemClock.elapsedRealtime();
121 updateText(mBase);
122 }
123
124 /**
Selim Cineked1a33c2016-02-18 17:12:57 -0800125 * Set this view to count down to the base instead of counting up from it.
126 *
127 * @param countDown whether this view should count down
128 *
129 * @see #setBase(long)
130 */
131 @android.view.RemotableViewMethod
132 public void setCountDown(boolean countDown) {
133 mCountDown = countDown;
Selim Cineka2a01712016-05-18 16:59:07 -0700134 updateText(SystemClock.elapsedRealtime());
Selim Cineked1a33c2016-02-18 17:12:57 -0800135 }
136
137 /**
138 * @return whether this view counts down
139 *
140 * @see #setCountDown(boolean)
141 */
142 public boolean isCountDown() {
143 return mCountDown;
144 }
145
146 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800147 * Set the time that the count-up timer is in reference to.
148 *
149 * @param base Use the {@link SystemClock#elapsedRealtime} time base.
150 */
151 @android.view.RemotableViewMethod
152 public void setBase(long base) {
153 mBase = base;
154 dispatchChronometerTick();
155 updateText(SystemClock.elapsedRealtime());
156 }
157
158 /**
159 * Return the base time as set through {@link #setBase}.
160 */
161 public long getBase() {
162 return mBase;
163 }
164
165 /**
166 * Sets the format string used for display. The Chronometer will display
167 * this string, with the first "%s" replaced by the current timer value in
168 * "MM:SS" or "H:MM:SS" form.
169 *
170 * If the format string is null, or if you never call setFormat(), the
171 * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
172 * form.
173 *
174 * @param format the format string.
175 */
176 @android.view.RemotableViewMethod
177 public void setFormat(String format) {
178 mFormat = format;
179 if (format != null && mFormatBuilder == null) {
180 mFormatBuilder = new StringBuilder(format.length() * 2);
181 }
182 }
183
184 /**
185 * Returns the current format string as set through {@link #setFormat}.
186 */
187 public String getFormat() {
188 return mFormat;
189 }
190
191 /**
192 * Sets the listener to be called when the chronometer changes.
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700193 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800194 * @param listener The listener.
195 */
196 public void setOnChronometerTickListener(OnChronometerTickListener listener) {
197 mOnChronometerTickListener = listener;
198 }
199
200 /**
201 * @return The listener (may be null) that is listening for chronometer change
202 * events.
203 */
204 public OnChronometerTickListener getOnChronometerTickListener() {
205 return mOnChronometerTickListener;
206 }
207
208 /**
209 * Start counting up. This does not affect the base as set from {@link #setBase}, just
210 * the view display.
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700211 *
212 * Chronometer works by regularly scheduling messages to the handler, even when the
213 * Widget is not visible. To make sure resource leaks do not occur, the user should
214 * make sure that each start() call has a reciprocal call to {@link #stop}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 */
216 public void start() {
217 mStarted = true;
218 updateRunning();
219 }
220
221 /**
222 * Stop counting up. This does not affect the base as set from {@link #setBase}, just
223 * the view display.
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700224 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800225 * This stops the messages to the handler, effectively releasing resources that would
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700226 * be held as the chronometer is running, via {@link #start}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227 */
228 public void stop() {
229 mStarted = false;
230 updateRunning();
231 }
232
233 /**
234 * The same as calling {@link #start} or {@link #stop}.
Jeffrey Sharkey3ff7eb92009-04-13 16:57:28 -0700235 * @hide pending API council approval
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800236 */
237 @android.view.RemotableViewMethod
238 public void setStarted(boolean started) {
239 mStarted = started;
240 updateRunning();
241 }
242
243 @Override
244 protected void onDetachedFromWindow() {
245 super.onDetachedFromWindow();
246 mVisible = false;
247 updateRunning();
248 }
249
250 @Override
251 protected void onWindowVisibilityChanged(int visibility) {
252 super.onWindowVisibilityChanged(visibility);
253 mVisible = visibility == VISIBLE;
254 updateRunning();
255 }
256
Simon Dubray814e1f52015-11-05 11:38:40 +0100257 @Override
258 protected void onVisibilityChanged(View changedView, int visibility) {
259 super.onVisibilityChanged(changedView, visibility);
260 updateRunning();
261 }
262
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800263 private synchronized void updateText(long now) {
Dan Sandlera79a7472015-06-05 16:52:22 -0400264 mNow = now;
Selim Cineked1a33c2016-02-18 17:12:57 -0800265 long seconds = mCountDown ? mBase - now : now - mBase;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800266 seconds /= 1000;
Selim Cineked1a33c2016-02-18 17:12:57 -0800267 boolean negative = false;
268 if (seconds < 0) {
269 seconds = -seconds;
270 negative = true;
271 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800272 String text = DateUtils.formatElapsedTime(mRecycle, seconds);
Selim Cineked1a33c2016-02-18 17:12:57 -0800273 if (negative) {
274 text = getResources().getString(R.string.negative_duration, text);
275 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276
277 if (mFormat != null) {
278 Locale loc = Locale.getDefault();
279 if (mFormatter == null || !loc.equals(mFormatterLocale)) {
280 mFormatterLocale = loc;
281 mFormatter = new Formatter(mFormatBuilder, loc);
282 }
283 mFormatBuilder.setLength(0);
284 mFormatterArgs[0] = text;
285 try {
286 mFormatter.format(mFormat, mFormatterArgs);
287 text = mFormatBuilder.toString();
288 } catch (IllegalFormatException ex) {
289 if (!mLogged) {
290 Log.w(TAG, "Illegal format string: " + mFormat);
291 mLogged = true;
292 }
293 }
294 }
295 setText(text);
296 }
297
298 private void updateRunning() {
Simon Dubray814e1f52015-11-05 11:38:40 +0100299 boolean running = mVisible && mStarted && isShown();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800300 if (running != mRunning) {
301 if (running) {
302 updateText(SystemClock.elapsedRealtime());
303 dispatchChronometerTick();
John Reckd0374c62015-10-20 13:25:01 -0700304 postDelayed(mTickRunnable, 1000);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305 } else {
John Reckd0374c62015-10-20 13:25:01 -0700306 removeCallbacks(mTickRunnable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800307 }
308 mRunning = running;
309 }
310 }
John Reckd0374c62015-10-20 13:25:01 -0700311
312 private final Runnable mTickRunnable = new Runnable() {
313 @Override
314 public void run() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800315 if (mRunning) {
316 updateText(SystemClock.elapsedRealtime());
317 dispatchChronometerTick();
John Reckd0374c62015-10-20 13:25:01 -0700318 postDelayed(mTickRunnable, 1000);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 }
320 }
321 };
322
323 void dispatchChronometerTick() {
324 if (mOnChronometerTickListener != null) {
325 mOnChronometerTickListener.onChronometerTick(this);
326 }
327 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800328
Dan Sandlera79a7472015-06-05 16:52:22 -0400329 private static final int MIN_IN_SEC = 60;
330 private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
331 private static String formatDuration(long ms) {
332 final Resources res = Resources.getSystem();
333 final StringBuilder text = new StringBuilder();
334
335 int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
336 if (duration < 0) {
337 duration = -duration;
338 }
339
340 int h = 0;
341 int m = 0;
342
343 if (duration >= HOUR_IN_SEC) {
344 h = duration / HOUR_IN_SEC;
345 duration -= h * HOUR_IN_SEC;
346 }
347 if (duration >= MIN_IN_SEC) {
348 m = duration / MIN_IN_SEC;
349 duration -= m * MIN_IN_SEC;
350 }
351 int s = duration;
352
353 try {
354 if (h > 0) {
355 text.append(res.getQuantityString(
356 com.android.internal.R.plurals.duration_hours, h, h));
357 }
358 if (m > 0) {
359 if (text.length() > 0) {
360 text.append(' ');
361 }
362 text.append(res.getQuantityString(
363 com.android.internal.R.plurals.duration_minutes, m, m));
364 }
365
366 if (text.length() > 0) {
367 text.append(' ');
368 }
369 text.append(res.getQuantityString(
370 com.android.internal.R.plurals.duration_seconds, s, s));
371 } catch (Resources.NotFoundException e) {
372 // Ignore; plurals throws an exception for an untranslated quantity for a given locale.
373 return null;
374 }
375 return text.toString();
376 }
377
378 @Override
379 public CharSequence getContentDescription() {
380 return formatDuration(mNow - mBase);
381 }
382
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800383 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -0800384 public CharSequence getAccessibilityClassName() {
385 return Chronometer.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800386 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800387}