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