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