aboutsummaryrefslogtreecommitdiff
path: root/media
diff options
context:
space:
mode:
authorNicole Borrelli <borrelli@google.com>2017-02-28 14:14:58 -0800
committerNicole Borrelli <borrelli@google.com>2017-05-04 14:10:27 -0700
commit2f144e3d4d6cc18b79f79a962aa8cd790d74b412 (patch)
treeaa32def7148d626aa54c13f2390ea5d0b5e025bc /media
parent83a1ddaa52c27e8edefe93a68b91c7e26668d0e4 (diff)
downloadandroid-2f144e3d4d6cc18b79f79a962aa8cd790d74b412.tar.gz
Vastly simplify the MediaBrowserService sample.
Bug: 37631425 Test: Existing tests pass. Manually verified. Change-Id: Id7f86295e1c8d7372b4f79b3e511a736190792bf
Diffstat (limited to 'media')
-rw-r--r--media/MediaBrowserService/Application/src/androidTest/java/com/example/android/mediabrowserservice/test/SampleTests.java2
-rw-r--r--media/MediaBrowserService/Application/src/main/AndroidManifest.xml33
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java99
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/BrowseFragment.java190
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationHelper.java86
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java354
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java29
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java1299
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java161
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java226
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueAdapter.java82
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueFragment.java290
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java208
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java54
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java83
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java55
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java97
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java115
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java149
-rw-r--r--media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java53
-rw-r--r--media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml2
-rw-r--r--media/MediaBrowserService/Application/src/main/res/layout/fragment_list.xml35
-rw-r--r--media/MediaBrowserService/Application/src/main/res/layout/media_list_item.xml4
-rw-r--r--media/MediaBrowserService/Application/src/main/res/values/styles.xml16
-rw-r--r--media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml174
-rw-r--r--media/MediaBrowserService/README.md8
-rw-r--r--media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties4
-rw-r--r--media/MediaBrowserService/template-params.xml22
28 files changed, 1080 insertions, 2850 deletions
diff --git a/media/MediaBrowserService/Application/src/androidTest/java/com/example/android/mediabrowserservice/test/SampleTests.java b/media/MediaBrowserService/Application/src/androidTest/java/com/example/android/mediabrowserservice/test/SampleTests.java
index 7bf34ebe..665170dd 100644
--- a/media/MediaBrowserService/Application/src/androidTest/java/com/example/android/mediabrowserservice/test/SampleTests.java
+++ b/media/MediaBrowserService/Application/src/androidTest/java/com/example/android/mediabrowserservice/test/SampleTests.java
@@ -30,8 +30,6 @@
*/
package com.example.android.mediabrowserservice.test;
-import com.example.android.mediabrowserservice.*;
-
import android.test.ActivityInstrumentationTestCase2;
/**
diff --git a/media/MediaBrowserService/Application/src/main/AndroidManifest.xml b/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
index 52c4011e..570aeb47 100644
--- a/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
+++ b/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?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");
@@ -18,7 +17,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.mediabrowserservice"
android:versionCode="1"
- android:versionName="1.0" >
+ android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -30,12 +29,15 @@
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning,MissingIntentFilterForMediaSearch">
- <meta-data android:name="com.google.android.gms.car.application"
- android:resource="@xml/automotive_app_desc"/>
+ <meta-data
+ android:name="com.google.android.gms.car.application"
+ android:resource="@xml/automotive_app_desc" />
- <activity android:name=".MusicPlayerActivity"
- android:label="@string/app_name">
+ <activity
+ android:name=".MusicPlayerActivity"
+ android:label="@string/app_name"
+ android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -60,13 +62,26 @@
<service
android:name=".MusicService"
- android:exported="true"
- >
+ android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
+ <!--
+ MediaSession, prior to API 21, uses a broadcast receiver to communicate with a
+ media session. It does not have to be this broadcast receiver, but it must
+ handle the action "android.intent.action.MEDIA_BUTTON".
+
+ Additionally, this is used to resume the service from an inactive state upon
+ receiving a media button event (such as "play").
+ -->
+ <receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+
</application>
</manifest>
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java
index 4154381e..0f6f13f3 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java
@@ -17,21 +17,38 @@
package com.example.android.mediabrowserservice;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.os.AsyncTask;
+import android.util.Log;
import android.util.LruCache;
-import com.example.android.mediabrowserservice.utils.BitmapHelper;
-import com.example.android.mediabrowserservice.utils.LogHelper;
-
+import java.io.BufferedInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
/**
* Implements a basic cache of album arts, with async loading support.
*/
public final class AlbumArtCache {
- private static final String TAG = LogHelper.makeLogTag(AlbumArtCache.class);
+ private static final String TAG = AlbumArtCache.class.getSimpleName();
+
+ /**
+ * Listener for downloading album art.
+ */
+ public abstract static class FetchListener {
+ public abstract void onFetched(String artUrl, Bitmap bigImage, Bitmap iconImage);
+
+ public void onError(String artUrl, Exception e) {
+ Log.e(TAG, "AlbumArtFetchListener: error while downloading " + artUrl, e);
+ }
+ }
+
+ // Max read limit that we allow our input stream to mark/reset.
+ private static final int MAX_READ_LIMIT_PER_IMG = 1024 * 1024;
- private static final int MAX_ALBUM_ART_CACHE_SIZE = 12*1024*1024; // 12 MB
+ private static final int MAX_ALBUM_ART_CACHE_SIZE = 12 * 1024 * 1024; // 12 MB
private static final int MAX_ART_WIDTH = 800; // pixels
private static final int MAX_ART_HEIGHT = 480; // pixels
@@ -57,12 +74,12 @@ public final class AlbumArtCache {
// Holds no more than MAX_ALBUM_ART_CACHE_SIZE bytes, bounded by maxmemory/4 and
// Integer.MAX_VALUE:
int maxSize = Math.min(MAX_ALBUM_ART_CACHE_SIZE,
- (int) (Math.min(Integer.MAX_VALUE, Runtime.getRuntime().maxMemory()/4)));
+ (int) (Math.min(Integer.MAX_VALUE, Runtime.getRuntime().maxMemory() / 4)));
mCache = new LruCache<String, Bitmap[]>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap[] value) {
return value[BIG_BITMAP_INDEX].getByteCount()
- + value[ICON_BITMAP_INDEX].getByteCount();
+ + value[ICON_BITMAP_INDEX].getByteCount();
}
};
}
@@ -84,28 +101,25 @@ public final class AlbumArtCache {
// a proper image loading library, like Glide.
Bitmap[] bitmap = mCache.get(artUrl);
if (bitmap != null) {
- LogHelper.d(TAG, "getOrFetch: album art is in cache, using it", artUrl);
+ Log.d(TAG, "getOrFetch: album art is in cache, using it: " + artUrl);
listener.onFetched(artUrl, bitmap[BIG_BITMAP_INDEX], bitmap[ICON_BITMAP_INDEX]);
return;
}
- LogHelper.d(TAG, "getOrFetch: starting asynctask to fetch ", artUrl);
+ Log.d(TAG, "getOrFetch: starting asynctask to fetch " + artUrl);
new AsyncTask<Void, Void, Bitmap[]>() {
@Override
protected Bitmap[] doInBackground(Void[] objects) {
Bitmap[] bitmaps;
try {
- Bitmap bitmap = BitmapHelper.fetchAndRescaleBitmap(artUrl,
- MAX_ART_WIDTH, MAX_ART_HEIGHT);
- Bitmap icon = BitmapHelper.scaleBitmap(bitmap,
- MAX_ART_WIDTH_ICON, MAX_ART_HEIGHT_ICON);
- bitmaps = new Bitmap[] {bitmap, icon};
+ Bitmap bitmap = fetchAndRescaleBitmap(artUrl, MAX_ART_WIDTH, MAX_ART_HEIGHT);
+ Bitmap icon = scaleBitmap(bitmap, MAX_ART_WIDTH_ICON, MAX_ART_HEIGHT_ICON);
+ bitmaps = new Bitmap[]{bitmap, icon};
mCache.put(artUrl, bitmaps);
} catch (IOException e) {
return null;
}
- LogHelper.d(TAG, "doInBackground: putting bitmap in cache. cache size=" +
- mCache.size());
+ Log.d(TAG, "doInBackground: putting bitmap in cache. cache size=" + mCache.size());
return bitmaps;
}
@@ -115,16 +129,59 @@ public final class AlbumArtCache {
listener.onError(artUrl, new IllegalArgumentException("got null bitmaps"));
} else {
listener.onFetched(artUrl,
- bitmaps[BIG_BITMAP_INDEX], bitmaps[ICON_BITMAP_INDEX]);
+ bitmaps[BIG_BITMAP_INDEX], bitmaps[ICON_BITMAP_INDEX]);
}
}
}.execute();
}
- public static abstract class FetchListener {
- public abstract void onFetched(String artUrl, Bitmap bigImage, Bitmap iconImage);
- public void onError(String artUrl, Exception e) {
- LogHelper.e(TAG, e, "AlbumArtFetchListener: error while downloading " + artUrl);
+ private Bitmap scaleBitmap(Bitmap src, int maxWidth, int maxHeight) {
+ double scaleFactor = Math.min(
+ ((double) maxWidth) / src.getWidth(), ((double) maxHeight) / src.getHeight());
+ return Bitmap.createScaledBitmap(src,
+ (int) (src.getWidth() * scaleFactor), (int) (src.getHeight() * scaleFactor), false);
+ }
+
+ private Bitmap scaleBitmap(int scaleFactor, InputStream inputStream) {
+ // Get the dimensions of the bitmap
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+
+ // Decode the image file into a Bitmap sized to fill the View
+ bitmapOptions.inJustDecodeBounds = false;
+ bitmapOptions.inSampleSize = scaleFactor;
+
+ return BitmapFactory.decodeStream(inputStream, null, bitmapOptions);
+ }
+
+ private int findScaleFactor(int targetWidth, int targetHeight, InputStream inputStream) {
+ // Get the dimensions of the bitmap
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(inputStream, null, bitmapOptions);
+ int actualWidth = bitmapOptions.outWidth;
+ int actualHeight = bitmapOptions.outHeight;
+
+ // Determine how much to scale down the image
+ return Math.min(actualWidth / targetWidth, actualHeight / targetHeight);
+ }
+
+ private Bitmap fetchAndRescaleBitmap(String uri, int width, int height)
+ throws IOException {
+ URL url = new URL(uri);
+ BufferedInputStream inputStream = null;
+ try {
+ HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ inputStream = new BufferedInputStream(urlConnection.getInputStream());
+ inputStream.mark(MAX_READ_LIMIT_PER_IMG);
+ int scaleFactor = findScaleFactor(width, height, inputStream);
+ Log.d(TAG, "Scaling bitmap " + uri + " by factor " + scaleFactor + " to support "
+ + width + "x" + height + "requested dimension");
+ inputStream.reset();
+ return scaleBitmap(scaleFactor, inputStream);
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
}
}
}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/BrowseFragment.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/BrowseFragment.java
index e4ccf76d..2e55cd89 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/BrowseFragment.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/BrowseFragment.java
@@ -15,12 +15,17 @@
*/
package com.example.android.mediabrowserservice;
-import android.app.Fragment;
import android.content.ComponentName;
import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
import android.os.Bundle;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -32,8 +37,6 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
-import com.example.android.mediabrowserservice.utils.LogHelper;
-
import java.util.ArrayList;
import java.util.List;
@@ -41,74 +44,105 @@ import java.util.List;
* A Fragment that lists all the various browsable queues available
* from a {@link android.service.media.MediaBrowserService}.
* <p/>
- * It uses a {@link MediaBrowser} to connect to the {@link MusicService}. Once connected,
- * the fragment subscribes to get all the children. All {@link MediaBrowser.MediaItem}'s
+ * It uses a {@link MediaBrowserCompat} to connect to the {@link MusicService}. Once connected,
+ * the fragment subscribes to get all the children. All {@link MediaBrowserCompat.MediaItem}'s
* that can be browsed are shown in a ListView.
*/
public class BrowseFragment extends Fragment {
- private static final String TAG = LogHelper.makeLogTag(BrowseFragment.class.getSimpleName());
+ private static final String TAG = BrowseFragment.class.getSimpleName();
public static final String ARG_MEDIA_ID = "media_id";
- public static interface FragmentDataHelper {
- void onMediaItemSelected(MediaBrowser.MediaItem item);
+ /**
+ * Interface between BrowseFragment and MusicPlayerActivity.
+ */
+ public interface FragmentDataHelper {
+ void onMediaItemSelected(MediaBrowserCompat.MediaItem item, boolean isPlaying);
}
// The mediaId to be used for subscribing for children using the MediaBrowser.
private String mMediaId;
- private MediaBrowser mMediaBrowser;
+ private MediaBrowserCompat mMediaBrowser;
private BrowseAdapter mBrowserAdapter;
- private MediaBrowser.SubscriptionCallback mSubscriptionCallback = new MediaBrowser.SubscriptionCallback() {
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
- mBrowserAdapter.clear();
- mBrowserAdapter.notifyDataSetInvalidated();
- for (MediaBrowser.MediaItem item : children) {
- mBrowserAdapter.add(item);
- }
- mBrowserAdapter.notifyDataSetChanged();
- }
-
- @Override
- public void onError(String id) {
- Toast.makeText(getActivity(), R.string.error_loading_media,
- Toast.LENGTH_LONG).show();
- }
- };
+ private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =
+ new MediaBrowserCompat.SubscriptionCallback() {
+
+ @Override
+ public void onChildrenLoaded(String parentId,
+ List<MediaBrowserCompat.MediaItem> children) {
+ mBrowserAdapter.clear();
+ mBrowserAdapter.notifyDataSetInvalidated();
+ for (MediaBrowserCompat.MediaItem item : children) {
+ mBrowserAdapter.add(item);
+ }
+ mBrowserAdapter.notifyDataSetChanged();
+ }
- private MediaBrowser.ConnectionCallback mConnectionCallback =
- new MediaBrowser.ConnectionCallback() {
- @Override
- public void onConnected() {
- LogHelper.d(TAG, "onConnected: session token " + mMediaBrowser.getSessionToken());
+ @Override
+ public void onError(String id) {
+ Toast.makeText(getActivity(), R.string.error_loading_media,
+ Toast.LENGTH_LONG).show();
+ }
+ };
+
+ private MediaBrowserCompat.ConnectionCallback mConnectionCallback =
+ new MediaBrowserCompat.ConnectionCallback() {
+ @Override
+ public void onConnected() {
+ Log.d(TAG, "onConnected: session token " + mMediaBrowser.getSessionToken());
+
+ if (mMediaId == null) {
+ mMediaId = mMediaBrowser.getRoot();
+ }
+ mMediaBrowser.subscribe(mMediaId, mSubscriptionCallback);
+ try {
+ MediaControllerCompat mediaController =
+ new MediaControllerCompat(getActivity(),
+ mMediaBrowser.getSessionToken());
+ MediaControllerCompat.setMediaController(getActivity(), mediaController);
+
+ // Register a Callback to stay in sync
+ mediaController.registerCallback(mControllerCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to MediaController", e);
+ }
+ }
- if (mMediaId == null) {
- mMediaId = mMediaBrowser.getRoot();
- }
- mMediaBrowser.subscribe(mMediaId, mSubscriptionCallback);
- if (mMediaBrowser.getSessionToken() == null) {
- throw new IllegalArgumentException("No Session token");
- }
- MediaController mediaController = new MediaController(getActivity(),
- mMediaBrowser.getSessionToken());
- getActivity().setMediaController(mediaController);
- }
+ @Override
+ public void onConnectionFailed() {
+ Log.e(TAG, "onConnectionFailed");
+ }
- @Override
- public void onConnectionFailed() {
- LogHelper.d(TAG, "onConnectionFailed");
- }
+ @Override
+ public void onConnectionSuspended() {
+ Log.d(TAG, "onConnectionSuspended");
+ MediaControllerCompat mediaController = MediaControllerCompat
+ .getMediaController(getActivity());
+ if (mediaController != null) {
+ mediaController.unregisterCallback(mControllerCallback);
+ MediaControllerCompat.setMediaController(getActivity(), null);
+ }
+ }
+ };
+
+ private MediaControllerCompat.Callback mControllerCallback =
+ new MediaControllerCompat.Callback() {
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ if (metadata != null) {
+ mBrowserAdapter.setCurrentMediaMetadata(metadata);
+ }
+ }
- @Override
- public void onConnectionSuspended() {
- LogHelper.d(TAG, "onConnectionSuspended");
- getActivity().setMediaController(null);
- }
- };
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ mBrowserAdapter.setPlaybackState(state);
+ mBrowserAdapter.notifyDataSetChanged();
+ }
+ };
public static BrowseFragment newInstance(String mediaId) {
Bundle args = new Bundle();
@@ -125,18 +159,16 @@ public class BrowseFragment extends Fragment {
mBrowserAdapter = new BrowseAdapter(getActivity());
- View controls = rootView.findViewById(R.id.controls);
- controls.setVisibility(View.GONE);
-
ListView listView = (ListView) rootView.findViewById(R.id.list_view);
listView.setAdapter(mBrowserAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- MediaBrowser.MediaItem item = mBrowserAdapter.getItem(position);
+ MediaBrowserCompat.MediaItem item = mBrowserAdapter.getItem(position);
+ boolean isPlaying = item.getMediaId().equals(mBrowserAdapter.getPlayingMediaId());
try {
FragmentDataHelper listener = (FragmentDataHelper) getActivity();
- listener.onMediaItemSelected(item);
+ listener.onMediaItemSelected(item, isPlaying);
} catch (ClassCastException ex) {
Log.e(TAG, "Exception trying to cast to FragmentDataHelper", ex);
}
@@ -146,7 +178,7 @@ public class BrowseFragment extends Fragment {
Bundle args = getArguments();
mMediaId = args.getString(ARG_MEDIA_ID, null);
- mMediaBrowser = new MediaBrowser(getActivity(),
+ mMediaBrowser = new MediaBrowserCompat(getActivity(),
new ComponentName(getActivity(), MusicService.class),
mConnectionCallback, null);
@@ -166,10 +198,29 @@ public class BrowseFragment extends Fragment {
}
// An adapter for showing the list of browsed MediaItem's
- private static class BrowseAdapter extends ArrayAdapter<MediaBrowser.MediaItem> {
+ private static class BrowseAdapter extends ArrayAdapter<MediaBrowserCompat.MediaItem> {
+ private String mCurrentMediaId;
+ private PlaybackStateCompat mPlaybackState;
public BrowseAdapter(Context context) {
- super(context, R.layout.media_list_item, new ArrayList<MediaBrowser.MediaItem>());
+ super(context, R.layout.media_list_item, new ArrayList<MediaBrowserCompat.MediaItem>());
+ }
+
+ @Nullable
+ public String getPlayingMediaId() {
+ boolean isPlaying = mPlaybackState != null
+ && mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ return isPlaying ? mCurrentMediaId : null;
+ }
+
+ private void setCurrentMediaMetadata(MediaMetadataCompat mediaMetadata) {
+ mCurrentMediaId = mediaMetadata != null
+ ? mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
+ : null;
+ }
+
+ private void setPlaybackState(PlaybackStateCompat playbackState) {
+ mPlaybackState = playbackState;
}
static class ViewHolder {
@@ -178,8 +229,9 @@ public class BrowseFragment extends Fragment {
TextView mDescriptionView;
}
+ @NonNull
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
ViewHolder holder;
@@ -196,15 +248,19 @@ public class BrowseFragment extends Fragment {
holder = (ViewHolder) convertView.getTag();
}
- MediaBrowser.MediaItem item = getItem(position);
+ MediaBrowserCompat.MediaItem item = getItem(position);
holder.mTitleView.setText(item.getDescription().getTitle());
holder.mDescriptionView.setText(item.getDescription().getDescription());
if (item.isPlayable()) {
- holder.mImageView.setImageDrawable(
- getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp));
+ int playRes = item.getMediaId().equals(getPlayingMediaId())
+ ? R.drawable.ic_equalizer_white_24dp
+ : R.drawable.ic_play_arrow_white_24dp;
+ holder.mImageView.setImageDrawable(getContext().getResources()
+ .getDrawable(playRes));
holder.mImageView.setVisibility(View.VISIBLE);
}
return convertView;
}
+
}
}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationHelper.java
new file mode 100644
index 00000000..4cda4057
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationHelper.java
@@ -0,0 +1,86 @@
+/*
+ * 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.mediabrowserservice;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaButtonReceiver;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v7.app.NotificationCompat;
+
+/**
+ * Helper class for building Media style Notifications from a
+ * {@link android.support.v4.media.session.MediaSessionCompat}.
+ */
+public class MediaNotificationHelper {
+ private MediaNotificationHelper() {
+ // Helper utility class; do not instantiate.
+ }
+
+ public static Notification createNotification(Context context,
+ MediaSessionCompat mediaSession) {
+ MediaControllerCompat controller = mediaSession.getController();
+ MediaMetadataCompat mMetadata = controller.getMetadata();
+ PlaybackStateCompat mPlaybackState = controller.getPlaybackState();
+
+ if (mMetadata == null || mPlaybackState == null) {
+ return null;
+ }
+
+ boolean isPlaying = mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ NotificationCompat.Action action = isPlaying
+ ? new NotificationCompat.Action(R.drawable.ic_pause_white_24dp,
+ context.getString(R.string.label_pause),
+ MediaButtonReceiver.buildMediaButtonPendingIntent(context,
+ PlaybackStateCompat.ACTION_PAUSE))
+ : new NotificationCompat.Action(R.drawable.ic_play_arrow_white_24dp,
+ context.getString(R.string.label_play),
+ MediaButtonReceiver.buildMediaButtonPendingIntent(context,
+ PlaybackStateCompat.ACTION_PLAY));
+
+ MediaDescriptionCompat description = mMetadata.getDescription();
+ Bitmap art = description.getIconBitmap();
+ if (art == null) {
+ // use a placeholder art while the remote art is being downloaded.
+ art = BitmapFactory.decodeResource(context.getResources(),
+ R.drawable.ic_default_art);
+ }
+
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context);
+ notificationBuilder
+ .setStyle(new NotificationCompat.MediaStyle()
+ // show only play/pause in compact view.
+ .setShowActionsInCompactView(new int[]{0})
+ .setMediaSession(mediaSession.getSessionToken()))
+ .addAction(action)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setShowWhen(false)
+ .setContentIntent(controller.getSessionActivity())
+ .setContentTitle(description.getTitle())
+ .setContentText(description.getSubtitle())
+ .setLargeIcon(art)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+
+ return notificationBuilder.build();
+ }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java
deleted file mode 100644
index 59da7285..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * 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.mediabrowserservice;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-
-import com.example.android.mediabrowserservice.utils.LogHelper;
-import com.example.android.mediabrowserservice.utils.ResourceHelper;
-
-/**
- * Keeps track of a notification and updates it automatically for a given
- * MediaSession. Maintaining a visible notification (usually) guarantees that the music service
- * won't be killed during playback.
- */
-public class MediaNotificationManager extends BroadcastReceiver {
- private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class);
-
- private static final int NOTIFICATION_ID = 412;
- private static final int REQUEST_CODE = 100;
-
- public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause";
- public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play";
- public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev";
- public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next";
-
- private final MusicService mService;
- private MediaSession.Token mSessionToken;
- private MediaController mController;
- private MediaController.TransportControls mTransportControls;
-
- private PlaybackState mPlaybackState;
- private MediaMetadata mMetadata;
-
- private NotificationManager mNotificationManager;
-
- private PendingIntent mPauseIntent;
- private PendingIntent mPlayIntent;
- private PendingIntent mPreviousIntent;
- private PendingIntent mNextIntent;
-
- private int mNotificationColor;
-
- private boolean mStarted = false;
-
- public MediaNotificationManager(MusicService service) {
- mService = service;
- updateSessionToken();
-
- mNotificationColor = ResourceHelper.getThemeColor(mService,
- android.R.attr.colorPrimary, Color.DKGRAY);
-
- mNotificationManager = (NotificationManager) mService
- .getSystemService(Context.NOTIFICATION_SERVICE);
-
- String pkg = mService.getPackageName();
- mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
- new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
- mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
- new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
- mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
- new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
- mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
- new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
-
- // Cancel all notifications to handle the case where the Service was killed and
- // restarted by the system.
- mNotificationManager.cancelAll();
- }
-
- /**
- * Posts the notification and starts tracking the session to keep it
- * updated. The notification will automatically be removed if the session is
- * destroyed before {@link #stopNotification} is called.
- */
- public void startNotification() {
- if (!mStarted) {
- mMetadata = mController.getMetadata();
- mPlaybackState = mController.getPlaybackState();
-
- // The notification must be updated after setting started to true
- Notification notification = createNotification();
- if (notification != null) {
- mController.registerCallback(mCb);
- IntentFilter filter = new IntentFilter();
- filter.addAction(ACTION_NEXT);
- filter.addAction(ACTION_PAUSE);
- filter.addAction(ACTION_PLAY);
- filter.addAction(ACTION_PREV);
- mService.registerReceiver(this, filter);
-
- mService.startForeground(NOTIFICATION_ID, notification);
- mStarted = true;
- }
- }
- }
-
- /**
- * Removes the notification and stops tracking the session. If the session
- * was destroyed this has no effect.
- */
- public void stopNotification() {
- if (mStarted) {
- mStarted = false;
- mController.unregisterCallback(mCb);
- try {
- mNotificationManager.cancel(NOTIFICATION_ID);
- mService.unregisterReceiver(this);
- } catch (IllegalArgumentException ex) {
- // ignore if the receiver is not registered.
- }
- mService.stopForeground(true);
- }
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- LogHelper.d(TAG, "Received intent with action " + action);
- switch (action) {
- case ACTION_PAUSE:
- mTransportControls.pause();
- break;
- case ACTION_PLAY:
- mTransportControls.play();
- break;
- case ACTION_NEXT:
- mTransportControls.skipToNext();
- break;
- case ACTION_PREV:
- mTransportControls.skipToPrevious();
- break;
- default:
- LogHelper.w(TAG, "Unknown intent ignored. Action=", action);
- }
- }
-
- /**
- * Update the state based on a change on the session token. Called either when
- * we are running for the first time or when the media session owner has destroyed the session
- * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()})
- */
- private void updateSessionToken() {
- MediaSession.Token freshToken = mService.getSessionToken();
- if (mSessionToken == null || !mSessionToken.equals(freshToken)) {
- if (mController != null) {
- mController.unregisterCallback(mCb);
- }
- mSessionToken = freshToken;
- mController = new MediaController(mService, mSessionToken);
- mTransportControls = mController.getTransportControls();
- if (mStarted) {
- mController.registerCallback(mCb);
- }
- }
- }
-
- private PendingIntent createContentIntent() {
- Intent openUI = new Intent(mService, MusicPlayerActivity.class);
- openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- return PendingIntent.getActivity(mService, REQUEST_CODE, openUI,
- PendingIntent.FLAG_CANCEL_CURRENT);
- }
-
- private final MediaController.Callback mCb = new MediaController.Callback() {
- @Override
- public void onPlaybackStateChanged(PlaybackState state) {
- mPlaybackState = state;
- LogHelper.d(TAG, "Received new playback state", state);
- if (state != null && (state.getState() == PlaybackState.STATE_STOPPED ||
- state.getState() == PlaybackState.STATE_NONE)) {
- stopNotification();
- } else {
- Notification notification = createNotification();
- if (notification != null) {
- mNotificationManager.notify(NOTIFICATION_ID, notification);
- }
- }
- }
-
- @Override
- public void onMetadataChanged(MediaMetadata metadata) {
- mMetadata = metadata;
- LogHelper.d(TAG, "Received new metadata ", metadata);
- Notification notification = createNotification();
- if (notification != null) {
- mNotificationManager.notify(NOTIFICATION_ID, notification);
- }
- }
-
- @Override
- public void onSessionDestroyed() {
- super.onSessionDestroyed();
- LogHelper.d(TAG, "Session was destroyed, resetting to the new session token");
- updateSessionToken();
- }
- };
-
- private Notification createNotification() {
- LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
- if (mMetadata == null || mPlaybackState == null) {
- return null;
- }
-
- Notification.Builder notificationBuilder = new Notification.Builder(mService);
- int playPauseButtonPosition = 0;
-
- // If skip to previous action is enabled
- if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
- notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp,
- mService.getString(R.string.label_previous), mPreviousIntent);
-
- // If there is a "skip to previous" button, the play/pause button will
- // be the second one. We need to keep track of it, because the MediaStyle notification
- // requires to specify the index of the buttons (actions) that should be visible
- // when in compact view.
- playPauseButtonPosition = 1;
- }
-
- addPlayPauseAction(notificationBuilder);
-
- // If skip to next action is enabled
- if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
- notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
- mService.getString(R.string.label_next), mNextIntent);
- }
-
- MediaDescription description = mMetadata.getDescription();
-
- String fetchArtUrl = null;
- Bitmap art = null;
- if (description.getIconUri() != null) {
- // This sample assumes the iconUri will be a valid URL formatted String, but
- // it can actually be any valid Android Uri formatted String.
- // async fetch the album art icon
- String artUrl = description.getIconUri().toString();
- art = AlbumArtCache.getInstance().getBigImage(artUrl);
- if (art == null) {
- fetchArtUrl = artUrl;
- // use a placeholder art while the remote art is being downloaded
- art = BitmapFactory.decodeResource(mService.getResources(),
- R.drawable.ic_default_art);
- }
- }
-
- notificationBuilder
- .setStyle(new Notification.MediaStyle()
- .setShowActionsInCompactView(
- new int[]{playPauseButtonPosition}) // show only play/pause in compact view
- .setMediaSession(mSessionToken))
- .setColor(mNotificationColor)
- .setSmallIcon(R.drawable.ic_notification)
- .setVisibility(Notification.VISIBILITY_PUBLIC)
- .setUsesChronometer(true)
- .setContentIntent(createContentIntent())
- .setContentTitle(description.getTitle())
- .setContentText(description.getSubtitle())
- .setLargeIcon(art);
-
- setNotificationPlaybackState(notificationBuilder);
- if (fetchArtUrl != null) {
- fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder);
- }
-
- return notificationBuilder.build();
- }
-
- private void addPlayPauseAction(Notification.Builder builder) {
- LogHelper.d(TAG, "updatePlayPauseAction");
- String label;
- int icon;
- PendingIntent intent;
- if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
- label = mService.getString(R.string.label_pause);
- icon = R.drawable.ic_pause_white_24dp;
- intent = mPauseIntent;
- } else {
- label = mService.getString(R.string.label_play);
- icon = R.drawable.ic_play_arrow_white_24dp;
- intent = mPlayIntent;
- }
- builder.addAction(new Notification.Action(icon, label, intent));
- }
-
- private void setNotificationPlaybackState(Notification.Builder builder) {
- LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState);
- if (mPlaybackState == null || !mStarted) {
- LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!");
- mService.stopForeground(true);
- return;
- }
- if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING
- && mPlaybackState.getPosition() >= 0) {
- LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ",
- (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds");
- builder
- .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition())
- .setShowWhen(true)
- .setUsesChronometer(true);
- } else {
- LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position");
- builder
- .setWhen(0)
- .setShowWhen(false)
- .setUsesChronometer(false);
- }
-
- // Make sure that the notification can be dismissed by the user when we are not playing:
- builder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING);
- }
-
- private void fetchBitmapFromURLAsync(final String bitmapUrl,
- final Notification.Builder builder) {
- AlbumArtCache.getInstance().fetch(bitmapUrl, new AlbumArtCache.FetchListener() {
- @Override
- public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
- if (mMetadata != null && mMetadata.getDescription() != null &&
- artUrl.equals(mMetadata.getDescription().getIconUri().toString())) {
- // If the media is still the same, update the notification:
- LogHelper.d(TAG, "fetchBitmapFromURLAsync: set bitmap to ", artUrl);
- builder.setLargeIcon(bitmap);
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
- }
- }
- });
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java
index ac231c70..0a3a7df8 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java
@@ -15,38 +15,43 @@
*/
package com.example.android.mediabrowserservice;
-import android.app.Activity;
-import android.media.browse.MediaBrowser;
import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v7.app.AppCompatActivity;
/**
* Main activity for the music player.
*/
-public class MusicPlayerActivity extends Activity
+public class MusicPlayerActivity extends AppCompatActivity
implements BrowseFragment.FragmentDataHelper {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
setContentView(R.layout.activity_player);
if (savedInstanceState == null) {
- getFragmentManager().beginTransaction()
+ getSupportFragmentManager().beginTransaction()
.add(R.id.container, BrowseFragment.newInstance(null))
.commit();
}
}
@Override
- public void onMediaItemSelected(MediaBrowser.MediaItem item) {
+ public void onMediaItemSelected(MediaBrowserCompat.MediaItem item, boolean isPlaying) {
if (item.isPlayable()) {
- getMediaController().getTransportControls().playFromMediaId(item.getMediaId(), null);
- QueueFragment queueFragment = QueueFragment.newInstance();
- getFragmentManager().beginTransaction()
- .replace(R.id.container, queueFragment)
- .addToBackStack(null)
- .commit();
+ MediaControllerCompat controller = MediaControllerCompat.getMediaController(this);
+ MediaControllerCompat.TransportControls controls = controller.getTransportControls();
+
+ // If the item is playing, pause it, otherwise start it
+ if (isPlaying) {
+ controls.pause();
+ } else {
+ controls.playFromMediaId(item.getMediaId(), null);
+ }
} else if (item.isBrowsable()) {
- getFragmentManager().beginTransaction()
+ getSupportFragmentManager().beginTransaction()
.replace(R.id.container, BrowseFragment.newInstance(item.getMediaId()))
.addToBackStack(null)
.commit();
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
index 05c8a0ed..1cfb023c 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
@@ -1,741 +1,564 @@
- /*
- * 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.
- */
+/*
+* 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.mediabrowserservice;
- import android.app.PendingIntent;
- import android.content.Context;
- import android.content.Intent;
- import android.graphics.Bitmap;
- import android.media.MediaDescription;
- import android.media.MediaMetadata;
- import android.media.browse.MediaBrowser.MediaItem;
- import android.media.session.MediaSession;
- import android.media.session.PlaybackState;
- import android.net.Uri;
- import android.os.Bundle;
- import android.os.Handler;
- import android.os.Message;
- import android.os.SystemClock;
- import android.service.media.MediaBrowserService;
- import android.text.TextUtils;
-
- import com.example.android.mediabrowserservice.model.MusicProvider;
- import com.example.android.mediabrowserservice.utils.CarHelper;
- import com.example.android.mediabrowserservice.utils.LogHelper;
- import com.example.android.mediabrowserservice.utils.MediaIDHelper;
- import com.example.android.mediabrowserservice.utils.QueueHelper;
-
- import java.lang.ref.WeakReference;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
-
- import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
- import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
- import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
-
- /**
- * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
- * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
- * exposes it through its MediaSession.Token, which allows the client to create a MediaController
- * that connects to and send control commands to the MediaSession remotely. This is useful for
- * user interfaces that need to interact with your media session, like Android Auto. You can
- * (should) also use the same service from your app's UI, which gives a seamless playback
- * experience to the user.
- *
- * To implement a MediaBrowserService, you need to:
- *
- * <ul>
- *
- * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing
- * related methods {@link android.service.media.MediaBrowserService#onGetRoot} and
- * {@link android.service.media.MediaBrowserService#onLoadChildren};
- * <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent
- * with the session's token {@link android.service.media.MediaBrowserService#setSessionToken};
- *
- * <li> Set a callback on the
- * {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}.
- * The callback will receive all the user's actions, like play, pause, etc;
- *
- * <li> Handle all the actual music playing using any method your app prefers (for example,
- * {@link android.media.MediaPlayer})
- *
- * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
- * {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)}
- * {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and
- * {@link android.media.session.MediaSession#setQueue(java.util.List)})
- *
- * <li> Declare and export the service in AndroidManifest with an intent receiver for the action
- * android.media.browse.MediaBrowserService
- *
- * </ul>
- *
- * To make your app compatible with Android Auto, you also need to:
- *
- * <ul>
- *
- * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
- * with a &lt;automotiveApp&gt; root element. For a media app, this must include
- * an &lt;uses name="media"/&gt; element as a child.
- * For example, in AndroidManifest.xml:
- * &lt;meta-data android:name="com.google.android.gms.car.application"
- * android:resource="@xml/automotive_app_desc"/&gt;
- * And in res/values/automotive_app_desc.xml:
- * &lt;automotiveApp&gt;
- * &lt;uses name="media"/&gt;
- * &lt;/automotiveApp&gt;
- *
- * </ul>
-
- * @see <a href="README.md">README.md</a> for more details.
- *
- */
-
- public class MusicService extends MediaBrowserService implements Playback.Callback {
-
- // The action of the incoming Intent indicating that it contains a command
- // to be executed (see {@link #onStartCommand})
- public static final String ACTION_CMD = "com.example.android.mediabrowserservice.ACTION_CMD";
- // The key in the extras of the incoming Intent indicating the command that
- // should be executed (see {@link #onStartCommand})
- public static final String CMD_NAME = "CMD_NAME";
- // A value of a CMD_NAME key in the extras of the incoming Intent that
- // indicates that the music playback should be paused (see {@link #onStartCommand})
- public static final String CMD_PAUSE = "CMD_PAUSE";
-
- private static final String TAG = LogHelper.makeLogTag(MusicService.class);
- // Action to thumbs up a media item
- private static final String CUSTOM_ACTION_THUMBS_UP =
- "com.example.android.mediabrowserservice.THUMBS_UP";
- // Delay stopSelf by using a handler.
- private static final int STOP_DELAY = 30000;
-
- // Music catalog manager
- private MusicProvider mMusicProvider;
- private MediaSession mSession;
- // "Now playing" queue:
- private List<MediaSession.QueueItem> mPlayingQueue;
- private int mCurrentIndexOnQueue;
- private MediaNotificationManager mMediaNotificationManager;
- // Indicates whether the service was started.
- private boolean mServiceStarted;
- private DelayedStopHandler mDelayedStopHandler = new DelayedStopHandler(this);
- private Playback mPlayback;
- private PackageValidator mPackageValidator;
-
- /*
- * (non-Javadoc)
- * @see android.app.Service#onCreate()
- */
- @Override
- public void onCreate() {
- super.onCreate();
- LogHelper.d(TAG, "onCreate");
-
- mPlayingQueue = new ArrayList<>();
- mMusicProvider = new MusicProvider();
- mPackageValidator = new PackageValidator(this);
-
- // Start a new MediaSession
- mSession = new MediaSession(this, "MusicService");
- setSessionToken(mSession.getSessionToken());
- mSession.setCallback(new MediaSessionCallback());
- mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
- MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
-
- mPlayback = new Playback(this, mMusicProvider);
- mPlayback.setState(PlaybackState.STATE_NONE);
- mPlayback.setCallback(this);
- mPlayback.start();
-
- Context context = getApplicationContext();
- Intent intent = new Intent(context, MusicPlayerActivity.class);
- PendingIntent pi = PendingIntent.getActivity(context, 99 /*request code*/,
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
- mSession.setSessionActivity(pi);
-
- Bundle extras = new Bundle();
- CarHelper.setSlotReservationFlags(extras, true, true, true);
- mSession.setExtras(extras);
-
- updatePlaybackState(null);
-
- mMediaNotificationManager = new MediaNotificationManager(this);
- }
-
- /**
- * (non-Javadoc)
- * @see android.app.Service#onStartCommand(android.content.Intent, int, int)
- */
- @Override
- public int onStartCommand(Intent startIntent, int flags, int startId) {
- if (startIntent != null) {
- String action = startIntent.getAction();
- String command = startIntent.getStringExtra(CMD_NAME);
- if (ACTION_CMD.equals(action)) {
- if (CMD_PAUSE.equals(command)) {
- if (mPlayback != null && mPlayback.isPlaying()) {
- handlePauseRequest();
- }
- }
- }
- }
- return START_STICKY;
- }
-
- /**
- * (non-Javadoc)
- * @see android.app.Service#onDestroy()
- */
- @Override
- public void onDestroy() {
- LogHelper.d(TAG, "onDestroy");
- // Service is being killed, so make sure we release our resources
- handleStopRequest(null);
-
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- // Always release the MediaSession to clean up resources
- // and notify associated MediaController(s).
- mSession.release();
- }
-
- @Override
- public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
- LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
- "; clientUid=" + clientUid + " ; rootHints=", rootHints);
- // To ensure you are not allowing any arbitrary app to browse your app's contents, you
- // need to check the origin:
- if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
- // If the request comes from an untrusted package, return null. No further calls will
- // be made to other media browsing methods.
- LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package "
- + clientPackageName);
- return null;
- }
- //noinspection StatementWithEmptyBody
- if (CarHelper.isValidCarPackage(clientPackageName)) {
- // Optional: if your app needs to adapt ads, music library or anything else that
- // needs to run differently when connected to the car, this is where you should handle
- // it.
- }
- return new BrowserRoot(MEDIA_ID_ROOT, null);
- }
-
- @Override
- public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
- if (!mMusicProvider.isInitialized()) {
- // Use result.detach to allow calling result.sendResult from another thread:
- result.detach();
-
- mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
- @Override
- public void onMusicCatalogReady(boolean success) {
- if (success) {
- loadChildrenImpl(parentMediaId, result);
- } else {
- updatePlaybackState(getString(R.string.error_no_metadata));
- result.sendResult(Collections.<MediaItem>emptyList());
- }
- }
- });
-
- } else {
- // If our music catalog is already loaded/cached, load them into result immediately
- loadChildrenImpl(parentMediaId, result);
- }
- }
-
- /**
- * Actual implementation of onLoadChildren that assumes that MusicProvider is already
- * initialized.
- */
- private void loadChildrenImpl(final String parentMediaId,
- final Result<List<MediaItem>> result) {
- LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
-
- List<MediaItem> mediaItems = new ArrayList<>();
-
- if (MEDIA_ID_ROOT.equals(parentMediaId)) {
- LogHelper.d(TAG, "OnLoadChildren.ROOT");
- mediaItems.add(new MediaItem(
- new MediaDescription.Builder()
- .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
- .setTitle(getString(R.string.browse_genres))
- .setIconUri(Uri.parse("android.resource://" +
- "com.example.android.mediabrowserservice/drawable/ic_by_genre"))
- .setSubtitle(getString(R.string.browse_genre_subtitle))
- .build(), MediaItem.FLAG_BROWSABLE
- ));
-
- } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
- LogHelper.d(TAG, "OnLoadChildren.GENRES");
- for (String genre : mMusicProvider.getGenres()) {
- MediaItem item = new MediaItem(
- new MediaDescription.Builder()
- .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre))
- .setTitle(genre)
- .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre))
- .build(), MediaItem.FLAG_BROWSABLE
- );
- mediaItems.add(item);
- }
-
- } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
- String genre = MediaIDHelper.getHierarchy(parentMediaId)[1];
- LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre);
- for (MediaMetadata track : mMusicProvider.getMusicsByGenre(genre)) {
- // Since mediaMetadata fields are immutable, we need to create a copy, so we
- // can set a hierarchy-aware mediaID. We will need to know the media hierarchy
- // when we get a onPlayFromMusicID call, so we can create the proper queue based
- // on where the music was selected from (by artist, by genre, random, etc)
- String hierarchyAwareMediaID = MediaIDHelper.createMediaID(
- track.getDescription().getMediaId(), MEDIA_ID_MUSICS_BY_GENRE, genre);
- MediaMetadata trackCopy = new MediaMetadata.Builder(track)
- .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
- .build();
- MediaItem bItem = new MediaItem(
- trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
- mediaItems.add(bItem);
- }
- } else {
- LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
- }
- LogHelper.d(TAG, "OnLoadChildren sending ", mediaItems.size(),
- " results for ", parentMediaId);
- result.sendResult(mediaItems);
- }
-
- private final class MediaSessionCallback extends MediaSession.Callback {
- @Override
- public void onPlay() {
- LogHelper.d(TAG, "play");
-
- if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
- mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
- mSession.setQueue(mPlayingQueue);
- mSession.setQueueTitle(getString(R.string.random_queue_title));
- // start playing from the beginning of the queue
- mCurrentIndexOnQueue = 0;
- }
-
- if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
- handlePlayRequest();
- }
- }
-
- @Override
- public void onSkipToQueueItem(long queueId) {
- LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
-
- if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
- // set the current index on queue from the music Id:
- mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
- // play the music
- handlePlayRequest();
- }
- }
-
- @Override
- public void onSeekTo(long position) {
- LogHelper.d(TAG, "onSeekTo:", position);
- mPlayback.seekTo((int) position);
- }
-
- @Override
- public void onPlayFromMediaId(String mediaId, Bundle extras) {
- LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras);
-
- // The mediaId used here is not the unique musicId. This one comes from the
- // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
- // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
- // so we can build the correct playing queue, based on where the track was
- // selected from.
- mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
- mSession.setQueue(mPlayingQueue);
- String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
- MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
- mSession.setQueueTitle(queueTitle);
-
- if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
- // set the current index on queue from the media Id:
- mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId);
-
- if (mCurrentIndexOnQueue < 0) {
- LogHelper.e(TAG, "playFromMediaId: media ID ", mediaId,
- " could not be found on queue. Ignoring.");
- } else {
- // play the music
- handlePlayRequest();
- }
- }
- }
-
- @Override
- public void onPause() {
- LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
- handlePauseRequest();
- }
-
- @Override
- public void onStop() {
- LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
- handleStopRequest(null);
- }
-
- @Override
- public void onSkipToNext() {
- LogHelper.d(TAG, "skipToNext");
- mCurrentIndexOnQueue++;
- if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
- // This sample's behavior: skipping to next when in last song returns to the
- // first song.
- mCurrentIndexOnQueue = 0;
- }
- if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
- handlePlayRequest();
- } else {
- LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
- mCurrentIndexOnQueue + " queue length=" +
- (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
- handleStopRequest("Cannot skip");
- }
- }
-
- @Override
- public void onSkipToPrevious() {
- LogHelper.d(TAG, "skipToPrevious");
- mCurrentIndexOnQueue--;
- if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
- // This sample's behavior: skipping to previous when in first song restarts the
- // first song.
- mCurrentIndexOnQueue = 0;
- }
- if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
- handlePlayRequest();
- } else {
- LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
- mCurrentIndexOnQueue + " queue length=" +
- (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
- handleStopRequest("Cannot skip");
- }
- }
-
- @Override
- public void onCustomAction(String action, Bundle extras) {
- if (CUSTOM_ACTION_THUMBS_UP.equals(action)) {
- LogHelper.i(TAG, "onCustomAction: favorite for current track");
- MediaMetadata track = getCurrentPlayingMusic();
- if (track != null) {
- String musicId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
- mMusicProvider.setFavorite(musicId, !mMusicProvider.isFavorite(musicId));
- }
- // playback state needs to be updated because the "Favorite" icon on the
- // custom action will change to reflect the new favorite state.
- updatePlaybackState(null);
- } else {
- LogHelper.e(TAG, "Unsupported action: ", action);
- }
- }
-
- @Override
- public void onPlayFromSearch(String query, Bundle extras) {
- LogHelper.d(TAG, "playFromSearch query=", query);
-
- if (TextUtils.isEmpty(query)) {
- // A generic search like "Play music" sends an empty query
- // and it's expected that we start playing something. What will be played depends
- // on the app: favorite playlist, "I'm feeling lucky", most recent, etc.
- mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
- } else {
- mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
- }
-
- LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size());
- mSession.setQueue(mPlayingQueue);
-
- if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
- // immediately start playing from the beginning of the search results
- mCurrentIndexOnQueue = 0;
-
- handlePlayRequest();
- } else {
- // if nothing was found, we need to warn the user and stop playing
- handleStopRequest(getString(R.string.no_search_results));
- }
- }
- }
-
- /**
- * Handle a request to play music
- */
- private void handlePlayRequest() {
- LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
-
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- if (!mServiceStarted) {
- LogHelper.v(TAG, "Starting service");
- // The MusicService needs to keep running even after the calling MediaBrowser
- // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
- // need to play media.
- startService(new Intent(getApplicationContext(), MusicService.class));
- mServiceStarted = true;
- }
-
- if (!mSession.isActive()) {
- mSession.setActive(true);
- }
-
- if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
- updateMetadata();
- mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
- }
- }
-
- /**
- * Handle a request to pause music
- */
- private void handlePauseRequest() {
- LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
- mPlayback.pause();
- // reset the delayed stop handler.
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
- }
-
- /**
- * Handle a request to stop music
- */
- private void handleStopRequest(String withError) {
- LogHelper.d(TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError);
- mPlayback.stop(true);
- // reset the delayed stop handler.
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
-
- updatePlaybackState(withError);
-
- // service is no longer necessary. Will be started again if needed.
- stopSelf();
- mServiceStarted = false;
- }
-
- private void updateMetadata() {
- if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
- LogHelper.e(TAG, "Can't retrieve current metadata.");
- updatePlaybackState(getResources().getString(R.string.error_no_metadata));
- return;
- }
- MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
- String musicId = MediaIDHelper.extractMusicIDFromMediaID(
- queueItem.getDescription().getMediaId());
- MediaMetadata track = mMusicProvider.getMusic(musicId);
- final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
- if (!musicId.equals(trackId)) {
- IllegalStateException e = new IllegalStateException("track ID should match musicId.");
- //noinspection WrongConstant
- LogHelper.e(TAG, "track ID should match musicId.",
- " musicId=", musicId, " trackId=", trackId,
- " mediaId from queueItem=", queueItem.getDescription().getMediaId(),
- " title from queueItem=", queueItem.getDescription().getTitle(),
- " mediaId from track=", track.getDescription().getMediaId(),
- " title from track=", track.getDescription().getTitle(),
- " source.hashcode from track=", track.getString(
- MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(),
- e);
- throw e;
- }
- LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
- mSession.setMetadata(track);
-
- // Set the proper album artwork on the media session, so it can be shown in the
- // locked screen and in other places.
- if (track.getDescription().getIconBitmap() == null &&
- track.getDescription().getIconUri() != null) {
- String albumUri = track.getDescription().getIconUri().toString();
- AlbumArtCache.getInstance().fetch(albumUri, new AlbumArtCache.FetchListener() {
- @Override
- public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
- MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
- MediaMetadata track = mMusicProvider.getMusic(trackId);
- track = new MediaMetadata.Builder(track)
-
- // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for
- // example, on the lockscreen background when the media session is active.
- .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
-
- // set small version of the album art in the DISPLAY_ICON. This is used on
- // the MediaDescription and thus it should be small to be serialized if
- // necessary..
- .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, icon)
-
- .build();
-
- mMusicProvider.updateMusic(trackId, track);
-
- // If we are still playing the same music
- String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID(
- queueItem.getDescription().getMediaId());
- if (trackId.equals(currentPlayingId)) {
- mSession.setMetadata(track);
- }
- }
- });
- }
- }
-
- /**
- * Update the current media player state, optionally showing an error message.
- *
- * @param error if not null, error message to present to the user.
- */
- private void updatePlaybackState(String error) {
- LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
- long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
- if (mPlayback != null && mPlayback.isConnected()) {
- position = mPlayback.getCurrentStreamPosition();
- }
-
- PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
- .setActions(getAvailableActions());
-
- setCustomAction(stateBuilder);
- int state = mPlayback.getState();
-
- // If there is an error message, send it to the playback state:
- if (error != null) {
- // Error states are really only supposed to be used for errors that cause playback to
- // stop unexpectedly and persist until the user takes action to fix it.
- stateBuilder.setErrorMessage(error);
- state = PlaybackState.STATE_ERROR;
- }
- stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
-
- // Set the activeQueueItemId if the current index is valid.
- if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
- MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
- stateBuilder.setActiveQueueItemId(item.getQueueId());
- }
-
- mSession.setPlaybackState(stateBuilder.build());
-
- if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
- mMediaNotificationManager.startNotification();
- }
- }
-
- private void setCustomAction(PlaybackState.Builder stateBuilder) {
- MediaMetadata currentMusic = getCurrentPlayingMusic();
- if (currentMusic != null) {
- // Set appropriate "Favorite" icon on Custom action:
- String musicId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
- int favoriteIcon = R.drawable.ic_star_off;
- if (mMusicProvider.isFavorite(musicId)) {
- favoriteIcon = R.drawable.ic_star_on;
- }
- LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
- musicId, " current favorite=", mMusicProvider.isFavorite(musicId));
- stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
- favoriteIcon);
- }
- }
-
- private long getAvailableActions() {
- long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
- PlaybackState.ACTION_PLAY_FROM_SEARCH;
- if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
- return actions;
- }
- if (mPlayback.isPlaying()) {
- actions |= PlaybackState.ACTION_PAUSE;
- }
- if (mCurrentIndexOnQueue > 0) {
- actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
- }
- if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
- actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
- }
- return actions;
- }
-
- private MediaMetadata getCurrentPlayingMusic() {
- if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
- MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
- if (item != null) {
- LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=",
- item.getDescription().getMediaId());
- return mMusicProvider.getMusic(
- MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
- }
- }
- return null;
- }
-
- /**
- * Implementation of the Playback.Callback interface
- */
- @Override
- public void onCompletion() {
- // The media player finished playing the current song, so we go ahead
- // and start the next.
- if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
- // In this sample, we restart the playing queue when it gets to the end:
- mCurrentIndexOnQueue++;
- if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
- mCurrentIndexOnQueue = 0;
- }
- handlePlayRequest();
- } else {
- // If there is nothing to play, we stop and release the resources:
- handleStopRequest(null);
- }
- }
-
- @Override
- public void onPlaybackStatusChanged(int state) {
- updatePlaybackState(null);
- }
-
- @Override
- public void onError(String error) {
- updatePlaybackState(error);
- }
-
- /**
- * A simple handler that stops the service if playback is not active (playing)
- */
- private static class DelayedStopHandler extends Handler {
- private final WeakReference<MusicService> mWeakReference;
-
- private DelayedStopHandler(MusicService service) {
- mWeakReference = new WeakReference<>(service);
- }
-
- @Override
- public void handleMessage(Message msg) {
- MusicService service = mWeakReference.get();
- if (service != null && service.mPlayback != null) {
- if (service.mPlayback.isPlaying()) {
- LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
- return;
- }
- LogHelper.d(TAG, "Stopping service with delay handler.");
- service.stopSelf();
- service.mServiceStarted = false;
- }
- }
- }
- }
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaButtonReceiver;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.example.android.mediabrowserservice.model.MusicProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static com.example.android.mediabrowserservice.model.MusicProvider.MEDIA_ID_EMPTY_ROOT;
+import static com.example.android.mediabrowserservice.model.MusicProvider.MEDIA_ID_ROOT;
+
+/**
+ * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
+ * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
+ * exposes it through its MediaSession.Token, which allows the client to create a MediaController
+ * that connects to and send control commands to the MediaSession remotely. This is useful for
+ * user interfaces that need to interact with your media session, like Android Auto. You can
+ * (should) also use the same service from your app's UI, which gives a seamless playback
+ * experience to the user.
+ * <p>
+ * To implement a MediaBrowserService, you need to:
+ * <p>
+ * <ul>
+ * <p>
+ * <li> Extend {@link android.support.v4.media.MediaBrowserServiceCompat}, implementing the media
+ * browsing related methods {@link android.support.v4.media.MediaBrowserServiceCompat#onGetRoot} and
+ * {@link android.support.v4.media.MediaBrowserServiceCompat#onLoadChildren};
+ * <li> In onCreate, start a new {@link android.support.v4.media.session.MediaSessionCompat} and
+ * notify its parent with the session's token
+ * {@link android.support.v4.media.MediaBrowserServiceCompat#setSessionToken};
+ * <p>
+ * <li> Set a callback on the
+ * {@link android.support.v4.media.session.MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}.
+ * The callback will receive all the user's actions, like play, pause, etc;
+ * <p>
+ * <li> Handle all the actual music playing using any method your app prefers (for example,
+ * {@link android.media.MediaPlayer})
+ * <p>
+ * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
+ * {@link android.support.v4.media.session.MediaSessionCompat#setPlaybackState(PlaybackStateCompat)}
+ * {@link android.support.v4.media.session.MediaSessionCompat#setMetadata(MediaMetadataCompat)} and
+ * if your implementation allows it,
+ * {@link android.support.v4.media.session.MediaSessionCompat#setQueue(List)})
+ * <p>
+ * <li> Declare and export the service in AndroidManifest with an intent receiver for the action
+ * android.media.browse.MediaBrowserService
+ * <li> Declare a broadcast receiver to receive media button events. This is required if your app
+ * supports Android KitKat or previous:
+ * &lt;receiver android:name="android.support.v4.media.session.MediaButtonReceiver"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/receiver&gt;
+ * <p>
+ * </ul>
+ * <p>
+ * To make your app compatible with Android Auto, you also need to:
+ * <p>
+ * <ul>
+ * <p>
+ * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
+ * with a &lt;automotiveApp&gt; root element. For a media app, this must include
+ * an &lt;uses name="media"/&gt; element as a child.
+ * For example, in AndroidManifest.xml:
+ * &lt;meta-data android:name="com.google.android.gms.car.application"
+ * android:resource="@xml/automotive_app_desc"/&gt;
+ * And in res/values/automotive_app_desc.xml:
+ * &lt;automotiveApp&gt;
+ * &lt;uses name="media"/&gt;
+ * &lt;/automotiveApp&gt;
+ * <p>
+ * </ul>
+ *
+ * @see <a href="README.md">README.md</a> for more details.
+ */
+
+public class MusicService extends MediaBrowserServiceCompat {
+ private static final String TAG = MusicService.class.getSimpleName();
+
+ // ID for our MediaNotification.
+ public static final int NOTIFICATION_ID = 412;
+
+ // Request code for starting the UI.
+ private static final int REQUEST_CODE = 99;
+
+ // Delay stopSelf by using a handler.
+ private static final long STOP_DELAY = TimeUnit.SECONDS.toMillis(30);
+ private static final int STOP_CMD = 0x7c48;
+
+ private MusicProvider mMusicProvider;
+ private MediaSessionCompat mSession;
+ public NotificationManagerCompat mNotificationManager;
+ // Indicates whether the service was started.
+ private boolean mServiceStarted;
+ private Playback mPlayback;
+ private MediaSessionCompat.QueueItem mCurrentMedia;
+ private AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+
+ /**
+ * Custom {@link Handler} to process the delayed stop command.
+ */
+ private Handler mDelayedStopHandler = new Handler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg == null || msg.what != STOP_CMD) {
+ return false;
+ }
+
+ if (!mPlayback.isPlaying()) {
+ Log.d(TAG, "Stopping service");
+ stopSelf();
+ mServiceStarted = false;
+ }
+ return false;
+ }
+ });
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Service#onCreate()
+ */
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "onCreate");
+
+ mMusicProvider = new MusicProvider();
+
+ // Start a new MediaSession.
+ mSession = new MediaSessionCompat(this, TAG);
+ setSessionToken(mSession.getSessionToken());
+ mSession.setCallback(new MediaSessionCallback());
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+
+ mPlayback = new Playback(this, mMusicProvider);
+ mPlayback.setCallback(new Playback.Callback() {
+ @Override
+ public void onPlaybackStatusChanged(int state) {
+ updatePlaybackState(null);
+ }
+
+ @Override
+ public void onCompletion() {
+ // In this simple implementation there isn't a play queue, so we simply 'stop' after
+ // the song is over.
+ handleStopRequest();
+ }
+
+ @Override
+ public void onError(String error) {
+ updatePlaybackState(error);
+ }
+ });
+
+ Context context = getApplicationContext();
+
+ // This is an Intent to launch the app's UI, used primarily by the ongoing notification.
+ Intent intent = new Intent(context, MusicPlayerActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent pi = PendingIntent.getActivity(context, REQUEST_CODE, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ mSession.setSessionActivity(pi);
+
+ mNotificationManager = NotificationManagerCompat.from(this);
+ mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(this);
+
+ updatePlaybackState(null);
+ }
+
+ /**
+ * (non-Javadoc)
+ *
+ * @see android.app.Service#onStartCommand(android.content.Intent, int, int)
+ */
+ @Override
+ public int onStartCommand(Intent startIntent, int flags, int startId) {
+ MediaButtonReceiver.handleIntent(mSession, startIntent);
+ return super.onStartCommand(startIntent, flags, startId);
+ }
+
+ /**
+ * (non-Javadoc)
+ *
+ * @see android.app.Service#onDestroy()
+ */
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy");
+ // Service is being killed, so make sure we release our resources
+ handleStopRequest();
+
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ // Always release the MediaSession to clean up resources
+ // and notify associated MediaController(s).
+ mSession.release();
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(@NonNull String clientPackageName,
+ int clientUid, Bundle rootHints) {
+ // Verify the client is authorized to browse media and return the root that
+ // makes the most sense here. In this example we simply verify the package name
+ // is the same as ours, but more complicated checks, and responses, are possible
+ if (!clientPackageName.equals(getPackageName())) {
+ // Allow the client to connect, but not browse, by returning an empty root
+ return new BrowserRoot(MEDIA_ID_EMPTY_ROOT, null);
+ }
+ return new BrowserRoot(MEDIA_ID_ROOT, null);
+ }
+
+ @Override
+ public void onLoadChildren(@NonNull final String parentMediaId,
+ @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
+ Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentMediaId);
+
+ if (!mMusicProvider.isInitialized()) {
+ // Use result.detach to allow calling result.sendResult from another thread:
+ result.detach();
+
+ mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
+ @Override
+ public void onMusicCatalogReady(boolean success) {
+ if (success) {
+ loadChildrenImpl(parentMediaId, result);
+ } else {
+ updatePlaybackState(getString(R.string.error_no_metadata));
+ result.sendResult(Collections.<MediaBrowserCompat.MediaItem>emptyList());
+ }
+ }
+ });
+
+ } else {
+ // If our music catalog is already loaded/cached, load them into result immediately
+ loadChildrenImpl(parentMediaId, result);
+ }
+ }
+
+ /**
+ * Actual implementation of onLoadChildren that assumes that MusicProvider is already
+ * initialized.
+ */
+ private void loadChildrenImpl(@NonNull final String parentMediaId,
+ final Result<List<MediaBrowserCompat.MediaItem>> result) {
+ List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
+
+ switch (parentMediaId) {
+ case MEDIA_ID_ROOT:
+ for (MediaMetadataCompat track : mMusicProvider.getAllMusics()) {
+ MediaBrowserCompat.MediaItem bItem =
+ new MediaBrowserCompat.MediaItem(track.getDescription(),
+ MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);
+ mediaItems.add(bItem);
+ }
+ break;
+ case MEDIA_ID_EMPTY_ROOT:
+ // Since the client provided the empty root we'll just send back an
+ // empty list
+ break;
+ default:
+ Log.w(TAG, "Skipping unmatched parentMediaId: " + parentMediaId);
+ break;
+ }
+ result.sendResult(mediaItems);
+ }
+
+ private final class MediaSessionCallback extends MediaSessionCompat.Callback {
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ Log.d(TAG, "playFromMediaId mediaId:" + mediaId + " extras=" + extras);
+
+ // The mediaId used here is not the unique musicId. This one comes from the
+ // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
+ // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
+ // so we can build the correct playing queue, based on where the track was
+ // selected from.
+ MediaMetadataCompat media = mMusicProvider.getMusic(mediaId);
+ if (media != null) {
+ mCurrentMedia =
+ new MediaSessionCompat.QueueItem(media.getDescription(), media.hashCode());
+
+ // play the music
+ handlePlayRequest();
+ }
+ }
+
+ @Override
+ public void onPlay() {
+ Log.d(TAG, "play");
+
+ if (mCurrentMedia != null) {
+ handlePlayRequest();
+ }
+ }
+
+ @Override
+ public void onSeekTo(long position) {
+ Log.d(TAG, "onSeekTo:" + position);
+ mPlayback.seekTo((int) position);
+ }
+
+ @Override
+ public void onPause() {
+ Log.d(TAG, "pause. current state=" + mPlayback.getState());
+ handlePauseRequest();
+ }
+
+ @Override
+ public void onStop() {
+ Log.d(TAG, "stop. current state=" + mPlayback.getState());
+ handleStopRequest();
+ }
+ }
+
+ /**
+ * Handle a request to play music
+ */
+ private void handlePlayRequest() {
+ Log.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
+
+ if (mCurrentMedia == null) {
+ // Nothing to play
+ return;
+ }
+
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ if (!mServiceStarted) {
+ Log.v(TAG, "Starting service");
+ // The MusicService needs to keep running even after the calling MediaBrowser
+ // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
+ // need to play media.
+ startService(new Intent(getApplicationContext(), MusicService.class));
+ mServiceStarted = true;
+ }
+
+ if (!mSession.isActive()) {
+ mSession.setActive(true);
+ }
+
+ updateMetadata();
+ mPlayback.play(mCurrentMedia);
+ }
+
+ /**
+ * Handle a request to pause music
+ */
+ private void handlePauseRequest() {
+ Log.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
+ mPlayback.pause();
+
+ // reset the delayed stop handler.
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ mDelayedStopHandler.sendEmptyMessageDelayed(STOP_CMD, STOP_DELAY);
+ }
+
+ /**
+ * Handle a request to stop music
+ */
+ private void handleStopRequest() {
+ Log.d(TAG, "handleStopRequest: mState=" + mPlayback.getState());
+ mPlayback.stop();
+ // reset the delayed stop handler.
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ mDelayedStopHandler.sendEmptyMessage(STOP_CMD);
+
+ updatePlaybackState(null);
+ }
+
+ private void updateMetadata() {
+ MediaSessionCompat.QueueItem queueItem = mCurrentMedia;
+ String musicId = queueItem.getDescription().getMediaId();
+ MediaMetadataCompat track = mMusicProvider.getMusic(musicId);
+
+ final String trackId = track.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
+ mSession.setMetadata(track);
+
+ // Set the proper album artwork on the media session, so it can be shown in the
+ // locked screen and in other places.
+ if (track.getDescription().getIconBitmap() == null
+ && track.getDescription().getIconUri() != null) {
+ fetchArtwork(trackId, track.getDescription().getIconUri());
+ postNotification();
+ }
+ }
+
+ private void fetchArtwork(final String trackId, final Uri albumUri) {
+ AlbumArtCache.getInstance().fetch(albumUri.toString(),
+ new AlbumArtCache.FetchListener() {
+ @Override
+ public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
+ MediaSessionCompat.QueueItem queueItem = mCurrentMedia;
+ MediaMetadataCompat track = mMusicProvider.getMusic(trackId);
+ track = new MediaMetadataCompat.Builder(track)
+
+ // Set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is
+ // used, for example, on the lockscreen background when the media
+ // session is active.
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
+
+ // Set small version of the album art in the DISPLAY_ICON. This is
+ // used on the MediaDescription and thus it should be small to be
+ // serialized if necessary.
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, icon)
+
+ .build();
+
+ mMusicProvider.updateMusic(trackId, track);
+
+ // If we are still playing the same music
+ String currentPlayingId = queueItem.getDescription().getMediaId();
+ if (trackId.equals(currentPlayingId)) {
+ mSession.setMetadata(track);
+ postNotification();
+ }
+ }
+ });
+ }
+
+ /**
+ * Update the current media player state, optionally showing an error message.
+ *
+ * @param error if not null, error message to present to the user.
+ */
+ private void updatePlaybackState(String error) {
+ Log.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
+ long position = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN;
+ if (mPlayback != null && mPlayback.isConnected()) {
+ position = mPlayback.getCurrentStreamPosition();
+ }
+
+ long playbackActions = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID;
+ if (mPlayback.isPlaying()) {
+ playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
+ }
+
+ PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
+ .setActions(playbackActions);
+
+ int state = mPlayback.getState();
+
+ // If there is an error message, send it to the playback state:
+ if (error != null) {
+ // Error states are really only supposed to be used for errors that cause playback to
+ // stop unexpectedly and persist until the user takes action to fix it.
+ stateBuilder.setErrorMessage(error);
+ state = PlaybackStateCompat.STATE_ERROR;
+ }
+
+ // Because the playback state is pulled from the Playback class lint thinks it may not
+ // match permitted values.
+ //noinspection WrongConstant
+ stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
+
+ // Set the activeQueueItemId if the current index is valid.
+ if (mCurrentMedia != null) {
+ stateBuilder.setActiveQueueItemId(mCurrentMedia.getQueueId());
+ }
+
+ mSession.setPlaybackState(stateBuilder.build());
+
+ if (state == PlaybackStateCompat.STATE_PLAYING) {
+ Notification notification = postNotification();
+ startForeground(NOTIFICATION_ID, notification);
+ mAudioBecomingNoisyReceiver.register();
+ } else {
+ if (state == PlaybackStateCompat.STATE_PAUSED) {
+ postNotification();
+ } else {
+ mNotificationManager.cancel(NOTIFICATION_ID);
+ }
+ stopForeground(false);
+ mAudioBecomingNoisyReceiver.unregister();
+ }
+ }
+
+ private Notification postNotification() {
+ Notification notification = MediaNotificationHelper.createNotification(this, mSession);
+ if (notification == null) {
+ return null;
+ }
+
+ mNotificationManager.notify(NOTIFICATION_ID, notification);
+ return notification;
+ }
+
+ /**
+ * Implementation of the AudioManager.ACTION_AUDIO_BECOMING_NOISY Receiver.
+ */
+
+ private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
+ private final Context mContext;
+ private boolean mIsRegistered = false;
+
+ private IntentFilter mAudioNoisyIntentFilter =
+ new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+
+ protected AudioBecomingNoisyReceiver(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ public void register() {
+ if (!mIsRegistered) {
+ mContext.registerReceiver(this, mAudioNoisyIntentFilter);
+ mIsRegistered = true;
+ }
+ }
+
+ public void unregister() {
+ if (mIsRegistered) {
+ mContext.unregisterReceiver(this);
+ mIsRegistered = false;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
+ handlePauseRequest();
+ }
+ }
+ }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java
deleted file mode 100644
index a6d1c859..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.mediabrowserservice;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.res.XmlResourceParser;
-import android.os.Process;
-import android.util.Base64;
-
-import com.example.android.mediabrowserservice.utils.LogHelper;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Validates that the calling package is authorized to browse a
- * {@link android.service.media.MediaBrowserService}.
- *
- * The list of allowed signing certificates and their corresponding package names is defined in
- * res/xml/allowed_media_browser_callers.xml.
- */
-public class PackageValidator {
- private static final String TAG = LogHelper.makeLogTag(PackageValidator.class);
-
- /**
- * Map allowed callers' certificate keys to the expected caller information.
- *
- */
- private final Map<String, ArrayList<CallerInfo>> mValidCertificates;
-
- public PackageValidator(Context ctx) {
- mValidCertificates = readValidCertificates(ctx.getResources().getXml(
- R.xml.allowed_media_browser_callers));
- }
-
- private Map<String, ArrayList<CallerInfo>> readValidCertificates(XmlResourceParser parser) {
- HashMap<String, ArrayList<CallerInfo>> validCertificates = new HashMap<>();
- try {
- int eventType = parser.next();
- while (eventType != XmlResourceParser.END_DOCUMENT) {
- if (eventType == XmlResourceParser.START_TAG
- && parser.getName().equals("signing_certificate")) {
-
- String name = parser.getAttributeValue(null, "name");
- String packageName = parser.getAttributeValue(null, "package");
- boolean isRelease = parser.getAttributeBooleanValue(null, "release", false);
- String certificate = parser.nextText().replaceAll("\\s|\\n", "");
-
- CallerInfo info = new CallerInfo(name, packageName, isRelease, certificate);
-
- ArrayList<CallerInfo> infos = validCertificates.get(certificate);
- if (infos == null) {
- infos = new ArrayList<>();
- validCertificates.put(certificate, infos);
- }
- LogHelper.v(TAG, "Adding allowed caller: ", info.name,
- " package=", info.packageName, " release=", info.release,
- " certificate=", certificate);
- infos.add(info);
- }
- eventType = parser.next();
- }
- } catch (XmlPullParserException | IOException e) {
- LogHelper.e(TAG, e, "Could not read allowed callers from XML.");
- }
- return validCertificates;
- }
-
- /**
- * @return false if the caller is not authorized to get data from this MediaBrowserService
- */
- @SuppressLint("PackageManagerGetSignatures")
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- boolean isCallerAllowed(Context context, String callingPackage, int callingUid) {
- // Always allow calls from the framework, self app or development environment.
- if (Process.SYSTEM_UID == callingUid || Process.myUid() == callingUid) {
- return true;
- }
- PackageManager packageManager = context.getPackageManager();
- PackageInfo packageInfo;
- try {
- packageInfo = packageManager.getPackageInfo(
- callingPackage, PackageManager.GET_SIGNATURES);
- } catch (PackageManager.NameNotFoundException e) {
- LogHelper.w(TAG, e, "Package manager can't find package: ", callingPackage);
- return false;
- }
- if (packageInfo.signatures.length != 1) {
- LogHelper.w(TAG, "Caller has more than one signature certificate!");
- return false;
- }
- String signature = Base64.encodeToString(
- packageInfo.signatures[0].toByteArray(), Base64.NO_WRAP);
-
- // Test for known signatures:
- ArrayList<CallerInfo> validCallers = mValidCertificates.get(signature);
- if (validCallers == null) {
- LogHelper.v(TAG, "Signature for caller ", callingPackage, " is not valid: \n"
- , signature);
- if (mValidCertificates.isEmpty()) {
- LogHelper.w(TAG, "The list of valid certificates is empty. Either your file ",
- "res/xml/allowed_media_browser_callers.xml is empty or there was an error ",
- "while reading it. Check previous log messages.");
- }
- return false;
- }
-
- // Check if the package name is valid for the certificate:
- StringBuffer expectedPackages = new StringBuffer();
- for (CallerInfo info: validCallers) {
- if (callingPackage.equals(info.packageName)) {
- LogHelper.v(TAG, "Valid caller: ", info.name, " package=", info.packageName,
- " release=", info.release);
- return true;
- }
- expectedPackages.append(info.packageName).append(' ');
- }
-
- LogHelper.i(TAG, "Caller has a valid certificate, but its package doesn't match any ",
- "expected package for the given certificate. Caller's package is ", callingPackage,
- ". Expected packages as defined in res/xml/allowed_media_browser_callers.xml are (",
- expectedPackages, "). This caller's certificate is: \n", signature);
-
- return false;
- }
-
- private final static class CallerInfo {
- final String name;
- final String packageName;
- final boolean release;
- final String signingCertificate;
-
- public CallerInfo(String name, String packageName, boolean release,
- String signingCertificate) {
- this.name = name;
- this.packageName = packageName;
- this.release = release;
- this.signingCertificate = signingCertificate;
- }
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
index 95d280f8..6af2d412 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
@@ -15,21 +15,18 @@
*/
package com.example.android.mediabrowserservice;
-import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.media.AudioManager;
-import android.media.MediaMetadata;
import android.media.MediaPlayer;
-import android.media.session.PlaybackState;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
+import android.util.Log;
import com.example.android.mediabrowserservice.model.MusicProvider;
-import com.example.android.mediabrowserservice.utils.LogHelper;
-import com.example.android.mediabrowserservice.utils.MediaIDHelper;
import java.io.IOException;
@@ -37,7 +34,6 @@ import static android.media.MediaPlayer.OnCompletionListener;
import static android.media.MediaPlayer.OnErrorListener;
import static android.media.MediaPlayer.OnPreparedListener;
import static android.media.MediaPlayer.OnSeekCompleteListener;
-import static android.media.session.MediaSession.QueueItem;
/**
* A class that implements local media playback using {@link android.media.MediaPlayer}
@@ -45,7 +41,27 @@ import static android.media.session.MediaSession.QueueItem;
public class Playback implements AudioManager.OnAudioFocusChangeListener,
OnCompletionListener, OnErrorListener, OnPreparedListener, OnSeekCompleteListener {
- private static final String TAG = LogHelper.makeLogTag(Playback.class);
+ private static final String TAG = Playback.class.getSimpleName();
+
+ /* package */ interface Callback {
+ /**
+ * On current music completed.
+ */
+ void onCompletion();
+
+ /**
+ * on Playback status changed
+ * Implementations can use this callback to update
+ * playback state on the media sessions.
+ */
+ void onPlaybackStatusChanged(int state);
+
+ /**
+ * @param error to be added to the PlaybackState
+ */
+ void onError(String error);
+
+ }
// The volume we set the media player to when we lose audio focus, but are
// allowed to reduce the volume instead of stopping playback.
@@ -58,15 +74,14 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
// we don't have focus, but can duck (play at a low volume)
private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
// we have full audio focus
- private static final int AUDIO_FOCUSED = 2;
+ private static final int AUDIO_FOCUSED = 2;
private final MusicService mService;
+ private final MusicProvider mMusicProvider;
private final WifiManager.WifiLock mWifiLock;
- private int mState;
+ private int mState = PlaybackStateCompat.STATE_NONE;
private boolean mPlayOnFocusGain;
private Callback mCallback;
- private MusicProvider mMusicProvider;
- private volatile boolean mAudioNoisyReceiverRegistered;
private volatile int mCurrentPosition;
private volatile String mCurrentMediaId;
@@ -75,45 +90,25 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
private AudioManager mAudioManager;
private MediaPlayer mMediaPlayer;
- private IntentFilter mAudioNoisyIntentFilter =
- new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
-
- private BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
- LogHelper.d(TAG, "Headphones disconnected.");
- if (isPlaying()) {
- Intent i = new Intent(context, MusicService.class);
- i.setAction(MusicService.ACTION_CMD);
- i.putExtra(MusicService.CMD_NAME, MusicService.CMD_PAUSE);
- mService.startService(i);
- }
- }
- }
- };
-
public Playback(MusicService service, MusicProvider musicProvider) {
+ Context context = service.getApplicationContext();
this.mService = service;
this.mMusicProvider = musicProvider;
- this.mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
- // Create the Wifi lock (this does not acquire the lock, this just creates it)
- this.mWifiLock = ((WifiManager) service.getSystemService(Context.WIFI_SERVICE))
- .createWifiLock(WifiManager.WIFI_MODE_FULL, "sample_lock");
- }
+ this.mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- public void start() {
+ // Create the Wifi lock (this does not acquire the lock, this just creates it).
+ this.mWifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
+ .createWifiLock(WifiManager.WIFI_MODE_FULL, "sample_lock");
}
- public void stop(boolean notifyListeners) {
- mState = PlaybackState.STATE_STOPPED;
- if (notifyListeners && mCallback != null) {
+ public void stop() {
+ mState = PlaybackStateCompat.STATE_STOPPED;
+ if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
mCurrentPosition = getCurrentStreamPosition();
// Give up Audio focus
giveUpAudioFocus();
- unregisterAudioNoisyReceiver();
// Relax all resources
relaxResources(true);
if (mWifiLock.isHeld()) {
@@ -121,10 +116,6 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
}
}
- public void setState(int state) {
- this.mState = state;
- }
-
public int getState() {
return mState;
}
@@ -138,14 +129,12 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
}
public int getCurrentStreamPosition() {
- return mMediaPlayer != null ?
- mMediaPlayer.getCurrentPosition() : mCurrentPosition;
+ return mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : mCurrentPosition;
}
- public void play(QueueItem item) {
+ public void play(MediaSessionCompat.QueueItem item) {
mPlayOnFocusGain = true;
tryToGetAudioFocus();
- registerAudioNoisyReceiver();
String mediaId = item.getDescription().getMediaId();
boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
if (mediaHasChanged) {
@@ -153,21 +142,19 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
mCurrentMediaId = mediaId;
}
- if (mState == PlaybackState.STATE_PAUSED && !mediaHasChanged && mMediaPlayer != null) {
+ if (mState == PlaybackStateCompat.STATE_PAUSED
+ && !mediaHasChanged && mMediaPlayer != null) {
configMediaPlayerState();
} else {
- mState = PlaybackState.STATE_STOPPED;
+ mState = PlaybackStateCompat.STATE_STOPPED;
relaxResources(false); // release everything except MediaPlayer
- MediaMetadata track = mMusicProvider.getMusic(
- MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
-
- //noinspection WrongConstant
- String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);
+ MediaMetadataCompat track = mMusicProvider.getMusic(item.getDescription().getMediaId());
+ String source = track.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI);
try {
createMediaPlayerIfNeeded();
- mState = PlaybackState.STATE_BUFFERING;
+ mState = PlaybackStateCompat.STATE_BUFFERING;
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(source);
@@ -188,17 +175,17 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
mCallback.onPlaybackStatusChanged(mState);
}
- } catch (IOException ex) {
- LogHelper.e(TAG, ex, "Exception playing song");
+ } catch (IOException ioException) {
+ Log.e(TAG, "Exception playing song", ioException);
if (mCallback != null) {
- mCallback.onError(ex.getMessage());
+ mCallback.onError(ioException.getMessage());
}
}
}
}
public void pause() {
- if (mState == PlaybackState.STATE_PLAYING) {
+ if (mState == PlaybackStateCompat.STATE_PLAYING) {
// Pause media player and cancel the 'foreground service' state.
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
@@ -206,24 +193,22 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
}
// while paused, retain the MediaPlayer but give up audio focus
relaxResources(false);
- giveUpAudioFocus();
}
- mState = PlaybackState.STATE_PAUSED;
+ mState = PlaybackStateCompat.STATE_PAUSED;
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
- unregisterAudioNoisyReceiver();
}
public void seekTo(int position) {
- LogHelper.d(TAG, "seekTo called with ", position);
+ Log.d(TAG, "seekTo called with " + position);
if (mMediaPlayer == null) {
- // If we do not have a current media player, simply update the current position
+ // If we do not have a current media player, simply update the current position.
mCurrentPosition = position;
} else {
if (mMediaPlayer.isPlaying()) {
- mState = PlaybackState.STATE_BUFFERING;
+ mState = PlaybackStateCompat.STATE_BUFFERING;
}
mMediaPlayer.seekTo(position);
if (mCallback != null) {
@@ -240,25 +225,20 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
* Try to get the system audio focus.
*/
private void tryToGetAudioFocus() {
- LogHelper.d(TAG, "tryToGetAudioFocus");
- if (mAudioFocus != AUDIO_FOCUSED) {
- int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
- if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- mAudioFocus = AUDIO_FOCUSED;
- }
- }
+ Log.d(TAG, "tryToGetAudioFocus");
+ int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ mAudioFocus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ ? AUDIO_FOCUSED : AUDIO_NO_FOCUS_NO_DUCK;
}
/**
* Give up the audio focus.
*/
private void giveUpAudioFocus() {
- LogHelper.d(TAG, "giveUpAudioFocus");
- if (mAudioFocus == AUDIO_FOCUSED) {
- if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
- }
+ Log.d(TAG, "giveUpAudioFocus");
+ if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
}
}
@@ -273,10 +253,10 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
* you are sure this is the case.
*/
private void configMediaPlayerState() {
- LogHelper.d(TAG, "configMediaPlayerState. mAudioFocus=", mAudioFocus);
+ Log.d(TAG, "configMediaPlayerState. mAudioFocus=" + mAudioFocus);
if (mAudioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
// If we don't have audio focus and can't duck, we have to pause,
- if (mState == PlaybackState.STATE_PLAYING) {
+ if (mState == PlaybackStateCompat.STATE_PLAYING) {
pause();
}
} else { // we have audio focus:
@@ -290,14 +270,14 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
// If we were playing when we lost focus, we need to resume playing.
if (mPlayOnFocusGain) {
if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
- LogHelper.d(TAG,"configMediaPlayerState startMediaPlayer. seeking to ",
- mCurrentPosition);
+ Log.d(TAG, "configMediaPlayerState startMediaPlayer. seeking to "
+ + mCurrentPosition);
if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
mMediaPlayer.start();
- mState = PlaybackState.STATE_PLAYING;
+ mState = PlaybackStateCompat.STATE_PLAYING;
} else {
mMediaPlayer.seekTo(mCurrentPosition);
- mState = PlaybackState.STATE_BUFFERING;
+ mState = PlaybackStateCompat.STATE_BUFFERING;
}
}
mPlayOnFocusGain = false;
@@ -310,18 +290,18 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
/**
* Called by AudioManager on audio focus changes.
- * Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener}
+ * Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener}.
*/
@Override
public void onAudioFocusChange(int focusChange) {
- LogHelper.d(TAG, "onAudioFocusChange. focusChange=", focusChange);
+ Log.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// We have gained focus:
mAudioFocus = AUDIO_FOCUSED;
- } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
- focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
- focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS
+ || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
+ || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// We have lost focus. If we can duck (low playback volume), we can keep playing.
// Otherwise, we need to pause the playback.
boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
@@ -329,29 +309,29 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
// If we are playing, we need to reset media player by calling configMediaPlayerState
// with mAudioFocus properly set.
- if (mState == PlaybackState.STATE_PLAYING && !canDuck) {
+ if (mState == PlaybackStateCompat.STATE_PLAYING && !canDuck) {
// If we don't have audio focus and can't duck, we save the information that
// we were playing, so that we can resume playback once we get the focus back.
mPlayOnFocusGain = true;
}
} else {
- LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: ", focusChange);
+ Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
}
configMediaPlayerState();
}
/**
- * Called when MediaPlayer has completed a seek
+ * Called when MediaPlayer has completed a seek.
*
* @see android.media.MediaPlayer.OnSeekCompleteListener
*/
@Override
- public void onSeekComplete(MediaPlayer mp) {
- LogHelper.d(TAG, "onSeekComplete from MediaPlayer:", mp.getCurrentPosition());
- mCurrentPosition = mp.getCurrentPosition();
- if (mState == PlaybackState.STATE_BUFFERING) {
+ public void onSeekComplete(MediaPlayer player) {
+ Log.d(TAG, "onSeekComplete from MediaPlayer:" + player.getCurrentPosition());
+ mCurrentPosition = player.getCurrentPosition();
+ if (mState == PlaybackStateCompat.STATE_BUFFERING) {
mMediaPlayer.start();
- mState = PlaybackState.STATE_PLAYING;
+ mState = PlaybackStateCompat.STATE_PLAYING;
}
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
@@ -365,7 +345,7 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
*/
@Override
public void onCompletion(MediaPlayer player) {
- LogHelper.d(TAG, "onCompletion from MediaPlayer");
+ Log.d(TAG, "onCompletion from MediaPlayer");
// The media player finished playing the current song, so we go ahead
// and start the next.
if (mCallback != null) {
@@ -380,7 +360,7 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
*/
@Override
public void onPrepared(MediaPlayer player) {
- LogHelper.d(TAG, "onPrepared from MediaPlayer");
+ Log.d(TAG, "onPrepared from MediaPlayer");
// The media player is done preparing. That means we can start playing if we
// have audio focus.
configMediaPlayerState();
@@ -394,8 +374,8 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
* @see android.media.MediaPlayer.OnErrorListener
*/
@Override
- public boolean onError(MediaPlayer mp, int what, int extra) {
- LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
+ public boolean onError(MediaPlayer player, int what, int extra) {
+ Log.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
if (mCallback != null) {
mCallback.onError("MediaPlayer error " + what + " (" + extra + ")");
}
@@ -408,7 +388,7 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
* already exists.
*/
private void createMediaPlayerIfNeeded() {
- LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? ", (mMediaPlayer==null));
+ Log.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer == null));
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
@@ -434,10 +414,10 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
* "foreground service" status, the wake locks and possibly the MediaPlayer.
*
* @param releaseMediaPlayer Indicates whether the Media Player should also
- * be released or not
+ * be released or not.
*/
private void relaxResources(boolean releaseMediaPlayer) {
- LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=", releaseMediaPlayer);
+ Log.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer);
mService.stopForeground(true);
@@ -453,38 +433,4 @@ public class Playback implements AudioManager.OnAudioFocusChangeListener,
mWifiLock.release();
}
}
-
- private void registerAudioNoisyReceiver() {
- if (!mAudioNoisyReceiverRegistered) {
- mService.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter);
- mAudioNoisyReceiverRegistered = true;
- }
- }
-
- private void unregisterAudioNoisyReceiver() {
- if (mAudioNoisyReceiverRegistered) {
- mService.unregisterReceiver(mAudioNoisyReceiver);
- mAudioNoisyReceiverRegistered = false;
- }
- }
-
- interface Callback {
- /**
- * On current music completed.
- */
- void onCompletion();
- /**
- * on Playback status changed
- * Implementations can use this callback to update
- * playback state on the media sessions.
- */
- void onPlaybackStatusChanged(int state);
-
- /**
- * @param error to be added to the PlaybackState
- */
- void onError(String error);
-
- }
-
}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueAdapter.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueAdapter.java
deleted file mode 100644
index 4f24e994..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueAdapter.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.mediabrowserservice;
-
-import android.app.Activity;
-import android.media.session.MediaSession;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-/**
- * A list adapter for items in a queue
- */
-public class QueueAdapter extends ArrayAdapter<MediaSession.QueueItem> {
-
- // The currently selected/active queue item Id.
- private long mActiveQueueItemId = MediaSession.QueueItem.UNKNOWN_ID;
-
- public QueueAdapter(Activity context) {
- super(context, R.layout.media_list_item, new ArrayList<MediaSession.QueueItem>());
- }
-
- public void setActiveQueueItemId(long id) {
- this.mActiveQueueItemId = id;
- }
-
- private static class ViewHolder {
- ImageView mImageView;
- TextView mTitleView;
- TextView mDescriptionView;
- }
-
- public View getView(int position, View convertView, ViewGroup parent) {
- ViewHolder holder;
-
- if (convertView == null) {
- convertView = LayoutInflater.from(getContext())
- .inflate(R.layout.media_list_item, parent, false);
- holder = new ViewHolder();
- holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq);
- holder.mTitleView = (TextView) convertView.findViewById(R.id.title);
- holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description);
- convertView.setTag(holder);
- } else {
- holder = (ViewHolder) convertView.getTag();
- }
-
- MediaSession.QueueItem item = getItem(position);
- holder.mTitleView.setText(item.getDescription().getTitle());
- if (item.getDescription().getDescription() != null) {
- holder.mDescriptionView.setText(item.getDescription().getDescription());
- }
-
- // If the itemId matches the active Id then use a different icon
- if (mActiveQueueItemId == item.getQueueId()) {
- holder.mImageView.setImageDrawable(
- getContext().getDrawable(R.drawable.ic_equalizer_white_24dp));
- } else {
- holder.mImageView.setImageDrawable(
- getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp));
- }
- return convertView;
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueFragment.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueFragment.java
deleted file mode 100644
index 5fe59908..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/QueueFragment.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * 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.mediabrowserservice;
-
-import android.annotation.SuppressLint;
-import android.app.Fragment;
-import android.content.ComponentName;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ImageButton;
-import android.widget.ListView;
-
-import com.example.android.mediabrowserservice.utils.LogHelper;
-
-import java.util.List;
-
-/**
- * A class that shows the Media Queue to the user.
- */
-public class QueueFragment extends Fragment {
-
- private static final String TAG = LogHelper.makeLogTag(QueueFragment.class.getSimpleName());
-
- private ImageButton mSkipNext;
- private ImageButton mSkipPrevious;
- private ImageButton mPlayPause;
-
- private MediaBrowser mMediaBrowser;
- private MediaController.TransportControls mTransportControls;
- private MediaController mMediaController;
- private PlaybackState mPlaybackState;
-
- private QueueAdapter mQueueAdapter;
-
- private MediaBrowser.ConnectionCallback mConnectionCallback =
- new MediaBrowser.ConnectionCallback() {
- @Override
- public void onConnected() {
- LogHelper.d(TAG, "onConnected: session token ", mMediaBrowser.getSessionToken());
-
- mMediaController = new MediaController(getActivity(),
- mMediaBrowser.getSessionToken());
- mTransportControls = mMediaController.getTransportControls();
- mMediaController.registerCallback(mSessionCallback);
-
- getActivity().setMediaController(mMediaController);
- mPlaybackState = mMediaController.getPlaybackState();
-
- List<MediaSession.QueueItem> queue = mMediaController.getQueue();
- if (queue != null) {
- mQueueAdapter.clear();
- mQueueAdapter.notifyDataSetInvalidated();
- mQueueAdapter.addAll(queue);
- mQueueAdapter.notifyDataSetChanged();
- }
- onPlaybackStateChanged(mPlaybackState);
- }
-
- @Override
- public void onConnectionFailed() {
- LogHelper.d(TAG, "onConnectionFailed");
- }
-
- @Override
- public void onConnectionSuspended() {
- LogHelper.d(TAG, "onConnectionSuspended");
- mMediaController.unregisterCallback(mSessionCallback);
- mTransportControls = null;
- mMediaController = null;
- getActivity().setMediaController(null);
- }
- };
-
- // Receive callbacks from the MediaController. Here we update our state such as which queue
- // is being shown, the current title and description and the PlaybackState.
- private MediaController.Callback mSessionCallback = new MediaController.Callback() {
-
- @Override
- public void onSessionDestroyed() {
- LogHelper.d(TAG, "Session destroyed. Need to fetch a new Media Session");
- }
-
- @Override
- public void onPlaybackStateChanged(@NonNull PlaybackState state) {
- LogHelper.d(TAG, "Received playback state change to state ", state.getState());
- mPlaybackState = state;
- QueueFragment.this.onPlaybackStateChanged(state);
- }
-
- @Override
- public void onQueueChanged(List<MediaSession.QueueItem> queue) {
- LogHelper.d(TAG, "onQueueChanged ", queue);
- if (queue != null) {
- mQueueAdapter.clear();
- mQueueAdapter.notifyDataSetInvalidated();
- mQueueAdapter.addAll(queue);
- mQueueAdapter.notifyDataSetChanged();
- }
- }
- };
-
- public static QueueFragment newInstance() {
- return new QueueFragment();
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_list, container, false);
-
- mSkipPrevious = (ImageButton) rootView.findViewById(R.id.skip_previous);
- mSkipPrevious.setEnabled(false);
- mSkipPrevious.setOnClickListener(mButtonListener);
-
- mSkipNext = (ImageButton) rootView.findViewById(R.id.skip_next);
- mSkipNext.setEnabled(false);
- mSkipNext.setOnClickListener(mButtonListener);
-
- mPlayPause = (ImageButton) rootView.findViewById(R.id.play_pause);
- mPlayPause.setEnabled(true);
- mPlayPause.setOnClickListener(mButtonListener);
-
- mQueueAdapter = new QueueAdapter(getActivity());
-
- ListView mListView = (ListView) rootView.findViewById(R.id.list_view);
- mListView.setAdapter(mQueueAdapter);
- mListView.setFocusable(true);
- mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- MediaSession.QueueItem item = mQueueAdapter.getItem(position);
- mTransportControls.skipToQueueItem(item.getQueueId());
- }
- });
-
- mMediaBrowser = new MediaBrowser(getActivity(),
- new ComponentName(getActivity(), MusicService.class),
- mConnectionCallback, null);
-
- return rootView;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (mMediaBrowser != null) {
- mMediaBrowser.connect();
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (mMediaController != null) {
- mMediaController.unregisterCallback(mSessionCallback);
- }
- if (mMediaBrowser != null) {
- mMediaBrowser.disconnect();
- }
- }
-
- @SuppressLint("SwitchIntDef")
- private void onPlaybackStateChanged(PlaybackState state) {
- LogHelper.d(TAG, "onPlaybackStateChanged ", state);
- if (state == null) {
- return;
- }
- mQueueAdapter.setActiveQueueItemId(state.getActiveQueueItemId());
- mQueueAdapter.notifyDataSetChanged();
- boolean enablePlay = false;
- StringBuilder statusBuilder = new StringBuilder();
- switch (state.getState()) {
- case PlaybackState.STATE_PLAYING:
- statusBuilder.append("playing");
- enablePlay = false;
- break;
- case PlaybackState.STATE_PAUSED:
- statusBuilder.append("paused");
- enablePlay = true;
- break;
- case PlaybackState.STATE_STOPPED:
- statusBuilder.append("ended");
- enablePlay = true;
- break;
- case PlaybackState.STATE_ERROR:
- statusBuilder.append("error: ").append(state.getErrorMessage());
- break;
- case PlaybackState.STATE_BUFFERING:
- statusBuilder.append("buffering");
- break;
- case PlaybackState.STATE_NONE:
- statusBuilder.append("none");
- enablePlay = false;
- break;
- case PlaybackState.STATE_CONNECTING:
- statusBuilder.append("connecting");
- break;
- default:
- statusBuilder.append(mPlaybackState);
- }
- statusBuilder.append(" -- At position: ").append(state.getPosition());
- LogHelper.d(TAG, statusBuilder.toString());
-
- if (enablePlay) {
- mPlayPause.setImageDrawable(
- getActivity().getDrawable(R.drawable.ic_play_arrow_white_24dp));
- } else {
- mPlayPause.setImageDrawable(getActivity().getDrawable(R.drawable.ic_pause_white_24dp));
- }
-
- mSkipPrevious.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
- mSkipNext.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
-
- LogHelper.d(TAG, "Queue From MediaController *** Title " +
- mMediaController.getQueueTitle() + "\n: Queue: " + mMediaController.getQueue() +
- "\n Metadata " + mMediaController.getMetadata());
- }
-
- private View.OnClickListener mButtonListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final int state = mPlaybackState == null ?
- PlaybackState.STATE_NONE : mPlaybackState.getState();
- switch (v.getId()) {
- case R.id.play_pause:
- LogHelper.d(TAG, "Play button pressed, in state " + state);
- if (state == PlaybackState.STATE_PAUSED ||
- state == PlaybackState.STATE_STOPPED ||
- state == PlaybackState.STATE_NONE) {
- playMedia();
- } else if (state == PlaybackState.STATE_PLAYING) {
- pauseMedia();
- }
- break;
- case R.id.skip_previous:
- LogHelper.d(TAG, "Start button pressed, in state " + state);
- skipToPrevious();
- break;
- case R.id.skip_next:
- skipToNext();
- break;
- }
- }
- };
-
- private void playMedia() {
- if (mTransportControls != null) {
- mTransportControls.play();
- }
- }
-
- private void pauseMedia() {
- if (mTransportControls != null) {
- mTransportControls.pause();
- }
- }
-
- private void skipToPrevious() {
- if (mTransportControls != null) {
- mTransportControls.skipToPrevious();
- }
- }
-
- private void skipToNext() {
- if (mTransportControls != null) {
- mTransportControls.skipToNext();
- }
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
index 949b0bdc..7eead436 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
@@ -16,11 +16,9 @@
package com.example.android.mediabrowserservice.model;
-import android.media.MediaMetadata;
import android.os.AsyncTask;
import android.support.v4.media.MediaMetadataCompat;
-
-import com.example.android.mediabrowserservice.utils.LogHelper;
+import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
@@ -33,26 +31,26 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
+import java.util.LinkedHashMap;
/**
* Utility class to get a list of MusicTrack's based on a server-side JSON
* configuration.
+ *
+ * In a real application this class may pull data from a remote server, as we do here,
+ * or potentially use {@link android.provider.MediaStore} to locate media files located on
+ * the device.
*/
public class MusicProvider {
- private static final String TAG = LogHelper.makeLogTag(MusicProvider.class);
+ private static final String TAG = MusicProvider.class.getSimpleName();
- private static final String CATALOG_URL =
- "http://storage.googleapis.com/automotive-media/music.json";
+ public static final String MEDIA_ID_ROOT = "__ROOT__";
+ public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY__";
- public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
+ private static final String CATALOG_URL =
+ "https://storage.googleapis.com/automotive-media/music.json";
private static final String JSON_MUSIC = "music";
private static final String JSON_TITLE = "title";
@@ -66,108 +64,54 @@ public class MusicProvider {
private static final String JSON_DURATION = "duration";
// Categorized caches for music track data:
- private ConcurrentMap<String, List<MediaMetadata>> mMusicListByGenre;
- private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
-
- private final Set<String> mFavoriteTracks;
+ private final LinkedHashMap<String, MediaMetadataCompat> mMusicListById;
- enum State {
+ private enum State {
NON_INITIALIZED, INITIALIZING, INITIALIZED
}
private volatile State mCurrentState = State.NON_INITIALIZED;
+ /**
+ * Callback used by MusicService.
+ */
public interface Callback {
void onMusicCatalogReady(boolean success);
}
public MusicProvider() {
- mMusicListByGenre = new ConcurrentHashMap<>();
- mMusicListById = new ConcurrentHashMap<>();
- mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
+ mMusicListById = new LinkedHashMap<>();
}
- /**
- * Get an iterator over the list of genres
- *
- * @return genres
- */
- public Iterable<String> getGenres() {
- if (mCurrentState != State.INITIALIZED) {
+ public Iterable<MediaMetadataCompat> getAllMusics() {
+ if (mCurrentState != State.INITIALIZED || mMusicListById.isEmpty()) {
return Collections.emptyList();
}
- return mMusicListByGenre.keySet();
+ return mMusicListById.values();
}
/**
- * Get music tracks of the given genre
- *
- */
- public Iterable<MediaMetadata> getMusicsByGenre(String genre) {
- if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
- return Collections.emptyList();
- }
- return mMusicListByGenre.get(genre);
- }
-
- /**
- * Very basic implementation of a search that filter music tracks which title containing
- * the given query.
+ * Return the MediaMetadata for the given musicID.
*
+ * @param musicId The unique music ID.
*/
- public Iterable<MediaMetadata> searchMusic(String titleQuery) {
- if (mCurrentState != State.INITIALIZED) {
- return Collections.emptyList();
- }
- ArrayList<MediaMetadata> result = new ArrayList<>();
- titleQuery = titleQuery.toLowerCase(Locale.US);
- for (MutableMediaMetadata track : mMusicListById.values()) {
- if (track.metadata.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase(Locale.US)
- .contains(titleQuery)) {
- result.add(track.metadata);
- }
- }
- return result;
+ public MediaMetadataCompat getMusic(String musicId) {
+ return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null;
}
/**
- * Return the MediaMetadata for the given musicID.
- *
- * @param musicId The unique, non-hierarchical music ID.
+ * Update the metadata associated with a musicId. If the musicId doesn't exist, the
+ * update is dropped. (That is, it does not create a new mediaId.)
+ * @param musicId The ID
+ * @param metadata New Metadata to associate with it
*/
- public MediaMetadata getMusic(String musicId) {
- return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId).metadata : null;
- }
-
- public synchronized void updateMusic(String musicId, MediaMetadata metadata) {
- MutableMediaMetadata track = mMusicListById.get(musicId);
- if (track == null) {
- return;
- }
-
- String oldGenre = track.metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
- String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
-
- track.metadata = metadata;
-
- // if genre has changed, we need to rebuild the list by genre
- if (!oldGenre.equals(newGenre)) {
- buildListsByGenre();
+ public synchronized void updateMusic(String musicId, MediaMetadataCompat metadata) {
+ MediaMetadataCompat track = mMusicListById.get(musicId);
+ if (track != null) {
+ mMusicListById.put(musicId, metadata);
}
}
- public void setFavorite(String musicId, boolean favorite) {
- if (favorite) {
- mFavoriteTracks.add(musicId);
- } else {
- mFavoriteTracks.remove(musicId);
- }
- }
-
- public boolean isFavorite(String musicId) {
- return mFavoriteTracks.contains(musicId);
- }
-
public boolean isInitialized() {
return mCurrentState == State.INITIALIZED;
}
@@ -177,9 +121,9 @@ public class MusicProvider {
* for future reference, keying tracks by musicId and grouping by genre.
*/
public void retrieveMediaAsync(final Callback callback) {
- LogHelper.d(TAG, "retrieveMediaAsync called");
+ Log.d(TAG, "retrieveMediaAsync called");
if (mCurrentState == State.INITIALIZED) {
- // Nothing to do, execute callback immediately
+ // Already initialized, so call back immediately.
callback.onMusicCatalogReady(true);
return;
}
@@ -201,21 +145,6 @@ public class MusicProvider {
}.execute();
}
- private synchronized void buildListsByGenre() {
- ConcurrentMap<String, List<MediaMetadata>> newMusicListByGenre = new ConcurrentHashMap<>();
-
- for (MutableMediaMetadata m : mMusicListById.values()) {
- String genre = m.metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
- List<MediaMetadata> list = newMusicListByGenre.get(genre);
- if (list == null) {
- list = new ArrayList<>();
- newMusicListByGenre.put(genre, list);
- }
- list.add(m.metadata);
- }
- mMusicListByGenre = newMusicListByGenre;
- }
-
private synchronized void retrieveMedia() {
try {
if (mCurrentState == State.NON_INITIALIZED) {
@@ -229,17 +158,16 @@ public class MusicProvider {
}
JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
if (tracks != null) {
- for (int j = 0; j < tracks.length(); j++) {
- MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path);
- String musicId = item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
- mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item));
+ for (int j = tracks.length() - 1; j >= 0; j--) {
+ MediaMetadataCompat item = buildFromJSON(tracks.getJSONObject(j), path);
+ String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
+ mMusicListById.put(musicId, item);
}
- buildListsByGenre();
}
mCurrentState = State.INITIALIZED;
}
- } catch (JSONException e) {
- LogHelper.e(TAG, e, "Could not retrieve music list");
+ } catch (JSONException jsonException) {
+ Log.e(TAG, "Could not retrieve music list", jsonException);
} finally {
if (mCurrentState != State.INITIALIZED) {
// Something bad happened, so we reset state to NON_INITIALIZED to allow
@@ -249,7 +177,9 @@ public class MusicProvider {
}
}
- private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException {
+ private MediaMetadataCompat buildFromJSON(JSONObject json, String basePath)
+ throws JSONException {
+
String title = json.getString(JSON_TITLE);
String album = json.getString(JSON_ALBUM);
String artist = json.getString(JSON_ARTIST);
@@ -260,13 +190,13 @@ public class MusicProvider {
int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
int duration = json.getInt(JSON_DURATION) * 1000; // ms
- LogHelper.d(TAG, "Found music track: ", json);
+ Log.d(TAG, "Found music track: " + json);
// Media is stored relative to JSON file
- if (!source.startsWith("http")) {
+ if (!source.startsWith("https")) {
source = basePath + source;
}
- if (!iconUrl.startsWith("http")) {
+ if (!iconUrl.startsWith("https")) {
iconUrl = basePath + iconUrl;
}
// Since we don't have a unique ID in the server, we fake one using the hashcode of
@@ -277,18 +207,17 @@ public class MusicProvider {
// mediaSession.setMetadata) is not a good idea for a real world music app, because
// the session metadata can be accessed by notification listeners. This is done in this
// sample for convenience only.
- //noinspection WrongConstant
- return new MediaMetadata.Builder()
- .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id)
- .putString(CUSTOM_METADATA_TRACK_SOURCE, source)
- .putString(MediaMetadata.METADATA_KEY_ALBUM, album)
- .putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
- .putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
- .putString(MediaMetadata.METADATA_KEY_GENRE, genre)
- .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl)
- .putString(MediaMetadata.METADATA_KEY_TITLE, title)
- .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber)
- .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount)
+ return new MediaMetadataCompat.Builder()
+ .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
+ .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, source)
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
+ .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl)
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
+ .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber)
+ .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount)
.build();
}
@@ -299,28 +228,29 @@ public class MusicProvider {
* @return result JSONObject containing the parsed representation.
*/
private JSONObject fetchJSONFromUrl(String urlString) {
- InputStream is = null;
+ InputStream inputStream = null;
try {
URL url = new URL(urlString);
URLConnection urlConnection = url.openConnection();
- is = new BufferedInputStream(urlConnection.getInputStream());
+ inputStream = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(
urlConnection.getInputStream(), "iso-8859-1"));
- StringBuilder sb = new StringBuilder();
+ StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
- sb.append(line);
+ stringBuilder.append(line);
}
- return new JSONObject(sb.toString());
- } catch (Exception e) {
- LogHelper.e(TAG, "Failed to parse the json for media list", e);
+ return new JSONObject(stringBuilder.toString());
+ } catch (IOException | JSONException exception) {
+ Log.e(TAG, "Failed to parse the json for media list", exception);
return null;
} finally {
- if (is != null) {
+ // If the inputStream was opened, try to close it now.
+ if (inputStream != null) {
try {
- is.close();
- } catch (IOException e) {
- // ignore
+ inputStream.close();
+ } catch (IOException ignored) {
+ // Ignore the exception since there's nothing left to do with the stream
}
}
}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java
deleted file mode 100644
index 1ee9d612..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.mediabrowserservice.model;
-
-import android.media.MediaMetadata;
-import android.text.TextUtils;
-
-/**
- * Holder class that encapsulates a MediaMetadata and allows the actual metadata to be modified
- * without requiring to rebuild the collections the metadata is in.
- */
-public class MutableMediaMetadata {
-
- public MediaMetadata metadata;
- public final String trackId;
-
- public MutableMediaMetadata(String trackId, MediaMetadata metadata) {
- this.metadata = metadata;
- this.trackId = trackId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || o.getClass() != MutableMediaMetadata.class) {
- return false;
- }
-
- MutableMediaMetadata that = (MutableMediaMetadata) o;
-
- return TextUtils.equals(trackId, that.trackId);
- }
-
- @Override
- public int hashCode() {
- return trackId.hashCode();
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java
deleted file mode 100644
index 7325130e..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.mediabrowserservice.utils;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-public class BitmapHelper {
- private static final String TAG = LogHelper.makeLogTag(BitmapHelper.class);
-
- // Max read limit that we allow our input stream to mark/reset.
- private static final int MAX_READ_LIMIT_PER_IMG = 1024 * 1024;
-
- public static Bitmap scaleBitmap(Bitmap src, int maxWidth, int maxHeight) {
- double scaleFactor = Math.min(
- ((double) maxWidth)/src.getWidth(), ((double) maxHeight)/src.getHeight());
- return Bitmap.createScaledBitmap(src,
- (int) (src.getWidth() * scaleFactor), (int) (src.getHeight() * scaleFactor), false);
- }
-
- public static Bitmap scaleBitmap(int scaleFactor, InputStream is) {
- // Get the dimensions of the bitmap
- BitmapFactory.Options bmOptions = new BitmapFactory.Options();
-
- // Decode the image file into a Bitmap sized to fill the View
- bmOptions.inJustDecodeBounds = false;
- bmOptions.inSampleSize = scaleFactor;
-
- return BitmapFactory.decodeStream(is, null, bmOptions);
- }
-
- public static int findScaleFactor(int targetW, int targetH, InputStream is) {
- // Get the dimensions of the bitmap
- BitmapFactory.Options bmOptions = new BitmapFactory.Options();
- bmOptions.inJustDecodeBounds = true;
- BitmapFactory.decodeStream(is, null, bmOptions);
- int actualW = bmOptions.outWidth;
- int actualH = bmOptions.outHeight;
-
- // Determine how much to scale down the image
- return Math.min(actualW/targetW, actualH/targetH);
- }
-
- @SuppressWarnings("SameParameterValue")
- public static Bitmap fetchAndRescaleBitmap(String uri, int width, int height)
- throws IOException {
- URL url = new URL(uri);
- BufferedInputStream is = null;
- try {
- HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
- is = new BufferedInputStream(urlConnection.getInputStream());
- is.mark(MAX_READ_LIMIT_PER_IMG);
- int scaleFactor = findScaleFactor(width, height, is);
- LogHelper.d(TAG, "Scaling bitmap ", uri, " by factor ", scaleFactor, " to support ",
- width, "x", height, "requested dimension");
- is.reset();
- return scaleBitmap(scaleFactor, is);
- } finally {
- if (is != null) {
- is.close();
- }
- }
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java
deleted file mode 100644
index 74861ba3..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.mediabrowserservice.utils;
-
-import android.os.Bundle;
-
-public class CarHelper {
- private static final String AUTO_APP_PACKAGE_NAME = "com.google.android.projection.gearhead";
-
- // Use these extras to reserve space for the corresponding actions, even when they are disabled
- // in the playbackstate, so the custom actions don't reflow.
- private static final String SLOT_RESERVATION_SKIP_TO_NEXT =
- "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
- private static final String SLOT_RESERVATION_SKIP_TO_PREV =
- "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
- private static final String SLOT_RESERVATION_QUEUE =
- "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
-
-
- public static boolean isValidCarPackage(String packageName) {
- return AUTO_APP_PACKAGE_NAME.equals(packageName);
- }
-
- public static void setSlotReservationFlags(Bundle extras, boolean reservePlayingQueueSlot,
- boolean reserveSkipToNextSlot, boolean reserveSkipToPrevSlot) {
- if (reservePlayingQueueSlot) {
- extras.putBoolean(SLOT_RESERVATION_QUEUE, true);
- } else {
- extras.remove(SLOT_RESERVATION_QUEUE);
- }
- if (reserveSkipToPrevSlot) {
- extras.putBoolean(SLOT_RESERVATION_SKIP_TO_PREV, true);
- } else {
- extras.remove(SLOT_RESERVATION_SKIP_TO_PREV);
- }
- if (reserveSkipToNextSlot) {
- extras.putBoolean(SLOT_RESERVATION_SKIP_TO_NEXT, true);
- } else {
- extras.remove(SLOT_RESERVATION_SKIP_TO_NEXT);
- }
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java
deleted file mode 100644
index 09d14d26..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.mediabrowserservice.utils;
-
-import android.util.Log;
-
-import com.example.android.mediabrowserservice.BuildConfig;
-
-public class LogHelper {
-
- private static final String LOG_PREFIX = "sample_";
- private static final int LOG_PREFIX_LENGTH = LOG_PREFIX.length();
- private static final int MAX_LOG_TAG_LENGTH = 23;
-
- public static String makeLogTag(String str) {
- if (str.length() > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) {
- return LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1);
- }
-
- return LOG_PREFIX + str;
- }
-
- /**
- * Don't use this when obfuscating class names!
- */
- public static String makeLogTag(Class cls) {
- return makeLogTag(cls.getSimpleName());
- }
-
-
- public static void v(String tag, Object... messages) {
- // Only log VERBOSE if build type is DEBUG
- if (BuildConfig.DEBUG) {
- log(tag, Log.VERBOSE, null, messages);
- }
- }
-
- public static void d(String tag, Object... messages) {
- // Only log DEBUG if build type is DEBUG
- if (BuildConfig.DEBUG) {
- log(tag, Log.DEBUG, null, messages);
- }
- }
-
- public static void i(String tag, Object... messages) {
- log(tag, Log.INFO, null, messages);
- }
-
- public static void w(String tag, Object... messages) {
- log(tag, Log.WARN, null, messages);
- }
-
- public static void w(String tag, Throwable t, Object... messages) {
- log(tag, Log.WARN, t, messages);
- }
-
- public static void e(String tag, Object... messages) {
- log(tag, Log.ERROR, null, messages);
- }
-
- public static void e(String tag, Throwable t, Object... messages) {
- log(tag, Log.ERROR, t, messages);
- }
-
- public static void log(String tag, int level, Throwable t, Object... messages) {
- if (Log.isLoggable(tag, level)) {
- String message;
- if (t == null && messages != null && messages.length == 1) {
- // handle this common case without the extra cost of creating a stringbuffer:
- message = messages[0].toString();
- } else {
- StringBuilder sb = new StringBuilder();
- if (messages != null) for (Object m : messages) {
- sb.append(m);
- }
- if (t != null) {
- sb.append("\n").append(Log.getStackTraceString(t));
- }
- message = sb.toString();
- }
- Log.println(level, tag, message);
- }
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java
deleted file mode 100644
index 604cf8ac..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.mediabrowserservice.utils;
-
-import java.util.Arrays;
-
-/**
- * Utility class to help on queue related tasks.
- */
-public class MediaIDHelper {
-
- private static final String TAG = LogHelper.makeLogTag(MediaIDHelper.class);
-
- // Media IDs used on browseable items of MediaBrowser
- public static final String MEDIA_ID_ROOT = "__ROOT__";
- public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__";
- public static final String MEDIA_ID_MUSICS_BY_SEARCH = "__BY_SEARCH__";
-
- private static final char CATEGORY_SEPARATOR = '/';
- private static final char LEAF_SEPARATOR = '|';
-
- public static String createMediaID(String musicID, String... categories) {
- // MediaIDs are of the form <categoryType>/<categoryValue>|<musicUniqueId>, to make it easy
- // to find the category (like genre) that a music was selected from, so we
- // can correctly build the playing queue. This is specially useful when
- // one music can appear in more than one list, like "by genre -> genre_1"
- // and "by artist -> artist_1".
- StringBuilder sb = new StringBuilder();
- if (categories != null && categories.length > 0) {
- sb.append(categories[0]);
- for (int i=1; i < categories.length; i++) {
- sb.append(CATEGORY_SEPARATOR).append(categories[i]);
- }
- }
- if (musicID != null) {
- sb.append(LEAF_SEPARATOR).append(musicID);
- }
- return sb.toString();
- }
-
- public static String createBrowseCategoryMediaID(String categoryType, String categoryValue) {
- return categoryType + CATEGORY_SEPARATOR + categoryValue;
- }
-
- /**
- * Extracts unique musicID from the mediaID. mediaID is, by this sample's convention, a
- * concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and unique
- * musicID. This is necessary so we know where the user selected the music from, when the music
- * exists in more than one music list, and thus we are able to correctly build the playing queue.
- *
- * @param mediaID that contains the musicID
- * @return musicID
- */
- public static String extractMusicIDFromMediaID(String mediaID) {
- int pos = mediaID.indexOf(LEAF_SEPARATOR);
- if (pos >= 0) {
- return mediaID.substring(pos+1);
- }
- return null;
- }
-
- /**
- * Extracts category and categoryValue from the mediaID. mediaID is, by this sample's
- * convention, a concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and
- * mediaID. This is necessary so we know where the user selected the music from, when the music
- * exists in more than one music list, and thus we are able to correctly build the playing queue.
- *
- * @param mediaID that contains a category and categoryValue.
- */
- public static String[] getHierarchy(String mediaID) {
- int pos = mediaID.indexOf(LEAF_SEPARATOR);
- if (pos >= 0) {
- mediaID = mediaID.substring(0, pos);
- }
- return mediaID.split(String.valueOf(CATEGORY_SEPARATOR));
- }
-
- public static String extractBrowseCategoryValueFromMediaID(String mediaID) {
- String[] hierarchy = getHierarchy(mediaID);
- if (hierarchy != null && hierarchy.length == 2) {
- return hierarchy[1];
- }
- return null;
- }
-
- private static boolean isBrowseable(String mediaID) {
- return mediaID.indexOf(LEAF_SEPARATOR) < 0;
- }
-
- public static String getParentMediaID(String mediaID) {
- String[] hierarchy = getHierarchy(mediaID);
- if (!isBrowseable(mediaID)) {
- return createMediaID(null, hierarchy);
- }
- if (hierarchy == null || hierarchy.length <= 1) {
- return MEDIA_ID_ROOT;
- }
- String[] parentHierarchy = Arrays.copyOf(hierarchy, hierarchy.length-1);
- return createMediaID(null, parentHierarchy);
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java
deleted file mode 100644
index 9a2caa8a..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.mediabrowserservice.utils;
-
-import android.media.MediaMetadata;
-import android.media.session.MediaSession;
-
-import com.example.android.mediabrowserservice.model.MusicProvider;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
-import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_SEARCH;
-
-/**
- * Utility class to help on queue related tasks.
- */
-public class QueueHelper {
-
- private static final String TAG = LogHelper.makeLogTag(QueueHelper.class);
-
- public static List<MediaSession.QueueItem> getPlayingQueue(String mediaId,
- MusicProvider musicProvider) {
-
- // extract the browsing hierarchy from the media ID:
- String[] hierarchy = MediaIDHelper.getHierarchy(mediaId);
-
- if (hierarchy.length != 2) {
- LogHelper.e(TAG, "Could not build a playing queue for this mediaId: ", mediaId);
- return null;
- }
-
- String categoryType = hierarchy[0];
- String categoryValue = hierarchy[1];
- LogHelper.d(TAG, "Creating playing queue for ", categoryType, ", ", categoryValue);
-
- Iterable<MediaMetadata> tracks = null;
- // This sample only supports genre and by_search category types.
- if (categoryType.equals(MEDIA_ID_MUSICS_BY_GENRE)) {
- tracks = musicProvider.getMusicsByGenre(categoryValue);
- } else if (categoryType.equals(MEDIA_ID_MUSICS_BY_SEARCH)) {
- tracks = musicProvider.searchMusic(categoryValue);
- }
-
- if (tracks == null) {
- LogHelper.e(TAG, "Unrecognized category type: ", categoryType, " for mediaId ", mediaId);
- return null;
- }
-
- return convertToQueue(tracks, hierarchy[0], hierarchy[1]);
- }
-
- public static List<MediaSession.QueueItem> getPlayingQueueFromSearch(String query,
- MusicProvider musicProvider) {
-
- LogHelper.d(TAG, "Creating playing queue for musics from search ", query);
-
- return convertToQueue(musicProvider.searchMusic(query), MEDIA_ID_MUSICS_BY_SEARCH, query);
- }
-
-
- public static int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
- String mediaId) {
- int index = 0;
- for (MediaSession.QueueItem item : queue) {
- if (mediaId.equals(item.getDescription().getMediaId())) {
- return index;
- }
- index++;
- }
- return -1;
- }
-
- public static int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
- long queueId) {
- int index = 0;
- for (MediaSession.QueueItem item : queue) {
- if (queueId == item.getQueueId()) {
- return index;
- }
- index++;
- }
- return -1;
- }
-
- private static List<MediaSession.QueueItem> convertToQueue(
- Iterable<MediaMetadata> tracks, String... categories) {
- List<MediaSession.QueueItem> queue = new ArrayList<>();
- int count = 0;
- for (MediaMetadata track : tracks) {
-
- // We create a hierarchy-aware mediaID, so we know what the queue is about by looking
- // at the QueueItem media IDs.
- String hierarchyAwareMediaID = MediaIDHelper.createMediaID(
- track.getDescription().getMediaId(), categories);
-
- MediaMetadata trackCopy = new MediaMetadata.Builder(track)
- .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
- .build();
-
- // We don't expect queues to change after created, so we use the item index as the
- // queueId. Any other number unique in the queue would work.
- MediaSession.QueueItem item = new MediaSession.QueueItem(
- trackCopy.getDescription(), count++);
- queue.add(item);
- }
- return queue;
-
- }
-
- /**
- * Create a random queue. For simplicity sake, instead of a random queue, we create a
- * queue using the first genre.
- *
- * @param musicProvider the provider used for fetching music.
- * @return list containing {@link android.media.session.MediaSession.QueueItem}'s
- */
- public static List<MediaSession.QueueItem> getRandomQueue(MusicProvider musicProvider) {
- Iterator<String> genres = musicProvider.getGenres().iterator();
- if (!genres.hasNext()) {
- return Collections.emptyList();
- }
- String genre = genres.next();
- Iterable<MediaMetadata> tracks = musicProvider.getMusicsByGenre(genre);
-
- return convertToQueue(tracks, MEDIA_ID_MUSICS_BY_GENRE, genre);
- }
-
- public static boolean isIndexPlayable(int index, List<MediaSession.QueueItem> queue) {
- return (queue != null && index >= 0 && index < queue.size());
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java
deleted file mode 100644
index ed4dcd00..00000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-* 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.mediabrowserservice.utils;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-
-/**
- * Generic reusable methods to handle resources.
- */
-public class ResourceHelper {
- /**
- * Get a color value from a theme attribute.
- * @param context used for getting the color.
- * @param attribute theme attribute.
- * @param defaultColor default to use.
- * @return color value
- */
- public static int getThemeColor(Context context, int attribute, int defaultColor) {
- int themeColor = 0;
- String packageName = context.getPackageName();
- try {
- Context packageContext = context.createPackageContext(packageName, 0);
- ApplicationInfo applicationInfo =
- context.getPackageManager().getApplicationInfo(packageName, 0);
- packageContext.setTheme(applicationInfo.theme);
- Resources.Theme theme = packageContext.getTheme();
- TypedArray ta = theme.obtainStyledAttributes(new int[] {attribute});
- themeColor = ta.getColor(0, defaultColor);
- ta.recycle();
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
- return themeColor;
- }
-}
diff --git a/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml b/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml
index 21cdbbd9..64b47eea 100644
--- a/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml
+++ b/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml
@@ -19,4 +19,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MusicPlayerActivity"
- tools:ignore="MergeRootFrame" />
+ tools:ignore="MergeRootFrame"/>
diff --git a/media/MediaBrowserService/Application/src/main/res/layout/fragment_list.xml b/media/MediaBrowserService/Application/src/main/res/layout/fragment_list.xml
index c169fec9..8c5985bc 100644
--- a/media/MediaBrowserService/Application/src/main/res/layout/fragment_list.xml
+++ b/media/MediaBrowserService/Application/src/main/res/layout/fragment_list.xml
@@ -14,43 +14,12 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="?android:colorBackground"
+ android:orientation="vertical"
android:padding="@dimen/fragment_list_padding">
- <LinearLayout
- android:id="@+id/controls"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <ImageButton
- android:id="@+id/skip_previous"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:src="@drawable/ic_skip_previous_white_24dp"
- android:contentDescription="@string/skip_previous"/>
-
- <ImageButton
- android:id="@+id/play_pause"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:src="@drawable/ic_play_arrow_white_24dp"
- android:contentDescription="@string/play_pause"/>
-
- <ImageButton
- android:id="@+id/skip_next"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:src="@drawable/ic_skip_next_white_24dp"
- android:contentDescription="@string/skip_next"/>
-
- </LinearLayout>
-
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
diff --git a/media/MediaBrowserService/Application/src/main/res/layout/media_list_item.xml b/media/MediaBrowserService/Application/src/main/res/layout/media_list_item.xml
index 72c0ccf3..c10ac51a 100644
--- a/media/MediaBrowserService/Application/src/main/res/layout/media_list_item.xml
+++ b/media/MediaBrowserService/Application/src/main/res/layout/media_list_item.xml
@@ -31,8 +31,8 @@
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:mode="twoLine"
- android:padding="@dimen/list_item_padding"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:padding="@dimen/list_item_padding">
<TextView
android:id="@+id/title"
diff --git a/media/MediaBrowserService/Application/src/main/res/values/styles.xml b/media/MediaBrowserService/Application/src/main/res/values/styles.xml
index 35a3e7a8..5d91ab4d 100644
--- a/media/MediaBrowserService/Application/src/main/res/values/styles.xml
+++ b/media/MediaBrowserService/Application/src/main/res/values/styles.xml
@@ -15,10 +15,14 @@
limitations under the License.
-->
<resources>
- <style name="AppTheme" parent="android:Theme.Material">
- <item name="android:colorPrimary">#ffff5722</item>
- <item name="android:colorPrimaryDark">#ffbf360c</item>
- <item name="android:colorAccent">#ffff5722</item>
+ <color name="colorPrimary">#ffff5722</color>
+ <color name="colorPrimaryDark">#ffbf360c</color>
+ <color name="colorAccent">#ffff5722</color>
+
+ <style name="AppTheme" parent="Theme.AppCompat">
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
</style>
<style name="CarTheme" parent="AppTheme">
@@ -28,14 +32,14 @@
- Notification icon badge tinting
- Overview “now playing” icon tinting
-->
- <item name="android:colorPrimaryDark">#ffbf360c</item>
+ <item name="colorPrimaryDark">#ffbf360c</item>
<!-- colorAccent is used in Android Auto for:
- Spinner
- progress bar
- floating action button background (Play/Pause in media apps)
-->
- <item name="android:colorAccent">#ffff5722</item>
+ <item name="colorAccent">#ffff5722</item>
</style>
</resources> \ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml b/media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml
deleted file mode 100644
index 5c326cf2..00000000
--- a/media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml
+++ /dev/null
@@ -1,174 +0,0 @@
-<?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.
--->
-<allowed_callers>
- <signing_certificate name="Android Auto" release="false"
- package="com.google.android.projection.gearhead">
- MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
- VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
- VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
- AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
- Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
- MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
- A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
- ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
- hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
- 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
- xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
- W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
- 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
- cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
- HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
- xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
- CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
- QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
- CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
- EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
- zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
- XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
- IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
- ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
- Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
- </signing_certificate>
- <signing_certificate name="Android Auto" release="false"
- package="com.google.android.projection.gearhead">
- MIIDvTCCAqWgAwIBAgIJAOfkBvDXw5bzMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
- BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
- aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
- A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwMjUxWhcNNDExMDEyMjMwMjUxWjB1
- MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
- bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
- aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
- CgKCAQEAou7wwBKFyznqpRretJ3EVp55/Yr049Ag5wlGvrCnjIP8DrMrU+skfKe1
- DmwpsLNtnhhiNH+J000Lok3hc8jdWKeKOopzKGDNvL/HvnS70Zyk26gj9jtMMHz9
- 2aZdpmwD67FNmTlG2FERr+TwMD5agaPnsFR2zla6ugUvHGzz65YDxpCZsQ/TowyD
- LnxgMagvhvS+Oex3yh2FN7pJfwS03KdGdkWPbLqf9Fem09s5jjeZW/O3RgnKoRPI
- J4QLK70efjAZqJyBGcDZyQMwOs+8HIknraf8+cRZJDzqOx7rttl8M3KGB2EFljTp
- 6/FyxJLnAo6QlXn7GrYalTI0yLU9dQIDAQABo1AwTjAdBgNVHQ4EFgQU9QPJ5xJE
- DA8MDQMrj0hm2/A2BRkwHwYDVR0jBBgwFoAU9QPJ5xJEDA8MDQMrj0hm2/A2BRkw
- DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEADcr5h1FR8IpmN4hSsUA9
- SnCQVyXa1GQhzpQgRbF+npkgOn2Mebp8bd28VpfgooD2OBNQXCUcZkn7pWj++ut9
- HhObHVaV5FNg0pdDqLna9QZ9Y4oS+ZrijK70XZ/EjlYUHvhu0pIjZAbD8CmCFlow
- SR55qCSjM5iS37LZB32SMr1BBiYrNAvncKjYQVK8ctTRzhpNQQPBgXBA98Xl+d1D
- Py00JWQuF0ssmhKcJuvfdEnFF7Hvaxz/gCQ9nzarQI3CJB8dOXVwF8mcyDRBz4JR
- +YDpXo6BD+fGt15ov+zmqC8xaT9P1/JgoDXiMhy/6rwgdi9WxPf8mb7TnBC+CksX
- 0A==
- </signing_certificate>
- <signing_certificate name="Android Auto" release="true"
- package="com.google.android.projection.gearhead">
- MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
- BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
- aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
- A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
- MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
- bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
- aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
- CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
- Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
- 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
- oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
- k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
- 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
- 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
- DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
- wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
- 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
- QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
- 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
- yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
- IQ==
- </signing_certificate>
- <signing_certificate name="Media Browser Service Simulator" release="true"
- package="com.google.android.mediasimulator">
- MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
- BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
- aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
- A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
- MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
- bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
- aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
- CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
- Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
- 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
- oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
- k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
- 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
- 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
- DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
- wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
- 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
- QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
- 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
- yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
- IQ==
- </signing_certificate>
- <signing_certificate name="Android Auto Simulator" release="true"
- package="com.google.android.autosimulator">
- MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
- VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
- VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
- AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
- Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
- MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
- A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
- ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
- hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
- 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
- xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
- W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
- 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
- cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
- HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
- xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
- CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
- QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
- CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
- EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
- zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
- XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
- IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
- ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
- Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
- </signing_certificate>
- <signing_certificate name="Media Browser Simulator" release="true"
- package="com.google.android.mediasimulator">
- MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
- VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
- VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
- AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
- Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
- MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
- A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
- ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
- hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
- 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
- xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
- W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
- 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
- cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
- HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
- xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
- CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
- QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
- CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
- EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
- zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
- XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
- IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
- ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
- Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
- </signing_certificate>
-</allowed_callers>
diff --git a/media/MediaBrowserService/README.md b/media/MediaBrowserService/README.md
index d2f34f46..1f4333a5 100644
--- a/media/MediaBrowserService/README.md
+++ b/media/MediaBrowserService/README.md
@@ -18,7 +18,7 @@ Introduction
To implement a MediaBrowserService, you need to:
-- Extend android.service.media.MediaBrowserService, implementing the media
+- Extend MediaBrowserServiceCompat, implementing the media
browsing related methods onGetRoot and onLoadChildren;
- In onCreate, start a new MediaSession and call super.setSessionToken() with
@@ -72,8 +72,8 @@ To make it compatible with Android Auto, you also need to:
Pre-requisites
--------------
-- Android SDK 24
-- Android Build Tools v24.0.2
+- Android SDK 25
+- Android Build Tools v25.0.2
- Android Support Repository
Screenshots
@@ -102,7 +102,7 @@ submitting a pull request through GitHub. Please see CONTRIBUTING.md for more de
License
-------
-Copyright 2016 The Android Open Source Project, Inc.
+Copyright 2017 The Android Open Source Project, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
diff --git a/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties b/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
index bc0629f8..ea826796 100644
--- a/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue Sep 27 20:41:43 JST 2016
+#Wed Mar 22 15:15:46 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
diff --git a/media/MediaBrowserService/template-params.xml b/media/MediaBrowserService/template-params.xml
index 9c8a11c5..737fadb8 100644
--- a/media/MediaBrowserService/template-params.xml
+++ b/media/MediaBrowserService/template-params.xml
@@ -19,7 +19,12 @@
<group>Media</group>
<package>com.example.android.mediabrowserservice</package>
- <minSdk>21</minSdk>
+ <minSdk>19</minSdk>
+
+ <!-- Include additional dependencies here.-->
+ <dependency>com.android.support:support-compat:25.3.0</dependency>
+ <dependency>com.android.support:support-media-compat:25.3.0</dependency>
+ <dependency>com.android.support:appcompat-v7:25.3.0</dependency>
<strings>
<intro>
@@ -40,6 +45,9 @@ local UI.
<!-- The basic templates have already been enabled. Uncomment more as desired. -->
<template src="base-build" />
<template src="base-application" />
+
+ <use_support_library_vector_drawables>true</use_support_library_vector_drawables>
+
<metadata>
<status>PUBLISHED</status>
<categories>Media</categories>
@@ -54,11 +62,11 @@ local UI.
<img>screenshots/3-music-notification.png</img>
</screenshots>
<api_refs>
- <android>android.service.media.MediaBrowserService</android>
- <android>android.media.browse.MediaBrowser</android>
- <android>android.media.session.MediaSession</android>
- <android>android.media.session.MediaController</android>
- <android>android.app.Notification.MediaStyle</android>
+ <android>android.support.v4.media.MediaBrowserServiceCompat</android>
+ <android>android.support.v4.media.MediaBrowserCompat</android>
+ <android>android.support.v4.media.session.MediaSessionCompat</android>
+ <android>android.support.v4.media.session.MediaControllerCompat</android>
+ <android>android.support.v7.app.NotificationCompat.MediaStyle</android>
</api_refs>
<description>
<![CDATA[
@@ -77,7 +85,7 @@ local UI.
<![CDATA[
To implement a MediaBrowserService, you need to:
-- Extend android.service.media.MediaBrowserService, implementing the media
+- Extend MediaBrowserServiceCompat, implementing the media
browsing related methods onGetRoot and onLoadChildren;
- In onCreate, start a new MediaSession and call super.setSessionToken() with