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 */ onChronometerTick(Chronometer chronometer)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 */ Chronometer(Context context)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 */ Chronometer(Context context, AttributeSet attrs)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 */ Chronometer(Context context, AttributeSet attrs, int defStyleAttr)110 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { 111 this(context, attrs, defStyleAttr, 0); 112 } 113 Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 init()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 setCountDown(boolean countDown)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 isCountDown()152 public boolean isCountDown() { 153 return mCountDown; 154 } 155 156 /** 157 * @return whether this is the final countdown 158 */ isTheFinalCountDown()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 setBase(long base)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 */ getBase()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 setFormat(String format)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 getFormat()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 */ setOnChronometerTickListener(OnChronometerTickListener listener)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 */ getOnChronometerTickListener()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 */ start()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 */ stop()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 setStarted(boolean started)265 public void setStarted(boolean started) { 266 mStarted = started; 267 updateRunning(); 268 } 269 270 @Override onDetachedFromWindow()271 protected void onDetachedFromWindow() { 272 super.onDetachedFromWindow(); 273 mVisible = false; 274 updateRunning(); 275 } 276 277 @Override onWindowVisibilityChanged(int visibility)278 protected void onWindowVisibilityChanged(int visibility) { 279 super.onWindowVisibilityChanged(visibility); 280 mVisible = visibility == VISIBLE; 281 updateRunning(); 282 } 283 284 @Override onVisibilityChanged(View changedView, int visibility)285 protected void onVisibilityChanged(View changedView, int visibility) { 286 super.onVisibilityChanged(changedView, visibility); 287 updateRunning(); 288 } 289 updateText(long now)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 updateRunning()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 dispatchChronometerTick()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; formatDuration(long ms)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 getContentDescription()391 public CharSequence getContentDescription() { 392 return formatDuration(mNow - mBase); 393 } 394 395 @Override getAccessibilityClassName()396 public CharSequence getAccessibilityClassName() { 397 return Chronometer.class.getName(); 398 } 399 } 400