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