• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Intent;
21  import android.content.res.TypedArray;
22  import android.icu.text.MeasureFormat;
23  import android.icu.text.MeasureFormat.FormatWidth;
24  import android.icu.util.Measure;
25  import android.icu.util.MeasureUnit;
26  import android.net.Uri;
27  import android.os.SystemClock;
28  import android.text.format.DateUtils;
29  import android.util.AttributeSet;
30  import android.util.Log;
31  import android.view.View;
32  import android.view.inspector.InspectableProperty;
33  import android.widget.RemoteViews.RemoteView;
34  
35  import com.android.internal.R;
36  
37  import java.util.ArrayList;
38  import java.util.Formatter;
39  import java.util.IllegalFormatException;
40  import java.util.Locale;
41  
42  /**
43   * Class that implements a simple timer.
44   * <p>
45   * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase,
46   * and it counts up from that, or if you don't give it a base time, it will use the
47   * time at which you call {@link #start}.
48   *
49   * <p>The timer can also count downward towards the base time by
50   * setting {@link #setCountDown(boolean)} to true.
51   *
52   *  <p>By default it will display the current
53   * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
54   * to format the timer value into an arbitrary string.
55   *
56   * @attr ref android.R.styleable#Chronometer_format
57   * @attr ref android.R.styleable#Chronometer_countDown
58   */
59  @RemoteView
60  public class Chronometer extends TextView {
61      private static final String TAG = "Chronometer";
62  
63      /**
64       * A callback that notifies when the chronometer has incremented on its own.
65       */
66      public interface OnChronometerTickListener {
67  
68          /**
69           * Notification that the chronometer has changed.
70           */
onChronometerTick(Chronometer chronometer)71          void onChronometerTick(Chronometer chronometer);
72  
73      }
74  
75      private long mBase;
76      private long mNow; // the currently displayed time
77      private boolean mVisible;
78      private boolean mStarted;
79      private boolean mRunning;
80      private boolean mLogged;
81      private String mFormat;
82      private Formatter mFormatter;
83      private Locale mFormatterLocale;
84      private Object[] mFormatterArgs = new Object[1];
85      private StringBuilder mFormatBuilder;
86      private OnChronometerTickListener mOnChronometerTickListener;
87      private StringBuilder mRecycle = new StringBuilder(8);
88      private boolean mCountDown;
89  
90      /**
91       * Initialize this Chronometer object.
92       * Sets the base to the current time.
93       */
Chronometer(Context context)94      public Chronometer(Context context) {
95          this(context, null, 0);
96      }
97  
98      /**
99       * Initialize with standard view layout information.
100       * Sets the base to the current time.
101       */
Chronometer(Context context, AttributeSet attrs)102      public Chronometer(Context context, AttributeSet attrs) {
103          this(context, attrs, 0);
104      }
105  
106      /**
107       * Initialize with standard view layout information and style.
108       * Sets the base to the current time.
109       */
Chronometer(Context context, AttributeSet attrs, int defStyleAttr)110      public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) {
111          this(context, attrs, defStyleAttr, 0);
112      }
113  
Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)114      public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
115          super(context, attrs, defStyleAttr, defStyleRes);
116  
117          final TypedArray a = context.obtainStyledAttributes(
118                  attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
119          saveAttributeDataForStyleable(context, com.android.internal.R.styleable.Chronometer,
120                  attrs, a, defStyleAttr, defStyleRes);
121          setFormat(a.getString(R.styleable.Chronometer_format));
122          setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false));
123          a.recycle();
124  
125          init();
126      }
127  
init()128      private void init() {
129          mBase = SystemClock.elapsedRealtime();
130          updateText(mBase);
131      }
132  
133      /**
134       * Set this view to count down to the base instead of counting up from it.
135       *
136       * @param countDown whether this view should count down
137       *
138       * @see #setBase(long)
139       */
140      @android.view.RemotableViewMethod
setCountDown(boolean countDown)141      public void setCountDown(boolean countDown) {
142          mCountDown = countDown;
143          updateText(SystemClock.elapsedRealtime());
144      }
145  
146      /**
147       * @return whether this view counts down
148       *
149       * @see #setCountDown(boolean)
150       */
151      @InspectableProperty
isCountDown()152      public boolean isCountDown() {
153          return mCountDown;
154      }
155  
156      /**
157       * @return whether this is the final countdown
158       */
isTheFinalCountDown()159      public boolean isTheFinalCountDown() {
160          try {
161              getContext().startActivity(
162                      new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))
163                              .addCategory(Intent.CATEGORY_BROWSABLE)
164                              .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
165                                      | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT));
166              return true;
167          } catch (Exception e) {
168              return false;
169          }
170      }
171  
172      /**
173       * Set the time that the count-up timer is in reference to.
174       *
175       * @param base Use the {@link SystemClock#elapsedRealtime} time base.
176       */
177      @android.view.RemotableViewMethod
setBase(long base)178      public void setBase(long base) {
179          mBase = base;
180          dispatchChronometerTick();
181          updateText(SystemClock.elapsedRealtime());
182      }
183  
184      /**
185       * Return the base time as set through {@link #setBase}.
186       */
getBase()187      public long getBase() {
188          return mBase;
189      }
190  
191      /**
192       * Sets the format string used for display.  The Chronometer will display
193       * this string, with the first "%s" replaced by the current timer value in
194       * "MM:SS" or "H:MM:SS" form.
195       *
196       * If the format string is null, or if you never call setFormat(), the
197       * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
198       * form.
199       *
200       * @param format the format string.
201       */
202      @android.view.RemotableViewMethod
setFormat(String format)203      public void setFormat(String format) {
204          mFormat = format;
205          if (format != null && mFormatBuilder == null) {
206              mFormatBuilder = new StringBuilder(format.length() * 2);
207          }
208      }
209  
210      /**
211       * Returns the current format string as set through {@link #setFormat}.
212       */
213      @InspectableProperty
getFormat()214      public String getFormat() {
215          return mFormat;
216      }
217  
218      /**
219       * Sets the listener to be called when the chronometer changes.
220       *
221       * @param listener The listener.
222       */
setOnChronometerTickListener(OnChronometerTickListener listener)223      public void setOnChronometerTickListener(OnChronometerTickListener listener) {
224          mOnChronometerTickListener = listener;
225      }
226  
227      /**
228       * @return The listener (may be null) that is listening for chronometer change
229       *         events.
230       */
getOnChronometerTickListener()231      public OnChronometerTickListener getOnChronometerTickListener() {
232          return mOnChronometerTickListener;
233      }
234  
235      /**
236       * Start counting up.  This does not affect the base as set from {@link #setBase}, just
237       * the view display.
238       *
239       * Chronometer works by regularly scheduling messages to the handler, even when the
240       * Widget is not visible.  To make sure resource leaks do not occur, the user should
241       * make sure that each start() call has a reciprocal call to {@link #stop}.
242       */
start()243      public void start() {
244          mStarted = true;
245          updateRunning();
246      }
247  
248      /**
249       * Stop counting up.  This does not affect the base as set from {@link #setBase}, just
250       * the view display.
251       *
252       * This stops the messages to the handler, effectively releasing resources that would
253       * be held as the chronometer is running, via {@link #start}.
254       */
stop()255      public void stop() {
256          mStarted = false;
257          updateRunning();
258      }
259  
260      /**
261       * The same as calling {@link #start} or {@link #stop}.
262       * @hide pending API council approval
263       */
264      @android.view.RemotableViewMethod
setStarted(boolean started)265      public void setStarted(boolean started) {
266          mStarted = started;
267          updateRunning();
268      }
269  
270      @Override
onDetachedFromWindow()271      protected void onDetachedFromWindow() {
272          super.onDetachedFromWindow();
273          mVisible = false;
274          updateRunning();
275      }
276  
277      @Override
onWindowVisibilityChanged(int visibility)278      protected void onWindowVisibilityChanged(int visibility) {
279          super.onWindowVisibilityChanged(visibility);
280          mVisible = visibility == VISIBLE;
281          updateRunning();
282      }
283  
284      @Override
onVisibilityChanged(View changedView, int visibility)285      protected void onVisibilityChanged(View changedView, int visibility) {
286          super.onVisibilityChanged(changedView, visibility);
287          updateRunning();
288      }
289  
updateText(long now)290      private synchronized void updateText(long now) {
291          mNow = now;
292          long seconds = mCountDown ? mBase - now : now - mBase;
293          seconds /= 1000;
294          boolean negative = false;
295          if (seconds < 0) {
296              seconds = -seconds;
297              negative = true;
298          }
299          String text = DateUtils.formatElapsedTime(mRecycle, seconds);
300          if (negative) {
301              text = getResources().getString(R.string.negative_duration, text);
302          }
303  
304          if (mFormat != null) {
305              Locale loc = Locale.getDefault();
306              if (mFormatter == null || !loc.equals(mFormatterLocale)) {
307                  mFormatterLocale = loc;
308                  mFormatter = new Formatter(mFormatBuilder, loc);
309              }
310              mFormatBuilder.setLength(0);
311              mFormatterArgs[0] = text;
312              try {
313                  mFormatter.format(mFormat, mFormatterArgs);
314                  text = mFormatBuilder.toString();
315              } catch (IllegalFormatException ex) {
316                  if (!mLogged) {
317                      Log.w(TAG, "Illegal format string: " + mFormat);
318                      mLogged = true;
319                  }
320              }
321          }
322          setText(text);
323      }
324  
updateRunning()325      private void updateRunning() {
326          boolean running = mVisible && mStarted && isShown();
327          if (running != mRunning) {
328              if (running) {
329                  updateText(SystemClock.elapsedRealtime());
330                  dispatchChronometerTick();
331                  postDelayed(mTickRunnable, 1000);
332              } else {
333                  removeCallbacks(mTickRunnable);
334              }
335              mRunning = running;
336          }
337      }
338  
339      private final Runnable mTickRunnable = new Runnable() {
340          @Override
341          public void run() {
342              if (mRunning) {
343                  updateText(SystemClock.elapsedRealtime());
344                  dispatchChronometerTick();
345                  postDelayed(mTickRunnable, 1000);
346              }
347          }
348      };
349  
dispatchChronometerTick()350      void dispatchChronometerTick() {
351          if (mOnChronometerTickListener != null) {
352              mOnChronometerTickListener.onChronometerTick(this);
353          }
354      }
355  
356      private static final int MIN_IN_SEC = 60;
357      private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
formatDuration(long ms)358      private static String formatDuration(long ms) {
359          int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
360          if (duration < 0) {
361              duration = -duration;
362          }
363  
364          int h = 0;
365          int m = 0;
366  
367          if (duration >= HOUR_IN_SEC) {
368              h = duration / HOUR_IN_SEC;
369              duration -= h * HOUR_IN_SEC;
370          }
371          if (duration >= MIN_IN_SEC) {
372              m = duration / MIN_IN_SEC;
373              duration -= m * MIN_IN_SEC;
374          }
375          final int s = duration;
376  
377          final ArrayList<Measure> measures = new ArrayList<Measure>();
378          if (h > 0) {
379              measures.add(new Measure(h, MeasureUnit.HOUR));
380          }
381          if (m > 0) {
382              measures.add(new Measure(m, MeasureUnit.MINUTE));
383          }
384          measures.add(new Measure(s, MeasureUnit.SECOND));
385  
386          return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE)
387                      .formatMeasures(measures.toArray(new Measure[measures.size()]));
388      }
389  
390      @Override
getContentDescription()391      public CharSequence getContentDescription() {
392          return formatDuration(mNow - mBase);
393      }
394  
395      @Override
getAccessibilityClassName()396      public CharSequence getAccessibilityClassName() {
397          return Chronometer.class.getName();
398      }
399  }
400