am 78bb2dad: Merge "Updated media browsing API & tests." into lmp-dev

* commit '78bb2dad2c1bb11c607c1d91ab89604c8f46c310':
  Updated media browsing API & tests.
This commit is contained in:
Yao Chen
2014-07-22 12:17:58 +00:00
committed by Android Git Automerger
40 changed files with 3169 additions and 5 deletions

View File

@@ -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 \

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.browse;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.graphics.Bitmap;
import android.media.session.MediaSession;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Handler;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Base class for media browse services.
* <p>
* Media browse services enable applications to browse media content provided by an application
* and ask the application to start playing it. They may also be used to control content that
* is already playing by way of a {@link MediaSession}.
* </p>
*
* To extend this class, you must declare the service in your manifest file with
* an intent filter with the {@link #SERVICE_ACTION} action.
*
* For example:
* </p><pre>
* &lt;service android:name=".MyMediaBrowserService"
* android:label="&#64;string/service_name" >
* &lt;intent-filter>
* &lt;action android:name="android.media.browse.MediaBrowserService" />
* &lt;/intent-filter>
* &lt;/service>
* </pre>
*
*/
public abstract class MediaBrowserService extends Service {
private static final String TAG = "MediaBrowserService";
private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
private final Handler mHandler = new Handler();
private ServiceBinder mBinder;
MediaSession.Token mSession;
/**
* All the info about a connection.
*/
private class ConnectionRecord {
String pkg;
Bundle rootHints;
IMediaBrowserServiceCallbacks callbacks;
Uri root;
HashSet<Uri> subscriptions = new HashSet();
}
/**
* The {@link Intent} that must be declared as handled by the service.
*/
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
private class ServiceBinder extends IMediaBrowserService.Stub {
@Override
public void connect(final String pkg, final Bundle rootHints,
final IMediaBrowserServiceCallbacks callbacks) {
final int uid = Binder.getCallingUid();
if (!isValidPackage(pkg, uid)) {
throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
+ " package=" + pkg);
}
mHandler.post(new Runnable() {
@Override
public void run() {
final IBinder b = callbacks.asBinder();
// Clear out the old subscriptions. We are getting new ones.
mConnections.remove(b);
final ConnectionRecord connection = new ConnectionRecord();
connection.pkg = pkg;
connection.rootHints = rootHints;
connection.callbacks = callbacks;
connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
// If they didn't return something, don't allow this client.
if (connection.root == null) {
Log.i(TAG, "No root for client " + pkg + " from service "
+ getClass().getName());
try {
callbacks.onConnectFailed();
} catch (RemoteException ex) {
Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
+ "pkg=" + pkg);
}
} else {
try {
mConnections.put(b, connection);
callbacks.onConnect(connection.root, mSession);
} catch (RemoteException ex) {
Log.w(TAG, "Calling onConnect() failed. Dropping client. "
+ "pkg=" + pkg);
mConnections.remove(b);
}
}
}
});
}
@Override
public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
mHandler.post(new Runnable() {
@Override
public void run() {
final IBinder b = callbacks.asBinder();
// Clear out the old subscriptions. We are getting new ones.
final ConnectionRecord old = mConnections.remove(b);
if (old != null) {
// TODO
}
}
});
}
@Override
public void addSubscription(final Uri uri, final IMediaBrowserServiceCallbacks callbacks) {
mHandler.post(new Runnable() {
@Override
public void run() {
final IBinder b = callbacks.asBinder();
// Get the record for the connection
final ConnectionRecord connection = mConnections.get(b);
if (connection == null) {
Log.w(TAG, "addSubscription for callback that isn't registered uri="
+ uri);
return;
}
MediaBrowserService.this.addSubscription(uri, connection);
}
});
}
@Override
public void removeSubscription(final Uri uri,
final IMediaBrowserServiceCallbacks callbacks) {
mHandler.post(new Runnable() {
@Override
public void run() {
final IBinder b = callbacks.asBinder();
ConnectionRecord connection = mConnections.get(b);
if (connection == null) {
Log.w(TAG, "removeSubscription for callback that isn't registered uri="
+ uri);
return;
}
if (!connection.subscriptions.remove(uri)) {
Log.w(TAG, "removeSubscription called for " + uri
+ " which is not subscribed");
}
}
});
}
}
@Override
public void onCreate() {
super.onCreate();
mBinder = new ServiceBinder();
}
@Override
public IBinder onBind(Intent intent) {
if (SERVICE_ACTION.equals(intent.getAction())) {
return mBinder;
}
return null;
}
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
}
/**
* Called to get the root uri for browsing by a particular client.
* <p>
* The implementation should verify that the client package has
* permission to access browse media information before returning
* the root uri; it should return null if the client is not
* allowed to access this information.
* </p>
*
* @param clientPackageName The package name of the application
* which is requesting access to browse media.
* @param clientUid The uid of the application which is requesting
* access to browse media.
* @param rootHints An optional bundle of service-specific arguments to send
* to the media browse service when connecting and retrieving the root uri
* for browsing, or null if none. The contents of this bundle may affect
* the information returned when browsing.
*/
public abstract @Nullable Uri onGetRoot(@NonNull String clientPackageName, int clientUid,
@Nullable Bundle rootHints);
/**
* Called to get information about the children of a media item.
*
* @param parentUri The uri of the parent media item whose
* children are to be queried.
* @return The list of children, or null if the uri is invalid.
*/
public abstract @Nullable List<MediaBrowserItem> onLoadChildren(@NonNull Uri parentUri);
/**
* Called to get the thumbnail of a particular media item.
*
* @param uri The uri of the media item.
* @param width The requested width of the icon in dp.
* @param height The requested height of the icon in dp.
* @param density The requested density of the icon. This is the approximate density of the
* screen on which the icon will be displayed. This density will be one of
* the android density buckets.
* @return The file descriptor of the thumbnail, which may then be loaded
* using a bitmap factory, or null if the item does not have a thumbnail.
*/
public abstract @Nullable Bitmap onGetThumbnail(@NonNull Uri uri,
int width, int height, int density);
/**
* Call to set the media session.
* <p>
* This must be called before onCreate returns.
*
* @return The media session token, must not be null.
*/
public void setSessionToken(MediaSession.Token token) {
if (token == null) {
throw new IllegalStateException(this.getClass().getName()
+ ".onCreateSession() set invalid MediaSession.Token");
}
mSession = token;
}
/**
* Gets the session token, or null if it has not yet been created
* or if it has been destroyed.
*/
public @Nullable MediaSession.Token getSessionToken() {
return mSession;
}
/**
* Notifies all connected media browsers that the content of
* the browse service has changed in some way.
* This will cause browsers to fetch subscribed content again.
*/
public void notifyChange() {
throw new RuntimeException("implement me");
}
/**
* Notifies all connected media browsers that the children of
* the specified Uri have changed in some way.
* This will cause browsers to fetch subscribed content again.
*
* @param parentUri The uri of the parent media item whose
* children changed.
*/
public void notifyChildrenChanged(@NonNull Uri parentUri) {
throw new RuntimeException("implement me");
}
/**
* Return whether the given package is one of the ones that is owned by the uid.
*/
private boolean isValidPackage(String pkg, int uid) {
if (pkg == null) {
return false;
}
final PackageManager pm = getPackageManager();
final String[] packages = pm.getPackagesForUid(uid);
final int N = packages.length;
for (int i=0; i<N; i++) {
if (packages[i].equals(pkg)) {
return true;
}
}
return false;
}
/**
* Save the subscription and if it is a new subscription send the results.
*/
private void addSubscription(Uri uri, ConnectionRecord connection) {
// Save the subscription
final boolean added = connection.subscriptions.add(uri);
// If this is a new subscription, send the results
if (added) {
performLoadChildren(uri, connection);
}
}
/**
* Call onLoadChildren and then send the results back to the connection.
* <p>
* Callers must make sure that this connection is still connected.
* <p>
* TODO: Think about caching and combining these calls.
*/
private void performLoadChildren(Uri uri, ConnectionRecord connection) {
final List<MediaBrowserItem> list = onLoadChildren(uri);
if (list == null) {
throw new IllegalStateException("onLoadChildren returned null for uri " + uri);
}
final ParceledListSlice<MediaBrowserItem> pls = new ParceledListSlice(list);
try {
connection.callbacks.onLoadChildren(uri, pls);
} catch (RemoteException ex) {
// The other side is in the process of crashing.
Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
+ " package=" + connection.pkg);
}
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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 *;
#}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
*/
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}