1 /* 2 * Copyright 2018 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 17 package com.android.car.media.common.source; 18 19 import static com.android.car.apps.common.util.CarAppsDebugUtils.idHash; 20 import static com.android.car.arch.common.LiveDataFunctions.dataOf; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.Application; 25 import android.car.Car; 26 import android.car.CarNotConnectedException; 27 import android.car.media.CarMediaManager; 28 import android.content.ComponentName; 29 import android.content.ContentValues; 30 import android.media.session.MediaController; 31 import android.os.Handler; 32 import android.os.RemoteException; 33 import android.support.v4.media.MediaBrowserCompat; 34 import android.support.v4.media.session.MediaControllerCompat; 35 import android.support.v4.media.session.MediaSessionCompat; 36 import android.util.Log; 37 38 import androidx.annotation.VisibleForTesting; 39 import androidx.lifecycle.AndroidViewModel; 40 import androidx.lifecycle.LiveData; 41 import androidx.lifecycle.MutableLiveData; 42 43 import com.android.car.media.common.MediaConstants; 44 45 import java.util.Objects; 46 47 /** 48 * Contains observable data needed for displaying playback and browse UI. 49 * MediaSourceViewModel is a singleton tied to the application to provide a single source of truth. 50 */ 51 public class MediaSourceViewModel extends AndroidViewModel { 52 private static final String TAG = "MediaSourceViewModel"; 53 54 private static MediaSourceViewModel sInstance; 55 private final Car mCar; 56 private CarMediaManager mCarMediaManager; 57 58 // Primary media source. 59 private final MutableLiveData<MediaSource> mPrimaryMediaSource = dataOf(null); 60 // Connected browser for the primary media source. 61 private final MutableLiveData<MediaBrowserCompat> mConnectedMediaBrowser = dataOf(null); 62 // Media controller for the connected browser. 63 private final MutableLiveData<MediaControllerCompat> mMediaController = dataOf(null); 64 65 private final Handler mHandler; 66 private final CarMediaManager.MediaSourceChangedListener mMediaSourceListener; 67 68 /** 69 * Factory for creating dependencies. Can be swapped out for testing. 70 */ 71 @VisibleForTesting 72 interface InputFactory { createMediaBrowserConnector(@onNull Application application, @NonNull MediaBrowserConnector.Callback connectedBrowserCallback)73 MediaBrowserConnector createMediaBrowserConnector(@NonNull Application application, 74 @NonNull MediaBrowserConnector.Callback connectedBrowserCallback); 75 getControllerForSession(@ullable MediaSessionCompat.Token session)76 MediaControllerCompat getControllerForSession(@Nullable MediaSessionCompat.Token session); 77 getCarApi()78 Car getCarApi(); 79 getCarMediaManager(Car carApi)80 CarMediaManager getCarMediaManager(Car carApi) throws CarNotConnectedException; 81 getMediaSource(String packageName)82 MediaSource getMediaSource(String packageName); 83 } 84 85 /** Returns the MediaSourceViewModel singleton tied to the application. */ get(@onNull Application application)86 public static MediaSourceViewModel get(@NonNull Application application) { 87 if (sInstance == null) { 88 sInstance = new MediaSourceViewModel(application); 89 } 90 return sInstance; 91 } 92 93 /** 94 * Create a new instance of MediaSourceViewModel 95 * 96 * @see AndroidViewModel 97 */ MediaSourceViewModel(@onNull Application application)98 private MediaSourceViewModel(@NonNull Application application) { 99 this(application, new InputFactory() { 100 @Override 101 public MediaBrowserConnector createMediaBrowserConnector( 102 @NonNull Application application, 103 @NonNull MediaBrowserConnector.Callback connectedBrowserCallback) { 104 return new MediaBrowserConnector(application, connectedBrowserCallback); 105 } 106 107 @Override 108 public MediaControllerCompat getControllerForSession( 109 @Nullable MediaSessionCompat.Token token) { 110 if (token == null) return null; 111 try { 112 return new MediaControllerCompat(application, token); 113 } catch (RemoteException e) { 114 Log.e(TAG, "Couldn't get MediaControllerCompat", e); 115 return null; 116 } 117 } 118 119 @Override 120 public Car getCarApi() { 121 return Car.createCar(application); 122 } 123 124 @Override 125 public CarMediaManager getCarMediaManager(Car carApi) throws CarNotConnectedException { 126 return (CarMediaManager) carApi.getCarManager(Car.CAR_MEDIA_SERVICE); 127 } 128 129 @Override 130 public MediaSource getMediaSource(String packageName) { 131 return packageName == null ? null : new MediaSource(application, packageName); 132 } 133 }); 134 } 135 136 private final InputFactory mInputFactory; 137 private final MediaBrowserConnector mBrowserConnector; 138 private final MediaBrowserConnector.Callback mConnectedBrowserCallback; 139 140 @VisibleForTesting MediaSourceViewModel(@onNull Application application, @NonNull InputFactory inputFactory)141 MediaSourceViewModel(@NonNull Application application, @NonNull InputFactory inputFactory) { 142 super(application); 143 144 mInputFactory = inputFactory; 145 mCar = inputFactory.getCarApi(); 146 147 mConnectedBrowserCallback = browser -> { 148 mConnectedMediaBrowser.setValue(browser); 149 if (browser != null) { 150 if (!browser.isConnected()) { 151 Log.e(TAG, "Browser is NOT connected !! " 152 + mPrimaryMediaSource.getValue().getPackageName() + idHash(browser)); 153 mMediaController.setValue(null); 154 } else { 155 mMediaController.setValue(mInputFactory.getControllerForSession( 156 browser.getSessionToken())); 157 } 158 } else { 159 mMediaController.setValue(null); 160 } 161 }; 162 mBrowserConnector = inputFactory.createMediaBrowserConnector(application, 163 mConnectedBrowserCallback); 164 165 mHandler = new Handler(application.getMainLooper()); 166 mMediaSourceListener = packageName -> mHandler.post( 167 () -> updateModelState(mInputFactory.getMediaSource(packageName))); 168 169 try { 170 mCarMediaManager = mInputFactory.getCarMediaManager(mCar); 171 mCarMediaManager.registerMediaSourceListener(mMediaSourceListener); 172 updateModelState(mInputFactory.getMediaSource(mCarMediaManager.getMediaSource())); 173 } catch (CarNotConnectedException e) { 174 Log.e(TAG, "Car not connected", e); 175 } 176 } 177 178 @VisibleForTesting getConnectedBrowserCallback()179 MediaBrowserConnector.Callback getConnectedBrowserCallback() { 180 return mConnectedBrowserCallback; 181 } 182 183 /** 184 * Returns a LiveData that emits the MediaSource that is to be browsed or displayed. 185 */ getPrimaryMediaSource()186 public LiveData<MediaSource> getPrimaryMediaSource() { 187 return mPrimaryMediaSource; 188 } 189 190 /** 191 * Updates the primary media source, and notifies content provider of new source 192 */ setPrimaryMediaSource(MediaSource mediaSource)193 public void setPrimaryMediaSource(MediaSource mediaSource) { 194 ContentValues values = new ContentValues(); 195 values.put(MediaConstants.KEY_PACKAGE_NAME, mediaSource.getPackageName()); 196 197 mCarMediaManager.setMediaSource(mediaSource.getPackageName()); 198 } 199 200 /** 201 * Returns a LiveData that emits the currently connected MediaBrowser. Emits {@code null} if no 202 * MediaSource is set, if the MediaSource does not support browsing, or if the MediaBrowser is 203 * not connected. 204 */ getConnectedMediaBrowser()205 public LiveData<MediaBrowserCompat> getConnectedMediaBrowser() { 206 return mConnectedMediaBrowser; 207 } 208 209 /** 210 * Returns a LiveData that emits a {@link MediaController} that allows controlling this media 211 * source, or emits {@code null} if the media source doesn't support browsing or the browser is 212 * not connected. 213 */ getMediaController()214 public LiveData<MediaControllerCompat> getMediaController() { 215 return mMediaController; 216 } 217 updateModelState(MediaSource newMediaSource)218 private void updateModelState(MediaSource newMediaSource) { 219 MediaSource oldMediaSource = mPrimaryMediaSource.getValue(); 220 221 if (Objects.equals(oldMediaSource, newMediaSource)) { 222 return; 223 } 224 225 // Reset dependent values to avoid propagating inconsistencies. 226 mMediaController.setValue(null); 227 mConnectedMediaBrowser.setValue(null); 228 mBrowserConnector.connectTo(null); 229 230 // Broadcast the new source 231 mPrimaryMediaSource.setValue(newMediaSource); 232 233 // Recompute dependent values 234 if (newMediaSource == null) { 235 return; 236 } 237 238 ComponentName browseService = newMediaSource.getBrowseServiceComponentName(); 239 if (browseService == null) { 240 Log.e(TAG, "No browseService for source: " + newMediaSource.getPackageName()); 241 } 242 mBrowserConnector.connectTo(browseService); 243 } 244 } 245