• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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