summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk8
-rw-r--r--api/current.txt71
-rw-r--r--media/java/android/media/browse/IMediaBrowserService.aidl20
-rw-r--r--media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl24
-rw-r--r--media/java/android/media/browse/MediaBrowser.java704
-rw-r--r--media/java/android/media/browse/MediaBrowserItem.java241
-rw-r--r--media/java/android/media/browse/MediaBrowserService.java363
-rw-r--r--services/core/java/com/android/server/ConnectivityService.java4
-rw-r--r--tests/MusicBrowserDemo/Android.mk35
-rw-r--r--tests/MusicBrowserDemo/AndroidManifest.xml45
-rw-r--r--tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.pngbin0 -> 4805 bytes
-rw-r--r--tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.pngbin0 -> 2592 bytes
-rw-r--r--tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.pngbin0 -> 5246 bytes
-rw-r--r--tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 14755 bytes
-rw-r--r--tests/MusicBrowserDemo/res/values/strings.xml21
-rw-r--r--tests/MusicBrowserDemo/res/values/styles.xml36
-rw-r--r--tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java149
-rw-r--r--tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java212
-rw-r--r--tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java48
-rw-r--r--tests/MusicServiceDemo/Android.mk35
-rw-r--r--tests/MusicServiceDemo/AndroidManifest.xml55
-rw-r--r--tests/MusicServiceDemo/proguard-project.txt20
-rw-r--r--tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.pngbin0 -> 4805 bytes
-rw-r--r--tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.pngbin0 -> 2592 bytes
-rw-r--r--tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.pngbin0 -> 5246 bytes
-rw-r--r--tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 14755 bytes
-rw-r--r--tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.pngbin0 -> 1632 bytes
-rw-r--r--tests/MusicServiceDemo/res/layout/activity_main.xml23
-rw-r--r--tests/MusicServiceDemo/res/layout/fragment_main.xml32
-rw-r--r--tests/MusicServiceDemo/res/values/colors.xml22
-rw-r--r--tests/MusicServiceDemo/res/values/dimens.xml23
-rw-r--r--tests/MusicServiceDemo/res/values/strings.xml25
-rw-r--r--tests/MusicServiceDemo/res/values/styles.xml36
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java246
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java101
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java77
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java266
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java70
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java24
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java138
40 files changed, 3169 insertions, 5 deletions
diff --git a/Android.mk b/Android.mk
index 79d13d9..7dfa6a0 100644
--- a/Android.mk
+++ b/Android.mk
@@ -322,15 +322,17 @@ LOCAL_SRC_FILES += \
media/java/android/media/IRemoteVolumeObserver.aidl \
media/java/android/media/IRingtonePlayer.aidl \
media/java/android/media/IVolumeController.aidl \
+ media/java/android/media/browse/IMediaBrowserService.aidl \
+ media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl \
+ media/java/android/media/projection/IMediaProjection.aidl \
+ media/java/android/media/projection/IMediaProjectionCallback.aidl \
+ media/java/android/media/projection/IMediaProjectionManager.aidl \
media/java/android/media/routing/IMediaRouteService.aidl \
media/java/android/media/routing/IMediaRouteClientCallback.aidl \
media/java/android/media/routing/IMediaRouter.aidl \
media/java/android/media/routing/IMediaRouterDelegate.aidl \
media/java/android/media/routing/IMediaRouterRoutingCallback.aidl \
media/java/android/media/routing/IMediaRouterStateCallback.aidl \
- media/java/android/media/projection/IMediaProjection.aidl \
- media/java/android/media/projection/IMediaProjectionCallback.aidl \
- media/java/android/media/projection/IMediaProjectionManager.aidl \
media/java/android/media/session/IActiveSessionsListener.aidl \
media/java/android/media/session/ISessionController.aidl \
media/java/android/media/session/ISessionControllerCallback.aidl \
diff --git a/api/current.txt b/api/current.txt
index e736d78..5688d14 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -16154,6 +16154,77 @@ package android.media.audiofx {
}
+package android.media.browse {
+
+ public final class MediaBrowser {
+ ctor public MediaBrowser(android.content.Context, android.content.ComponentName, android.media.browse.MediaBrowser.ConnectionCallback, android.os.Bundle);
+ method public void connect();
+ method public void disconnect();
+ method public android.net.Uri getRoot();
+ method public android.media.session.MediaSession.Token getSessionToken();
+ method public boolean isConnected();
+ method public void loadThumbnail(android.net.Uri, int, int, int, android.media.browse.MediaBrowser.ThumbnailCallback);
+ method public void subscribe(android.net.Uri, android.media.browse.MediaBrowser.SubscriptionCallback);
+ method public void unsubscribe(android.net.Uri);
+ }
+
+ public static class MediaBrowser.ConnectionCallback {
+ ctor public MediaBrowser.ConnectionCallback();
+ method public void onConnected();
+ method public void onConnectionFailed();
+ method public void onConnectionSuspended();
+ }
+
+ public static abstract class MediaBrowser.SubscriptionCallback {
+ ctor public MediaBrowser.SubscriptionCallback();
+ method public void onChildrenLoaded(android.net.Uri, java.util.List<android.media.browse.MediaBrowserItem>);
+ method public void onError(android.net.Uri);
+ }
+
+ public static abstract class MediaBrowser.ThumbnailCallback {
+ ctor public MediaBrowser.ThumbnailCallback();
+ method public void onError(android.net.Uri);
+ method public void onThumbnailLoaded(android.net.Uri, android.graphics.Bitmap);
+ }
+
+ public final class MediaBrowserItem implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.os.Bundle getExtras();
+ method public int getFlags();
+ method public java.lang.CharSequence getSummary();
+ method public java.lang.CharSequence getTitle();
+ method public android.net.Uri getUri();
+ method public boolean isBrowsable();
+ method public boolean isPlayable();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ field public static final int FLAG_BROWSABLE = 1; // 0x1
+ field public static final int FLAG_PLAYABLE = 2; // 0x2
+ }
+
+ public static final class MediaBrowserItem.Builder {
+ ctor public MediaBrowserItem.Builder(android.net.Uri, int, java.lang.CharSequence);
+ method public android.media.browse.MediaBrowserItem build();
+ method public android.media.browse.MediaBrowserItem.Builder setExtras(android.os.Bundle);
+ method public android.media.browse.MediaBrowserItem.Builder setSummary(java.lang.CharSequence);
+ }
+
+ public abstract class MediaBrowserService extends android.app.Service {
+ ctor public MediaBrowserService();
+ method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
+ method public android.media.session.MediaSession.Token getSessionToken();
+ method public void notifyChange();
+ method public void notifyChildrenChanged(android.net.Uri);
+ method public android.os.IBinder onBind(android.content.Intent);
+ method public abstract android.net.Uri onGetRoot(java.lang.String, int, android.os.Bundle);
+ method public abstract android.graphics.Bitmap onGetThumbnail(android.net.Uri, int, int, int);
+ method public abstract java.util.List<android.media.browse.MediaBrowserItem> onLoadChildren(android.net.Uri);
+ method public void setSessionToken(android.media.session.MediaSession.Token);
+ field public static final java.lang.String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+ }
+
+}
+
package android.media.effect {
public abstract class Effect {
diff --git a/media/java/android/media/browse/IMediaBrowserService.aidl b/media/java/android/media/browse/IMediaBrowserService.aidl
new file mode 100644
index 0000000..4b2cb9d
--- /dev/null
+++ b/media/java/android/media/browse/IMediaBrowserService.aidl
@@ -0,0 +1,20 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+
+package android.media.browse;
+
+import android.media.browse.IMediaBrowserServiceCallbacks;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * Media API allows clients to browse through hierarchy of a user’s media collection,
+ * playback a specific media entry and interact with the now playing queue.
+ * @hide
+ */
+oneway interface IMediaBrowserService {
+ void connect(String pkg, in Bundle rootHints, IMediaBrowserServiceCallbacks callbacks);
+ void disconnect(IMediaBrowserServiceCallbacks callbacks);
+
+ void addSubscription(in Uri uri, IMediaBrowserServiceCallbacks callbacks);
+ void removeSubscription(in Uri uri, IMediaBrowserServiceCallbacks callbacks);
+} \ No newline at end of file
diff --git a/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl b/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
new file mode 100644
index 0000000..ead7624
--- /dev/null
+++ b/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+
+package android.media.browse;
+
+import android.content.pm.ParceledListSlice;
+import android.media.session.MediaSession;
+import android.net.Uri;
+
+/**
+ * Media API allows clients to browse through hierarchy of a user’s media collection,
+ * playback a specific media entry and interact with the now playing queue.
+ * @hide
+ */
+oneway interface IMediaBrowserServiceCallbacks {
+ /**
+ * Invoked when the connected has been established.
+ * @param root The root Uri for browsing.
+ * @param session The {@link MediaSession.Token media session token} that can be used to control
+ * the playback of the media app.
+ */
+ void onConnect(in Uri root, in MediaSession.Token session);
+ void onConnectFailed();
+ void onLoadChildren(in Uri uri, in ParceledListSlice list);
+}
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
new file mode 100644
index 0000000..beec5f9
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.browse;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Browses media content offered by a link MediaBrowserService.
+ * <p>
+ * This object is not thread-safe. All calls should happen on the thread on which the browser
+ * was constructed.
+ * </p>
+ */
+public final class MediaBrowser {
+ private static final String TAG = "MediaBrowser";
+ private static final boolean DBG = false;
+
+ private static final int CONNECT_STATE_DISCONNECTED = 0;
+ private static final int CONNECT_STATE_CONNECTING = 1;
+ private static final int CONNECT_STATE_CONNECTED = 2;
+ private static final int CONNECT_STATE_SUSPENDED = 3;
+
+ private final Context mContext;
+ private final ComponentName mServiceComponent;
+ private final ConnectionCallback mCallback;
+ private final Bundle mRootHints;
+ private final Handler mHandler = new Handler();
+ private final ArrayMap<Uri,Subscription> mSubscriptions =
+ new ArrayMap<Uri, MediaBrowser.Subscription>();
+
+ private int mState = CONNECT_STATE_DISCONNECTED;
+ private MediaServiceConnection mServiceConnection;
+ private IMediaBrowserService mServiceBinder;
+ private IMediaBrowserServiceCallbacks mServiceCallbacks;
+ private Uri mRootUri;
+ private MediaSession.Token mMediaSessionToken;
+
+ /**
+ * Creates a media browser for the specified media browse service.
+ *
+ * @param context The context.
+ * @param serviceComponent The component name of the media browse service.
+ * @param callback The connection callback.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browse service when connecting and retrieving the root uri
+ * for browsing, or null if none. The contents of this bundle may affect
+ * the information returned when browsing.
+ */
+ public MediaBrowser(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (serviceComponent == null) {
+ throw new IllegalArgumentException("service component must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("connection callback must not be null");
+ }
+ mContext = context;
+ mServiceComponent = serviceComponent;
+ mCallback = callback;
+ mRootHints = rootHints;
+ }
+
+ /**
+ * Connects to the media browse service.
+ * <p>
+ * The connection callback specified in the constructor will be invoked
+ * when the connection completes or fails.
+ * </p>
+ */
+ public void connect() {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ throw new IllegalStateException("connect() called while not disconnected (state="
+ + getStateLabel(mState) + ")");
+ }
+ // TODO: remove this extra check.
+ if (DBG) {
+ if (mServiceConnection != null) {
+ throw new RuntimeException("mServiceConnection should be null. Instead it is "
+ + mServiceConnection);
+ }
+ }
+ if (mServiceBinder != null) {
+ throw new RuntimeException("mServiceBinder should be null. Instead it is "
+ + mServiceBinder);
+ }
+ if (mServiceCallbacks != null) {
+ throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
+ + mServiceCallbacks);
+ }
+
+ mState = CONNECT_STATE_CONNECTING;
+
+ final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+ intent.setComponent(mServiceComponent);
+
+ final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
+
+ try {
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed binding to service " + mServiceComponent);
+
+ // Tell them that it didn't work. We are already on the main thread,
+ // but we don't want to do callbacks inside of connect(). So post it,
+ // and then check that we are on the same ServiceConnection. We know
+ // we won't also get an onServiceConnected or onServiceDisconnected,
+ // so we won't be doing double callbacks.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Ensure that nobody else came in or tried to connect again.
+ if (thisConnection == mServiceConnection) {
+ forceCloseConnection();
+ mCallback.onConnectionFailed();
+ }
+ }
+ });
+ }
+
+ if (DBG) {
+ Log.d(TAG, "connect...");
+ dump();
+ }
+ }
+
+ /**
+ * Disconnects from the media browse service.
+ * @more
+ * After this, no more callbacks will be received.
+ */
+ public void disconnect() {
+ // It's ok to call this any state, because allowing this lets apps not have
+ // to check isConnected() unnecessarily. They won't appreciate the extra
+ // assertions for this. We do everything we can here to go back to a sane state.
+ if (mServiceCallbacks != null) {
+ try {
+ mServiceBinder.disconnect(mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // We are disconnecting anyway. Log, just for posterity but it's not
+ // a big problem.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ }
+ }
+ forceCloseConnection();
+
+ if (DBG) {
+ Log.d(TAG, "disconnect...");
+ dump();
+ }
+ }
+
+ /**
+ * Null out the variables and unbind from the service. This doesn't include
+ * calling disconnect on the service, because we only try to do that in the
+ * clean shutdown cases.
+ * <p>
+ * Everywhere that calls this EXCEPT for disconnect() should follow it with
+ * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
+ * for a clean shutdown, but everywhere else is a dirty shutdown and should
+ * notify the app.
+ */
+ private void forceCloseConnection() {
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ mState = CONNECT_STATE_DISCONNECTED;
+ mServiceConnection = null;
+ mServiceBinder = null;
+ mServiceCallbacks = null;
+ mRootUri = null;
+ mMediaSessionToken = null;
+ }
+
+ /**
+ * Returns whether the browser is connected to the service.
+ */
+ public boolean isConnected() {
+ return mState == CONNECT_STATE_CONNECTED;
+ }
+
+ /**
+ * Gets the root Uri.
+ * <p>
+ * Note that the root uri may become invalid or change when when the
+ * browser is disconnected.
+ * </p>
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @NonNull Uri getRoot() {
+ if (mState != CONNECT_STATE_CONNECTED) {
+ throw new IllegalStateException("getSessionToken() called while not connected (state="
+ + getStateLabel(mState) + ")");
+ }
+ return mRootUri;
+ }
+
+ /**
+ * Gets the media session token associated with the media browser.
+ * <p>
+ * Note that the session token may become invalid or change when when the
+ * browser is disconnected.
+ * </p>
+ *
+ * @return The session token for the browser, never null.
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @NonNull MediaSession.Token getSessionToken() {
+ if (mState != CONNECT_STATE_CONNECTED) {
+ throw new IllegalStateException("getSessionToken() called while not connected (state="
+ + mState + ")");
+ }
+ return mMediaSessionToken;
+ }
+
+ /**
+ * Queries for information about the media items that are contained within
+ * the specified Uri and subscribes to receive updates when they change.
+ * <p>
+ * The list of subscriptions is maintained even when not connected and is
+ * restored after reconnection. It is ok to subscribe while not connected
+ * but the results will not be returned until the connection completes.
+ * </p><p>
+ * If the uri is already subscribed with a different callback then the new
+ * callback will replace the previous one.
+ * </p>
+ *
+ * @param parentUri The uri of the parent media item whose list of children
+ * will be subscribed.
+ * @param callback The callback to receive the list of children.
+ */
+ public void subscribe(@NonNull Uri parentUri, @NonNull SubscriptionCallback callback) {
+ // Check arguments.
+ if (parentUri == null) {
+ throw new IllegalArgumentException("parentUri is null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+
+ // Update or create the subscription.
+ Subscription sub = mSubscriptions.get(parentUri);
+ boolean newSubscription = sub == null;
+ if (newSubscription) {
+ sub = new Subscription(parentUri);
+ mSubscriptions.put(parentUri, sub);
+ }
+ sub.callback = callback;
+
+ // If we are connected, tell the service that we are watching. If we aren't
+ // connected, the service will be told when we connect.
+ if (mState == CONNECT_STATE_CONNECTED && newSubscription) {
+ try {
+ mServiceBinder.addSubscription(parentUri, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + parentUri);
+ }
+ }
+ }
+
+ /**
+ * Unsubscribes for changes to the children of the specified Uri.
+ * <p>
+ * The query callback will no longer be invoked for results associated with
+ * this Uri once this method returns.
+ * </p>
+ *
+ * @param parentUri The uri of the parent media item whose list of children
+ * will be unsubscribed.
+ */
+ public void unsubscribe(@NonNull Uri parentUri) {
+ // Check arguments.
+ if (parentUri == null) {
+ throw new IllegalArgumentException("parentUri is null");
+ }
+
+ // Remove from our list.
+ final Subscription sub = mSubscriptions.remove(parentUri);
+
+ // Tell the service if necessary.
+ if (mState == CONNECT_STATE_CONNECTED && sub != null) {
+ try {
+ mServiceBinder.removeSubscription(parentUri, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "removeSubscription failed with RemoteException parentUri=" + parentUri);
+ }
+ }
+ }
+
+ /**
+ * Loads the thumbnail of a media item.
+ *
+ * @param uri The uri of the media item.
+ * @param width The preferred width of the icon in dp.
+ * @param height The preferred width of the icon in dp.
+ * @param density The preferred density of the icon. Must be one of the android
+ * density buckets.
+ * @param callback The callback to receive the thumbnail.
+ *
+ * @throws IllegalStateException if not connected. TODO: Is this restriction necessary?
+ */
+ public void loadThumbnail(@NonNull Uri uri, int width, int height, int density,
+ @NonNull ThumbnailCallback callback) {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * For debugging.
+ */
+ private static String getStateLabel(int state) {
+ switch (state) {
+ case CONNECT_STATE_DISCONNECTED:
+ return "CONNECT_STATE_DISCONNECTED";
+ case CONNECT_STATE_CONNECTING:
+ return "CONNECT_STATE_CONNECTING";
+ case CONNECT_STATE_CONNECTED:
+ return "CONNECT_STATE_CONNECTED";
+ case CONNECT_STATE_SUSPENDED:
+ return "CONNECT_STATE_SUSPENDED";
+ default:
+ return "UNKNOWN/" + state;
+ }
+ }
+
+ private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
+ final Uri root, final MediaSession.Token session) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Check to make sure there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onConnect")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState="
+ + getStateLabel(mState) + "... ignoring");
+ return;
+ }
+ mRootUri = root;
+ mMediaSessionToken = session;
+ mState = CONNECT_STATE_CONNECTED;
+
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mCallback.onConnected();
+
+ // we may receive some subscriptions before we are connected, so re-subscribe
+ // everything now
+ for (Uri uri : mSubscriptions.keySet()) {
+ try {
+ mServiceBinder.addSubscription(uri, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + uri);
+ }
+ }
+
+ }
+ });
+ }
+
+ private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Log.e(TAG, "onConnectFailed for " + mServiceComponent);
+
+ // Check to make sure there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onConnectFailed")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState="
+ + getStateLabel(mState) + "... ignoring");
+ return;
+ }
+
+ // Clean up
+ forceCloseConnection();
+
+ // Tell the app.
+ mCallback.onConnectionFailed();
+ }
+ });
+ }
+
+ private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, final Uri uri,
+ final ParceledListSlice list) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Check that there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onLoadChildren")) {
+ return;
+ }
+
+ List<MediaBrowserItem> data = list.getList();
+ if (DBG) {
+ Log.d(TAG, "onLoadChildren for " + mServiceComponent + " uri=" + uri);
+ }
+ if (data == null) {
+ data = Collections.emptyList();
+ }
+
+ // Check that the subscription is still subscribed.
+ final Subscription subscription = mSubscriptions.get(uri);
+ if (subscription == null) {
+ if (DBG) {
+ Log.d(TAG, "onLoadChildren for uri that isn't subscribed uri="
+ + uri);
+ }
+ return;
+ }
+
+ // Tell the app.
+ subscription.callback.onChildrenLoaded(uri, data);
+ }
+ });
+ }
+
+
+ /**
+ * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
+ */
+ private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
+ if (mServiceCallbacks != callback) {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
+ + mServiceCallbacks + " this=" + this);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private ServiceCallbacks getNewServiceCallbacks() {
+ return new ServiceCallbacks(this);
+ }
+
+ /**
+ * Log internal state.
+ * @hide
+ */
+ void dump() {
+ Log.d(TAG, "MediaBrowser...");
+ Log.d(TAG, " mServiceComponent=" + mServiceComponent);
+ Log.d(TAG, " mCallback=" + mCallback);
+ Log.d(TAG, " mRootHints=" + mRootHints);
+ Log.d(TAG, " mState=" + getStateLabel(mState));
+ Log.d(TAG, " mServiceConnection=" + mServiceConnection);
+ Log.d(TAG, " mServiceBinder=" + mServiceBinder);
+ Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks);
+ Log.d(TAG, " mRootUri=" + mRootUri);
+ Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
+ }
+
+
+ /**
+ * Callbacks for connection related events.
+ */
+ public static class ConnectionCallback {
+ /**
+ * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Invoked when the client is disconnected from the media browser.
+ */
+ public void onConnectionSuspended() {
+ }
+
+ /**
+ * Invoked when the connection to the media browser failed.
+ */
+ public void onConnectionFailed() {
+ }
+ }
+
+ /**
+ * Callbacks for subscription related events.
+ */
+ public static abstract class SubscriptionCallback {
+ /**
+ * Called when the list of children is loaded or updated.
+ */
+ public void onChildrenLoaded(@NonNull Uri parentUri,
+ @NonNull List<MediaBrowserItem> children) {
+ }
+
+ /**
+ * Called when the Uri doesn't exist or other errors in subscribing.
+ * <p>
+ * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
+ * called, because some errors may heal themselves.
+ * </p>
+ */
+ public void onError(@NonNull Uri uri) {
+ }
+ }
+
+ /**
+ * Callbacks for thumbnail loading.
+ */
+ public static abstract class ThumbnailCallback {
+ /**
+ * Called when the thumbnail is loaded.
+ */
+ public void onThumbnailLoaded(@NonNull Uri uri, @NonNull Bitmap bitmap) {
+ }
+
+ /**
+ * Called when the Uri doesn’t exist or the bitmap cannot be loaded.
+ */
+ public void onError(@NonNull Uri uri) {
+ }
+ }
+
+ /**
+ * ServiceConnection to the other app.
+ */
+ private class MediaServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ if (DBG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
+ + " binder=" + binder);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't called
+ // disconnect().
+ if (!isCurrent("onServiceConnected")) {
+ return;
+ }
+
+ // Save their binder
+ mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
+
+ // We make a new mServiceCallbacks each time we connect so that we can drop
+ // responses from previous connections.
+ mServiceCallbacks = getNewServiceCallbacks();
+
+ // Call connect, which is async. When we get a response from that we will
+ // say that we're connected.
+ try {
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Connect failed, which isn't good. But the auto-reconnect on the service
+ // will take over and we will come back. We will also get the
+ // onServiceDisconnected, which has all the cleanup code. So let that do it.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DBG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
+ + " this=" + this + " mServiceConnection=" + mServiceConnection);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't called
+ // disconnect().
+ if (!isCurrent("onServiceDisconnected")) {
+ return;
+ }
+
+ // Clear out what we set in onServiceConnected
+ mServiceBinder = null;
+ mServiceCallbacks = null;
+
+ // And tell the app that it's suspended.
+ mState = CONNECT_STATE_SUSPENDED;
+ mCallback.onConnectionSuspended();
+ }
+
+ /**
+ * Return true if this is the current ServiceConnection. Also logs if it's not.
+ */
+ private boolean isCurrent(String funcName) {
+ if (mServiceConnection != this) {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ // Check mState, because otherwise this log is noisy.
+ Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
+ + mServiceConnection + " this=" + this);
+ }
+ return false;
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Callbacks from the service.
+ */
+ private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
+ private WeakReference<MediaBrowser> mMediaBrowser;
+
+ public ServiceCallbacks(MediaBrowser mediaBrowser) {
+ mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
+ }
+
+ /**
+ * The other side has acknowledged our connection. The parameters to this function
+ * are the initial data as requested.
+ */
+ @Override
+ public void onConnect(final Uri root, final MediaSession.Token session) {
+ MediaBrowser mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onServiceConnected(this, root, session);
+ }
+ }
+
+ /**
+ * The other side does not like us. Tell the app via onConnectionFailed.
+ */
+ @Override
+ public void onConnectFailed() {
+ MediaBrowser mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onConnectionFailed(this);
+ }
+ }
+
+ @Override
+ public void onLoadChildren(final Uri uri, final ParceledListSlice list) {
+ MediaBrowser mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onLoadChildren(this, uri, list);
+ }
+ }
+ }
+
+ private static class Subscription {
+ final Uri uri;
+ SubscriptionCallback callback;
+
+ Subscription(Uri u) {
+ this.uri = u;
+ }
+ }
+}
diff --git a/media/java/android/media/browse/MediaBrowserItem.java b/media/java/android/media/browse/MediaBrowserItem.java
new file mode 100644
index 0000000..119f687
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowserItem.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.browse;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.net.Uri;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Describes a media item in the list of items offered by a {@link MediaBrowserService}.
+ */
+public final class MediaBrowserItem implements Parcelable {
+ private final Uri mUri;
+ private final int mFlags;
+ private final CharSequence mTitle;
+ private final CharSequence mSummary;
+ private final Bundle mExtras;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+ public @interface Flags { }
+
+ /**
+ * Flag: Indicates that the item has children of its own.
+ */
+ public static final int FLAG_BROWSABLE = 1 << 0;
+
+ /**
+ * Flag: Indicates that the item is playable.
+ * <p>
+ * The Uri of this item may be passed to link android.media.session.MediaController#play(Uri)
+ * to start playing it.
+ * </p>
+ */
+ public static final int FLAG_PLAYABLE = 1 << 1;
+
+ /**
+ * Initialize a MediaBrowserItem object.
+ */
+ private MediaBrowserItem(@NonNull Uri uri, int flags, @NonNull CharSequence title,
+ CharSequence summary, Bundle extras) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri can not be null");
+ }
+ if (title == null) {
+ throw new IllegalArgumentException("title can not be null");
+ }
+ mUri = uri;
+ mFlags = flags;
+ mTitle = title;
+ mSummary = summary;
+ mExtras = extras;
+ }
+
+ /**
+ * Private constructor.
+ */
+ private MediaBrowserItem(Parcel in) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ mFlags = in.readInt();
+ mTitle = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mSummary = in.readCharSequence();
+ } else {
+ mSummary = null;
+ }
+ if (in.readInt() != 0) {
+ mExtras = Bundle.CREATOR.createFromParcel(in);
+ } else {
+ mExtras = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mUri.writeToParcel(out, flags);
+ out.writeInt(mFlags);
+ out.writeCharSequence(mTitle);
+ if (mSummary != null) {
+ out.writeInt(1);
+ out.writeCharSequence(mSummary);
+ } else {
+ out.writeInt(0);
+ }
+ if (mExtras != null) {
+ out.writeInt(1);
+ mExtras.writeToParcel(out, flags);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Parcelable.Creator<MediaBrowserItem> CREATOR =
+ new Parcelable.Creator<MediaBrowserItem>() {
+ @Override
+ public MediaBrowserItem createFromParcel(Parcel in) {
+ return new MediaBrowserItem(in);
+ }
+
+ @Override
+ public MediaBrowserItem[] newArray(int size) {
+ return new MediaBrowserItem[size];
+ }
+ };
+
+ /**
+ * Gets the Uri of the item.
+ */
+ public @NonNull Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Gets the flags of the item.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether this item is browsable.
+ * @see #FLAG_BROWSABLE
+ */
+ public boolean isBrowsable() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ /**
+ * Returns whether this item is playable.
+ * @see #FLAG_PLAYABLE
+ */
+ public boolean isPlayable() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ /**
+ * Gets the title of the item.
+ * @more
+ * The title will be shown as the first line of text when
+ * describing each item to the user.
+ */
+ public @NonNull CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Gets summary of the item, or null if none.
+ * @more
+ * The summary will be shown as the second line of text when
+ * describing each item to the user.
+ */
+ public @Nullable CharSequence getSummary() {
+ return mSummary;
+ }
+
+ /**
+ * Gets additional service-specified extras about the
+ * item or its content, or null if none.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Builder for {@link MediaBrowserItem} objects.
+ */
+ public static final class Builder {
+ private final Uri mUri;
+ private final int mFlags;
+ private final CharSequence mTitle;
+ private CharSequence mSummary;
+ private Bundle mExtras;
+
+ /**
+ * Creates an item builder.
+ */
+ public Builder(@NonNull Uri uri, @Flags int flags, @NonNull CharSequence title) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri can not be null");
+ }
+ if (title == null) {
+ throw new IllegalArgumentException("title can not be null");
+ }
+ mUri = uri;
+ mFlags = flags;
+ mTitle = title;
+ }
+
+ /**
+ * Sets summary of the item, or null if none.
+ */
+ public @NonNull Builder setSummary(@Nullable CharSequence summary) {
+ mSummary = summary;
+ return this;
+ }
+
+ /**
+ * Sets additional service-specified extras about the
+ * item or its content, or null if none.
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the item.
+ */
+ public @NonNull MediaBrowserItem build() {
+ return new MediaBrowserItem(mUri, mFlags, mTitle, mSummary, mExtras);
+ }
+ }
+}
+
diff --git a/media/java/android/media/browse/MediaBrowserService.java b/media/java/android/media/browse/MediaBrowserService.java
new file mode 100644
index 0000000..ceb4b03
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowserService.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.browse;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Base class for media browse services.
+ * <p>
+ * Media browse services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession}.
+ * </p>
+ *
+ * To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_ACTION} action.
+ *
+ * For example:
+ * </p><pre>
+ * &lt;service android:name=".MyMediaBrowserService"
+ * android:label="&#64;string/service_name" >
+ * &lt;intent-filter>
+ * &lt;action android:name="android.media.browse.MediaBrowserService" />
+ * &lt;/intent-filter>
+ * &lt;/service>
+ * </pre>
+ *
+ */
+public abstract class MediaBrowserService extends Service {
+ private static final String TAG = "MediaBrowserService";
+
+ private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
+ private final Handler mHandler = new Handler();
+ private ServiceBinder mBinder;
+ MediaSession.Token mSession;
+
+ /**
+ * All the info about a connection.
+ */
+ private class ConnectionRecord {
+ String pkg;
+ Bundle rootHints;
+ IMediaBrowserServiceCallbacks callbacks;
+ Uri root;
+ HashSet<Uri> subscriptions = new HashSet();
+ }
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+
+ private class ServiceBinder extends IMediaBrowserService.Stub {
+ @Override
+ public void connect(final String pkg, final Bundle rootHints,
+ final IMediaBrowserServiceCallbacks callbacks) {
+
+ final int uid = Binder.getCallingUid();
+ if (!isValidPackage(pkg, uid)) {
+ throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
+ + " package=" + pkg);
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Clear out the old subscriptions. We are getting new ones.
+ mConnections.remove(b);
+
+ final ConnectionRecord connection = new ConnectionRecord();
+ connection.pkg = pkg;
+ connection.rootHints = rootHints;
+ connection.callbacks = callbacks;
+
+ connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
+
+ // If they didn't return something, don't allow this client.
+ if (connection.root == null) {
+ Log.i(TAG, "No root for client " + pkg + " from service "
+ + getClass().getName());
+ try {
+ callbacks.onConnectFailed();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
+ + "pkg=" + pkg);
+ }
+ } else {
+ try {
+ mConnections.put(b, connection);
+ callbacks.onConnect(connection.root, mSession);
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Calling onConnect() failed. Dropping client. "
+ + "pkg=" + pkg);
+ mConnections.remove(b);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Clear out the old subscriptions. We are getting new ones.
+ final ConnectionRecord old = mConnections.remove(b);
+ if (old != null) {
+ // TODO
+ }
+ }
+ });
+ }
+
+
+ @Override
+ public void addSubscription(final Uri uri, final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Get the record for the connection
+ final ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "addSubscription for callback that isn't registered uri="
+ + uri);
+ return;
+ }
+
+ MediaBrowserService.this.addSubscription(uri, connection);
+ }
+ });
+ }
+
+ @Override
+ public void removeSubscription(final Uri uri,
+ final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "removeSubscription for callback that isn't registered uri="
+ + uri);
+ return;
+ }
+ if (!connection.subscriptions.remove(uri)) {
+ Log.w(TAG, "removeSubscription called for " + uri
+ + " which is not subscribed");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBinder = new ServiceBinder();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (SERVICE_ACTION.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ }
+
+ /**
+ * Called to get the root uri for browsing by a particular client.
+ * <p>
+ * The implementation should verify that the client package has
+ * permission to access browse media information before returning
+ * the root uri; it should return null if the client is not
+ * allowed to access this information.
+ * </p>
+ *
+ * @param clientPackageName The package name of the application
+ * which is requesting access to browse media.
+ * @param clientUid The uid of the application which is requesting
+ * access to browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browse service when connecting and retrieving the root uri
+ * for browsing, or null if none. The contents of this bundle may affect
+ * the information returned when browsing.
+ */
+ public abstract @Nullable Uri onGetRoot(@NonNull String clientPackageName, int clientUid,
+ @Nullable Bundle rootHints);
+
+ /**
+ * Called to get information about the children of a media item.
+ *
+ * @param parentUri The uri of the parent media item whose
+ * children are to be queried.
+ * @return The list of children, or null if the uri is invalid.
+ */
+ public abstract @Nullable List<MediaBrowserItem> onLoadChildren(@NonNull Uri parentUri);
+
+ /**
+ * Called to get the thumbnail of a particular media item.
+ *
+ * @param uri The uri of the media item.
+ * @param width The requested width of the icon in dp.
+ * @param height The requested height of the icon in dp.
+ * @param density The requested density of the icon. This is the approximate density of the
+ * screen on which the icon will be displayed. This density will be one of
+ * the android density buckets.
+ * @return The file descriptor of the thumbnail, which may then be loaded
+ * using a bitmap factory, or null if the item does not have a thumbnail.
+ */
+ public abstract @Nullable Bitmap onGetThumbnail(@NonNull Uri uri,
+ int width, int height, int density);
+
+ /**
+ * Call to set the media session.
+ * <p>
+ * This must be called before onCreate returns.
+ *
+ * @return The media session token, must not be null.
+ */
+ public void setSessionToken(MediaSession.Token token) {
+ if (token == null) {
+ throw new IllegalStateException(this.getClass().getName()
+ + ".onCreateSession() set invalid MediaSession.Token");
+ }
+ mSession = token;
+ }
+
+ /**
+ * Gets the session token, or null if it has not yet been created
+ * or if it has been destroyed.
+ */
+ public @Nullable MediaSession.Token getSessionToken() {
+ return mSession;
+ }
+
+ /**
+ * Notifies all connected media browsers that the content of
+ * the browse service has changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ */
+ public void notifyChange() {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * Notifies all connected media browsers that the children of
+ * the specified Uri have changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ *
+ * @param parentUri The uri of the parent media item whose
+ * children changed.
+ */
+ public void notifyChildrenChanged(@NonNull Uri parentUri) {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * Return whether the given package is one of the ones that is owned by the uid.
+ */
+ private boolean isValidPackage(String pkg, int uid) {
+ if (pkg == null) {
+ return false;
+ }
+ final PackageManager pm = getPackageManager();
+ final String[] packages = pm.getPackagesForUid(uid);
+ final int N = packages.length;
+ for (int i=0; i<N; i++) {
+ if (packages[i].equals(pkg)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Save the subscription and if it is a new subscription send the results.
+ */
+ private void addSubscription(Uri uri, ConnectionRecord connection) {
+ // Save the subscription
+ final boolean added = connection.subscriptions.add(uri);
+
+ // If this is a new subscription, send the results
+ if (added) {
+ performLoadChildren(uri, connection);
+ }
+ }
+
+ /**
+ * Call onLoadChildren and then send the results back to the connection.
+ * <p>
+ * Callers must make sure that this connection is still connected.
+ * <p>
+ * TODO: Think about caching and combining these calls.
+ */
+ private void performLoadChildren(Uri uri, ConnectionRecord connection) {
+ final List<MediaBrowserItem> list = onLoadChildren(uri);
+ if (list == null) {
+ throw new IllegalStateException("onLoadChildren returned null for uri " + uri);
+ }
+ final ParceledListSlice<MediaBrowserItem> pls = new ParceledListSlice(list);
+ try {
+ connection.callbacks.onLoadChildren(uri, pls);
+ } catch (RemoteException ex) {
+ // The other side is in the process of crashing.
+ Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
+ + " package=" + connection.pkg);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 9945909..ca9f6eb 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -185,8 +185,8 @@ import javax.net.ssl.SSLSession;
public class ConnectivityService extends IConnectivityManager.Stub {
private static final String TAG = "ConnectivityService";
- private static final boolean DBG = true;
- private static final boolean VDBG = true; // STOPSHIP
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false; // STOPSHIP
// network sampling debugging
private static final boolean SAMPLE_DBG = false;
diff --git a/tests/MusicBrowserDemo/Android.mk b/tests/MusicBrowserDemo/Android.mk
new file mode 100644
index 0000000..207774b
--- /dev/null
+++ b/tests/MusicBrowserDemo/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := MusicBrowserDemo
+#LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-v4 \
+ android-support-v7-appcompat
+
+LOCAL_RESOURCE_DIR := \
+ $(LOCAL_PATH)/res \
+ frameworks/support/v7/appcompat/res
+LOCAL_PROGUARD_ENABLED := disabled
+#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_AAPT_FLAGS := \
+ --auto-add-overlay \
+ --extra-packages android.support.v7.appcompat
+include $(BUILD_PACKAGE)
diff --git a/tests/MusicBrowserDemo/AndroidManifest.xml b/tests/MusicBrowserDemo/AndroidManifest.xml
new file mode 100644
index 0000000..d2acfe2
--- /dev/null
+++ b/tests/MusicBrowserDemo/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.musicbrowserdemo"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk
+ android:minSdkVersion="9"
+ android:targetSdkVersion="19" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme"
+ >
+
+ <activity
+ android:name="com.example.android.musicbrowserdemo.MainActivity"
+ android:label="@string/app_name"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..47d6854
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..01b53fd
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..af762f2
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..eef47aa
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/values/strings.xml b/tests/MusicBrowserDemo/res/values/strings.xml
new file mode 100644
index 0000000..858f278
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <string name="app_name">Music Browser</string>
+
+</resources>
diff --git a/tests/MusicBrowserDemo/res/values/styles.xml b/tests/MusicBrowserDemo/res/values/styles.xml
new file mode 100644
index 0000000..b83662d
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="Theme.AppCompat.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources>
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
new file mode 100644
index 0000000..c0f3a7f
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicbrowserdemo;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.browse.MediaBrowserService;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// TODO: Include an icon.
+
+public class AppListFragment extends ListFragment {
+
+ private Adapter mAdapter;
+ private List<Item> mItems;
+
+ public AppListFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final Item item = mItems.get(position);
+
+ Log.i("AppListFragment", "Item clicked: " + position + " -- " + item.component);
+
+ final BrowserListFragment fragment = new BrowserListFragment();
+
+ final Bundle args = new Bundle();
+ args.putParcelable(BrowserListFragment.ARG_COMPONENT, item.component);
+ fragment.setArguments(args);
+
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ private static class Item {
+ final String label;
+ final ComponentName component;
+
+ Item(String l, ComponentName c) {
+ this.label = l;
+ this.component = c;
+ }
+ }
+
+ private class Adapter extends BaseAdapter {
+ private final LayoutInflater mInflater;
+
+ Adapter() {
+ super();
+
+ final Context context = getActivity();
+ mInflater = LayoutInflater.from(context);
+
+ // Load the data
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+ final List<ResolveInfo> list = pm.queryIntentServices(intent, 0);
+ final int N = list.size();
+ mItems = new ArrayList(N);
+ for (int i=0; i<N; i++) {
+ final ResolveInfo ri = list.get(i);
+ mItems.add(new Item(ri.loadLabel(pm).toString(), new ComponentName(
+ ri.serviceInfo.applicationInfo.packageName,
+ ri.serviceInfo.name)));
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public Item getItem(int position) {
+ return mItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
+ }
+
+ final TextView tv = (TextView)convertView;
+ final Item item = mItems.get(position);
+ tv.setText(item.label);
+
+ return convertView;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+ }
+}
+
+
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java
new file mode 100644
index 0000000..3fc468d
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicbrowserdemo;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.browse.MediaBrowser;
+import android.media.browse.MediaBrowserItem;
+import android.media.browse.MediaBrowserService;
+import android.os.Bundle;
+import android.net.Uri;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BrowserListFragment extends ListFragment {
+ private static final String TAG = "BrowserListFragment";
+
+ // Hints
+ public static final String HINT_DISPLAY = "com.example.android.musicbrowserdemo.DISPLAY";
+
+ // For args
+ public static final String ARG_COMPONENT = "component";
+ public static final String ARG_URI = "uri";
+
+ private Adapter mAdapter;
+ private List<Item> mItems = new ArrayList();
+ private ComponentName mComponent;
+ private Uri mUri;
+ private MediaBrowser mBrowser;
+
+ private static class Item {
+ final MediaBrowserItem media;
+
+ Item(MediaBrowserItem m) {
+ this.media = m;
+ }
+ }
+
+ public BrowserListFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Log.d(TAG, "onActivityCreated -- " + hashCode());
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+
+ // Get our arguments
+ final Bundle args = getArguments();
+ mComponent = args.getParcelable(ARG_COMPONENT);
+ mUri = args.getParcelable(ARG_URI);
+
+ // A hint about who we are, so the service can customize the results if it wants to.
+ final Bundle rootHints = new Bundle();
+ rootHints.putBoolean(HINT_DISPLAY, true);
+
+ mBrowser = new MediaBrowser(getActivity(), mComponent, mConnectionCallbacks, rootHints);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mBrowser.connect();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mBrowser.disconnect();
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final Item item = mItems.get(position);
+
+ Log.i("BrowserListFragment", "Item clicked: " + position + " -- "
+ + mAdapter.getItem(position).media.getUri());
+
+ final BrowserListFragment fragment = new BrowserListFragment();
+
+ final Bundle args = new Bundle();
+ args.putParcelable(BrowserListFragment.ARG_COMPONENT, mComponent);
+ args.putParcelable(BrowserListFragment.ARG_URI, item.media.getUri());
+ fragment.setArguments(args);
+
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(null)
+ .commit();
+
+ }
+
+ final MediaBrowser.ConnectionCallback mConnectionCallbacks
+ = new MediaBrowser.ConnectionCallback() {
+ @Override
+ public void onConnected() {
+ Log.d(TAG, "mConnectionCallbacks.onConnected");
+ if (mUri == null) {
+ mUri = mBrowser.getRoot();
+ }
+ mBrowser.subscribe(mUri, new MediaBrowser.SubscriptionCallback() {
+ @Override
+ public void onChildrenLoaded(Uri parentUri, List<MediaBrowserItem> children) {
+ Log.d(TAG, "onChildrenLoaded parentUri=" + parentUri
+ + " children= " + children);
+ mItems.clear();
+ final int N = children.size();
+ for (int i=0; i<N; i++) {
+ mItems.add(new Item(children.get(i)));
+ }
+ mAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onError(Uri parentUri) {
+ Log.d(TAG, "onError parentUri=" + parentUri);
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ Log.d(TAG, "mConnectionCallbacks.onConnectionSuspended");
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ Log.d(TAG, "mConnectionCallbacks.onConnectionFailed");
+ }
+ };
+
+ private class Adapter extends BaseAdapter {
+ private final LayoutInflater mInflater;
+
+ Adapter() {
+ super();
+
+ final Context context = getActivity();
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public Item getItem(int position) {
+ return mItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
+ }
+
+ final TextView tv = (TextView)convertView;
+ final Item item = mItems.get(position);
+ tv.setText(item.media.getTitle());
+
+ return convertView;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+ }
+}
+
+
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java
new file mode 100644
index 0000000..ed91aad
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicbrowserdemo;
+
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+/**
+ * Main activity class.
+ */
+public class MainActivity extends FragmentActivity {
+
+ private static final String BROWSER_FRAGMENT_TAG = "browser";
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ Log.d("MainActivity", "-------------------------------------------------------");
+
+ // If we are starting afresh, start at the app list.
+ final FragmentManager fm = getSupportFragmentManager();
+ if (fm.findFragmentById(android.R.id.content) == null) {
+ fm.beginTransaction().add(android.R.id.content, new AppListFragment()).commit();
+ }
+ }
+}
+
diff --git a/tests/MusicServiceDemo/Android.mk b/tests/MusicServiceDemo/Android.mk
new file mode 100644
index 0000000..feef67a
--- /dev/null
+++ b/tests/MusicServiceDemo/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := MusicServiceDemo
+#LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-v4 \
+ android-support-v7-appcompat
+
+LOCAL_RESOURCE_DIR := \
+ $(LOCAL_PATH)/res \
+ frameworks/support/v7/appcompat/res
+LOCAL_PROGUARD_ENABLED := disabled
+#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_AAPT_FLAGS := \
+ --auto-add-overlay \
+ --extra-packages android.support.v7.appcompat
+include $(BUILD_PACKAGE)
diff --git a/tests/MusicServiceDemo/AndroidManifest.xml b/tests/MusicServiceDemo/AndroidManifest.xml
new file mode 100644
index 0000000..4178a80
--- /dev/null
+++ b/tests/MusicServiceDemo/AndroidManifest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.musicservicedemo"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <uses-sdk
+ android:minSdkVersion="9"
+ android:targetSdkVersion="19" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme"
+ >
+
+ <activity
+ android:name="com.example.android.automotive.musicplayer.MainActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <service
+ android:name=".BrowserService"
+ android:exported="true"
+ >
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowseService" />
+ </intent-filter>
+ </service>
+ </application>
+
+</manifest>
diff --git a/tests/MusicServiceDemo/proguard-project.txt b/tests/MusicServiceDemo/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/tests/MusicServiceDemo/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..47d6854
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..01b53fd
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..af762f2
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..eef47aa
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png
new file mode 100644
index 0000000..ea98c95
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/layout/activity_main.xml b/tests/MusicServiceDemo/res/layout/activity_main.xml
new file mode 100644
index 0000000..71753e3
--- /dev/null
+++ b/tests/MusicServiceDemo/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.example.android.automotive.musicplayer.MainActivity"
+ tools:ignore="MergeRootFrame" />
diff --git a/tests/MusicServiceDemo/res/layout/fragment_main.xml b/tests/MusicServiceDemo/res/layout/fragment_main.xml
new file mode 100644
index 0000000..8796e86
--- /dev/null
+++ b/tests/MusicServiceDemo/res/layout/fragment_main.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context="com.example.android.automotive.musicplayer.MainActivity$PlaceholderFragment" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name" />
+
+</RelativeLayout>
diff --git a/tests/MusicServiceDemo/res/values/colors.xml b/tests/MusicServiceDemo/res/values/colors.xml
new file mode 100644
index 0000000..44dd05d
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <color name="yellow">#ffffff00</color>
+ <color name="green">#ff00ff00</color>
+ <color name="blue">#ff0000ff</color>
+ <color name="red">#ffff0000</color>
+</resources>
diff --git a/tests/MusicServiceDemo/res/values/dimens.xml b/tests/MusicServiceDemo/res/values/dimens.xml
new file mode 100644
index 0000000..9f63ef2
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+
+</resources>
diff --git a/tests/MusicServiceDemo/res/values/strings.xml b/tests/MusicServiceDemo/res/values/strings.xml
new file mode 100644
index 0000000..14c0171
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <string name="app_name">Music Service Demo</string>
+ <string name="action_settings">Settings</string>
+ <string name="thumbs_up">Thumbs Up</string>
+ <string name="music_error">No music found</string>
+ <string name="now_playing">Now Playing</string>
+
+</resources>
diff --git a/tests/MusicServiceDemo/res/values/styles.xml b/tests/MusicServiceDemo/res/values/styles.xml
new file mode 100644
index 0000000..b83662d
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="Theme.AppCompat.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources>
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
new file mode 100644
index 0000000..9ca156f
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.app.SearchManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.res.Resources.NotFoundException;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.media.browse.MediaBrowserItem;
+import android.media.browse.MediaBrowserService;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.example.android.musicservicedemo.browser.MusicProvider;
+import com.example.android.musicservicedemo.browser.MusicProviderTask;
+import com.example.android.musicservicedemo.browser.MusicProviderTaskListener;
+import com.example.android.musicservicedemo.browser.MusicTrack;
+
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Service that implements MediaBrowserService and returns our menu hierarchy.
+ */
+public class BrowserService extends MediaBrowserService {
+ private static final String TAG = "BrowserService";
+
+ // URI paths for browsing music
+ public static final String BROWSE_ROOT_BASE_PATH = "browse";
+ public static final String NOW_PLAYING_PATH = "now_playing";
+ public static final String PIANO_BASE_PATH = "piano";
+ public static final String VOICE_BASE_PATH = "voice";
+
+ // Content URIs
+ public static final String AUTHORITY = "com.example.android.automotive.musicplayer";
+ public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+ public static final Uri BROWSE_URI = Uri.withAppendedPath(BASE_URI, BROWSE_ROOT_BASE_PATH);
+
+ // URI matcher constants for browsing paths
+ public static final int BROWSE_ROOT = 1;
+ public static final int NOW_PLAYING = 2;
+ public static final int PIANO = 3;
+ public static final int VOICE = 4;
+
+ // Map the the URI paths with the URI matcher constants
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ static {
+ sUriMatcher.addURI(AUTHORITY, BROWSE_ROOT_BASE_PATH, BROWSE_ROOT);
+ sUriMatcher.addURI(AUTHORITY, NOW_PLAYING_PATH, NOW_PLAYING);
+ sUriMatcher.addURI(AUTHORITY, PIANO_BASE_PATH, PIANO);
+ sUriMatcher.addURI(AUTHORITY, VOICE_BASE_PATH, VOICE);
+ }
+
+ // Media metadata that will be provided for a media container
+ public static final String[] MEDIA_CONTAINER_PROJECTION = {
+ "uri",
+ "title",
+ "subtitle",
+ "image_uri",
+ "supported_actions"
+ };
+
+ // MusicProvider will download the music catalog
+ private MusicProvider mMusicProvider;
+
+ private MediaSession mSession;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mSession = new MediaSession(this, "com.example.android.musicservicedemo.BrowserService");
+ setSessionToken(mSession.getSessionToken());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public Uri onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ return BROWSE_URI;
+ }
+
+ @Override
+ public List<MediaBrowserItem> onLoadChildren(Uri parentUri) {
+ final ArrayList<MediaBrowserItem> results = new ArrayList();
+
+ for (int i=0; i<10; i++) {
+ results.add(new MediaBrowserItem.Builder(Uri.withAppendedPath(BASE_URI, Integer.toString(i)),
+ MediaBrowserItem.FLAG_BROWSABLE, "Title " + i).setSummary("Summary " + i).build());
+ }
+
+ return results;
+ }
+
+ @Override
+ public Bitmap onGetThumbnail(Uri uri, int width, int height, int density) {
+ return null;
+ }
+
+ /*
+ @Override
+ public void query(final Query query, final IMetadataResultHandler metadataResultHandler,
+ final IErrorHandler errorHandler)
+ throws RemoteException {
+ Log.d(TAG, "query: " + query);
+ Utils.checkNotNull(query);
+ Utils.checkNotNull(metadataResultHandler);
+ Utils.checkNotNull(errorHandler);
+
+ // Handle async response
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ // Pre-load the list of music
+ List<MusicTrack> musicTracks = getMusicList();
+ if (musicTracks == null) {
+ notifyListenersOnPlaybackStateUpdate(getCurrentPlaybackState());
+ errorHandler.onError(new Error(Error.UNKNOWN,
+ getString(R.string.music_error)));
+ return;
+ }
+
+ final Uri uri = query.getUri();
+ int match = sUriMatcher.match(uri);
+ Log.d(TAG, "Queried: " + uri + "; match: " + match);
+ switch (match) {
+ case BROWSE_ROOT:
+ {
+ Log.d(TAG, "Browse_root");
+
+ try {
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootContainerCurser();
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error delivering metadata in the callback.", e);
+ }
+ break;
+ }
+ case NOW_PLAYING:
+ {
+ try {
+ Log.d(TAG, "query NOW_PLAYING");
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootItemCursor(
+ PIANO);
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error querying NOW_PLAYING");
+ }
+ break;
+ }
+ case PIANO:
+ {
+ try {
+ Log.d(TAG, "query PIANO");
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootItemCursor(
+ PIANO);
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error querying PIANO");
+ }
+ break;
+ }
+ case VOICE:
+ {
+ try {
+ Log.d(TAG, "query VOICE");
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootItemCursor(
+ VOICE);
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error querying VOICE");
+ }
+ break;
+ }
+ default:
+ {
+ Log.w(TAG, "Skipping unmatched URI: " + uri);
+ }
+ }
+ } catch (NotFoundException e) {
+ Log.e(TAG, "::run:", e);
+ } catch (RemoteException e) {
+ Log.e(TAG, "::run:", e);
+ }
+ } // end run
+ }).start();
+ }
+
+ */
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
new file mode 100644
index 0000000..db45b9d
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
@@ -0,0 +1,101 @@
+/* Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBarActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.android.musicservicedemo.R;
+
+// TODO Local UI
+
+/**
+ * Main activity of the app.
+ */
+public class MainActivity extends ActionBarActivity {
+
+ private static final String LOG = "MainActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.container, new PlaceholderFragment())
+ .commit();
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ // Inflate the menu; this adds items to the action bar if it is present.
+ //getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+ // if (id == R.id.action_settings) {
+ // return true;
+ // }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * A placeholder fragment containing a simple view.
+ */
+ public static class PlaceholderFragment extends Fragment {
+
+ public PlaceholderFragment() {
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * android.support.v4.app.Fragment#onCreateView(android.view.LayoutInflater
+ * , android.view.ViewGroup, android.os.Bundle)
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_main, container, false);
+ return rootView;
+ }
+ }
+
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
new file mode 100644
index 0000000..3589761
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class Utils {
+
+ private static final String TAG = "Utils";
+
+ /**
+ * Utility method to check that parameters are not null
+ *
+ * @param object
+ */
+ public static final void checkNotNull(Object object) {
+ if (object == null) {
+ throw new NullPointerException();
+ }
+ }
+
+ /**
+ * Utility to download a bitmap
+ *
+ * @param source
+ * @return
+ */
+ public static Bitmap getBitmapFromURL(String source) {
+ try {
+ URL url = new URL(source);
+ HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
+ httpConnection.setDoInput(true);
+ httpConnection.connect();
+ InputStream inputStream = httpConnection.getInputStream();
+ return BitmapFactory.decodeStream(inputStream);
+ } catch (IOException e) {
+ Log.e(TAG, "getBitmapFromUrl: " + source, e);
+ }
+ return null;
+ }
+
+ /**
+ * Utility method to wrap an index
+ *
+ * @param i
+ * @param size
+ * @return
+ */
+ public static int wrapIndex(int i, int size) {
+ int m = i % size;
+ if (m < 0) { // java modulus can be negative
+ m += size;
+ }
+ return m;
+ }
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
new file mode 100644
index 0000000..15038d7
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+import android.database.MatrixCursor;
+import android.media.session.PlaybackState;
+import android.net.Uri;
+import android.util.Log;
+
+import com.example.android.musicservicedemo.BrowserService;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class to get a list of MusicTrack's based on a server-side JSON
+ * configuration.
+ */
+public class MusicProvider {
+
+ private static final String TAG = "MusicProvider";
+
+ private static final String MUSIC_URL = "http://storage.googleapis.com/automotive-media/music.json";
+
+ private static String MUSIC = "music";
+ private static String TITLE = "title";
+ private static String ALBUM = "album";
+ private static String ARTIST = "artist";
+ private static String GENRE = "genre";
+ private static String SOURCE = "source";
+ private static String IMAGE = "image";
+ private static String TRACK_NUMBER = "trackNumber";
+ private static String TOTAL_TRACK_COUNT = "totalTrackCount";
+ private static String DURATION = "duration";
+
+ // Cache for music track data
+ private static List<MusicTrack> mMusicList;
+
+ /**
+ * Get the cached list of music tracks
+ *
+ * @return
+ * @throws JSONException
+ */
+ public List<MusicTrack> getMedia() throws JSONException {
+ if (null != mMusicList && mMusicList.size() > 0) {
+ return mMusicList;
+ }
+ return null;
+ }
+
+ /**
+ * Get the list of music tracks from a server and return the list of
+ * MusicTrack objects.
+ *
+ * @return
+ * @throws JSONException
+ */
+ public List<MusicTrack> retreiveMedia() throws JSONException {
+ if (null != mMusicList) {
+ return mMusicList;
+ }
+ int slashPos = MUSIC_URL.lastIndexOf('/');
+ String path = MUSIC_URL.substring(0, slashPos + 1);
+ JSONObject jsonObj = parseUrl(MUSIC_URL);
+
+ try {
+ JSONArray videos = jsonObj.getJSONArray(MUSIC);
+ if (null != videos) {
+ mMusicList = new ArrayList<MusicTrack>();
+ for (int j = 0; j < videos.length(); j++) {
+ JSONObject music = videos.getJSONObject(j);
+ String title = music.getString(TITLE);
+ String album = music.getString(ALBUM);
+ String artist = music.getString(ARTIST);
+ String genre = music.getString(GENRE);
+ String source = music.getString(SOURCE);
+ // Media is stored relative to JSON file
+ if (!source.startsWith("http")) {
+ source = path + source;
+ }
+ String image = music.getString(IMAGE);
+ if (!image.startsWith("http")) {
+ image = path + image;
+ }
+ int trackNumber = music.getInt(TRACK_NUMBER);
+ int totalTrackCount = music.getInt(TOTAL_TRACK_COUNT);
+ int duration = music.getInt(DURATION) * 1000; // ms
+
+ mMusicList.add(new MusicTrack(title, album, artist, genre, source,
+ image, trackNumber, totalTrackCount, duration));
+ }
+ }
+ } catch (NullPointerException e) {
+ Log.e(TAG, "retreiveMedia", e);
+ }
+ return mMusicList;
+ }
+
+ /**
+ * Download a JSON file from a server, parse the content and return the JSON
+ * object.
+ *
+ * @param urlString
+ * @return
+ */
+ private JSONObject parseUrl(String urlString) {
+ InputStream is = null;
+ try {
+ java.net.URL url = new java.net.URL(urlString);
+ URLConnection urlConnection = url.openConnection();
+ is = new BufferedInputStream(urlConnection.getInputStream());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ urlConnection.getInputStream(), "iso-8859-1"), 8);
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ return new JSONObject(sb.toString());
+ } catch (Exception e) {
+ Log.d(TAG, "Failed to parse the json for media list", e);
+ return null;
+ } finally {
+ if (null != is) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ public MatrixCursor getRootContainerCurser() {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+ Uri.Builder pianoBuilder = new Uri.Builder();
+ pianoBuilder.authority(BrowserService.AUTHORITY);
+ pianoBuilder.appendPath(BrowserService.PIANO_BASE_PATH);
+ matrixCursor.addRow(new Object[] {
+ pianoBuilder.build(),
+ BrowserService.PIANO_BASE_PATH,
+ "subtitle",
+ null,
+ 0
+ });
+
+ Uri.Builder voiceBuilder = new Uri.Builder();
+ voiceBuilder.authority(BrowserService.AUTHORITY);
+ voiceBuilder.appendPath(BrowserService.VOICE_BASE_PATH);
+ matrixCursor.addRow(new Object[] {
+ voiceBuilder.build(),
+ BrowserService.VOICE_BASE_PATH,
+ "subtitle",
+ null,
+ 0
+ });
+ return matrixCursor;
+ }
+
+ public MatrixCursor getRootItemCursor(int type) {
+ if (type == BrowserService.NOW_PLAYING) {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+ try {
+ // Just return all of the tracks for now
+ List<MusicTrack> musicTracks = retreiveMedia();
+ for (MusicTrack musicTrack : musicTracks) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.authority(BrowserService.AUTHORITY);
+ builder.appendPath(BrowserService.NOW_PLAYING_PATH);
+ builder.appendPath(musicTrack.getTitle());
+ matrixCursor.addRow(new Object[] {
+ builder.build(),
+ musicTrack.getTitle(),
+ musicTrack.getArtist(),
+ musicTrack.getImage(),
+ PlaybackState.ACTION_PLAY
+ });
+ Log.d(TAG, "Uri " + builder.build());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "::getRootItemCursor:", e);
+ }
+
+ Log.d(TAG, "cursor: " + matrixCursor.getCount());
+ return matrixCursor;
+ } else if (type == BrowserService.PIANO) {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+ try {
+ List<MusicTrack> musicTracks = retreiveMedia();
+ for (MusicTrack musicTrack : musicTracks) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.authority(BrowserService.AUTHORITY);
+ builder.appendPath(BrowserService.PIANO_BASE_PATH);
+ builder.appendPath(musicTrack.getTitle());
+ matrixCursor.addRow(new Object[] {
+ builder.build(),
+ musicTrack.getTitle(),
+ musicTrack.getArtist(),
+ musicTrack.getImage(),
+ PlaybackState.ACTION_PLAY
+ });
+ Log.d(TAG, "Uri " + builder.build());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "::getRootItemCursor:", e);
+ }
+
+ Log.d(TAG, "cursor: " + matrixCursor.getCount());
+ return matrixCursor;
+ } else if (type == BrowserService.VOICE) {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+ try {
+ List<MusicTrack> musicTracks = retreiveMedia();
+ for (MusicTrack musicTrack : musicTracks) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.authority(BrowserService.AUTHORITY);
+ builder.appendPath(BrowserService.VOICE_BASE_PATH);
+ builder.appendPath(musicTrack.getTitle());
+ matrixCursor.addRow(new Object[] {
+ builder.build(),
+ musicTrack.getTitle(),
+ musicTrack.getArtist(),
+ musicTrack.getImage(),
+ PlaybackState.ACTION_PLAY
+ });
+ Log.d(TAG, "Uri " + builder.build());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "::getRootItemCursor:", e);
+ }
+
+ Log.d(TAG, "cursor: " + matrixCursor.getCount());
+ return matrixCursor;
+
+ }
+ return null;
+ }
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
new file mode 100644
index 0000000..ffda110
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONException;
+
+/**
+ * Asynchronous task to retrieve the music data using MusicProvider.
+ */
+public class MusicProviderTask extends AsyncTask<Void, Void, Void> {
+
+ private static final String TAG = "MusicProviderTask";
+
+ MusicProvider mMusicProvider;
+ MusicProviderTaskListener mMusicProviderTaskListener;
+
+ /**
+ * Initialize the task with the provider to download the music data and the
+ * listener to be informed when the task is done.
+ *
+ * @param musicProvider
+ * @param listener
+ */
+ public MusicProviderTask(MusicProvider musicProvider,
+ MusicProviderTaskListener listener) {
+ mMusicProvider = musicProvider;
+ mMusicProviderTaskListener = listener;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.os.AsyncTask#doInBackground(java.lang.Object[])
+ */
+ @Override
+ protected Void doInBackground(Void... arg0) {
+ try {
+ mMusicProvider.retreiveMedia();
+ } catch (JSONException e) {
+ Log.e(TAG, "::doInBackground:", e);
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.os.AsyncTask#onPostExecute(java.lang.Object)
+ */
+ @Override
+ protected void onPostExecute(Void result) {
+ mMusicProviderTaskListener.onMusicProviderTaskCompleted();
+ }
+
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
new file mode 100644
index 0000000..b1d168f
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+/**
+ * Callback listener for completion of MusicProviderTask
+ */
+public interface MusicProviderTaskListener {
+ public void onMusicProviderTaskCompleted();
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
new file mode 100644
index 0000000..02ea899
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+/**
+ * A class to model music track metadata.
+ */
+public class MusicTrack {
+
+ private static final String TAG = "MusicTrack";
+
+ private String mTitle;
+ private String mAlbum;
+ private String mArtist;
+ private String mGenre;
+ private String mSource;
+ private String mImage;
+ private int mTrackNumber;
+ private int mTotalTrackCount;
+ private int mDuration;
+
+ /**
+ * Constructor creating a MusicTrack instance.
+ *
+ * @param title
+ * @param album
+ * @param artist
+ * @param genre
+ * @param source
+ * @param image
+ * @param trackNumber
+ * @param totalTrackCount
+ * @param duration
+ */
+ public MusicTrack(String title, String album, String artist, String genre, String source,
+ String image, int trackNumber, int totalTrackCount, int duration) {
+ this.mTitle = title;
+ this.mAlbum = album;
+ this.mArtist = artist;
+ this.mGenre = genre;
+ this.mSource = source;
+ this.mImage = image;
+ this.mTrackNumber = trackNumber;
+ this.mTotalTrackCount = totalTrackCount;
+ this.mDuration = duration;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public void setTitle(String mTitle) {
+ this.mTitle = mTitle;
+ }
+
+ public String getAlbum() {
+ return mAlbum;
+ }
+
+ public void setAlbum(String mAlbum) {
+ this.mAlbum = mAlbum;
+ }
+
+ public String getArtist() {
+ return mArtist;
+ }
+
+ public void setArtist(String mArtist) {
+ this.mArtist = mArtist;
+ }
+
+ public String getGenre() {
+ return mGenre;
+ }
+
+ public void setGenre(String mGenre) {
+ this.mGenre = mGenre;
+ }
+
+ public String getSource() {
+ return mSource;
+ }
+
+ public void setSource(String mSource) {
+ this.mSource = mSource;
+ }
+
+ public String getImage() {
+ return mImage;
+ }
+
+ public void setImage(String mImage) {
+ this.mImage = mImage;
+ }
+
+ public int getTrackNumber() {
+ return mTrackNumber;
+ }
+
+ public void setTrackNumber(int mTrackNumber) {
+ this.mTrackNumber = mTrackNumber;
+ }
+
+ public int getTotalTrackCount() {
+ return mTotalTrackCount;
+ }
+
+ public void setTotalTrackCount(int mTotalTrackCount) {
+ this.mTotalTrackCount = mTotalTrackCount;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+
+ public void setDuration(int mDuration) {
+ this.mDuration = mDuration;
+ }
+
+ public String toString() {
+ return mTitle;
+ }
+
+}