• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.car;
17 
18 import android.app.ActivityManager;
19 import android.car.media.CarMediaManager;
20 import android.car.media.CarMediaManager.MediaSourceChangedListener;
21 import android.car.media.ICarMedia;
22 import android.car.media.ICarMediaSourceListener;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.SharedPreferences;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.media.session.MediaController;
31 import android.media.session.MediaController.TransportControls;
32 import android.media.session.MediaSession;
33 import android.media.session.MediaSession.Token;
34 import android.media.session.MediaSessionManager;
35 import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
36 import android.media.session.PlaybackState;
37 import android.os.Handler;
38 import android.os.HandlerThread;
39 import android.os.RemoteCallbackList;
40 import android.os.RemoteException;
41 import android.service.media.MediaBrowserService;
42 import android.text.TextUtils;
43 import android.util.Log;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.Nullable;
47 
48 import com.android.car.user.CarUserService;
49 
50 import java.io.PrintWriter;
51 import java.util.ArrayDeque;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Deque;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.stream.Collectors;
59 
60 /**
61  * CarMediaService manages the currently active media source for car apps. This is different from
62  * the MediaSessionManager's active sessions, as there can only be one active source in the car,
63  * through both browse and playback.
64  *
65  * In the car, the active media source does not necessarily have an active MediaSession, e.g. if
66  * it were being browsed only. However, that source is still considered the active source, and
67  * should be the source displayed in any Media related UIs (Media Center, home screen, etc).
68  */
69 public class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
70 
71     private static final String SOURCE_KEY = "media_source";
72     private static final String PLAYBACK_STATE_KEY = "playback_state";
73     private static final String SHARED_PREF = "com.android.car.media.car_media_service";
74     private static final String PACKAGE_NAME_SEPARATOR = ",";
75 
76     private Context mContext;
77     private final MediaSessionManager mMediaSessionManager;
78     private MediaSessionUpdater mMediaSessionUpdater;
79     private String mPrimaryMediaPackage;
80     private SharedPreferences mSharedPrefs;
81     // MediaController for the current active user's active media session. This controller can be
82     // null if playback has not been started yet.
83     private MediaController mActiveUserMediaController;
84     private SessionChangedListener mSessionsListener;
85     private boolean mStartPlayback;
86 
87     private RemoteCallbackList<ICarMediaSourceListener> mMediaSourceListeners =
88             new RemoteCallbackList();
89 
90     // Handler to receive PlaybackState callbacks from the active media controller.
91     private Handler mHandler;
92     private HandlerThread mHandlerThread;
93 
94     /** The package name of the last media source that was removed while being primary. */
95     private String mRemovedMediaSourcePackage;
96 
97     /**
98      * Listens to {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED}
99      * so we can reset the media source to null when its application is uninstalled, and restore it
100      * when the application is reinstalled.
101      */
102     private BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() {
103         @Override
104         public void onReceive(Context context, Intent intent) {
105             if (intent.getData() == null) {
106                 return;
107             }
108             String intentPackage = intent.getData().getSchemeSpecificPart();
109             if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
110                 if (mPrimaryMediaPackage != null && mPrimaryMediaPackage.equals(intentPackage)) {
111                     mRemovedMediaSourcePackage = intentPackage;
112                     setPrimaryMediaSource(null);
113                 }
114             } else if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())
115                     || Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
116                 if (mRemovedMediaSourcePackage != null
117                         && mRemovedMediaSourcePackage.equals(intentPackage)
118                         && isMediaService(intentPackage)) {
119                     setPrimaryMediaSource(mRemovedMediaSourcePackage);
120                 }
121             }
122         }
123     };
124 
125     private BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
126         @Override
127         public void onReceive(Context context, Intent intent) {
128             updateMediaSessionCallbackForCurrentUser();
129         }
130     };
131 
CarMediaService(Context context)132     public CarMediaService(Context context) {
133         mContext = context;
134         mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
135         mMediaSessionUpdater = new MediaSessionUpdater();
136 
137         mHandlerThread = new HandlerThread(CarLog.TAG_MEDIA);
138         mHandlerThread.start();
139         mHandler = new Handler(mHandlerThread.getLooper());
140 
141         IntentFilter filter = new IntentFilter();
142         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
143         filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
144         filter.addAction(Intent.ACTION_PACKAGE_ADDED);
145         filter.addDataScheme("package");
146         mContext.registerReceiver(mPackageRemovedReceiver, filter);
147 
148         IntentFilter userSwitchFilter = new IntentFilter();
149         userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
150         mContext.registerReceiver(mUserSwitchReceiver, userSwitchFilter);
151 
152         updateMediaSessionCallbackForCurrentUser();
153     }
154 
155     @Override
init()156     public void init() {
157         CarLocalServices.getService(CarUserService.class).runOnUser0Unlock(() -> {
158             mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
159             mPrimaryMediaPackage = getLastMediaPackage();
160             mStartPlayback = mSharedPrefs.getInt(PLAYBACK_STATE_KEY, PlaybackState.STATE_NONE)
161                     == PlaybackState.STATE_PLAYING;
162             notifyListeners();
163         });
164     }
165 
166     @Override
release()167     public void release() {
168         mMediaSessionUpdater.unregisterCallbacks();
169     }
170 
171     @Override
dump(PrintWriter writer)172     public void dump(PrintWriter writer) {
173         writer.println("*CarMediaService*");
174         writer.println("\tCurrent media package: " + mPrimaryMediaPackage);
175         if (mActiveUserMediaController != null) {
176             writer.println(
177                     "\tCurrent media controller: " + mActiveUserMediaController.getPackageName());
178         }
179         writer.println("\tNumber of active media sessions: "
180                 + mMediaSessionManager.getActiveSessionsForUser(null,
181                         ActivityManager.getCurrentUser()).size());
182     }
183 
184     /**
185      * @see {@link CarMediaManager#setMediaSource(String)}
186      */
187     @Override
setMediaSource(String packageName)188     public synchronized void setMediaSource(String packageName) {
189         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
190         setPrimaryMediaSource(packageName);
191     }
192 
193     /**
194      * @see {@link CarMediaManager#getMediaSource()}
195      */
196     @Override
getMediaSource()197     public synchronized String getMediaSource() {
198         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
199         return mPrimaryMediaPackage;
200     }
201 
202     /**
203      * @see {@link CarMediaManager#registerMediaSourceListener(MediaSourceChangedListener)}
204      */
205     @Override
registerMediaSourceListener(ICarMediaSourceListener callback)206     public synchronized void registerMediaSourceListener(ICarMediaSourceListener callback) {
207         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
208         mMediaSourceListeners.register(callback);
209     }
210 
211     /**
212      * @see {@link CarMediaManager#unregisterMediaSourceListener(ICarMediaSourceListener)}
213      */
214     @Override
unregisterMediaSourceListener(ICarMediaSourceListener callback)215     public synchronized void unregisterMediaSourceListener(ICarMediaSourceListener callback) {
216         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
217         mMediaSourceListeners.unregister(callback);
218     }
219 
updateMediaSessionCallbackForCurrentUser()220     private void updateMediaSessionCallbackForCurrentUser() {
221         if (mSessionsListener != null) {
222             mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);
223         }
224         mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());
225         mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsListener, null,
226                 ActivityManager.getCurrentUser(), null);
227         mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(
228                 null, ActivityManager.getCurrentUser()));
229     }
230 
231     /**
232      * Attempts to play the current source using MediaController.TransportControls.play()
233      */
play()234     private void play() {
235         if (mActiveUserMediaController != null) {
236             TransportControls controls = mActiveUserMediaController.getTransportControls();
237             if (controls != null) {
238                 controls.play();
239             }
240         }
241     }
242 
243     /**
244      * Attempts to stop the current source using MediaController.TransportControls.stop()
245      */
stop()246     private void stop() {
247         if (mActiveUserMediaController != null) {
248             TransportControls controls = mActiveUserMediaController.getTransportControls();
249             if (controls != null) {
250                 controls.stop();
251             }
252         }
253     }
254 
255     private class SessionChangedListener implements OnActiveSessionsChangedListener {
256         private final int mCurrentUser;
257 
SessionChangedListener(int currentUser)258         SessionChangedListener(int currentUser) {
259             mCurrentUser = currentUser;
260         }
261 
262         @Override
onActiveSessionsChanged(List<MediaController> controllers)263         public void onActiveSessionsChanged(List<MediaController> controllers) {
264             if (ActivityManager.getCurrentUser() != mCurrentUser) {
265                 Log.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);
266                 return;
267             }
268             mMediaSessionUpdater.registerCallbacks(controllers);
269         }
270     }
271 
272     private class MediaControllerCallback extends MediaController.Callback {
273 
274         private final MediaController mMediaController;
275         private int mPreviousPlaybackState;
276 
MediaControllerCallback(MediaController mediaController)277         private MediaControllerCallback(MediaController mediaController) {
278             mMediaController = mediaController;
279             PlaybackState state = mediaController.getPlaybackState();
280             mPreviousPlaybackState = (state == null) ? PlaybackState.STATE_NONE : state.getState();
281         }
282 
register()283         private void register() {
284             mMediaController.registerCallback(this);
285         }
286 
unregister()287         private void unregister() {
288             mMediaController.unregisterCallback(this);
289         }
290 
291         @Override
onPlaybackStateChanged(@ullable PlaybackState state)292         public void onPlaybackStateChanged(@Nullable PlaybackState state) {
293             if (state.getState() == PlaybackState.STATE_PLAYING
294                     && state.getState() != mPreviousPlaybackState) {
295                 setPrimaryMediaSource(mMediaController.getPackageName());
296             }
297             mPreviousPlaybackState = state.getState();
298         }
299     }
300 
301     private class MediaSessionUpdater {
302         private Map<Token, MediaControllerCallback> mCallbacks = new HashMap<>();
303 
304         /**
305          * Register a {@link MediaControllerCallback} for each given controller. Note that if a
306          * controller was already watched, we don't register a callback again. This prevents an
307          * undesired revert of the primary media source. Callbacks for previously watched
308          * controllers that are not present in the given list are unregistered.
309          */
registerCallbacks(List<MediaController> newControllers)310         private void registerCallbacks(List<MediaController> newControllers) {
311 
312             List<MediaController> additions = new ArrayList<>(newControllers.size());
313             Map<MediaSession.Token, MediaControllerCallback> updatedCallbacks =
314                     new HashMap<>(newControllers.size());
315 
316             for (MediaController controller : newControllers) {
317                 MediaSession.Token token = controller.getSessionToken();
318                 MediaControllerCallback callback = mCallbacks.get(token);
319                 if (callback == null) {
320                     callback = new MediaControllerCallback(controller);
321                     callback.register();
322                     additions.add(controller);
323                 }
324                 updatedCallbacks.put(token, callback);
325             }
326 
327             for (MediaSession.Token token : mCallbacks.keySet()) {
328                 if (!updatedCallbacks.containsKey(token)) {
329                     mCallbacks.get(token).unregister();
330                 }
331             }
332 
333             mCallbacks = updatedCallbacks;
334             updatePrimaryMediaSourceWithCurrentlyPlaying(additions);
335             // If there are no playing media sources, and we don't currently have the controller
336             // for the active source, check the new active sessions for a matching controller.
337             if (mActiveUserMediaController == null) {
338                 updateActiveMediaController(additions);
339             }
340         }
341 
342         /**
343          * Unregister all MediaController callbacks
344          */
unregisterCallbacks()345         private void unregisterCallbacks() {
346             for (Map.Entry<Token, MediaControllerCallback> entry : mCallbacks.entrySet()) {
347                 entry.getValue().unregister();
348             }
349         }
350     }
351 
352     /**
353      * Updates the primary media source, then notifies content observers of the change
354      */
setPrimaryMediaSource(@ullable String packageName)355     private synchronized void setPrimaryMediaSource(@Nullable String packageName) {
356         if (mPrimaryMediaPackage != null && mPrimaryMediaPackage.equals((packageName))) {
357             return;
358         }
359 
360         stop();
361 
362         mStartPlayback = false;
363         mPrimaryMediaPackage = packageName;
364         updateActiveMediaController(mMediaSessionManager
365                 .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
366 
367         if (mSharedPrefs != null) {
368             if (!TextUtils.isEmpty(mPrimaryMediaPackage)) {
369                 saveLastMediaPackage(mPrimaryMediaPackage);
370                 mRemovedMediaSourcePackage = null;
371             }
372         } else {
373             // Shouldn't reach this unless there is some other error in CarService
374             Log.e(CarLog.TAG_MEDIA, "Error trying to save last media source, prefs uninitialized");
375         }
376         notifyListeners();
377     }
378 
notifyListeners()379     private void notifyListeners() {
380         int i = mMediaSourceListeners.beginBroadcast();
381         while (i-- > 0) {
382             try {
383                 ICarMediaSourceListener callback = mMediaSourceListeners.getBroadcastItem(i);
384                 callback.onMediaSourceChanged(mPrimaryMediaPackage);
385             } catch (RemoteException e) {
386                 Log.e(CarLog.TAG_MEDIA, "calling onMediaSourceChanged failed " + e);
387             }
388         }
389         mMediaSourceListeners.finishBroadcast();
390     }
391 
392     private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
393         @Override
394         public void onPlaybackStateChanged(PlaybackState state) {
395             savePlaybackState(state);
396             // Try to start playback if the new state allows the play action
397             maybeRestartPlayback(state);
398         }
399     };
400 
401     /**
402      * Finds the currently playing media source, then updates the active source if different
403      */
updatePrimaryMediaSourceWithCurrentlyPlaying( List<MediaController> controllers)404     private synchronized void updatePrimaryMediaSourceWithCurrentlyPlaying(
405             List<MediaController> controllers) {
406         for (MediaController controller : controllers) {
407             if (controller.getPlaybackState() != null
408                     && controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING) {
409                 if (mPrimaryMediaPackage == null || !mPrimaryMediaPackage.equals(
410                         controller.getPackageName())) {
411                     setPrimaryMediaSource(controller.getPackageName());
412                 }
413                 return;
414             }
415         }
416     }
417 
isMediaService(String packageName)418     private boolean isMediaService(String packageName) {
419         return getBrowseServiceClassName(packageName) != null;
420     }
421 
getBrowseServiceClassName(@onNull String packageName)422     private String getBrowseServiceClassName(@NonNull String packageName) {
423         PackageManager packageManager = mContext.getPackageManager();
424         Intent mediaIntent = new Intent();
425         mediaIntent.setPackage(packageName);
426         mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
427 
428         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
429                 PackageManager.GET_RESOLVED_FILTER);
430 
431         if (mediaServices == null || mediaServices.isEmpty()) {
432             return null;
433         }
434         return mediaServices.get(0).serviceInfo.name;
435     }
436 
saveLastMediaPackage(@onNull String packageName)437     private void saveLastMediaPackage(@NonNull String packageName) {
438         String serialized = mSharedPrefs.getString(SOURCE_KEY, null);
439         if (serialized == null) {
440             mSharedPrefs.edit().putString(SOURCE_KEY, packageName).apply();
441         } else {
442             Deque<String> packageNames = getPackageNameList(serialized);
443             packageNames.remove(packageName);
444             packageNames.addFirst(packageName);
445             mSharedPrefs.edit().putString(SOURCE_KEY, serializePackageNameList(packageNames))
446                     .apply();
447         }
448     }
449 
getLastMediaPackage()450     private String getLastMediaPackage() {
451         String serialized = mSharedPrefs.getString(SOURCE_KEY, null);
452         if (!TextUtils.isEmpty(serialized)) {
453             for (String packageName : getPackageNameList(serialized)) {
454                 if (isMediaService(packageName)) {
455                     return packageName;
456                 }
457             }
458         }
459 
460         String defaultSourcePackage = mContext.getString(R.string.default_media_application);
461         if (isMediaService(defaultSourcePackage)) {
462             return defaultSourcePackage;
463         }
464         return null;
465     }
466 
serializePackageNameList(Deque<String> packageNames)467     private String serializePackageNameList(Deque<String> packageNames) {
468         return packageNames.stream().collect(Collectors.joining(PACKAGE_NAME_SEPARATOR));
469     }
470 
getPackageNameList(String serialized)471     private Deque<String> getPackageNameList(String serialized) {
472         String[] packageNames = serialized.split(PACKAGE_NAME_SEPARATOR);
473         return new ArrayDeque(Arrays.asList(packageNames));
474     }
475 
savePlaybackState(PlaybackState playbackState)476     private void savePlaybackState(PlaybackState playbackState) {
477         int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE;
478         if (state == PlaybackState.STATE_PLAYING) {
479             // No longer need to request play if audio was resumed already via some other means,
480             // e.g. Assistant starts playback, user uses hardware button, etc.
481             mStartPlayback = false;
482         }
483         if (mSharedPrefs != null) {
484             mSharedPrefs.edit().putInt(PLAYBACK_STATE_KEY, state).apply();
485         }
486     }
487 
maybeRestartPlayback(PlaybackState state)488     private void maybeRestartPlayback(PlaybackState state) {
489         if (mStartPlayback && state != null
490                 && (state.getActions() & PlaybackState.ACTION_PLAY) != 0) {
491             play();
492             mStartPlayback = false;
493         }
494     }
495 
496     /**
497      * Updates active media controller from the list that has the same package name as the primary
498      * media package. Clears callback and resets media controller to null if not found.
499      */
updateActiveMediaController(List<MediaController> mediaControllers)500     private void updateActiveMediaController(List<MediaController> mediaControllers) {
501         if (mPrimaryMediaPackage == null) {
502             return;
503         }
504         if (mActiveUserMediaController != null) {
505             mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
506             mActiveUserMediaController = null;
507         }
508         for (MediaController controller : mediaControllers) {
509             if (mPrimaryMediaPackage.equals(controller.getPackageName())) {
510                 mActiveUserMediaController = controller;
511                 // Specify Handler to receive callbacks on, to avoid defaulting to the calling
512                 // thread; this method can be called from the MediaSessionManager callback.
513                 // Using the version of this method without passing a handler causes a
514                 // RuntimeException for failing to create a Handler.
515                 PlaybackState state = mActiveUserMediaController.getPlaybackState();
516                 savePlaybackState(state);
517                 mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler);
518                 maybeRestartPlayback(state);
519                 return;
520             }
521         }
522     }
523 }
524