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