Home | History | Annotate | Download | only in widget
      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          */
     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      */
     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      */
    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      */
    110     public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) {
    111         this(context, attrs, defStyleAttr, 0);
    112     }
    113 
    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 
    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
    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
    152     public boolean isCountDown() {
    153         return mCountDown;
    154     }
    155 
    156     /**
    157      * @return whether this is the final countdown
    158      */
    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
    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      */
    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
    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
    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      */
    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      */
    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      */
    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      */
    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
    265     public void setStarted(boolean started) {
    266         mStarted = started;
    267         updateRunning();
    268     }
    269 
    270     @Override
    271     protected void onDetachedFromWindow() {
    272         super.onDetachedFromWindow();
    273         mVisible = false;
    274         updateRunning();
    275     }
    276 
    277     @Override
    278     protected void onWindowVisibilityChanged(int visibility) {
    279         super.onWindowVisibilityChanged(visibility);
    280         mVisible = visibility == VISIBLE;
    281         updateRunning();
    282     }
    283 
    284     @Override
    285     protected void onVisibilityChanged(View changedView, int visibility) {
    286         super.onVisibilityChanged(changedView, visibility);
    287         updateRunning();
    288     }
    289 
    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 
    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 
    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;
    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
    391     public CharSequence getContentDescription() {
    392         return formatDuration(mNow - mBase);
    393     }
    394 
    395     @Override
    396     public CharSequence getAccessibilityClassName() {
    397         return Chronometer.class.getName();
    398     }
    399 }
    400