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 android.app.Notification;
19 import android.content.Context;
20 import android.media.MediaMetadata;
21 import android.media.session.MediaController;
22 import android.media.session.MediaSession;
23 import android.media.session.MediaSessionManager;
24 import android.media.session.PlaybackState;
25 import android.os.UserHandle;
26 import android.util.Log;
27 
28 import com.android.systemui.Dumpable;
29 
30 import java.io.FileDescriptor;
31 import java.io.PrintWriter;
32 import java.util.ArrayList;
33 import java.util.List;
34 
35 /**
36  * Handles tasks and state related to media notifications. For example, there is a 'current' media
37  * notification, which this class keeps track of.
38  */
39 public class NotificationMediaManager implements Dumpable {
40     private static final String TAG = "NotificationMediaManager";
41     public static final boolean DEBUG_MEDIA = false;
42 
43     private final Context mContext;
44     private final MediaSessionManager mMediaSessionManager;
45 
46     protected NotificationPresenter mPresenter;
47     protected NotificationEntryManager mEntryManager;
48     private MediaController mMediaController;
49     private String mMediaNotificationKey;
50     private MediaMetadata mMediaMetadata;
51 
52     private final MediaController.Callback mMediaListener = new MediaController.Callback() {
53         @Override
54         public void onPlaybackStateChanged(PlaybackState state) {
55             super.onPlaybackStateChanged(state);
56             if (DEBUG_MEDIA) {
57                 Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
58             }
59             if (state != null) {
60                 if (!isPlaybackActive(state.getState())) {
61                     clearCurrentMediaNotification();
62                     mPresenter.updateMediaMetaData(true, true);
63                 }
64             }
65         }
66 
67         @Override
68         public void onMetadataChanged(MediaMetadata metadata) {
69             super.onMetadataChanged(metadata);
70             if (DEBUG_MEDIA) {
71                 Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
72             }
73             mMediaMetadata = metadata;
74             mPresenter.updateMediaMetaData(true, true);
75         }
76     };
77 
NotificationMediaManager(Context context)78     public NotificationMediaManager(Context context) {
79         mContext = context;
80         mMediaSessionManager
81                 = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
82         // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
83         // in session state
84     }
85 
setUpWithPresenter(NotificationPresenter presenter, NotificationEntryManager entryManager)86     public void setUpWithPresenter(NotificationPresenter presenter,
87             NotificationEntryManager entryManager) {
88         mPresenter = presenter;
89         mEntryManager = entryManager;
90     }
91 
onNotificationRemoved(String key)92     public void onNotificationRemoved(String key) {
93         if (key.equals(mMediaNotificationKey)) {
94             clearCurrentMediaNotification();
95             mPresenter.updateMediaMetaData(true, true);
96         }
97     }
98 
getMediaNotificationKey()99     public String getMediaNotificationKey() {
100         return mMediaNotificationKey;
101     }
102 
getMediaMetadata()103     public MediaMetadata getMediaMetadata() {
104         return mMediaMetadata;
105     }
106 
findAndUpdateMediaNotifications()107     public void findAndUpdateMediaNotifications() {
108         boolean metaDataChanged = false;
109 
110         synchronized (mEntryManager.getNotificationData()) {
111             ArrayList<NotificationData.Entry> activeNotifications = mEntryManager
112                     .getNotificationData().getActiveNotifications();
113             final int N = activeNotifications.size();
114 
115             // Promote the media notification with a controller in 'playing' state, if any.
116             NotificationData.Entry mediaNotification = null;
117             MediaController controller = null;
118             for (int i = 0; i < N; i++) {
119                 final NotificationData.Entry entry = activeNotifications.get(i);
120 
121                 if (isMediaNotification(entry)) {
122                     final MediaSession.Token token =
123                             entry.notification.getNotification().extras.getParcelable(
124                                     Notification.EXTRA_MEDIA_SESSION);
125                     if (token != null) {
126                         MediaController aController = new MediaController(mContext, token);
127                         if (PlaybackState.STATE_PLAYING ==
128                                 getMediaControllerPlaybackState(aController)) {
129                             if (DEBUG_MEDIA) {
130                                 Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching "
131                                         + entry.notification.getKey());
132                             }
133                             mediaNotification = entry;
134                             controller = aController;
135                             break;
136                         }
137                     }
138                 }
139             }
140             if (mediaNotification == null) {
141                 // Still nothing? OK, let's just look for live media sessions and see if they match
142                 // one of our notifications. This will catch apps that aren't (yet!) using media
143                 // notifications.
144 
145                 if (mMediaSessionManager != null) {
146                     // TODO: Should this really be for all users?
147                     final List<MediaController> sessions
148                             = mMediaSessionManager.getActiveSessionsForUser(
149                             null,
150                             UserHandle.USER_ALL);
151 
152                     for (MediaController aController : sessions) {
153                         if (PlaybackState.STATE_PLAYING ==
154                                 getMediaControllerPlaybackState(aController)) {
155                             // now to see if we have one like this
156                             final String pkg = aController.getPackageName();
157 
158                             for (int i = 0; i < N; i++) {
159                                 final NotificationData.Entry entry = activeNotifications.get(i);
160                                 if (entry.notification.getPackageName().equals(pkg)) {
161                                     if (DEBUG_MEDIA) {
162                                         Log.v(TAG, "DEBUG_MEDIA: found controller matching "
163                                                 + entry.notification.getKey());
164                                     }
165                                     controller = aController;
166                                     mediaNotification = entry;
167                                     break;
168                                 }
169                             }
170                         }
171                     }
172                 }
173             }
174 
175             if (controller != null && !sameSessions(mMediaController, controller)) {
176                 // We have a new media session
177                 clearCurrentMediaNotificationSession();
178                 mMediaController = controller;
179                 mMediaController.registerCallback(mMediaListener);
180                 mMediaMetadata = mMediaController.getMetadata();
181                 if (DEBUG_MEDIA) {
182                     Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: "
183                             + mMediaController + ", receive metadata: " + mMediaMetadata);
184                 }
185 
186                 metaDataChanged = true;
187             }
188 
189             if (mediaNotification != null
190                     && !mediaNotification.notification.getKey().equals(mMediaNotificationKey)) {
191                 mMediaNotificationKey = mediaNotification.notification.getKey();
192                 if (DEBUG_MEDIA) {
193                     Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
194                             + mMediaNotificationKey);
195                 }
196             }
197         }
198 
199         if (metaDataChanged) {
200             mEntryManager.updateNotifications();
201         }
202         mPresenter.updateMediaMetaData(metaDataChanged, true);
203     }
204 
clearCurrentMediaNotification()205     public void clearCurrentMediaNotification() {
206         mMediaNotificationKey = null;
207         clearCurrentMediaNotificationSession();
208     }
209 
210     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)211     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
212         pw.print("    mMediaSessionManager=");
213         pw.println(mMediaSessionManager);
214         pw.print("    mMediaNotificationKey=");
215         pw.println(mMediaNotificationKey);
216         pw.print("    mMediaController=");
217         pw.print(mMediaController);
218         if (mMediaController != null) {
219             pw.print(" state=" + mMediaController.getPlaybackState());
220         }
221         pw.println();
222         pw.print("    mMediaMetadata=");
223         pw.print(mMediaMetadata);
224         if (mMediaMetadata != null) {
225             pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE));
226         }
227         pw.println();
228     }
229 
isPlaybackActive(int state)230     private boolean isPlaybackActive(int state) {
231         return state != PlaybackState.STATE_STOPPED && state != PlaybackState.STATE_ERROR
232                 && state != PlaybackState.STATE_NONE;
233     }
234 
sameSessions(MediaController a, MediaController b)235     private boolean sameSessions(MediaController a, MediaController b) {
236         if (a == b) {
237             return true;
238         }
239         if (a == null) {
240             return false;
241         }
242         return a.controlsSameSession(b);
243     }
244 
getMediaControllerPlaybackState(MediaController controller)245     private int getMediaControllerPlaybackState(MediaController controller) {
246         if (controller != null) {
247             final PlaybackState playbackState = controller.getPlaybackState();
248             if (playbackState != null) {
249                 return playbackState.getState();
250             }
251         }
252         return PlaybackState.STATE_NONE;
253     }
254 
isMediaNotification(NotificationData.Entry entry)255     private boolean isMediaNotification(NotificationData.Entry entry) {
256         // TODO: confirm that there's a valid media key
257         return entry.getExpandedContentView() != null &&
258                 entry.getExpandedContentView()
259                         .findViewById(com.android.internal.R.id.media_actions) != null;
260     }
261 
clearCurrentMediaNotificationSession()262     private void clearCurrentMediaNotificationSession() {
263         mMediaMetadata = null;
264         if (mMediaController != null) {
265             if (DEBUG_MEDIA) {
266                 Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
267                         + mMediaController.getPackageName());
268             }
269             mMediaController.unregisterCallback(mMediaListener);
270         }
271         mMediaController = null;
272     }
273 }
274