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 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 {
     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          */
     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 {
    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 
    126     public synchronized static MediaManager getInstance(Context context) {
    127         if (sInstance == null) {
    128             sInstance = new MediaManager(context.getApplicationContext());
    129         }
    130         return sInstance;
    131     }
    132 
    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      */
    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      */
    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 
    202     public ComponentName getCurrentComponent() {
    203         return mCurrentComponent;
    204     }
    205 
    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      */
    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      */
    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 
    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      */
    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 
    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 
    385     public CharSequence getMediaClientName() {
    386         return mName;
    387     }
    388 
    389     public int getMediaClientPrimaryColor() {
    390         return mPrimaryColor;
    391     }
    392 
    393     public int getMediaClientPrimaryColorDark() {
    394         return mPrimaryColorDark;
    395     }
    396 
    397     public int getMediaClientAccentColor() {
    398         return mAccentColor;
    399     }
    400 
    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      */
    414     private void disconnectCurrentBrowser() {
    415         if (mBrowser != null) {
    416             mBrowser.disconnect();
    417             mBrowser = null;
    418         }
    419     }
    420 
    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 
    477     private void notifyStatusMessage(String str) {
    478         for (Listener l : mListeners) {
    479             l.onStatusMessageChanged(str);
    480         }
    481     }
    482 
    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 
    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 
    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 
    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;
    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 
    543         MediaManagerCallback(MediaManager callback) {
    544             mWeakCallback = new WeakReference<>(callback);
    545         }
    546 
    547         @Override
    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
    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 
    569         private MediaManagerConnectionCallback(MediaManager callback) {
    570             mWeakCallback = new WeakReference<>(callback);
    571         }
    572 
    573         @Override
    574         public void onConnected() {
    575             MediaManager callback = mWeakCallback.get();
    576             if (callback == null) {
    577                 return;
    578             }
    579             callback.doOnConnected();
    580         }
    581 
    582         @Override
    583         public void onConnectionSuspended() {}
    584 
    585         @Override
    586         public void onConnectionFailed() {
    587             MediaManager callback = mWeakCallback.get();
    588             if (callback == null) {
    589                 return;
    590             }
    591             callback.doOnConnectionFailed();
    592         }
    593     }
    594 }
    595