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