• 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 package com.android.systemui.statusbar;
17 
18 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
19 import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_MEDIA_FAKE_ARTWORK;
20 import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_WALLPAPER;
21 import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK;
22 
23 import android.annotation.MainThread;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.Notification;
27 import android.content.Context;
28 import android.graphics.Bitmap;
29 import android.graphics.drawable.BitmapDrawable;
30 import android.graphics.drawable.ColorDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.Icon;
33 import android.media.MediaMetadata;
34 import android.media.session.MediaController;
35 import android.media.session.MediaSession;
36 import android.media.session.MediaSessionManager;
37 import android.media.session.PlaybackState;
38 import android.os.AsyncTask;
39 import android.os.Trace;
40 import android.os.UserHandle;
41 import android.service.notification.NotificationListenerService;
42 import android.service.notification.NotificationStats;
43 import android.service.notification.StatusBarNotification;
44 import android.util.ArraySet;
45 import android.util.Log;
46 import android.view.View;
47 import android.widget.ImageView;
48 
49 import com.android.internal.statusbar.NotificationVisibility;
50 import com.android.systemui.Dependency;
51 import com.android.systemui.Dumpable;
52 import com.android.systemui.animation.Interpolators;
53 import com.android.systemui.colorextraction.SysuiColorExtractor;
54 import com.android.systemui.dagger.qualifiers.Main;
55 import com.android.systemui.media.MediaData;
56 import com.android.systemui.media.MediaDataManager;
57 import com.android.systemui.media.SmartspaceMediaData;
58 import com.android.systemui.plugins.statusbar.StatusBarStateController;
59 import com.android.systemui.statusbar.dagger.StatusBarModule;
60 import com.android.systemui.statusbar.notification.NotificationEntryListener;
61 import com.android.systemui.statusbar.notification.NotificationEntryManager;
62 import com.android.systemui.statusbar.notification.collection.NotifCollection;
63 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
64 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
65 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
66 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
67 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
68 import com.android.systemui.statusbar.phone.BiometricUnlockController;
69 import com.android.systemui.statusbar.phone.KeyguardBypassController;
70 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
71 import com.android.systemui.statusbar.phone.ScrimController;
72 import com.android.systemui.statusbar.phone.ScrimState;
73 import com.android.systemui.statusbar.phone.StatusBar;
74 import com.android.systemui.statusbar.policy.KeyguardStateController;
75 import com.android.systemui.util.DeviceConfigProxy;
76 import com.android.systemui.util.Utils;
77 import com.android.systemui.util.concurrency.DelayableExecutor;
78 
79 import java.io.FileDescriptor;
80 import java.io.PrintWriter;
81 import java.lang.ref.WeakReference;
82 import java.util.ArrayList;
83 import java.util.Collection;
84 import java.util.HashSet;
85 import java.util.List;
86 import java.util.Objects;
87 import java.util.Set;
88 
89 import dagger.Lazy;
90 
91 /**
92  * Handles tasks and state related to media notifications. For example, there is a 'current' media
93  * notification, which this class keeps track of.
94  */
95 public class NotificationMediaManager implements Dumpable {
96     private static final String TAG = "NotificationMediaManager";
97     public static final boolean DEBUG_MEDIA = false;
98 
99     private final StatusBarStateController mStatusBarStateController
100             = Dependency.get(StatusBarStateController.class);
101     private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
102     private final KeyguardStateController mKeyguardStateController = Dependency.get(
103             KeyguardStateController.class);
104     private final KeyguardBypassController mKeyguardBypassController;
105     private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
106     static {
107         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_NONE);
108         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
109         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
110         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
111         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING);
112     }
113 
114     private final NotificationEntryManager mEntryManager;
115     private final MediaDataManager mMediaDataManager;
116     private final NotifPipeline mNotifPipeline;
117     private final NotifCollection mNotifCollection;
118     private final boolean mUsingNotifPipeline;
119 
120     @Nullable
121     private Lazy<NotificationShadeWindowController> mNotificationShadeWindowController;
122 
123     @Nullable
124     private BiometricUnlockController mBiometricUnlockController;
125     @Nullable
126     private ScrimController mScrimController;
127     @Nullable
128     private LockscreenWallpaper mLockscreenWallpaper;
129 
130     private final DelayableExecutor mMainExecutor;
131 
132     private final Context mContext;
133     private final MediaSessionManager mMediaSessionManager;
134     private final ArrayList<MediaListener> mMediaListeners;
135     private final Lazy<StatusBar> mStatusBarLazy;
136     private final MediaArtworkProcessor mMediaArtworkProcessor;
137     private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>();
138 
139     protected NotificationPresenter mPresenter;
140     private MediaController mMediaController;
141     private String mMediaNotificationKey;
142     private MediaMetadata mMediaMetadata;
143 
144     private BackDropView mBackdrop;
145     private ImageView mBackdropFront;
146     private ImageView mBackdropBack;
147 
148     private final MediaController.Callback mMediaListener = new MediaController.Callback() {
149         @Override
150         public void onPlaybackStateChanged(PlaybackState state) {
151             super.onPlaybackStateChanged(state);
152             if (DEBUG_MEDIA) {
153                 Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
154             }
155             if (state != null) {
156                 if (!isPlaybackActive(state.getState())) {
157                     clearCurrentMediaNotification();
158                 }
159                 findAndUpdateMediaNotifications();
160             }
161         }
162 
163         @Override
164         public void onMetadataChanged(MediaMetadata metadata) {
165             super.onMetadataChanged(metadata);
166             if (DEBUG_MEDIA) {
167                 Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
168             }
169             mMediaArtworkProcessor.clearCache();
170             mMediaMetadata = metadata;
171             dispatchUpdateMediaMetaData(true /* changed */, true /* allowAnimation */);
172         }
173     };
174 
175     /**
176      * Injected constructor. See {@link StatusBarModule}.
177      */
NotificationMediaManager( Context context, Lazy<StatusBar> statusBarLazy, Lazy<NotificationShadeWindowController> notificationShadeWindowController, NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, NotifPipeline notifPipeline, NotifCollection notifCollection, FeatureFlags featureFlags, @Main DelayableExecutor mainExecutor, DeviceConfigProxy deviceConfig, MediaDataManager mediaDataManager)178     public NotificationMediaManager(
179             Context context,
180             Lazy<StatusBar> statusBarLazy,
181             Lazy<NotificationShadeWindowController> notificationShadeWindowController,
182             NotificationEntryManager notificationEntryManager,
183             MediaArtworkProcessor mediaArtworkProcessor,
184             KeyguardBypassController keyguardBypassController,
185             NotifPipeline notifPipeline,
186             NotifCollection notifCollection,
187             FeatureFlags featureFlags,
188             @Main DelayableExecutor mainExecutor,
189             DeviceConfigProxy deviceConfig,
190             MediaDataManager mediaDataManager) {
191         mContext = context;
192         mMediaArtworkProcessor = mediaArtworkProcessor;
193         mKeyguardBypassController = keyguardBypassController;
194         mMediaListeners = new ArrayList<>();
195         // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
196         // in session state
197         mMediaSessionManager = (MediaSessionManager) mContext.getSystemService(
198                 Context.MEDIA_SESSION_SERVICE);
199         // TODO: use KeyguardStateController#isOccluded to remove this dependency
200         mStatusBarLazy = statusBarLazy;
201         mNotificationShadeWindowController = notificationShadeWindowController;
202         mEntryManager = notificationEntryManager;
203         mMainExecutor = mainExecutor;
204         mMediaDataManager = mediaDataManager;
205         mNotifPipeline = notifPipeline;
206         mNotifCollection = notifCollection;
207 
208         if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
209             setupNEM();
210             mUsingNotifPipeline = false;
211         } else {
212             setupNotifPipeline();
213             mUsingNotifPipeline = true;
214         }
215     }
216 
setupNotifPipeline()217     private void setupNotifPipeline() {
218         mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
219             @Override
220             public void onEntryAdded(@NonNull NotificationEntry entry) {
221                 mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
222             }
223 
224             @Override
225             public void onEntryUpdated(NotificationEntry entry) {
226                 mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
227             }
228 
229             @Override
230             public void onEntryBind(NotificationEntry entry, StatusBarNotification sbn) {
231                 findAndUpdateMediaNotifications();
232             }
233 
234             @Override
235             public void onEntryRemoved(@NonNull NotificationEntry entry, int reason) {
236                 removeEntry(entry);
237             }
238 
239             @Override
240             public void onEntryCleanUp(@NonNull NotificationEntry entry) {
241                 removeEntry(entry);
242             }
243         });
244 
245         mMediaDataManager.addListener(new MediaDataManager.Listener() {
246             @Override
247             public void onMediaDataLoaded(@NonNull String key,
248                     @Nullable String oldKey, @NonNull MediaData data, boolean immediately,
249                     boolean isSsReactivated) {
250             }
251 
252             @Override
253             public void onSmartspaceMediaDataLoaded(@NonNull String key,
254                     @NonNull SmartspaceMediaData data, boolean shouldPrioritize) {
255             }
256 
257             @Override
258             public void onMediaDataRemoved(@NonNull String key) {
259                 mNotifPipeline.getAllNotifs()
260                         .stream()
261                         .filter(entry -> Objects.equals(entry.getKey(), key))
262                         .findAny()
263                         .ifPresent(entry -> {
264                             // TODO(b/160713608): "removing" this notification won't happen and
265                             //  won't send the 'deleteIntent' if the notification is ongoing.
266                             mNotifCollection.dismissNotification(entry,
267                                     getDismissedByUserStats(entry));
268                         });
269             }
270 
271             @Override
272             public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {}
273         });
274     }
275 
setupNEM()276     private void setupNEM() {
277         mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
278 
279             @Override
280             public void onPendingEntryAdded(NotificationEntry entry) {
281                 mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
282             }
283 
284             @Override
285             public void onPreEntryUpdated(NotificationEntry entry) {
286                 mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
287             }
288 
289             @Override
290             public void onEntryInflated(NotificationEntry entry) {
291                 findAndUpdateMediaNotifications();
292             }
293 
294             @Override
295             public void onEntryReinflated(NotificationEntry entry) {
296                 findAndUpdateMediaNotifications();
297             }
298 
299             @Override
300             public void onEntryRemoved(
301                     @NonNull NotificationEntry entry,
302                     @Nullable NotificationVisibility visibility,
303                     boolean removedByUser,
304                     int reason) {
305                 removeEntry(entry);
306             }
307         });
308 
309         // Pending entries are never inflated, and will never generate a call to onEntryRemoved().
310         // This can happen when notifications are added and canceled before inflation. Add this
311         // separate listener for cleanup, since media inflation occurs onPendingEntryAdded().
312         mEntryManager.addCollectionListener(new NotifCollectionListener() {
313             @Override
314             public void onEntryCleanUp(@NonNull NotificationEntry entry) {
315                 removeEntry(entry);
316             }
317         });
318 
319         mMediaDataManager.addListener(new MediaDataManager.Listener() {
320             @Override
321             public void onMediaDataLoaded(@NonNull String key,
322                     @Nullable String oldKey, @NonNull MediaData data, boolean immediately,
323                     boolean isSsReactivated) {
324             }
325 
326             @Override
327             public void onSmartspaceMediaDataLoaded(@NonNull String key,
328                     @NonNull SmartspaceMediaData data, boolean shouldPrioritize) {
329 
330             }
331 
332             @Override
333             public void onMediaDataRemoved(@NonNull String key) {
334                 NotificationEntry entry = mEntryManager.getPendingOrActiveNotif(key);
335                 if (entry != null) {
336                     // TODO(b/160713608): "removing" this notification won't happen and
337                     //  won't send the 'deleteIntent' if the notification is ongoing.
338                     mEntryManager.performRemoveNotification(entry.getSbn(),
339                             getDismissedByUserStats(entry),
340                             NotificationListenerService.REASON_CANCEL);
341                 }
342             }
343 
344             @Override
345             public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {}
346         });
347     }
348 
getDismissedByUserStats(NotificationEntry entry)349     private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) {
350         final int activeNotificationsCount;
351         if (mUsingNotifPipeline) {
352             activeNotificationsCount = mNotifPipeline.getShadeListCount();
353         } else {
354             activeNotificationsCount = mEntryManager.getActiveNotificationsCount();
355         }
356         return new DismissedByUserStats(
357                 NotificationStats.DISMISSAL_SHADE, // Add DISMISSAL_MEDIA?
358                 NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
359                 NotificationVisibility.obtain(
360                         entry.getKey(),
361                         entry.getRanking().getRank(),
362                         activeNotificationsCount,
363                         /* visible= */ true,
364                         NotificationLogger.getNotificationLocation(entry)));
365     }
366 
removeEntry(NotificationEntry entry)367     private void removeEntry(NotificationEntry entry) {
368         onNotificationRemoved(entry.getKey());
369         mMediaDataManager.onNotificationRemoved(entry.getKey());
370     }
371 
372     /**
373      * Check if a state should be considered actively playing
374      * @param state a PlaybackState
375      * @return true if playing
376      */
isPlayingState(int state)377     public static boolean isPlayingState(int state) {
378         return !PAUSED_MEDIA_STATES.contains(state);
379     }
380 
setUpWithPresenter(NotificationPresenter presenter)381     public void setUpWithPresenter(NotificationPresenter presenter) {
382         mPresenter = presenter;
383     }
384 
onNotificationRemoved(String key)385     public void onNotificationRemoved(String key) {
386         if (key.equals(mMediaNotificationKey)) {
387             clearCurrentMediaNotification();
388             dispatchUpdateMediaMetaData(true /* changed */, true /* allowEnterAnimation */);
389         }
390     }
391 
getMediaNotificationKey()392     public String getMediaNotificationKey() {
393         return mMediaNotificationKey;
394     }
395 
getMediaMetadata()396     public MediaMetadata getMediaMetadata() {
397         return mMediaMetadata;
398     }
399 
getMediaIcon()400     public Icon getMediaIcon() {
401         if (mMediaNotificationKey == null) {
402             return null;
403         }
404         if (mUsingNotifPipeline) {
405             // TODO(b/169655596): Either add O(1) lookup, or cache this icon?
406             return mNotifPipeline.getAllNotifs().stream()
407                 .filter(entry -> Objects.equals(entry.getKey(), mMediaNotificationKey))
408                 .findAny()
409                 .map(entry -> entry.getIcons().getShelfIcon())
410                 .map(StatusBarIconView::getSourceIcon)
411                 .orElse(null);
412         } else {
413             synchronized (mEntryManager) {
414                 NotificationEntry entry = mEntryManager
415                     .getActiveNotificationUnfiltered(mMediaNotificationKey);
416                 if (entry == null || entry.getIcons().getShelfIcon() == null) {
417                     return null;
418                 }
419 
420                 return entry.getIcons().getShelfIcon().getSourceIcon();
421             }
422         }
423     }
424 
addCallback(MediaListener callback)425     public void addCallback(MediaListener callback) {
426         mMediaListeners.add(callback);
427         callback.onPrimaryMetadataOrStateChanged(mMediaMetadata,
428                 getMediaControllerPlaybackState(mMediaController));
429     }
430 
removeCallback(MediaListener callback)431     public void removeCallback(MediaListener callback) {
432         mMediaListeners.remove(callback);
433     }
434 
findAndUpdateMediaNotifications()435     public void findAndUpdateMediaNotifications() {
436         boolean metaDataChanged;
437         if (mUsingNotifPipeline) {
438             // TODO(b/169655907): get the semi-filtered notifications for current user
439             Collection<NotificationEntry> allNotifications = mNotifPipeline.getAllNotifs();
440             metaDataChanged = findPlayingMediaNotification(allNotifications);
441         } else {
442             synchronized (mEntryManager) {
443                 Collection<NotificationEntry> allNotifications = mEntryManager.getAllNotifs();
444                 metaDataChanged = findPlayingMediaNotification(allNotifications);
445             }
446 
447             if (metaDataChanged) {
448                 mEntryManager.updateNotifications("NotificationMediaManager - metaDataChanged");
449             }
450 
451         }
452         dispatchUpdateMediaMetaData(metaDataChanged, true /* allowEnterAnimation */);
453     }
454 
455     /**
456      * Find a notification and media controller associated with the playing media session, and
457      * update this manager's internal state.
458      * @return whether the current MediaMetadata changed (and needs to be announced to listeners).
459      */
findPlayingMediaNotification( @onNull Collection<NotificationEntry> allNotifications)460     private boolean findPlayingMediaNotification(
461             @NonNull Collection<NotificationEntry> allNotifications) {
462         boolean metaDataChanged = false;
463         // Promote the media notification with a controller in 'playing' state, if any.
464         NotificationEntry mediaNotification = null;
465         MediaController controller = null;
466         for (NotificationEntry entry : allNotifications) {
467             if (entry.isMediaNotification()) {
468                 final MediaSession.Token token =
469                         entry.getSbn().getNotification().extras.getParcelable(
470                                 Notification.EXTRA_MEDIA_SESSION);
471                 if (token != null) {
472                     MediaController aController = new MediaController(mContext, token);
473                     if (PlaybackState.STATE_PLAYING
474                             == getMediaControllerPlaybackState(aController)) {
475                         if (DEBUG_MEDIA) {
476                             Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching "
477                                     + entry.getSbn().getKey());
478                         }
479                         mediaNotification = entry;
480                         controller = aController;
481                         break;
482                     }
483                 }
484             }
485         }
486         if (mediaNotification == null) {
487             // Still nothing? OK, let's just look for live media sessions and see if they match
488             // one of our notifications. This will catch apps that aren't (yet!) using media
489             // notifications.
490 
491             if (mMediaSessionManager != null) {
492                 // TODO: Should this really be for all users? It appears that inactive users
493                 //  can't have active sessions, which would mean it is fine.
494                 final List<MediaController> sessions =
495                         mMediaSessionManager.getActiveSessionsForUser(null, UserHandle.ALL);
496 
497                 for (MediaController aController : sessions) {
498                     // now to see if we have one like this
499                     final String pkg = aController.getPackageName();
500 
501                     for (NotificationEntry entry : allNotifications) {
502                         if (entry.getSbn().getPackageName().equals(pkg)) {
503                             if (DEBUG_MEDIA) {
504                                 Log.v(TAG, "DEBUG_MEDIA: found controller matching "
505                                         + entry.getSbn().getKey());
506                             }
507                             controller = aController;
508                             mediaNotification = entry;
509                             break;
510                         }
511                     }
512                 }
513             }
514         }
515 
516         if (controller != null && !sameSessions(mMediaController, controller)) {
517             // We have a new media session
518             clearCurrentMediaNotificationSession();
519             mMediaController = controller;
520             mMediaController.registerCallback(mMediaListener);
521             mMediaMetadata = mMediaController.getMetadata();
522             if (DEBUG_MEDIA) {
523                 Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: "
524                         + mMediaController + ", receive metadata: " + mMediaMetadata);
525             }
526 
527             metaDataChanged = true;
528         }
529 
530         if (mediaNotification != null
531                 && !mediaNotification.getSbn().getKey().equals(mMediaNotificationKey)) {
532             mMediaNotificationKey = mediaNotification.getSbn().getKey();
533             if (DEBUG_MEDIA) {
534                 Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
535                         + mMediaNotificationKey);
536             }
537         }
538 
539         return metaDataChanged;
540     }
541 
clearCurrentMediaNotification()542     public void clearCurrentMediaNotification() {
543         mMediaNotificationKey = null;
544         clearCurrentMediaNotificationSession();
545     }
546 
dispatchUpdateMediaMetaData(boolean changed, boolean allowEnterAnimation)547     private void dispatchUpdateMediaMetaData(boolean changed, boolean allowEnterAnimation) {
548         if (mPresenter != null) {
549             mPresenter.updateMediaMetaData(changed, allowEnterAnimation);
550         }
551         @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController);
552         ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners);
553         for (int i = 0; i < callbacks.size(); i++) {
554             callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
555         }
556     }
557 
558     @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)559     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
560         pw.print("    mMediaSessionManager=");
561         pw.println(mMediaSessionManager);
562         pw.print("    mMediaNotificationKey=");
563         pw.println(mMediaNotificationKey);
564         pw.print("    mMediaController=");
565         pw.print(mMediaController);
566         if (mMediaController != null) {
567             pw.print(" state=" + mMediaController.getPlaybackState());
568         }
569         pw.println();
570         pw.print("    mMediaMetadata=");
571         pw.print(mMediaMetadata);
572         if (mMediaMetadata != null) {
573             pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE));
574         }
575         pw.println();
576     }
577 
isPlaybackActive(int state)578     private boolean isPlaybackActive(int state) {
579         return state != PlaybackState.STATE_STOPPED && state != PlaybackState.STATE_ERROR
580                 && state != PlaybackState.STATE_NONE;
581     }
582 
sameSessions(MediaController a, MediaController b)583     private boolean sameSessions(MediaController a, MediaController b) {
584         if (a == b) {
585             return true;
586         }
587         if (a == null) {
588             return false;
589         }
590         return a.controlsSameSession(b);
591     }
592 
getMediaControllerPlaybackState(MediaController controller)593     private int getMediaControllerPlaybackState(MediaController controller) {
594         if (controller != null) {
595             final PlaybackState playbackState = controller.getPlaybackState();
596             if (playbackState != null) {
597                 return playbackState.getState();
598             }
599         }
600         return PlaybackState.STATE_NONE;
601     }
602 
clearCurrentMediaNotificationSession()603     private void clearCurrentMediaNotificationSession() {
604         mMediaArtworkProcessor.clearCache();
605         mMediaMetadata = null;
606         if (mMediaController != null) {
607             if (DEBUG_MEDIA) {
608                 Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
609                         + mMediaController.getPackageName());
610             }
611             mMediaController.unregisterCallback(mMediaListener);
612         }
613         mMediaController = null;
614     }
615 
616     /**
617      * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
618      */
updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation)619     public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
620         Trace.beginSection("StatusBar#updateMediaMetaData");
621         if (!SHOW_LOCKSCREEN_MEDIA_ARTWORK) {
622             Trace.endSection();
623             return;
624         }
625 
626         if (mBackdrop == null) {
627             Trace.endSection();
628             return; // called too early
629         }
630 
631         boolean wakeAndUnlock = mBiometricUnlockController != null
632             && mBiometricUnlockController.isWakeAndUnlock();
633         if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) {
634             mBackdrop.setVisibility(View.INVISIBLE);
635             Trace.endSection();
636             return;
637         }
638 
639         MediaMetadata mediaMetadata = getMediaMetadata();
640 
641         if (DEBUG_MEDIA) {
642             Log.v(TAG, "DEBUG_MEDIA: updating album art for notification "
643                     + getMediaNotificationKey()
644                     + " metadata=" + mediaMetadata
645                     + " metaDataChanged=" + metaDataChanged
646                     + " state=" + mStatusBarStateController.getState());
647         }
648 
649         Bitmap artworkBitmap = null;
650         if (mediaMetadata != null && !mKeyguardBypassController.getBypassEnabled()) {
651             artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
652             if (artworkBitmap == null) {
653                 artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
654             }
655         }
656 
657         // Process artwork on a background thread and send the resulting bitmap to
658         // finishUpdateMediaMetaData.
659         if (metaDataChanged) {
660             for (AsyncTask<?, ?, ?> task : mProcessArtworkTasks) {
661                 task.cancel(true);
662             }
663             mProcessArtworkTasks.clear();
664         }
665         if (artworkBitmap != null && !Utils.useQsMediaPlayer(mContext)) {
666             mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged,
667                     allowEnterAnimation).execute(artworkBitmap));
668         } else {
669             finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null);
670         }
671 
672         Trace.endSection();
673     }
674 
finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation, @Nullable Bitmap bmp)675     private void finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation,
676             @Nullable Bitmap bmp) {
677         Drawable artworkDrawable = null;
678         if (bmp != null) {
679             artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp);
680         }
681         boolean hasMediaArtwork = artworkDrawable != null;
682         boolean allowWhenShade = false;
683         if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) {
684             Bitmap lockWallpaper =
685                     mLockscreenWallpaper != null ? mLockscreenWallpaper.getBitmap() : null;
686             if (lockWallpaper != null) {
687                 artworkDrawable = new LockscreenWallpaper.WallpaperDrawable(
688                         mBackdropBack.getResources(), lockWallpaper);
689                 // We're in the SHADE mode on the SIM screen - yet we still need to show
690                 // the lockscreen wallpaper in that mode.
691                 allowWhenShade = mStatusBarStateController.getState() == KEYGUARD;
692             }
693         }
694 
695         NotificationShadeWindowController windowController =
696                 mNotificationShadeWindowController.get();
697         boolean hideBecauseOccluded = mStatusBarLazy.get().isOccluded();
698 
699         final boolean hasArtwork = artworkDrawable != null;
700         mColorExtractor.setHasMediaArtwork(hasMediaArtwork);
701         if (mScrimController != null) {
702             mScrimController.setHasBackdrop(hasArtwork);
703         }
704 
705         if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK)
706                 && (mStatusBarStateController.getState() != StatusBarState.SHADE || allowWhenShade)
707                 &&  mBiometricUnlockController != null && mBiometricUnlockController.getMode()
708                         != BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
709                 && !hideBecauseOccluded) {
710             // time to show some art!
711             if (mBackdrop.getVisibility() != View.VISIBLE) {
712                 mBackdrop.setVisibility(View.VISIBLE);
713                 if (allowEnterAnimation) {
714                     mBackdrop.setAlpha(0);
715                     mBackdrop.animate().alpha(1f);
716                 } else {
717                     mBackdrop.animate().cancel();
718                     mBackdrop.setAlpha(1f);
719                 }
720                 if (windowController != null) {
721                     windowController.setBackdropShowing(true);
722                 }
723                 metaDataChanged = true;
724                 if (DEBUG_MEDIA) {
725                     Log.v(TAG, "DEBUG_MEDIA: Fading in album artwork");
726                 }
727             }
728             if (metaDataChanged) {
729                 if (mBackdropBack.getDrawable() != null) {
730                     Drawable drawable =
731                             mBackdropBack.getDrawable().getConstantState()
732                                     .newDrawable(mBackdropFront.getResources()).mutate();
733                     mBackdropFront.setImageDrawable(drawable);
734                     mBackdropFront.setAlpha(1f);
735                     mBackdropFront.setVisibility(View.VISIBLE);
736                 } else {
737                     mBackdropFront.setVisibility(View.INVISIBLE);
738                 }
739 
740                 if (DEBUG_MEDIA_FAKE_ARTWORK) {
741                     final int c = 0xFF000000 | (int)(Math.random() * 0xFFFFFF);
742                     Log.v(TAG, String.format("DEBUG_MEDIA: setting new color: 0x%08x", c));
743                     mBackdropBack.setBackgroundColor(0xFFFFFFFF);
744                     mBackdropBack.setImageDrawable(new ColorDrawable(c));
745                 } else {
746                     mBackdropBack.setImageDrawable(artworkDrawable);
747                 }
748 
749                 if (mBackdropFront.getVisibility() == View.VISIBLE) {
750                     if (DEBUG_MEDIA) {
751                         Log.v(TAG, "DEBUG_MEDIA: Crossfading album artwork from "
752                                 + mBackdropFront.getDrawable()
753                                 + " to "
754                                 + mBackdropBack.getDrawable());
755                     }
756                     mBackdropFront.animate()
757                             .setDuration(250)
758                             .alpha(0f).withEndAction(mHideBackdropFront);
759                 }
760             }
761         } else {
762             // need to hide the album art, either because we are unlocked, on AOD
763             // or because the metadata isn't there to support it
764             if (mBackdrop.getVisibility() != View.GONE) {
765                 if (DEBUG_MEDIA) {
766                     Log.v(TAG, "DEBUG_MEDIA: Fading out album artwork");
767                 }
768                 boolean cannotAnimateDoze = mStatusBarStateController.isDozing()
769                         && !ScrimState.AOD.getAnimateChange();
770                 boolean needsBypassFading = mKeyguardStateController.isBypassFadingAnimation();
771                 if (((mBiometricUnlockController != null && mBiometricUnlockController.getMode()
772                         == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
773                                 || cannotAnimateDoze) && !needsBypassFading)
774                         || hideBecauseOccluded) {
775 
776                     // We are unlocking directly - no animation!
777                     mBackdrop.setVisibility(View.GONE);
778                     mBackdropBack.setImageDrawable(null);
779                     if (windowController != null) {
780                         windowController.setBackdropShowing(false);
781                     }
782                 } else {
783                     if (windowController != null) {
784                         windowController.setBackdropShowing(false);
785                     }
786                     mBackdrop.animate()
787                             .alpha(0)
788                             .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
789                             .setDuration(300)
790                             .setStartDelay(0)
791                             .withEndAction(() -> {
792                                 mBackdrop.setVisibility(View.GONE);
793                                 mBackdropFront.animate().cancel();
794                                 mBackdropBack.setImageDrawable(null);
795                                 mMainExecutor.execute(mHideBackdropFront);
796                             });
797                     if (mKeyguardStateController.isKeyguardFadingAway()) {
798                         mBackdrop.animate()
799                                 .setDuration(
800                                         mKeyguardStateController.getShortenedFadingAwayDuration())
801                                 .setStartDelay(
802                                         mKeyguardStateController.getKeyguardFadingAwayDelay())
803                                 .setInterpolator(Interpolators.LINEAR)
804                                 .start();
805                     }
806                 }
807             }
808         }
809     }
810 
setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack, ScrimController scrimController, LockscreenWallpaper lockscreenWallpaper)811     public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack,
812             ScrimController scrimController, LockscreenWallpaper lockscreenWallpaper) {
813         mBackdrop = backdrop;
814         mBackdropFront = backdropFront;
815         mBackdropBack = backdropBack;
816         mScrimController = scrimController;
817         mLockscreenWallpaper = lockscreenWallpaper;
818     }
819 
setBiometricUnlockController(BiometricUnlockController biometricUnlockController)820     public void setBiometricUnlockController(BiometricUnlockController biometricUnlockController) {
821         mBiometricUnlockController = biometricUnlockController;
822     }
823 
824     /**
825      * Hide the album artwork that is fading out and release its bitmap.
826      */
827     protected final Runnable mHideBackdropFront = new Runnable() {
828         @Override
829         public void run() {
830             if (DEBUG_MEDIA) {
831                 Log.v(TAG, "DEBUG_MEDIA: removing fade layer");
832             }
833             mBackdropFront.setVisibility(View.INVISIBLE);
834             mBackdropFront.animate().cancel();
835             mBackdropFront.setImageDrawable(null);
836         }
837     };
838 
processArtwork(Bitmap artwork)839     private Bitmap processArtwork(Bitmap artwork) {
840         return mMediaArtworkProcessor.processArtwork(mContext, artwork);
841     }
842 
843     @MainThread
removeTask(AsyncTask<?, ?, ?> task)844     private void removeTask(AsyncTask<?, ?, ?> task) {
845         mProcessArtworkTasks.remove(task);
846     }
847 
848     /**
849      * {@link AsyncTask} to prepare album art for use as backdrop on lock screen.
850      */
851     private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> {
852 
853         private final WeakReference<NotificationMediaManager> mManagerRef;
854         private final boolean mMetaDataChanged;
855         private final boolean mAllowEnterAnimation;
856 
ProcessArtworkTask(NotificationMediaManager manager, boolean changed, boolean allowAnimation)857         ProcessArtworkTask(NotificationMediaManager manager, boolean changed,
858                 boolean allowAnimation) {
859             mManagerRef = new WeakReference<>(manager);
860             mMetaDataChanged = changed;
861             mAllowEnterAnimation = allowAnimation;
862         }
863 
864         @Override
doInBackground(Bitmap... bitmaps)865         protected Bitmap doInBackground(Bitmap... bitmaps) {
866             NotificationMediaManager manager = mManagerRef.get();
867             if (manager == null || bitmaps.length == 0 || isCancelled()) {
868                 return null;
869             }
870             return manager.processArtwork(bitmaps[0]);
871         }
872 
873         @Override
onPostExecute(@ullable Bitmap result)874         protected void onPostExecute(@Nullable Bitmap result) {
875             NotificationMediaManager manager = mManagerRef.get();
876             if (manager != null && !isCancelled()) {
877                 manager.removeTask(this);
878                 manager.finishUpdateMediaMetaData(mMetaDataChanged, mAllowEnterAnimation, result);
879             }
880         }
881 
882         @Override
onCancelled(Bitmap result)883         protected void onCancelled(Bitmap result) {
884             if (result != null) {
885                 result.recycle();
886             }
887             NotificationMediaManager manager = mManagerRef.get();
888             if (manager != null) {
889                 manager.removeTask(this);
890             }
891         }
892     }
893 
894     public interface MediaListener {
895         /**
896          * Called whenever there's new metadata or playback state.
897          * @param metadata Current metadata.
898          * @param state Current playback state
899          * @see PlaybackState.State
900          */
onPrimaryMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state)901         default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata,
902                 @PlaybackState.State int state) {}
903     }
904 }
905