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.res.TypedArray;
     21 import android.os.Handler;
     22 import android.os.Message;
     23 import android.os.SystemClock;
     24 import android.text.format.DateUtils;
     25 import android.util.AttributeSet;
     26 import android.util.Log;
     27 import android.view.accessibility.AccessibilityEvent;
     28 import android.view.accessibility.AccessibilityNodeInfo;
     29 import android.widget.RemoteViews.RemoteView;
     30 
     31 import java.util.Formatter;
     32 import java.util.IllegalFormatException;
     33 import 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
     40  * time at which you call {@link #start}.  By default it will display the current
     41  * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
     42  * to format the timer value into an arbitrary string.
     43  *
     44  * @attr ref android.R.styleable#Chronometer_format
     45  */
     46 @RemoteView
     47 public class Chronometer extends TextView {
     48     private static final String TAG = "Chronometer";
     49 
     50     /**
     51      * A callback that notifies when the chronometer has incremented on its own.
     52      */
     53     public interface OnChronometerTickListener {
     54 
     55         /**
     56          * Notification that the chronometer has changed.
     57          */
     58         void onChronometerTick(Chronometer chronometer);
     59 
     60     }
     61 
     62     private long mBase;
     63     private boolean mVisible;
     64     private boolean mStarted;
     65     private boolean mRunning;
     66     private boolean mLogged;
     67     private String mFormat;
     68     private Formatter mFormatter;
     69     private Locale mFormatterLocale;
     70     private Object[] mFormatterArgs = new Object[1];
     71     private StringBuilder mFormatBuilder;
     72     private OnChronometerTickListener mOnChronometerTickListener;
     73     private StringBuilder mRecycle = new StringBuilder(8);
     74 
     75     private static final int TICK_WHAT = 2;
     76 
     77     /**
     78      * Initialize this Chronometer object.
     79      * Sets the base to the current time.
     80      */
     81     public Chronometer(Context context) {
     82         this(context, null, 0);
     83     }
     84 
     85     /**
     86      * Initialize with standard view layout information.
     87      * Sets the base to the current time.
     88      */
     89     public Chronometer(Context context, AttributeSet attrs) {
     90         this(context, attrs, 0);
     91     }
     92 
     93     /**
     94      * Initialize with standard view layout information and style.
     95      * Sets the base to the current time.
     96      */
     97     public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) {
     98         this(context, attrs, defStyleAttr, 0);
     99     }
    100 
    101     public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    102         super(context, attrs, defStyleAttr, defStyleRes);
    103 
    104         final TypedArray a = context.obtainStyledAttributes(
    105                 attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
    106         setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
    107         a.recycle();
    108 
    109         init();
    110     }
    111 
    112     private void init() {
    113         mBase = SystemClock.elapsedRealtime();
    114         updateText(mBase);
    115     }
    116 
    117     /**
    118      * Set the time that the count-up timer is in reference to.
    119      *
    120      * @param base Use the {@link SystemClock#elapsedRealtime} time base.
    121      */
    122     @android.view.RemotableViewMethod
    123     public void setBase(long base) {
    124         mBase = base;
    125         dispatchChronometerTick();
    126         updateText(SystemClock.elapsedRealtime());
    127     }
    128 
    129     /**
    130      * Return the base time as set through {@link #setBase}.
    131      */
    132     public long getBase() {
    133         return mBase;
    134     }
    135 
    136     /**
    137      * Sets the format string used for display.  The Chronometer will display
    138      * this string, with the first "%s" replaced by the current timer value in
    139      * "MM:SS" or "H:MM:SS" form.
    140      *
    141      * If the format string is null, or if you never call setFormat(), the
    142      * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
    143      * form.
    144      *
    145      * @param format the format string.
    146      */
    147     @android.view.RemotableViewMethod
    148     public void setFormat(String format) {
    149         mFormat = format;
    150         if (format != null && mFormatBuilder == null) {
    151             mFormatBuilder = new StringBuilder(format.length() * 2);
    152         }
    153     }
    154 
    155     /**
    156      * Returns the current format string as set through {@link #setFormat}.
    157      */
    158     public String getFormat() {
    159         return mFormat;
    160     }
    161 
    162     /**
    163      * Sets the listener to be called when the chronometer changes.
    164      *
    165      * @param listener The listener.
    166      */
    167     public void setOnChronometerTickListener(OnChronometerTickListener listener) {
    168         mOnChronometerTickListener = listener;
    169     }
    170 
    171     /**
    172      * @return The listener (may be null) that is listening for chronometer change
    173      *         events.
    174      */
    175     public OnChronometerTickListener getOnChronometerTickListener() {
    176         return mOnChronometerTickListener;
    177     }
    178 
    179     /**
    180      * Start counting up.  This does not affect the base as set from {@link #setBase}, just
    181      * the view display.
    182      *
    183      * Chronometer works by regularly scheduling messages to the handler, even when the
    184      * Widget is not visible.  To make sure resource leaks do not occur, the user should
    185      * make sure that each start() call has a reciprocal call to {@link #stop}.
    186      */
    187     public void start() {
    188         mStarted = true;
    189         updateRunning();
    190     }
    191 
    192     /**
    193      * Stop counting up.  This does not affect the base as set from {@link #setBase}, just
    194      * the view display.
    195      *
    196      * This stops the messages to the handler, effectively releasing resources that would
    197      * be held as the chronometer is running, via {@link #start}.
    198      */
    199     public void stop() {
    200         mStarted = false;
    201         updateRunning();
    202     }
    203 
    204     /**
    205      * The same as calling {@link #start} or {@link #stop}.
    206      * @hide pending API council approval
    207      */
    208     @android.view.RemotableViewMethod
    209     public void setStarted(boolean started) {
    210         mStarted = started;
    211         updateRunning();
    212     }
    213 
    214     @Override
    215     protected void onDetachedFromWindow() {
    216         super.onDetachedFromWindow();
    217         mVisible = false;
    218         updateRunning();
    219     }
    220 
    221     @Override
    222     protected void onWindowVisibilityChanged(int visibility) {
    223         super.onWindowVisibilityChanged(visibility);
    224         mVisible = visibility == VISIBLE;
    225         updateRunning();
    226     }
    227 
    228     private synchronized void updateText(long now) {
    229         long seconds = now - mBase;
    230         seconds /= 1000;
    231         String text = DateUtils.formatElapsedTime(mRecycle, seconds);
    232 
    233         if (mFormat != null) {
    234             Locale loc = Locale.getDefault();
    235             if (mFormatter == null || !loc.equals(mFormatterLocale)) {
    236                 mFormatterLocale = loc;
    237                 mFormatter = new Formatter(mFormatBuilder, loc);
    238             }
    239             mFormatBuilder.setLength(0);
    240             mFormatterArgs[0] = text;
    241             try {
    242                 mFormatter.format(mFormat, mFormatterArgs);
    243                 text = mFormatBuilder.toString();
    244             } catch (IllegalFormatException ex) {
    245                 if (!mLogged) {
    246                     Log.w(TAG, "Illegal format string: " + mFormat);
    247                     mLogged = true;
    248                 }
    249             }
    250         }
    251         setText(text);
    252     }
    253 
    254     private void updateRunning() {
    255         boolean running = mVisible && mStarted;
    256         if (running != mRunning) {
    257             if (running) {
    258                 updateText(SystemClock.elapsedRealtime());
    259                 dispatchChronometerTick();
    260                 mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000);
    261             } else {
    262                 mHandler.removeMessages(TICK_WHAT);
    263             }
    264             mRunning = running;
    265         }
    266     }
    267 
    268     private Handler mHandler = new Handler() {
    269         public void handleMessage(Message m) {
    270             if (mRunning) {
    271                 updateText(SystemClock.elapsedRealtime());
    272                 dispatchChronometerTick();
    273                 sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000);
    274             }
    275         }
    276     };
    277 
    278     void dispatchChronometerTick() {
    279         if (mOnChronometerTickListener != null) {
    280             mOnChronometerTickListener.onChronometerTick(this);
    281         }
    282     }
    283 
    284     @Override
    285     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    286         super.onInitializeAccessibilityEvent(event);
    287         event.setClassName(Chronometer.class.getName());
    288     }
    289 
    290     @Override
    291     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    292         super.onInitializeAccessibilityNodeInfo(info);
    293         info.setClassName(Chronometer.class.getName());
    294     }
    295 }
    296