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