+ * The implementation should verify that the client package has
+ * permission to access browse media information before returning
+ * the root uri; it should return null if the client is not
+ * allowed to access this information.
+ *
+ *
+ * @param clientPackageName The package name of the application
+ * which is requesting access to browse media.
+ * @param clientUid The uid of the application which is requesting
+ * access to browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browse service when connecting and retrieving the root uri
+ * for browsing, or null if none. The contents of this bundle may affect
+ * the information returned when browsing.
+ */
+ public abstract @Nullable Uri onGetRoot(@NonNull String clientPackageName, int clientUid,
+ @Nullable Bundle rootHints);
+
+ /**
+ * Called to get information about the children of a media item.
+ *
+ * @param parentUri The uri of the parent media item whose
+ * children are to be queried.
+ * @return The list of children, or null if the uri is invalid.
+ */
+ public abstract @Nullable List onLoadChildren(@NonNull Uri parentUri);
+
+ /**
+ * Called to get the thumbnail of a particular media item.
+ *
+ * @param uri The uri of the media item.
+ * @param width The requested width of the icon in dp.
+ * @param height The requested height of the icon in dp.
+ * @param density The requested density of the icon. This is the approximate density of the
+ * screen on which the icon will be displayed. This density will be one of
+ * the android density buckets.
+ * @return The file descriptor of the thumbnail, which may then be loaded
+ * using a bitmap factory, or null if the item does not have a thumbnail.
+ */
+ public abstract @Nullable Bitmap onGetThumbnail(@NonNull Uri uri,
+ int width, int height, int density);
+
+ /**
+ * Call to set the media session.
+ *
+ * This must be called before onCreate returns.
+ *
+ * @return The media session token, must not be null.
+ */
+ public void setSessionToken(MediaSession.Token token) {
+ if (token == null) {
+ throw new IllegalStateException(this.getClass().getName()
+ + ".onCreateSession() set invalid MediaSession.Token");
+ }
+ mSession = token;
+ }
+
+ /**
+ * Gets the session token, or null if it has not yet been created
+ * or if it has been destroyed.
+ */
+ public @Nullable MediaSession.Token getSessionToken() {
+ return mSession;
+ }
+
+ /**
+ * Notifies all connected media browsers that the content of
+ * the browse service has changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ */
+ public void notifyChange() {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * Notifies all connected media browsers that the children of
+ * the specified Uri have changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ *
+ * @param parentUri The uri of the parent media item whose
+ * children changed.
+ */
+ public void notifyChildrenChanged(@NonNull Uri parentUri) {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * Return whether the given package is one of the ones that is owned by the uid.
+ */
+ private boolean isValidPackage(String pkg, int uid) {
+ if (pkg == null) {
+ return false;
+ }
+ final PackageManager pm = getPackageManager();
+ final String[] packages = pm.getPackagesForUid(uid);
+ final int N = packages.length;
+ for (int i=0; i
+ * Callers must make sure that this connection is still connected.
+ *
+ * TODO: Think about caching and combining these calls.
+ */
+ private void performLoadChildren(Uri uri, ConnectionRecord connection) {
+ final List list = onLoadChildren(uri);
+ if (list == null) {
+ throw new IllegalStateException("onLoadChildren returned null for uri " + uri);
+ }
+ final ParceledListSlice pls = new ParceledListSlice(list);
+ try {
+ connection.callbacks.onLoadChildren(uri, pls);
+ } catch (RemoteException ex) {
+ // The other side is in the process of crashing.
+ Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
+ + " package=" + connection.pkg);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 9945909a00711..ca9f6eb82781c 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -185,8 +185,8 @@ import javax.net.ssl.SSLSession;
public class ConnectivityService extends IConnectivityManager.Stub {
private static final String TAG = "ConnectivityService";
- private static final boolean DBG = true;
- private static final boolean VDBG = true; // STOPSHIP
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false; // STOPSHIP
// network sampling debugging
private static final boolean SAMPLE_DBG = false;
diff --git a/tests/MusicBrowserDemo/Android.mk b/tests/MusicBrowserDemo/Android.mk
new file mode 100644
index 0000000000000..207774b07a050
--- /dev/null
+++ b/tests/MusicBrowserDemo/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := MusicBrowserDemo
+#LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-v4 \
+ android-support-v7-appcompat
+
+LOCAL_RESOURCE_DIR := \
+ $(LOCAL_PATH)/res \
+ frameworks/support/v7/appcompat/res
+LOCAL_PROGUARD_ENABLED := disabled
+#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_AAPT_FLAGS := \
+ --auto-add-overlay \
+ --extra-packages android.support.v7.appcompat
+include $(BUILD_PACKAGE)
diff --git a/tests/MusicBrowserDemo/AndroidManifest.xml b/tests/MusicBrowserDemo/AndroidManifest.xml
new file mode 100644
index 0000000000000..d2acfe2ad871e
--- /dev/null
+++ b/tests/MusicBrowserDemo/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000..47d6854e26d5d
Binary files /dev/null and b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png differ
diff --git a/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000000000..01b53fd505e89
Binary files /dev/null and b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png differ
diff --git a/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000000..af762f2bee4e1
Binary files /dev/null and b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000..eef47aa34b404
Binary files /dev/null and b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/tests/MusicBrowserDemo/res/values/strings.xml b/tests/MusicBrowserDemo/res/values/strings.xml
new file mode 100644
index 0000000000000..858f278fa74fc
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ Music Browser
+
+
diff --git a/tests/MusicBrowserDemo/res/values/styles.xml b/tests/MusicBrowserDemo/res/values/styles.xml
new file mode 100644
index 0000000000000..b83662d6fdec0
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
new file mode 100644
index 0000000000000..c0f3a7f75825d
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicbrowserdemo;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.browse.MediaBrowserService;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// TODO: Include an icon.
+
+public class AppListFragment extends ListFragment {
+
+ private Adapter mAdapter;
+ private List- mItems;
+
+ public AppListFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final Item item = mItems.get(position);
+
+ Log.i("AppListFragment", "Item clicked: " + position + " -- " + item.component);
+
+ final BrowserListFragment fragment = new BrowserListFragment();
+
+ final Bundle args = new Bundle();
+ args.putParcelable(BrowserListFragment.ARG_COMPONENT, item.component);
+ fragment.setArguments(args);
+
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ private static class Item {
+ final String label;
+ final ComponentName component;
+
+ Item(String l, ComponentName c) {
+ this.label = l;
+ this.component = c;
+ }
+ }
+
+ private class Adapter extends BaseAdapter {
+ private final LayoutInflater mInflater;
+
+ Adapter() {
+ super();
+
+ final Context context = getActivity();
+ mInflater = LayoutInflater.from(context);
+
+ // Load the data
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+ final List list = pm.queryIntentServices(intent, 0);
+ final int N = list.size();
+ mItems = new ArrayList(N);
+ for (int i=0; i mItems = new ArrayList();
+ private ComponentName mComponent;
+ private Uri mUri;
+ private MediaBrowser mBrowser;
+
+ private static class Item {
+ final MediaBrowserItem media;
+
+ Item(MediaBrowserItem m) {
+ this.media = m;
+ }
+ }
+
+ public BrowserListFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Log.d(TAG, "onActivityCreated -- " + hashCode());
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+
+ // Get our arguments
+ final Bundle args = getArguments();
+ mComponent = args.getParcelable(ARG_COMPONENT);
+ mUri = args.getParcelable(ARG_URI);
+
+ // A hint about who we are, so the service can customize the results if it wants to.
+ final Bundle rootHints = new Bundle();
+ rootHints.putBoolean(HINT_DISPLAY, true);
+
+ mBrowser = new MediaBrowser(getActivity(), mComponent, mConnectionCallbacks, rootHints);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mBrowser.connect();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mBrowser.disconnect();
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final Item item = mItems.get(position);
+
+ Log.i("BrowserListFragment", "Item clicked: " + position + " -- "
+ + mAdapter.getItem(position).media.getUri());
+
+ final BrowserListFragment fragment = new BrowserListFragment();
+
+ final Bundle args = new Bundle();
+ args.putParcelable(BrowserListFragment.ARG_COMPONENT, mComponent);
+ args.putParcelable(BrowserListFragment.ARG_URI, item.media.getUri());
+ fragment.setArguments(args);
+
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(null)
+ .commit();
+
+ }
+
+ final MediaBrowser.ConnectionCallback mConnectionCallbacks
+ = new MediaBrowser.ConnectionCallback() {
+ @Override
+ public void onConnected() {
+ Log.d(TAG, "mConnectionCallbacks.onConnected");
+ if (mUri == null) {
+ mUri = mBrowser.getRoot();
+ }
+ mBrowser.subscribe(mUri, new MediaBrowser.SubscriptionCallback() {
+ @Override
+ public void onChildrenLoaded(Uri parentUri, List children) {
+ Log.d(TAG, "onChildrenLoaded parentUri=" + parentUri
+ + " children= " + children);
+ mItems.clear();
+ final int N = children.size();
+ for (int i=0; i
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicServiceDemo/proguard-project.txt b/tests/MusicServiceDemo/proguard-project.txt
new file mode 100644
index 0000000000000..f2fe1559a2178
--- /dev/null
+++ b/tests/MusicServiceDemo/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000..47d6854e26d5d
Binary files /dev/null and b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png differ
diff --git a/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000000000..01b53fd505e89
Binary files /dev/null and b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png differ
diff --git a/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000000..af762f2bee4e1
Binary files /dev/null and b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000..eef47aa34b404
Binary files /dev/null and b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png
new file mode 100644
index 0000000000000..ea98c95bbbd8c
Binary files /dev/null and b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png differ
diff --git a/tests/MusicServiceDemo/res/layout/activity_main.xml b/tests/MusicServiceDemo/res/layout/activity_main.xml
new file mode 100644
index 0000000000000..71753e3405874
--- /dev/null
+++ b/tests/MusicServiceDemo/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+
+
+
diff --git a/tests/MusicServiceDemo/res/layout/fragment_main.xml b/tests/MusicServiceDemo/res/layout/fragment_main.xml
new file mode 100644
index 0000000000000..8796e86d7b228
--- /dev/null
+++ b/tests/MusicServiceDemo/res/layout/fragment_main.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/tests/MusicServiceDemo/res/values/colors.xml b/tests/MusicServiceDemo/res/values/colors.xml
new file mode 100644
index 0000000000000..44dd05d737742
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ #ffffff00
+ #ff00ff00
+ #ff0000ff
+ #ffff0000
+
diff --git a/tests/MusicServiceDemo/res/values/dimens.xml b/tests/MusicServiceDemo/res/values/dimens.xml
new file mode 100644
index 0000000000000..9f63ef29d0e10
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/dimens.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ 16dp
+ 16dp
+
+
diff --git a/tests/MusicServiceDemo/res/values/strings.xml b/tests/MusicServiceDemo/res/values/strings.xml
new file mode 100644
index 0000000000000..14c01711f624b
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ Music Service Demo
+ Settings
+ Thumbs Up
+ No music found
+ Now Playing
+
+
diff --git a/tests/MusicServiceDemo/res/values/styles.xml b/tests/MusicServiceDemo/res/values/styles.xml
new file mode 100644
index 0000000000000..b83662d6fdec0
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
new file mode 100644
index 0000000000000..9ca156f5d5d02
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.app.SearchManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.res.Resources.NotFoundException;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.media.browse.MediaBrowserItem;
+import android.media.browse.MediaBrowserService;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.example.android.musicservicedemo.browser.MusicProvider;
+import com.example.android.musicservicedemo.browser.MusicProviderTask;
+import com.example.android.musicservicedemo.browser.MusicProviderTaskListener;
+import com.example.android.musicservicedemo.browser.MusicTrack;
+
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Service that implements MediaBrowserService and returns our menu hierarchy.
+ */
+public class BrowserService extends MediaBrowserService {
+ private static final String TAG = "BrowserService";
+
+ // URI paths for browsing music
+ public static final String BROWSE_ROOT_BASE_PATH = "browse";
+ public static final String NOW_PLAYING_PATH = "now_playing";
+ public static final String PIANO_BASE_PATH = "piano";
+ public static final String VOICE_BASE_PATH = "voice";
+
+ // Content URIs
+ public static final String AUTHORITY = "com.example.android.automotive.musicplayer";
+ public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+ public static final Uri BROWSE_URI = Uri.withAppendedPath(BASE_URI, BROWSE_ROOT_BASE_PATH);
+
+ // URI matcher constants for browsing paths
+ public static final int BROWSE_ROOT = 1;
+ public static final int NOW_PLAYING = 2;
+ public static final int PIANO = 3;
+ public static final int VOICE = 4;
+
+ // Map the the URI paths with the URI matcher constants
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ static {
+ sUriMatcher.addURI(AUTHORITY, BROWSE_ROOT_BASE_PATH, BROWSE_ROOT);
+ sUriMatcher.addURI(AUTHORITY, NOW_PLAYING_PATH, NOW_PLAYING);
+ sUriMatcher.addURI(AUTHORITY, PIANO_BASE_PATH, PIANO);
+ sUriMatcher.addURI(AUTHORITY, VOICE_BASE_PATH, VOICE);
+ }
+
+ // Media metadata that will be provided for a media container
+ public static final String[] MEDIA_CONTAINER_PROJECTION = {
+ "uri",
+ "title",
+ "subtitle",
+ "image_uri",
+ "supported_actions"
+ };
+
+ // MusicProvider will download the music catalog
+ private MusicProvider mMusicProvider;
+
+ private MediaSession mSession;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mSession = new MediaSession(this, "com.example.android.musicservicedemo.BrowserService");
+ setSessionToken(mSession.getSessionToken());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public Uri onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ return BROWSE_URI;
+ }
+
+ @Override
+ public List onLoadChildren(Uri parentUri) {
+ final ArrayList results = new ArrayList();
+
+ for (int i=0; i<10; i++) {
+ results.add(new MediaBrowserItem.Builder(Uri.withAppendedPath(BASE_URI, Integer.toString(i)),
+ MediaBrowserItem.FLAG_BROWSABLE, "Title " + i).setSummary("Summary " + i).build());
+ }
+
+ return results;
+ }
+
+ @Override
+ public Bitmap onGetThumbnail(Uri uri, int width, int height, int density) {
+ return null;
+ }
+
+ /*
+ @Override
+ public void query(final Query query, final IMetadataResultHandler metadataResultHandler,
+ final IErrorHandler errorHandler)
+ throws RemoteException {
+ Log.d(TAG, "query: " + query);
+ Utils.checkNotNull(query);
+ Utils.checkNotNull(metadataResultHandler);
+ Utils.checkNotNull(errorHandler);
+
+ // Handle async response
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ // Pre-load the list of music
+ List musicTracks = getMusicList();
+ if (musicTracks == null) {
+ notifyListenersOnPlaybackStateUpdate(getCurrentPlaybackState());
+ errorHandler.onError(new Error(Error.UNKNOWN,
+ getString(R.string.music_error)));
+ return;
+ }
+
+ final Uri uri = query.getUri();
+ int match = sUriMatcher.match(uri);
+ Log.d(TAG, "Queried: " + uri + "; match: " + match);
+ switch (match) {
+ case BROWSE_ROOT:
+ {
+ Log.d(TAG, "Browse_root");
+
+ try {
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootContainerCurser();
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error delivering metadata in the callback.", e);
+ }
+ break;
+ }
+ case NOW_PLAYING:
+ {
+ try {
+ Log.d(TAG, "query NOW_PLAYING");
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootItemCursor(
+ PIANO);
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error querying NOW_PLAYING");
+ }
+ break;
+ }
+ case PIANO:
+ {
+ try {
+ Log.d(TAG, "query PIANO");
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootItemCursor(
+ PIANO);
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error querying PIANO");
+ }
+ break;
+ }
+ case VOICE:
+ {
+ try {
+ Log.d(TAG, "query VOICE");
+ MatrixCursor matrixCursor = mMusicProvider
+ .getRootItemCursor(
+ VOICE);
+ DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+ matrixCursor, null);
+ Log.d(TAG, "on metadata response called " + holder.getCount());
+ metadataResultHandler.onMetadataResponse(holder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error querying VOICE");
+ }
+ break;
+ }
+ default:
+ {
+ Log.w(TAG, "Skipping unmatched URI: " + uri);
+ }
+ }
+ } catch (NotFoundException e) {
+ Log.e(TAG, "::run:", e);
+ } catch (RemoteException e) {
+ Log.e(TAG, "::run:", e);
+ }
+ } // end run
+ }).start();
+ }
+
+ */
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
new file mode 100644
index 0000000000000..db45b9db1802c
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
@@ -0,0 +1,101 @@
+/* Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBarActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.android.musicservicedemo.R;
+
+// TODO Local UI
+
+/**
+ * Main activity of the app.
+ */
+public class MainActivity extends ActionBarActivity {
+
+ private static final String LOG = "MainActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.container, new PlaceholderFragment())
+ .commit();
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ // Inflate the menu; this adds items to the action bar if it is present.
+ //getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+ // if (id == R.id.action_settings) {
+ // return true;
+ // }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * A placeholder fragment containing a simple view.
+ */
+ public static class PlaceholderFragment extends Fragment {
+
+ public PlaceholderFragment() {
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * android.support.v4.app.Fragment#onCreateView(android.view.LayoutInflater
+ * , android.view.ViewGroup, android.os.Bundle)
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_main, container, false);
+ return rootView;
+ }
+ }
+
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
new file mode 100644
index 0000000000000..35897611e8d66
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class Utils {
+
+ private static final String TAG = "Utils";
+
+ /**
+ * Utility method to check that parameters are not null
+ *
+ * @param object
+ */
+ public static final void checkNotNull(Object object) {
+ if (object == null) {
+ throw new NullPointerException();
+ }
+ }
+
+ /**
+ * Utility to download a bitmap
+ *
+ * @param source
+ * @return
+ */
+ public static Bitmap getBitmapFromURL(String source) {
+ try {
+ URL url = new URL(source);
+ HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
+ httpConnection.setDoInput(true);
+ httpConnection.connect();
+ InputStream inputStream = httpConnection.getInputStream();
+ return BitmapFactory.decodeStream(inputStream);
+ } catch (IOException e) {
+ Log.e(TAG, "getBitmapFromUrl: " + source, e);
+ }
+ return null;
+ }
+
+ /**
+ * Utility method to wrap an index
+ *
+ * @param i
+ * @param size
+ * @return
+ */
+ public static int wrapIndex(int i, int size) {
+ int m = i % size;
+ if (m < 0) { // java modulus can be negative
+ m += size;
+ }
+ return m;
+ }
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
new file mode 100644
index 0000000000000..15038d78f2edd
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+import android.database.MatrixCursor;
+import android.media.session.PlaybackState;
+import android.net.Uri;
+import android.util.Log;
+
+import com.example.android.musicservicedemo.BrowserService;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class to get a list of MusicTrack's based on a server-side JSON
+ * configuration.
+ */
+public class MusicProvider {
+
+ private static final String TAG = "MusicProvider";
+
+ private static final String MUSIC_URL = "http://storage.googleapis.com/automotive-media/music.json";
+
+ private static String MUSIC = "music";
+ private static String TITLE = "title";
+ private static String ALBUM = "album";
+ private static String ARTIST = "artist";
+ private static String GENRE = "genre";
+ private static String SOURCE = "source";
+ private static String IMAGE = "image";
+ private static String TRACK_NUMBER = "trackNumber";
+ private static String TOTAL_TRACK_COUNT = "totalTrackCount";
+ private static String DURATION = "duration";
+
+ // Cache for music track data
+ private static List mMusicList;
+
+ /**
+ * Get the cached list of music tracks
+ *
+ * @return
+ * @throws JSONException
+ */
+ public List getMedia() throws JSONException {
+ if (null != mMusicList && mMusicList.size() > 0) {
+ return mMusicList;
+ }
+ return null;
+ }
+
+ /**
+ * Get the list of music tracks from a server and return the list of
+ * MusicTrack objects.
+ *
+ * @return
+ * @throws JSONException
+ */
+ public List retreiveMedia() throws JSONException {
+ if (null != mMusicList) {
+ return mMusicList;
+ }
+ int slashPos = MUSIC_URL.lastIndexOf('/');
+ String path = MUSIC_URL.substring(0, slashPos + 1);
+ JSONObject jsonObj = parseUrl(MUSIC_URL);
+
+ try {
+ JSONArray videos = jsonObj.getJSONArray(MUSIC);
+ if (null != videos) {
+ mMusicList = new ArrayList();
+ for (int j = 0; j < videos.length(); j++) {
+ JSONObject music = videos.getJSONObject(j);
+ String title = music.getString(TITLE);
+ String album = music.getString(ALBUM);
+ String artist = music.getString(ARTIST);
+ String genre = music.getString(GENRE);
+ String source = music.getString(SOURCE);
+ // Media is stored relative to JSON file
+ if (!source.startsWith("http")) {
+ source = path + source;
+ }
+ String image = music.getString(IMAGE);
+ if (!image.startsWith("http")) {
+ image = path + image;
+ }
+ int trackNumber = music.getInt(TRACK_NUMBER);
+ int totalTrackCount = music.getInt(TOTAL_TRACK_COUNT);
+ int duration = music.getInt(DURATION) * 1000; // ms
+
+ mMusicList.add(new MusicTrack(title, album, artist, genre, source,
+ image, trackNumber, totalTrackCount, duration));
+ }
+ }
+ } catch (NullPointerException e) {
+ Log.e(TAG, "retreiveMedia", e);
+ }
+ return mMusicList;
+ }
+
+ /**
+ * Download a JSON file from a server, parse the content and return the JSON
+ * object.
+ *
+ * @param urlString
+ * @return
+ */
+ private JSONObject parseUrl(String urlString) {
+ InputStream is = null;
+ try {
+ java.net.URL url = new java.net.URL(urlString);
+ URLConnection urlConnection = url.openConnection();
+ is = new BufferedInputStream(urlConnection.getInputStream());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ urlConnection.getInputStream(), "iso-8859-1"), 8);
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ return new JSONObject(sb.toString());
+ } catch (Exception e) {
+ Log.d(TAG, "Failed to parse the json for media list", e);
+ return null;
+ } finally {
+ if (null != is) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ public MatrixCursor getRootContainerCurser() {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+ Uri.Builder pianoBuilder = new Uri.Builder();
+ pianoBuilder.authority(BrowserService.AUTHORITY);
+ pianoBuilder.appendPath(BrowserService.PIANO_BASE_PATH);
+ matrixCursor.addRow(new Object[] {
+ pianoBuilder.build(),
+ BrowserService.PIANO_BASE_PATH,
+ "subtitle",
+ null,
+ 0
+ });
+
+ Uri.Builder voiceBuilder = new Uri.Builder();
+ voiceBuilder.authority(BrowserService.AUTHORITY);
+ voiceBuilder.appendPath(BrowserService.VOICE_BASE_PATH);
+ matrixCursor.addRow(new Object[] {
+ voiceBuilder.build(),
+ BrowserService.VOICE_BASE_PATH,
+ "subtitle",
+ null,
+ 0
+ });
+ return matrixCursor;
+ }
+
+ public MatrixCursor getRootItemCursor(int type) {
+ if (type == BrowserService.NOW_PLAYING) {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+ try {
+ // Just return all of the tracks for now
+ List musicTracks = retreiveMedia();
+ for (MusicTrack musicTrack : musicTracks) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.authority(BrowserService.AUTHORITY);
+ builder.appendPath(BrowserService.NOW_PLAYING_PATH);
+ builder.appendPath(musicTrack.getTitle());
+ matrixCursor.addRow(new Object[] {
+ builder.build(),
+ musicTrack.getTitle(),
+ musicTrack.getArtist(),
+ musicTrack.getImage(),
+ PlaybackState.ACTION_PLAY
+ });
+ Log.d(TAG, "Uri " + builder.build());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "::getRootItemCursor:", e);
+ }
+
+ Log.d(TAG, "cursor: " + matrixCursor.getCount());
+ return matrixCursor;
+ } else if (type == BrowserService.PIANO) {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+ try {
+ List musicTracks = retreiveMedia();
+ for (MusicTrack musicTrack : musicTracks) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.authority(BrowserService.AUTHORITY);
+ builder.appendPath(BrowserService.PIANO_BASE_PATH);
+ builder.appendPath(musicTrack.getTitle());
+ matrixCursor.addRow(new Object[] {
+ builder.build(),
+ musicTrack.getTitle(),
+ musicTrack.getArtist(),
+ musicTrack.getImage(),
+ PlaybackState.ACTION_PLAY
+ });
+ Log.d(TAG, "Uri " + builder.build());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "::getRootItemCursor:", e);
+ }
+
+ Log.d(TAG, "cursor: " + matrixCursor.getCount());
+ return matrixCursor;
+ } else if (type == BrowserService.VOICE) {
+ MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+ try {
+ List musicTracks = retreiveMedia();
+ for (MusicTrack musicTrack : musicTracks) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.authority(BrowserService.AUTHORITY);
+ builder.appendPath(BrowserService.VOICE_BASE_PATH);
+ builder.appendPath(musicTrack.getTitle());
+ matrixCursor.addRow(new Object[] {
+ builder.build(),
+ musicTrack.getTitle(),
+ musicTrack.getArtist(),
+ musicTrack.getImage(),
+ PlaybackState.ACTION_PLAY
+ });
+ Log.d(TAG, "Uri " + builder.build());
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "::getRootItemCursor:", e);
+ }
+
+ Log.d(TAG, "cursor: " + matrixCursor.getCount());
+ return matrixCursor;
+
+ }
+ return null;
+ }
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
new file mode 100644
index 0000000000000..ffda1108e662b
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONException;
+
+/**
+ * Asynchronous task to retrieve the music data using MusicProvider.
+ */
+public class MusicProviderTask extends AsyncTask {
+
+ private static final String TAG = "MusicProviderTask";
+
+ MusicProvider mMusicProvider;
+ MusicProviderTaskListener mMusicProviderTaskListener;
+
+ /**
+ * Initialize the task with the provider to download the music data and the
+ * listener to be informed when the task is done.
+ *
+ * @param musicProvider
+ * @param listener
+ */
+ public MusicProviderTask(MusicProvider musicProvider,
+ MusicProviderTaskListener listener) {
+ mMusicProvider = musicProvider;
+ mMusicProviderTaskListener = listener;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.os.AsyncTask#doInBackground(java.lang.Object[])
+ */
+ @Override
+ protected Void doInBackground(Void... arg0) {
+ try {
+ mMusicProvider.retreiveMedia();
+ } catch (JSONException e) {
+ Log.e(TAG, "::doInBackground:", e);
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.os.AsyncTask#onPostExecute(java.lang.Object)
+ */
+ @Override
+ protected void onPostExecute(Void result) {
+ mMusicProviderTaskListener.onMusicProviderTaskCompleted();
+ }
+
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
new file mode 100644
index 0000000000000..b1d168fe34e1e
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+/**
+ * Callback listener for completion of MusicProviderTask
+ */
+public interface MusicProviderTaskListener {
+ public void onMusicProviderTaskCompleted();
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
new file mode 100644
index 0000000000000..02ea89918f7df
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+/**
+ * A class to model music track metadata.
+ */
+public class MusicTrack {
+
+ private static final String TAG = "MusicTrack";
+
+ private String mTitle;
+ private String mAlbum;
+ private String mArtist;
+ private String mGenre;
+ private String mSource;
+ private String mImage;
+ private int mTrackNumber;
+ private int mTotalTrackCount;
+ private int mDuration;
+
+ /**
+ * Constructor creating a MusicTrack instance.
+ *
+ * @param title
+ * @param album
+ * @param artist
+ * @param genre
+ * @param source
+ * @param image
+ * @param trackNumber
+ * @param totalTrackCount
+ * @param duration
+ */
+ public MusicTrack(String title, String album, String artist, String genre, String source,
+ String image, int trackNumber, int totalTrackCount, int duration) {
+ this.mTitle = title;
+ this.mAlbum = album;
+ this.mArtist = artist;
+ this.mGenre = genre;
+ this.mSource = source;
+ this.mImage = image;
+ this.mTrackNumber = trackNumber;
+ this.mTotalTrackCount = totalTrackCount;
+ this.mDuration = duration;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public void setTitle(String mTitle) {
+ this.mTitle = mTitle;
+ }
+
+ public String getAlbum() {
+ return mAlbum;
+ }
+
+ public void setAlbum(String mAlbum) {
+ this.mAlbum = mAlbum;
+ }
+
+ public String getArtist() {
+ return mArtist;
+ }
+
+ public void setArtist(String mArtist) {
+ this.mArtist = mArtist;
+ }
+
+ public String getGenre() {
+ return mGenre;
+ }
+
+ public void setGenre(String mGenre) {
+ this.mGenre = mGenre;
+ }
+
+ public String getSource() {
+ return mSource;
+ }
+
+ public void setSource(String mSource) {
+ this.mSource = mSource;
+ }
+
+ public String getImage() {
+ return mImage;
+ }
+
+ public void setImage(String mImage) {
+ this.mImage = mImage;
+ }
+
+ public int getTrackNumber() {
+ return mTrackNumber;
+ }
+
+ public void setTrackNumber(int mTrackNumber) {
+ this.mTrackNumber = mTrackNumber;
+ }
+
+ public int getTotalTrackCount() {
+ return mTotalTrackCount;
+ }
+
+ public void setTotalTrackCount(int mTotalTrackCount) {
+ this.mTotalTrackCount = mTotalTrackCount;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+
+ public void setDuration(int mDuration) {
+ this.mDuration = mDuration;
+ }
+
+ public String toString() {
+ return mTitle;
+ }
+
+}