aboutsummaryrefslogtreecommitdiff
path: root/media/MediaBrowserService/Application/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'media/MediaBrowserService/Application/src/main/java')
-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
18 files changed, 1020 insertions, 2610 deletions
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;
- }
-}