• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
setControllersByPackageName(List<MediaController> newControllers)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          */
onActiveSourceChanged()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      */
ActiveMediaSourceManager(Context context)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      */
selectMediaController(List<MediaController> controllers)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 
dump(String title, List<MediaController> controllers)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 
getStateName(@laybackState.State int state)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      */
getTopMostController(List<MediaController> controllers)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 
changeMediaController(MediaController mediaController)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      */
start()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      */
stop()247     private void stop() {
248         mMediaSessionUpdater.setControllersByPackageName(new ArrayList<>());
249         mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener);
250         changeMediaController(null);
251         mIsStarted = false;
252     }
253 
notify(Consumer<Observer> notification)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
getMediaController()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      */
registerObserver(Observer observer)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      */
unregisterObserver(Observer observer)294     public void unregisterObserver(Observer observer) {
295         mObservers.remove(observer);
296         if (mObservers.isEmpty() && mIsStarted) {
297             stop();
298         }
299     }
300 
getLastKnownActivePackageName()301     private String getLastKnownActivePackageName() {
302         return mSharedPreferences.getString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, null);
303     }
304 
setLastKnownActivePackageName(String packageName)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      */
getControllerForPackage(String packageName)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      */
isPlaying(String packageName)328     public boolean isPlaying(String packageName) {
329         return mMediaController != null && mMediaController.getPackageName().equals(packageName);
330     }
331 }
332