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