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