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