Import new sample.
Test: Manually verified.
Bug: 66707949
Change-Id: I63bc713a0793a27a6a9ebfaa09f324411aeefd76
diff --git a/media/MediaBrowserService/Application/.gitignore b/media/MediaBrowserService/Application/.gitignore
deleted file mode 100644
index 6eb878d..0000000
--- a/media/MediaBrowserService/Application/.gitignore
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2013 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-src/template/
-src/common/
-build.gradle
diff --git a/media/MediaBrowserService/Application/build.gradle b/media/MediaBrowserService/Application/build.gradle
new file mode 100644
index 0000000..3621f95
--- /dev/null
+++ b/media/MediaBrowserService/Application/build.gradle
@@ -0,0 +1,70 @@
+
+buildscript {
+ repositories {
+ jcenter()
+ maven {
+ url 'https://maven.google.com'
+ }
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.3'
+ }
+}
+
+apply plugin: 'com.android.application'
+
+repositories {
+ jcenter()
+ maven {
+ url 'https://maven.google.com'
+ }
+}
+
+dependencies {
+
+ compile "com.android.support:support-v4:26.1.0"
+ compile "com.android.support:support-v13:26.1.0"
+ compile "com.android.support:cardview-v7:26.1.0"
+
+ compile 'com.android.support:appcompat-v7:26.1.0'
+ compile 'com.android.support.constraint:constraint-layout:1.0.2'
+}
+
+// The sample build uses multiple directories to
+// keep boilerplate and common code separate from
+// the main sample code.
+List<String> dirs = [
+ 'main', // main sample code; look here for the interesting stuff.
+ 'common', // components that are reused by multiple samples
+ 'template'] // boilerplate code that is generated by the sample template process
+
+android {
+ compileSdkVersion 26
+
+ buildToolsVersion "26.0.1"
+
+ defaultConfig {
+ minSdkVersion 19
+ targetSdkVersion 26
+
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ sourceSets {
+ main {
+ dirs.each { dir ->
+ java.srcDirs "src/${dir}/java"
+ res.srcDirs "src/${dir}/res"
+ }
+ }
+ androidTest.setRoot('tests')
+ androidTest.java.srcDirs = ['tests/src']
+
+ }
+
+}
diff --git a/media/MediaBrowserService/Application/proguard-project.txt b/media/MediaBrowserService/Application/proguard-project.txt
deleted file mode 100644
index 0d8f171..0000000
--- a/media/MediaBrowserService/Application/proguard-project.txt
+++ /dev/null
@@ -1,20 +0,0 @@
- To enable ProGuard in your project, edit project.properties
-# to define the proguard.config property as described in that file.
-#
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the ProGuard
-# include property in project.properties.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/media/MediaBrowserService/Application/proguard-rules.pro b/media/MediaBrowserService/Application/proguard-rules.pro
new file mode 100644
index 0000000..a16aac3
--- /dev/null
+++ b/media/MediaBrowserService/Application/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/nazmul/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
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
deleted file mode 100644
index 665170d..0000000
--- a/media/MediaBrowserService/Application/src/androidTest/java/com/example/android/mediabrowserservice/test/SampleTests.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
-* Copyright 2013 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
-/*
-* 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.test;
-
-import android.test.ActivityInstrumentationTestCase2;
-
-/**
-* Tests for MediaBrowserService sample.
-*/
-public class SampleTests extends ActivityInstrumentationTestCase2<MainActivity> {
-
- private MainActivity mTestActivity;
- private MediaBrowserServiceFragment mTestFragment;
-
- public SampleTests() {
- super(MainActivity.class);
- }
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- // Starts the activity under test using the default Intent with:
- // action = {@link Intent#ACTION_MAIN}
- // flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK}
- // All other fields are null or empty.
- mTestActivity = getActivity();
- mTestFragment = (MediaBrowserServiceFragment)
- mTestActivity.getSupportFragmentManager().getFragments().get(1);
- }
-
- /**
- * Test if the test fixture has been set up correctly.
- */
- public void testPreconditions() {
- //Try to add a message to add context to your assertions. These messages will be shown if
- //a tests fails and make it easy to understand why a test failed
- assertNotNull("mTestActivity is null", mTestActivity);
- assertNotNull("mTestFragment is null", mTestFragment);
- }
-
- /**
- * Add more tests below.
- */
-
-}
diff --git a/media/MediaBrowserService/Application/src/main/AndroidManifest.xml b/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
index 570aeb4..6af29f0 100644
--- a/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
+++ b/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
@@ -1,70 +1,48 @@
-<?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.
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="com.example.android.mediabrowserservice"
- android:versionCode="1"
- android:versionName="1.0">
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.WAKE_LOCK" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.mediasession">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
<application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@style/AppTheme"
- tools:ignore="GoogleAppIndexingWarning,MissingIntentFilterForMediaSearch">
-
- <meta-data
- android:name="com.google.android.gms.car.application"
- android:resource="@xml/automotive_app_desc" />
-
-
- <activity
- android:name=".MusicPlayerActivity"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:launchMode="singleTop">
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name=".ui.MainActivity"
+ android:launchMode="singleTop">
<intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
+ <action android:name="android.intent.action.MAIN"/>
+
+ <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- <!-- (OPTIONAL) use this meta data to indicate which icon should be used in media
- notifications (for example, when the music changes and the user is
- looking at another app) -->
- <meta-data
- android:name="com.google.android.gms.car.notification.SmallIcon"
- android:resource="@drawable/ic_notification" />
-
- <!--
- (OPTIONAL) use this meta data to override the theme from which Android Auto will
- look for colors. If you don't set this, Android Auto will look
- for color attributes in your application theme.
- -->
- <meta-data
- android:name="com.google.android.gms.car.application.theme"
- android:resource="@style/CarTheme" />
-
<service
- android:name=".MusicService"
- android:exported="true">
+ android:name=".service.MusicService"
+ android:enabled="true"
+ android:exported="true">
<intent-filter>
- <action android:name="android.media.browse.MediaBrowserService" />
+ <action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
@@ -78,10 +56,9 @@
-->
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<intent-filter>
- <action android:name="android.intent.action.MEDIA_BUTTON" />
+ <action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
-
</application>
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/assets/jazz_in_paris.mp3 b/media/MediaBrowserService/Application/src/main/assets/jazz_in_paris.mp3
new file mode 100644
index 0000000..9a98517
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/assets/jazz_in_paris.mp3
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/assets/the_coldest_shoulder.mp3 b/media/MediaBrowserService/Application/src/main/assets/the_coldest_shoulder.mp3
new file mode 100644
index 0000000..5f043d5
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/assets/the_coldest_shoulder.mp3
Binary files differ
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
deleted file mode 100644
index 0f6f13f..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java
+++ /dev/null
@@ -1,187 +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.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.AsyncTask;
-import android.util.Log;
-import android.util.LruCache;
-
-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 = 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_ART_WIDTH = 800; // pixels
- private static final int MAX_ART_HEIGHT = 480; // pixels
-
- // Resolution reasonable for carrying around as an icon (generally in
- // MediaDescription.getIconBitmap). This should not be bigger than necessary, because
- // the MediaDescription object should be lightweight. If you set it too high and try to
- // serialize the MediaDescription, you may get FAILED BINDER TRANSACTION errors.
- private static final int MAX_ART_WIDTH_ICON = 128; // pixels
- private static final int MAX_ART_HEIGHT_ICON = 128; // pixels
-
- private static final int BIG_BITMAP_INDEX = 0;
- private static final int ICON_BITMAP_INDEX = 1;
-
- private final LruCache<String, Bitmap[]> mCache;
-
- private static final AlbumArtCache sInstance = new AlbumArtCache();
-
- public static AlbumArtCache getInstance() {
- return sInstance;
- }
-
- private 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)));
- 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();
- }
- };
- }
-
- public Bitmap getBigImage(String artUrl) {
- Bitmap[] result = mCache.get(artUrl);
- return result == null ? null : result[BIG_BITMAP_INDEX];
- }
-
- public Bitmap getIconImage(String artUrl) {
- Bitmap[] result = mCache.get(artUrl);
- return result == null ? null : result[ICON_BITMAP_INDEX];
- }
-
- public void fetch(final String artUrl, final FetchListener listener) {
- // WARNING: for the sake of simplicity, simultaneous multi-thread fetch requests
- // are not handled properly: they may cause redundant costly operations, like HTTP
- // requests and bitmap rescales. For production-level apps, we recommend you use
- // a proper image loading library, like Glide.
- Bitmap[] bitmap = mCache.get(artUrl);
- if (bitmap != null) {
- Log.d(TAG, "getOrFetch: album art is in cache, using it: " + artUrl);
- listener.onFetched(artUrl, bitmap[BIG_BITMAP_INDEX], bitmap[ICON_BITMAP_INDEX]);
- return;
- }
- 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 = 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;
- }
- Log.d(TAG, "doInBackground: putting bitmap in cache. cache size=" + mCache.size());
- return bitmaps;
- }
-
- @Override
- protected void onPostExecute(Bitmap[] bitmaps) {
- if (bitmaps == null) {
- listener.onError(artUrl, new IllegalArgumentException("got null bitmaps"));
- } else {
- listener.onFetched(artUrl,
- bitmaps[BIG_BITMAP_INDEX], bitmaps[ICON_BITMAP_INDEX]);
- }
- }
- }.execute();
- }
-
- 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
deleted file mode 100644
index 2e55cd8..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/BrowseFragment.java
+++ /dev/null
@@ -1,266 +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.content.ComponentName;
-import android.content.Context;
-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;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.ArrayList;
-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 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 = BrowseFragment.class.getSimpleName();
-
- public static final String ARG_MEDIA_ID = "media_id";
-
- /**
- * 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 MediaBrowserCompat mMediaBrowser;
- private BrowseAdapter mBrowserAdapter;
-
- 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();
- }
-
- @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);
- }
- }
-
- @Override
- public void onConnectionFailed() {
- Log.e(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 onPlaybackStateChanged(PlaybackStateCompat state) {
- mBrowserAdapter.setPlaybackState(state);
- mBrowserAdapter.notifyDataSetChanged();
- }
- };
-
- public static BrowseFragment newInstance(String mediaId) {
- Bundle args = new Bundle();
- args.putString(ARG_MEDIA_ID, mediaId);
- BrowseFragment fragment = new BrowseFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_list, container, false);
-
- mBrowserAdapter = new BrowseAdapter(getActivity());
-
- 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) {
- MediaBrowserCompat.MediaItem item = mBrowserAdapter.getItem(position);
- boolean isPlaying = item.getMediaId().equals(mBrowserAdapter.getPlayingMediaId());
- try {
- FragmentDataHelper listener = (FragmentDataHelper) getActivity();
- listener.onMediaItemSelected(item, isPlaying);
- } catch (ClassCastException ex) {
- Log.e(TAG, "Exception trying to cast to FragmentDataHelper", ex);
- }
- }
- });
-
- Bundle args = getArguments();
- mMediaId = args.getString(ARG_MEDIA_ID, null);
-
- mMediaBrowser = new MediaBrowserCompat(getActivity(),
- new ComponentName(getActivity(), MusicService.class),
- mConnectionCallback, null);
-
- return rootView;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mMediaBrowser.connect();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- mMediaBrowser.disconnect();
- }
-
- // An adapter for showing the list of browsed MediaItem's
- 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<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 {
- ImageView mImageView;
- TextView mTitleView;
- TextView mDescriptionView;
- }
-
- @NonNull
- @Override
- public View getView(int position, View convertView, @NonNull 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.mImageView.setVisibility(View.GONE);
- holder.mTitleView = (TextView) convertView.findViewById(R.id.title);
- holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description);
- convertView.setTag(holder);
- } else {
- holder = (ViewHolder) convertView.getTag();
- }
-
- MediaBrowserCompat.MediaItem item = getItem(position);
- holder.mTitleView.setText(item.getDescription().getTitle());
- holder.mDescriptionView.setText(item.getDescription().getDescription());
- if (item.isPlayable()) {
- 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
deleted file mode 100644
index 4cda405..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationHelper.java
+++ /dev/null
@@ -1,86 +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.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/MusicPlayerActivity.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java
deleted file mode 100644
index 0a3a7df..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicPlayerActivity.java
+++ /dev/null
@@ -1,60 +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.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 AppCompatActivity
- implements BrowseFragment.FragmentDataHelper {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_player);
- if (savedInstanceState == null) {
- getSupportFragmentManager().beginTransaction()
- .add(R.id.container, BrowseFragment.newInstance(null))
- .commit();
- }
- }
-
- @Override
- public void onMediaItemSelected(MediaBrowserCompat.MediaItem item, boolean isPlaying) {
- if (item.isPlayable()) {
- 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()) {
- 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
deleted file mode 100644
index 1cfb023..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
+++ /dev/null
@@ -1,564 +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.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/Playback.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
deleted file mode 100644
index 6af2d41..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
+++ /dev/null
@@ -1,436 +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.content.Context;
-import android.media.AudioManager;
-import android.media.MediaPlayer;
-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 java.io.IOException;
-
-import static android.media.MediaPlayer.OnCompletionListener;
-import static android.media.MediaPlayer.OnErrorListener;
-import static android.media.MediaPlayer.OnPreparedListener;
-import static android.media.MediaPlayer.OnSeekCompleteListener;
-
-/**
- * A class that implements local media playback using {@link android.media.MediaPlayer}
- */
-public class Playback implements AudioManager.OnAudioFocusChangeListener,
- OnCompletionListener, OnErrorListener, OnPreparedListener, OnSeekCompleteListener {
-
- 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.
- public static final float VOLUME_DUCK = 0.2f;
- // The volume we set the media player when we have audio focus.
- public static final float VOLUME_NORMAL = 1.0f;
-
- // we don't have audio focus, and can't duck (play at a low volume)
- private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
- // 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 final MusicService mService;
- private final MusicProvider mMusicProvider;
- private final WifiManager.WifiLock mWifiLock;
- private int mState = PlaybackStateCompat.STATE_NONE;
- private boolean mPlayOnFocusGain;
- private Callback mCallback;
- private volatile int mCurrentPosition;
- private volatile String mCurrentMediaId;
-
- // Type of audio focus we have:
- private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
- private AudioManager mAudioManager;
- private MediaPlayer mMediaPlayer;
-
- public Playback(MusicService service, MusicProvider musicProvider) {
- Context context = service.getApplicationContext();
- this.mService = service;
- this.mMusicProvider = musicProvider;
- this.mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-
- // 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() {
- mState = PlaybackStateCompat.STATE_STOPPED;
- if (mCallback != null) {
- mCallback.onPlaybackStatusChanged(mState);
- }
- mCurrentPosition = getCurrentStreamPosition();
- // Give up Audio focus
- giveUpAudioFocus();
- // Relax all resources
- relaxResources(true);
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- }
-
- public int getState() {
- return mState;
- }
-
- public boolean isConnected() {
- return true;
- }
-
- public boolean isPlaying() {
- return mPlayOnFocusGain || (mMediaPlayer != null && mMediaPlayer.isPlaying());
- }
-
- public int getCurrentStreamPosition() {
- return mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : mCurrentPosition;
- }
-
- public void play(MediaSessionCompat.QueueItem item) {
- mPlayOnFocusGain = true;
- tryToGetAudioFocus();
- String mediaId = item.getDescription().getMediaId();
- boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
- if (mediaHasChanged) {
- mCurrentPosition = 0;
- mCurrentMediaId = mediaId;
- }
-
- if (mState == PlaybackStateCompat.STATE_PAUSED
- && !mediaHasChanged && mMediaPlayer != null) {
- configMediaPlayerState();
- } else {
- mState = PlaybackStateCompat.STATE_STOPPED;
- relaxResources(false); // release everything except MediaPlayer
- MediaMetadataCompat track = mMusicProvider.getMusic(item.getDescription().getMediaId());
-
- String source = track.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI);
- try {
- createMediaPlayerIfNeeded();
-
- mState = PlaybackStateCompat.STATE_BUFFERING;
-
- mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- mMediaPlayer.setDataSource(source);
-
- // Starts preparing the media player in the background. When
- // it's done, it will call our OnPreparedListener (that is,
- // the onPrepared() method on this class, since we set the
- // listener to 'this'). Until the media player is prepared,
- // we *cannot* call start() on it!
- mMediaPlayer.prepareAsync();
-
- // If we are streaming from the internet, we want to hold a
- // Wifi lock, which prevents the Wifi radio from going to
- // sleep while the song is playing.
- mWifiLock.acquire();
-
- if (mCallback != null) {
- mCallback.onPlaybackStatusChanged(mState);
- }
-
- } catch (IOException ioException) {
- Log.e(TAG, "Exception playing song", ioException);
- if (mCallback != null) {
- mCallback.onError(ioException.getMessage());
- }
- }
- }
- }
-
- public void pause() {
- if (mState == PlaybackStateCompat.STATE_PLAYING) {
- // Pause media player and cancel the 'foreground service' state.
- if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
- mMediaPlayer.pause();
- mCurrentPosition = mMediaPlayer.getCurrentPosition();
- }
- // while paused, retain the MediaPlayer but give up audio focus
- relaxResources(false);
- }
- mState = PlaybackStateCompat.STATE_PAUSED;
- if (mCallback != null) {
- mCallback.onPlaybackStatusChanged(mState);
- }
- }
-
- public void seekTo(int 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.
- mCurrentPosition = position;
- } else {
- if (mMediaPlayer.isPlaying()) {
- mState = PlaybackStateCompat.STATE_BUFFERING;
- }
- mMediaPlayer.seekTo(position);
- if (mCallback != null) {
- mCallback.onPlaybackStatusChanged(mState);
- }
- }
- }
-
- public void setCallback(Callback callback) {
- this.mCallback = callback;
- }
-
- /**
- * Try to get the system audio focus.
- */
- private void tryToGetAudioFocus() {
- 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() {
- Log.d(TAG, "giveUpAudioFocus");
- if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
- }
- }
-
- /**
- * Reconfigures MediaPlayer according to audio focus settings and
- * starts/restarts it. This method starts/restarts the MediaPlayer
- * respecting the current audio focus state. So if we have focus, it will
- * play normally; if we don't have focus, it will either leave the
- * MediaPlayer paused or set it to a low volume, depending on what is
- * allowed by the current focus settings. This method assumes mPlayer !=
- * null, so if you are calling it, you have to do so from a context where
- * you are sure this is the case.
- */
- private void configMediaPlayerState() {
- 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 == PlaybackStateCompat.STATE_PLAYING) {
- pause();
- }
- } else { // we have audio focus:
- if (mAudioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
- mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
- } else {
- if (mMediaPlayer != null) {
- mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
- } // else do something for remote client.
- }
- // If we were playing when we lost focus, we need to resume playing.
- if (mPlayOnFocusGain) {
- if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
- Log.d(TAG, "configMediaPlayerState startMediaPlayer. seeking to "
- + mCurrentPosition);
- if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
- mMediaPlayer.start();
- mState = PlaybackStateCompat.STATE_PLAYING;
- } else {
- mMediaPlayer.seekTo(mCurrentPosition);
- mState = PlaybackStateCompat.STATE_BUFFERING;
- }
- }
- mPlayOnFocusGain = false;
- }
- }
- if (mCallback != null) {
- mCallback.onPlaybackStatusChanged(mState);
- }
- }
-
- /**
- * Called by AudioManager on audio focus changes.
- * Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener}.
- */
- @Override
- public void onAudioFocusChange(int 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) {
- // 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;
- mAudioFocus = canDuck ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK;
-
- // If we are playing, we need to reset media player by calling configMediaPlayerState
- // with mAudioFocus properly set.
- 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 {
- Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
- }
- configMediaPlayerState();
- }
-
- /**
- * Called when MediaPlayer has completed a seek.
- *
- * @see android.media.MediaPlayer.OnSeekCompleteListener
- */
- @Override
- public void onSeekComplete(MediaPlayer player) {
- Log.d(TAG, "onSeekComplete from MediaPlayer:" + player.getCurrentPosition());
- mCurrentPosition = player.getCurrentPosition();
- if (mState == PlaybackStateCompat.STATE_BUFFERING) {
- mMediaPlayer.start();
- mState = PlaybackStateCompat.STATE_PLAYING;
- }
- if (mCallback != null) {
- mCallback.onPlaybackStatusChanged(mState);
- }
- }
-
- /**
- * Called when media player is done playing current song.
- *
- * @see android.media.MediaPlayer.OnCompletionListener
- */
- @Override
- public void onCompletion(MediaPlayer player) {
- 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) {
- mCallback.onCompletion();
- }
- }
-
- /**
- * Called when media player is done preparing.
- *
- * @see android.media.MediaPlayer.OnPreparedListener
- */
- @Override
- public void onPrepared(MediaPlayer player) {
- Log.d(TAG, "onPrepared from MediaPlayer");
- // The media player is done preparing. That means we can start playing if we
- // have audio focus.
- configMediaPlayerState();
- }
-
- /**
- * Called when there's an error playing media. When this happens, the media
- * player goes to the Error state. We warn the user about the error and
- * reset the media player.
- *
- * @see android.media.MediaPlayer.OnErrorListener
- */
- @Override
- 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 + ")");
- }
- return true; // true indicates we handled the error
- }
-
- /**
- * Makes sure the media player exists and has been reset. This will create
- * the media player if needed, or reset the existing media player if one
- * already exists.
- */
- private void createMediaPlayerIfNeeded() {
- Log.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer == null));
- if (mMediaPlayer == null) {
- mMediaPlayer = new MediaPlayer();
-
- // Make sure the media player will acquire a wake-lock while
- // playing. If we don't do that, the CPU might go to sleep while the
- // song is playing, causing playback to stop.
- mMediaPlayer.setWakeMode(mService.getApplicationContext(),
- PowerManager.PARTIAL_WAKE_LOCK);
-
- // we want the media player to notify us when it's ready preparing,
- // and when it's done playing:
- mMediaPlayer.setOnPreparedListener(this);
- mMediaPlayer.setOnCompletionListener(this);
- mMediaPlayer.setOnErrorListener(this);
- mMediaPlayer.setOnSeekCompleteListener(this);
- } else {
- mMediaPlayer.reset();
- }
- }
-
- /**
- * Releases resources used by the service for playback. This includes the
- * "foreground service" status, the wake locks and possibly the MediaPlayer.
- *
- * @param releaseMediaPlayer Indicates whether the Media Player should also
- * be released or not.
- */
- private void relaxResources(boolean releaseMediaPlayer) {
- Log.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer);
-
- mService.stopForeground(true);
-
- // stop and release the Media Player, if it's available
- if (releaseMediaPlayer && mMediaPlayer != null) {
- mMediaPlayer.reset();
- mMediaPlayer.release();
- mMediaPlayer = null;
- }
-
- // we can also release the Wifi lock, if we're holding it
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- }
-}
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
deleted file mode 100644
index 7eead43..0000000
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
+++ /dev/null
@@ -1,258 +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.os.AsyncTask;
-import android.support.v4.media.MediaMetadataCompat;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.Collections;
-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 = MusicProvider.class.getSimpleName();
-
- public static final String MEDIA_ID_ROOT = "__ROOT__";
- public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY__";
-
- 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";
- private static final String JSON_ALBUM = "album";
- private static final String JSON_ARTIST = "artist";
- private static final String JSON_GENRE = "genre";
- private static final String JSON_SOURCE = "source";
- private static final String JSON_IMAGE = "image";
- private static final String JSON_TRACK_NUMBER = "trackNumber";
- private static final String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
- private static final String JSON_DURATION = "duration";
-
- // Categorized caches for music track data:
- private final LinkedHashMap<String, MediaMetadataCompat> mMusicListById;
-
- 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() {
- mMusicListById = new LinkedHashMap<>();
- }
-
- public Iterable<MediaMetadataCompat> getAllMusics() {
- if (mCurrentState != State.INITIALIZED || mMusicListById.isEmpty()) {
- return Collections.emptyList();
- }
- return mMusicListById.values();
- }
-
- /**
- * Return the MediaMetadata for the given musicID.
- *
- * @param musicId The unique music ID.
- */
- public MediaMetadataCompat getMusic(String musicId) {
- return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null;
- }
-
- /**
- * 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 synchronized void updateMusic(String musicId, MediaMetadataCompat metadata) {
- MediaMetadataCompat track = mMusicListById.get(musicId);
- if (track != null) {
- mMusicListById.put(musicId, metadata);
- }
- }
-
- public boolean isInitialized() {
- return mCurrentState == State.INITIALIZED;
- }
-
- /**
- * Get the list of music tracks from a server and caches the track information
- * for future reference, keying tracks by musicId and grouping by genre.
- */
- public void retrieveMediaAsync(final Callback callback) {
- Log.d(TAG, "retrieveMediaAsync called");
- if (mCurrentState == State.INITIALIZED) {
- // Already initialized, so call back immediately.
- callback.onMusicCatalogReady(true);
- return;
- }
-
- // Asynchronously load the music catalog in a separate thread
- new AsyncTask<Void, Void, State>() {
- @Override
- protected State doInBackground(Void... params) {
- retrieveMedia();
- return mCurrentState;
- }
-
- @Override
- protected void onPostExecute(State current) {
- if (callback != null) {
- callback.onMusicCatalogReady(current == State.INITIALIZED);
- }
- }
- }.execute();
- }
-
- private synchronized void retrieveMedia() {
- try {
- if (mCurrentState == State.NON_INITIALIZED) {
- mCurrentState = State.INITIALIZING;
-
- int slashPos = CATALOG_URL.lastIndexOf('/');
- String path = CATALOG_URL.substring(0, slashPos + 1);
- JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);
- if (jsonObj == null) {
- return;
- }
- JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
- if (tracks != null) {
- 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);
- }
- }
- mCurrentState = State.INITIALIZED;
- }
- } 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
- // retries (eg if the network connection is temporary unavailable)
- mCurrentState = State.NON_INITIALIZED;
- }
- }
- }
-
- 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);
- String genre = json.getString(JSON_GENRE);
- String source = json.getString(JSON_SOURCE);
- String iconUrl = json.getString(JSON_IMAGE);
- int trackNumber = json.getInt(JSON_TRACK_NUMBER);
- int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
- int duration = json.getInt(JSON_DURATION) * 1000; // ms
-
- Log.d(TAG, "Found music track: " + json);
-
- // Media is stored relative to JSON file
- if (!source.startsWith("https")) {
- source = basePath + source;
- }
- 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
- // the music source. In a real world app, this could come from the server.
- String id = String.valueOf(source.hashCode());
-
- // Adding the music source to the MediaMetadata (and consequently using it in the
- // 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.
- 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();
- }
-
- /**
- * Download a JSON file from a server, parse the content and return the JSON
- * object.
- *
- * @return result JSONObject containing the parsed representation.
- */
- private JSONObject fetchJSONFromUrl(String urlString) {
- InputStream inputStream = null;
- try {
- URL url = new URL(urlString);
- URLConnection urlConnection = url.openConnection();
- inputStream = new BufferedInputStream(urlConnection.getInputStream());
- BufferedReader reader = new BufferedReader(new InputStreamReader(
- urlConnection.getInputStream(), "iso-8859-1"));
- StringBuilder stringBuilder = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- stringBuilder.append(line);
- }
- 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 the inputStream was opened, try to close it now.
- if (inputStream != null) {
- try {
- 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/mediasession/client/MediaBrowserAdapter.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/client/MediaBrowserAdapter.java
new file mode 100644
index 0000000..d2f0b7f
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/client/MediaBrowserAdapter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2017 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.mediasession.client;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+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.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.example.android.mediasession.service.MusicService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter for a MediaBrowser that handles connecting, disconnecting,
+ * and basic browsing.
+ */
+public class MediaBrowserAdapter {
+
+ private static final String TAG = MediaBrowserAdapter.class.getSimpleName();
+
+ /**
+ * Helper class for easily subscribing to changes in a MediaBrowserService connection.
+ */
+ public static abstract class MediaBrowserChangeListener {
+
+ public void onConnected(@Nullable MediaControllerCompat mediaController) {
+ }
+
+ public void onMetadataChanged(@Nullable MediaMetadataCompat mediaMetadata) {
+ }
+
+ public void onPlaybackStateChanged(@Nullable PlaybackStateCompat playbackState) {
+ }
+ }
+
+ private final InternalState mState;
+
+ private final Context mContext;
+ private final List<MediaBrowserChangeListener> mListeners = new ArrayList<>();
+
+ private final MediaBrowserConnectionCallback mMediaBrowserConnectionCallback =
+ new MediaBrowserConnectionCallback();
+ private final MediaControllerCallback mMediaControllerCallback =
+ new MediaControllerCallback();
+ private final MediaBrowserSubscriptionCallback mMediaBrowserSubscriptionCallback =
+ new MediaBrowserSubscriptionCallback();
+
+ private MediaBrowserCompat mMediaBrowser;
+
+ @Nullable
+ private MediaControllerCompat mMediaController;
+
+ public MediaBrowserAdapter(Context context) {
+ mContext = context;
+ mState = new InternalState();
+ }
+
+ public void onStart() {
+ if (mMediaBrowser == null) {
+ mMediaBrowser =
+ new MediaBrowserCompat(
+ mContext,
+ new ComponentName(mContext, MusicService.class),
+ mMediaBrowserConnectionCallback,
+ null);
+ mMediaBrowser.connect();
+ }
+ Log.d(TAG, "onStart: Creating MediaBrowser, and connecting");
+ }
+
+ public void onStop() {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mMediaControllerCallback);
+ mMediaController = null;
+ }
+ if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+ mMediaBrowser.disconnect();
+ mMediaBrowser = null;
+ }
+ resetState();
+ Log.d(TAG, "onStop: Releasing MediaController, Disconnecting from MediaBrowser");
+ }
+
+ /**
+ * The internal state of the app needs to revert to what it looks like when it started before
+ * any connections to the {@link MusicService} happens via the {@link MediaSessionCompat}.
+ */
+ private void resetState() {
+ mState.reset();
+ performOnAllListeners(new ListenerCommand() {
+ @Override
+ public void perform(@NonNull MediaBrowserChangeListener listener) {
+ listener.onPlaybackStateChanged(null);
+ }
+ });
+ Log.d(TAG, "resetState: ");
+ }
+
+ public MediaControllerCompat.TransportControls getTransportControls() {
+ if (mMediaController == null) {
+ Log.d(TAG, "getTransportControls: MediaController is null!");
+ throw new IllegalStateException();
+ }
+ return mMediaController.getTransportControls();
+ }
+
+ public void addListener(MediaBrowserChangeListener listener) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ }
+
+ public void removeListener(MediaBrowserChangeListener listener) {
+ if (listener != null) {
+ if (mListeners.contains(listener)) {
+ mListeners.remove(listener);
+ }
+ }
+ }
+
+ public void performOnAllListeners(@NonNull ListenerCommand command) {
+ for (MediaBrowserChangeListener listener : mListeners) {
+ if (listener != null) {
+ try {
+ command.perform(listener);
+ } catch (Exception e) {
+ removeListener(listener);
+ }
+ }
+ }
+ }
+
+ public interface ListenerCommand {
+
+ void perform(@NonNull MediaBrowserChangeListener listener);
+ }
+
+ // Receives callbacks from the MediaBrowser when it has successfully connected to the
+ // MediaBrowserService (MusicService).
+ public class MediaBrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+
+ // Happens as a result of onStart().
+ @Override
+ public void onConnected() {
+ try {
+ // Get a MediaController for the MediaSession.
+ mMediaController = new MediaControllerCompat(mContext,
+ mMediaBrowser.getSessionToken());
+ mMediaController.registerCallback(mMediaControllerCallback);
+
+ // Sync existing MediaSession state to the UI.
+ mMediaControllerCallback.onMetadataChanged(
+ mMediaController.getMetadata());
+ mMediaControllerCallback
+ .onPlaybackStateChanged(mMediaController.getPlaybackState());
+
+ performOnAllListeners(new ListenerCommand() {
+ @Override
+ public void perform(@NonNull MediaBrowserChangeListener listener) {
+ listener.onConnected(mMediaController);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.d(TAG, String.format("onConnected: Problem: %s", e.toString()));
+ throw new RuntimeException(e);
+ }
+
+ mMediaBrowser.subscribe(mMediaBrowser.getRoot(), mMediaBrowserSubscriptionCallback);
+ }
+ }
+
+ // Receives callbacks from the MediaBrowser when the MediaBrowserService has loaded new media
+ // that is ready for playback.
+ public class MediaBrowserSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId,
+ @NonNull List<MediaBrowserCompat.MediaItem> children) {
+ assert mMediaController != null;
+
+ // Queue up all media items for this simple sample.
+ for (final MediaBrowserCompat.MediaItem mediaItem : children) {
+ mMediaController.addQueueItem(mediaItem.getDescription());
+ }
+
+ // Call "playFromMedia" so the UI is updated.
+ mMediaController.getTransportControls().prepare();
+ }
+ }
+
+ // Receives callbacks from the MediaController and updates the UI state,
+ // i.e.: Which is the current item, whether it's playing or paused, etc.
+ public class MediaControllerCallback extends MediaControllerCompat.Callback {
+
+ @Override
+ public void onMetadataChanged(final MediaMetadataCompat metadata) {
+ // Filtering out needless updates, given that the metadata has not changed.
+ if (isMediaIdSame(metadata, mState.getMediaMetadata())) {
+ Log.d(TAG, "onMetadataChanged: Filtering out needless onMetadataChanged() update");
+ return;
+ } else {
+ mState.setMediaMetadata(metadata);
+ }
+ performOnAllListeners(new ListenerCommand() {
+ @Override
+ public void perform(@NonNull MediaBrowserChangeListener listener) {
+ listener.onMetadataChanged(metadata);
+ }
+ });
+ }
+
+ @Override
+ public void onPlaybackStateChanged(@Nullable final PlaybackStateCompat state) {
+ mState.setPlaybackState(state);
+ performOnAllListeners(new ListenerCommand() {
+ @Override
+ public void perform(@NonNull MediaBrowserChangeListener listener) {
+ listener.onPlaybackStateChanged(state);
+ }
+ });
+ }
+
+ // This might happen if the MusicService is killed while the Activity is in the
+ // foreground and onStart() has been called (but not onStop()).
+ @Override
+ public void onSessionDestroyed() {
+ resetState();
+ onPlaybackStateChanged(null);
+ Log.d(TAG, "onSessionDestroyed: MusicService is dead!!!");
+ }
+
+ private boolean isMediaIdSame(MediaMetadataCompat currentMedia,
+ MediaMetadataCompat newMedia) {
+ if (currentMedia == null || newMedia == null) {
+ return false;
+ }
+ String newMediaId =
+ newMedia.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
+ String currentMediaId =
+ currentMedia.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
+ return newMediaId.equals(currentMediaId);
+ }
+
+ }
+
+ // A holder class that contains the internal state.
+ public class InternalState {
+
+ private PlaybackStateCompat playbackState;
+ private MediaMetadataCompat mediaMetadata;
+
+ public void reset() {
+ playbackState = null;
+ mediaMetadata = null;
+ }
+
+ public PlaybackStateCompat getPlaybackState() {
+ return playbackState;
+ }
+
+ public void setPlaybackState(PlaybackStateCompat playbackState) {
+ this.playbackState = playbackState;
+ }
+
+ public MediaMetadataCompat getMediaMetadata() {
+ return mediaMetadata;
+ }
+
+ public void setMediaMetadata(MediaMetadataCompat mediaMetadata) {
+ this.mediaMetadata = mediaMetadata;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/MusicService.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/MusicService.java
new file mode 100644
index 0000000..5d98bc2
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/MusicService.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2017 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.mediasession.service;
+
+import android.app.Notification;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.example.android.mediasession.service.contentcatalogs.MusicLibrary;
+import com.example.android.mediasession.service.notifications.MediaNotificationManager;
+import com.example.android.mediasession.service.players.MediaPlayerAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MusicService extends MediaBrowserServiceCompat {
+
+ private static final String TAG = MusicService.class.getSimpleName();
+
+ private MediaSessionCompat mSession;
+ private PlayerAdapter mPlayback;
+ private MediaNotificationManager mMediaNotificationManager;
+ private MediaSessionCallback mCallback;
+ private boolean mServiceInStartedState;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Create a new MediaSession.
+ mSession = new MediaSessionCompat(this, "MusicService");
+ mCallback = new MediaSessionCallback();
+ mSession.setCallback(mCallback);
+ mSession.setFlags(
+ MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
+ MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS |
+ MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ setSessionToken(mSession.getSessionToken());
+
+ mMediaNotificationManager = new MediaNotificationManager(this);
+
+ mPlayback = new MediaPlayerAdapter(this, new MediaPlayerListener());
+ Log.d(TAG, "onCreate: MusicService creating MediaSession, and MediaNotificationManager");
+ }
+
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ stopSelf();
+ }
+
+ @Override
+ public void onDestroy() {
+ mMediaNotificationManager.onDestroy();
+ mPlayback.stop();
+ mSession.release();
+ Log.d(TAG, "onDestroy: MediaPlayerAdapter stopped, and MediaSession released");
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(@NonNull String clientPackageName,
+ int clientUid,
+ Bundle rootHints) {
+ return new BrowserRoot(MusicLibrary.getRoot(), null);
+ }
+
+ @Override
+ public void onLoadChildren(
+ @NonNull final String parentMediaId,
+ @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
+ result.sendResult(MusicLibrary.getMediaItems());
+ }
+
+ // MediaSession Callback: Transport Controls -> MediaPlayerAdapter
+ public class MediaSessionCallback extends MediaSessionCompat.Callback {
+ private final List<MediaSessionCompat.QueueItem> mPlaylist = new ArrayList<>();
+ private int mQueueIndex = -1;
+ private MediaMetadataCompat mPreparedMedia;
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description) {
+ mPlaylist.add(new MediaSessionCompat.QueueItem(description, description.hashCode()));
+ mQueueIndex = (mQueueIndex == -1) ? 0 : mQueueIndex;
+ }
+
+ @Override
+ public void onRemoveQueueItem(MediaDescriptionCompat description) {
+ mPlaylist.remove(new MediaSessionCompat.QueueItem(description, description.hashCode()));
+ mQueueIndex = (mPlaylist.isEmpty()) ? -1 : mQueueIndex;
+ }
+
+ @Override
+ public void onPrepare() {
+ if (mQueueIndex < 0 && mPlaylist.isEmpty()) {
+ // Nothing to play.
+ return;
+ }
+
+ final String mediaId = mPlaylist.get(mQueueIndex).getDescription().getMediaId();
+ mPreparedMedia = MusicLibrary.getMetadata(MusicService.this, mediaId);
+ mSession.setMetadata(mPreparedMedia);
+
+ if (!mSession.isActive()) {
+ mSession.setActive(true);
+ }
+ }
+
+ @Override
+ public void onPlay() {
+ if (!isReadyToPlay()) {
+ // Nothing to play.
+ return;
+ }
+
+ if (mPreparedMedia == null) {
+ onPrepare();
+ }
+
+ mPlayback.playFromMedia(mPreparedMedia);
+ Log.d(TAG, "onPlayFromMediaId: MediaSession active");
+ }
+
+ @Override
+ public void onPause() {
+ mPlayback.pause();
+ }
+
+ @Override
+ public void onStop() {
+ mPlayback.stop();
+ mSession.setActive(false);
+ }
+
+ @Override
+ public void onSkipToNext() {
+ mQueueIndex = (++mQueueIndex % mPlaylist.size());
+ mPreparedMedia = null;
+ onPlay();
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ mQueueIndex = mQueueIndex > 0 ? mQueueIndex - 1 : mPlaylist.size() - 1;
+ mPreparedMedia = null;
+ onPlay();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ mPlayback.seekTo(pos);
+ }
+
+ private boolean isReadyToPlay() {
+ return (!mPlaylist.isEmpty());
+ }
+ }
+
+ // MediaPlayerAdapter Callback: MediaPlayerAdapter state -> MusicService.
+ public class MediaPlayerListener extends PlaybackInfoListener {
+
+ private final ServiceManager mServiceManager;
+
+ MediaPlayerListener() {
+ mServiceManager = new ServiceManager();
+ }
+
+ @Override
+ public void onPlaybackStateChange(PlaybackStateCompat state) {
+ // Report the state to the MediaSession.
+ mSession.setPlaybackState(state);
+
+ // Manage the started state of this service.
+ switch (state.getState()) {
+ case PlaybackStateCompat.STATE_PLAYING:
+ mServiceManager.moveServiceToStartedState(state);
+ break;
+ case PlaybackStateCompat.STATE_PAUSED:
+ mServiceManager.updateNotificationForPause(state);
+ break;
+ case PlaybackStateCompat.STATE_STOPPED:
+ mServiceManager.moveServiceOutOfStartedState(state);
+ break;
+ }
+ }
+
+ class ServiceManager {
+
+ private void moveServiceToStartedState(PlaybackStateCompat state) {
+ Notification notification =
+ mMediaNotificationManager.getNotification(
+ mPlayback.getCurrentMedia(), state, getSessionToken());
+
+ if (!mServiceInStartedState) {
+ ContextCompat.startForegroundService(
+ MusicService.this,
+ new Intent(MusicService.this, MusicService.class));
+ mServiceInStartedState = true;
+ }
+
+ startForeground(MediaNotificationManager.NOTIFICATION_ID, notification);
+ }
+
+ private void updateNotificationForPause(PlaybackStateCompat state) {
+ stopForeground(false);
+ Notification notification =
+ mMediaNotificationManager.getNotification(
+ mPlayback.getCurrentMedia(), state, getSessionToken());
+ mMediaNotificationManager.getNotificationManager()
+ .notify(MediaNotificationManager.NOTIFICATION_ID, notification);
+ }
+
+ private void moveServiceOutOfStartedState(PlaybackStateCompat state) {
+ stopForeground(true);
+ stopSelf();
+ mServiceInStartedState = false;
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/PlaybackInfoListener.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/PlaybackInfoListener.java
new file mode 100644
index 0000000..f19abd2
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/PlaybackInfoListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 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.mediasession.service;
+
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import com.example.android.mediasession.service.players.MediaPlayerAdapter;
+
+/**
+ * Listener to provide state updates from {@link MediaPlayerAdapter} (the media player)
+ * to {@link MusicService} (the service that holds our {@link MediaSessionCompat}.
+ */
+public abstract class PlaybackInfoListener {
+
+ public abstract void onPlaybackStateChange(PlaybackStateCompat state);
+
+ public void onPlaybackCompleted() {
+ }
+}
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/PlayerAdapter.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/PlayerAdapter.java
new file mode 100644
index 0000000..83c810e
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/PlayerAdapter.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 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.mediasession.service;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.net.wifi.WifiManager;
+import android.support.annotation.NonNull;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+/**
+ * Abstract player implementation that handles playing music with proper handling of headphones
+ * and audio focus.
+ */
+public abstract class PlayerAdapter {
+
+ private static final float MEDIA_VOLUME_DEFAULT = 1.0f;
+ private static final float MEDIA_VOLUME_DUCK = 0.2f;
+
+ private static final IntentFilter AUDIO_NOISY_INTENT_FILTER =
+ new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+
+ private boolean mAudioNoisyReceiverRegistered = false;
+ private final BroadcastReceiver mAudioNoisyReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
+ if (isPlaying()) {
+ pause();
+ }
+ }
+ }
+ };
+
+ private final Context mApplicationContext;
+ private final AudioManager mAudioManager;
+ private final AudioFocusHelper mAudioFocusHelper;
+
+ private boolean mPlayOnAudioFocus = false;
+
+ public PlayerAdapter(@NonNull Context context) {
+ mApplicationContext = context.getApplicationContext();
+ mAudioManager = (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE);
+ mAudioFocusHelper = new AudioFocusHelper();
+ }
+
+ public abstract void playFromMedia(MediaMetadataCompat metadata);
+
+ public abstract MediaMetadataCompat getCurrentMedia();
+
+ public abstract boolean isPlaying();
+
+ public final void play() {
+ if (mAudioFocusHelper.requestAudioFocus()) {
+ registerAudioNoisyReceiver();
+ onPlay();
+ }
+ }
+
+ /**
+ * Called when media is ready to be played and indicates the app has audio focus.
+ */
+ protected abstract void onPlay();
+
+ public final void pause() {
+ if (!mPlayOnAudioFocus) {
+ mAudioFocusHelper.abandonAudioFocus();
+ }
+
+ unregisterAudioNoisyReceiver();
+ onPause();
+ }
+
+ /**
+ * Called when media must be paused.
+ */
+ protected abstract void onPause();
+
+ public final void stop() {
+ mAudioFocusHelper.abandonAudioFocus();
+ unregisterAudioNoisyReceiver();
+ onStop();
+ }
+
+ /**
+ * Called when the media must be stopped. The player should clean up resources at this
+ * point.
+ */
+ protected abstract void onStop();
+
+ public abstract void seekTo(long position);
+
+ public abstract void setVolume(float volume);
+
+ private void registerAudioNoisyReceiver() {
+ if (!mAudioNoisyReceiverRegistered) {
+ mApplicationContext.registerReceiver(mAudioNoisyReceiver, AUDIO_NOISY_INTENT_FILTER);
+ mAudioNoisyReceiverRegistered = true;
+ }
+ }
+
+ private void unregisterAudioNoisyReceiver() {
+ if (mAudioNoisyReceiverRegistered) {
+ mApplicationContext.unregisterReceiver(mAudioNoisyReceiver);
+ mAudioNoisyReceiverRegistered = false;
+ }
+ }
+
+ /**
+ * Helper class for managing audio focus related tasks.
+ */
+ private final class AudioFocusHelper
+ implements AudioManager.OnAudioFocusChangeListener {
+
+ private boolean requestAudioFocus() {
+ final int result = mAudioManager.requestAudioFocus(this,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+
+ private void abandonAudioFocus() {
+ mAudioManager.abandonAudioFocus(this);
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (mPlayOnAudioFocus && !isPlaying()) {
+ play();
+ } else if (isPlaying()) {
+ setVolume(MEDIA_VOLUME_DEFAULT);
+ }
+ mPlayOnAudioFocus = false;
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ setVolume(MEDIA_VOLUME_DUCK);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ if (isPlaying()) {
+ mPlayOnAudioFocus = true;
+ pause();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ mAudioManager.abandonAudioFocus(this);
+ mPlayOnAudioFocus = false;
+ stop();
+ break;
+ }
+ }
+ }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/contentcatalogs/MusicLibrary.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/contentcatalogs/MusicLibrary.java
new file mode 100644
index 0000000..dacb009
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/contentcatalogs/MusicLibrary.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2017 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.mediasession.service.contentcatalogs;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+
+import com.example.android.mediasession.BuildConfig;
+import com.example.android.mediasession.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+
+public class MusicLibrary {
+
+ private static final TreeMap<String, MediaMetadataCompat> music = new TreeMap<>();
+ private static final HashMap<String, Integer> albumRes = new HashMap<>();
+ private static final HashMap<String, String> musicFileName = new HashMap<>();
+
+ static {
+ createMediaMetadataCompat(
+ "Jazz_In_Paris",
+ "Jazz in Paris",
+ "Media Right Productions",
+ "Jazz & Blues",
+ "Jazz",
+ 103,
+ TimeUnit.SECONDS,
+ "jazz_in_paris.mp3",
+ R.drawable.album_jazz_blues,
+ "album_jazz_blues");
+ createMediaMetadataCompat(
+ "The_Coldest_Shoulder",
+ "The Coldest Shoulder",
+ "The 126ers",
+ "Youtube Audio Library Rock 2",
+ "Rock",
+ 160,
+ TimeUnit.SECONDS,
+ "the_coldest_shoulder.mp3",
+ R.drawable.album_youtube_audio_library_rock_2,
+ "album_youtube_audio_library_rock_2");
+ }
+
+ public static String getRoot() {
+ return "root";
+ }
+
+ private static String getAlbumArtUri(String albumArtResName) {
+ return ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+ BuildConfig.APPLICATION_ID + "/drawable/" + albumArtResName;
+ }
+
+ public static String getMusicFilename(String mediaId) {
+ return musicFileName.containsKey(mediaId) ? musicFileName.get(mediaId) : null;
+ }
+
+ private static int getAlbumRes(String mediaId) {
+ return albumRes.containsKey(mediaId) ? albumRes.get(mediaId) : 0;
+ }
+
+ public static Bitmap getAlbumBitmap(Context context, String mediaId) {
+ return BitmapFactory.decodeResource(context.getResources(),
+ MusicLibrary.getAlbumRes(mediaId));
+ }
+
+ public static List<MediaBrowserCompat.MediaItem> getMediaItems() {
+ List<MediaBrowserCompat.MediaItem> result = new ArrayList<>();
+ for (MediaMetadataCompat metadata : music.values()) {
+ result.add(
+ new MediaBrowserCompat.MediaItem(
+ metadata.getDescription(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE));
+ }
+ return result;
+ }
+
+ public static MediaMetadataCompat getMetadata(Context context, String mediaId) {
+ MediaMetadataCompat metadataWithoutBitmap = music.get(mediaId);
+ Bitmap albumArt = getAlbumBitmap(context, mediaId);
+
+ // Since MediaMetadataCompat is immutable, we need to create a copy to set the album art.
+ // We don't set it initially on all items so that they don't take unnecessary memory.
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ for (String key :
+ new String[]{
+ MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
+ MediaMetadataCompat.METADATA_KEY_ALBUM,
+ MediaMetadataCompat.METADATA_KEY_ARTIST,
+ MediaMetadataCompat.METADATA_KEY_GENRE,
+ MediaMetadataCompat.METADATA_KEY_TITLE
+ }) {
+ builder.putString(key, metadataWithoutBitmap.getString(key));
+ }
+ builder.putLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION,
+ metadataWithoutBitmap.getLong(MediaMetadataCompat.METADATA_KEY_DURATION));
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt);
+ return builder.build();
+ }
+
+ private static void createMediaMetadataCompat(
+ String mediaId,
+ String title,
+ String artist,
+ String album,
+ String genre,
+ long duration,
+ TimeUnit durationUnit,
+ String musicFilename,
+ int albumArtResId,
+ String albumArtResName) {
+ music.put(
+ mediaId,
+ new MediaMetadataCompat.Builder()
+ .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+ TimeUnit.MILLISECONDS.convert(duration, durationUnit))
+ .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
+ .putString(
+ MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
+ getAlbumArtUri(albumArtResName))
+ .putString(
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
+ getAlbumArtUri(albumArtResName))
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
+ .build());
+ albumRes.put(mediaId, albumArtResId);
+ musicFileName.put(mediaId, musicFilename);
+ }
+}
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/notifications/MediaNotificationManager.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/notifications/MediaNotificationManager.java
new file mode 100644
index 0000000..0274494
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/notifications/MediaNotificationManager.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2017 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.mediasession.service.notifications;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.app.NotificationCompat.MediaStyle;
+import android.support.v4.media.session.MediaButtonReceiver;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v4.os.BuildCompat;
+import android.util.Log;
+
+import com.example.android.mediasession.R;
+import com.example.android.mediasession.service.MusicService;
+import com.example.android.mediasession.service.PlaybackInfoListener;
+import com.example.android.mediasession.service.contentcatalogs.MusicLibrary;
+import com.example.android.mediasession.ui.MainActivity;
+
+
+/**
+ * Keeps track of a notification and updates it automatically for a given MediaSession. This is
+ * required so that the music service don't get killed during playback.
+ */
+public class MediaNotificationManager {
+
+ public static final int NOTIFICATION_ID = 412;
+
+ private static final String TAG = MediaNotificationManager.class.getSimpleName();
+ private static final String CHANNEL_ID = "com.example.android.musicplayer.channel";
+ private static final int REQUEST_CODE = 501;
+
+ private final MusicService mService;
+
+ private final NotificationCompat.Action mPlayAction;
+ private final NotificationCompat.Action mPauseAction;
+ private final NotificationCompat.Action mNextAction;
+ private final NotificationCompat.Action mPrevAction;
+ private final NotificationManager mNotificationManager;
+
+ public MediaNotificationManager(MusicService service) {
+ mService = service;
+
+ mNotificationManager =
+ (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ mPlayAction =
+ new NotificationCompat.Action(
+ R.drawable.ic_play_arrow_white_24dp,
+ mService.getString(R.string.label_play),
+ MediaButtonReceiver.buildMediaButtonPendingIntent(
+ mService,
+ PlaybackStateCompat.ACTION_PLAY));
+ mPauseAction =
+ new NotificationCompat.Action(
+ R.drawable.ic_pause_white_24dp,
+ mService.getString(R.string.label_pause),
+ MediaButtonReceiver.buildMediaButtonPendingIntent(
+ mService,
+ PlaybackStateCompat.ACTION_PAUSE));
+ mNextAction =
+ new NotificationCompat.Action(
+ R.drawable.ic_skip_next_white_24dp,
+ mService.getString(R.string.label_next),
+ MediaButtonReceiver.buildMediaButtonPendingIntent(
+ mService,
+ PlaybackStateCompat.ACTION_SKIP_TO_NEXT));
+ mPrevAction =
+ new NotificationCompat.Action(
+ R.drawable.ic_skip_previous_white_24dp,
+ mService.getString(R.string.label_previous),
+ MediaButtonReceiver.buildMediaButtonPendingIntent(
+ mService,
+ PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS));
+
+ // Cancel all notifications to handle the case where the Service was killed and
+ // restarted by the system.
+ mNotificationManager.cancelAll();
+ }
+
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy: ");
+ }
+
+ public NotificationManager getNotificationManager() {
+ return mNotificationManager;
+ }
+
+ public Notification getNotification(MediaMetadataCompat metadata,
+ @NonNull PlaybackStateCompat state,
+ MediaSessionCompat.Token token) {
+ boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING;
+ MediaDescriptionCompat description = metadata.getDescription();
+ NotificationCompat.Builder builder =
+ buildNotification(state, token, isPlaying, description);
+ return builder.build();
+ }
+
+ private NotificationCompat.Builder buildNotification(@NonNull PlaybackStateCompat state,
+ MediaSessionCompat.Token token,
+ boolean isPlaying,
+ MediaDescriptionCompat description) {
+
+ // Create the (mandatory) notification channel when running on Android Oreo.
+ if (isAndroidOOrHigher()) {
+ createChannel();
+ }
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(mService, CHANNEL_ID);
+ builder.setStyle(
+ new MediaStyle()
+ .setMediaSession(token)
+ .setShowActionsInCompactView(0, 1, 2)
+ // For backwards compatibility with Android L and earlier.
+ .setShowCancelButton(true)
+ .setCancelButtonIntent(
+ MediaButtonReceiver.buildMediaButtonPendingIntent(
+ mService,
+ PlaybackStateCompat.ACTION_STOP)))
+ .setColor(ContextCompat.getColor(mService, R.color.notification_bg))
+ .setSmallIcon(R.drawable.ic_stat_image_audiotrack)
+ // Pending intent that is fired when user clicks on notification.
+ .setContentIntent(createContentIntent())
+ // Title - Usually Song name.
+ .setContentTitle(description.getTitle())
+ // Subtitle - Usually Artist name.
+ .setContentText(description.getSubtitle())
+ .setLargeIcon(MusicLibrary.getAlbumBitmap(mService, description.getMediaId()))
+ // When notification is deleted (when playback is paused and notification can be
+ // deleted) fire MediaButtonPendingIntent with ACTION_STOP.
+ .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(
+ mService, PlaybackStateCompat.ACTION_STOP))
+ // Show controls on lock screen even when user hides sensitive content.
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+
+ // If skip to next action is enabled.
+ if ((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) {
+ builder.addAction(mPrevAction);
+ }
+
+ builder.addAction(isPlaying ? mPauseAction : mPlayAction);
+
+ // If skip to prev action is enabled.
+ if ((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) {
+ builder.addAction(mNextAction);
+ }
+
+ return builder;
+ }
+
+ // Does nothing on versions of Android earlier than O.
+ @RequiresApi(Build.VERSION_CODES.O)
+ private void createChannel() {
+ if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) {
+ // The user-visible name of the channel.
+ CharSequence name = "MediaSession";
+ // The user-visible description of the channel.
+ String description = "MediaSession and MediaPlayer";
+ int importance = NotificationManager.IMPORTANCE_LOW;
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
+ // Configure the notification channel.
+ mChannel.setDescription(description);
+ mChannel.enableLights(true);
+ // Sets the notification light color for notifications posted to this
+ // channel, if the device supports this feature.
+ mChannel.setLightColor(Color.RED);
+ mChannel.enableVibration(true);
+ mChannel.setVibrationPattern(
+ new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
+ mNotificationManager.createNotificationChannel(mChannel);
+ Log.d(TAG, "createChannel: New channel created");
+ } else {
+ Log.d(TAG, "createChannel: Existing channel reused");
+ }
+ }
+
+ private boolean isAndroidOOrHigher() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ }
+
+ private PendingIntent createContentIntent() {
+ Intent openUI = new Intent(mService, MainActivity.class);
+ openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ return PendingIntent.getActivity(
+ mService, REQUEST_CODE, openUI, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+}
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/players/MediaPlayerAdapter.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/players/MediaPlayerAdapter.java
new file mode 100644
index 0000000..d24f44f
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/service/players/MediaPlayerAdapter.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2017 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.mediasession.service.players;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaPlayer;
+import android.os.SystemClock;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.example.android.mediasession.service.PlaybackInfoListener;
+import com.example.android.mediasession.service.PlayerAdapter;
+import com.example.android.mediasession.service.contentcatalogs.MusicLibrary;
+import com.example.android.mediasession.ui.MainActivity;
+
+/**
+ * Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter}
+ * so that {@link MainActivity} can control music playback.
+ */
+public final class MediaPlayerAdapter extends PlayerAdapter {
+
+ private final Context mContext;
+ private MediaPlayer mMediaPlayer;
+ private String mFilename;
+ private PlaybackInfoListener mPlaybackInfoListener;
+ private MediaMetadataCompat mCurrentMedia;
+ private int mState;
+ private boolean mCurrentMediaPlayedToCompletion;
+
+ // Work-around for a MediaPlayer bug related to the behavior of MediaPlayer.seekTo()
+ // while not playing.
+ private int mSeekWhileNotPlaying = -1;
+
+ public MediaPlayerAdapter(Context context, PlaybackInfoListener listener) {
+ super(context);
+ mContext = context.getApplicationContext();
+ mPlaybackInfoListener = listener;
+ }
+
+ /**
+ * Once the {@link MediaPlayer} is released, it can't be used again, and another one has to be
+ * created. In the onStop() method of the {@link MainActivity} the {@link MediaPlayer} is
+ * released. Then in the onStart() of the {@link MainActivity} a new {@link MediaPlayer}
+ * object has to be created. That's why this method is private, and called by load(int) and
+ * not the constructor.
+ */
+ private void initializeMediaPlayer() {
+ if (mMediaPlayer == null) {
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ mPlaybackInfoListener.onPlaybackCompleted();
+
+ // Set the state to "paused" because it most closely matches the state
+ // in MediaPlayer with regards to available state transitions compared
+ // to "stop".
+ // Paused allows: seekTo(), start(), pause(), stop()
+ // Stop allows: stop()
+ setNewState(PlaybackStateCompat.STATE_PAUSED);
+ }
+ });
+ }
+ }
+
+ // Implements PlaybackControl.
+ @Override
+ public void playFromMedia(MediaMetadataCompat metadata) {
+ mCurrentMedia = metadata;
+ final String mediaId = metadata.getDescription().getMediaId();
+ playFile(MusicLibrary.getMusicFilename(mediaId));
+ }
+
+ @Override
+ public MediaMetadataCompat getCurrentMedia() {
+ return mCurrentMedia;
+ }
+
+ private void playFile(String filename) {
+ boolean mediaChanged = (mFilename == null || !filename.equals(mFilename));
+ if (mCurrentMediaPlayedToCompletion) {
+ // Last audio file was played to completion, the resourceId hasn't changed, but the
+ // player was released, so force a reload of the media file for playback.
+ mediaChanged = true;
+ mCurrentMediaPlayedToCompletion = false;
+ }
+ if (!mediaChanged) {
+ if (!isPlaying()) {
+ play();
+ }
+ return;
+ } else {
+ release();
+ }
+
+ mFilename = filename;
+
+ initializeMediaPlayer();
+
+ try {
+ AssetFileDescriptor assetFileDescriptor = mContext.getAssets().openFd(mFilename);
+ mMediaPlayer.setDataSource(
+ assetFileDescriptor.getFileDescriptor(),
+ assetFileDescriptor.getStartOffset(),
+ assetFileDescriptor.getLength());
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to open file: " + mFilename, e);
+ }
+
+ try {
+ mMediaPlayer.prepare();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to open file: " + mFilename, e);
+ }
+
+ play();
+ }
+
+ @Override
+ public void onStop() {
+ // Regardless of whether or not the MediaPlayer has been created / started, the state must
+ // be updated, so that MediaNotificationManager can take down the notification.
+ setNewState(PlaybackStateCompat.STATE_STOPPED);
+ release();
+ }
+
+ private void release() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return mMediaPlayer != null && mMediaPlayer.isPlaying();
+ }
+
+ @Override
+ protected void onPlay() {
+ if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
+ mMediaPlayer.start();
+ setNewState(PlaybackStateCompat.STATE_PLAYING);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ setNewState(PlaybackStateCompat.STATE_PAUSED);
+ }
+ }
+
+ // This is the main reducer for the player state machine.
+ private void setNewState(@PlaybackStateCompat.State int newPlayerState) {
+ mState = newPlayerState;
+
+ // Whether playback goes to completion, or whether it is stopped, the
+ // mCurrentMediaPlayedToCompletion is set to true.
+ if (mState == PlaybackStateCompat.STATE_STOPPED) {
+ mCurrentMediaPlayedToCompletion = true;
+ }
+
+ // Work around for MediaPlayer.getCurrentPosition() when it changes while not playing.
+ final long reportPosition;
+ if (mSeekWhileNotPlaying >= 0) {
+ reportPosition = mSeekWhileNotPlaying;
+
+ if (mState == PlaybackStateCompat.STATE_PLAYING) {
+ mSeekWhileNotPlaying = -1;
+ }
+ } else {
+ reportPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
+ }
+
+ final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder();
+ stateBuilder.setActions(getAvailableActions());
+ stateBuilder.setState(mState,
+ reportPosition,
+ 1.0f,
+ SystemClock.elapsedRealtime());
+ mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build());
+ }
+
+ /**
+ * Set the current capabilities available on this session. Note: If a capability is not
+ * listed in the bitmask of capabilities then the MediaSession will not handle it. For
+ * example, if you don't want ACTION_STOP to be handled by the MediaSession, then don't
+ * included it in the bitmask that's returned.
+ */
+ @PlaybackStateCompat.Actions
+ private long getAvailableActions() {
+ long actions = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
+ | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ switch (mState) {
+ case PlaybackStateCompat.STATE_STOPPED:
+ actions |= PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE;
+ break;
+ case PlaybackStateCompat.STATE_PLAYING:
+ actions |= PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_SEEK_TO;
+ break;
+ case PlaybackStateCompat.STATE_PAUSED:
+ actions |= PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_STOP;
+ break;
+ default:
+ actions |= PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_PAUSE;
+ }
+ return actions;
+ }
+
+ @Override
+ public void seekTo(long position) {
+ if (mMediaPlayer != null) {
+ if (!mMediaPlayer.isPlaying()) {
+ mSeekWhileNotPlaying = (int) position;
+ }
+ mMediaPlayer.seekTo((int) position);
+
+ // Set the state (to the current state) because the position changed and should
+ // be reported to clients.
+ setNewState(mState);
+ }
+ }
+
+ @Override
+ public void setVolume(float volume) {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.setVolume(volume, volume);
+ }
+ }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/ui/MainActivity.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/ui/MainActivity.java
new file mode 100644
index 0000000..4991789
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/ui/MainActivity.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017 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.mediasession.ui;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.example.android.mediasession.R;
+import com.example.android.mediasession.client.MediaBrowserAdapter;
+import com.example.android.mediasession.service.contentcatalogs.MusicLibrary;
+
+public class MainActivity extends AppCompatActivity {
+
+ private ImageView mAlbumArt;
+ private TextView mTitleTextView;
+ private TextView mArtistTextView;
+ private ImageView mMediaControlsImage;
+ private MediaSeekBar mSeekBarAudio;
+
+ private MediaBrowserAdapter mMediaBrowserAdapter;
+
+ private boolean mIsPlaying;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ initializeUI();
+ mMediaBrowserAdapter = new MediaBrowserAdapter(this);
+ mMediaBrowserAdapter.addListener(new MediaBrowserListener());
+ }
+
+ private void initializeUI() {
+ mTitleTextView = (TextView) findViewById(R.id.song_title);
+ mArtistTextView = (TextView) findViewById(R.id.song_artist);
+ mAlbumArt = (ImageView) findViewById(R.id.album_art);
+ mMediaControlsImage = (ImageView) findViewById(R.id.media_controls);
+ mSeekBarAudio = (MediaSeekBar) findViewById(R.id.seekbar_audio);
+
+ final Button buttonPrevious = (Button) findViewById(R.id.button_previous);
+ buttonPrevious.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mMediaBrowserAdapter.getTransportControls().skipToPrevious();
+ }
+ });
+
+ final Button buttonPlay = (Button) findViewById(R.id.button_play);
+ buttonPlay.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mIsPlaying) {
+ mMediaBrowserAdapter.getTransportControls().pause();
+ } else {
+ mMediaBrowserAdapter.getTransportControls().play();
+ }
+ }
+ });
+
+ final Button buttonNext = (Button) findViewById(R.id.button_next);
+ buttonNext.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mMediaBrowserAdapter.getTransportControls().skipToNext();
+ }
+ });
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mMediaBrowserAdapter.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mSeekBarAudio.disconnectController();
+ mMediaBrowserAdapter.onStop();
+ }
+
+ private class MediaBrowserListener extends MediaBrowserAdapter.MediaBrowserChangeListener {
+
+ @Override
+ public void onConnected(@Nullable MediaControllerCompat mediaController) {
+ super.onConnected(mediaController);
+ mSeekBarAudio.setMediaController(mediaController);
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat playbackState) {
+ mIsPlaying = playbackState != null &&
+ playbackState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ mMediaControlsImage.setPressed(mIsPlaying);
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat mediaMetadata) {
+ if (mediaMetadata == null) {
+ return;
+ }
+ mTitleTextView.setText(
+ mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
+ mArtistTextView.setText(
+ mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
+ mAlbumArt.setImageBitmap(MusicLibrary.getAlbumBitmap(
+ MainActivity.this,
+ mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)));
+ }
+ }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/ui/MediaSeekBar.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/ui/MediaSeekBar.java
new file mode 100644
index 0000000..71de9ca
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediasession/ui/MediaSeekBar.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2017 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.mediasession.ui;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.support.v4.media.MediaMetadataCompat;
+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.widget.AppCompatSeekBar;
+import android.util.AttributeSet;
+import android.view.animation.LinearInterpolator;
+import android.widget.SeekBar;
+
+/**
+ * SeekBar that can be used with a {@link MediaSessionCompat} to track and seek in playing
+ * media.
+ */
+
+public class MediaSeekBar extends AppCompatSeekBar {
+ private MediaControllerCompat mMediaController;
+ private ControllerCallback mControllerCallback;
+
+ private boolean mIsTracking = false;
+ private OnSeekBarChangeListener mOnSeekBarChangeListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mIsTracking = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mMediaController.getTransportControls().seekTo(getProgress());
+ mIsTracking = false;
+ }
+ };
+ private ValueAnimator mProgressAnimator;
+
+ public MediaSeekBar(Context context) {
+ super(context);
+ super.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
+ }
+
+ public MediaSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ super.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
+ }
+
+ public MediaSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ super.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
+ }
+
+ @Override
+ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
+ // Prohibit adding seek listeners to this subclass.
+ }
+
+ public void setMediaController(final MediaControllerCompat mediaController) {
+ if (mediaController != null) {
+ mControllerCallback = new ControllerCallback();
+ mediaController.registerCallback(mControllerCallback);
+ } else if (mMediaController != null) {
+ mMediaController.unregisterCallback(mControllerCallback);
+ mControllerCallback = null;
+ }
+ mMediaController = mediaController;
+ }
+
+ public void disconnectController() {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mControllerCallback);
+ mControllerCallback = null;
+ mMediaController = null;
+ }
+ }
+
+ private class ControllerCallback
+ extends MediaControllerCompat.Callback
+ implements ValueAnimator.AnimatorUpdateListener {
+
+ @Override
+ public void onSessionDestroyed() {
+ super.onSessionDestroyed();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ super.onPlaybackStateChanged(state);
+
+ // If there's an ongoing animation, stop it now.
+ if (mProgressAnimator != null) {
+ mProgressAnimator.cancel();
+ mProgressAnimator = null;
+ }
+
+ final int progress = state != null
+ ? (int) state.getPosition()
+ : 0;
+ setProgress(progress);
+
+ // If the media is playing then the seekbar should follow it, and the easiest
+ // way to do that is to create a ValueAnimator to update it so the bar reaches
+ // the end of the media the same time as playback gets there (or close enough).
+ if (state != null && state.getState() == PlaybackStateCompat.STATE_PLAYING) {
+ final int timeToEnd = (int) ((getMax() - progress) / state.getPlaybackSpeed());
+
+ mProgressAnimator = ValueAnimator.ofInt(progress, getMax())
+ .setDuration(timeToEnd);
+ mProgressAnimator.setInterpolator(new LinearInterpolator());
+ mProgressAnimator.addUpdateListener(this);
+ mProgressAnimator.start();
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ super.onMetadataChanged(metadata);
+
+ final int max = metadata != null
+ ? (int) metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
+ : 0;
+ setProgress(0);
+ setMax(max);
+ }
+
+ @Override
+ public void onAnimationUpdate(final ValueAnimator valueAnimator) {
+ // If the user is changing the slider, cancel the animation.
+ if (mIsTracking) {
+ valueAnimator.cancel();
+ return;
+ }
+
+ final int animatedIntValue = (int) valueAnimator.getAnimatedValue();
+ setProgress(animatedIntValue);
+ }
+ }
+}
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index 05ef6f6..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png
deleted file mode 100644
index a8cba40..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_pause_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_pause_white_24dp.png
index b4bdbb5..f7b3c1e 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_pause_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_pause_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png
index 164385d..be73548 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_shuffle_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_shuffle_white_24dp.png
deleted file mode 100644
index 3eeb0ef..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_shuffle_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png
index 4eaf7ca..3c6ae11 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png
index e59dedb..6883e61 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png
new file mode 100755
index 0000000..bbbaa0e
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index f894fb8..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_pause_white_24dp.png
new file mode 100644
index 0000000..499341f
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_pause_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png
new file mode 100644
index 0000000..bebdf37
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png
new file mode 100644
index 0000000..d55f7e6
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png
new file mode 100644
index 0000000..259797d
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png
new file mode 100755
index 0000000..e445a0c
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-nodpi/album_jazz_blues.jpg b/media/MediaBrowserService/Application/src/main/res/drawable-nodpi/album_jazz_blues.jpg
new file mode 100644
index 0000000..d3ffefc
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-nodpi/album_jazz_blues.jpg
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg b/media/MediaBrowserService/Application/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg
new file mode 100644
index 0000000..8d5020c
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png
deleted file mode 100644
index dbba844..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 43ade5e..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png
index 14b6d17..e31b5b0 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png
index a55d199..929ffec 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png
deleted file mode 100644
index 8ce3a60..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png
index f282b92..343b7bb 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png
index 2522877..e926c5e 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png
new file mode 100755
index 0000000..5481659
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_by_genre.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_by_genre.png
deleted file mode 100644
index da3b4a7..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_by_genre.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_default_art.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_default_art.png
deleted file mode 100644
index dfb9e67..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_default_art.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png
deleted file mode 100644
index b82a8d9..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_launcher.png
deleted file mode 100644
index 3058c27..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_media_with_pause.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_media_with_pause.png
new file mode 100644
index 0000000..c6041da
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_media_with_pause.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_media_with_play.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_media_with_play.png
new file mode 100644
index 0000000..a0da57b
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_media_with_play.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png
index 72dfa9f..1e71ba3 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png
index 043acd8..4abfe88 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png
deleted file mode 100644
index 718b6b5..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png
index 4fe6088..616e47f 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png
index 2c9310a..1ec9d65 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_star_off.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_star_off.png
deleted file mode 100644
index fb7afb0..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_star_off.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_star_on.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_star_on.png
deleted file mode 100644
index 6f7fc75..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_star_on.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png
new file mode 100755
index 0000000..f214042
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-xxxhdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/drawable-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 6b4e4a2..0000000
--- a/media/MediaBrowserService/Application/src/main/res/drawable-xxxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable/ic_play_pause_toggle.xml b/media/MediaBrowserService/Application/src/main/res/drawable/ic_play_pause_toggle.xml
new file mode 100644
index 0000000..2fe59d9
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/drawable/ic_play_pause_toggle.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_media_with_play" android:state_pressed="false" />
+ <item android:drawable="@drawable/ic_media_with_pause" />
+</selector>
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/res/layout/activity_main.xml b/media/MediaBrowserService/Application/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..76b8699
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/layout/activity_main.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 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.
+ -->
+
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/frameLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:background="@drawable/album_jazz_blues">
+
+ <ImageView
+ android:id="@+id/album_art"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitXY"
+ tools:ignore="ContentDescription" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:background="#a0ffffff"
+ app:layout_constraintBottom_toBottomOf="@+id/song_artist"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/song_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="0dp"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ app:layout_constraintBottom_toBottomOf="@+id/song_artist"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Song Title" />
+
+ <TextView
+ android:id="@+id/song_artist"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingBottom="12dp"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/song_title"
+ tools:text="Song Artist" />
+
+ <ImageView
+ android:id="@+id/media_controls"
+ android:layout_width="192dp"
+ android:layout_height="192dp"
+ android:alpha=".9"
+ android:src="@drawable/ic_play_pause_toggle"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/guideline"
+ tools:ignore="ContentDescription" />
+
+ <Button
+ android:id="@+id/button_previous"
+ android:layout_width="50dp"
+ android:layout_height="0dp"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/label_previous"
+ android:text=""
+ app:layout_constraintBottom_toBottomOf="@+id/media_controls"
+ app:layout_constraintStart_toStartOf="@+id/media_controls"
+ app:layout_constraintTop_toTopOf="@+id/media_controls" />
+
+ <Button
+ android:id="@+id/button_play"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/label_play_pause"
+ android:text=""
+ app:layout_constraintBottom_toBottomOf="@+id/media_controls"
+ app:layout_constraintEnd_toStartOf="@+id/button_next"
+ app:layout_constraintStart_toEndOf="@+id/button_previous"
+ app:layout_constraintTop_toTopOf="@+id/media_controls" />
+
+ <Button
+ android:id="@+id/button_next"
+ android:layout_width="50dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/label_next"
+ android:text=""
+ app:layout_constraintBottom_toBottomOf="@+id/media_controls"
+ app:layout_constraintEnd_toEndOf="@+id/media_controls"
+ app:layout_constraintTop_toTopOf="@+id/media_controls" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent=".5" />
+
+ <com.example.android.mediasession.ui.MediaSeekBar
+ android:id="@+id/seekbar_audio"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="16dp"
+ android:paddingBottom="16dp"
+ android:paddingTop="16dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml b/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml
deleted file mode 100644
index 64b47ee..0000000
--- a/media/MediaBrowserService/Application/src/main/res/layout/activity_player.xml
+++ /dev/null
@@ -1,22 +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.
- -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".MusicPlayerActivity"
- 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
deleted file mode 100644
index 8c5985b..0000000
--- a/media/MediaBrowserService/Application/src/main/res/layout/fragment_list.xml
+++ /dev/null
@@ -1,29 +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.
- -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:colorBackground"
- android:orientation="vertical"
- android:padding="@dimen/fragment_list_padding">
-
- <ListView
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- </ListView>
-
-</LinearLayout>
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
deleted file mode 100644
index c10ac51..0000000
--- a/media/MediaBrowserService/Application/src/main/res/layout/media_list_item.xml
+++ /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.
- -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:listPreferredItemHeight"
- android:orientation="horizontal">
-
- <ImageView
- android:id="@+id/play_eq"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:contentDescription="@string/play_item"
- android:src="@drawable/ic_play_arrow_white_24dp"/>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:mode="twoLine"
- android:orientation="vertical"
- android:padding="@dimen/list_item_padding">
-
- <TextView
- android:id="@+id/title"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/margin_text_view"
- android:layout_marginTop="@dimen/margin_text_view"
- android:textAppearance="?android:attr/textAppearanceMedium"/>
-
- <TextView
- android:id="@+id/description"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/margin_text_view"
- android:layout_marginTop="@dimen/margin_text_view"
- android:textAppearance="?android:attr/textAppearanceSmall"/>
-
- </LinearLayout>
-
-</LinearLayout>
diff --git a/media/MediaBrowserService/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..1d1b1a0
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..11570be
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..dc44e16
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..5826256
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/media/MediaBrowserService/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..b226a47
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/values/colors.xml b/media/MediaBrowserService/Application/src/main/res/values/colors.xml
new file mode 100644
index 0000000..323a89b
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/values/colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+ <color name="notification_bg">#e91e63</color>
+</resources>
diff --git a/media/MediaBrowserService/Application/src/main/res/values/dimens.xml b/media/MediaBrowserService/Application/src/main/res/values/dimens.xml
deleted file mode 100644
index e57a8c9..0000000
--- a/media/MediaBrowserService/Application/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,21 +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.
- -->
-<resources>
- <dimen name="fragment_list_padding">16dp</dimen>
- <dimen name="list_item_padding">4dp</dimen>
- <dimen name="margin_text_view">6dp</dimen>
-</resources>
diff --git a/media/MediaBrowserService/Application/src/main/res/values/strings.xml b/media/MediaBrowserService/Application/src/main/res/values/strings.xml
index 9a4a50a..93b3eac 100644
--- a/media/MediaBrowserService/Application/src/main/res/values/strings.xml
+++ b/media/MediaBrowserService/Application/src/main/res/values/strings.xml
@@ -1,34 +1,25 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2014 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
-->
+
<resources>
-
<string name="app_name">MediaBrowserService Sample</string>
- <string name="favorite">Favorite</string>
- <string name="error_no_metadata">Unable to retrieve metadata.</string>
- <string name="browse_genres">Genres</string>
- <string name="browse_genre_subtitle">Songs by genre</string>
- <string name="browse_musics_by_genre_subtitle">%1$s songs</string>
- <string name="random_queue_title">Random music</string>
- <string name="error_cannot_skip">Cannot skip</string>
- <string name="error_loading_media">Error Loading Media</string>
- <string name="play_item">Play item</string>
- <string name="skip_previous">Skip to previous</string>
- <string name="play_pause">play or pause</string>
- <string name="skip_next">Skip to next</string>
- <string name="no_search_results">No search results.</string>
-
+ <string name="label_stop">Stop</string>
+ <string name="label_pause">Pause</string>
+ <string name="label_play">Play</string>
+ <string name="label_play_pause">Play and pause toggle</string>
+ <string name="label_previous">Previous track</string>
+ <string name="label_next">Next track</string>
</resources>
diff --git a/media/MediaBrowserService/Application/src/main/res/values/strings_notifications.xml b/media/MediaBrowserService/Application/src/main/res/values/strings_notifications.xml
deleted file mode 100644
index 7e1ccd5..0000000
--- a/media/MediaBrowserService/Application/src/main/res/values/strings_notifications.xml
+++ /dev/null
@@ -1,24 +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.
- -->
-<resources>
-
- <string name="label_pause">Pause</string>
- <string name="label_play">Play</string>
- <string name="label_previous">Previous</string>
- <string name="label_next">Next</string>
-
-</resources>
diff --git a/media/MediaBrowserService/Application/src/main/res/values/styles.xml b/media/MediaBrowserService/Application/src/main/res/values/styles.xml
index 5d91ab4..69ee900 100644
--- a/media/MediaBrowserService/Application/src/main/res/values/styles.xml
+++ b/media/MediaBrowserService/Application/src/main/res/values/styles.xml
@@ -1,45 +1,27 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2014 The Android Open Source Project
+ ~ Copyright 2017 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.
+ -->
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
<resources>
- <color name="colorPrimary">#ffff5722</color>
- <color name="colorPrimaryDark">#ffbf360c</color>
- <color name="colorAccent">#ffff5722</color>
- <style name="AppTheme" parent="Theme.AppCompat">
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
- <style name="CarTheme" parent="AppTheme">
- <!-- colorPrimaryDark is currently used in Android Auto for:
- - App background
- - Drawer right side ("more" custom actions) background
- - Notification icon badge tinting
- - Overview “now playing” icon tinting
- -->
- <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="colorAccent">#ffff5722</item>
- </style>
-
-</resources>
\ No newline at end of file
+</resources>
diff --git a/media/MediaBrowserService/Application/src/main/res/xml/automotive_app_desc.xml b/media/MediaBrowserService/Application/src/main/res/xml/automotive_app_desc.xml
deleted file mode 100644
index a84750b..0000000
--- a/media/MediaBrowserService/Application/src/main/res/xml/automotive_app_desc.xml
+++ /dev/null
@@ -1,19 +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.
- -->
-<automotiveApp>
- <uses name="media"/>
-</automotiveApp>
diff --git a/media/MediaBrowserService/CONTRIB.md b/media/MediaBrowserService/CONTRIB.md
deleted file mode 100644
index 14a4fcf..0000000
--- a/media/MediaBrowserService/CONTRIB.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# How to become a contributor and submit your own code
-
-## Contributor License Agreements
-
-We'd love to accept your sample apps and patches! Before we can take them, we
-have to jump a couple of legal hurdles.
-
-Please fill out either the individual or corporate Contributor License Agreement (CLA).
-
- * If you are an individual writing original source code and you're sure you
- own the intellectual property, then you'll need to sign an [individual CLA]
- (https://developers.google.com/open-source/cla/individual).
- * If you work for a company that wants to allow you to contribute your work,
- then you'll need to sign a [corporate CLA]
- (https://developers.google.com/open-source/cla/corporate).
-
-Follow either of the two links above to access the appropriate CLA and
-instructions for how to sign and return it. Once we receive it, we'll be able to
-accept your pull requests.
-
-## Contributing A Patch
-
-1. Submit an issue describing your proposed change to the repo in question.
-1. The repo owner will respond to your issue promptly.
-1. If your proposed change is accepted, and you haven't already done so, sign a
- Contributor License Agreement (see details above).
-1. Fork the desired repo, develop and test your code changes.
-1. Ensure that your code adheres to the existing style in the sample to which
- you are contributing. Refer to the
- [Android Code Style Guide]
- (https://source.android.com/source/code-style.html) for the
- recommended coding standards for this organization.
-1. Ensure that your code has an appropriate set of unit tests which all pass.
-1. Submit a pull request.
-
diff --git a/media/MediaBrowserService/README.md b/media/MediaBrowserService/README.md
index 1f4333a..2deee4e 100644
--- a/media/MediaBrowserService/README.md
+++ b/media/MediaBrowserService/README.md
@@ -2,16 +2,22 @@
Android MediaBrowserService Sample
===================================
-This sample shows how to implement an audio media app that provides
-media library metadata and playback controls through a standard
-service. It exposes a simple music library through the new
-MediaBrowserService and provides MediaSession callbacks. This allows
-it to be used in Android Auto, for example.
-When not connected to a car, the app has a very simple UI that browses
-the media library and provides simple playback controls. When
-connected to Android Auto, the same service provides data and callback
-to the Android Auto UI in the same manner as it provides them to the
-local UI.
+This sample shows how to implement a media app that allows
+background playback of audio, and provide a media library
+that is exposed to other apps.
+1. It allows other apps control media playback externally
+using MediaSession. This allows playback to be controlled by
+the Google Assistant, for example.
+2. It exposes a simple music library through MediaBrowserService.
+And it provides MediaSession callbacks. This allows it to be used
+by Android Auto, for example.
+When not connected to a car, the app has a very simple UI that
+allows for playback as well as skip to previous and next tracks.
+To learn more about MediaSession and MediaBrowserService, read
+this [article on Medium](https://medium.com/google-developers/understanding-mediasession-part-4-4-dcc77c535f99)
+that goes into the architectural details of these APIs.
+
+<img src="screenshots/architecture.png" height="400" alt="Architecture Diagram"/>
Introduction
------------
@@ -72,14 +78,14 @@
Pre-requisites
--------------
-- Android SDK 25
-- Android Build Tools v25.0.2
+- Android SDK 26
+- Android Build Tools v26.0.1
- Android Support Repository
Screenshots
-------------
-<img src="screenshots/1-main.png" height="400" alt="Screenshot"/> <img src="screenshots/2-music-play.png" height="400" alt="Screenshot"/> <img src="screenshots/3-music-notification.png" height="400" alt="Screenshot"/>
+<img src="screenshots/1-main.png" height="400" alt="Screenshot"/> <img src="screenshots/2-notification.png" height="400" alt="Screenshot"/>
Getting Started
---------------
diff --git a/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties b/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
index fdc22bf..a93175f 100644
--- a/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Mar 22 15:15:46 PDT 2017
+#Tue Sep 19 11:11:29 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/media/MediaBrowserService/packaging.yaml b/media/MediaBrowserService/packaging.yaml
index bcd9ac5..b1d7bbd 100644
--- a/media/MediaBrowserService/packaging.yaml
+++ b/media/MediaBrowserService/packaging.yaml
@@ -10,6 +10,6 @@
languages: [Java]
solutions: [Mobile]
github: googlesamples/android-MediaBrowserService
-level: BEGINNER
-icon: MediaBrowserServiceSample/src/main/res/drawable-xxhdpi/ic_launcher.png
+level: ADVANCED
+icon: MediaBrowserServiceSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
license: apache2-android
diff --git a/media/MediaBrowserService/screenshots/1-main.png b/media/MediaBrowserService/screenshots/1-main.png
index 1b17d0e..f5e08a2 100644
--- a/media/MediaBrowserService/screenshots/1-main.png
+++ b/media/MediaBrowserService/screenshots/1-main.png
Binary files differ
diff --git a/media/MediaBrowserService/screenshots/2-music-play.png b/media/MediaBrowserService/screenshots/2-music-play.png
deleted file mode 100644
index 1c1439c..0000000
--- a/media/MediaBrowserService/screenshots/2-music-play.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/screenshots/2-notification.png b/media/MediaBrowserService/screenshots/2-notification.png
new file mode 100644
index 0000000..92ed70e
--- /dev/null
+++ b/media/MediaBrowserService/screenshots/2-notification.png
Binary files differ
diff --git a/media/MediaBrowserService/screenshots/3-music-notification.png b/media/MediaBrowserService/screenshots/3-music-notification.png
deleted file mode 100644
index c86c045..0000000
--- a/media/MediaBrowserService/screenshots/3-music-notification.png
+++ /dev/null
Binary files differ
diff --git a/media/MediaBrowserService/screenshots/architecture.png b/media/MediaBrowserService/screenshots/architecture.png
new file mode 100644
index 0000000..7cc48bc
--- /dev/null
+++ b/media/MediaBrowserService/screenshots/architecture.png
Binary files differ
diff --git a/media/MediaBrowserService/screenshots/icon-web.png b/media/MediaBrowserService/screenshots/icon-web.png
index 99928a8..df4c249 100644
--- a/media/MediaBrowserService/screenshots/icon-web.png
+++ b/media/MediaBrowserService/screenshots/icon-web.png
Binary files differ
diff --git a/media/MediaBrowserService/template-params.xml b/media/MediaBrowserService/template-params.xml
index 737fadb..9abe212 100644
--- a/media/MediaBrowserService/template-params.xml
+++ b/media/MediaBrowserService/template-params.xml
@@ -17,14 +17,15 @@
<sample>
<name>MediaBrowserService</name>
<group>Media</group>
- <package>com.example.android.mediabrowserservice</package>
+ <package>com.example.android.mediasession</package>
<minSdk>19</minSdk>
+ <auto_add_support_lib>true</auto_add_support_lib>
+ <use_java8>true</use_java8>
<!-- Include additional dependencies here.-->
- <dependency>com.android.support:support-compat:25.3.0</dependency>
- <dependency>com.android.support:support-media-compat:25.3.0</dependency>
- <dependency>com.android.support:appcompat-v7:25.3.0</dependency>
+ <dependency>com.android.support:appcompat-v7:26.1.0</dependency>
+ <dependency>com.android.support.constraint:constraint-layout:1.0.2</dependency>
<strings>
<intro>
@@ -32,22 +33,14 @@
This sample shows how to implement an audio media app that provides
media library metadata and playback controls through a standard
service. It exposes a simple music library through the new
-MediaBrowserService and provides MediaSession callbacks. This allows
-it to be used in Android Auto, for example.
-When not connected to a car, the app has a very simple UI that browses
-the media library and provides simple playback controls. When
-connected to Android Auto, the same service provides data and callback
-to the Android Auto UI in the same manner as it provides them to the
-local UI.
-]]></intro>
+MediaBrowserService and provides MediaSession callbacks.]]>
+ </intro>
</strings>
<!-- The basic templates have already been enabled. Uncomment more as desired. -->
<template src="base-build" />
<template src="base-application" />
- <use_support_library_vector_drawables>true</use_support_library_vector_drawables>
-
<metadata>
<status>PUBLISHED</status>
<categories>Media</categories>
@@ -58,28 +51,33 @@
<icon>screenshots/icon-web.png</icon>
<screenshots>
<img>screenshots/1-main.png</img>
- <img>screenshots/2-music-play.png</img>
- <img>screenshots/3-music-notification.png</img>
+ <img>screenshots/2-notification.png</img>
</screenshots>
<api_refs>
<android>android.support.v4.media.MediaBrowserServiceCompat</android>
<android>android.support.v4.media.MediaBrowserCompat</android>
<android>android.support.v4.media.session.MediaSessionCompat</android>
<android>android.support.v4.media.session.MediaControllerCompat</android>
- <android>android.support.v7.app.NotificationCompat.MediaStyle</android>
+ <android>android.support.v4.media.app.NotificationCompat.MediaStyle</android>
</api_refs>
<description>
<![CDATA[
-This sample shows how to implement an audio media app that provides
-media library metadata and playback controls through a standard
-service. It exposes a simple music library through the new
-MediaBrowserService and provides MediaSession callbacks. This allows
-it to be used in Android Auto, for example.
-When not connected to a car, the app has a very simple UI that browses
-the media library and provides simple playback controls. When
-connected to Android Auto, the same service provides data and callback
-to the Android Auto UI in the same manner as it provides them to the
-local UI.
+This sample shows how to implement a media app that allows
+background playback of audio, and provide a media library
+that is exposed to other apps.
+1. It allows other apps control media playback externally
+using MediaSession. This allows playback to be controlled by
+the Google Assistant, for example.
+2. It exposes a simple music library through MediaBrowserService.
+And it provides MediaSession callbacks. This allows it to be used
+by Android Auto, for example.
+When not connected to a car, the app has a very simple UI that
+allows for playback as well as skip to previous and next tracks.
+To learn more about MediaSession and MediaBrowserService, read
+this [article on Medium](https://medium.com/google-developers/understanding-mediasession-part-4-4-dcc77c535f99)
+that goes into the architectural details of these APIs.
+
+<img src="screenshots/architecture.png" height="400" alt="Architecture Diagram"/>
]]></description>
<intro>
<![CDATA[