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