1 /* 2 * Copyright (C) 2010 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 static android.text.format.DateUtils.DAY_IN_MILLIS; 20 import static android.text.format.DateUtils.HOUR_IN_MILLIS; 21 import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 22 import static android.text.format.DateUtils.YEAR_IN_MILLIS; 23 import static android.text.format.Time.getJulianDay; 24 25 import android.annotation.UnsupportedAppUsage; 26 import android.app.ActivityThread; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.res.Configuration; 32 import android.content.res.TypedArray; 33 import android.database.ContentObserver; 34 import android.os.Handler; 35 import android.text.format.Time; 36 import android.util.AttributeSet; 37 import android.view.accessibility.AccessibilityNodeInfo; 38 import android.view.inspector.InspectableProperty; 39 import android.widget.RemoteViews.RemoteView; 40 41 import com.android.internal.R; 42 43 import java.text.DateFormat; 44 import java.util.ArrayList; 45 import java.util.Calendar; 46 import java.util.Date; 47 import java.util.TimeZone; 48 49 // 50 // TODO 51 // - listen for the next threshold time to update the view. 52 // - listen for date format pref changed 53 // - put the AM/PM in a smaller font 54 // 55 56 /** 57 * Displays a given time in a convenient human-readable foramt. 58 * 59 * @hide 60 */ 61 @RemoteView 62 public class DateTimeView extends TextView { 63 private static final int SHOW_TIME = 0; 64 private static final int SHOW_MONTH_DAY_YEAR = 1; 65 66 Date mTime; 67 long mTimeMillis; 68 69 int mLastDisplay = -1; 70 DateFormat mLastFormat; 71 72 private long mUpdateTimeMillis; 73 private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>(); 74 private String mNowText; 75 private boolean mShowRelativeTime; 76 DateTimeView(Context context)77 public DateTimeView(Context context) { 78 this(context, null); 79 } 80 81 @UnsupportedAppUsage DateTimeView(Context context, AttributeSet attrs)82 public DateTimeView(Context context, AttributeSet attrs) { 83 super(context, attrs); 84 final TypedArray a = context.obtainStyledAttributes(attrs, 85 com.android.internal.R.styleable.DateTimeView, 0, 86 0); 87 88 final int N = a.getIndexCount(); 89 for (int i = 0; i < N; i++) { 90 int attr = a.getIndex(i); 91 switch (attr) { 92 case R.styleable.DateTimeView_showRelative: 93 boolean relative = a.getBoolean(i, false); 94 setShowRelativeTime(relative); 95 break; 96 } 97 } 98 a.recycle(); 99 } 100 101 @Override onAttachedToWindow()102 protected void onAttachedToWindow() { 103 super.onAttachedToWindow(); 104 ReceiverInfo ri = sReceiverInfo.get(); 105 if (ri == null) { 106 ri = new ReceiverInfo(); 107 sReceiverInfo.set(ri); 108 } 109 ri.addView(this); 110 // The view may not be added to the view hierarchy immediately right after setTime() 111 // is called which means it won't get any update from intents before being added. 112 // In such case, the view might show the incorrect relative time after being added to the 113 // view hierarchy until the next update intent comes. 114 // So we update the time here if mShowRelativeTime is enabled to prevent this case. 115 if (mShowRelativeTime) { 116 update(); 117 } 118 } 119 120 @Override onDetachedFromWindow()121 protected void onDetachedFromWindow() { 122 super.onDetachedFromWindow(); 123 final ReceiverInfo ri = sReceiverInfo.get(); 124 if (ri != null) { 125 ri.removeView(this); 126 } 127 } 128 129 @android.view.RemotableViewMethod 130 @UnsupportedAppUsage setTime(long time)131 public void setTime(long time) { 132 Time t = new Time(); 133 t.set(time); 134 mTimeMillis = t.toMillis(false); 135 mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0); 136 update(); 137 } 138 139 @android.view.RemotableViewMethod setShowRelativeTime(boolean showRelativeTime)140 public void setShowRelativeTime(boolean showRelativeTime) { 141 mShowRelativeTime = showRelativeTime; 142 updateNowText(); 143 update(); 144 } 145 146 /** 147 * Returns whether this view shows relative time 148 * 149 * @return True if it shows relative time, false otherwise 150 */ 151 @InspectableProperty(name = "showReleative", hasAttributeId = false) isShowRelativeTime()152 public boolean isShowRelativeTime() { 153 return mShowRelativeTime; 154 } 155 156 @Override 157 @android.view.RemotableViewMethod setVisibility(@isibility int visibility)158 public void setVisibility(@Visibility int visibility) { 159 boolean gotVisible = visibility != GONE && getVisibility() == GONE; 160 super.setVisibility(visibility); 161 if (gotVisible) { 162 update(); 163 } 164 } 165 166 @UnsupportedAppUsage update()167 void update() { 168 if (mTime == null || getVisibility() == GONE) { 169 return; 170 } 171 if (mShowRelativeTime) { 172 updateRelativeTime(); 173 return; 174 } 175 176 int display; 177 Date time = mTime; 178 179 Time t = new Time(); 180 t.set(mTimeMillis); 181 t.second = 0; 182 183 t.hour -= 12; 184 long twelveHoursBefore = t.toMillis(false); 185 t.hour += 12; 186 long twelveHoursAfter = t.toMillis(false); 187 t.hour = 0; 188 t.minute = 0; 189 long midnightBefore = t.toMillis(false); 190 t.monthDay++; 191 long midnightAfter = t.toMillis(false); 192 193 long nowMillis = System.currentTimeMillis(); 194 t.set(nowMillis); 195 t.second = 0; 196 nowMillis = t.normalize(false); 197 198 // Choose the display mode 199 choose_display: { 200 if ((nowMillis >= midnightBefore && nowMillis < midnightAfter) 201 || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) { 202 display = SHOW_TIME; 203 break choose_display; 204 } 205 // Else, show month day and year. 206 display = SHOW_MONTH_DAY_YEAR; 207 break choose_display; 208 } 209 210 // Choose the format 211 DateFormat format; 212 if (display == mLastDisplay && mLastFormat != null) { 213 // use cached format 214 format = mLastFormat; 215 } else { 216 switch (display) { 217 case SHOW_TIME: 218 format = getTimeFormat(); 219 break; 220 case SHOW_MONTH_DAY_YEAR: 221 format = DateFormat.getDateInstance(DateFormat.SHORT); 222 break; 223 default: 224 throw new RuntimeException("unknown display value: " + display); 225 } 226 mLastFormat = format; 227 } 228 229 // Set the text 230 String text = format.format(mTime); 231 setText(text); 232 233 // Schedule the next update 234 if (display == SHOW_TIME) { 235 // Currently showing the time, update at the later of twelve hours after or midnight. 236 mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter; 237 } else { 238 // Currently showing the date 239 if (mTimeMillis < nowMillis) { 240 // If the time is in the past, don't schedule an update 241 mUpdateTimeMillis = 0; 242 } else { 243 // If hte time is in the future, schedule one at the earlier of twelve hours 244 // before or midnight before. 245 mUpdateTimeMillis = twelveHoursBefore < midnightBefore 246 ? twelveHoursBefore : midnightBefore; 247 } 248 } 249 } 250 251 private void updateRelativeTime() { 252 long now = System.currentTimeMillis(); 253 long duration = Math.abs(now - mTimeMillis); 254 int count; 255 long millisIncrease; 256 boolean past = (now >= mTimeMillis); 257 String result; 258 if (duration < MINUTE_IN_MILLIS) { 259 setText(mNowText); 260 mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1; 261 return; 262 } else if (duration < HOUR_IN_MILLIS) { 263 count = (int)(duration / MINUTE_IN_MILLIS); 264 result = String.format(getContext().getResources().getQuantityString(past 265 ? com.android.internal.R.plurals.duration_minutes_shortest 266 : com.android.internal.R.plurals.duration_minutes_shortest_future, 267 count), 268 count); 269 millisIncrease = MINUTE_IN_MILLIS; 270 } else if (duration < DAY_IN_MILLIS) { 271 count = (int)(duration / HOUR_IN_MILLIS); 272 result = String.format(getContext().getResources().getQuantityString(past 273 ? com.android.internal.R.plurals.duration_hours_shortest 274 : com.android.internal.R.plurals.duration_hours_shortest_future, 275 count), 276 count); 277 millisIncrease = HOUR_IN_MILLIS; 278 } else if (duration < YEAR_IN_MILLIS) { 279 // In weird cases it can become 0 because of daylight savings 280 TimeZone timeZone = TimeZone.getDefault(); 281 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1); 282 result = String.format(getContext().getResources().getQuantityString(past 283 ? com.android.internal.R.plurals.duration_days_shortest 284 : com.android.internal.R.plurals.duration_days_shortest_future, 285 count), 286 count); 287 if (past || count != 1) { 288 mUpdateTimeMillis = computeNextMidnight(timeZone); 289 millisIncrease = -1; 290 } else { 291 millisIncrease = DAY_IN_MILLIS; 292 } 293 294 } else { 295 count = (int)(duration / YEAR_IN_MILLIS); 296 result = String.format(getContext().getResources().getQuantityString(past 297 ? com.android.internal.R.plurals.duration_years_shortest 298 : com.android.internal.R.plurals.duration_years_shortest_future, 299 count), 300 count); 301 millisIncrease = YEAR_IN_MILLIS; 302 } 303 if (millisIncrease != -1) { 304 if (past) { 305 mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1; 306 } else { 307 mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1; 308 } 309 } 310 setText(result); 311 } 312 313 /** 314 * @param timeZone the timezone we are in 315 * @return the timepoint in millis at UTC at midnight in the current timezone 316 */ computeNextMidnight(TimeZone timeZone)317 private long computeNextMidnight(TimeZone timeZone) { 318 Calendar c = Calendar.getInstance(); 319 c.setTimeZone(timeZone); 320 c.add(Calendar.DAY_OF_MONTH, 1); 321 c.set(Calendar.HOUR_OF_DAY, 0); 322 c.set(Calendar.MINUTE, 0); 323 c.set(Calendar.SECOND, 0); 324 c.set(Calendar.MILLISECOND, 0); 325 return c.getTimeInMillis(); 326 } 327 328 @Override onConfigurationChanged(Configuration newConfig)329 protected void onConfigurationChanged(Configuration newConfig) { 330 super.onConfigurationChanged(newConfig); 331 updateNowText(); 332 update(); 333 } 334 updateNowText()335 private void updateNowText() { 336 if (!mShowRelativeTime) { 337 return; 338 } 339 mNowText = getContext().getResources().getString( 340 com.android.internal.R.string.now_string_shortest); 341 } 342 343 // Return the date difference for the two times in a given timezone. dayDistance(TimeZone timeZone, long startTime, long endTime)344 private static int dayDistance(TimeZone timeZone, long startTime, 345 long endTime) { 346 return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000) 347 - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000); 348 } 349 getTimeFormat()350 private DateFormat getTimeFormat() { 351 return android.text.format.DateFormat.getTimeFormat(getContext()); 352 } 353 clearFormatAndUpdate()354 void clearFormatAndUpdate() { 355 mLastFormat = null; 356 update(); 357 } 358 359 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)360 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 361 super.onInitializeAccessibilityNodeInfoInternal(info); 362 if (mShowRelativeTime) { 363 // The short version of the time might not be completely understandable and for 364 // accessibility we rather have a longer version. 365 long now = System.currentTimeMillis(); 366 long duration = Math.abs(now - mTimeMillis); 367 int count; 368 boolean past = (now >= mTimeMillis); 369 String result; 370 if (duration < MINUTE_IN_MILLIS) { 371 result = mNowText; 372 } else if (duration < HOUR_IN_MILLIS) { 373 count = (int)(duration / MINUTE_IN_MILLIS); 374 result = String.format(getContext().getResources().getQuantityString(past 375 ? com.android.internal. 376 R.plurals.duration_minutes_relative 377 : com.android.internal. 378 R.plurals.duration_minutes_relative_future, 379 count), 380 count); 381 } else if (duration < DAY_IN_MILLIS) { 382 count = (int)(duration / HOUR_IN_MILLIS); 383 result = String.format(getContext().getResources().getQuantityString(past 384 ? com.android.internal. 385 R.plurals.duration_hours_relative 386 : com.android.internal. 387 R.plurals.duration_hours_relative_future, 388 count), 389 count); 390 } else if (duration < YEAR_IN_MILLIS) { 391 // In weird cases it can become 0 because of daylight savings 392 TimeZone timeZone = TimeZone.getDefault(); 393 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1); 394 result = String.format(getContext().getResources().getQuantityString(past 395 ? com.android.internal. 396 R.plurals.duration_days_relative 397 : com.android.internal. 398 R.plurals.duration_days_relative_future, 399 count), 400 count); 401 402 } else { 403 count = (int)(duration / YEAR_IN_MILLIS); 404 result = String.format(getContext().getResources().getQuantityString(past 405 ? com.android.internal. 406 R.plurals.duration_years_relative 407 : com.android.internal. 408 R.plurals.duration_years_relative_future, 409 count), 410 count); 411 } 412 info.setText(result); 413 } 414 } 415 416 /** 417 * @hide 418 */ setReceiverHandler(Handler handler)419 public static void setReceiverHandler(Handler handler) { 420 ReceiverInfo ri = sReceiverInfo.get(); 421 if (ri == null) { 422 ri = new ReceiverInfo(); 423 sReceiverInfo.set(ri); 424 } 425 ri.setHandler(handler); 426 } 427 428 private static class ReceiverInfo { 429 private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>(); 430 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 431 @Override 432 public void onReceive(Context context, Intent intent) { 433 String action = intent.getAction(); 434 if (Intent.ACTION_TIME_TICK.equals(action)) { 435 if (System.currentTimeMillis() < getSoonestUpdateTime()) { 436 // The update() function takes a few milliseconds to run because of 437 // all of the time conversions it needs to do, so we can't do that 438 // every minute. 439 return; 440 } 441 } 442 // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format. 443 updateAll(); 444 } 445 }; 446 447 private final ContentObserver mObserver = new ContentObserver(new Handler()) { 448 @Override 449 public void onChange(boolean selfChange) { 450 updateAll(); 451 } 452 }; 453 454 private Handler mHandler = new Handler(); 455 addView(DateTimeView v)456 public void addView(DateTimeView v) { 457 synchronized (mAttachedViews) { 458 final boolean register = mAttachedViews.isEmpty(); 459 mAttachedViews.add(v); 460 if (register) { 461 register(getApplicationContextIfAvailable(v.getContext())); 462 } 463 } 464 } 465 removeView(DateTimeView v)466 public void removeView(DateTimeView v) { 467 synchronized (mAttachedViews) { 468 final boolean removed = mAttachedViews.remove(v); 469 // Only unregister once when we remove the last view in the list otherwise we risk 470 // trying to unregister a receiver that is no longer registered. 471 if (removed && mAttachedViews.isEmpty()) { 472 unregister(getApplicationContextIfAvailable(v.getContext())); 473 } 474 } 475 } 476 updateAll()477 void updateAll() { 478 synchronized (mAttachedViews) { 479 final int count = mAttachedViews.size(); 480 for (int i = 0; i < count; i++) { 481 DateTimeView view = mAttachedViews.get(i); 482 view.post(() -> view.clearFormatAndUpdate()); 483 } 484 } 485 } 486 getSoonestUpdateTime()487 long getSoonestUpdateTime() { 488 long result = Long.MAX_VALUE; 489 synchronized (mAttachedViews) { 490 final int count = mAttachedViews.size(); 491 for (int i = 0; i < count; i++) { 492 final long time = mAttachedViews.get(i).mUpdateTimeMillis; 493 if (time < result) { 494 result = time; 495 } 496 } 497 } 498 return result; 499 } 500 getApplicationContextIfAvailable(Context context)501 static final Context getApplicationContextIfAvailable(Context context) { 502 final Context ac = context.getApplicationContext(); 503 return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext(); 504 } 505 register(Context context)506 void register(Context context) { 507 final IntentFilter filter = new IntentFilter(); 508 filter.addAction(Intent.ACTION_TIME_TICK); 509 filter.addAction(Intent.ACTION_TIME_CHANGED); 510 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); 511 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 512 context.registerReceiver(mReceiver, filter, null, mHandler); 513 } 514 unregister(Context context)515 void unregister(Context context) { 516 context.unregisterReceiver(mReceiver); 517 } 518 setHandler(Handler handler)519 public void setHandler(Handler handler) { 520 mHandler = handler; 521 synchronized (mAttachedViews) { 522 if (!mAttachedViews.isEmpty()) { 523 unregister(mAttachedViews.get(0).getContext()); 524 register(mAttachedViews.get(0).getContext()); 525 } 526 } 527 } 528 } 529 } 530