diff options
40 files changed, 3169 insertions, 5 deletions
@@ -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> + * <service android:name=".MyMediaBrowserService" + * android:label="@string/service_name" > + * <intent-filter> + * <action android:name="android.media.browse.MediaBrowserService" /> + * </intent-filter> + * </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 Binary files differnew file mode 100644 index 0000000..47d6854 --- /dev/null +++ b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png diff --git a/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..01b53fd --- /dev/null +++ b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png diff --git a/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..af762f2 --- /dev/null +++ b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png diff --git a/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..eef47aa --- /dev/null +++ b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..47d6854 --- /dev/null +++ b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png diff --git a/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..01b53fd --- /dev/null +++ b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png diff --git a/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..af762f2 --- /dev/null +++ b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..eef47aa --- /dev/null +++ b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png Binary files differnew file mode 100644 index 0000000..ea98c95 --- /dev/null +++ b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png 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; + } + +} |