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.browse;
18 
19 import static androidx.lifecycle.Transformations.map;
20 
21 import static com.android.car.arch.common.LiveDataFunctions.dataOf;
22 import static com.android.car.arch.common.LiveDataFunctions.loadingSwitchMap;
23 import static com.android.car.arch.common.LiveDataFunctions.pair;
24 import static com.android.car.arch.common.LiveDataFunctions.split;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.Application;
29 import android.os.Bundle;
30 import android.support.v4.media.MediaBrowserCompat;
31 import android.text.TextUtils;
32 
33 import androidx.annotation.RestrictTo;
34 import androidx.lifecycle.AndroidViewModel;
35 import androidx.lifecycle.LiveData;
36 import androidx.lifecycle.MediatorLiveData;
37 import androidx.lifecycle.MutableLiveData;
38 
39 import com.android.car.arch.common.FutureData;
40 import com.android.car.arch.common.switching.SwitchingLiveData;
41 import com.android.car.media.common.MediaConstants;
42 import com.android.car.media.common.MediaItemMetadata;
43 
44 import java.util.List;
45 
46 /**
47  * Contains observable data needed for displaying playback and browse/search UI. Instances can be
48  * obtained via {@link MediaBrowserViewModel.Factory}
49  */
50 @RestrictTo(RestrictTo.Scope.LIBRARY)
51 class MediaBrowserViewModelImpl extends AndroidViewModel implements MediaBrowserViewModel {
52 
53     private final boolean mIsRoot;
54 
55     private final SwitchingLiveData<MediaBrowserCompat> mMediaBrowserSwitch =
56             SwitchingLiveData.newInstance();
57 
58     final MutableLiveData<String> mCurrentBrowseId = dataOf(null);
59     final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
60     private final LiveData<MediaBrowserCompat> mConnectedMediaBrowser =
61             map(mMediaBrowserSwitch.asLiveData(), MediaBrowserViewModelImpl::requireConnected);
62 
63     private final LiveData<FutureData<List<MediaItemMetadata>>> mSearchedMediaItems;
64     private final LiveData<FutureData<List<MediaItemMetadata>>> mBrowsedMediaItems;
65 
66     private final LiveData<BrowseState> mBrowseState;
67 
68     private final LiveData<String> mPackageName;
69 
MediaBrowserViewModelImpl(@onNull Application application, boolean isRoot)70     MediaBrowserViewModelImpl(@NonNull Application application, boolean isRoot) {
71         super(application);
72 
73         mIsRoot = isRoot;
74 
75         mPackageName = map(mConnectedMediaBrowser,
76                 mediaBrowser -> {
77                     if (mediaBrowser == null) return null;
78                     return mediaBrowser.getServiceComponent().getPackageName();
79                 });
80 
81         mBrowsedMediaItems =
82                 loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentBrowseId),
83                         split((mediaBrowser, browseId) -> {
84                             if (mediaBrowser == null || (!mIsRoot && browseId == null)) {
85                                 return null;
86                             }
87 
88                             String parentId = (mIsRoot) ? mediaBrowser.getRoot() : browseId;
89                             return new BrowsedMediaItems(mediaBrowser, parentId);
90                         }));
91         mSearchedMediaItems =
92                 loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentSearchQuery),
93                         split((mediaBrowser, query) ->
94                                 (mediaBrowser == null || TextUtils.isEmpty(query))
95                                         ? null
96                                         : new SearchedMediaItems(mediaBrowser, query)));
97 
98         mBrowseState = new MediatorLiveData<BrowseState>() {
99             {
100                 setValue(BrowseState.EMPTY);
101                 addSource(mBrowsedMediaItems, items -> update());
102             }
103 
104             private void update() {
105                 setValue(getState());
106             }
107 
108             private BrowseState getState() {
109                 if (mBrowsedMediaItems.getValue() == null) {
110                     // Uninitialized
111                     return BrowseState.EMPTY;
112                 }
113                 if (mBrowsedMediaItems.getValue().isLoading()) {
114                     return BrowseState.LOADING;
115                 }
116                 List<MediaItemMetadata> items = mBrowsedMediaItems.getValue().getData();
117                 if (items == null) {
118                     // Normally this could be null if it hasn't been initialized, but in that case
119                     // isLoading would not be false, so this means it must have encountered an
120                     // error.
121                     return BrowseState.ERROR;
122                 }
123                 if (items.isEmpty()) {
124                     return BrowseState.EMPTY;
125                 }
126                 return BrowseState.LOADED;
127             }
128         };
129 
130     }
131 
requireConnected(@ullable MediaBrowserCompat mediaBrowser)132     private static MediaBrowserCompat requireConnected(@Nullable MediaBrowserCompat mediaBrowser) {
133         if (mediaBrowser != null && !mediaBrowser.isConnected()) {
134             throw new IllegalStateException(
135                     "Only connected MediaBrowsers may be provided to MediaBrowserViewModel.");
136         }
137         return mediaBrowser;
138     }
139 
140     /**
141      * Set the source {@link MediaBrowserCompat} to use for browsing. If {@code mediaBrowser} emits
142      * non-null, the MediaBrowser emitted must already be in a connected state.
143      */
setConnectedMediaBrowser(@ullable LiveData<MediaBrowserCompat> mediaBrowser)144     void setConnectedMediaBrowser(@Nullable LiveData<MediaBrowserCompat> mediaBrowser) {
145         mMediaBrowserSwitch.setSource(mediaBrowser);
146     }
147 
getMediaBrowserSource()148     LiveData<? extends MediaBrowserCompat> getMediaBrowserSource() {
149         return mMediaBrowserSwitch.getSource();
150     }
151 
152     @Override
getPackageName()153     public LiveData<String> getPackageName() {
154         return mPackageName;
155     }
156 
157     @Override
getBrowseState()158     public LiveData<BrowseState> getBrowseState() {
159         return mBrowseState;
160     }
161 
162     @Override
getBrowsedMediaItems()163     public LiveData<FutureData<List<MediaItemMetadata>>> getBrowsedMediaItems() {
164         return mBrowsedMediaItems;
165     }
166 
167     @Override
getSearchedMediaItems()168     public LiveData<FutureData<List<MediaItemMetadata>>> getSearchedMediaItems() {
169         return mSearchedMediaItems;
170     }
171 
172     @SuppressWarnings("deprecation")
173     @Override
supportsSearch()174     public LiveData<Boolean> supportsSearch() {
175         return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
176             if (mediaBrowserCompat == null) {
177                 return false;
178             }
179             Bundle extras = mediaBrowserCompat.getExtras();
180             if (extras == null) {
181                 return false;
182             }
183             if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED)) {
184                 return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED);
185             }
186             if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE)) {
187                 return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE);
188             }
189             return false;
190         });
191     }
192 
193     @SuppressWarnings("deprecation")
194     @Override
195     public LiveData<Integer> rootBrowsableHint() {
196         return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
197             if (mediaBrowserCompat == null) {
198                 return 0;
199             }
200             Bundle extras = mediaBrowserCompat.getExtras();
201             if (extras == null) {
202                 return 0;
203             }
204             if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT)) {
205                 return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT, 0);
206             }
207             if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE)) {
208                 return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE, 0);
209             }
210             return 0;
211         });
212     }
213 
214     @SuppressWarnings("deprecation")
215     @Override
216     public LiveData<Integer> rootPlayableHint() {
217         return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
218             if (mediaBrowserCompat == null) {
219                 return 0;
220             }
221             Bundle extras = mediaBrowserCompat.getExtras();
222             if (extras == null) {
223                 return 0;
224             }
225             if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT)) {
226                 return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT, 0);
227             }
228             if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE)) {
229                 return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE, 0);
230             }
231             return 0;
232         });
233     }
234 }
235