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