Merge "(settings-lib) Apply SUW theme to screen lock page in Settings" into pi-dev
diff --git a/car-media-common/src/com/android/car/media/common/MediaSource.java b/car-media-common/src/com/android/car/media/common/MediaSource.java
index 398c88f..6beb309 100644
--- a/car-media-common/src/com/android/car/media/common/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/MediaSource.java
@@ -38,14 +38,17 @@
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.media.session.MediaSession;
+import android.os.Bundle;
import android.os.Handler;
import android.service.media.MediaBrowserService;
import android.support.annotation.ColorInt;
import android.util.Log;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
@@ -66,6 +69,10 @@
"com.google.android.gms.car.application.theme";
/** Mark used to indicate that we couldn't find a color and the default one should be used */
private static final int DEFAULT_COLOR = 0;
+ /** Number of times we will retry obtaining the list of children of a certain node */
+ private static final int CHILDREN_SUBSCRIPTION_RETRIES = 3;
+ /** Time between retries while trying to obtain the list of children of a certain node */
+ private static final int CHILDREN_SUBSCRIPTION_RETRY_TIME_MS = 1000;
private final String mPackageName;
@Nullable
@@ -115,10 +122,12 @@
/**
* This method is called whenever media items are loaded or updated.
*
+ * @param mediaSource {@link MediaSource} these items belongs to
* @param parentId identifier of the items parent.
* @param items items loaded, or null if there was an error trying to load them.
*/
- void onChildrenLoaded(String parentId, @Nullable List<MediaItemMetadata> items);
+ void onChildrenLoaded(MediaSource mediaSource, String parentId,
+ @Nullable List<MediaItemMetadata> items);
}
private final MediaBrowser.ConnectionCallback mConnectionCallback =
@@ -216,16 +225,29 @@
}
}
+ private Map<String, ChildrenSubscription> mChildrenSubscriptions = new HashMap<>();
+
/**
- * Subscribes to changes on the list of media item children of the given parent.
+ * Obtains the root node in the browse tree of this node.
+ */
+ public String getRoot() {
+ if (mRootNode == null) {
+ mRootNode = mBrowser.getRoot();
+ }
+ return mRootNode;
+ }
+
+ /**
+ * Subscribes to changes on the list of media item children of the given parent. Multiple
+ * subscription can be added to the same node. If the node has been already loaded, then all
+ * new subscription will immediately obtain a copy of the last obtained list.
*
* @param parentId parent of the children to load, or null to indicate children of the root
* node.
* @param callback callback used to provide updates on the subscribed node.
* @throws IllegalStateException if browsing is not available or it is not connected.
*/
- public void subscribeChildren(@Nullable String parentId,
- ItemsSubscription callback) {
+ public void subscribeChildren(@Nullable String parentId, ItemsSubscription callback) {
if (mBrowser == null) {
throw new IllegalStateException("Browsing is not available for this source: "
+ getName());
@@ -235,17 +257,28 @@
+ "connected: " + getName());
}
mRootNode = mBrowser.getRoot();
- mBrowser.subscribe(parentId != null ? parentId : mRootNode,
- wrapCallback(callback));
+
+ String itemId = parentId != null ? parentId : mRootNode;
+ ChildrenSubscription subscription = mChildrenSubscriptions.get(itemId);
+ if (subscription != null) {
+ subscription.add(callback);
+ } else {
+ subscription = new ChildrenSubscription(mBrowser, itemId);
+ subscription.add(callback);
+ mChildrenSubscriptions.put(itemId, subscription);
+ subscription.start(CHILDREN_SUBSCRIPTION_RETRIES,
+ CHILDREN_SUBSCRIPTION_RETRY_TIME_MS);
+ }
}
/**
* Unsubscribes to changes on the list of media items children of the given parent
*
* @param parentId parent to unsubscribe, or null to unsubscribe from the root node.
+ * @param callback callback to remove
* @throws IllegalStateException if browsing is not available or it is not connected.
*/
- public void unsubscribeChildren(@Nullable String parentId) {
+ public void unsubscribeChildren(@Nullable String parentId, ItemsSubscription callback) {
// If we are not connected
if (mBrowser == null) {
throw new IllegalStateException("Browsing is not available for this source: "
@@ -256,25 +289,155 @@
// there is nothing we can do.
return;
}
- mBrowser.unsubscribe(parentId != null ? parentId : mRootNode);
+
+ String itemId = parentId != null ? parentId : mRootNode;
+ ChildrenSubscription subscription = mChildrenSubscriptions.get(itemId);
+ if (subscription != null) {
+ subscription.remove(callback);
+ if (subscription.count() == 0) {
+ subscription.stop();
+ mChildrenSubscriptions.remove(itemId);
+ }
+ }
}
- private MediaBrowser.SubscriptionCallback wrapCallback(ItemsSubscription subscription) {
- return new MediaBrowser.SubscriptionCallback() {
+ /**
+ * {@link MediaBrowser.SubscriptionCallback} wrapper used to overcome the lack of a reliable
+ * method to obtain the initial list of children of a given node.
+ * <p>
+ * When some 3rd party apps go through configuration changes (i.e., in the case of user-switch),
+ * they leave subscriptions in an intermediate state where neither
+ * {@link MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)} nor
+ * {@link MediaBrowser.SubscriptionCallback#onError(String)} are invoked.
+ * <p>
+ * This wrapper works around this problem by retrying the subscription a given number of times
+ * if no data is received after a certain amount of time. This process is started by calling
+ * {@link #start(int, int)}, passing the number of retries and delay between them as
+ * parameters.
+ */
+ private class ChildrenSubscription extends MediaBrowser.SubscriptionCallback {
+ private List<MediaItemMetadata> mItems;
+ private boolean mIsDataLoaded;
+ private List<ItemsSubscription> mSubscriptions = new ArrayList<>();
+ private String mParentId;
+ private int mRetries;
+ private int mRetryDelay;
+ private MediaBrowser mMediaBrowser;
+ private Runnable mRetryRunnable = new Runnable() {
@Override
- public void onChildrenLoaded(String parentId,
- List<MediaBrowser.MediaItem> children) {
- List<MediaItemMetadata> items = children.stream()
- .map(child -> new MediaItemMetadata(child))
- .collect(Collectors.toList());
- subscription.onChildrenLoaded(parentId, items);
- }
-
- @Override
- public void onError(String parentId) {
- subscription.onChildrenLoaded(parentId, null);
+ public void run() {
+ if (!mIsDataLoaded) {
+ if (mRetries > 0) {
+ mRetries--;
+ mMediaBrowser.unsubscribe(mParentId);
+ mMediaBrowser.subscribe(mParentId, ChildrenSubscription.this);
+ mHandler.postDelayed(this, mRetryDelay);
+ } else {
+ mItems = null;
+ mIsDataLoaded = true;
+ notifySubscriptions();
+ }
+ }
}
};
+
+ /**
+ * Creates a subscription to the list of children of a certain media browse item
+ *
+ * @param mediaBrowser {@link MediaBrowser} used to create the subscription
+ * @param parentId identifier of the parent node to subscribe to
+ */
+ ChildrenSubscription(@NonNull MediaBrowser mediaBrowser, String parentId) {
+ mParentId = parentId;
+ mMediaBrowser = mediaBrowser;
+ }
+
+ /**
+ * Adds a subscriber to this list of children
+ */
+ void add(ItemsSubscription subscription) {
+ mSubscriptions.add(subscription);
+ if (mIsDataLoaded) {
+ subscription.onChildrenLoaded(MediaSource.this, mParentId, mItems);
+ }
+ }
+
+ /**
+ * Removes a subscriber previously added with {@link #add(ItemsSubscription)}
+ */
+ void remove(ItemsSubscription subscription) {
+ mSubscriptions.remove(subscription);
+ }
+
+ /**
+ * Number of subscribers currently registered
+ */
+ int count() {
+ return mSubscriptions.size();
+ }
+
+ /**
+ * Starts trying to obtain the list of children
+ *
+ * @param retries number of times to retry. If children are not obtained in this time then
+ * the {@link ItemsSubscription#onChildrenLoaded(MediaSource, String, List)}
+ * will be invoked with a NULL list.
+ * @param retryDelay time between retries in milliseconds
+ */
+ void start(int retries, int retryDelay) {
+ if (mIsDataLoaded) {
+ notifySubscriptions();
+ mMediaBrowser.subscribe(mParentId, this);
+ } else {
+ mRetries = retries;
+ mRetryDelay = retryDelay;
+ mHandler.post(mRetryRunnable);
+ }
+ }
+
+ /**
+ * Stops retrying
+ */
+ void stop() {
+ mHandler.removeCallbacks(mRetryRunnable);
+ mMediaBrowser.unsubscribe(mParentId);
+ }
+
+ @Override
+ public void onChildrenLoaded(String parentId,
+ List<MediaBrowser.MediaItem> children) {
+ mHandler.removeCallbacks(mRetryRunnable);
+ mItems = children.stream()
+ .map(child -> new MediaItemMetadata(child))
+ .collect(Collectors.toList());
+ mIsDataLoaded = true;
+ notifySubscriptions();
+ }
+
+ @Override
+ public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children,
+ Bundle options) {
+ onChildrenLoaded(parentId, children);
+ }
+
+ @Override
+ public void onError(String parentId) {
+ mHandler.removeCallbacks(mRetryRunnable);
+ mItems = null;
+ mIsDataLoaded = true;
+ notifySubscriptions();
+ }
+
+ @Override
+ public void onError(String parentId, Bundle options) {
+ onError(parentId);
+ }
+
+ private void notifySubscriptions() {
+ for (ItemsSubscription subscription : mSubscriptions) {
+ subscription.onChildrenLoaded(MediaSource.this, mParentId, mItems);
+ }
+ }
}
private void extractComponentInfo(@NonNull String packageName,
@@ -495,4 +658,9 @@
public int hashCode() {
return Objects.hash(mPackageName, mBrowseServiceClassName);
}
+
+ @Override
+ public String toString() {
+ return getPackageName();
+ }
}