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