• 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 static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE;
19 import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
20 
21 import android.annotation.TestApi;
22 import android.app.ActivityManager;
23 import android.car.Car;
24 import android.car.media.CarMediaManager;
25 import android.car.media.CarMediaManager.MediaSourceChangedListener;
26 import android.car.media.CarMediaManager.MediaSourceMode;
27 import android.car.media.ICarMedia;
28 import android.car.media.ICarMediaSourceListener;
29 import android.car.user.CarUserManager;
30 import android.car.user.CarUserManager.UserLifecycleListener;
31 import android.content.BroadcastReceiver;
32 import android.content.ComponentName;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.pm.ResolveInfo;
39 import android.media.session.MediaController;
40 import android.media.session.MediaController.TransportControls;
41 import android.media.session.MediaSession;
42 import android.media.session.MediaSession.Token;
43 import android.media.session.MediaSessionManager;
44 import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
45 import android.media.session.PlaybackState;
46 import android.os.Bundle;
47 import android.os.Handler;
48 import android.os.HandlerThread;
49 import android.os.Looper;
50 import android.os.RemoteCallbackList;
51 import android.os.RemoteException;
52 import android.os.UserHandle;
53 import android.os.UserManager;
54 import android.service.media.MediaBrowserService;
55 import android.text.TextUtils;
56 import android.util.Log;
57 
58 import androidx.annotation.NonNull;
59 import androidx.annotation.Nullable;
60 
61 import com.android.car.user.CarUserService;
62 import com.android.internal.annotations.GuardedBy;
63 import com.android.internal.annotations.VisibleForTesting;
64 
65 import java.io.PrintWriter;
66 import java.util.ArrayDeque;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Deque;
70 import java.util.HashMap;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.stream.Collectors;
74 
75 /**
76  * CarMediaService manages the currently active media source for car apps. This is different from
77  * the MediaSessionManager's active sessions, as there can only be one active source in the car,
78  * through both browse and playback.
79  *
80  * In the car, the active media source does not necessarily have an active MediaSession, e.g. if
81  * it were being browsed only. However, that source is still considered the active source, and
82  * should be the source displayed in any Media related UIs (Media Center, home screen, etc).
83  */
84 public class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
85 
86     private static final String SOURCE_KEY = "media_source_component";
87     private static final String SOURCE_KEY_SEPARATOR = "_";
88     private static final String PLAYBACK_STATE_KEY = "playback_state";
89     private static final String SHARED_PREF = "com.android.car.media.car_media_service";
90     private static final String COMPONENT_NAME_SEPARATOR = ",";
91     private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION";
92     private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay";
93 
94     private static final int MEDIA_SOURCE_MODES = 2;
95 
96     // XML configuration options for autoplay on media source change.
97     private static final int AUTOPLAY_CONFIG_NEVER = 0;
98     private static final int AUTOPLAY_CONFIG_ALWAYS = 1;
99     // This mode uses the current source's last stored playback state to resume playback
100     private static final int AUTOPLAY_CONFIG_RETAIN_PER_SOURCE = 2;
101     // This mode uses the previous source's playback state to resume playback
102     private static final int AUTOPLAY_CONFIG_RETAIN_PREVIOUS = 3;
103 
104     private final Context mContext;
105     private final CarUserService mUserService;
106     private final UserManager mUserManager;
107     private final MediaSessionManager mMediaSessionManager;
108     private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater();
109     @GuardedBy("mLock")
110     private ComponentName[] mPrimaryMediaComponents = new ComponentName[MEDIA_SOURCE_MODES];
111     private SharedPreferences mSharedPrefs;
112     // MediaController for the current active user's active media session. This controller can be
113     // null if playback has not been started yet.
114     private MediaController mActiveUserMediaController;
115     private SessionChangedListener mSessionsListener;
116     private int mPlayOnMediaSourceChangedConfig;
117     private int mPlayOnBootConfig;
118     private boolean mIndependentPlaybackConfig;
119     private int mCurrentPlaybackState;
120 
121     private boolean mPendingInit;
122 
123     @GuardedBy("mLock")
124     private final RemoteCallbackList<ICarMediaSourceListener>[] mMediaSourceListeners =
125             new RemoteCallbackList[MEDIA_SOURCE_MODES];
126 
127     private final Handler mMainHandler = new Handler(Looper.getMainLooper());
128 
129 
130     private final HandlerThread mHandlerThread  = CarServiceUtils.getHandlerThread(
131             getClass().getSimpleName());
132     // Handler to receive PlaybackState callbacks from the active media controller.
133     private final Handler mHandler = new Handler(mHandlerThread.getLooper());
134     private final Object mLock = new Object();
135 
136     /** The component name of the last media source that was removed while being primary. */
137     private ComponentName[] mRemovedMediaSourceComponents = new ComponentName[MEDIA_SOURCE_MODES];
138 
139     private final IntentFilter mPackageUpdateFilter;
140     private boolean mIsPackageUpdateReceiverRegistered;
141 
142     /**
143      * Listens to {@link Intent#ACTION_PACKAGE_REMOVED}, so we can fall back to a previously used
144      * media source when the active source is uninstalled.
145      */
146     private final BroadcastReceiver mPackageUpdateReceiver = new BroadcastReceiver() {
147         @Override
148         public void onReceive(Context context, Intent intent) {
149             if (intent.getData() == null) {
150                 return;
151             }
152             String intentPackage = intent.getData().getSchemeSpecificPart();
153             if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
154                 synchronized (mLock) {
155                     for (int i = 0; i < MEDIA_SOURCE_MODES; i++) {
156                         if (mPrimaryMediaComponents[i] != null
157                                 && mPrimaryMediaComponents[i].getPackageName().equals(
158                                 intentPackage)) {
159                             if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
160                                 // If package is being replaced, it may not be removed from
161                                 // PackageManager queries  when we check for available
162                                 // MediaBrowseServices, so we iterate to find the next available
163                                 // source.
164                                 for (ComponentName component : getLastMediaSources(i)) {
165                                     if (!mPrimaryMediaComponents[i].getPackageName()
166                                             .equals(component.getPackageName())) {
167                                         mRemovedMediaSourceComponents[i] =
168                                                 mPrimaryMediaComponents[i];
169                                         if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
170                                             Log.d(CarLog.TAG_MEDIA,
171                                                     "temporarily replacing updated media source "
172                                                             + mPrimaryMediaComponents[i]
173                                                             + "with backup source: "
174                                                             + component);
175                                         }
176                                         setPrimaryMediaSource(component, i);
177                                         return;
178                                     }
179                                 }
180                                 Log.e(CarLog.TAG_MEDIA, "No available backup media source");
181                             } else {
182                                 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
183                                     Log.d(CarLog.TAG_MEDIA, "replacing removed media source "
184                                             + mPrimaryMediaComponents[i] + "with backup source: "
185                                             + getLastMediaSource(i));
186                                 }
187                                 mRemovedMediaSourceComponents[i] = null;
188                                 setPrimaryMediaSource(getLastMediaSource(i), i);
189                             }
190                         }
191                     }
192                 }
193             } else if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())
194                     || Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
195                 for (int i = 0; i < MEDIA_SOURCE_MODES; i++) {
196                     if (mRemovedMediaSourceComponents[i] != null && mRemovedMediaSourceComponents[i]
197                             .getPackageName().equals(intentPackage)) {
198                         if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
199                             Log.d(CarLog.TAG_MEDIA, "restoring removed source: "
200                                     + mRemovedMediaSourceComponents[i]);
201                         }
202                         setPrimaryMediaSource(mRemovedMediaSourceComponents[i], i);
203                     }
204                 }
205             }
206         }
207     };
208 
209     private final UserLifecycleListener mUserLifecycleListener = event -> {
210         if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
211             Log.d(CarLog.TAG_MEDIA, "CarMediaService.onEvent(" + event + ")");
212         }
213 
214         switch (event.getEventType()) {
215             case CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING:
216                 maybeInitUser(event.getUserId());
217                 break;
218             case CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED:
219                 onUserUnlock(event.getUserId());
220                 break;
221         }
222     };
223 
CarMediaService(Context context, CarUserService userService)224     public CarMediaService(Context context, CarUserService userService) {
225         mContext = context;
226         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
227         mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
228         mMediaSourceListeners[MEDIA_SOURCE_MODE_PLAYBACK] = new RemoteCallbackList();
229         mMediaSourceListeners[MEDIA_SOURCE_MODE_BROWSE] = new RemoteCallbackList();
230         mIndependentPlaybackConfig = mContext.getResources().getBoolean(
231                 R.bool.config_mediaSourceIndependentPlayback);
232 
233         mPackageUpdateFilter = new IntentFilter();
234         mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
235         mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
236         mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
237         mPackageUpdateFilter.addDataScheme("package");
238         mUserService = userService;
239         mUserService.addUserLifecycleListener(mUserLifecycleListener);
240 
241         mPlayOnMediaSourceChangedConfig =
242                 mContext.getResources().getInteger(R.integer.config_mediaSourceChangedAutoplay);
243         mPlayOnBootConfig = mContext.getResources().getInteger(R.integer.config_mediaBootAutoplay);
244     }
245 
246     @Override
247     // This method is called from ICarImpl after CarMediaService is created.
init()248     public void init() {
249         int currentUser = ActivityManager.getCurrentUser();
250         maybeInitUser(currentUser);
251     }
252 
maybeInitUser(int userId)253     private void maybeInitUser(int userId) {
254         if (userId == UserHandle.USER_SYSTEM) {
255             return;
256         }
257         if (mUserManager.isUserUnlocked(userId)) {
258             initUser(userId);
259         } else {
260             mPendingInit = true;
261         }
262     }
263 
initUser(int userId)264     private void initUser(int userId) {
265         // SharedPreferences are shared among different users thus only need initialized once. And
266         // they should be initialized after user 0 is unlocked because SharedPreferences in
267         // credential encrypted storage are not available until after user 0 is unlocked.
268         // initUser() is called when the current foreground user is unlocked, and by that time user
269         // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
270         if (mSharedPrefs == null) {
271             mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
272         }
273 
274         if (mIsPackageUpdateReceiverRegistered) {
275             mContext.unregisterReceiver(mPackageUpdateReceiver);
276         }
277         UserHandle currentUser = new UserHandle(userId);
278         mContext.registerReceiverAsUser(mPackageUpdateReceiver, currentUser,
279                 mPackageUpdateFilter, null, null);
280         mIsPackageUpdateReceiverRegistered = true;
281 
282         mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = isCurrentUserEphemeral()
283                 ? getDefaultMediaSource() : getLastMediaSource(MEDIA_SOURCE_MODE_PLAYBACK);
284         mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = isCurrentUserEphemeral()
285                 ? getDefaultMediaSource() : getLastMediaSource(MEDIA_SOURCE_MODE_BROWSE);
286         mActiveUserMediaController = null;
287 
288         updateMediaSessionCallbackForCurrentUser();
289         notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK);
290         notifyListeners(MEDIA_SOURCE_MODE_BROWSE);
291 
292         startMediaConnectorService(shouldStartPlayback(mPlayOnBootConfig), currentUser);
293     }
294 
295     /**
296      * Starts a service on the current user that binds to the media browser of the current media
297      * source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
298      * provide an API to connect on a specific user. Additionally, this service will attempt to
299      * resume playback using the MediaSession obtained via the media browser connection, which
300      * is more reliable than using active MediaSessions from MediaSessionManager.
301      */
startMediaConnectorService(boolean startPlayback, UserHandle currentUser)302     private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) {
303         Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION);
304         serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection));
305         serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback);
306         mContext.startForegroundServiceAsUser(serviceStart, currentUser);
307     }
308 
sharedPrefsInitialized()309     private boolean sharedPrefsInitialized() {
310         if (mSharedPrefs == null) {
311             // It shouldn't reach this but let's be cautious.
312             Log.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
313             String className = getClass().getName();
314             for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
315                 // Let's print the useful logs only.
316                 String log = ste.toString();
317                 if (log.contains(className)) {
318                     Log.e(CarLog.TAG_MEDIA, log);
319                 }
320             }
321             return false;
322         }
323         return true;
324     }
325 
isCurrentUserEphemeral()326     private boolean isCurrentUserEphemeral() {
327         return mUserManager.getUserInfo(ActivityManager.getCurrentUser()).isEphemeral();
328     }
329 
330     @Override
release()331     public void release() {
332         mMediaSessionUpdater.unregisterCallbacks();
333         mUserService.removeUserLifecycleListener(mUserLifecycleListener);
334     }
335 
336     @Override
dump(PrintWriter writer)337     public void dump(PrintWriter writer) {
338         writer.println("*CarMediaService*");
339         writer.println("\tCurrent playback media component: "
340                 + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "-"
341                 : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString()));
342         writer.println("\tCurrent browse media component: "
343                 + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] == null ? "-"
344                 : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE].flattenToString()));
345         if (mActiveUserMediaController != null) {
346             writer.println(
347                     "\tCurrent media controller: " + mActiveUserMediaController.getPackageName());
348             writer.println(
349                     "\tCurrent browse service extra: " + getClassName(mActiveUserMediaController));
350         }
351         writer.println("\tNumber of active media sessions: " + mMediaSessionManager
352                 .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()).size());
353 
354         writer.println("\tPlayback media source history: ");
355         for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_PLAYBACK)) {
356             writer.println("\t" + name.flattenToString());
357         }
358         writer.println("\tBrowse media source history: ");
359         for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_BROWSE)) {
360             writer.println("\t" + name.flattenToString());
361         }
362 
363     }
364 
365     /**
366      * @see {@link CarMediaManager#setMediaSource(ComponentName)}
367      */
368     @Override
setMediaSource(@onNull ComponentName componentName, @MediaSourceMode int mode)369     public void setMediaSource(@NonNull ComponentName componentName,
370             @MediaSourceMode int mode) {
371         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
372         if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
373             Log.d(CarLog.TAG_MEDIA, "Changing media source to: " + componentName.getPackageName());
374         }
375         setPrimaryMediaSource(componentName, mode);
376     }
377 
378     /**
379      * @see {@link CarMediaManager#getMediaSource()}
380      */
381     @Override
getMediaSource(@arMediaManager.MediaSourceMode int mode)382     public ComponentName getMediaSource(@CarMediaManager.MediaSourceMode int mode) {
383         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
384         synchronized (mLock) {
385             return mPrimaryMediaComponents[mode];
386         }
387     }
388 
389     /**
390      * @see {@link CarMediaManager#registerMediaSourceListener(MediaSourceChangedListener)}
391      */
392     @Override
registerMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode)393     public void registerMediaSourceListener(ICarMediaSourceListener callback,
394             @MediaSourceMode int mode) {
395         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
396         synchronized (mLock) {
397             mMediaSourceListeners[mode].register(callback);
398         }
399     }
400 
401     /**
402      * @see {@link CarMediaManager#unregisterMediaSourceListener(ICarMediaSourceListener)}
403      */
404     @Override
unregisterMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode)405     public void unregisterMediaSourceListener(ICarMediaSourceListener callback,
406             @MediaSourceMode int mode) {
407         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
408         synchronized (mLock) {
409             mMediaSourceListeners[mode].unregister(callback);
410         }
411     }
412 
413     @Override
getLastMediaSources(@arMediaManager.MediaSourceMode int mode)414     public List<ComponentName> getLastMediaSources(@CarMediaManager.MediaSourceMode int mode) {
415         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
416         String key = getMediaSourceKey(mode);
417         String serialized = mSharedPrefs.getString(key, "");
418         return getComponentNameList(serialized).stream()
419                 .map(name -> ComponentName.unflattenFromString(name)).collect(Collectors.toList());
420     }
421 
422     /** See {@link CarMediaManager#isIndependentPlaybackConfig}. */
423     @Override
424     @TestApi
isIndependentPlaybackConfig()425     public boolean isIndependentPlaybackConfig() {
426         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
427         synchronized (mLock) {
428             return mIndependentPlaybackConfig;
429         }
430     }
431 
432     /** See {@link CarMediaManager#setIndependentPlaybackConfig}. */
433     @Override
434     @TestApi
setIndependentPlaybackConfig(boolean independent)435     public void setIndependentPlaybackConfig(boolean independent) {
436         ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
437         synchronized (mLock) {
438             mIndependentPlaybackConfig = independent;
439         }
440     }
441 
442     // TODO(b/153115826): this method was used to be called from the ICar binder thread, but it's
443     // now called by UserCarService. Currently UserCarServie is calling every listener in one
444     // non-main thread, but it's not clear how the final behavior will be. So, for now it's ok
445     // to post it to mMainHandler, but once b/145689885 is fixed, we might not need it.
onUserUnlock(int userId)446     private void onUserUnlock(int userId) {
447         mMainHandler.post(() -> {
448             // No need to handle system user, non current foreground user.
449             if (userId == UserHandle.USER_SYSTEM
450                     || userId != ActivityManager.getCurrentUser()) {
451                 return;
452             }
453             if (mPendingInit) {
454                 initUser(userId);
455                 mPendingInit = false;
456                 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
457                     Log.d(CarLog.TAG_MEDIA,
458                             "User " + userId + " is now unlocked");
459                 }
460             }
461         });
462     }
463 
updateMediaSessionCallbackForCurrentUser()464     private void updateMediaSessionCallbackForCurrentUser() {
465         if (mSessionsListener != null) {
466             mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);
467         }
468         mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());
469         mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsListener, null,
470                 ActivityManager.getCurrentUser(), null);
471         mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(
472                 null, ActivityManager.getCurrentUser()));
473     }
474 
475     /**
476      * Attempts to stop the current source using MediaController.TransportControls.stop()
477      * This method also unregisters callbacks to the active media controller before calling stop(),
478      * to preserve the PlaybackState before stopping.
479      */
stopAndUnregisterCallback()480     private void stopAndUnregisterCallback() {
481         if (mActiveUserMediaController != null) {
482             mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
483             if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
484                 Log.d(CarLog.TAG_MEDIA, "stopping " + mActiveUserMediaController.getPackageName());
485             }
486             TransportControls controls = mActiveUserMediaController.getTransportControls();
487             if (controls != null) {
488                 controls.stop();
489             } else {
490                 Log.e(CarLog.TAG_MEDIA, "Can't stop playback, transport controls unavailable "
491                         + mActiveUserMediaController.getPackageName());
492             }
493         }
494     }
495 
496     private class SessionChangedListener implements OnActiveSessionsChangedListener {
497         private final int mCurrentUser;
498 
SessionChangedListener(int currentUser)499         SessionChangedListener(int currentUser) {
500             mCurrentUser = currentUser;
501         }
502 
503         @Override
onActiveSessionsChanged(List<MediaController> controllers)504         public void onActiveSessionsChanged(List<MediaController> controllers) {
505             if (ActivityManager.getCurrentUser() != mCurrentUser) {
506                 Log.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);
507                 return;
508             }
509             mMediaSessionUpdater.registerCallbacks(controllers);
510         }
511     }
512 
513     private class MediaControllerCallback extends MediaController.Callback {
514 
515         private final MediaController mMediaController;
516         private int mPreviousPlaybackState;
517 
MediaControllerCallback(MediaController mediaController)518         private MediaControllerCallback(MediaController mediaController) {
519             mMediaController = mediaController;
520             PlaybackState state = mediaController.getPlaybackState();
521             mPreviousPlaybackState = (state == null) ? PlaybackState.STATE_NONE : state.getState();
522         }
523 
register()524         private void register() {
525             mMediaController.registerCallback(this);
526         }
527 
unregister()528         private void unregister() {
529             mMediaController.unregisterCallback(this);
530         }
531 
532         @Override
onPlaybackStateChanged(@ullable PlaybackState state)533         public void onPlaybackStateChanged(@Nullable PlaybackState state) {
534             if (state.getState() == PlaybackState.STATE_PLAYING
535                     && state.getState() != mPreviousPlaybackState) {
536                 ComponentName mediaSource = getMediaSource(mMediaController.getPackageName(),
537                         getClassName(mMediaController));
538                 if (mediaSource != null
539                         && !mediaSource.equals(mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK])
540                         && Log.isLoggable(CarLog.TAG_MEDIA, Log.INFO)) {
541                     Log.i(CarLog.TAG_MEDIA, "Changing media source due to playback state change: "
542                             + mediaSource.flattenToString());
543                 }
544                 setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK);
545             }
546             mPreviousPlaybackState = state.getState();
547         }
548     }
549 
550     private class MediaSessionUpdater {
551         private Map<Token, MediaControllerCallback> mCallbacks = new HashMap<>();
552 
553         /**
554          * Register a {@link MediaControllerCallback} for each given controller. Note that if a
555          * controller was already watched, we don't register a callback again. This prevents an
556          * undesired revert of the primary media source. Callbacks for previously watched
557          * controllers that are not present in the given list are unregistered.
558          */
registerCallbacks(List<MediaController> newControllers)559         private void registerCallbacks(List<MediaController> newControllers) {
560 
561             List<MediaController> additions = new ArrayList<>(newControllers.size());
562             Map<MediaSession.Token, MediaControllerCallback> updatedCallbacks =
563                     new HashMap<>(newControllers.size());
564 
565             for (MediaController controller : newControllers) {
566                 MediaSession.Token token = controller.getSessionToken();
567                 MediaControllerCallback callback = mCallbacks.get(token);
568                 if (callback == null) {
569                     callback = new MediaControllerCallback(controller);
570                     callback.register();
571                     additions.add(controller);
572                 }
573                 updatedCallbacks.put(token, callback);
574             }
575 
576             for (MediaSession.Token token : mCallbacks.keySet()) {
577                 if (!updatedCallbacks.containsKey(token)) {
578                     mCallbacks.get(token).unregister();
579                 }
580             }
581 
582             mCallbacks = updatedCallbacks;
583             updatePrimaryMediaSourceWithCurrentlyPlaying(additions);
584             // If there are no playing media sources, and we don't currently have the controller
585             // for the active source, check the active sessions for a matching controller. If this
586             // is called after a user switch, its possible for a matching controller to already be
587             // active before the user is unlocked, so we check all of the current controllers
588             if (mActiveUserMediaController == null) {
589                 updateActiveMediaController(newControllers);
590             }
591         }
592 
593         /**
594          * Unregister all MediaController callbacks
595          */
unregisterCallbacks()596         private void unregisterCallbacks() {
597             for (Map.Entry<Token, MediaControllerCallback> entry : mCallbacks.entrySet()) {
598                 entry.getValue().unregister();
599             }
600         }
601     }
602 
603     /**
604      * Updates the primary media source, then notifies content observers of the change
605      * Will update both the playback and browse sources if independent playback is not supported
606      */
setPrimaryMediaSource(@onNull ComponentName componentName, @CarMediaManager.MediaSourceMode int mode)607     private void setPrimaryMediaSource(@NonNull ComponentName componentName,
608             @CarMediaManager.MediaSourceMode int mode) {
609         synchronized (mLock) {
610             if (mPrimaryMediaComponents[mode] != null
611                     && mPrimaryMediaComponents[mode].equals((componentName))) {
612                 return;
613             }
614         }
615 
616         if (!mIndependentPlaybackConfig) {
617             setPlaybackMediaSource(componentName);
618             setBrowseMediaSource(componentName);
619         } else if (mode == MEDIA_SOURCE_MODE_PLAYBACK) {
620             setPlaybackMediaSource(componentName);
621         } else if (mode == MEDIA_SOURCE_MODE_BROWSE) {
622             setBrowseMediaSource(componentName);
623         }
624     }
625 
setPlaybackMediaSource(ComponentName playbackMediaSource)626     private void setPlaybackMediaSource(ComponentName playbackMediaSource) {
627         stopAndUnregisterCallback();
628 
629         mActiveUserMediaController = null;
630         synchronized (mLock) {
631             mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = playbackMediaSource;
632         }
633 
634         if (playbackMediaSource != null
635                 && !TextUtils.isEmpty(playbackMediaSource.flattenToString())) {
636             if (!isCurrentUserEphemeral()) {
637                 saveLastMediaSource(playbackMediaSource, MEDIA_SOURCE_MODE_PLAYBACK);
638             }
639             if (playbackMediaSource
640                     .equals(mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_PLAYBACK])) {
641                 mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_PLAYBACK] = null;
642             }
643         }
644 
645         notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK);
646 
647         startMediaConnectorService(shouldStartPlayback(mPlayOnMediaSourceChangedConfig),
648                 new UserHandle(ActivityManager.getCurrentUser()));
649         // Reset current playback state for the new source, in the case that the app is in an error
650         // state (e.g. not signed in). This state will be updated from the app callback registered
651         // below, to make sure mCurrentPlaybackState reflects the current source only.
652         mCurrentPlaybackState = PlaybackState.STATE_NONE;
653         updateActiveMediaController(mMediaSessionManager
654                 .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
655     }
656 
setBrowseMediaSource(ComponentName browseMediaSource)657     private void setBrowseMediaSource(ComponentName browseMediaSource) {
658         synchronized (mLock) {
659             mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = browseMediaSource;
660         }
661 
662         if (browseMediaSource != null && !TextUtils.isEmpty(browseMediaSource.flattenToString())) {
663             if (!isCurrentUserEphemeral()) {
664                 saveLastMediaSource(browseMediaSource, MEDIA_SOURCE_MODE_BROWSE);
665             }
666             if (browseMediaSource
667                     .equals(mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_BROWSE])) {
668                 mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_BROWSE] = null;
669             }
670         }
671 
672         notifyListeners(MEDIA_SOURCE_MODE_BROWSE);
673     }
674 
notifyListeners(@arMediaManager.MediaSourceMode int mode)675     private void notifyListeners(@CarMediaManager.MediaSourceMode int mode) {
676         synchronized (mLock) {
677             int i = mMediaSourceListeners[mode].beginBroadcast();
678             while (i-- > 0) {
679                 try {
680                     ICarMediaSourceListener callback =
681                             mMediaSourceListeners[mode].getBroadcastItem(i);
682                     callback.onMediaSourceChanged(mPrimaryMediaComponents[mode]);
683                 } catch (RemoteException e) {
684                     Log.e(CarLog.TAG_MEDIA, "calling onMediaSourceChanged failed " + e);
685                 }
686             }
687             mMediaSourceListeners[mode].finishBroadcast();
688         }
689     }
690 
691     private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
692         @Override
693         public void onPlaybackStateChanged(PlaybackState state) {
694             if (!isCurrentUserEphemeral()) {
695                 savePlaybackState(state);
696             }
697         }
698     };
699 
700     /**
701      * Finds the currently playing media source, then updates the active source if the component
702      * name is different.
703      */
updatePrimaryMediaSourceWithCurrentlyPlaying( List<MediaController> controllers)704     private void updatePrimaryMediaSourceWithCurrentlyPlaying(
705             List<MediaController> controllers) {
706         for (MediaController controller : controllers) {
707             if (controller.getPlaybackState() != null
708                     && controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING) {
709                 String newPackageName = controller.getPackageName();
710                 String newClassName = getClassName(controller);
711                 if (!matchPrimaryMediaSource(newPackageName, newClassName,
712                         MEDIA_SOURCE_MODE_PLAYBACK)) {
713                     ComponentName mediaSource = getMediaSource(newPackageName, newClassName);
714                     if (Log.isLoggable(CarLog.TAG_MEDIA, Log.INFO)) {
715                         if (mediaSource != null) {
716                             Log.i(CarLog.TAG_MEDIA,
717                                     "MediaController changed, updating media source to: "
718                                             + mediaSource.flattenToString());
719                         } else {
720                             // Some apps, like Chrome, have a MediaSession but no
721                             // MediaBrowseService. Media Center doesn't consider such apps as
722                             // valid media sources.
723                             Log.i(CarLog.TAG_MEDIA,
724                                     "MediaController changed, but no media browse service found "
725                                             + "in package: " + newPackageName);
726                         }
727                     }
728                     setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK);
729                 }
730                 return;
731             }
732         }
733     }
734 
matchPrimaryMediaSource(@onNull String newPackageName, @NonNull String newClassName, @CarMediaManager.MediaSourceMode int mode)735     private boolean matchPrimaryMediaSource(@NonNull String newPackageName,
736             @NonNull String newClassName, @CarMediaManager.MediaSourceMode int mode) {
737         synchronized (mLock) {
738             if (mPrimaryMediaComponents[mode] != null
739                     && mPrimaryMediaComponents[mode].getPackageName().equals(newPackageName)) {
740                 // If the class name of currently active source is not specified, only checks
741                 // package name; otherwise checks both package name and class name.
742                 if (TextUtils.isEmpty(newClassName)) {
743                     return true;
744                 } else {
745                     return newClassName.equals(mPrimaryMediaComponents[mode].getClassName());
746                 }
747             }
748         }
749         return false;
750     }
751 
752     /**
753      * Returns {@code true} if the provided component has a valid {@link MediaBrowseService}.
754      */
755     @VisibleForTesting
isMediaService(@onNull ComponentName componentName)756     public boolean isMediaService(@NonNull ComponentName componentName) {
757         return getMediaService(componentName) != null;
758     }
759 
760     /*
761      * Gets the media service that matches the componentName for the current foreground user.
762      */
getMediaService(@onNull ComponentName componentName)763     private ComponentName getMediaService(@NonNull ComponentName componentName) {
764         String packageName = componentName.getPackageName();
765         String className = componentName.getClassName();
766 
767         PackageManager packageManager = mContext.getPackageManager();
768         Intent mediaIntent = new Intent();
769         mediaIntent.setPackage(packageName);
770         mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
771         List<ResolveInfo> mediaServices = packageManager.queryIntentServicesAsUser(mediaIntent,
772                 PackageManager.GET_RESOLVED_FILTER, ActivityManager.getCurrentUser());
773 
774         for (ResolveInfo service : mediaServices) {
775             String serviceName = service.serviceInfo.name;
776             if (!TextUtils.isEmpty(serviceName)
777                     // If className is not specified, returns the first service in the package;
778                     // otherwise returns the matched service.
779                     // TODO(b/136274456): find a proper way to handle the case where there are
780                     //  multiple services and the className is not specified.
781 
782                     && (TextUtils.isEmpty(className) || serviceName.equals(className))) {
783                 return new ComponentName(packageName, serviceName);
784             }
785         }
786 
787         if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
788             Log.d(CarLog.TAG_MEDIA, "No MediaBrowseService with ComponentName: "
789                     + componentName.flattenToString());
790         }
791         return null;
792     }
793 
794     /*
795      * Gets the component name of the media service.
796      */
797     @Nullable
getMediaSource(@onNull String packageName, @NonNull String className)798     private ComponentName getMediaSource(@NonNull String packageName, @NonNull String className) {
799         return getMediaService(new ComponentName(packageName, className));
800     }
801 
saveLastMediaSource(@onNull ComponentName component, int mode)802     private void saveLastMediaSource(@NonNull ComponentName component, int mode) {
803         if (!sharedPrefsInitialized()) {
804             return;
805         }
806         String componentName = component.flattenToString();
807         String key = getMediaSourceKey(mode);
808         String serialized = mSharedPrefs.getString(key, null);
809         if (serialized == null) {
810             mSharedPrefs.edit().putString(key, componentName).apply();
811         } else {
812             Deque<String> componentNames = new ArrayDeque<>(getComponentNameList(serialized));
813             componentNames.remove(componentName);
814             componentNames.addFirst(componentName);
815             mSharedPrefs.edit().putString(key, serializeComponentNameList(componentNames)).apply();
816         }
817     }
818 
getLastMediaSource(int mode)819     private @NonNull ComponentName getLastMediaSource(int mode) {
820         if (sharedPrefsInitialized()) {
821             String key = getMediaSourceKey(mode);
822             String serialized = mSharedPrefs.getString(key, "");
823             if (!TextUtils.isEmpty(serialized)) {
824                 for (String name : getComponentNameList(serialized)) {
825                     ComponentName componentName = ComponentName.unflattenFromString(name);
826                     if (isMediaService(componentName)) {
827                         return componentName;
828                     }
829                 }
830             }
831         }
832         return getDefaultMediaSource();
833     }
834 
getDefaultMediaSource()835     private ComponentName getDefaultMediaSource() {
836         String defaultMediaSource = mContext.getString(R.string.config_defaultMediaSource);
837         ComponentName defaultComponent = ComponentName.unflattenFromString(defaultMediaSource);
838         if (isMediaService(defaultComponent)) {
839             return defaultComponent;
840         }
841         return null;
842     }
843 
serializeComponentNameList(Deque<String> componentNames)844     private String serializeComponentNameList(Deque<String> componentNames) {
845         return componentNames.stream().collect(Collectors.joining(COMPONENT_NAME_SEPARATOR));
846     }
847 
getComponentNameList(@onNull String serialized)848     private List<String> getComponentNameList(@NonNull String serialized) {
849         String[] componentNames = serialized.split(COMPONENT_NAME_SEPARATOR);
850         return (Arrays.asList(componentNames));
851     }
852 
savePlaybackState(PlaybackState playbackState)853     private void savePlaybackState(PlaybackState playbackState) {
854         if (!sharedPrefsInitialized()) {
855             return;
856         }
857         int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE;
858         mCurrentPlaybackState = state;
859         String key = getPlaybackStateKey();
860         mSharedPrefs.edit().putInt(key, state).apply();
861     }
862 
863     /**
864      * Builds a string key for saving the playback state for a specific media source (and user)
865      */
getPlaybackStateKey()866     private String getPlaybackStateKey() {
867         synchronized (mLock) {
868             return PLAYBACK_STATE_KEY + ActivityManager.getCurrentUser()
869                     + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? ""
870                     : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString());
871         }
872     }
873 
getMediaSourceKey(int mode)874     private String getMediaSourceKey(int mode) {
875         return SOURCE_KEY + mode + SOURCE_KEY_SEPARATOR + ActivityManager.getCurrentUser();
876     }
877 
878     /**
879      * Updates active media controller from the list that has the same component name as the primary
880      * media component. Clears callback and resets media controller to null if not found.
881      */
updateActiveMediaController(List<MediaController> mediaControllers)882     private void updateActiveMediaController(List<MediaController> mediaControllers) {
883         if (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null) {
884             return;
885         }
886         if (mActiveUserMediaController != null) {
887             mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
888             mActiveUserMediaController = null;
889         }
890         for (MediaController controller : mediaControllers) {
891             if (matchPrimaryMediaSource(controller.getPackageName(), getClassName(controller),
892                     MEDIA_SOURCE_MODE_PLAYBACK)) {
893                 mActiveUserMediaController = controller;
894                 PlaybackState state = mActiveUserMediaController.getPlaybackState();
895                 if (!isCurrentUserEphemeral()) {
896                     savePlaybackState(state);
897                 }
898                 // Specify Handler to receive callbacks on, to avoid defaulting to the calling
899                 // thread; this method can be called from the MediaSessionManager callback.
900                 // Using the version of this method without passing a handler causes a
901                 // RuntimeException for failing to create a Handler.
902                 mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler);
903                 return;
904             }
905         }
906     }
907 
908     /**
909      * Returns whether we should autoplay the current media source
910      */
shouldStartPlayback(int config)911     private boolean shouldStartPlayback(int config) {
912         switch (config) {
913             case AUTOPLAY_CONFIG_NEVER:
914                 return false;
915             case AUTOPLAY_CONFIG_ALWAYS:
916                 return true;
917             case AUTOPLAY_CONFIG_RETAIN_PER_SOURCE:
918                 if (!sharedPrefsInitialized()) {
919                     return false;
920                 }
921                 return mSharedPrefs.getInt(getPlaybackStateKey(), PlaybackState.STATE_NONE)
922                         == PlaybackState.STATE_PLAYING;
923             case AUTOPLAY_CONFIG_RETAIN_PREVIOUS:
924                 return mCurrentPlaybackState == PlaybackState.STATE_PLAYING;
925             default:
926                 Log.e(CarLog.TAG_MEDIA, "Unsupported playback configuration: " + config);
927                 return false;
928         }
929     }
930 
931     @NonNull
getClassName(@onNull MediaController controller)932     private static String getClassName(@NonNull MediaController controller) {
933         Bundle sessionExtras = controller.getExtras();
934         String value =
935                 sessionExtras == null ? "" : sessionExtras.getString(
936                         Car.CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION);
937         return value != null ? value : "";
938     }
939 }
940