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.media; 17 18 import static com.android.systemui.Flags.mediaControlsUserInitiatedDeleteintent; 19 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.Notification; 23 import android.content.Context; 24 import android.graphics.drawable.Icon; 25 import android.media.MediaMetadata; 26 import android.media.session.MediaController; 27 import android.media.session.MediaSession; 28 import android.media.session.PlaybackState; 29 import android.os.Handler; 30 import android.service.notification.NotificationStats; 31 import android.service.notification.StatusBarNotification; 32 import android.util.Log; 33 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.systemui.Dumpable; 37 import com.android.systemui.dagger.qualifiers.Background; 38 import com.android.systemui.dagger.qualifiers.Main; 39 import com.android.systemui.dump.DumpManager; 40 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; 41 import com.android.systemui.media.controls.shared.model.MediaData; 42 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData; 43 import com.android.systemui.statusbar.NotificationPresenter; 44 import com.android.systemui.statusbar.StatusBarIconView; 45 import com.android.systemui.statusbar.dagger.CentralSurfacesModule; 46 import com.android.systemui.statusbar.notification.collection.NotifCollection; 47 import com.android.systemui.statusbar.notification.collection.NotifPipeline; 48 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 49 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 50 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 51 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; 52 53 import java.io.PrintWriter; 54 import java.util.ArrayList; 55 import java.util.Collection; 56 import java.util.HashSet; 57 import java.util.List; 58 import java.util.Objects; 59 import java.util.Optional; 60 import java.util.concurrent.Executor; 61 62 /** 63 * Handles tasks and state related to media notifications. For example, there is a 'current' media 64 * notification, which this class keeps track of. 65 */ 66 public class NotificationMediaManager implements Dumpable { 67 private static final String TAG = "NotificationMediaManager"; 68 public static final boolean DEBUG_MEDIA = false; 69 70 private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>(); 71 private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>(); 72 static { 73 PAUSED_MEDIA_STATES.add(PlaybackState.STATE_NONE); 74 PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED); 75 PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED); 76 PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR); 77 CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING); 78 CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_BUFFERING); 79 } 80 81 private final NotificationVisibilityProvider mVisibilityProvider; 82 private final MediaDataManager mMediaDataManager; 83 private final NotifPipeline mNotifPipeline; 84 private final NotifCollection mNotifCollection; 85 86 private final Context mContext; 87 private final ArrayList<MediaListener> mMediaListeners; 88 89 private final Executor mBackgroundExecutor; 90 private final Handler mHandler; 91 92 protected NotificationPresenter mPresenter; 93 @VisibleForTesting 94 MediaController mMediaController; 95 private String mMediaNotificationKey; 96 private MediaMetadata mMediaMetadata; 97 98 @VisibleForTesting 99 final MediaController.Callback mMediaListener = new MediaController.Callback() { 100 @Override 101 public void onPlaybackStateChanged(PlaybackState state) { 102 super.onPlaybackStateChanged(state); 103 if (DEBUG_MEDIA) { 104 Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state); 105 } 106 if (state != null) { 107 if (!isPlaybackActive(state.getState())) { 108 clearCurrentMediaNotification(); 109 } 110 findAndUpdateMediaNotifications(); 111 } 112 } 113 114 @Override 115 public void onMetadataChanged(MediaMetadata metadata) { 116 super.onMetadataChanged(metadata); 117 if (DEBUG_MEDIA) { 118 Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata); 119 } 120 mBackgroundExecutor.execute(() -> setMediaMetadata(metadata)); 121 dispatchUpdateMediaMetaData(); 122 } 123 }; 124 setMediaMetadata(MediaMetadata metadata)125 private void setMediaMetadata(MediaMetadata metadata) { 126 mMediaMetadata = metadata; 127 } 128 129 /** 130 * Injected constructor. See {@link CentralSurfacesModule}. 131 */ NotificationMediaManager( Context context, NotificationVisibilityProvider visibilityProvider, NotifPipeline notifPipeline, NotifCollection notifCollection, MediaDataManager mediaDataManager, DumpManager dumpManager, @Background Executor backgroundExecutor, @Main Handler handler )132 public NotificationMediaManager( 133 Context context, 134 NotificationVisibilityProvider visibilityProvider, 135 NotifPipeline notifPipeline, 136 NotifCollection notifCollection, 137 MediaDataManager mediaDataManager, 138 DumpManager dumpManager, 139 @Background Executor backgroundExecutor, 140 @Main Handler handler 141 ) { 142 mContext = context; 143 mMediaListeners = new ArrayList<>(); 144 mVisibilityProvider = visibilityProvider; 145 mMediaDataManager = mediaDataManager; 146 mNotifPipeline = notifPipeline; 147 mNotifCollection = notifCollection; 148 mBackgroundExecutor = backgroundExecutor; 149 mHandler = handler; 150 151 setupNotifPipeline(); 152 153 dumpManager.registerDumpable(this); 154 } 155 setupNotifPipeline()156 private void setupNotifPipeline() { 157 mNotifPipeline.addCollectionListener(new NotifCollectionListener() { 158 @Override 159 public void onEntryAdded(@NonNull NotificationEntry entry) { 160 mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); 161 } 162 163 @Override 164 public void onEntryUpdated(NotificationEntry entry) { 165 mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); 166 } 167 168 @Override 169 public void onEntryBind(NotificationEntry entry, StatusBarNotification sbn) { 170 findAndUpdateMediaNotifications(); 171 } 172 173 @Override 174 public void onEntryRemoved(@NonNull NotificationEntry entry, int reason) { 175 removeEntry(entry); 176 } 177 178 @Override 179 public void onEntryCleanUp(@NonNull NotificationEntry entry) { 180 removeEntry(entry); 181 } 182 }); 183 184 mMediaDataManager.addListener(new MediaDataManager.Listener() { 185 @Override 186 public void onMediaDataLoaded(@NonNull String key, 187 @Nullable String oldKey, @NonNull MediaData data, boolean immediately, 188 int receivedSmartspaceCardLatency, boolean isSsReactivated) { 189 } 190 191 @Override 192 public void onSmartspaceMediaDataLoaded(@NonNull String key, 193 @NonNull SmartspaceMediaData data, boolean shouldPrioritize) { 194 } 195 196 @Override 197 public void onMediaDataRemoved(@NonNull String key, boolean userInitiated) { 198 if (mediaControlsUserInitiatedDeleteintent() && !userInitiated) { 199 // Dismissing the notification will send the app's deleteIntent, so ignore if 200 // this was an automatic removal 201 Log.d(TAG, "Not dismissing " + key + " because it was removed by the system"); 202 return; 203 } 204 mNotifPipeline.getAllNotifs() 205 .stream() 206 .filter(entry -> Objects.equals(entry.getKey(), key)) 207 .findAny() 208 .ifPresent(entry -> { 209 mNotifCollection.dismissNotification(entry, 210 getDismissedByUserStats(entry)); 211 }); 212 } 213 214 @Override 215 public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {} 216 }); 217 } 218 getDismissedByUserStats(NotificationEntry entry)219 private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) { 220 return new DismissedByUserStats( 221 NotificationStats.DISMISSAL_SHADE, // Add DISMISSAL_MEDIA? 222 NotificationStats.DISMISS_SENTIMENT_NEUTRAL, 223 mVisibilityProvider.obtain(entry, /* visible= */ true)); 224 } 225 removeEntry(NotificationEntry entry)226 private void removeEntry(NotificationEntry entry) { 227 onNotificationRemoved(entry.getKey()); 228 mMediaDataManager.onNotificationRemoved(entry.getKey()); 229 } 230 231 /** 232 * Check if a state should be considered actively playing 233 * @param state a PlaybackState 234 * @return true if playing 235 */ isPlayingState(int state)236 public static boolean isPlayingState(int state) { 237 return !PAUSED_MEDIA_STATES.contains(state) 238 && !CONNECTING_MEDIA_STATES.contains(state); 239 } 240 241 /** 242 * Check if a state should be considered as connecting 243 * @param state a PlaybackState 244 * @return true if connecting or buffering 245 */ isConnectingState(int state)246 public static boolean isConnectingState(int state) { 247 return CONNECTING_MEDIA_STATES.contains(state); 248 } 249 setUpWithPresenter(NotificationPresenter presenter)250 public void setUpWithPresenter(NotificationPresenter presenter) { 251 mPresenter = presenter; 252 } 253 onNotificationRemoved(String key)254 public void onNotificationRemoved(String key) { 255 if (key.equals(mMediaNotificationKey)) { 256 clearCurrentMediaNotification(); 257 dispatchUpdateMediaMetaData(); 258 } 259 } 260 261 @Nullable getMediaNotificationKey()262 public String getMediaNotificationKey() { 263 return mMediaNotificationKey; 264 } 265 getMediaMetadata()266 public MediaMetadata getMediaMetadata() { 267 return mMediaMetadata; 268 } 269 getMediaIcon()270 public Icon getMediaIcon() { 271 if (mMediaNotificationKey == null) { 272 return null; 273 } 274 return Optional.ofNullable(mNotifPipeline.getEntry(mMediaNotificationKey)) 275 .map(entry -> entry.getIcons().getShelfIcon()) 276 .map(StatusBarIconView::getSourceIcon) 277 .orElse(null); 278 } 279 addCallback(MediaListener callback)280 public void addCallback(MediaListener callback) { 281 mMediaListeners.add(callback); 282 mBackgroundExecutor.execute(() -> updateMediaMetaData(callback)); 283 } 284 updateMediaMetaData(MediaListener callback)285 private void updateMediaMetaData(MediaListener callback) { 286 int playbackState = getMediaControllerPlaybackState(mMediaController); 287 mHandler.post( 288 () -> callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, playbackState)); 289 } 290 removeCallback(MediaListener callback)291 public void removeCallback(MediaListener callback) { 292 mMediaListeners.remove(callback); 293 } 294 findAndUpdateMediaNotifications()295 public void findAndUpdateMediaNotifications() { 296 // TODO(b/169655907): get the semi-filtered notifications for current user 297 Collection<NotificationEntry> allNotifications = mNotifPipeline.getAllNotifs(); 298 // Create new sbn list to be accessed in background thread. 299 List<StatusBarNotification> statusBarNotifications = new ArrayList<>(); 300 for (NotificationEntry entry : allNotifications) { 301 statusBarNotifications.add(entry.getSbn()); 302 } 303 mBackgroundExecutor.execute(() -> findPlayingMediaNotification(statusBarNotifications)); 304 dispatchUpdateMediaMetaData(); 305 } 306 307 /** 308 * Find a notification and media controller associated with the playing media session, and 309 * update this manager's internal state. 310 * This method must be called in background. 311 * TODO(b/273443374) check this method 312 */ findPlayingMediaNotification(@onNull List<StatusBarNotification> allNotifications)313 void findPlayingMediaNotification(@NonNull List<StatusBarNotification> allNotifications) { 314 // Promote the media notification with a controller in 'playing' state, if any. 315 StatusBarNotification statusBarNotification = null; 316 MediaController controller = null; 317 for (StatusBarNotification sbn : allNotifications) { 318 Notification notif = sbn.getNotification(); 319 if (notif.isMediaNotification()) { 320 final MediaSession.Token token = 321 sbn.getNotification().extras.getParcelable( 322 Notification.EXTRA_MEDIA_SESSION, MediaSession.Token.class); 323 if (token != null) { 324 MediaController aController = new MediaController(mContext, token); 325 if (PlaybackState.STATE_PLAYING 326 == getMediaControllerPlaybackState(aController)) { 327 if (DEBUG_MEDIA) { 328 Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching " 329 + sbn.getKey()); 330 } 331 statusBarNotification = sbn; 332 controller = aController; 333 break; 334 } 335 } 336 } 337 } 338 339 setUpControllerAndKey(controller, statusBarNotification); 340 } 341 setUpControllerAndKey( MediaController controller, StatusBarNotification mediaNotification)342 private void setUpControllerAndKey( 343 MediaController controller, 344 StatusBarNotification mediaNotification) { 345 if (controller != null && !sameSessions(mMediaController, controller)) { 346 // We have a new media session 347 clearCurrentMediaNotificationSession(); 348 mMediaController = controller; 349 mMediaController.registerCallback(mMediaListener, mHandler); 350 mMediaMetadata = mMediaController.getMetadata(); 351 if (DEBUG_MEDIA) { 352 Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: " 353 + mMediaController + ", receive metadata: " + mMediaMetadata); 354 } 355 } 356 357 if (mediaNotification != null 358 && !mediaNotification.getKey().equals(mMediaNotificationKey)) { 359 mMediaNotificationKey = mediaNotification.getKey(); 360 if (DEBUG_MEDIA) { 361 Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key=" 362 + mMediaNotificationKey); 363 } 364 } 365 } 366 clearCurrentMediaNotification()367 public void clearCurrentMediaNotification() { 368 mBackgroundExecutor.execute(this::clearMediaNotification); 369 } 370 clearMediaNotification()371 private void clearMediaNotification() { 372 mMediaNotificationKey = null; 373 clearCurrentMediaNotificationSession(); 374 } 375 dispatchUpdateMediaMetaData()376 private void dispatchUpdateMediaMetaData() { 377 ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners); 378 mBackgroundExecutor.execute(() -> updateMediaMetaData(callbacks)); 379 } 380 updateMediaMetaData(List<MediaListener> callbacks)381 private void updateMediaMetaData(List<MediaListener> callbacks) { 382 @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController); 383 mHandler.post(() -> { 384 for (int i = 0; i < callbacks.size(); i++) { 385 callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state); 386 } 387 }); 388 } 389 390 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)391 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 392 pw.print(" mMediaNotificationKey="); 393 pw.println(mMediaNotificationKey); 394 pw.print(" mMediaController="); 395 pw.print(mMediaController); 396 if (mMediaController != null) { 397 pw.print(" state=" + mMediaController.getPlaybackState()); 398 } 399 pw.println(); 400 pw.print(" mMediaMetadata="); 401 pw.print(mMediaMetadata); 402 if (mMediaMetadata != null) { 403 pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE)); 404 } 405 pw.println(); 406 } 407 isPlaybackActive(int state)408 private boolean isPlaybackActive(int state) { 409 return state != PlaybackState.STATE_STOPPED && state != PlaybackState.STATE_ERROR 410 && state != PlaybackState.STATE_NONE; 411 } 412 sameSessions(MediaController a, MediaController b)413 private boolean sameSessions(MediaController a, MediaController b) { 414 if (a == b) { 415 return true; 416 } 417 if (a == null) { 418 return false; 419 } 420 return a.controlsSameSession(b); 421 } 422 getMediaControllerPlaybackState(MediaController controller)423 private int getMediaControllerPlaybackState(MediaController controller) { 424 if (controller != null) { 425 final PlaybackState playbackState = controller.getPlaybackState(); 426 if (playbackState != null) { 427 return playbackState.getState(); 428 } 429 } 430 return PlaybackState.STATE_NONE; 431 } 432 clearCurrentMediaNotificationSession()433 private void clearCurrentMediaNotificationSession() { 434 mMediaMetadata = null; 435 if (mMediaController != null) { 436 if (DEBUG_MEDIA) { 437 Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: " 438 + mMediaController.getPackageName()); 439 } 440 mMediaController.unregisterCallback(mMediaListener); 441 } 442 mMediaController = null; 443 } 444 445 public interface MediaListener { 446 /** 447 * Called whenever there's new metadata or playback state. 448 * @param metadata Current metadata. 449 * @param state Current playback state 450 * @see PlaybackState.State 451 */ onPrimaryMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state)452 default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, 453 @PlaybackState.State int state) {} 454 } 455 } 456