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