1 /* 2 * Copyright (C) 2020 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 com.android.car.apps.common.util.LiveDataFunctions.dataOf; 20 21 import static java.util.stream.Collectors.toList; 22 23 import android.app.Application; 24 import android.os.Bundle; 25 import android.support.v4.media.MediaBrowserCompat; 26 import android.support.v4.media.MediaBrowserCompat.SearchCallback; 27 import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.lifecycle.LiveData; 35 import androidx.lifecycle.MutableLiveData; 36 37 import com.android.car.apps.common.util.FutureData; 38 import com.android.car.media.common.MediaItemMetadata; 39 import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState; 40 import com.android.car.media.common.source.MediaSource; 41 import com.android.car.media.common.source.MediaSourceViewModel; 42 43 import java.util.Collections; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.stream.Collectors; 49 50 51 /** 52 * Fulfills media items search and children queries. The latter also provides the last list of 53 * results alongside the new list so that differences can be calculated and acted upon. 54 */ 55 public class MediaItemsRepository { 56 private static final String TAG = "MediaItemsRepository"; 57 58 /** One instance per MEDIA_SOURCE_MODE. */ 59 private static MediaItemsRepository[] sInstances = new MediaItemsRepository[2]; 60 61 /** Returns the MediaItemsRepository "singleton" tied to the application for the given mode. */ get(@onNull Application application, int mode)62 public static MediaItemsRepository get(@NonNull Application application, int mode) { 63 if (sInstances[mode] == null) { 64 sInstances[mode] = new MediaItemsRepository( 65 MediaSourceViewModel.get(application, mode).getBrowsingState()); 66 } 67 return sInstances[mode]; 68 } 69 70 /** The live data providing the updates for a query. */ 71 public static class MediaItemsLiveData 72 extends LiveData<FutureData<List<MediaItemMetadata>>> { 73 MediaItemsLiveData()74 private MediaItemsLiveData() { 75 this(true); 76 } 77 MediaItemsLiveData(boolean initAsLoading)78 private MediaItemsLiveData(boolean initAsLoading) { 79 if (initAsLoading) { 80 setLoading(); 81 } else { 82 clear(); 83 } 84 } 85 onDataLoaded(List<MediaItemMetadata> old, List<MediaItemMetadata> list)86 private void onDataLoaded(List<MediaItemMetadata> old, List<MediaItemMetadata> list) { 87 setValue(FutureData.newLoadedData(old, list)); 88 } 89 setLoading()90 private void setLoading() { 91 setValue(FutureData.newLoadingData()); 92 } 93 clear()94 private void clear() { 95 setValue(null); 96 } 97 } 98 99 private static class MediaChildren { 100 final String mNodeId; 101 final MediaItemsLiveData mLiveData = new MediaItemsLiveData(); 102 List<MediaItemMetadata> mPreviousValue = Collections.emptyList(); 103 MediaChildren(String nodeId)104 MediaChildren(String nodeId) { 105 mNodeId = nodeId; 106 } 107 } 108 109 private static class PerMediaSourceCache { 110 String mRootId; 111 Map<String, MediaChildren> mChildrenByNodeId = new HashMap<>(); 112 } 113 114 private BrowsingState mBrowsingState; 115 private final Map<MediaSource, PerMediaSourceCache> mCaches = new HashMap<>(); 116 private final MutableLiveData<BrowsingState> mBrowsingStateLiveData = dataOf(null); 117 private final MediaItemsLiveData mRootMediaItems = new MediaItemsLiveData(); 118 private final MediaItemsLiveData mSearchMediaItems = new MediaItemsLiveData(/*loading*/ false); 119 120 private String mSearchQuery; 121 122 @VisibleForTesting MediaItemsRepository(LiveData<BrowsingState> browsingState)123 public MediaItemsRepository(LiveData<BrowsingState> browsingState) { 124 browsingState.observeForever(this::onMediaBrowsingStateChanged); 125 } 126 127 /** 128 * Rebroadcasts browsing state changes before the repository takes any action on them. 129 */ getBrowsingState()130 public LiveData<BrowsingState> getBrowsingState() { 131 return mBrowsingStateLiveData; 132 } 133 134 /** 135 * Convenience wrapper for root media items. The live data is the same instance for all 136 * media sources. 137 */ getRootMediaItems()138 public MediaItemsLiveData getRootMediaItems() { 139 return mRootMediaItems; 140 } 141 142 /** 143 * Returns the results from the current search query. The live data is the same instance 144 * for all media sources. 145 */ getSearchMediaItems()146 public MediaItemsLiveData getSearchMediaItems() { 147 return mSearchMediaItems; 148 } 149 150 /** Returns the children of the given node. */ getMediaChildren(String nodeId)151 public MediaItemsLiveData getMediaChildren(String nodeId) { 152 PerMediaSourceCache cache = getCache(); 153 MediaChildren items = cache.mChildrenByNodeId.get(nodeId); 154 if (items == null) { 155 items = new MediaChildren(nodeId); 156 cache.mChildrenByNodeId.put(nodeId, items); 157 } 158 159 // Always refresh the subscription (to work around bugs in media apps). 160 mBrowsingState.mBrowser.unsubscribe(nodeId); 161 mBrowsingState.mBrowser.subscribe(nodeId, mBrowseCallback); 162 163 return items.mLiveData; 164 } 165 166 /** Sets the search query. Results will be given through {@link #getSearchMediaItems}. */ setSearchQuery(String query)167 public void setSearchQuery(String query) { 168 mSearchQuery = query; 169 if (TextUtils.isEmpty(mSearchQuery)) { 170 clearSearchResults(); 171 } else { 172 mSearchMediaItems.setLoading(); 173 mBrowsingState.mBrowser.search(mSearchQuery, null, mSearchCallback); 174 } 175 } 176 clearSearchResults()177 private void clearSearchResults() { 178 mSearchMediaItems.clear(); 179 } 180 getMediaSource()181 private MediaSource getMediaSource() { 182 return (mBrowsingState != null) ? mBrowsingState.mMediaSource : null; 183 } 184 onMediaBrowsingStateChanged(BrowsingState newBrowsingState)185 private void onMediaBrowsingStateChanged(BrowsingState newBrowsingState) { 186 mBrowsingState = newBrowsingState; 187 if (mBrowsingState == null) { 188 Log.e(TAG, "Null browsing state (no media source!)"); 189 return; 190 } 191 mBrowsingStateLiveData.setValue(mBrowsingState); 192 switch (mBrowsingState.mConnectionStatus) { 193 case CONNECTING: 194 mRootMediaItems.setLoading(); 195 break; 196 case CONNECTED: 197 String rootId = mBrowsingState.mBrowser.getRoot(); 198 getCache().mRootId = rootId; 199 getMediaChildren(rootId); 200 break; 201 case DISCONNECTING: 202 unsubscribeNodes(); 203 clearSearchResults(); 204 clearNodes(); 205 break; 206 case REJECTED: 207 case SUSPENDED: 208 onBrowseData(getCache().mRootId, null); 209 clearSearchResults(); 210 clearNodes(); 211 } 212 } 213 getCache()214 private PerMediaSourceCache getCache() { 215 PerMediaSourceCache cache = mCaches.get(getMediaSource()); 216 if (cache == null) { 217 cache = new PerMediaSourceCache(); 218 mCaches.put(getMediaSource(), cache); 219 } 220 return cache; 221 } 222 223 /** Does NOT clear the cache. */ unsubscribeNodes()224 private void unsubscribeNodes() { 225 PerMediaSourceCache cache = getCache(); 226 for (String nodeId : cache.mChildrenByNodeId.keySet()) { 227 mBrowsingState.mBrowser.unsubscribe(nodeId); 228 } 229 } 230 231 /** Does NOT unsubscribe nodes. */ clearNodes()232 private void clearNodes() { 233 PerMediaSourceCache cache = getCache(); 234 cache.mChildrenByNodeId.clear(); 235 } 236 onBrowseData(@onNull String parentId, @Nullable List<MediaItemMetadata> list)237 private void onBrowseData(@NonNull String parentId, @Nullable List<MediaItemMetadata> list) { 238 PerMediaSourceCache cache = getCache(); 239 MediaChildren children = cache.mChildrenByNodeId.get(parentId); 240 if (children == null) { 241 if (Log.isLoggable(TAG, Log.WARN)) { 242 Log.w(TAG, "Browse parent not in the cache: " + parentId); 243 } 244 return; 245 } 246 247 List<MediaItemMetadata> old = children.mPreviousValue; 248 children.mPreviousValue = list; 249 children.mLiveData.onDataLoaded(old, list); 250 251 if (Objects.equals(parentId, cache.mRootId)) { 252 mRootMediaItems.onDataLoaded(old, list); 253 } 254 } 255 onSearchData(@ullable List<MediaItemMetadata> list)256 private void onSearchData(@Nullable List<MediaItemMetadata> list) { 257 mSearchMediaItems.onDataLoaded(null, list); 258 } 259 260 private final SubscriptionCallback mBrowseCallback = new SubscriptionCallback() { 261 @Override 262 public void onChildrenLoaded(@NonNull String parentId, 263 @NonNull List<MediaBrowserCompat.MediaItem> children) { 264 265 onBrowseData(parentId, children.stream() 266 .filter(Objects::nonNull) 267 .map(MediaItemMetadata::new) 268 .collect(Collectors.toList())); 269 } 270 271 @Override 272 public void onChildrenLoaded(@NonNull String parentId, 273 @NonNull List<MediaBrowserCompat.MediaItem> children, 274 @NonNull Bundle options) { 275 onChildrenLoaded(parentId, children); 276 } 277 278 @Override 279 public void onError(@NonNull String parentId) { 280 onBrowseData(parentId, null); 281 } 282 283 @Override 284 public void onError(@NonNull String parentId, @NonNull Bundle options) { 285 onError(parentId); 286 } 287 }; 288 289 private final SearchCallback mSearchCallback = new SearchCallback() { 290 @Override 291 public void onSearchResult(@NonNull String query, Bundle extras, 292 @NonNull List<MediaBrowserCompat.MediaItem> items) { 293 super.onSearchResult(query, extras, items); 294 if (Objects.equals(mSearchQuery, query)) { 295 onSearchData(items.stream() 296 .filter(Objects::nonNull) 297 .map(MediaItemMetadata::new) 298 .collect(toList())); 299 } 300 } 301 302 @Override 303 public void onError(@NonNull String query, Bundle extras) { 304 super.onError(query, extras); 305 if (Objects.equals(mSearchQuery, query)) { 306 onSearchData(null); 307 } 308 } 309 }; 310 } 311