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