1 package org.robolectric.shadows.support.v4; 2 3 import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE; 4 import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE; 5 6 import android.content.ComponentName; 7 import android.content.Context; 8 import android.net.Uri; 9 import android.os.Bundle; 10 import android.os.Handler; 11 import android.support.annotation.NonNull; 12 import android.support.annotation.Nullable; 13 import android.support.v4.media.MediaBrowserCompat; 14 import android.support.v4.media.MediaBrowserCompat.ConnectionCallback; 15 import android.support.v4.media.MediaBrowserCompat.ItemCallback; 16 import android.support.v4.media.MediaBrowserCompat.MediaItem; 17 import android.support.v4.media.MediaBrowserCompat.SearchCallback; 18 import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; 19 import android.support.v4.media.MediaBrowserServiceCompat; 20 import android.support.v4.media.MediaMetadataCompat; 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.LinkedHashMap; 24 import java.util.List; 25 import java.util.Map; 26 import org.robolectric.annotation.Implementation; 27 import org.robolectric.annotation.Implements; 28 import org.robolectric.annotation.RealObject; 29 import org.robolectric.shadow.api.Shadow; 30 import org.robolectric.util.ReflectionHelpers.ClassParameter; 31 32 /** 33 * This will mimic the connection to a {@link MediaBrowserServiceCompat} by creating and maintaining 34 * its own account of {@link MediaItem}s. 35 */ 36 @Implements(MediaBrowserCompat.class) 37 public class ShadowMediaBrowserCompat { 38 39 private final Handler handler = new Handler(); 40 private @RealObject MediaBrowserCompat mediaBrowser; 41 42 private final Map<String, MediaItem> mediaItems = new LinkedHashMap<>(); 43 private final Map<MediaItem, List<MediaItem>> mediaItemChildren = new LinkedHashMap<>(); 44 45 private boolean isConnected; 46 private ConnectionCallback connectionCallback; 47 private String rootId = "root_id"; 48 49 @Implementation __constructor__( Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)50 protected void __constructor__( 51 Context context, 52 ComponentName serviceComponent, 53 ConnectionCallback callback, 54 Bundle rootHints) { 55 connectionCallback = callback; 56 Shadow.invokeConstructor( 57 MediaBrowserCompat.class, 58 mediaBrowser, 59 ClassParameter.from(Context.class, context), 60 ClassParameter.from(ComponentName.class, serviceComponent), 61 ClassParameter.from(ConnectionCallback.class, callback), 62 ClassParameter.from(Bundle.class, rootHints)); 63 } 64 65 @Implementation connect()66 protected void connect() { 67 handler.post( 68 () -> { 69 isConnected = true; 70 connectionCallback.onConnected(); 71 }); 72 } 73 74 @Implementation disconnect()75 protected void disconnect() { 76 handler.post( 77 () -> { 78 isConnected = false; 79 }); 80 } 81 82 @Implementation isConnected()83 protected boolean isConnected() { 84 return isConnected; 85 } 86 87 @Implementation getRoot()88 protected String getRoot() { 89 if (!isConnected) { 90 throw new IllegalStateException("Can't call getRoot() while not connected."); 91 } 92 return rootId; 93 } 94 95 @Implementation getItem(@onNull final String mediaId, @NonNull final ItemCallback cb)96 protected void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 97 // mediaItem will be null when there is no MediaItem that matches the given mediaId. 98 final MediaItem mediaItem = mediaItems.get(mediaId); 99 100 if (isConnected && mediaItem != null) { 101 handler.post(() -> cb.onItemLoaded(mediaItem)); 102 } else { 103 handler.post(() -> cb.onError(mediaId)); 104 } 105 } 106 107 @Implementation subscribe(@onNull String parentId, @NonNull SubscriptionCallback callback)108 protected void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { 109 subscribe(parentId, null, callback); 110 } 111 112 @Implementation subscribe( @onNull String parentId, @Nullable Bundle options, @NonNull SubscriptionCallback callback)113 protected void subscribe( 114 @NonNull String parentId, @Nullable Bundle options, @NonNull SubscriptionCallback callback) { 115 if (isConnected) { 116 final MediaItem parentItem = mediaItems.get(parentId); 117 List<MediaItem> children = 118 mediaItemChildren.get(parentItem) == null 119 ? Collections.emptyList() 120 : mediaItemChildren.get(parentItem); 121 handler.post( 122 () -> callback.onChildrenLoaded(parentId, applyOptionsToResults(children, options))); 123 } else { 124 handler.post(() -> callback.onError(parentId)); 125 } 126 } 127 applyOptionsToResults(List<MediaItem> results, final Bundle options)128 private List<MediaItem> applyOptionsToResults(List<MediaItem> results, final Bundle options) { 129 if (results == null || options == null) { 130 return results; 131 } 132 final int resultsSize = results.size(); 133 final int page = options.getInt(EXTRA_PAGE, -1); 134 final int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1); 135 if (page == -1 && pageSize == -1) { 136 return results; 137 } 138 139 final int firstItemIndex = page * pageSize; 140 final int lastItemIndex = firstItemIndex + pageSize; 141 if (page < 0 || pageSize < 1 || firstItemIndex >= resultsSize) { 142 return Collections.emptyList(); 143 } 144 return results.subList(firstItemIndex, Math.min(lastItemIndex, resultsSize)); 145 } 146 147 /** 148 * This differs from real Android search logic. Search results will contain all {@link 149 * MediaItem}'s with a title that {@param query} is a substring of. 150 */ 151 @Implementation search( @onNull final String query, final Bundle extras, @NonNull SearchCallback callback)152 protected void search( 153 @NonNull final String query, final Bundle extras, @NonNull SearchCallback callback) { 154 if (isConnected) { 155 final List<MediaItem> searchResults = new ArrayList<>(); 156 for (MediaItem item : mediaItems.values()) { 157 final String mediaTitle = item.getDescription().getTitle().toString().toLowerCase(); 158 if (mediaTitle.contains(query.toLowerCase())) { 159 searchResults.add(item); 160 } 161 } 162 handler.post(() -> callback.onSearchResult(query, extras, searchResults)); 163 } else { 164 handler.post(() -> callback.onError(query, extras)); 165 } 166 } 167 168 /** 169 * Sets the root id. Can be called more than once. 170 * 171 * @param mediaId the id of the root MediaItem. This MediaItem should already have been created. 172 */ setRootId(String mediaId)173 public void setRootId(String mediaId) { 174 rootId = mediaId; 175 } 176 177 /** 178 * Creates a MediaItem and returns it. 179 * 180 * @param parentId the id of the parent MediaItem. If the MediaItem to be created will be the 181 * root, parentId should be null. 182 * @param mediaId the id of the MediaItem to be created. 183 * @param title the title of the MediaItem to be created. 184 * @param flag says if the MediaItem to be created is browsable and/or playable. 185 * @return the newly created MediaItem. 186 */ createMediaItem(String parentId, String mediaId, String title, int flag)187 public MediaItem createMediaItem(String parentId, String mediaId, String title, int flag) { 188 final MediaMetadataCompat metadataCompat = 189 new MediaMetadataCompat.Builder() 190 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId) 191 .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) 192 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, Uri.parse(mediaId).toString()) 193 .build(); 194 final MediaItem mediaItem = new MediaItem(metadataCompat.getDescription(), flag); 195 mediaItems.put(mediaId, mediaItem); 196 197 // If this MediaItem is the child of a MediaItem that has already been created. This applies to 198 // all MediaItems except the root. 199 if (parentId != null) { 200 final MediaItem parentItem = mediaItems.get(parentId); 201 List<MediaItem> children = mediaItemChildren.get(parentItem); 202 if (children == null) { 203 children = new ArrayList<>(); 204 mediaItemChildren.put(parentItem, children); 205 } 206 children.add(mediaItem); 207 } 208 209 return mediaItem; 210 } 211 212 /** @return a copy of the internal {@link Map} that maps {@link MediaItem}s to their children. */ getCopyOfMediaItemChildren()213 public Map<MediaItem, List<MediaItem>> getCopyOfMediaItemChildren() { 214 final Map<MediaItem, List<MediaItem>> copyOfMediaItemChildren = new LinkedHashMap<>(); 215 for (MediaItem parent : mediaItemChildren.keySet()) { 216 List<MediaItem> children = new ArrayList<>(mediaItemChildren.get(parent)); 217 copyOfMediaItemChildren.put(parent, children); 218 } 219 return copyOfMediaItemChildren; 220 } 221 } 222