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