• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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