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