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.app.StatusBarManager; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.res.TypedArray; 25 import android.graphics.Rect; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Parcelable; 29 import android.os.SystemClock; 30 import android.os.UserHandle; 31 import android.text.Spannable; 32 import android.text.SpannableStringBuilder; 33 import android.text.format.DateFormat; 34 import android.text.style.CharacterStyle; 35 import android.text.style.RelativeSizeSpan; 36 import android.util.AttributeSet; 37 import android.view.Display; 38 import android.view.View; 39 import android.widget.TextView; 40 41 import com.android.settingslib.Utils; 42 import com.android.systemui.DemoMode; 43 import com.android.systemui.Dependency; 44 import com.android.systemui.FontSizeUtils; 45 import com.android.systemui.R; 46 import com.android.systemui.broadcast.BroadcastDispatcher; 47 import com.android.systemui.plugins.DarkIconDispatcher; 48 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 49 import com.android.systemui.settings.CurrentUserTracker; 50 import com.android.systemui.statusbar.CommandQueue; 51 import com.android.systemui.statusbar.phone.StatusBarIconController; 52 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; 53 import com.android.systemui.tuner.TunerService; 54 import com.android.systemui.tuner.TunerService.Tunable; 55 56 import libcore.icu.LocaleData; 57 58 import java.text.SimpleDateFormat; 59 import java.util.Calendar; 60 import java.util.Locale; 61 import java.util.TimeZone; 62 63 /** 64 * Digital clock for the status bar. 65 */ 66 public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.Callbacks, 67 DarkReceiver, ConfigurationListener { 68 69 public static final String CLOCK_SECONDS = "clock_seconds"; 70 private static final String CLOCK_SUPER_PARCELABLE = "clock_super_parcelable"; 71 private static final String CURRENT_USER_ID = "current_user_id"; 72 private static final String VISIBLE_BY_POLICY = "visible_by_policy"; 73 private static final String VISIBLE_BY_USER = "visible_by_user"; 74 private static final String SHOW_SECONDS = "show_seconds"; 75 private static final String VISIBILITY = "visibility"; 76 77 private final CurrentUserTracker mCurrentUserTracker; 78 private final CommandQueue mCommandQueue; 79 private int mCurrentUserId; 80 81 private boolean mClockVisibleByPolicy = true; 82 private boolean mClockVisibleByUser = true; 83 84 private boolean mAttached; 85 private boolean mScreenReceiverRegistered; 86 private Calendar mCalendar; 87 private String mClockFormatString; 88 private SimpleDateFormat mClockFormat; 89 private SimpleDateFormat mContentDescriptionFormat; 90 private Locale mLocale; 91 92 private static final int AM_PM_STYLE_NORMAL = 0; 93 private static final int AM_PM_STYLE_SMALL = 1; 94 private static final int AM_PM_STYLE_GONE = 2; 95 96 private final int mAmPmStyle; 97 private final boolean mShowDark; 98 private boolean mShowSeconds; 99 private Handler mSecondsHandler; 100 101 /** 102 * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings 103 * for text. 104 */ 105 private boolean mUseWallpaperTextColor; 106 107 /** 108 * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized. 109 */ 110 private int mNonAdaptedColor; 111 112 private final BroadcastDispatcher mBroadcastDispatcher; 113 Clock(Context context, AttributeSet attrs)114 public Clock(Context context, AttributeSet attrs) { 115 this(context, attrs, 0); 116 } 117 Clock(Context context, AttributeSet attrs, int defStyle)118 public Clock(Context context, AttributeSet attrs, int defStyle) { 119 super(context, attrs, defStyle); 120 mCommandQueue = Dependency.get(CommandQueue.class); 121 TypedArray a = context.getTheme().obtainStyledAttributes( 122 attrs, 123 R.styleable.Clock, 124 0, 0); 125 try { 126 mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE); 127 mShowDark = a.getBoolean(R.styleable.Clock_showDark, true); 128 mNonAdaptedColor = getCurrentTextColor(); 129 } finally { 130 a.recycle(); 131 } 132 mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); 133 mCurrentUserTracker = new CurrentUserTracker(mBroadcastDispatcher) { 134 @Override 135 public void onUserSwitched(int newUserId) { 136 mCurrentUserId = newUserId; 137 } 138 }; 139 } 140 141 @Override onSaveInstanceState()142 public Parcelable onSaveInstanceState() { 143 Bundle bundle = new Bundle(); 144 bundle.putParcelable(CLOCK_SUPER_PARCELABLE, super.onSaveInstanceState()); 145 bundle.putInt(CURRENT_USER_ID, mCurrentUserId); 146 bundle.putBoolean(VISIBLE_BY_POLICY, mClockVisibleByPolicy); 147 bundle.putBoolean(VISIBLE_BY_USER, mClockVisibleByUser); 148 bundle.putBoolean(SHOW_SECONDS, mShowSeconds); 149 bundle.putInt(VISIBILITY, getVisibility()); 150 151 return bundle; 152 } 153 154 @Override onRestoreInstanceState(Parcelable state)155 public void onRestoreInstanceState(Parcelable state) { 156 if (state == null || !(state instanceof Bundle)) { 157 super.onRestoreInstanceState(state); 158 return; 159 } 160 161 Bundle bundle = (Bundle) state; 162 Parcelable superState = bundle.getParcelable(CLOCK_SUPER_PARCELABLE); 163 super.onRestoreInstanceState(superState); 164 if (bundle.containsKey(CURRENT_USER_ID)) { 165 mCurrentUserId = bundle.getInt(CURRENT_USER_ID); 166 } 167 mClockVisibleByPolicy = bundle.getBoolean(VISIBLE_BY_POLICY, true); 168 mClockVisibleByUser = bundle.getBoolean(VISIBLE_BY_USER, true); 169 mShowSeconds = bundle.getBoolean(SHOW_SECONDS, false); 170 if (bundle.containsKey(VISIBILITY)) { 171 super.setVisibility(bundle.getInt(VISIBILITY)); 172 } 173 } 174 175 @Override onAttachedToWindow()176 protected void onAttachedToWindow() { 177 super.onAttachedToWindow(); 178 179 if (!mAttached) { 180 mAttached = true; 181 IntentFilter filter = new IntentFilter(); 182 183 filter.addAction(Intent.ACTION_TIME_TICK); 184 filter.addAction(Intent.ACTION_TIME_CHANGED); 185 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 186 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); 187 filter.addAction(Intent.ACTION_USER_SWITCHED); 188 189 // NOTE: This receiver could run before this method returns, as it's not dispatching 190 // on the main thread and BroadcastDispatcher may not need to register with Context. 191 // The receiver will return immediately if the view does not have a Handler yet. 192 mBroadcastDispatcher.registerReceiverWithHandler(mIntentReceiver, filter, 193 Dependency.get(Dependency.TIME_TICK_HANDLER), UserHandle.ALL); 194 Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS, 195 StatusBarIconController.ICON_BLACKLIST); 196 mCommandQueue.addCallback(this); 197 if (mShowDark) { 198 Dependency.get(DarkIconDispatcher.class).addDarkReceiver(this); 199 } 200 mCurrentUserTracker.startTracking(); 201 mCurrentUserId = mCurrentUserTracker.getCurrentUserId(); 202 } 203 204 // The time zone may have changed while the receiver wasn't registered, so update the Time 205 mCalendar = Calendar.getInstance(TimeZone.getDefault()); 206 mClockFormatString = ""; 207 208 // Make sure we update to the current time 209 updateClock(); 210 updateClockVisibility(); 211 updateShowSeconds(); 212 } 213 214 @Override onDetachedFromWindow()215 protected void onDetachedFromWindow() { 216 super.onDetachedFromWindow(); 217 if (mScreenReceiverRegistered) { 218 mScreenReceiverRegistered = false; 219 mBroadcastDispatcher.unregisterReceiver(mScreenReceiver); 220 if (mSecondsHandler != null) { 221 mSecondsHandler.removeCallbacks(mSecondTick); 222 mSecondsHandler = null; 223 } 224 } 225 if (mAttached) { 226 mBroadcastDispatcher.unregisterReceiver(mIntentReceiver); 227 mAttached = false; 228 Dependency.get(TunerService.class).removeTunable(this); 229 mCommandQueue.removeCallback(this); 230 if (mShowDark) { 231 Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(this); 232 } 233 mCurrentUserTracker.stopTracking(); 234 } 235 } 236 237 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 238 @Override 239 public void onReceive(Context context, Intent intent) { 240 // If the handler is null, it means we received a broadcast while the view has not 241 // finished being attached or in the process of being detached. 242 // In that case, do not post anything. 243 Handler handler = getHandler(); 244 if (handler == null) return; 245 246 String action = intent.getAction(); 247 if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)) { 248 String tz = intent.getStringExtra(Intent.EXTRA_TIMEZONE); 249 handler.post(() -> { 250 mCalendar = Calendar.getInstance(TimeZone.getTimeZone(tz)); 251 if (mClockFormat != null) { 252 mClockFormat.setTimeZone(mCalendar.getTimeZone()); 253 } 254 }); 255 } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { 256 final Locale newLocale = getResources().getConfiguration().locale; 257 handler.post(() -> { 258 if (!newLocale.equals(mLocale)) { 259 mLocale = newLocale; 260 mClockFormatString = ""; // force refresh 261 } 262 }); 263 } 264 handler.post(() -> updateClock()); 265 } 266 }; 267 268 @Override setVisibility(int visibility)269 public void setVisibility(int visibility) { 270 if (visibility == View.VISIBLE && !shouldBeVisible()) { 271 return; 272 } 273 274 super.setVisibility(visibility); 275 } 276 setClockVisibleByUser(boolean visible)277 public void setClockVisibleByUser(boolean visible) { 278 mClockVisibleByUser = visible; 279 updateClockVisibility(); 280 } 281 setClockVisibilityByPolicy(boolean visible)282 public void setClockVisibilityByPolicy(boolean visible) { 283 mClockVisibleByPolicy = visible; 284 updateClockVisibility(); 285 } 286 shouldBeVisible()287 private boolean shouldBeVisible() { 288 return mClockVisibleByPolicy && mClockVisibleByUser; 289 } 290 updateClockVisibility()291 private void updateClockVisibility() { 292 boolean visible = shouldBeVisible(); 293 int visibility = visible ? View.VISIBLE : View.GONE; 294 super.setVisibility(visibility); 295 } 296 updateClock()297 final void updateClock() { 298 if (mDemoMode) return; 299 mCalendar.setTimeInMillis(System.currentTimeMillis()); 300 setText(getSmallTime()); 301 setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime())); 302 } 303 304 @Override onTuningChanged(String key, String newValue)305 public void onTuningChanged(String key, String newValue) { 306 if (CLOCK_SECONDS.equals(key)) { 307 mShowSeconds = TunerService.parseIntegerSwitch(newValue, false); 308 updateShowSeconds(); 309 } else { 310 setClockVisibleByUser(!StatusBarIconController.getIconBlacklist(getContext(), newValue) 311 .contains("clock")); 312 updateClockVisibility(); 313 } 314 } 315 316 @Override disable(int displayId, int state1, int state2, boolean animate)317 public void disable(int displayId, int state1, int state2, boolean animate) { 318 if (displayId != getDisplay().getDisplayId()) { 319 return; 320 } 321 boolean clockVisibleByPolicy = (state1 & StatusBarManager.DISABLE_CLOCK) == 0; 322 if (clockVisibleByPolicy != mClockVisibleByPolicy) { 323 setClockVisibilityByPolicy(clockVisibleByPolicy); 324 } 325 } 326 327 @Override onDarkChanged(Rect area, float darkIntensity, int tint)328 public void onDarkChanged(Rect area, float darkIntensity, int tint) { 329 mNonAdaptedColor = DarkIconDispatcher.getTint(area, this, tint); 330 if (!mUseWallpaperTextColor) { 331 setTextColor(mNonAdaptedColor); 332 } 333 } 334 335 @Override onDensityOrFontScaleChanged()336 public void onDensityOrFontScaleChanged() { 337 FontSizeUtils.updateFontSize(this, R.dimen.status_bar_clock_size); 338 setPaddingRelative( 339 mContext.getResources().getDimensionPixelSize( 340 R.dimen.status_bar_clock_starting_padding), 341 0, 342 mContext.getResources().getDimensionPixelSize( 343 R.dimen.status_bar_clock_end_padding), 344 0); 345 } 346 347 /** 348 * Sets whether the clock uses the wallpaperTextColor. If we're not using it, we'll revert back 349 * to dark-mode-based/tinted colors. 350 * 351 * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for text color 352 */ useWallpaperTextColor(boolean shouldUseWallpaperTextColor)353 public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) { 354 if (shouldUseWallpaperTextColor == mUseWallpaperTextColor) { 355 return; 356 } 357 mUseWallpaperTextColor = shouldUseWallpaperTextColor; 358 359 if (mUseWallpaperTextColor) { 360 setTextColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor)); 361 } else { 362 setTextColor(mNonAdaptedColor); 363 } 364 } 365 updateShowSeconds()366 private void updateShowSeconds() { 367 if (mShowSeconds) { 368 // Wait until we have a display to start trying to show seconds. 369 if (mSecondsHandler == null && getDisplay() != null) { 370 mSecondsHandler = new Handler(); 371 if (getDisplay().getState() == Display.STATE_ON) { 372 mSecondsHandler.postAtTime(mSecondTick, 373 SystemClock.uptimeMillis() / 1000 * 1000 + 1000); 374 } 375 mScreenReceiverRegistered = true; 376 IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); 377 filter.addAction(Intent.ACTION_SCREEN_ON); 378 mBroadcastDispatcher.registerReceiver(mScreenReceiver, filter); 379 } 380 } else { 381 if (mSecondsHandler != null) { 382 mScreenReceiverRegistered = false; 383 mBroadcastDispatcher.unregisterReceiver(mScreenReceiver); 384 mSecondsHandler.removeCallbacks(mSecondTick); 385 mSecondsHandler = null; 386 updateClock(); 387 } 388 } 389 } 390 getSmallTime()391 private final CharSequence getSmallTime() { 392 Context context = getContext(); 393 boolean is24 = DateFormat.is24HourFormat(context, mCurrentUserId); 394 LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); 395 396 final char MAGIC1 = '\uEF00'; 397 final char MAGIC2 = '\uEF01'; 398 399 SimpleDateFormat sdf; 400 String format = mShowSeconds 401 ? is24 ? d.timeFormat_Hms : d.timeFormat_hms 402 : is24 ? d.timeFormat_Hm : d.timeFormat_hm; 403 if (!format.equals(mClockFormatString)) { 404 mContentDescriptionFormat = new SimpleDateFormat(format); 405 /* 406 * Search for an unquoted "a" in the format string, so we can 407 * add dummy characters around it to let us find it again after 408 * formatting and change its size. 409 */ 410 if (mAmPmStyle != AM_PM_STYLE_NORMAL) { 411 int a = -1; 412 boolean quoted = false; 413 for (int i = 0; i < format.length(); i++) { 414 char c = format.charAt(i); 415 416 if (c == '\'') { 417 quoted = !quoted; 418 } 419 if (!quoted && c == 'a') { 420 a = i; 421 break; 422 } 423 } 424 425 if (a >= 0) { 426 // Move a back so any whitespace before AM/PM is also in the alternate size. 427 final int b = a; 428 while (a > 0 && Character.isWhitespace(format.charAt(a-1))) { 429 a--; 430 } 431 format = format.substring(0, a) + MAGIC1 + format.substring(a, b) 432 + "a" + MAGIC2 + format.substring(b + 1); 433 } 434 } 435 mClockFormat = sdf = new SimpleDateFormat(format); 436 mClockFormatString = format; 437 } else { 438 sdf = mClockFormat; 439 } 440 String result = sdf.format(mCalendar.getTime()); 441 442 if (mAmPmStyle != AM_PM_STYLE_NORMAL) { 443 int magic1 = result.indexOf(MAGIC1); 444 int magic2 = result.indexOf(MAGIC2); 445 if (magic1 >= 0 && magic2 > magic1) { 446 SpannableStringBuilder formatted = new SpannableStringBuilder(result); 447 if (mAmPmStyle == AM_PM_STYLE_GONE) { 448 formatted.delete(magic1, magic2+1); 449 } else { 450 if (mAmPmStyle == AM_PM_STYLE_SMALL) { 451 CharacterStyle style = new RelativeSizeSpan(0.7f); 452 formatted.setSpan(style, magic1, magic2, 453 Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 454 } 455 formatted.delete(magic2, magic2 + 1); 456 formatted.delete(magic1, magic1 + 1); 457 } 458 return formatted; 459 } 460 } 461 462 return result; 463 464 } 465 466 private boolean mDemoMode; 467 468 @Override dispatchDemoCommand(String command, Bundle args)469 public void dispatchDemoCommand(String command, Bundle args) { 470 if (!mDemoMode && command.equals(COMMAND_ENTER)) { 471 mDemoMode = true; 472 } else if (mDemoMode && command.equals(COMMAND_EXIT)) { 473 mDemoMode = false; 474 updateClock(); 475 } else if (mDemoMode && command.equals(COMMAND_CLOCK)) { 476 String millis = args.getString("millis"); 477 String hhmm = args.getString("hhmm"); 478 if (millis != null) { 479 mCalendar.setTimeInMillis(Long.parseLong(millis)); 480 } else if (hhmm != null && hhmm.length() == 4) { 481 int hh = Integer.parseInt(hhmm.substring(0, 2)); 482 int mm = Integer.parseInt(hhmm.substring(2)); 483 boolean is24 = DateFormat.is24HourFormat(getContext(), mCurrentUserId); 484 if (is24) { 485 mCalendar.set(Calendar.HOUR_OF_DAY, hh); 486 } else { 487 mCalendar.set(Calendar.HOUR, hh); 488 } 489 mCalendar.set(Calendar.MINUTE, mm); 490 } 491 setText(getSmallTime()); 492 setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime())); 493 } 494 } 495 496 private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { 497 @Override 498 public void onReceive(Context context, Intent intent) { 499 String action = intent.getAction(); 500 if (Intent.ACTION_SCREEN_OFF.equals(action)) { 501 if (mSecondsHandler != null) { 502 mSecondsHandler.removeCallbacks(mSecondTick); 503 } 504 } else if (Intent.ACTION_SCREEN_ON.equals(action)) { 505 if (mSecondsHandler != null) { 506 mSecondsHandler.postAtTime(mSecondTick, 507 SystemClock.uptimeMillis() / 1000 * 1000 + 1000); 508 } 509 } 510 } 511 }; 512 513 private final Runnable mSecondTick = new Runnable() { 514 @Override 515 public void run() { 516 if (mCalendar != null) { 517 updateClock(); 518 } 519 mSecondsHandler.postAtTime(this, SystemClock.uptimeMillis() / 1000 * 1000 + 1000); 520 } 521 }; 522 } 523 524