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