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