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