• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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