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 */ onChronometerTick(Chronometer chronometer)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 */ Chronometer(Context context)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 */ Chronometer(Context context, AttributeSet attrs)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 */ Chronometer(Context context, AttributeSet attrs, int defStyleAttr)109 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { 110 this(context, attrs, defStyleAttr, 0); 111 } 112 Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 init()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 setCountDown(boolean countDown)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 */ isCountDown()148 public boolean isCountDown() { 149 return mCountDown; 150 } 151 152 /** 153 * @return whether this is the final countdown 154 */ isTheFinalCountDown()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 setBase(long base)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 */ getBase()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 setFormat(String format)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 */ getFormat()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 */ setOnChronometerTickListener(OnChronometerTickListener listener)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 */ getOnChronometerTickListener()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 */ start()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 */ stop()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 setStarted(boolean started)260 public void setStarted(boolean started) { 261 mStarted = started; 262 updateRunning(); 263 } 264 265 @Override onDetachedFromWindow()266 protected void onDetachedFromWindow() { 267 super.onDetachedFromWindow(); 268 mVisible = false; 269 updateRunning(); 270 } 271 272 @Override onWindowVisibilityChanged(int visibility)273 protected void onWindowVisibilityChanged(int visibility) { 274 super.onWindowVisibilityChanged(visibility); 275 mVisible = visibility == VISIBLE; 276 updateRunning(); 277 } 278 279 @Override onVisibilityChanged(View changedView, int visibility)280 protected void onVisibilityChanged(View changedView, int visibility) { 281 super.onVisibilityChanged(changedView, visibility); 282 updateRunning(); 283 } 284 updateText(long now)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 updateRunning()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 dispatchChronometerTick()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; formatDuration(long ms)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 getContentDescription()386 public CharSequence getContentDescription() { 387 return formatDuration(mNow - mBase); 388 } 389 390 @Override getAccessibilityClassName()391 public CharSequence getAccessibilityClassName() { 392 return Chronometer.class.getName(); 393 } 394 } 395