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