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