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