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