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 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 86 public void setUpWithPresenter(NotificationPresenter presenter, 87 NotificationEntryManager entryManager) { 88 mPresenter = presenter; 89 mEntryManager = entryManager; 90 } 91 92 public void onNotificationRemoved(String key) { 93 if (key.equals(mMediaNotificationKey)) { 94 clearCurrentMediaNotification(); 95 mPresenter.updateMediaMetaData(true, true); 96 } 97 } 98 99 public String getMediaNotificationKey() { 100 return mMediaNotificationKey; 101 } 102 103 public MediaMetadata getMediaMetadata() { 104 return mMediaMetadata; 105 } 106 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 205 public void clearCurrentMediaNotification() { 206 mMediaNotificationKey = null; 207 clearCurrentMediaNotificationSession(); 208 } 209 210 @Override 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 230 private boolean isPlaybackActive(int state) { 231 return state != PlaybackState.STATE_STOPPED && state != PlaybackState.STATE_ERROR 232 && state != PlaybackState.STATE_NONE; 233 } 234 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 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 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 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