1 /*
2  * Copyright (C) 2017 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.keyguard;
18 
19 import android.annotation.AnyThread;
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.graphics.Typeface;
28 import android.graphics.drawable.Icon;
29 import android.icu.text.DateFormat;
30 import android.icu.text.DisplayContext;
31 import android.media.MediaMetadata;
32 import android.media.session.PlaybackState;
33 import android.net.Uri;
34 import android.os.Handler;
35 import android.os.Trace;
36 import android.provider.Settings;
37 import android.service.notification.ZenModeConfig;
38 import android.text.TextUtils;
39 import android.text.style.StyleSpan;
40 
41 import androidx.core.graphics.drawable.IconCompat;
42 import androidx.slice.Slice;
43 import androidx.slice.SliceProvider;
44 import androidx.slice.builders.ListBuilder;
45 import androidx.slice.builders.ListBuilder.RowBuilder;
46 import androidx.slice.builders.SliceAction;
47 
48 import com.android.internal.annotations.VisibleForTesting;
49 import com.android.keyguard.KeyguardUpdateMonitor;
50 import com.android.keyguard.KeyguardUpdateMonitorCallback;
51 import com.android.systemui.R;
52 import com.android.systemui.SystemUIAppComponentFactoryBase;
53 import com.android.systemui.dagger.qualifiers.Background;
54 import com.android.systemui.plugins.statusbar.StatusBarStateController;
55 import com.android.systemui.settings.UserTracker;
56 import com.android.systemui.statusbar.NotificationMediaManager;
57 import com.android.systemui.statusbar.StatusBarState;
58 import com.android.systemui.statusbar.phone.DozeParameters;
59 import com.android.systemui.statusbar.phone.KeyguardBypassController;
60 import com.android.systemui.statusbar.policy.NextAlarmController;
61 import com.android.systemui.statusbar.policy.ZenModeController;
62 import com.android.systemui.util.wakelock.SettableWakeLock;
63 import com.android.systemui.util.wakelock.WakeLock;
64 import com.android.systemui.util.wakelock.WakeLockLogger;
65 
66 import java.util.Date;
67 import java.util.Locale;
68 import java.util.TimeZone;
69 import java.util.concurrent.TimeUnit;
70 
71 import javax.inject.Inject;
72 
73 /**
74  * Simple Slice provider that shows the current date.
75  *
76  * Injection is handled by {@link SystemUIAppComponentFactoryBase} +
77  * {@link com.android.systemui.dagger.GlobalRootComponent#inject(KeyguardSliceProvider)}.
78  */
79 public class KeyguardSliceProvider extends SliceProvider implements
80         NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback,
81         NotificationMediaManager.MediaListener, StatusBarStateController.StateListener,
82         SystemUIAppComponentFactoryBase.ContextInitializer {
83 
84     private static final String TAG = "KgdSliceProvider";
85 
86     private static final StyleSpan BOLD_STYLE = new StyleSpan(Typeface.BOLD);
87     public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main";
88     private static final String KEYGUARD_HEADER_URI =
89             "content://com.android.systemui.keyguard/header";
90     public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date";
91     public static final String KEYGUARD_NEXT_ALARM_URI =
92             "content://com.android.systemui.keyguard/alarm";
93     public static final String KEYGUARD_DND_URI = "content://com.android.systemui.keyguard/dnd";
94     public static final String KEYGUARD_MEDIA_URI =
95             "content://com.android.systemui.keyguard/media";
96     public static final String KEYGUARD_ACTION_URI =
97             "content://com.android.systemui.keyguard/action";
98 
99     /**
100      * Only show alarms that will ring within N hours.
101      */
102     @VisibleForTesting
103     static final int ALARM_VISIBILITY_HOURS = 12;
104 
105     private static final Object sInstanceLock = new Object();
106     private static KeyguardSliceProvider sInstance;
107 
108     protected final Uri mSliceUri;
109     protected final Uri mHeaderUri;
110     protected final Uri mDateUri;
111     protected final Uri mAlarmUri;
112     protected final Uri mDndUri;
113     protected final Uri mMediaUri;
114     private final Date mCurrentTime = new Date();
115     private final Handler mHandler;
116     private final Handler mMediaHandler;
117     private final AlarmManager.OnAlarmListener mUpdateNextAlarm = this::updateNextAlarm;
118     @Inject
119     public DozeParameters mDozeParameters;
120     @VisibleForTesting
121     protected SettableWakeLock mMediaWakeLock;
122     @Inject
123     public ZenModeController mZenModeController;
124     private String mDatePattern;
125     private DateFormat mDateFormat;
126     private String mLastText;
127     private boolean mRegistered;
128     private String mNextAlarm;
129     @Inject
130     public NextAlarmController mNextAlarmController;
131     @Inject
132     public AlarmManager mAlarmManager;
133     @Inject
134     public ContentResolver mContentResolver;
135     private AlarmManager.AlarmClockInfo mNextAlarmInfo;
136     private PendingIntent mPendingIntent;
137     @Inject
138     public NotificationMediaManager mMediaManager;
139     @Inject
140     public StatusBarStateController mStatusBarStateController;
141     @Inject
142     public KeyguardBypassController mKeyguardBypassController;
143     @Inject
144     public KeyguardUpdateMonitor mKeyguardUpdateMonitor;
145     @Inject
146     UserTracker mUserTracker;
147     private CharSequence mMediaTitle;
148     private CharSequence mMediaArtist;
149     protected boolean mDozing;
150     private int mStatusBarState;
151     private boolean mMediaIsVisible;
152     private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback;
153     @Inject
154     WakeLockLogger mWakeLockLogger;
155     @Inject
156     @Background
157     Handler mBgHandler;
158 
159     /**
160      * Receiver responsible for time ticking and updating the date format.
161      */
162     @VisibleForTesting
163     final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
164         @Override
165         public void onReceive(Context context, Intent intent) {
166             final String action = intent.getAction();
167             if (Intent.ACTION_DATE_CHANGED.equals(action)) {
168                 synchronized (this) {
169                     updateClockLocked();
170                 }
171             } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
172                 synchronized (this) {
173                     cleanDateFormatLocked();
174                 }
175             }
176         }
177     };
178 
179     @VisibleForTesting
180     final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
181             new KeyguardUpdateMonitorCallback() {
182                 @Override
183                 public void onTimeChanged() {
184                     synchronized (this) {
185                         updateClockLocked();
186                     }
187                 }
188 
189                 @Override
190                 public void onTimeZoneChanged(TimeZone timeZone) {
191                     synchronized (this) {
192                         cleanDateFormatLocked();
193                     }
194                 }
195             };
196 
getAttachedInstance()197     public static KeyguardSliceProvider getAttachedInstance() {
198         return KeyguardSliceProvider.sInstance;
199     }
200 
KeyguardSliceProvider()201     public KeyguardSliceProvider() {
202         mHandler = new Handler();
203         mMediaHandler = new Handler();
204         mSliceUri = Uri.parse(KEYGUARD_SLICE_URI);
205         mHeaderUri = Uri.parse(KEYGUARD_HEADER_URI);
206         mDateUri = Uri.parse(KEYGUARD_DATE_URI);
207         mAlarmUri = Uri.parse(KEYGUARD_NEXT_ALARM_URI);
208         mDndUri = Uri.parse(KEYGUARD_DND_URI);
209         mMediaUri = Uri.parse(KEYGUARD_MEDIA_URI);
210     }
211 
212     @AnyThread
213     @Override
onBindSlice(Uri sliceUri)214     public Slice onBindSlice(Uri sliceUri) {
215         Trace.beginSection("KeyguardSliceProvider#onBindSlice");
216         Slice slice;
217         synchronized (this) {
218             ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY);
219             if (needsMediaLocked()) {
220                 addMediaLocked(builder);
221             } else {
222                 builder.addRow(new RowBuilder(mDateUri).setTitle(mLastText));
223             }
224             addNextAlarmLocked(builder);
225             addZenModeLocked(builder);
226             addPrimaryActionLocked(builder);
227             slice = builder.build();
228         }
229         Trace.endSection();
230         return slice;
231     }
232 
needsMediaLocked()233     protected boolean needsMediaLocked() {
234         boolean keepWhenAwake = mKeyguardBypassController != null
235                 && mKeyguardBypassController.getBypassEnabled() && mDozeParameters.getAlwaysOn();
236         // Show header if music is playing and the status bar is in the shade state. This way, an
237         // animation isn't necessary when pressing power and transitioning to AOD.
238         boolean keepWhenShade = mStatusBarState == StatusBarState.SHADE && mMediaIsVisible;
239         return !TextUtils.isEmpty(mMediaTitle) && mMediaIsVisible && (mDozing || keepWhenAwake
240                 || keepWhenShade);
241     }
242 
addMediaLocked(ListBuilder listBuilder)243     protected void addMediaLocked(ListBuilder listBuilder) {
244         if (TextUtils.isEmpty(mMediaTitle)) {
245             return;
246         }
247         listBuilder.setHeader(new ListBuilder.HeaderBuilder(mHeaderUri).setTitle(mMediaTitle));
248 
249         if (!TextUtils.isEmpty(mMediaArtist)) {
250             RowBuilder albumBuilder = new RowBuilder(mMediaUri);
251             albumBuilder.setTitle(mMediaArtist);
252 
253             Icon mediaIcon = mMediaManager == null ? null : mMediaManager.getMediaIcon();
254             IconCompat mediaIconCompat = mediaIcon == null ? null
255                     : IconCompat.createFromIcon(getContext(), mediaIcon);
256             if (mediaIconCompat != null) {
257                 albumBuilder.addEndItem(mediaIconCompat, ListBuilder.ICON_IMAGE);
258             }
259 
260             listBuilder.addRow(albumBuilder);
261         }
262     }
263 
addPrimaryActionLocked(ListBuilder builder)264     protected void addPrimaryActionLocked(ListBuilder builder) {
265         // Add simple action because API requires it; Keyguard handles presenting
266         // its own slices so this action + icon are actually never used.
267         IconCompat icon = IconCompat.createWithResource(getContext(),
268                 R.drawable.ic_access_alarms_big);
269         SliceAction action = SliceAction.createDeeplink(mPendingIntent, icon,
270                 ListBuilder.ICON_IMAGE, mLastText);
271         RowBuilder primaryActionRow = new RowBuilder(Uri.parse(KEYGUARD_ACTION_URI))
272                 .setPrimaryAction(action);
273         builder.addRow(primaryActionRow);
274     }
275 
addNextAlarmLocked(ListBuilder builder)276     protected void addNextAlarmLocked(ListBuilder builder) {
277         if (TextUtils.isEmpty(mNextAlarm)) {
278             return;
279         }
280         IconCompat alarmIcon = IconCompat.createWithResource(getContext(),
281                 R.drawable.ic_access_alarms_big);
282         RowBuilder alarmRowBuilder = new RowBuilder(mAlarmUri)
283                 .setTitle(mNextAlarm)
284                 .addEndItem(alarmIcon, ListBuilder.ICON_IMAGE);
285         builder.addRow(alarmRowBuilder);
286     }
287 
288     /**
289      * Add zen mode (DND) icon to slice if it's enabled.
290      * @param builder The slice builder.
291      */
addZenModeLocked(ListBuilder builder)292     protected void addZenModeLocked(ListBuilder builder) {
293         if (!isDndOn()) {
294             return;
295         }
296         RowBuilder dndBuilder = new RowBuilder(mDndUri)
297                 .setContentDescription(getContext().getResources()
298                         .getString(R.string.accessibility_quick_settings_dnd))
299                 .addEndItem(
300                     IconCompat.createWithResource(getContext(), R.drawable.stat_sys_dnd),
301                     ListBuilder.ICON_IMAGE);
302         builder.addRow(dndBuilder);
303     }
304 
305     /**
306      * Return true if DND is enabled.
307      */
isDndOn()308     protected boolean isDndOn() {
309         return mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF;
310     }
311 
312     @Override
onCreateSliceProvider()313     public boolean onCreateSliceProvider() {
314         mContextAvailableCallback.onContextAvailable(getContext());
315         mMediaWakeLock = new SettableWakeLock(
316                 WakeLock.createPartial(getContext(), mWakeLockLogger, "media"), "media");
317         synchronized (KeyguardSliceProvider.sInstanceLock) {
318             KeyguardSliceProvider oldInstance = KeyguardSliceProvider.sInstance;
319             if (oldInstance != null) {
320                 oldInstance.onDestroy();
321             }
322             mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern);
323             mPendingIntent = PendingIntent.getActivity(getContext(), 0,
324                     new Intent(getContext(), KeyguardSliceProvider.class),
325                     PendingIntent.FLAG_IMMUTABLE);
326             mMediaManager.addCallback(this);
327             mStatusBarStateController.addCallback(this);
328             mNextAlarmController.addCallback(this);
329             mZenModeController.addCallback(this);
330             KeyguardSliceProvider.sInstance = this;
331             registerClockUpdate();
332             updateClockLocked();
333         }
334         return true;
335     }
336 
337     @VisibleForTesting
onDestroy()338     protected void onDestroy() {
339         synchronized (KeyguardSliceProvider.sInstanceLock) {
340             mNextAlarmController.removeCallback(this);
341             mZenModeController.removeCallback(this);
342             mMediaWakeLock.setAcquired(false);
343             mAlarmManager.cancel(mUpdateNextAlarm);
344             if (mRegistered) {
345                 mRegistered = false;
346                 mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
347                 getContext().unregisterReceiver(mIntentReceiver);
348             }
349             KeyguardSliceProvider.sInstance = null;
350         }
351     }
352 
353     @Override
onZenChanged(int zen)354     public void onZenChanged(int zen) {
355         notifyChange();
356     }
357 
358     @Override
onConfigChanged(ZenModeConfig config)359     public void onConfigChanged(ZenModeConfig config) {
360         notifyChange();
361     }
362 
updateNextAlarm()363     private void updateNextAlarm() {
364         synchronized (this) {
365             if (withinNHoursLocked(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) {
366                 String pattern = android.text.format.DateFormat.is24HourFormat(getContext(),
367                         mUserTracker.getUserId()) ? "HH:mm" : "h:mm";
368                 mNextAlarm = android.text.format.DateFormat.format(pattern,
369                         mNextAlarmInfo.getTriggerTime()).toString();
370             } else {
371                 mNextAlarm = "";
372             }
373         }
374         notifyChange();
375     }
376 
withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours)377     private boolean withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours) {
378         if (alarmClockInfo == null) {
379             return false;
380         }
381 
382         long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours);
383         return mNextAlarmInfo.getTriggerTime() <= limit;
384     }
385 
386     /**
387      * Registers a broadcast receiver for clock updates, include date, time zone and manually
388      * changing the date/time via the settings app.
389      */
390     @VisibleForTesting
registerClockUpdate()391     protected void registerClockUpdate() {
392         synchronized (this) {
393             if (mRegistered) {
394                 return;
395             }
396 
397             IntentFilter filter = new IntentFilter();
398             filter.addAction(Intent.ACTION_DATE_CHANGED);
399             filter.addAction(Intent.ACTION_LOCALE_CHANGED);
400             getContext().registerReceiver(mIntentReceiver, filter, null /* permission*/,
401                     null /* scheduler */);
402             mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
403             mRegistered = true;
404         }
405     }
406 
407     @VisibleForTesting
isRegistered()408     boolean isRegistered() {
409         synchronized (this) {
410             return mRegistered;
411         }
412     }
413 
updateClockLocked()414     protected void updateClockLocked() {
415         final String text = getFormattedDateLocked();
416         if (!text.equals(mLastText)) {
417             mLastText = text;
418             notifyChange();
419         }
420     }
421 
getFormattedDateLocked()422     protected String getFormattedDateLocked() {
423         if (mDateFormat == null) {
424             final Locale l = Locale.getDefault();
425             DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l);
426             // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of
427             // CAPITALIZATION_FOR_STANDALONE is to address
428             // https://unicode-org.atlassian.net/browse/ICU-21631
429             // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE
430             format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE);
431             mDateFormat = format;
432         }
433         mCurrentTime.setTime(System.currentTimeMillis());
434         return mDateFormat.format(mCurrentTime);
435     }
436 
437     @VisibleForTesting
cleanDateFormatLocked()438     void cleanDateFormatLocked() {
439         mDateFormat = null;
440     }
441 
442     @Override
onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm)443     public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
444         synchronized (this) {
445             mNextAlarmInfo = nextAlarm;
446             mAlarmManager.cancel(mUpdateNextAlarm);
447 
448             long triggerAt = mNextAlarmInfo == null ? -1 : mNextAlarmInfo.getTriggerTime()
449                     - TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS);
450             if (triggerAt > 0) {
451                 mAlarmManager.setExact(AlarmManager.RTC, triggerAt, "lock_screen_next_alarm",
452                         mUpdateNextAlarm, mHandler);
453             }
454         }
455         updateNextAlarm();
456     }
457 
458     /**
459      * Called whenever new media metadata is available.
460      * @param metadata New metadata.
461      */
462     @Override
onPrimaryMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state)463     public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata,
464             @PlaybackState.State int state) {
465         synchronized (this) {
466             boolean nextVisible = NotificationMediaManager.isPlayingState(state);
467             mMediaHandler.removeCallbacksAndMessages(null);
468             if (mMediaIsVisible && !nextVisible && mStatusBarState != StatusBarState.SHADE) {
469                 // We need to delay this event for a few millis when stopping to avoid jank in the
470                 // animation. The media app might not send its update when buffering, and the slice
471                 // would end up without a header for 0.5 second.
472                 mMediaWakeLock.setAcquired(true);
473                 mMediaHandler.postDelayed(() -> {
474                     synchronized (this) {
475                         updateMediaStateLocked(metadata, state);
476                         mMediaWakeLock.setAcquired(false);
477                     }
478                 }, 2000);
479             } else {
480                 mMediaWakeLock.setAcquired(false);
481                 updateMediaStateLocked(metadata, state);
482             }
483         }
484     }
485 
updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state)486     private void updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state) {
487         boolean nextVisible = NotificationMediaManager.isPlayingState(state);
488         CharSequence title = null;
489         if (metadata != null) {
490             title = metadata.getText(MediaMetadata.METADATA_KEY_TITLE);
491             if (TextUtils.isEmpty(title)) {
492                 title = getContext().getResources().getString(R.string.music_controls_no_title);
493             }
494         }
495         CharSequence artist = metadata == null ? null : metadata.getText(
496                 MediaMetadata.METADATA_KEY_ARTIST);
497 
498         if (nextVisible == mMediaIsVisible && TextUtils.equals(title, mMediaTitle)
499                 && TextUtils.equals(artist, mMediaArtist)) {
500             return;
501         }
502         mMediaTitle = title;
503         mMediaArtist = artist;
504         mMediaIsVisible = nextVisible;
505         notifyChange();
506     }
507 
notifyChange()508     protected void notifyChange() {
509         mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */));
510     }
511 
512     @Override
onDozingChanged(boolean isDozing)513     public void onDozingChanged(boolean isDozing) {
514         final boolean notify;
515         synchronized (this) {
516             boolean neededMedia = needsMediaLocked();
517             mDozing = isDozing;
518             notify = neededMedia != needsMediaLocked();
519         }
520         if (notify) {
521             notifyChange();
522         }
523     }
524 
525     @Override
onStateChanged(int newState)526     public void onStateChanged(int newState) {
527         final boolean notify;
528         synchronized (this) {
529             boolean needsMedia = needsMediaLocked();
530             mStatusBarState = newState;
531             notify = needsMedia != needsMediaLocked();
532         }
533         if (notify) {
534             notifyChange();
535         }
536     }
537 
538     @Override
setContextAvailableCallback( SystemUIAppComponentFactoryBase.ContextAvailableCallback callback)539     public void setContextAvailableCallback(
540             SystemUIAppComponentFactoryBase.ContextAvailableCallback callback) {
541         mContextAvailableCallback = callback;
542     }
543 }
544