Home | History | Annotate | Download | only in media
      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 {
     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          */
    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 {
    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 
    129     public synchronized static MediaManager getInstance(Context context) {
    130         if (sInstance == null) {
    131             sInstance = new MediaManager(context.getApplicationContext());
    132         }
    133         return sInstance;
    134     }
    135 
    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      */
    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      */
    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 
    205     public ComponentName getCurrentComponent() {
    206         return mCurrentComponent;
    207     }
    208 
    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      */
    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      */
    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 
    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      */
    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 
    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 
    388     public CharSequence getMediaClientName() {
    389         return mName;
    390     }
    391 
    392     public int getMediaClientPrimaryColor() {
    393         return mPrimaryColor;
    394     }
    395 
    396     public int getMediaClientPrimaryColorDark() {
    397         return mPrimaryColorDark;
    398     }
    399 
    400     public int getMediaClientAccentColor() {
    401         return mAccentColor;
    402     }
    403 
    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      */
    417     private void disconnectCurrentBrowser() {
    418         if (mBrowser != null) {
    419             mBrowser.disconnect();
    420             mBrowser = null;
    421         }
    422     }
    423 
    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 
    480     private void notifyStatusMessage(String str) {
    481         for (Listener l : mListeners) {
    482             l.onStatusMessageChanged(str);
    483         }
    484     }
    485 
    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 
    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 
    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 
    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;
    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 
    546         MediaManagerCallback(MediaManager callback) {
    547             mWeakCallback = new WeakReference<>(callback);
    548         }
    549 
    550         @Override
    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
    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 
    572         private MediaManagerConnectionCallback(MediaManager callback) {
    573             mWeakCallback = new WeakReference<>(callback);
    574         }
    575 
    576         @Override
    577         public void onConnected() {
    578             MediaManager callback = mWeakCallback.get();
    579             if (callback == null) {
    580                 return;
    581             }
    582             callback.doOnConnected();
    583         }
    584 
    585         @Override
    586         public void onConnectionSuspended() {}
    587 
    588         @Override
    589         public void onConnectionFailed() {
    590             MediaManager callback = mWeakCallback.get();
    591             if (callback == null) {
    592                 return;
    593             }
    594             callback.doOnConnectionFailed();
    595         }
    596     }
    597 }
    598