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