From 17d47989ee53c9e54f250d29a343ba949edf0ff9 Mon Sep 17 00:00:00 2001 From: Yao Chen Date: Thu, 17 Jul 2014 14:39:34 -0700 Subject: [PATCH] Updated media browsing API & tests. Change-Id: I3a1e3eead99a98b1890e05843dd90203cee517b7 --- Android.mk | 8 +- api/current.txt | 71 ++ .../media/browse/IMediaBrowserService.aidl | 20 + .../browse/IMediaBrowserServiceCallbacks.aidl | 24 + .../android/media/browse/MediaBrowser.java | 704 ++++++++++++++++++ .../media/browse/MediaBrowserItem.java | 241 ++++++ .../media/browse/MediaBrowserService.java | 363 +++++++++ .../android/server/ConnectivityService.java | 4 +- tests/MusicBrowserDemo/Android.mk | 35 + tests/MusicBrowserDemo/AndroidManifest.xml | 45 ++ .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4805 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2592 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5246 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14755 bytes tests/MusicBrowserDemo/res/values/strings.xml | 21 + tests/MusicBrowserDemo/res/values/styles.xml | 36 + .../musicbrowserdemo/AppListFragment.java | 149 ++++ .../musicbrowserdemo/BrowserListFragment.java | 212 ++++++ .../musicbrowserdemo/MainActivity.java | 48 ++ tests/MusicServiceDemo/Android.mk | 35 + tests/MusicServiceDemo/AndroidManifest.xml | 55 ++ tests/MusicServiceDemo/proguard-project.txt | 20 + .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4805 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2592 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5246 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14755 bytes .../res/drawable-xxhdpi/thumbsup.png | Bin 0 -> 1632 bytes .../res/layout/activity_main.xml | 23 + .../res/layout/fragment_main.xml | 32 + tests/MusicServiceDemo/res/values/colors.xml | 22 + tests/MusicServiceDemo/res/values/dimens.xml | 23 + tests/MusicServiceDemo/res/values/strings.xml | 25 + tests/MusicServiceDemo/res/values/styles.xml | 36 + .../musicservicedemo/BrowserService.java | 246 ++++++ .../musicservicedemo/MainActivity.java | 101 +++ .../android/musicservicedemo/Utils.java | 77 ++ .../browser/MusicProvider.java | 266 +++++++ .../browser/MusicProviderTask.java | 70 ++ .../browser/MusicProviderTaskListener.java | 24 + .../musicservicedemo/browser/MusicTrack.java | 138 ++++ 40 files changed, 3169 insertions(+), 5 deletions(-) create mode 100644 media/java/android/media/browse/IMediaBrowserService.aidl create mode 100644 media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl create mode 100644 media/java/android/media/browse/MediaBrowser.java create mode 100644 media/java/android/media/browse/MediaBrowserItem.java create mode 100644 media/java/android/media/browse/MediaBrowserService.java create mode 100644 tests/MusicBrowserDemo/Android.mk create mode 100644 tests/MusicBrowserDemo/AndroidManifest.xml create mode 100644 tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png create mode 100644 tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png create mode 100644 tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png create mode 100644 tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png create mode 100644 tests/MusicBrowserDemo/res/values/strings.xml create mode 100644 tests/MusicBrowserDemo/res/values/styles.xml create mode 100644 tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java create mode 100644 tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java create mode 100644 tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java create mode 100644 tests/MusicServiceDemo/Android.mk create mode 100644 tests/MusicServiceDemo/AndroidManifest.xml create mode 100644 tests/MusicServiceDemo/proguard-project.txt create mode 100644 tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png create mode 100644 tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png create mode 100644 tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png create mode 100644 tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png create mode 100644 tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png create mode 100644 tests/MusicServiceDemo/res/layout/activity_main.xml create mode 100644 tests/MusicServiceDemo/res/layout/fragment_main.xml create mode 100644 tests/MusicServiceDemo/res/values/colors.xml create mode 100644 tests/MusicServiceDemo/res/values/dimens.xml create mode 100644 tests/MusicServiceDemo/res/values/strings.xml create mode 100644 tests/MusicServiceDemo/res/values/styles.xml create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java diff --git a/Android.mk b/Android.mk index 79d13d90663c5..7dfa6a0c67a86 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 e736d78aa24ba..5688d1420c6c1 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); + 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 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 0000000000000..4b2cb9d2bc134 --- /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 0000000000000..ead76243e7cd1 --- /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 0000000000000..beec5f9a80f68 --- /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. + *

+ * This object is not thread-safe. All calls should happen on the thread on which the browser + * was constructed. + *

+ */ +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 mSubscriptions = + new ArrayMap(); + + 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. + *

+ * The connection callback specified in the constructor will be invoked + * when the connection completes or fails. + *

+ */ + 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. + *

+ * 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. + *

+ * Note that the root uri may become invalid or change when when the + * browser is disconnected. + *

+ * + * @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. + *

+ * Note that the session token may become invalid or change when when the + * browser is disconnected. + *

+ * + * @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. + *

+ * 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. + *

+ * If the uri is already subscribed with a different callback then the new + * callback will replace the previous one. + *

+ * + * @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. + *

+ * The query callback will no longer be invoked for results associated with + * this Uri once this method returns. + *

+ * + * @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 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 children) { + } + + /** + * Called when the Uri doesn't exist or other errors in subscribing. + *

+ * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} + * called, because some errors may heal themselves. + *

+ */ + 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 mMediaBrowser; + + public ServiceCallbacks(MediaBrowser mediaBrowser) { + mMediaBrowser = new WeakReference(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 0000000000000..119f687308a3a --- /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. + *

+ * The Uri of this item may be passed to link android.media.session.MediaController#play(Uri) + * to start playing it. + *

+ */ + 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 CREATOR = + new Parcelable.Creator() { + @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 0000000000000..ceb4b03b88490 --- /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. + *

+ * 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}. + *

+ * + * 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: + *

+ * <service android:name=".MyMediaBrowserService"
+ *          android:label="@string/service_name" >
+ *     <intent-filter>
+ *         <action android:name="android.media.browse.MediaBrowserService" />
+ *     </intent-filter>
+ * </service>
+ * 
+ * + */ +public abstract class MediaBrowserService extends Service { + private static final String TAG = "MediaBrowserService"; + + private final ArrayMap 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 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. + *

+ * 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. + *

+ * + * @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 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. + *

+ * 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 + * Callers must make sure that this connection is still connected. + *

+ * TODO: Think about caching and combining these calls. + */ + private void performLoadChildren(Uri uri, ConnectionRecord connection) { + final List list = onLoadChildren(uri); + if (list == null) { + throw new IllegalStateException("onLoadChildren returned null for uri " + uri); + } + final ParceledListSlice 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 9945909a00711..ca9f6eb82781c 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 0000000000000..207774b07a050 --- /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 0000000000000..d2acfe2ad871e --- /dev/null +++ b/tests/MusicBrowserDemo/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..47d6854e26d5da581349cb94029645a9b827ce8c GIT binary patch literal 4805 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 + + + + Music Browser + + diff --git a/tests/MusicBrowserDemo/res/values/styles.xml b/tests/MusicBrowserDemo/res/values/styles.xml new file mode 100644 index 0000000000000..b83662d6fdec0 --- /dev/null +++ b/tests/MusicBrowserDemo/res/values/styles.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + 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 0000000000000..c0f3a7f75825d --- /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 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 list = pm.queryIntentServices(intent, 0); + final int N = list.size(); + mItems = new ArrayList(N); + for (int i=0; i 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 children) { + Log.d(TAG, "onChildrenLoaded parentUri=" + parentUri + + " children= " + children); + mItems.clear(); + final int N = children.size(); + for (int i=0; i + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/MusicServiceDemo/proguard-project.txt b/tests/MusicServiceDemo/proguard-project.txt new file mode 100644 index 0000000000000..f2fe1559a2178 --- /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 0000000000000000000000000000000000000000..47d6854e26d5da581349cb94029645a9b827ce8c GIT binary patch literal 4805 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 eL_|a+mcIeH^M|Sxz&RKI0000 literal 0 HcmV?d00001 diff --git a/tests/MusicServiceDemo/res/layout/activity_main.xml b/tests/MusicServiceDemo/res/layout/activity_main.xml new file mode 100644 index 0000000000000..71753e3405874 --- /dev/null +++ b/tests/MusicServiceDemo/res/layout/activity_main.xml @@ -0,0 +1,23 @@ + + + diff --git a/tests/MusicServiceDemo/res/layout/fragment_main.xml b/tests/MusicServiceDemo/res/layout/fragment_main.xml new file mode 100644 index 0000000000000..8796e86d7b228 --- /dev/null +++ b/tests/MusicServiceDemo/res/layout/fragment_main.xml @@ -0,0 +1,32 @@ + + + + + + + diff --git a/tests/MusicServiceDemo/res/values/colors.xml b/tests/MusicServiceDemo/res/values/colors.xml new file mode 100644 index 0000000000000..44dd05d737742 --- /dev/null +++ b/tests/MusicServiceDemo/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + + #ffffff00 + #ff00ff00 + #ff0000ff + #ffff0000 + diff --git a/tests/MusicServiceDemo/res/values/dimens.xml b/tests/MusicServiceDemo/res/values/dimens.xml new file mode 100644 index 0000000000000..9f63ef29d0e10 --- /dev/null +++ b/tests/MusicServiceDemo/res/values/dimens.xml @@ -0,0 +1,23 @@ + + + + + + 16dp + 16dp + + diff --git a/tests/MusicServiceDemo/res/values/strings.xml b/tests/MusicServiceDemo/res/values/strings.xml new file mode 100644 index 0000000000000..14c01711f624b --- /dev/null +++ b/tests/MusicServiceDemo/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + + Music Service Demo + Settings + Thumbs Up + No music found + Now Playing + + diff --git a/tests/MusicServiceDemo/res/values/styles.xml b/tests/MusicServiceDemo/res/values/styles.xml new file mode 100644 index 0000000000000..b83662d6fdec0 --- /dev/null +++ b/tests/MusicServiceDemo/res/values/styles.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + 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 0000000000000..9ca156f5d5d02 --- /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 onLoadChildren(Uri parentUri) { + final ArrayList 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 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 0000000000000..db45b9db1802c --- /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 0000000000000..35897611e8d66 --- /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 0000000000000..15038d78f2edd --- /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 mMusicList; + + /** + * Get the cached list of music tracks + * + * @return + * @throws JSONException + */ + public List 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 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(); + 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 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 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 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 0000000000000..ffda1108e662b --- /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 { + + 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 0000000000000..b1d168fe34e1e --- /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 0000000000000..02ea89918f7df --- /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; + } + +}