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