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