/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.widget;

import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.icu.text.MeasureFormat;
import android.icu.text.MeasureFormat.FormatWidth;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.net.Uri;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews.RemoteView;

import com.android.internal.R;

import java.util.ArrayList;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.Locale;

Class that implements a simple timer.

You can give it a start time in the SystemClock.elapsedRealtime timebase, and it counts up from that, or if you don't give it a base time, it will use the time at which you call start.

The timer can also count downward towards the base time by setting setCountDown(boolean) to true.

By default it will display the current timer value in the form "MM:SS" or "H:MM:SS", or you can use setFormat to format the timer value into an arbitrary string.

@attrref android.R.styleable#Chronometer_format
@attrref android.R.styleable#Chronometer_countDown
/** * Class that implements a simple timer. * <p> * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, * and it counts up from that, or if you don't give it a base time, it will use the * time at which you call {@link #start}. * * <p>The timer can also count downward towards the base time by * setting {@link #setCountDown(boolean)} to true. * * <p>By default it will display the current * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} * to format the timer value into an arbitrary string. * * @attr ref android.R.styleable#Chronometer_format * @attr ref android.R.styleable#Chronometer_countDown */
@RemoteView public class Chronometer extends TextView { private static final String TAG = "Chronometer";
A callback that notifies when the chronometer has incremented on its own.
/** * A callback that notifies when the chronometer has incremented on its own. */
public interface OnChronometerTickListener {
Notification that the chronometer has changed.
/** * Notification that the chronometer has changed. */
void onChronometerTick(Chronometer chronometer); } private long mBase; private long mNow; // the currently displayed time private boolean mVisible; private boolean mStarted; private boolean mRunning; private boolean mLogged; private String mFormat; private Formatter mFormatter; private Locale mFormatterLocale; private Object[] mFormatterArgs = new Object[1]; private StringBuilder mFormatBuilder; private OnChronometerTickListener mOnChronometerTickListener; private StringBuilder mRecycle = new StringBuilder(8); private boolean mCountDown;
Initialize this Chronometer object. Sets the base to the current time.
/** * Initialize this Chronometer object. * Sets the base to the current time. */
public Chronometer(Context context) { this(context, null, 0); }
Initialize with standard view layout information. Sets the base to the current time.
/** * Initialize with standard view layout information. * Sets the base to the current time. */
public Chronometer(Context context, AttributeSet attrs) { this(context, attrs, 0); }
Initialize with standard view layout information and style. Sets the base to the current time.
/** * Initialize with standard view layout information and style. * Sets the base to the current time. */
public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); setFormat(a.getString(R.styleable.Chronometer_format)); setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false)); a.recycle(); init(); } private void init() { mBase = SystemClock.elapsedRealtime(); updateText(mBase); }
Set this view to count down to the base instead of counting up from it.
Params:
  • countDown – whether this view should count down
See Also:
/** * Set this view to count down to the base instead of counting up from it. * * @param countDown whether this view should count down * * @see #setBase(long) */
@android.view.RemotableViewMethod public void setCountDown(boolean countDown) { mCountDown = countDown; updateText(SystemClock.elapsedRealtime()); }
See Also:
Returns:whether this view counts down
/** * @return whether this view counts down * * @see #setCountDown(boolean) */
public boolean isCountDown() { return mCountDown; }
Returns:whether this is the final countdown
/** * @return whether this is the final countdown */
public boolean isTheFinalCountDown() { try { getContext().startActivity( new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw")) .addCategory(Intent.CATEGORY_BROWSABLE) .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)); return true; } catch (Exception e) { return false; } }
Set the time that the count-up timer is in reference to.
Params:
/** * Set the time that the count-up timer is in reference to. * * @param base Use the {@link SystemClock#elapsedRealtime} time base. */
@android.view.RemotableViewMethod public void setBase(long base) { mBase = base; dispatchChronometerTick(); updateText(SystemClock.elapsedRealtime()); }
Return the base time as set through setBase.
/** * Return the base time as set through {@link #setBase}. */
public long getBase() { return mBase; }
Sets the format string used for display. The Chronometer will display this string, with the first "%s" replaced by the current timer value in "MM:SS" or "H:MM:SS" form. If the format string is null, or if you never call setFormat(), the Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" form.
Params:
  • format – the format string.
/** * Sets the format string used for display. The Chronometer will display * this string, with the first "%s" replaced by the current timer value in * "MM:SS" or "H:MM:SS" form. * * If the format string is null, or if you never call setFormat(), the * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" * form. * * @param format the format string. */
@android.view.RemotableViewMethod public void setFormat(String format) { mFormat = format; if (format != null && mFormatBuilder == null) { mFormatBuilder = new StringBuilder(format.length() * 2); } }
Returns the current format string as set through setFormat.
/** * Returns the current format string as set through {@link #setFormat}. */
public String getFormat() { return mFormat; }
Sets the listener to be called when the chronometer changes.
Params:
  • listener – The listener.
/** * Sets the listener to be called when the chronometer changes. * * @param listener The listener. */
public void setOnChronometerTickListener(OnChronometerTickListener listener) { mOnChronometerTickListener = listener; }
Returns:The listener (may be null) that is listening for chronometer change events.
/** * @return The listener (may be null) that is listening for chronometer change * events. */
public OnChronometerTickListener getOnChronometerTickListener() { return mOnChronometerTickListener; }
Start counting up. This does not affect the base as set from setBase, just the view display. Chronometer works by regularly scheduling messages to the handler, even when the Widget is not visible. To make sure resource leaks do not occur, the user should make sure that each start() call has a reciprocal call to stop.
/** * Start counting up. This does not affect the base as set from {@link #setBase}, just * the view display. * * Chronometer works by regularly scheduling messages to the handler, even when the * Widget is not visible. To make sure resource leaks do not occur, the user should * make sure that each start() call has a reciprocal call to {@link #stop}. */
public void start() { mStarted = true; updateRunning(); }
Stop counting up. This does not affect the base as set from setBase, just the view display. This stops the messages to the handler, effectively releasing resources that would be held as the chronometer is running, via start.
/** * Stop counting up. This does not affect the base as set from {@link #setBase}, just * the view display. * * This stops the messages to the handler, effectively releasing resources that would * be held as the chronometer is running, via {@link #start}. */
public void stop() { mStarted = false; updateRunning(); }
The same as calling start or stop.
@hidepending API council approval
/** * The same as calling {@link #start} or {@link #stop}. * @hide pending API council approval */
@android.view.RemotableViewMethod public void setStarted(boolean started) { mStarted = started; updateRunning(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mVisible = false; updateRunning(); } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); mVisible = visibility == VISIBLE; updateRunning(); } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); updateRunning(); } private synchronized void updateText(long now) { mNow = now; long seconds = mCountDown ? mBase - now : now - mBase; seconds /= 1000; boolean negative = false; if (seconds < 0) { seconds = -seconds; negative = true; } String text = DateUtils.formatElapsedTime(mRecycle, seconds); if (negative) { text = getResources().getString(R.string.negative_duration, text); } if (mFormat != null) { Locale loc = Locale.getDefault(); if (mFormatter == null || !loc.equals(mFormatterLocale)) { mFormatterLocale = loc; mFormatter = new Formatter(mFormatBuilder, loc); } mFormatBuilder.setLength(0); mFormatterArgs[0] = text; try { mFormatter.format(mFormat, mFormatterArgs); text = mFormatBuilder.toString(); } catch (IllegalFormatException ex) { if (!mLogged) { Log.w(TAG, "Illegal format string: " + mFormat); mLogged = true; } } } setText(text); } private void updateRunning() { boolean running = mVisible && mStarted && isShown(); if (running != mRunning) { if (running) { updateText(SystemClock.elapsedRealtime()); dispatchChronometerTick(); postDelayed(mTickRunnable, 1000); } else { removeCallbacks(mTickRunnable); } mRunning = running; } } private final Runnable mTickRunnable = new Runnable() { @Override public void run() { if (mRunning) { updateText(SystemClock.elapsedRealtime()); dispatchChronometerTick(); postDelayed(mTickRunnable, 1000); } } }; void dispatchChronometerTick() { if (mOnChronometerTickListener != null) { mOnChronometerTickListener.onChronometerTick(this); } } private static final int MIN_IN_SEC = 60; private static final int HOUR_IN_SEC = MIN_IN_SEC*60; private static String formatDuration(long ms) { int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS); if (duration < 0) { duration = -duration; } int h = 0; int m = 0; if (duration >= HOUR_IN_SEC) { h = duration / HOUR_IN_SEC; duration -= h * HOUR_IN_SEC; } if (duration >= MIN_IN_SEC) { m = duration / MIN_IN_SEC; duration -= m * MIN_IN_SEC; } final int s = duration; final ArrayList<Measure> measures = new ArrayList<Measure>(); if (h > 0) { measures.add(new Measure(h, MeasureUnit.HOUR)); } if (m > 0) { measures.add(new Measure(m, MeasureUnit.MINUTE)); } measures.add(new Measure(s, MeasureUnit.SECOND)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE) .formatMeasures(measures.toArray(new Measure[measures.size()])); } @Override public CharSequence getContentDescription() { return formatDuration(mNow - mBase); } @Override public CharSequence getAccessibilityClassName() { return Chronometer.class.getName(); } }