2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
16 package com.android.systemui.statusbar;
18 import static com.android.systemui.Dependency.MAIN_HANDLER;
19 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
20 import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_MEDIA_FAKE_ARTWORK;
21 import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_WALLPAPER;
22 import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK;
24 import android.annotation.MainThread;
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.Handler;
40 import android.os.Trace;
41 import android.os.UserHandle;
42 import android.provider.DeviceConfig;
43 import android.provider.DeviceConfig.Properties;
44 import android.util.ArraySet;
45 import android.util.Log;
46 import android.view.View;
47 import android.widget.ImageView;
49 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
50 import com.android.internal.statusbar.NotificationVisibility;
51 import com.android.systemui.Dependency;
52 import com.android.systemui.Dumpable;
53 import com.android.systemui.Interpolators;
54 import com.android.systemui.colorextraction.SysuiColorExtractor;
55 import com.android.systemui.plugins.statusbar.StatusBarStateController;
56 import com.android.systemui.statusbar.notification.NotificationEntryListener;
57 import com.android.systemui.statusbar.notification.NotificationEntryManager;
58 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
59 import com.android.systemui.statusbar.phone.BiometricUnlockController;
60 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
61 import com.android.systemui.statusbar.phone.ScrimController;
62 import com.android.systemui.statusbar.phone.ScrimState;
63 import com.android.systemui.statusbar.phone.ShadeController;
64 import com.android.systemui.statusbar.phone.StatusBarWindowController;
65 import com.android.systemui.statusbar.policy.KeyguardMonitor;
67 import java.io.FileDescriptor;
68 import java.io.PrintWriter;
69 import java.lang.ref.WeakReference;
70 import java.util.ArrayList;
71 import java.util.List;
74 import javax.inject.Inject;
75 import javax.inject.Singleton;
80 * Handles tasks and state related to media notifications. For example, there is a 'current' media
81 * notification, which this class keeps track of.
84 public class NotificationMediaManager implements Dumpable {
85 private static final String TAG = "NotificationMediaManager";
86 public static final boolean DEBUG_MEDIA = false;
88 private final StatusBarStateController mStatusBarStateController
89 = Dependency.get(StatusBarStateController.class);
90 private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
91 private final KeyguardMonitor mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
94 private NotificationEntryManager mEntryManager;
96 // Late binding, also @Nullable due to being in com.android.systemui.statusbar.phone package
98 private Lazy<ShadeController> mShadeController;
100 private Lazy<StatusBarWindowController> mStatusBarWindowController;
103 private BiometricUnlockController mBiometricUnlockController;
105 private ScrimController mScrimController;
107 private LockscreenWallpaper mLockscreenWallpaper;
109 private final Handler mHandler = Dependency.get(MAIN_HANDLER);
111 private final Context mContext;
112 private final MediaSessionManager mMediaSessionManager;
113 private final ArrayList<MediaListener> mMediaListeners;
114 private final MediaArtworkProcessor mMediaArtworkProcessor;
115 private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>();
117 protected NotificationPresenter mPresenter;
118 private MediaController mMediaController;
119 private String mMediaNotificationKey;
120 private MediaMetadata mMediaMetadata;
122 private BackDropView mBackdrop;
123 private ImageView mBackdropFront;
124 private ImageView mBackdropBack;
126 private boolean mShowCompactMediaSeekbar;
127 private final DeviceConfig.OnPropertiesChangedListener mPropertiesChangedListener =
128 new DeviceConfig.OnPropertiesChangedListener() {
130 public void onPropertiesChanged(Properties properties) {
131 for (String name : properties.getKeyset()) {
132 if (SystemUiDeviceConfigFlags.COMPACT_MEDIA_SEEKBAR_ENABLED.equals(name)) {
133 String value = properties.getString(name, null);
135 Log.v(TAG, "DEBUG_MEDIA: compact media seekbar flag updated: " + value);
137 mShowCompactMediaSeekbar = "true".equals(value);
143 private final MediaController.Callback mMediaListener = new MediaController.Callback() {
145 public void onPlaybackStateChanged(PlaybackState state) {
146 super.onPlaybackStateChanged(state);
148 Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
151 if (!isPlaybackActive(state.getState())) {
152 clearCurrentMediaNotification();
154 dispatchUpdateMediaMetaData(true /* changed */, true /* allowAnimation */);
159 public void onMetadataChanged(MediaMetadata metadata) {
160 super.onMetadataChanged(metadata);
162 Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
164 mMediaArtworkProcessor.clearCache();
165 mMediaMetadata = metadata;
166 dispatchUpdateMediaMetaData(true /* changed */, true /* allowAnimation */);
171 public NotificationMediaManager(
173 Lazy<ShadeController> shadeController,
174 Lazy<StatusBarWindowController> statusBarWindowController,
175 NotificationEntryManager notificationEntryManager,
176 MediaArtworkProcessor mediaArtworkProcessor) {
178 mMediaArtworkProcessor = mediaArtworkProcessor;
179 mMediaListeners = new ArrayList<>();
181 = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
182 // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
184 mShadeController = shadeController;
185 mStatusBarWindowController = statusBarWindowController;
186 mEntryManager = notificationEntryManager;
187 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
189 public void onEntryRemoved(
190 NotificationEntry entry,
191 NotificationVisibility visibility,
192 boolean removedByUser) {
193 onNotificationRemoved(entry.key);
197 mShowCompactMediaSeekbar = "true".equals(
198 DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
199 SystemUiDeviceConfigFlags.COMPACT_MEDIA_SEEKBAR_ENABLED));
201 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
202 mContext.getMainExecutor(),
203 mPropertiesChangedListener);
206 public void setUpWithPresenter(NotificationPresenter presenter) {
207 mPresenter = presenter;
210 public void onNotificationRemoved(String key) {
211 if (key.equals(mMediaNotificationKey)) {
212 clearCurrentMediaNotification();
213 dispatchUpdateMediaMetaData(true /* changed */, true /* allowEnterAnimation */);
217 public String getMediaNotificationKey() {
218 return mMediaNotificationKey;
221 public MediaMetadata getMediaMetadata() {
222 return mMediaMetadata;
225 public boolean getShowCompactMediaSeekbar() {
226 return mShowCompactMediaSeekbar;
229 public Icon getMediaIcon() {
230 if (mMediaNotificationKey == null) {
233 synchronized (mEntryManager.getNotificationData()) {
234 NotificationEntry entry = mEntryManager.getNotificationData().get(mMediaNotificationKey);
235 if (entry == null || entry.expandedIcon == null) {
239 return entry.expandedIcon.getSourceIcon();
243 public void addCallback(MediaListener callback) {
244 mMediaListeners.add(callback);
245 callback.onMetadataOrStateChanged(mMediaMetadata,
246 getMediaControllerPlaybackState(mMediaController));
249 public void removeCallback(MediaListener callback) {
250 mMediaListeners.remove(callback);
253 public void findAndUpdateMediaNotifications() {
254 boolean metaDataChanged = false;
256 synchronized (mEntryManager.getNotificationData()) {
257 ArrayList<NotificationEntry> activeNotifications =
258 mEntryManager.getNotificationData().getActiveNotifications();
259 final int N = activeNotifications.size();
261 // Promote the media notification with a controller in 'playing' state, if any.
262 NotificationEntry mediaNotification = null;
263 MediaController controller = null;
264 for (int i = 0; i < N; i++) {
265 final NotificationEntry entry = activeNotifications.get(i);
267 if (entry.isMediaNotification()) {
268 final MediaSession.Token token =
269 entry.notification.getNotification().extras.getParcelable(
270 Notification.EXTRA_MEDIA_SESSION);
272 MediaController aController = new MediaController(mContext, token);
273 if (PlaybackState.STATE_PLAYING ==
274 getMediaControllerPlaybackState(aController)) {
276 Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching "
277 + entry.notification.getKey());
279 mediaNotification = entry;
280 controller = aController;
286 if (mediaNotification == null) {
287 // Still nothing? OK, let's just look for live media sessions and see if they match
288 // one of our notifications. This will catch apps that aren't (yet!) using media
291 if (mMediaSessionManager != null) {
292 // TODO: Should this really be for all users?
293 final List<MediaController> sessions
294 = mMediaSessionManager.getActiveSessionsForUser(
296 UserHandle.USER_ALL);
298 for (MediaController aController : sessions) {
299 if (PlaybackState.STATE_PLAYING ==
300 getMediaControllerPlaybackState(aController)) {
301 // now to see if we have one like this
302 final String pkg = aController.getPackageName();
304 for (int i = 0; i < N; i++) {
305 final NotificationEntry entry = activeNotifications.get(i);
306 if (entry.notification.getPackageName().equals(pkg)) {
308 Log.v(TAG, "DEBUG_MEDIA: found controller matching "
309 + entry.notification.getKey());
311 controller = aController;
312 mediaNotification = entry;
321 if (controller != null && !sameSessions(mMediaController, controller)) {
322 // We have a new media session
323 clearCurrentMediaNotificationSession();
324 mMediaController = controller;
325 mMediaController.registerCallback(mMediaListener);
326 mMediaMetadata = mMediaController.getMetadata();
328 Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: "
329 + mMediaController + ", receive metadata: " + mMediaMetadata);
332 metaDataChanged = true;
335 if (mediaNotification != null
336 && !mediaNotification.notification.getKey().equals(mMediaNotificationKey)) {
337 mMediaNotificationKey = mediaNotification.notification.getKey();
339 Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
340 + mMediaNotificationKey);
345 if (metaDataChanged) {
346 mEntryManager.updateNotifications();
349 dispatchUpdateMediaMetaData(metaDataChanged, true /* allowEnterAnimation */);
352 public void clearCurrentMediaNotification() {
353 mMediaNotificationKey = null;
354 clearCurrentMediaNotificationSession();
357 private void dispatchUpdateMediaMetaData(boolean changed, boolean allowEnterAnimation) {
358 if (mPresenter != null) {
359 mPresenter.updateMediaMetaData(changed, allowEnterAnimation);
361 @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController);
362 ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners);
363 for (int i = 0; i < callbacks.size(); i++) {
364 callbacks.get(i).onMetadataOrStateChanged(mMediaMetadata, state);
369 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
370 pw.print(" mMediaSessionManager=");
371 pw.println(mMediaSessionManager);
372 pw.print(" mMediaNotificationKey=");
373 pw.println(mMediaNotificationKey);
374 pw.print(" mMediaController=");
375 pw.print(mMediaController);
376 if (mMediaController != null) {
377 pw.print(" state=" + mMediaController.getPlaybackState());
380 pw.print(" mMediaMetadata=");
381 pw.print(mMediaMetadata);
382 if (mMediaMetadata != null) {
383 pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE));
388 private boolean isPlaybackActive(int state) {
389 return state != PlaybackState.STATE_STOPPED && state != PlaybackState.STATE_ERROR
390 && state != PlaybackState.STATE_NONE;
393 private boolean sameSessions(MediaController a, MediaController b) {
400 return a.controlsSameSession(b);
403 private int getMediaControllerPlaybackState(MediaController controller) {
404 if (controller != null) {
405 final PlaybackState playbackState = controller.getPlaybackState();
406 if (playbackState != null) {
407 return playbackState.getState();
410 return PlaybackState.STATE_NONE;
413 private void clearCurrentMediaNotificationSession() {
414 mMediaArtworkProcessor.clearCache();
415 mMediaMetadata = null;
416 if (mMediaController != null) {
418 Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
419 + mMediaController.getPackageName());
421 mMediaController.unregisterCallback(mMediaListener);
423 mMediaController = null;
427 * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
429 public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
430 Trace.beginSection("StatusBar#updateMediaMetaData");
431 if (!SHOW_LOCKSCREEN_MEDIA_ARTWORK) {
436 if (mBackdrop == null) {
438 return; // called too early
441 boolean wakeAndUnlock = mBiometricUnlockController != null
442 && mBiometricUnlockController.isWakeAndUnlock();
443 if (mKeyguardMonitor.isLaunchTransitionFadingAway() || wakeAndUnlock) {
444 mBackdrop.setVisibility(View.INVISIBLE);
449 MediaMetadata mediaMetadata = getMediaMetadata();
452 Log.v(TAG, "DEBUG_MEDIA: updating album art for notification "
453 + getMediaNotificationKey()
454 + " metadata=" + mediaMetadata
455 + " metaDataChanged=" + metaDataChanged
456 + " state=" + mStatusBarStateController.getState());
459 Bitmap artworkBitmap = null;
460 if (mediaMetadata != null) {
461 artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
462 if (artworkBitmap == null) {
463 artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
467 // Process artwork on a background thread and send the resulting bitmap to
468 // finishUpdateMediaMetaData.
469 if (metaDataChanged) {
470 for (AsyncTask<?, ?, ?> task : mProcessArtworkTasks) {
473 mProcessArtworkTasks.clear();
475 if (artworkBitmap != null) {
476 mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged,
477 allowEnterAnimation).execute(artworkBitmap));
479 finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null);
485 private void finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation,
486 @Nullable Bitmap bmp) {
487 Drawable artworkDrawable = null;
489 artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp);
491 boolean hasMediaArtwork = artworkDrawable != null;
492 boolean allowWhenShade = false;
493 if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) {
494 Bitmap lockWallpaper =
495 mLockscreenWallpaper != null ? mLockscreenWallpaper.getBitmap() : null;
496 if (lockWallpaper != null) {
497 artworkDrawable = new LockscreenWallpaper.WallpaperDrawable(
498 mBackdropBack.getResources(), lockWallpaper);
499 // We're in the SHADE mode on the SIM screen - yet we still need to show
500 // the lockscreen wallpaper in that mode.
501 allowWhenShade = mStatusBarStateController.getState() == KEYGUARD;
505 ShadeController shadeController = mShadeController.get();
506 StatusBarWindowController windowController = mStatusBarWindowController.get();
507 boolean hideBecauseOccluded = shadeController != null && shadeController.isOccluded();
509 final boolean hasArtwork = artworkDrawable != null;
510 mColorExtractor.setHasMediaArtwork(hasMediaArtwork);
511 if (mScrimController != null) {
512 mScrimController.setHasBackdrop(hasArtwork);
515 if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK)
516 && (mStatusBarStateController.getState() != StatusBarState.SHADE || allowWhenShade)
517 && mBiometricUnlockController != null && mBiometricUnlockController.getMode()
518 != BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
519 && !hideBecauseOccluded) {
520 // time to show some art!
521 if (mBackdrop.getVisibility() != View.VISIBLE) {
522 mBackdrop.setVisibility(View.VISIBLE);
523 if (allowEnterAnimation) {
524 mBackdrop.setAlpha(0);
525 mBackdrop.animate().alpha(1f);
527 mBackdrop.animate().cancel();
528 mBackdrop.setAlpha(1f);
530 if (windowController != null) {
531 windowController.setBackdropShowing(true);
533 metaDataChanged = true;
535 Log.v(TAG, "DEBUG_MEDIA: Fading in album artwork");
538 if (metaDataChanged) {
539 if (mBackdropBack.getDrawable() != null) {
541 mBackdropBack.getDrawable().getConstantState()
542 .newDrawable(mBackdropFront.getResources()).mutate();
543 mBackdropFront.setImageDrawable(drawable);
544 mBackdropFront.setAlpha(1f);
545 mBackdropFront.setVisibility(View.VISIBLE);
547 mBackdropFront.setVisibility(View.INVISIBLE);
550 if (DEBUG_MEDIA_FAKE_ARTWORK) {
551 final int c = 0xFF000000 | (int)(Math.random() * 0xFFFFFF);
552 Log.v(TAG, String.format("DEBUG_MEDIA: setting new color: 0x%08x", c));
553 mBackdropBack.setBackgroundColor(0xFFFFFFFF);
554 mBackdropBack.setImageDrawable(new ColorDrawable(c));
556 mBackdropBack.setImageDrawable(artworkDrawable);
559 if (mBackdropFront.getVisibility() == View.VISIBLE) {
561 Log.v(TAG, "DEBUG_MEDIA: Crossfading album artwork from "
562 + mBackdropFront.getDrawable()
564 + mBackdropBack.getDrawable());
566 mBackdropFront.animate()
568 .alpha(0f).withEndAction(mHideBackdropFront);
572 // need to hide the album art, either because we are unlocked, on AOD
573 // or because the metadata isn't there to support it
574 if (mBackdrop.getVisibility() != View.GONE) {
576 Log.v(TAG, "DEBUG_MEDIA: Fading out album artwork");
578 boolean cannotAnimateDoze = shadeController != null
579 && shadeController.isDozing()
580 && !ScrimState.AOD.getAnimateChange();
581 if (mBiometricUnlockController != null && mBiometricUnlockController.getMode()
582 == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
583 || hideBecauseOccluded || cannotAnimateDoze) {
585 // We are unlocking directly - no animation!
586 mBackdrop.setVisibility(View.GONE);
587 mBackdropBack.setImageDrawable(null);
588 if (windowController != null) {
589 windowController.setBackdropShowing(false);
592 if (windowController != null) {
593 windowController.setBackdropShowing(false);
597 .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
600 .withEndAction(() -> {
601 mBackdrop.setVisibility(View.GONE);
602 mBackdropFront.animate().cancel();
603 mBackdropBack.setImageDrawable(null);
604 mHandler.post(mHideBackdropFront);
606 if (mKeyguardMonitor.isKeyguardFadingAway()) {
608 // Make it disappear faster, as the focus should be on the activity
610 .setDuration(mKeyguardMonitor.getKeyguardFadingAwayDuration() / 2)
611 .setStartDelay(mKeyguardMonitor.getKeyguardFadingAwayDelay())
612 .setInterpolator(Interpolators.LINEAR)
620 public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack,
621 ScrimController scrimController, LockscreenWallpaper lockscreenWallpaper) {
622 mBackdrop = backdrop;
623 mBackdropFront = backdropFront;
624 mBackdropBack = backdropBack;
625 mScrimController = scrimController;
626 mLockscreenWallpaper = lockscreenWallpaper;
629 public void setBiometricUnlockController(BiometricUnlockController biometricUnlockController) {
630 mBiometricUnlockController = biometricUnlockController;
634 * Hide the album artwork that is fading out and release its bitmap.
636 protected final Runnable mHideBackdropFront = new Runnable() {
640 Log.v(TAG, "DEBUG_MEDIA: removing fade layer");
642 mBackdropFront.setVisibility(View.INVISIBLE);
643 mBackdropFront.animate().cancel();
644 mBackdropFront.setImageDrawable(null);
648 private Bitmap processArtwork(Bitmap artwork) {
649 return mMediaArtworkProcessor.processArtwork(mContext, artwork);
653 private void removeTask(AsyncTask<?, ?, ?> task) {
654 mProcessArtworkTasks.remove(task);
658 * {@link AsyncTask} to prepare album art for use as backdrop on lock screen.
660 private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> {
662 private final WeakReference<NotificationMediaManager> mManagerRef;
663 private final boolean mMetaDataChanged;
664 private final boolean mAllowEnterAnimation;
666 ProcessArtworkTask(NotificationMediaManager manager, boolean changed,
667 boolean allowAnimation) {
668 mManagerRef = new WeakReference<>(manager);
669 mMetaDataChanged = changed;
670 mAllowEnterAnimation = allowAnimation;
674 protected Bitmap doInBackground(Bitmap... bitmaps) {
675 NotificationMediaManager manager = mManagerRef.get();
676 if (manager == null || bitmaps.length == 0 || isCancelled()) {
679 return manager.processArtwork(bitmaps[0]);
683 protected void onPostExecute(@Nullable Bitmap result) {
684 NotificationMediaManager manager = mManagerRef.get();
685 if (manager != null && !isCancelled()) {
686 manager.removeTask(this);
687 manager.finishUpdateMediaMetaData(mMetaDataChanged, mAllowEnterAnimation, result);
692 protected void onCancelled(Bitmap result) {
693 if (result != null) {
696 NotificationMediaManager manager = mManagerRef.get();
697 if (manager != null) {
698 manager.removeTask(this);
703 public interface MediaListener {
705 * Called whenever there's new metadata or playback state.
706 * @param metadata Current metadata.
707 * @param state Current playback state
708 * @see PlaybackState.State
710 void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state);