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