1 /* 2 * Copyright (C) 2006 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 com.android.systemui.statusbar.policy; 18 19 import android.annotation.NonNull; 20 import android.app.StatusBarManager; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.res.TypedArray; 26 import android.graphics.Rect; 27 import android.icu.lang.UCharacter; 28 import android.icu.text.DateTimePatternGenerator; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.Parcelable; 32 import android.os.SystemClock; 33 import android.os.UserHandle; 34 import android.text.Spannable; 35 import android.text.SpannableStringBuilder; 36 import android.text.TextUtils; 37 import android.text.format.DateFormat; 38 import android.text.style.CharacterStyle; 39 import android.text.style.RelativeSizeSpan; 40 import android.util.AttributeSet; 41 import android.util.TypedValue; 42 import android.view.ContextThemeWrapper; 43 import android.view.Display; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.widget.TextView; 47 48 import com.android.settingslib.Utils; 49 import com.android.systemui.Dependency; 50 import com.android.systemui.FontSizeUtils; 51 import com.android.systemui.broadcast.BroadcastDispatcher; 52 import com.android.systemui.demomode.DemoModeCommandReceiver; 53 import com.android.systemui.plugins.DarkIconDispatcher; 54 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 55 import com.android.systemui.res.R; 56 import com.android.systemui.settings.UserTracker; 57 import com.android.systemui.statusbar.CommandQueue; 58 import com.android.systemui.statusbar.phone.ui.StatusBarIconController; 59 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; 60 import com.android.systemui.tuner.TunerService; 61 import com.android.systemui.tuner.TunerService.Tunable; 62 63 import java.text.SimpleDateFormat; 64 import java.util.ArrayList; 65 import java.util.Calendar; 66 import java.util.Locale; 67 import java.util.TimeZone; 68 69 /** 70 * Digital clock for the status bar. 71 */ 72 public class Clock extends TextView implements 73 DemoModeCommandReceiver, 74 Tunable, 75 CommandQueue.Callbacks, 76 DarkReceiver, ConfigurationListener { 77 78 public static final String CLOCK_SECONDS = "clock_seconds"; 79 private static final String CLOCK_SUPER_PARCELABLE = "clock_super_parcelable"; 80 private static final String CURRENT_USER_ID = "current_user_id"; 81 private static final String VISIBLE_BY_POLICY = "visible_by_policy"; 82 private static final String VISIBLE_BY_USER = "visible_by_user"; 83 private static final String SHOW_SECONDS = "show_seconds"; 84 private static final String VISIBILITY = "visibility"; 85 86 private final UserTracker mUserTracker; 87 private final CommandQueue mCommandQueue; 88 private int mCurrentUserId; 89 90 private boolean mClockVisibleByPolicy = true; 91 private boolean mClockVisibleByUser = true; 92 93 private boolean mAttached; 94 private boolean mScreenReceiverRegistered; 95 private Calendar mCalendar; 96 private String mContentDescriptionFormatString; 97 private SimpleDateFormat mClockFormat; 98 private SimpleDateFormat mContentDescriptionFormat; 99 private Locale mLocale; 100 private DateTimePatternGenerator mDateTimePatternGenerator; 101 102 private static final int AM_PM_STYLE_NORMAL = 0; 103 private static final int AM_PM_STYLE_SMALL = 1; 104 private static final int AM_PM_STYLE_GONE = 2; 105 106 private final int mAmPmStyle; 107 private boolean mShowSeconds; 108 private Handler mSecondsHandler; 109 110 // Fields to cache the width so the clock remains at an approximately constant width 111 private int mCharsAtCurrentWidth = -1; 112 private int mCachedWidth = -1; 113 114 /** 115 * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized. 116 */ 117 private int mNonAdaptedColor; 118 119 private final BroadcastDispatcher mBroadcastDispatcher; 120 121 private final UserTracker.Callback mUserChangedCallback = 122 new UserTracker.Callback() { 123 @Override 124 public void onUserChanged(int newUser, @NonNull Context userContext) { 125 mCurrentUserId = newUser; 126 updateClock(); 127 } 128 }; 129 Clock(Context context, AttributeSet attrs)130 public Clock(Context context, AttributeSet attrs) { 131 this(context, attrs, 0); 132 } 133 Clock(Context context, AttributeSet attrs, int defStyle)134 public Clock(Context context, AttributeSet attrs, int defStyle) { 135 super(context, attrs, defStyle); 136 mCommandQueue = Dependency.get(CommandQueue.class); 137 TypedArray a = context.getTheme().obtainStyledAttributes( 138 attrs, 139 R.styleable.Clock, 140 0, 0); 141 try { 142 mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE); 143 mNonAdaptedColor = getCurrentTextColor(); 144 } finally { 145 a.recycle(); 146 } 147 mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); 148 mUserTracker = Dependency.get(UserTracker.class); 149 150 setIncludeFontPadding(false); 151 } 152 153 @Override onSaveInstanceState()154 public Parcelable onSaveInstanceState() { 155 Bundle bundle = new Bundle(); 156 bundle.putParcelable(CLOCK_SUPER_PARCELABLE, super.onSaveInstanceState()); 157 bundle.putInt(CURRENT_USER_ID, mCurrentUserId); 158 bundle.putBoolean(VISIBLE_BY_POLICY, mClockVisibleByPolicy); 159 bundle.putBoolean(VISIBLE_BY_USER, mClockVisibleByUser); 160 bundle.putBoolean(SHOW_SECONDS, mShowSeconds); 161 bundle.putInt(VISIBILITY, getVisibility()); 162 163 return bundle; 164 } 165 166 @Override onRestoreInstanceState(Parcelable state)167 public void onRestoreInstanceState(Parcelable state) { 168 if (state == null || !(state instanceof Bundle)) { 169 super.onRestoreInstanceState(state); 170 return; 171 } 172 173 Bundle bundle = (Bundle) state; 174 Parcelable superState = bundle.getParcelable(CLOCK_SUPER_PARCELABLE); 175 super.onRestoreInstanceState(superState); 176 if (bundle.containsKey(CURRENT_USER_ID)) { 177 mCurrentUserId = bundle.getInt(CURRENT_USER_ID); 178 } 179 mClockVisibleByPolicy = bundle.getBoolean(VISIBLE_BY_POLICY, true); 180 mClockVisibleByUser = bundle.getBoolean(VISIBLE_BY_USER, true); 181 mShowSeconds = bundle.getBoolean(SHOW_SECONDS, false); 182 if (bundle.containsKey(VISIBILITY)) { 183 super.setVisibility(bundle.getInt(VISIBILITY)); 184 } 185 } 186 187 @Override onAttachedToWindow()188 protected void onAttachedToWindow() { 189 super.onAttachedToWindow(); 190 191 if (!mAttached) { 192 mAttached = true; 193 IntentFilter filter = new IntentFilter(); 194 195 filter.addAction(Intent.ACTION_TIME_TICK); 196 filter.addAction(Intent.ACTION_TIME_CHANGED); 197 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 198 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); 199 200 // NOTE: This receiver could run before this method returns, as it's not dispatching 201 // on the main thread and BroadcastDispatcher may not need to register with Context. 202 // The receiver will return immediately if the view does not have a Handler yet. 203 mBroadcastDispatcher.registerReceiverWithHandler(mIntentReceiver, filter, 204 Dependency.get(Dependency.TIME_TICK_HANDLER), UserHandle.ALL); 205 Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS, 206 StatusBarIconController.ICON_HIDE_LIST); 207 mCommandQueue.addCallback(this); 208 mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); 209 mCurrentUserId = mUserTracker.getUserId(); 210 } 211 212 // The time zone may have changed while the receiver wasn't registered, so update the Time 213 mCalendar = Calendar.getInstance(TimeZone.getDefault()); 214 mContentDescriptionFormatString = ""; 215 mDateTimePatternGenerator = null; 216 217 // Make sure we update to the current time 218 updateClock(); 219 updateClockVisibility(); 220 updateShowSeconds(); 221 } 222 223 @Override onDetachedFromWindow()224 protected void onDetachedFromWindow() { 225 super.onDetachedFromWindow(); 226 if (mScreenReceiverRegistered) { 227 mScreenReceiverRegistered = false; 228 mBroadcastDispatcher.unregisterReceiver(mScreenReceiver); 229 if (mSecondsHandler != null) { 230 mSecondsHandler.removeCallbacks(mSecondTick); 231 mSecondsHandler = null; 232 } 233 } 234 if (mAttached) { 235 mBroadcastDispatcher.unregisterReceiver(mIntentReceiver); 236 mAttached = false; 237 Dependency.get(TunerService.class).removeTunable(this); 238 mCommandQueue.removeCallback(this); 239 mUserTracker.removeCallback(mUserChangedCallback); 240 } 241 } 242 243 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 244 @Override 245 public void onReceive(Context context, Intent intent) { 246 // If the handler is null, it means we received a broadcast while the view has not 247 // finished being attached or in the process of being detached. 248 // In that case, do not post anything. 249 Handler handler = getHandler(); 250 if (handler == null) return; 251 252 String action = intent.getAction(); 253 if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)) { 254 String tz = intent.getStringExtra(Intent.EXTRA_TIMEZONE); 255 handler.post(() -> { 256 mCalendar = Calendar.getInstance(TimeZone.getTimeZone(tz)); 257 if (mClockFormat != null) { 258 mClockFormat.setTimeZone(mCalendar.getTimeZone()); 259 } 260 }); 261 } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { 262 final Locale newLocale = getResources().getConfiguration().locale; 263 handler.post(() -> { 264 if (!newLocale.equals(mLocale)) { 265 mLocale = newLocale; 266 // Force refresh of dependent variables. 267 mContentDescriptionFormatString = ""; 268 mDateTimePatternGenerator = null; 269 } 270 }); 271 } 272 handler.post(() -> updateClock()); 273 } 274 }; 275 276 @Override setVisibility(int visibility)277 public void setVisibility(int visibility) { 278 if (visibility == View.VISIBLE && !shouldBeVisible()) { 279 return; 280 } 281 282 super.setVisibility(visibility); 283 } 284 setClockVisibleByUser(boolean visible)285 public void setClockVisibleByUser(boolean visible) { 286 mClockVisibleByUser = visible; 287 updateClockVisibility(); 288 } 289 setClockVisibilityByPolicy(boolean visible)290 public void setClockVisibilityByPolicy(boolean visible) { 291 mClockVisibleByPolicy = visible; 292 updateClockVisibility(); 293 } 294 shouldBeVisible()295 private boolean shouldBeVisible() { 296 return mClockVisibleByPolicy && mClockVisibleByUser; 297 } 298 updateClockVisibility()299 private void updateClockVisibility() { 300 boolean visible = shouldBeVisible(); 301 int visibility = visible ? View.VISIBLE : View.GONE; 302 super.setVisibility(visibility); 303 } 304 updateClock()305 final void updateClock() { 306 if (mDemoMode) return; 307 mCalendar.setTimeInMillis(System.currentTimeMillis()); 308 CharSequence smallTime = getSmallTime(); 309 // Setting text actually triggers a layout pass (because the text view is set to 310 // wrap_content width and TextView always relayouts for this). Avoid needless 311 // relayout if the text didn't actually change. 312 if (!TextUtils.equals(smallTime, getText())) { 313 setText(smallTime); 314 } 315 setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime())); 316 } 317 318 /** 319 * In order to avoid the clock growing and shrinking due to proportional fonts, we want to 320 * cache the drawn width at a given number of characters (removing the cache when it changes), 321 * and only use the biggest value. This means that the clock width with grow to the maximum 322 * size over time, but reset whenever the number of characters changes (or the configuration 323 * changes) 324 */ 325 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)326 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 327 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 328 329 int chars = getText().length(); 330 if (chars != mCharsAtCurrentWidth) { 331 mCharsAtCurrentWidth = chars; 332 mCachedWidth = getMeasuredWidth(); 333 return; 334 } 335 336 int measuredWidth = getMeasuredWidth(); 337 if (mCachedWidth > measuredWidth) { 338 setMeasuredDimension(mCachedWidth, getMeasuredHeight()); 339 } else { 340 mCachedWidth = measuredWidth; 341 } 342 } 343 344 @Override onTuningChanged(String key, String newValue)345 public void onTuningChanged(String key, String newValue) { 346 if (CLOCK_SECONDS.equals(key)) { 347 mShowSeconds = TunerService.parseIntegerSwitch(newValue, false); 348 updateShowSeconds(); 349 } else if (StatusBarIconController.ICON_HIDE_LIST.equals(key)) { 350 setClockVisibleByUser(!StatusBarIconController.getIconHideList(getContext(), newValue) 351 .contains("clock")); 352 updateClockVisibility(); 353 } 354 } 355 356 @Override disable(int displayId, int state1, int state2, boolean animate)357 public void disable(int displayId, int state1, int state2, boolean animate) { 358 if (displayId != getDisplay().getDisplayId()) { 359 return; 360 } 361 boolean clockVisibleByPolicy = (state1 & StatusBarManager.DISABLE_CLOCK) == 0; 362 if (clockVisibleByPolicy != mClockVisibleByPolicy) { 363 setClockVisibilityByPolicy(clockVisibleByPolicy); 364 } 365 } 366 367 @Override onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint)368 public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) { 369 mNonAdaptedColor = DarkIconDispatcher.getTint(areas, this, tint); 370 setTextColor(mNonAdaptedColor); 371 } 372 373 // Update text color based when shade scrim changes color. onColorsChanged(boolean lightTheme)374 public void onColorsChanged(boolean lightTheme) { 375 final Context context = new ContextThemeWrapper(mContext, 376 lightTheme ? R.style.Theme_SystemUI_LightWallpaper : R.style.Theme_SystemUI); 377 setTextColor(Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)); 378 } 379 380 @Override onDensityOrFontScaleChanged()381 public void onDensityOrFontScaleChanged() { 382 reloadDimens(); 383 } 384 reloadDimens()385 private void reloadDimens() { 386 // reset mCachedWidth so the new width would be updated properly when next onMeasure 387 mCachedWidth = -1; 388 389 FontSizeUtils.updateFontSize(this, R.dimen.status_bar_clock_size); 390 setPaddingRelative( 391 mContext.getResources().getDimensionPixelSize( 392 R.dimen.status_bar_clock_starting_padding), 393 0, 394 mContext.getResources().getDimensionPixelSize( 395 R.dimen.status_bar_clock_end_padding), 396 0); 397 398 float fontHeight = getPaint().getFontMetricsInt(null); 399 setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight); 400 401 ViewGroup.LayoutParams lp = getLayoutParams(); 402 if (lp != null) { 403 lp.height = (int) Math.ceil(fontHeight); 404 setLayoutParams(lp); 405 } 406 } 407 updateShowSeconds()408 private void updateShowSeconds() { 409 if (mShowSeconds) { 410 // Wait until we have a display to start trying to show seconds. 411 if (mSecondsHandler == null && getDisplay() != null) { 412 mSecondsHandler = new Handler(); 413 if (getDisplay().getState() == Display.STATE_ON) { 414 mSecondsHandler.postAtTime(mSecondTick, 415 SystemClock.uptimeMillis() / 1000 * 1000 + 1000); 416 } 417 mScreenReceiverRegistered = true; 418 IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); 419 filter.addAction(Intent.ACTION_SCREEN_ON); 420 mBroadcastDispatcher.registerReceiver(mScreenReceiver, filter); 421 } 422 } else { 423 if (mSecondsHandler != null) { 424 mScreenReceiverRegistered = false; 425 mBroadcastDispatcher.unregisterReceiver(mScreenReceiver); 426 mSecondsHandler.removeCallbacks(mSecondTick); 427 mSecondsHandler = null; 428 updateClock(); 429 } 430 } 431 } 432 getSmallTime()433 private final CharSequence getSmallTime() { 434 Context context = getContext(); 435 boolean is24 = DateFormat.is24HourFormat(context, mCurrentUserId); 436 if (mDateTimePatternGenerator == null) { 437 // Despite its name, getInstance creates a cloned instance, so reuse the generator to 438 // avoid unnecessary churn. 439 mDateTimePatternGenerator = DateTimePatternGenerator.getInstance( 440 context.getResources().getConfiguration().locale); 441 } 442 443 final char MAGIC1 = '\uEF00'; 444 final char MAGIC2 = '\uEF01'; 445 446 final String formatSkeleton = mShowSeconds 447 ? is24 ? "Hms" : "hms" 448 : is24 ? "Hm" : "hm"; 449 String format = mDateTimePatternGenerator.getBestPattern(formatSkeleton); 450 if (!format.equals(mContentDescriptionFormatString)) { 451 mContentDescriptionFormatString = format; 452 mContentDescriptionFormat = new SimpleDateFormat(format); 453 /* 454 * Search for an unquoted "a" in the format string, so we can 455 * add marker characters around it to let us find it again after 456 * formatting and change its size. 457 */ 458 if (mAmPmStyle != AM_PM_STYLE_NORMAL) { 459 int a = -1; 460 boolean quoted = false; 461 for (int i = 0; i < format.length(); i++) { 462 char c = format.charAt(i); 463 464 if (c == '\'') { 465 quoted = !quoted; 466 } 467 if (!quoted && c == 'a') { 468 a = i; 469 break; 470 } 471 } 472 473 if (a >= 0) { 474 // Move a back so any whitespace before AM/PM is also in the alternate size. 475 final int b = a; 476 while (a > 0 && UCharacter.isUWhiteSpace(format.charAt(a - 1))) { 477 a--; 478 } 479 format = format.substring(0, a) + MAGIC1 + format.substring(a, b) 480 + "a" + MAGIC2 + format.substring(b + 1); 481 } 482 } 483 mClockFormat = new SimpleDateFormat(format); 484 } 485 String result = mClockFormat.format(mCalendar.getTime()); 486 487 if (mAmPmStyle != AM_PM_STYLE_NORMAL) { 488 int magic1 = result.indexOf(MAGIC1); 489 int magic2 = result.indexOf(MAGIC2); 490 if (magic1 >= 0 && magic2 > magic1) { 491 SpannableStringBuilder formatted = new SpannableStringBuilder(result); 492 if (mAmPmStyle == AM_PM_STYLE_GONE) { 493 formatted.delete(magic1, magic2+1); 494 } else { 495 if (mAmPmStyle == AM_PM_STYLE_SMALL) { 496 CharacterStyle style = new RelativeSizeSpan(0.7f); 497 formatted.setSpan(style, magic1, magic2, 498 Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 499 } 500 formatted.delete(magic2, magic2 + 1); 501 formatted.delete(magic1, magic1 + 1); 502 } 503 return formatted; 504 } 505 } 506 507 return result; 508 509 } 510 511 private boolean mDemoMode; 512 513 @Override dispatchDemoCommand(String command, Bundle args)514 public void dispatchDemoCommand(String command, Bundle args) { 515 // Only registered for COMMAND_CLOCK 516 String millis = args.getString("millis"); 517 String hhmm = args.getString("hhmm"); 518 if (millis != null) { 519 mCalendar.setTimeInMillis(Long.parseLong(millis)); 520 } else if (hhmm != null && hhmm.length() == 4) { 521 int hh = Integer.parseInt(hhmm.substring(0, 2)); 522 int mm = Integer.parseInt(hhmm.substring(2)); 523 boolean is24 = DateFormat.is24HourFormat(getContext(), mCurrentUserId); 524 if (is24) { 525 mCalendar.set(Calendar.HOUR_OF_DAY, hh); 526 } else { 527 mCalendar.set(Calendar.HOUR, hh); 528 } 529 mCalendar.set(Calendar.MINUTE, mm); 530 } 531 setText(getSmallTime()); 532 setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime())); 533 } 534 535 @Override onDemoModeStarted()536 public void onDemoModeStarted() { 537 mDemoMode = true; 538 } 539 540 @Override onDemoModeFinished()541 public void onDemoModeFinished() { 542 mDemoMode = false; 543 updateClock(); 544 } 545 546 private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { 547 @Override 548 public void onReceive(Context context, Intent intent) { 549 String action = intent.getAction(); 550 if (Intent.ACTION_SCREEN_OFF.equals(action)) { 551 if (mSecondsHandler != null) { 552 mSecondsHandler.removeCallbacks(mSecondTick); 553 } 554 } else if (Intent.ACTION_SCREEN_ON.equals(action)) { 555 if (mSecondsHandler != null) { 556 mSecondsHandler.postAtTime(mSecondTick, 557 SystemClock.uptimeMillis() / 1000 * 1000 + 1000); 558 } 559 } 560 } 561 }; 562 563 private final Runnable mSecondTick = new Runnable() { 564 @Override 565 public void run() { 566 if (mCalendar != null) { 567 updateClock(); 568 } 569 mSecondsHandler.postAtTime(this, SystemClock.uptimeMillis() / 1000 * 1000 + 1000); 570 } 571 }; 572 } 573 574