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