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