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