• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.media;
17 
18 import android.app.SearchManager;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.SharedPreferences;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.content.pm.ServiceInfo;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.media.browse.MediaBrowser;
30 import android.media.session.MediaController;
31 import android.media.session.MediaSession;
32 import android.media.session.PlaybackState;
33 import android.os.Bundle;
34 import android.service.media.MediaBrowserService;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 import java.lang.ref.WeakReference;
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Manages which media app we should connect to. The manager also retrieves various attributes
44  * from the media app and share among different components in GearHead media app.
45  */
46 public class MediaManager {
47     private static final String TAG = "GH.MediaManager";
48     private static final String PREFS_FILE_NAME = "MediaClientManager.Preferences";
49     /** The package of the most recently used media component **/
50     private static final String PREFS_KEY_PACKAGE = "media_package";
51     /** The class of the most recently used media class **/
52     private static final String PREFS_KEY_CLASS = "media_class";
53     /** Third-party defined application theme to use **/
54     private static final String THEME_META_DATA_NAME = "com.google.android.gms.car.application.theme";
55 
56     public static final String KEY_MEDIA_COMPONENT = "media_component";
57     /** Intent extra specifying the package with the MediaBrowser **/
58     public static final String KEY_MEDIA_PACKAGE = "media_package";
59     /** Intent extra specifying the MediaBrowserService **/
60     public static final String KEY_MEDIA_CLASS = "media_class";
61 
62     /**
63      * Flag for when GSA is not 100% confident on the query and therefore, the result in the
64      * {@link #KEY_MEDIA_PACKAGE_FROM_GSA} should be ignored.
65      */
66     private static final String KEY_IGNORE_ORIGINAL_PKG =
67             "com.google.android.projection.gearhead.ignore_original_pkg";
68 
69     /**
70      * Intent extra specifying the package name of the media app that should handle
71      * {@link android.provider.MediaStore#INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH}. This must match
72      * KEY_PACKAGE defined in ProjectionIntentStarter in GSA.
73      */
74     public static final String KEY_MEDIA_PACKAGE_FROM_GSA =
75             "android.intent.action.original_package";
76 
77     private static final String GOOGLE_PLAY_MUSIC_PACKAGE = "com.google.android.music";
78     // Extras along with the Knowledge Graph that are not meant to be seen by external apps.
79     private static final String[] INTERNAL_EXTRAS = {"KEY_LAUNCH_HANDOVER_UNDERNEATH",
80             "com.google.android.projection.gearhead.ignore_original_pkg"};
81 
82     private static final Intent MEDIA_BROWSER_INTENT =
83             new Intent(MediaBrowserService.SERVICE_INTERFACE);
84     private static MediaManager sInstance;
85 
86     private final MediaController.Callback mMediaControllerCallback =
87             new MediaManagerCallback(this);
88     private final MediaBrowser.ConnectionCallback mMediaBrowserConnectionCallback =
89             new MediaManagerConnectionCallback(this);
90 
91     public interface Listener {
onMediaAppChanged(ComponentName componentName)92         void onMediaAppChanged(ComponentName componentName);
93 
94         /**
95          * Called when we want to show a message on playback screen.
96          * @param msg if null, dismiss any previous message and
97          *            restore the track title and subtitle.
98          */
onStatusMessageChanged(String msg)99         void onStatusMessageChanged(String msg);
100     }
101 
102     /**
103      * An adapter interface to abstract the specifics of how media services are queried. This allows
104      * for Vanagon to query for allowed media services without the need to connect to carClientApi.
105      */
106     public interface ServiceAdapter {
queryAllowedServices(Intent providerIntent)107         List<ResolveInfo> queryAllowedServices(Intent providerIntent);
108     }
109 
110     private int mPrimaryColor;
111     private int mPrimaryColorDark;
112     private int mAccentColor;
113     private CharSequence mName;
114 
115     private final Context mContext;
116     private final List<Listener> mListeners = new ArrayList<>();
117 
118     private ServiceAdapter mServiceAdapter;
119     private Intent mPendingSearchIntent;
120 
121     private MediaController mController;
122     private MediaBrowser mBrowser;
123     private ComponentName mCurrentComponent;
124     private PendingMsg mPendingMsg;
125 
getInstance(Context context)126     public synchronized static MediaManager getInstance(Context context) {
127         if (sInstance == null) {
128             sInstance = new MediaManager(context.getApplicationContext());
129         }
130         return sInstance;
131     }
132 
MediaManager(Context context)133     private MediaManager(Context context) {
134         mContext = context;
135 
136         // Set some sane default values for the attributes
137         mName = "";
138         int color = context.getResources().getColor(android.R.color.background_dark);
139         mPrimaryColor = color;
140         mAccentColor = color;
141         mPrimaryColorDark = color;
142     }
143 
144     /**
145      * Returns the default component used to load media.
146      */
getDefaultComponent(ServiceAdapter serviceAdapter)147     public ComponentName getDefaultComponent(ServiceAdapter serviceAdapter) {
148         SharedPreferences prefs = mContext
149                 .getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE);
150         String packageName = prefs.getString(PREFS_KEY_PACKAGE, null);
151         String className = prefs.getString(PREFS_KEY_CLASS, null);
152         final Intent providerIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
153         List<ResolveInfo> mediaApps = serviceAdapter.queryAllowedServices(providerIntent);
154 
155         // check if the previous component we connected to is still valid.
156         if (packageName != null && className != null) {
157             boolean componentValid = false;
158             for (ResolveInfo info : mediaApps) {
159                 if (info.serviceInfo.packageName.equals(packageName)
160                         && info.serviceInfo.name.equals(className)) {
161                     componentValid = true;
162                 }
163             }
164             // if not valid, null it and we will bring up the lens switcher or connect to another
165             // app (this may happen when the app has been uninstalled)
166             if (!componentValid) {
167                 packageName = null;
168                 className = null;
169             }
170         }
171 
172         // If there are no apps used before or previous app is not valid,
173         // try to connect to a supported media app.
174         if (packageName == null || className == null) {
175             // Only one app installed, connect to it.
176             if (mediaApps.size() == 1) {
177                 ResolveInfo info = mediaApps.get(0);
178                 packageName = info.serviceInfo.packageName;
179                 className = info.serviceInfo.name;
180             } else {
181                 // there are '0' or >1 media apps installed; don't know what to run
182                 return null;
183             }
184         }
185         return new ComponentName(packageName, className);
186     }
187 
188     /**
189      * Connects to the most recently used media app if it exists and return true.
190      * Otherwise check the number of supported media apps installed,
191      * if only one installed, connect to it return true. Otherwise return false.
192      */
connectToMostRecentMediaComponent(ServiceAdapter serviceAdapter)193     public boolean connectToMostRecentMediaComponent(ServiceAdapter serviceAdapter) {
194         ComponentName component = getDefaultComponent(serviceAdapter);
195         if (component != null) {
196             setMediaClientComponent(serviceAdapter, component);
197             return true;
198         }
199         return false;
200     }
201 
getCurrentComponent()202     public ComponentName getCurrentComponent() {
203         return mCurrentComponent;
204     }
205 
setMediaClientComponent(ComponentName component)206     public void setMediaClientComponent(ComponentName component) {
207         setMediaClientComponent(null, component);
208     }
209 
210     /**
211      * Change the media component. This will connect to a {@link android.media.browse.MediaBrowser} if necessary.
212      * All registered listener will be updated with the new component.
213      */
setMediaClientComponent(ServiceAdapter serviceAdapter, ComponentName component)214     public void setMediaClientComponent(ServiceAdapter serviceAdapter, ComponentName component) {
215         if (Log.isLoggable(TAG, Log.VERBOSE)) {
216             Log.v(TAG, "setMediaClientComponent(), "
217                     + "component: " + (component == null ? "<< NULL >>" : component.toString()));
218         }
219 
220         if (component == null) {
221             return;
222         }
223 
224         // mController will be set to null if previously connected media session has crashed.
225         if (mCurrentComponent != null && mCurrentComponent.equals(component)
226                 && mController != null) {
227             if (Log.isLoggable(TAG, Log.DEBUG)) {
228                 Log.d(TAG, "Already connected to " + component.toString());
229             }
230             return;
231         }
232 
233         mCurrentComponent = component;
234         mServiceAdapter = serviceAdapter;
235         disconnectCurrentBrowser();
236         updateClientPackageAttributes(mCurrentComponent);
237 
238         if (mController != null) {
239             mController.unregisterCallback(mMediaControllerCallback);
240             mController = null;
241         }
242         mBrowser = new MediaBrowser(mContext, component, mMediaBrowserConnectionCallback, null);
243         if (Log.isLoggable(TAG, Log.DEBUG)) {
244             Log.d(TAG, "Connecting to " + component.toString());
245         }
246         mBrowser.connect();
247 
248         writeComponentToPrefs(component);
249 
250         ArrayList<Listener> temp = new ArrayList<Listener>(mListeners);
251         for (Listener listener : temp) {
252             listener.onMediaAppChanged(mCurrentComponent);
253         }
254     }
255 
256     /**
257      * Processes the search intent using the current media app. If it's not connected yet, store it
258      * in the {@code mPendingSearchIntent} and process it when the app is connected.
259      *
260      * @param intent The intent containing the query and
261      *            MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH action
262      */
processSearchIntent(Intent intent)263     public void processSearchIntent(Intent intent) {
264         if (Log.isLoggable(TAG, Log.VERBOSE)) {
265             Log.v(TAG, "processSearchIntent(), query: "
266                     + (intent == null ? "<< NULL >>" : intent.getStringExtra(SearchManager.QUERY)));
267         }
268         if (intent == null) {
269             return;
270         }
271         mPendingSearchIntent = intent;
272 
273         String mediaPackageName;
274         if (intent.getBooleanExtra(KEY_IGNORE_ORIGINAL_PKG, false)) {
275             if (Log.isLoggable(TAG, Log.DEBUG)) {
276                 Log.d(TAG, "Ignoring package from gsa and falling back to default media app");
277             }
278             mediaPackageName = null;
279         } else if (intent.hasExtra(KEY_MEDIA_PACKAGE_FROM_GSA)) {
280             // Legacy way of piping through the media app package.
281             mediaPackageName = intent.getStringExtra(KEY_MEDIA_PACKAGE_FROM_GSA);
282             if (Log.isLoggable(TAG, Log.DEBUG)) {
283                 Log.d(TAG, "Package from extras: " + mediaPackageName);
284             }
285         } else {
286             mediaPackageName = intent.getPackage();
287             if (Log.isLoggable(TAG, Log.DEBUG)) {
288                 Log.d(TAG, "Package from getPackage(): " + mediaPackageName);
289             }
290         }
291 
292         if (mediaPackageName != null && mCurrentComponent != null
293                 && !mediaPackageName.equals(mCurrentComponent.getPackageName())) {
294             final ComponentName componentName =
295                     getMediaBrowserComponent(mServiceAdapter, mediaPackageName);
296             if (componentName == null) {
297                 Log.w(TAG, "There are no matching media app to handle intent: " + intent);
298                 return;
299             }
300             setMediaClientComponent(mServiceAdapter, componentName);
301             // It's safe to return here as pending search intent will be processed
302             // when newly created media controller for the new media component is connected.
303             return;
304         }
305 
306         String query = mPendingSearchIntent.getStringExtra(SearchManager.QUERY);
307         if (mController != null) {
308             mController.getTransportControls().pause();
309             mPendingMsg = new PendingMsg(PendingMsg.STATUS_UPDATE,
310                     mContext.getResources().getString(R.string.loading));
311             notifyStatusMessage(mPendingMsg.mMsg);
312             Bundle extras = mPendingSearchIntent.getExtras();
313             // Remove two extras that are not meant to be seen by external apps.
314             if (!GOOGLE_PLAY_MUSIC_PACKAGE.equals(mediaPackageName)) {
315                 for (String key : INTERNAL_EXTRAS) {
316                     extras.remove(key);
317                 }
318             }
319             mController.getTransportControls().playFromSearch(query, extras);
320             mPendingSearchIntent = null;
321         } else {
322             if (Log.isLoggable(TAG, Log.DEBUG)) {
323                 Log.d(TAG, "No controller for search intent; save it for later");
324             }
325         }
326     }
327 
328 
getMediaBrowserComponent(ServiceAdapter serviceAdapter, final String packageName)329     private ComponentName getMediaBrowserComponent(ServiceAdapter serviceAdapter,
330             final String packageName) {
331         List<ResolveInfo> queryResults = serviceAdapter.queryAllowedServices(MEDIA_BROWSER_INTENT);
332         if (queryResults != null) {
333             for (int i = 0, N = queryResults.size(); i < N; ++i) {
334                 final ResolveInfo ri = queryResults.get(i);
335                 if (ri != null && ri.serviceInfo != null
336                         && ri.serviceInfo.packageName.equals(packageName)) {
337                     return new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name);
338                 }
339             }
340         }
341         return null;
342     }
343 
344     /**
345      * Add a listener to get media app changes.
346      * Your listener will be called with the initial values when the listener is added.
347      */
addListener(Listener listener)348     public void addListener(Listener listener) {
349         mListeners.add(listener);
350         if (Log.isLoggable(TAG, Log.VERBOSE)) {
351             Log.v(TAG, "addListener(); count: " + mListeners.size());
352         }
353 
354         if (mCurrentComponent != null) {
355             listener.onMediaAppChanged(mCurrentComponent);
356         }
357 
358         if (mPendingMsg != null) {
359             listener.onStatusMessageChanged(mPendingMsg.mMsg);
360         }
361     }
362 
removeListener(Listener listener)363     public void removeListener(Listener listener) {
364         mListeners.remove(listener);
365 
366         if (Log.isLoggable(TAG, Log.VERBOSE)) {
367             Log.v(TAG, "removeListener(); count: " + mListeners.size());
368         }
369 
370         if (mListeners.size() == 0) {
371             if (Log.isLoggable(TAG, Log.DEBUG)) {
372                 Log.d(TAG, "no manager listeners; destroy manager instance");
373             }
374 
375             synchronized (MediaManager.class) {
376                 sInstance = null;
377             }
378 
379             if (mBrowser != null) {
380                 mBrowser.disconnect();
381             }
382         }
383     }
384 
getMediaClientName()385     public CharSequence getMediaClientName() {
386         return mName;
387     }
388 
getMediaClientPrimaryColor()389     public int getMediaClientPrimaryColor() {
390         return mPrimaryColor;
391     }
392 
getMediaClientPrimaryColorDark()393     public int getMediaClientPrimaryColorDark() {
394         return mPrimaryColorDark;
395     }
396 
getMediaClientAccentColor()397     public int getMediaClientAccentColor() {
398         return mAccentColor;
399     }
400 
writeComponentToPrefs(ComponentName componentName)401     private void writeComponentToPrefs(ComponentName componentName) {
402         // Store selected media service to shared preference.
403         SharedPreferences prefs = mContext
404                 .getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE);
405         SharedPreferences.Editor editor = prefs.edit();
406         editor.putString(PREFS_KEY_PACKAGE, componentName.getPackageName());
407         editor.putString(PREFS_KEY_CLASS, componentName.getClassName());
408         editor.apply();
409     }
410 
411     /**
412      * Disconnect from the current media browser service if any, and notify the listeners.
413      */
disconnectCurrentBrowser()414     private void disconnectCurrentBrowser() {
415         if (mBrowser != null) {
416             mBrowser.disconnect();
417             mBrowser = null;
418         }
419     }
420 
updateClientPackageAttributes(ComponentName componentName)421     private void updateClientPackageAttributes(ComponentName componentName) {
422         TypedArray ta = null;
423         try {
424             String packageName = componentName.getPackageName();
425             ApplicationInfo applicationInfo =
426                     mContext.getPackageManager().getApplicationInfo(packageName,
427                             PackageManager.GET_META_DATA);
428             ServiceInfo serviceInfo = mContext.getPackageManager().getServiceInfo(
429                     componentName, PackageManager.GET_META_DATA);
430 
431             // Get the proper app name, check service label, then application label.
432             CharSequence name = "";
433             if (serviceInfo.labelRes != 0) {
434                 name = serviceInfo.loadLabel(mContext.getPackageManager());
435             } else if (applicationInfo.labelRes != 0) {
436                 name = applicationInfo.loadLabel(mContext.getPackageManager());
437             }
438             if (TextUtils.isEmpty(name)) {
439                 name = mContext.getResources().getString(R.string.unknown_media_provider_name);
440             }
441             mName = name;
442 
443             // Get the proper theme, check theme for service, then application.
444             int appTheme = 0;
445             if (serviceInfo.metaData != null) {
446                 appTheme = serviceInfo.metaData.getInt(THEME_META_DATA_NAME);
447             }
448             if (appTheme == 0 && applicationInfo.metaData != null) {
449                 appTheme = applicationInfo.metaData.getInt(THEME_META_DATA_NAME);
450             }
451             if (appTheme == 0) {
452                 appTheme = applicationInfo.theme;
453             }
454 
455             Context packageContext = mContext.createPackageContext(packageName, 0);
456             packageContext.setTheme(appTheme);
457             Resources.Theme theme = packageContext.getTheme();
458             ta = theme.obtainStyledAttributes(new int[] {
459                     android.R.attr.colorPrimary,
460                     android.R.attr.colorAccent,
461                     android.R.attr.colorPrimaryDark
462             });
463             int defaultColor =
464                     mContext.getResources().getColor(android.R.color.background_dark);
465             mPrimaryColor = ta.getColor(0, defaultColor);
466             mAccentColor = ta.getColor(1, defaultColor);
467             mPrimaryColorDark = ta.getColor(2, defaultColor);
468         } catch (PackageManager.NameNotFoundException e) {
469             Log.e(TAG, "Unable to update media client package attributes.", e);
470         } finally {
471             if (ta != null) {
472                 ta.recycle();
473             }
474         }
475     }
476 
notifyStatusMessage(String str)477     private void notifyStatusMessage(String str) {
478         for (Listener l : mListeners) {
479             l.onStatusMessageChanged(str);
480         }
481     }
482 
doPlaybackStateChanged(PlaybackState playbackState)483     private void doPlaybackStateChanged(PlaybackState playbackState) {
484         // Display error message in MediaPlaybackFragment.
485         if (mPendingMsg == null) {
486             return;
487         }
488         // Dismiss the error msg if any,
489         // and dismiss status update msg if the state is now playing
490         if ((mPendingMsg.mType == PendingMsg.ERROR) ||
491                 (playbackState.getState() == PlaybackState.STATE_PLAYING
492                         && mPendingMsg.mType == PendingMsg.STATUS_UPDATE)) {
493             mPendingMsg = null;
494             notifyStatusMessage(null);
495         }
496     }
497 
doOnSessionDestroyed()498     private void doOnSessionDestroyed() {
499         if (Log.isLoggable(TAG, Log.VERBOSE)) {
500             Log.v(TAG, "Media session destroyed");
501         }
502         if (mController != null) {
503             mController.unregisterCallback(mMediaControllerCallback);
504         }
505         mController = null;
506         mServiceAdapter = null;
507     }
508 
doOnConnected()509     private void doOnConnected() {
510         // existing mController has been disconnected before we call MediaBrowser.connect()
511         MediaSession.Token token = mBrowser.getSessionToken();
512         if (token == null) {
513             Log.e(TAG, "Media session token is null");
514             return;
515         }
516         mController = new MediaController(mContext, token);
517         mController.registerCallback(mMediaControllerCallback);
518         processSearchIntent(mPendingSearchIntent);
519     }
520 
doOnConnectionFailed()521     private void doOnConnectionFailed() {
522         Log.w(TAG, "Media browser connection FAILED!");
523         // disconnect anyway to make sure we get into a sanity state
524         mBrowser.disconnect();
525         mBrowser = null;
526     }
527 
528     private static class PendingMsg {
529         public static final int ERROR = 0;
530         public static final int STATUS_UPDATE = 1;
531 
532         public int mType;
533         public String mMsg;
PendingMsg(int type, String msg)534         public PendingMsg(int type, String msg) {
535             mType = type;
536             mMsg = msg;
537         }
538     }
539 
540     private static class MediaManagerCallback extends MediaController.Callback {
541         private final WeakReference<MediaManager> mWeakCallback;
542 
MediaManagerCallback(MediaManager callback)543         MediaManagerCallback(MediaManager callback) {
544             mWeakCallback = new WeakReference<>(callback);
545         }
546 
547         @Override
onPlaybackStateChanged(PlaybackState playbackState)548         public void onPlaybackStateChanged(PlaybackState playbackState) {
549             MediaManager callback = mWeakCallback.get();
550             if (callback == null) {
551                 return;
552             }
553             callback.doPlaybackStateChanged(playbackState);
554         }
555 
556         @Override
onSessionDestroyed()557         public void onSessionDestroyed() {
558             MediaManager callback = mWeakCallback.get();
559             if (callback == null) {
560                 return;
561             }
562             callback.doOnSessionDestroyed();
563         }
564     }
565 
566     private static class MediaManagerConnectionCallback extends MediaBrowser.ConnectionCallback {
567         private final WeakReference<MediaManager> mWeakCallback;
568 
MediaManagerConnectionCallback(MediaManager callback)569         private MediaManagerConnectionCallback(MediaManager callback) {
570             mWeakCallback = new WeakReference<>(callback);
571         }
572 
573         @Override
onConnected()574         public void onConnected() {
575             MediaManager callback = mWeakCallback.get();
576             if (callback == null) {
577                 return;
578             }
579             callback.doOnConnected();
580         }
581 
582         @Override
onConnectionSuspended()583         public void onConnectionSuspended() {}
584 
585         @Override
onConnectionFailed()586         public void onConnectionFailed() {
587             MediaManager callback = mWeakCallback.get();
588             if (callback == null) {
589                 return;
590             }
591             callback.doOnConnectionFailed();
592         }
593     }
594 }
595