1 /* 2 * Copyright 2018 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 17 package com.android.car.media.common; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.content.SharedPreferences; 22 import android.media.session.MediaController; 23 import android.media.session.MediaSessionManager; 24 import android.media.session.PlaybackState; 25 import android.os.Handler; 26 import android.util.Log; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.function.Consumer; 31 32 /** 33 * This is an abstractions over {@link MediaSessionManager} that provides information about the 34 * currently "active" media session. 35 * <p> 36 * It automatically determines the foreground media app (the one that would normally 37 * receive playback events) and exposes metadata and events from such app, or when a different app 38 * becomes foreground. 39 * <p> 40 * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission to be held by the 41 * calling app. 42 */ 43 public class ActiveMediaSourceManager { 44 private static final String TAG = "ActiveSourceManager"; 45 46 private static final String PLAYBACK_MODEL_SHARED_PREFS = 47 "com.android.car.media.PLAYBACK_MODEL"; 48 private static final String PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY = 49 "active_packagename"; 50 51 private final MediaSessionManager mMediaSessionManager; 52 private final Handler mHandler = new Handler(); 53 private final Context mContext; 54 private final List<Observer> mObservers = new ArrayList<>(); 55 private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater(); 56 private final SharedPreferences mSharedPreferences; 57 @Nullable 58 private MediaController mMediaController; 59 private boolean mIsStarted; 60 61 /** 62 * Temporary work-around to bug b/76017849. 63 * MediaSessionManager is not notifying media session priority changes. 64 * As a work-around we subscribe to playback state changes on all controllers to detect 65 * potential priority changes. 66 * This might cause a few unnecessary checks, but selecting the top-most controller is a 67 * cheap operation. 68 */ 69 private class MediaSessionUpdater { 70 private List<MediaController> mControllers = new ArrayList<>(); 71 72 private MediaController.Callback mCallback = new MediaController.Callback() { 73 @Override 74 public void onPlaybackStateChanged(PlaybackState state) { 75 selectMediaController(mMediaSessionManager.getActiveSessions(null)); 76 } 77 78 @Override 79 public void onSessionDestroyed() { 80 selectMediaController(mMediaSessionManager.getActiveSessions(null)); 81 } 82 }; 83 84 void setControllersByPackageName(List<MediaController> newControllers) { 85 for (MediaController oldController : mControllers) { 86 oldController.unregisterCallback(mCallback); 87 } 88 for (MediaController newController : newControllers) { 89 newController.registerCallback(mCallback); 90 } 91 mControllers.clear(); 92 mControllers.addAll(newControllers); 93 } 94 } 95 96 /** 97 * An observer of this model 98 */ 99 public interface Observer { 100 /** 101 * Called when the top source media app changes. 102 */ 103 void onActiveSourceChanged(); 104 } 105 106 private MediaSessionManager.OnActiveSessionsChangedListener mSessionChangeListener = 107 this::selectMediaController; 108 109 /** 110 * Creates a {@link ActiveMediaSourceManager}. This instance is going to be inactive until 111 * {@link #start()} method is invoked. 112 */ 113 public ActiveMediaSourceManager(Context context) { 114 mContext = context; 115 mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); 116 mSharedPreferences = mContext.getSharedPreferences(PLAYBACK_MODEL_SHARED_PREFS, 117 Context.MODE_PRIVATE); 118 } 119 120 /** 121 * Selects one of the provided controllers as the "currently playing" one. 122 */ 123 private void selectMediaController(List<MediaController> controllers) { 124 if (Log.isLoggable(TAG, Log.DEBUG)) { 125 dump("Selecting a media controller from: ", controllers); 126 } 127 changeMediaController(getTopMostController(controllers)); 128 mMediaSessionUpdater.setControllersByPackageName(controllers); 129 } 130 131 private void dump(String title, List<MediaController> controllers) { 132 Log.d(TAG, title + " (total: " + controllers.size() + ")"); 133 for (MediaController controller : controllers) { 134 String stateName = getStateName(controller.getPlaybackState() != null 135 ? controller.getPlaybackState().getState() 136 : PlaybackState.STATE_NONE); 137 Log.d(TAG, String.format("\t%s: %s", 138 controller.getPackageName(), 139 stateName)); 140 } 141 } 142 143 private String getStateName(@PlaybackState.State int state) { 144 switch (state) { 145 case PlaybackState.STATE_NONE: 146 return "NONE"; 147 case PlaybackState.STATE_STOPPED: 148 return "STOPPED"; 149 case PlaybackState.STATE_PAUSED: 150 return "PAUSED"; 151 case PlaybackState.STATE_PLAYING: 152 return "PLAYING"; 153 case PlaybackState.STATE_FAST_FORWARDING: 154 return "FORWARDING"; 155 case PlaybackState.STATE_REWINDING: 156 return "REWINDING"; 157 case PlaybackState.STATE_BUFFERING: 158 return "BUFFERING"; 159 case PlaybackState.STATE_ERROR: 160 return "ERROR"; 161 case PlaybackState.STATE_CONNECTING: 162 return "CONNECTING"; 163 case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: 164 return "SKIPPING_TO_PREVIOUS"; 165 case PlaybackState.STATE_SKIPPING_TO_NEXT: 166 return "SKIPPING_TO_NEXT"; 167 case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM: 168 return "SKIPPING_TO_QUEUE_ITEM"; 169 default: 170 return "UNKNOWN"; 171 } 172 } 173 174 /** 175 * @return the controller most likely to be the currently active one, out of the list of 176 * active controllers repoted by {@link MediaSessionManager}. It does so by picking the first 177 * one (in order of priority) which an active state as reported by 178 * {@link MediaController#getPlaybackState()} 179 */ 180 private MediaController getTopMostController(List<MediaController> controllers) { 181 if (controllers != null && controllers.size() > 0) { 182 for (MediaController candidate : controllers) { 183 @PlaybackState.State int state = candidate.getPlaybackState() != null 184 ? candidate.getPlaybackState().getState() 185 : PlaybackState.STATE_NONE; 186 if (state == PlaybackState.STATE_BUFFERING 187 || state == PlaybackState.STATE_CONNECTING 188 || state == PlaybackState.STATE_FAST_FORWARDING 189 || state == PlaybackState.STATE_PLAYING 190 || state == PlaybackState.STATE_REWINDING 191 || state == PlaybackState.STATE_SKIPPING_TO_NEXT 192 || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS 193 || state == PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM) { 194 return candidate; 195 } 196 } 197 // If no source is active, we go for the last known source 198 String packageName = getLastKnownActivePackageName(); 199 if (packageName != null) { 200 for (MediaController candidate : controllers) { 201 if (candidate.getPackageName().equals(packageName)) { 202 return candidate; 203 } 204 } 205 } 206 return controllers.get(0); 207 } 208 return null; 209 } 210 211 private void changeMediaController(MediaController mediaController) { 212 if (Log.isLoggable(TAG, Log.DEBUG)) { 213 Log.d(TAG, "New media controller: " + (mediaController != null 214 ? mediaController.getPackageName() : null)); 215 } 216 if ((mediaController == null && mMediaController == null) 217 || (mediaController != null && mMediaController != null 218 && mediaController.getPackageName().equals(mMediaController.getPackageName()))) { 219 // If no change, do nothing. 220 return; 221 } 222 mMediaController = mediaController; 223 setLastKnownActivePackageName(mMediaController != null 224 ? mMediaController.getPackageName() 225 : null); 226 notify(Observer::onActiveSourceChanged); 227 } 228 229 /** 230 * Starts following changes on the list of active media sources. If any changes happen, all 231 * observers registered through {@link #registerObserver(Observer)} will be notified. 232 * <p> 233 * Calling this method might cause an immediate {@link Observer#onActiveSourceChanged()} 234 * event in case the current media source is different than the last known one. 235 */ 236 private void start() { 237 mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionChangeListener, null); 238 selectMediaController(mMediaSessionManager.getActiveSessions(null)); 239 mIsStarted = true; 240 } 241 242 /** 243 * Stops following changes on the list of active media sources. This method could cause an 244 * immediate {@link PlaybackModel.PlaybackObserver#onSourceChanged()} event if a media source 245 * was already connected. 246 */ 247 private void stop() { 248 mMediaSessionUpdater.setControllersByPackageName(new ArrayList<>()); 249 mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener); 250 changeMediaController(null); 251 mIsStarted = false; 252 } 253 254 private void notify(Consumer<Observer> notification) { 255 mHandler.post(() -> { 256 List<Observer> observers = new ArrayList<>(mObservers); 257 for (Observer observer : observers) { 258 notification.accept(observer); 259 } 260 }); 261 } 262 263 /** 264 * @return a {@link MediaController} providing access to metadata of the currently playing media 265 * source, or NULL if no media source has an active session. Changes on this value will 266 * be notified through {@link Observer#onActiveSourceChanged()} 267 */ 268 @Nullable 269 public MediaController getMediaController() { 270 return mIsStarted 271 ? mMediaController 272 : getTopMostController(mMediaSessionManager.getActiveSessions(null)); 273 } 274 275 /** 276 * Registers an observer to be notified of media events. If the model is not started yet it 277 * will start right away. If the model was already started, the observer will receive an 278 * immediate {@link Observer#onActiveSourceChanged()} event. 279 */ 280 public void registerObserver(Observer observer) { 281 mObservers.add(observer); 282 if (!mIsStarted) { 283 start(); 284 } else { 285 observer.onActiveSourceChanged(); 286 } 287 } 288 289 /** 290 * Unregisters an observer previously registered using 291 * {@link #registerObserver(Observer)}. There are no other observers the model will 292 * stop tracking changes right away. 293 */ 294 public void unregisterObserver(Observer observer) { 295 mObservers.remove(observer); 296 if (mObservers.isEmpty() && mIsStarted) { 297 stop(); 298 } 299 } 300 301 private String getLastKnownActivePackageName() { 302 return mSharedPreferences.getString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, null); 303 } 304 305 private void setLastKnownActivePackageName(String packageName) { 306 mSharedPreferences.edit() 307 .putString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, packageName) 308 .apply(); 309 } 310 311 /** 312 * Returns the {@link MediaController} corresponding to the given package name, or NULL if 313 * no active session exists for it. 314 */ 315 public @Nullable MediaController getControllerForPackage(String packageName) { 316 List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null); 317 for (MediaController controller : controllers) { 318 if (controller.getPackageName().equals(packageName)) { 319 return controller; 320 } 321 } 322 return null; 323 } 324 325 /** 326 * Returns true if the given package name corresponds to the top most media source. 327 */ 328 public boolean isPlaying(String packageName) { 329 return mMediaController != null && mMediaController.getPackageName().equals(packageName); 330 } 331 } 332