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