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