Update browseable samples for lmp-docs

Synced to commit df5e5013422b81b4fd05c0ac9fd964b13624847a. Includes
new samples for Android Auto.

Change-Id: I3fec46e2a6b3f196682a92f1afd91eb682dc2dc1
This commit is contained in:
Trevor Johns
2014-11-12 11:39:30 -08:00
parent fcd28181a1
commit 527a4f30a6
684 changed files with 10100 additions and 10207 deletions

View File

@@ -0,0 +1,65 @@
<?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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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" />
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="21" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<activity android:name="com.example.android.mediabrowserservice.MusicPlayerActivity"
android:label="@string/app_name">
<intent-filter>
<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" />
<service
android:name="com.example.android.mediabrowserservice.MusicService"
android:exported="true"
>
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,11 @@
page.tags="MediaBrowserService"
sample.group=Media
@jd:body
<p>
This sample shows how to implement an audio media app that provides
media library metadata and playback controls through a standard
service.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,22 @@
<!--
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" />

View File

@@ -0,0 +1,60 @@
<!--
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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/fragment_list_padding">
<LinearLayout
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/skip_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/ic_skip_previous_white_24dp"
android:contentDescription="@string/skip_previous"/>
<ImageButton
android:id="@+id/play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/ic_play_arrow_white_24dp"
android:contentDescription="@string/play_pause"/>
<ImageButton
android:id="@+id/skip_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/ic_skip_next_white_24dp"
android:contentDescription="@string/skip_next"/>
</LinearLayout>
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>

View File

@@ -0,0 +1,55 @@
<!--
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:padding="@dimen/list_item_padding"
android:orientation="vertical">
<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>

View File

@@ -0,0 +1,33 @@
<?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>
<style name="AppBaseTheme" parent="android:Theme.Material">
<!-- colorPrimary is used for Notification icon and bottom facet bar icons
and overflow actions -->
<item name="android:colorPrimary">#ffff5722</item>
<!-- colorPrimaryDark is used for background -->
<item name="android:colorPrimaryDark">#ffbf360c</item>
<!-- colorAccent is sparingly used for accents, like floating action button highlight,
progress on playbar-->
<item name="android:colorAccent">#ffff5722</item>
</style>
</resources>

View File

@@ -0,0 +1,21 @@
<?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>

View File

@@ -0,0 +1,33 @@
<?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="app_name">Auto Music Demo</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>
</resources>

View File

@@ -0,0 +1,24 @@
<?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>
<string name="error_empty_metadata">Empty metadata!</string>
</resources>

View File

@@ -0,0 +1,26 @@
<?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>
<style name="AppTheme" parent="AppBaseTheme">
</style>
<style name="AppBaseTheme" parent="android:Theme.Light">
</style>
</resources>

View File

@@ -0,0 +1,19 @@
<?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>

View File

@@ -0,0 +1,210 @@
/*
* 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.Fragment;
import android.content.ComponentName;
import android.content.Context;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.os.Bundle;
import android.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 com.example.android.mediabrowserservice.utils.LogHelper;
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 MediaBrowser} to connect to the {@link MusicService}. Once connected,
* the fragment subscribes to get all the children. All {@link MediaBrowser.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";
public static interface FragmentDataHelper {
void onMediaItemSelected(MediaBrowser.MediaItem item);
}
// The mediaId to be used for subscribing for children using the MediaBrowser.
private String mMediaId;
private MediaBrowser mMediaBrowser;
private BrowseAdapter mBrowserAdapter;
private MediaBrowser.SubscriptionCallback mSubscriptionCallback = new MediaBrowser.SubscriptionCallback() {
@Override
public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
mBrowserAdapter.clear();
mBrowserAdapter.notifyDataSetInvalidated();
for (MediaBrowser.MediaItem item : children) {
mBrowserAdapter.add(item);
}
mBrowserAdapter.notifyDataSetChanged();
}
@Override
public void onError(String id) {
Toast.makeText(getActivity(), R.string.error_loading_media,
Toast.LENGTH_LONG).show();
}
};
private MediaBrowser.ConnectionCallback mConnectionCallback =
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
LogHelper.d(TAG, "onConnected: session token " + mMediaBrowser.getSessionToken());
if (mMediaId == null) {
mMediaId = mMediaBrowser.getRoot();
}
mMediaBrowser.subscribe(mMediaId, mSubscriptionCallback);
if (mMediaBrowser.getSessionToken() == null) {
throw new IllegalArgumentException("No Session token");
}
MediaController mediaController = new MediaController(getActivity(),
mMediaBrowser.getSessionToken());
getActivity().setMediaController(mediaController);
}
@Override
public void onConnectionFailed() {
LogHelper.d(TAG, "onConnectionFailed");
}
@Override
public void onConnectionSuspended() {
LogHelper.d(TAG, "onConnectionSuspended");
getActivity().setMediaController(null);
}
};
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());
View controls = rootView.findViewById(R.id.controls);
controls.setVisibility(View.GONE);
ListView listView = (ListView) rootView.findViewById(R.id.list_view);
listView.setAdapter(mBrowserAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MediaBrowser.MediaItem item = mBrowserAdapter.getItem(position);
try {
FragmentDataHelper listener = (FragmentDataHelper) getActivity();
listener.onMediaItemSelected(item);
} 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 MediaBrowser(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<MediaBrowser.MediaItem> {
public BrowseAdapter(Context context) {
super(context, R.layout.media_list_item, new ArrayList<MediaBrowser.MediaItem>());
}
static class ViewHolder {
ImageView mImageView;
TextView mTitleView;
TextView mDescriptionView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.media_list_item, parent, false);
holder = new ViewHolder();
holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq);
holder.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();
}
MediaBrowser.MediaItem item = getItem(position);
holder.mTitleView.setText(item.getDescription().getTitle());
holder.mDescriptionView.setText(item.getDescription().getDescription());
if (item.isPlayable()) {
holder.mImageView.setImageDrawable(
getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp));
holder.mImageView.setVisibility(View.VISIBLE);
}
return convertView;
}
}
}

View File

@@ -0,0 +1,381 @@
/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.AsyncTask;
import android.util.LruCache;
import android.util.SparseArray;
import com.example.android.mediabrowserservice.utils.BitmapHelper;
import com.example.android.mediabrowserservice.utils.LogHelper;
import java.io.IOException;
/**
* Keeps track of a notification and updates it automatically for a given
* MediaSession. Maintaining a visible notification (usually) guarantees that the music service
* won't be killed during playback.
*/
public class MediaNotification extends BroadcastReceiver {
private static final String TAG = "MediaNotification";
private static final int NOTIFICATION_ID = 412;
public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause";
public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play";
public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev";
public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next";
private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024;
private final MusicService mService;
private MediaSession.Token mSessionToken;
private MediaController mController;
private MediaController.TransportControls mTransportControls;
private final SparseArray<PendingIntent> mIntents = new SparseArray<PendingIntent>();
private final LruCache<String, Bitmap> mAlbumArtCache;
private PlaybackState mPlaybackState;
private MediaMetadata mMetadata;
private Notification.Builder mNotificationBuilder;
private NotificationManager mNotificationManager;
private Notification.Action mPlayPauseAction;
private String mCurrentAlbumArt;
private int mNotificationColor;
private boolean mStarted = false;
public MediaNotification(MusicService service) {
mService = service;
updateSessionToken();
// simple album art cache that holds no more than
// MAX_ALBUM_ART_CACHE_SIZE bytes:
mAlbumArtCache = new LruCache<String, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
mNotificationColor = getNotificationColor();
mNotificationManager = (NotificationManager) mService
.getSystemService(Context.NOTIFICATION_SERVICE);
String pkg = mService.getPackageName();
mIntents.put(R.drawable.ic_pause_white_24dp, PendingIntent.getBroadcast(mService, 100,
new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
mIntents.put(R.drawable.ic_play_arrow_white_24dp, PendingIntent.getBroadcast(mService, 100,
new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
mIntents.put(R.drawable.ic_skip_previous_white_24dp, PendingIntent.getBroadcast(mService, 100,
new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
mIntents.put(R.drawable.ic_skip_next_white_24dp, PendingIntent.getBroadcast(mService, 100,
new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
}
protected int getNotificationColor() {
int notificationColor = 0;
String packageName = mService.getPackageName();
try {
Context packageContext = mService.createPackageContext(packageName, 0);
ApplicationInfo applicationInfo =
mService.getPackageManager().getApplicationInfo(packageName, 0);
packageContext.setTheme(applicationInfo.theme);
Resources.Theme theme = packageContext.getTheme();
TypedArray ta = theme.obtainStyledAttributes(
new int[] {android.R.attr.colorPrimary});
notificationColor = ta.getColor(0, Color.DKGRAY);
ta.recycle();
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return notificationColor;
}
/**
* Posts the notification and starts tracking the session to keep it
* updated. The notification will automatically be removed if the session is
* destroyed before {@link #stopNotification} is called.
*/
public void startNotification() {
if (!mStarted) {
mController.registerCallback(mCb);
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_NEXT);
filter.addAction(ACTION_PAUSE);
filter.addAction(ACTION_PLAY);
filter.addAction(ACTION_PREV);
mService.registerReceiver(this, filter);
mMetadata = mController.getMetadata();
mPlaybackState = mController.getPlaybackState();
mStarted = true;
// The notification must be updated after setting started to true
updateNotificationMetadata();
}
}
/**
* Removes the notification and stops tracking the session. If the session
* was destroyed this has no effect.
*/
public void stopNotification() {
mStarted = false;
mController.unregisterCallback(mCb);
try {
mNotificationManager.cancel(NOTIFICATION_ID);
mService.unregisterReceiver(this);
} catch (IllegalArgumentException ex) {
// ignore if the receiver is not registered.
}
mService.stopForeground(true);
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
LogHelper.d(TAG, "Received intent with action " + action);
if (ACTION_PAUSE.equals(action)) {
mTransportControls.pause();
} else if (ACTION_PLAY.equals(action)) {
mTransportControls.play();
} else if (ACTION_NEXT.equals(action)) {
mTransportControls.skipToNext();
} else if (ACTION_PREV.equals(action)) {
mTransportControls.skipToPrevious();
}
}
/**
* Update the state based on a change on the session token. Called either when
* we are running for the first time or when the media session owner has destroyed the session
* (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()})
*/
private void updateSessionToken() {
MediaSession.Token freshToken = mService.getSessionToken();
if (mSessionToken == null || !mSessionToken.equals(freshToken)) {
if (mController != null) {
mController.unregisterCallback(mCb);
}
mSessionToken = freshToken;
mController = new MediaController(mService, mSessionToken);
mTransportControls = mController.getTransportControls();
if (mStarted) {
mController.registerCallback(mCb);
}
}
}
private final MediaController.Callback mCb = new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
mPlaybackState = state;
LogHelper.d(TAG, "Received new playback state", state);
updateNotificationPlaybackState();
}
@Override
public void onMetadataChanged(MediaMetadata metadata) {
mMetadata = metadata;
LogHelper.d(TAG, "Received new metadata ", metadata);
updateNotificationMetadata();
}
@Override
public void onSessionDestroyed() {
super.onSessionDestroyed();
LogHelper.d(TAG, "Session was destroyed, resetting to the new session token");
updateSessionToken();
}
};
private void updateNotificationMetadata() {
LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
if (mMetadata == null || mPlaybackState == null) {
return;
}
updatePlayPauseAction();
mNotificationBuilder = new Notification.Builder(mService);
int playPauseActionIndex = 0;
// If skip to previous action is enabled
if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
mNotificationBuilder
.addAction(R.drawable.ic_skip_previous_white_24dp,
mService.getString(R.string.label_previous),
mIntents.get(R.drawable.ic_skip_previous_white_24dp));
playPauseActionIndex = 1;
}
mNotificationBuilder.addAction(mPlayPauseAction);
// If skip to next action is enabled
if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
mService.getString(R.string.label_next),
mIntents.get(R.drawable.ic_skip_next_white_24dp));
}
MediaDescription description = mMetadata.getDescription();
String fetchArtUrl = null;
Bitmap art = description.getIconBitmap();
if (art == null && description.getIconUri() != null) {
// This sample assumes the iconUri will be a valid URL formatted String, but
// it can actually be any valid Android Uri formatted String.
// async fetch the album art icon
String artUrl = description.getIconUri().toString();
art = mAlbumArtCache.get(artUrl);
if (art == null) {
fetchArtUrl = artUrl;
// use a placeholder art while the remote art is being downloaded
art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art);
}
}
mNotificationBuilder
.setStyle(new Notification.MediaStyle()
.setShowActionsInCompactView(playPauseActionIndex) // only show play/pause in compact view
.setMediaSession(mSessionToken))
.setColor(mNotificationColor)
.setSmallIcon(R.drawable.ic_notification)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setContentTitle(description.getTitle())
.setContentText(description.getSubtitle())
.setLargeIcon(art);
updateNotificationPlaybackState();
mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
if (fetchArtUrl != null) {
fetchBitmapFromURLAsync(fetchArtUrl);
}
}
private void updatePlayPauseAction() {
LogHelper.d(TAG, "updatePlayPauseAction");
String playPauseLabel = "";
int playPauseIcon;
if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
playPauseLabel = mService.getString(R.string.label_pause);
playPauseIcon = R.drawable.ic_pause_white_24dp;
} else {
playPauseLabel = mService.getString(R.string.label_play);
playPauseIcon = R.drawable.ic_play_arrow_white_24dp;
}
if (mPlayPauseAction == null) {
mPlayPauseAction = new Notification.Action(playPauseIcon, playPauseLabel,
mIntents.get(playPauseIcon));
} else {
mPlayPauseAction.icon = playPauseIcon;
mPlayPauseAction.title = playPauseLabel;
mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon);
}
}
private void updateNotificationPlaybackState() {
LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState);
if (mPlaybackState == null || !mStarted) {
LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!");
mService.stopForeground(true);
return;
}
if (mNotificationBuilder == null) {
LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!");
return;
}
if (mPlaybackState.getPosition() >= 0) {
LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ",
(System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds");
mNotificationBuilder
.setWhen(System.currentTimeMillis() - mPlaybackState.getPosition())
.setShowWhen(true)
.setUsesChronometer(true);
mNotificationBuilder.setShowWhen(true);
} else {
LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position");
mNotificationBuilder
.setWhen(0)
.setShowWhen(false)
.setUsesChronometer(false);
}
updatePlayPauseAction();
// Make sure that the notification can be dismissed by the user when we are not playing:
mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
}
public void fetchBitmapFromURLAsync(final String source) {
LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source);
new AsyncTask<Void, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Void[] objects) {
Bitmap bitmap = null;
try {
bitmap = BitmapHelper.fetchAndRescaleBitmap(source,
BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT);
mAlbumArtCache.put(source, bitmap);
} catch (IOException e) {
LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null && mMetadata != null &&
mNotificationBuilder != null && mMetadata.getDescription() != null &&
!source.equals(mMetadata.getDescription().getIconUri())) {
// If the media is still the same, update the notification:
LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source);
mNotificationBuilder.setLargeIcon(bitmap);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
}
}
}.execute();
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice;
import android.app.Activity;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.os.Bundle;
/**
* Main activity for the music player.
*/
public class MusicPlayerActivity extends Activity
implements BrowseFragment.FragmentDataHelper {
private static final String TAG = MusicPlayerActivity.class.getSimpleName();
private MediaBrowser mMediaBrowser;
private MediaController mMediaController;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, BrowseFragment.newInstance(null))
.commit();
}
}
@Override
public void onMediaItemSelected(MediaBrowser.MediaItem item) {
if (item.isPlayable()) {
getMediaController().getTransportControls().playFromMediaId(item.getMediaId(), null);
QueueFragment queueFragment = QueueFragment.newInstance();
getFragmentManager().beginTransaction()
.replace(R.id.container, queueFragment)
.addToBackStack(null)
.commit();
} else if (item.isBrowsable()) {
getFragmentManager().beginTransaction()
.replace(R.id.container, BrowseFragment.newInstance(item.getMediaId()))
.addToBackStack(null)
.commit();
}
}
}

View File

@@ -0,0 +1,936 @@
/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.browse.MediaBrowser;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.service.media.MediaBrowserService;
import com.example.android.mediabrowserservice.model.MusicProvider;
import com.example.android.mediabrowserservice.utils.LogHelper;
import com.example.android.mediabrowserservice.utils.MediaIDHelper;
import com.example.android.mediabrowserservice.utils.QueueHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID;
/**
* This class provides a MediaBrowser through a service. It exposes the media library to a browsing
* client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
* exposes it through its MediaSession.Token, which allows the client to create a MediaController
* that connects to and send control commands to the MediaSession remotely. This is useful for
* user interfaces that need to interact with your media session, like Android Auto. You can
* (should) also use the same service from your app's UI, which gives a seamless playback
* experience to the user.
*
* To implement a MediaBrowserService, you need to:
*
* <ul>
*
* <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing
* related methods {@link android.service.media.MediaBrowserService#onGetRoot} and
* {@link android.service.media.MediaBrowserService#onLoadChildren};
* <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent
* with the session's token {@link android.service.media.MediaBrowserService#setSessionToken};
*
* <li> Set a callback on the
* {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}.
* The callback will receive all the user's actions, like play, pause, etc;
*
* <li> Handle all the actual music playing using any method your app prefers (for example,
* {@link android.media.MediaPlayer})
*
* <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
* {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)}
* {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and
* {@link android.media.session.MediaSession#setQueue(java.util.List)})
*
* <li> Declare and export the service in AndroidManifest with an intent receiver for the action
* android.media.browse.MediaBrowserService
*
* </ul>
*
* To make your app compatible with Android Auto, you also need to:
*
* <ul>
*
* <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
* with a &lt;automotiveApp&gt; root element. For a media app, this must include
* an &lt;uses name="media"/&gt; element as a child.
* For example, in AndroidManifest.xml:
* &lt;meta-data android:name="com.google.android.gms.car.application"
* android:resource="@xml/automotive_app_desc"/&gt;
* And in res/values/automotive_app_desc.xml:
* &lt;automotiveApp&gt;
* &lt;uses name="media"/&gt;
* &lt;/automotiveApp&gt;
*
* </ul>
* @see <a href="README.md">README.md</a> for more details.
*
*/
public class MusicService extends MediaBrowserService implements OnPreparedListener,
OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener {
private static final String TAG = "MusicService";
// Action to thumbs up a media item
private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up";
// Delay stopSelf by using a handler.
private static final int STOP_DELAY = 30000;
// 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;
public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead";
public static final String ANDROID_AUTO_EMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator";
// Music catalog manager
private MusicProvider mMusicProvider;
private MediaSession mSession;
private MediaPlayer mMediaPlayer;
// "Now playing" queue:
private List<MediaSession.QueueItem> mPlayingQueue;
private int mCurrentIndexOnQueue;
// Current local media player state
private int mState = PlaybackState.STATE_NONE;
// Wifi lock that we hold when streaming files from the internet, in order
// to prevent the device from shutting off the Wifi radio
private WifiLock mWifiLock;
private MediaNotification mMediaNotification;
// Indicates whether the service was started.
private boolean mServiceStarted;
enum AudioFocus {
NoFocusNoDuck, // we don't have audio focus, and can't duck
NoFocusCanDuck, // we don't have focus, but can play at a low volume
// ("ducking")
Focused // we have full audio focus
}
// Type of audio focus we have:
private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck;
private AudioManager mAudioManager;
// Indicates if we should start playing immediately after we gain focus.
private boolean mPlayOnFocusGain;
private Handler mDelayedStopHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) ||
mPlayOnFocusGain) {
LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
return;
}
LogHelper.d(TAG, "Stopping service with delay handler.");
stopSelf();
mServiceStarted = false;
}
};
/*
* (non-Javadoc)
* @see android.app.Service#onCreate()
*/
@Override
public void onCreate() {
super.onCreate();
LogHelper.d(TAG, "onCreate");
mPlayingQueue = new ArrayList<>();
// Create the Wifi lock (this does not acquire the lock, this just creates it)
mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock");
// Create the music catalog metadata provider
mMusicProvider = new MusicProvider();
mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
@Override
public void onMusicCatalogReady(boolean success) {
mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR;
}
});
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// Start a new MediaSession
mSession = new MediaSession(this, "MusicService");
setSessionToken(mSession.getSessionToken());
mSession.setCallback(new MediaSessionCallback());
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
// Use these extras to reserve space for the corresponding actions, even when they are disabled
// in the playbackstate, so the custom actions don't reflow.
Bundle extras = new Bundle();
extras.putBoolean(
"com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT",
true);
extras.putBoolean(
"com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS",
true);
// If you want to reserve the Queue slot when there is no queue
// (mSession.setQueue(emptylist)), uncomment the lines below:
// extras.putBoolean(
// "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE",
// true);
mSession.setExtras(extras);
updatePlaybackState(null);
mMediaNotification = new MediaNotification(this);
}
/*
* (non-Javadoc)
* @see android.app.Service#onDestroy()
*/
@Override
public void onDestroy() {
LogHelper.d(TAG, "onDestroy");
// Service is being killed, so make sure we release our resources
handleStopRequest(null);
mDelayedStopHandler.removeCallbacksAndMessages(null);
// In particular, always release the MediaSession to clean up resources
// and notify associated MediaController(s).
mSession.release();
}
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
"; clientUid=" + clientUid + " ; rootHints=", rootHints);
// To ensure you are not allowing any arbitrary app to browse your app's contents, you
// need to check the origin:
if (!ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName) &&
!ANDROID_AUTO_EMULATOR_PACKAGE_NAME.equals(clientPackageName) &&
!getApplication().getPackageName().equals(clientPackageName)) {
// If the request comes from an untrusted package, return null. No further calls will
// be made to other media browsing methods.
LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " + clientPackageName);
return null;
}
if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) {
// Optional: if your app needs to adapt ads, music library or anything else that
// needs to run differently when connected to the car, this is where you should handle
// it.
}
return new BrowserRoot(MEDIA_ID_ROOT, null);
}
@Override
public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
if (!mMusicProvider.isInitialized()) {
// Use result.detach to allow calling result.sendResult from another thread:
result.detach();
mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
@Override
public void onMusicCatalogReady(boolean success) {
if (success) {
loadChildrenImpl(parentMediaId, result);
} else {
updatePlaybackState(getString(R.string.error_no_metadata));
result.sendResult(new ArrayList<MediaItem>());
}
}
});
} else {
// If our music catalog is already loaded/cached, load them into result immediately
loadChildrenImpl(parentMediaId, result);
}
}
/**
* Actual implementation of onLoadChildren that assumes that MusicProvider is already
* initialized.
*/
private void loadChildrenImpl(final String parentMediaId,
final Result<List<MediaBrowser.MediaItem>> result) {
LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
if (MEDIA_ID_ROOT.equals(parentMediaId)) {
LogHelper.d(TAG, "OnLoadChildren.ROOT");
mediaItems.add(new MediaBrowser.MediaItem(
new MediaDescription.Builder()
.setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
.setTitle(getString(R.string.browse_genres))
.setIconUri(Uri.parse("android.resource://" +
"com.example.android.mediabrowserservice/drawable/ic_by_genre"))
.setSubtitle(getString(R.string.browse_genre_subtitle))
.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
));
} else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
LogHelper.d(TAG, "OnLoadChildren.GENRES");
for (String genre: mMusicProvider.getGenres()) {
MediaBrowser.MediaItem item = new MediaBrowser.MediaItem(
new MediaDescription.Builder()
.setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre))
.setTitle(genre)
.setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre))
.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
);
mediaItems.add(item);
}
} else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1];
LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre);
for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) {
// Since mediaMetadata fields are immutable, we need to create a copy, so we
// can set a hierarchy-aware mediaID. We will need to know the media hierarchy
// when we get a onPlayFromMusicID call, so we can create the proper queue based
// on where the music was selected from (by artist, by genre, random, etc)
String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID(
MEDIA_ID_MUSICS_BY_GENRE, genre, track);
MediaMetadata trackCopy = new MediaMetadata.Builder(track)
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
.build();
MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem(
trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
mediaItems.add(bItem);
}
} else {
LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
}
result.sendResult(mediaItems);
}
private final class MediaSessionCallback extends MediaSession.Callback {
@Override
public void onPlay() {
LogHelper.d(TAG, "play");
if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
mSession.setQueue(mPlayingQueue);
mSession.setQueueTitle(getString(R.string.random_queue_title));
// start playing from the beginning of the queue
mCurrentIndexOnQueue = 0;
}
if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
handlePlayRequest();
}
}
@Override
public void onSkipToQueueItem(long queueId) {
LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
// set the current index on queue from the music Id:
mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
// play the music
handlePlayRequest();
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras);
// The mediaId used here is not the unique musicId. This one comes from the
// MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
// the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
// so we can build the correct playing queue, based on where the track was
// selected from.
mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
mSession.setQueue(mPlayingQueue);
String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
mSession.setQueueTitle(queueTitle);
if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId);
// set the current index on queue from the music Id:
mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(
mPlayingQueue, uniqueMusicID);
// play the music
handlePlayRequest();
}
}
@Override
public void onPause() {
LogHelper.d(TAG, "pause. current state=" + mState);
handlePauseRequest();
}
@Override
public void onStop() {
LogHelper.d(TAG, "stop. current state=" + mState);
handleStopRequest(null);
}
@Override
public void onSkipToNext() {
LogHelper.d(TAG, "skipToNext");
mCurrentIndexOnQueue++;
if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
mCurrentIndexOnQueue = 0;
}
if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
mState = PlaybackState.STATE_PLAYING;
handlePlayRequest();
} else {
LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
mCurrentIndexOnQueue + " queue length=" +
(mPlayingQueue == null ? "null" : mPlayingQueue.size()));
handleStopRequest("Cannot skip");
}
}
@Override
public void onSkipToPrevious() {
LogHelper.d(TAG, "skipToPrevious");
mCurrentIndexOnQueue--;
if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
// This sample's behavior: skipping to previous when in first song restarts the
// first song.
mCurrentIndexOnQueue = 0;
}
if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
mState = PlaybackState.STATE_PLAYING;
handlePlayRequest();
} else {
LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
mCurrentIndexOnQueue + " queue length=" +
(mPlayingQueue == null ? "null" : mPlayingQueue.size()));
handleStopRequest("Cannot skip");
}
}
@Override
public void onCustomAction(String action, Bundle extras) {
if (CUSTOM_ACTION_THUMBS_UP.equals(action)) {
LogHelper.i(TAG, "onCustomAction: favorite for current track");
MediaMetadata track = getCurrentPlayingMusic();
if (track != null) {
String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId));
}
updatePlaybackState(null);
} else {
LogHelper.e(TAG, "Unsupported action: ", action);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
LogHelper.d(TAG, "playFromSearch query=", query);
mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size());
mSession.setQueue(mPlayingQueue);
if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
// start playing from the beginning of the queue
mCurrentIndexOnQueue = 0;
handlePlayRequest();
}
}
}
/*
* Called when media player is done playing current song.
* @see android.media.MediaPlayer.OnCompletionListener
*/
@Override
public void onCompletion(MediaPlayer player) {
LogHelper.d(TAG, "onCompletion from MediaPlayer");
// The media player finished playing the current song, so we go ahead
// and start the next.
if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
// In this sample, we restart the playing queue when it gets to the end:
mCurrentIndexOnQueue++;
if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
mCurrentIndexOnQueue = 0;
}
handlePlayRequest();
} else {
// If there is nothing to play, we stop and release the resources:
handleStopRequest(null);
}
}
/*
* Called when media player is done preparing.
* @see android.media.MediaPlayer.OnPreparedListener
*/
@Override
public void onPrepared(MediaPlayer player) {
LogHelper.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 mp, int what, int extra) {
LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
handleStopRequest("MediaPlayer error " + what + " (" + extra + ")");
return true; // true indicates we handled the error
}
/**
* Called by AudioManager on audio focus changes.
*/
@Override
public void onAudioFocusChange(int focusChange) {
LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// We have gained focus:
mAudioFocus = AudioFocus.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 ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck;
// If we are playing, we need to reset media player by calling configMediaPlayerState
// with mAudioFocus properly set.
if (mState == PlaybackState.STATE_PLAYING && !canDuck) {
// If we don't have audio focus and can't duck, we save the information that
// we were playing, so that we can resume playback once we get the focus back.
mPlayOnFocusGain = true;
}
} else {
LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
}
configMediaPlayerState();
}
/**
* Handle a request to play music
*/
private void handlePlayRequest() {
LogHelper.d(TAG, "handlePlayRequest: mState=" + mState);
mDelayedStopHandler.removeCallbacksAndMessages(null);
if (!mServiceStarted) {
LogHelper.v(TAG, "Starting service");
// The MusicService needs to keep running even after the calling MediaBrowser
// is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
// need to play media.
startService(new Intent(getApplicationContext(), MusicService.class));
mServiceStarted = true;
}
mPlayOnFocusGain = true;
tryToGetAudioFocus();
if (!mSession.isActive()) {
mSession.setActive(true);
}
// actually play the song
if (mState == PlaybackState.STATE_PAUSED) {
// If we're paused, just continue playback and restore the
// 'foreground service' state.
configMediaPlayerState();
} else {
// If we're stopped or playing a song,
// just go ahead to the new song and (re)start playing
playCurrentSong();
}
}
/**
* Handle a request to pause music
*/
private void handlePauseRequest() {
LogHelper.d(TAG, "handlePauseRequest: mState=" + mState);
if (mState == PlaybackState.STATE_PLAYING) {
// Pause media player and cancel the 'foreground service' state.
mState = PlaybackState.STATE_PAUSED;
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
}
// while paused, retain the MediaPlayer but give up audio focus
relaxResources(false);
giveUpAudioFocus();
}
updatePlaybackState(null);
}
/**
* Handle a request to stop music
*/
private void handleStopRequest(String withError) {
LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError);
mState = PlaybackState.STATE_STOPPED;
// let go of all resources...
relaxResources(true);
giveUpAudioFocus();
updatePlaybackState(withError);
mMediaNotification.stopNotification();
// service is no longer necessary. Will be started again if needed.
stopSelf();
mServiceStarted = false;
}
/**
* 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) {
LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer);
// stop being a foreground service
stopForeground(true);
// reset the delayed stop handler.
mDelayedStopHandler.removeCallbacksAndMessages(null);
mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
// 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();
}
}
/**
* 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() {
LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus);
if (mAudioFocus == AudioFocus.NoFocusNoDuck) {
// If we don't have audio focus and can't duck, we have to pause,
if (mState == PlaybackState.STATE_PLAYING) {
handlePauseRequest();
}
} else { // we have audio focus:
if (mAudioFocus == AudioFocus.NoFocusCanDuck) {
mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
} else {
mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
}
// If we were playing when we lost focus, we need to resume playing.
if (mPlayOnFocusGain) {
if (!mMediaPlayer.isPlaying()) {
LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer.");
mMediaPlayer.start();
}
mPlayOnFocusGain = false;
mState = PlaybackState.STATE_PLAYING;
}
}
updatePlaybackState(null);
}
/**
* 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() {
LogHelper.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(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);
} else {
mMediaPlayer.reset();
}
}
/**
* Starts playing the current song in the playing queue.
*/
void playCurrentSong() {
MediaMetadata track = getCurrentPlayingMusic();
if (track == null) {
LogHelper.e(TAG, "playSong: ignoring request to play next song, because cannot" +
" find it." +
" currentIndex=" + mCurrentIndexOnQueue +
" playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size()));
return;
}
String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);
LogHelper.d(TAG, "playSong: current (" + mCurrentIndexOnQueue + ") in playingQueue. " +
" musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) +
" source=" + source);
mState = PlaybackState.STATE_STOPPED;
relaxResources(false); // release everything except MediaPlayer
try {
createMediaPlayerIfNeeded();
mState = PlaybackState.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();
updatePlaybackState(null);
updateMetadata();
} catch (IOException ex) {
LogHelper.e(TAG, ex, "IOException playing song");
updatePlaybackState(ex.getMessage());
}
}
private void updateMetadata() {
if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
LogHelper.e(TAG, "Can't retrieve current metadata.");
mState = PlaybackState.STATE_ERROR;
updatePlaybackState(getResources().getString(R.string.error_no_metadata));
return;
}
MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
String mediaId = queueItem.getDescription().getMediaId();
MediaMetadata track = mMusicProvider.getMusic(mediaId);
String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
if (!mediaId.equals(trackId)) {
throw new IllegalStateException("track ID (" + trackId + ") " +
"should match mediaId (" + mediaId + ")");
}
LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId);
mSession.setMetadata(track);
}
/**
* Update the current media player state, optionally showing an error message.
*
* @param error if not null, error message to present to the user.
*
*/
private void updatePlaybackState(String error) {
LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState);
long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
position = mMediaPlayer.getCurrentPosition();
}
PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
.setActions(getAvailableActions());
setCustomAction(stateBuilder);
// 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);
mState = PlaybackState.STATE_ERROR;
}
stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime());
// Set the activeQueueItemId if the current index is valid.
if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
stateBuilder.setActiveQueueItemId(item.getQueueId());
}
mSession.setPlaybackState(stateBuilder.build());
if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) {
mMediaNotification.startNotification();
}
}
private void setCustomAction(PlaybackState.Builder stateBuilder) {
MediaMetadata currentMusic = getCurrentPlayingMusic();
if (currentMusic != null) {
// Set appropriate "Favorite" icon on Custom action:
String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
int favoriteIcon = R.drawable.ic_star_off;
if (mMusicProvider.isFavorite(mediaId)) {
favoriteIcon = R.drawable.ic_star_on;
}
LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId));
stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
favoriteIcon);
}
}
private long getAvailableActions() {
long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
PlaybackState.ACTION_PLAY_FROM_SEARCH;
if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
return actions;
}
if (mState == PlaybackState.STATE_PLAYING) {
actions |= PlaybackState.ACTION_PAUSE;
}
if (mCurrentIndexOnQueue > 0) {
actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
}
if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
}
return actions;
}
private MediaMetadata getCurrentPlayingMusic() {
if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
if (item != null) {
LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=",
item.getDescription().getMediaId());
return mMusicProvider.getMusic(item.getDescription().getMediaId());
}
}
return null;
}
/**
* Try to get the system audio focus.
*/
void tryToGetAudioFocus() {
LogHelper.d(TAG, "tryToGetAudioFocus");
if (mAudioFocus != AudioFocus.Focused) {
int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocus = AudioFocus.Focused;
}
}
}
/**
* Give up the audio focus.
*/
void giveUpAudioFocus() {
LogHelper.d(TAG, "giveUpAudioFocus");
if (mAudioFocus == AudioFocus.Focused) {
if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocus = AudioFocus.NoFocusNoDuck;
}
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice;
import android.app.Activity;
import android.media.session.MediaSession;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
/**
* A list adapter for items in a queue
*/
public class QueueAdapter extends ArrayAdapter<MediaSession.QueueItem> {
// The currently selected/active queue item Id.
private long mActiveQueueItemId = MediaSession.QueueItem.UNKNOWN_ID;
public QueueAdapter(Activity context) {
super(context, R.layout.media_list_item, new ArrayList<MediaSession.QueueItem>());
}
public void setActiveQueueItemId(long id) {
this.mActiveQueueItemId = id;
}
private static class ViewHolder {
ImageView mImageView;
TextView mTitleView;
TextView mDescriptionView;
}
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.media_list_item, parent, false);
holder = new ViewHolder();
holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq);
holder.mTitleView = (TextView) convertView.findViewById(R.id.title);
holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
MediaSession.QueueItem item = getItem(position);
holder.mTitleView.setText(item.getDescription().getTitle());
if (item.getDescription().getDescription() != null) {
holder.mDescriptionView.setText(item.getDescription().getDescription());
}
// If the itemId matches the active Id then use a different icon
if (mActiveQueueItemId == item.getQueueId()) {
holder.mImageView.setImageDrawable(
getContext().getDrawable(R.drawable.ic_equalizer_white_24dp));
} else {
holder.mImageView.setImageDrawable(
getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp));
}
return convertView;
}
}

View File

@@ -0,0 +1,295 @@
/*
* 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.Fragment;
import android.content.ComponentName;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageButton;
import android.widget.ListView;
import com.example.android.mediabrowserservice.utils.LogHelper;
import java.util.List;
/**
* A class that shows the Media Queue to the user.
*/
public class QueueFragment extends Fragment {
private static final String TAG = QueueFragment.class.getSimpleName();
private ImageButton mSkipNext;
private ImageButton mSkipPrevious;
private ImageButton mPlayPause;
private MediaBrowser mMediaBrowser;
private MediaController.TransportControls mTransportControls;
private MediaController mMediaController;
private PlaybackState mPlaybackState;
private QueueAdapter mQueueAdapter;
private MediaBrowser.ConnectionCallback mConnectionCallback =
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
LogHelper.d(TAG, "onConnected: session token ", mMediaBrowser.getSessionToken());
if (mMediaBrowser.getSessionToken() == null) {
throw new IllegalArgumentException("No Session token");
}
mMediaController = new MediaController(getActivity(),
mMediaBrowser.getSessionToken());
mTransportControls = mMediaController.getTransportControls();
mMediaController.registerCallback(mSessionCallback);
getActivity().setMediaController(mMediaController);
mPlaybackState = mMediaController.getPlaybackState();
List<MediaSession.QueueItem> queue = mMediaController.getQueue();
if (queue != null) {
mQueueAdapter.clear();
mQueueAdapter.notifyDataSetInvalidated();
mQueueAdapter.addAll(queue);
mQueueAdapter.notifyDataSetChanged();
}
onPlaybackStateChanged(mPlaybackState);
}
@Override
public void onConnectionFailed() {
LogHelper.d(TAG, "onConnectionFailed");
}
@Override
public void onConnectionSuspended() {
LogHelper.d(TAG, "onConnectionSuspended");
mMediaController.unregisterCallback(mSessionCallback);
mTransportControls = null;
mMediaController = null;
getActivity().setMediaController(null);
}
};
// Receive callbacks from the MediaController. Here we update our state such as which queue
// is being shown, the current title and description and the PlaybackState.
private MediaController.Callback mSessionCallback = new MediaController.Callback() {
@Override
public void onSessionDestroyed() {
LogHelper.d(TAG, "Session destroyed. Need to fetch a new Media Session");
}
@Override
public void onPlaybackStateChanged(PlaybackState state) {
if (state == null) {
return;
}
LogHelper.d(TAG, "Received playback state change to state ", state.getState());
mPlaybackState = state;
QueueFragment.this.onPlaybackStateChanged(state);
}
@Override
public void onQueueChanged(List<MediaSession.QueueItem> queue) {
LogHelper.d(TAG, "onQueueChanged ", queue);
if (queue != null) {
mQueueAdapter.clear();
mQueueAdapter.notifyDataSetInvalidated();
mQueueAdapter.addAll(queue);
mQueueAdapter.notifyDataSetChanged();
}
}
};
public static QueueFragment newInstance() {
return new QueueFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_list, container, false);
mSkipPrevious = (ImageButton) rootView.findViewById(R.id.skip_previous);
mSkipPrevious.setEnabled(false);
mSkipPrevious.setOnClickListener(mButtonListener);
mSkipNext = (ImageButton) rootView.findViewById(R.id.skip_next);
mSkipNext.setEnabled(false);
mSkipNext.setOnClickListener(mButtonListener);
mPlayPause = (ImageButton) rootView.findViewById(R.id.play_pause);
mPlayPause.setEnabled(true);
mPlayPause.setOnClickListener(mButtonListener);
mQueueAdapter = new QueueAdapter(getActivity());
ListView mListView = (ListView) rootView.findViewById(R.id.list_view);
mListView.setAdapter(mQueueAdapter);
mListView.setFocusable(true);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MediaSession.QueueItem item = mQueueAdapter.getItem(position);
mTransportControls.skipToQueueItem(item.getQueueId());
}
});
mMediaBrowser = new MediaBrowser(getActivity(),
new ComponentName(getActivity(), MusicService.class),
mConnectionCallback, null);
return rootView;
}
@Override
public void onResume() {
super.onResume();
if (mMediaBrowser != null) {
mMediaBrowser.connect();
}
}
@Override
public void onPause() {
super.onPause();
if (mMediaController != null) {
mMediaController.unregisterCallback(mSessionCallback);
}
if (mMediaBrowser != null) {
mMediaBrowser.disconnect();
}
}
private void onPlaybackStateChanged(PlaybackState state) {
LogHelper.d(TAG, "onPlaybackStateChanged ", state);
if (state == null) {
return;
}
mQueueAdapter.setActiveQueueItemId(state.getActiveQueueItemId());
mQueueAdapter.notifyDataSetChanged();
boolean enablePlay = false;
StringBuilder statusBuilder = new StringBuilder();
switch (state.getState()) {
case PlaybackState.STATE_PLAYING:
statusBuilder.append("playing");
enablePlay = false;
break;
case PlaybackState.STATE_PAUSED:
statusBuilder.append("paused");
enablePlay = true;
break;
case PlaybackState.STATE_STOPPED:
statusBuilder.append("ended");
enablePlay = true;
break;
case PlaybackState.STATE_ERROR:
statusBuilder.append("error: ").append(state.getErrorMessage());
break;
case PlaybackState.STATE_BUFFERING:
statusBuilder.append("buffering");
break;
case PlaybackState.STATE_NONE:
statusBuilder.append("none");
enablePlay = false;
break;
case PlaybackState.STATE_CONNECTING:
statusBuilder.append("connecting");
break;
default:
statusBuilder.append(mPlaybackState);
}
statusBuilder.append(" -- At position: ").append(state.getPosition());
LogHelper.d(TAG, statusBuilder.toString());
if (enablePlay) {
mPlayPause.setImageDrawable(
getActivity().getDrawable(R.drawable.ic_play_arrow_white_24dp));
} else {
mPlayPause.setImageDrawable(getActivity().getDrawable(R.drawable.ic_pause_white_24dp));
}
mSkipPrevious.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
mSkipNext.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
LogHelper.d(TAG, "Queue From MediaController *** Title " +
mMediaController.getQueueTitle() + "\n: Queue: " + mMediaController.getQueue() +
"\n Metadata " + mMediaController.getMetadata());
}
private View.OnClickListener mButtonListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final int state = mPlaybackState == null ?
PlaybackState.STATE_NONE : mPlaybackState.getState();
switch (v.getId()) {
case R.id.play_pause:
LogHelper.d(TAG, "Play button pressed, in state " + state);
if (state == PlaybackState.STATE_PAUSED ||
state == PlaybackState.STATE_STOPPED ||
state == PlaybackState.STATE_NONE) {
playMedia();
} else if (state == PlaybackState.STATE_PLAYING) {
pauseMedia();
}
break;
case R.id.skip_previous:
LogHelper.d(TAG, "Start button pressed, in state " + state);
skipToPrevious();
break;
case R.id.skip_next:
skipToNext();
break;
}
}
};
private void playMedia() {
if (mTransportControls != null) {
mTransportControls.play();
}
}
private void pauseMedia() {
if (mTransportControls != null) {
mTransportControls.pause();
}
}
private void skipToPrevious() {
if (mTransportControls != null) {
mTransportControls.skipToPrevious();
}
}
private void skipToNext() {
if (mTransportControls != null) {
mTransportControls.skipToNext();
}
}
}

View File

@@ -0,0 +1,296 @@
/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice.model;
import android.media.MediaMetadata;
import android.os.AsyncTask;
import com.example.android.mediabrowserservice.utils.LogHelper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* Utility class to get a list of MusicTrack's based on a server-side JSON
* configuration.
*/
public class MusicProvider {
private static final String TAG = "MusicProvider";
private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json";
public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
private static String JSON_MUSIC = "music";
private static String JSON_TITLE = "title";
private static String JSON_ALBUM = "album";
private static String JSON_ARTIST = "artist";
private static String JSON_GENRE = "genre";
private static String JSON_SOURCE = "source";
private static String JSON_IMAGE = "image";
private static String JSON_TRACK_NUMBER = "trackNumber";
private static String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
private static String JSON_DURATION = "duration";
private final ReentrantLock initializationLock = new ReentrantLock();
// Categorized caches for music track data:
private final HashMap<String, List<MediaMetadata>> mMusicListByGenre;
private final HashMap<String, MediaMetadata> mMusicListById;
private final HashSet<String> mFavoriteTracks;
enum State {
NON_INITIALIZED, INITIALIZING, INITIALIZED;
}
private State mCurrentState = State.NON_INITIALIZED;
public interface Callback {
void onMusicCatalogReady(boolean success);
}
public MusicProvider() {
mMusicListByGenre = new HashMap<>();
mMusicListById = new HashMap<>();
mFavoriteTracks = new HashSet<>();
}
/**
* Get an iterator over the list of genres
*
* @return
*/
public Iterable<String> getGenres() {
if (mCurrentState != State.INITIALIZED) {
return new ArrayList<String>(0);
}
return mMusicListByGenre.keySet();
}
/**
* Get music tracks of the given genre
*
* @return
*/
public Iterable<MediaMetadata> getMusicsByGenre(String genre) {
if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
return new ArrayList<MediaMetadata>();
}
return mMusicListByGenre.get(genre);
}
/**
* Very basic implementation of a search that filter music tracks which title containing
* the given query.
*
* @return
*/
public Iterable<MediaMetadata> searchMusics(String titleQuery) {
ArrayList<MediaMetadata> result = new ArrayList<>();
if (mCurrentState != State.INITIALIZED) {
return result;
}
titleQuery = titleQuery.toLowerCase();
for (MediaMetadata track: mMusicListById.values()) {
if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase()
.contains(titleQuery)) {
result.add(track);
}
}
return result;
}
public MediaMetadata getMusic(String mediaId) {
return mMusicListById.get(mediaId);
}
public void setFavorite(String mediaId, boolean favorite) {
if (favorite) {
mFavoriteTracks.add(mediaId);
} else {
mFavoriteTracks.remove(mediaId);
}
}
public boolean isFavorite(String musicId) {
return mFavoriteTracks.contains(musicId);
}
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 mediaId and grouping by genre.
*
* @return
*/
public void retrieveMedia(final Callback callback) {
if (mCurrentState == State.INITIALIZED) {
// Nothing to do, execute callback immediately
callback.onMusicCatalogReady(true);
return;
}
// Asynchronously load the music catalog in a separate thread
new AsyncTask() {
@Override
protected Object doInBackground(Object[] objects) {
retrieveMediaAsync(callback);
return null;
}
}.execute();
}
private void retrieveMediaAsync(Callback callback) {
initializationLock.lock();
try {
if (mCurrentState == State.NON_INITIALIZED) {
mCurrentState = State.INITIALIZING;
int slashPos = CATALOG_URL.lastIndexOf('/');
String path = CATALOG_URL.substring(0, slashPos + 1);
JSONObject jsonObj = parseUrl(CATALOG_URL);
JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
if (tracks != null) {
for (int j = 0; j < tracks.length(); j++) {
MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path);
String genre = item.getString(MediaMetadata.METADATA_KEY_GENRE);
List<MediaMetadata> list = mMusicListByGenre.get(genre);
if (list == null) {
list = new ArrayList<>();
}
list.add(item);
mMusicListByGenre.put(genre, list);
mMusicListById.put(item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID),
item);
}
}
mCurrentState = State.INITIALIZED;
}
} catch (RuntimeException | JSONException e) {
LogHelper.e(TAG, e, "Could not retrieve music list");
} 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;
}
initializationLock.unlock();
if (callback != null) {
callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED);
}
}
}
private MediaMetadata 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
LogHelper.d(TAG, "Found music track: ", json);
// Media is stored relative to JSON file
if (!source.startsWith("http")) {
source = basePath + source;
}
if (!iconUrl.startsWith("http")) {
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 MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id)
.putString(CUSTOM_METADATA_TRACK_SOURCE, source)
.putString(MediaMetadata.METADATA_KEY_ALBUM, album)
.putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
.putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
.putString(MediaMetadata.METADATA_KEY_GENRE, genre)
.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl)
.putString(MediaMetadata.METADATA_KEY_TITLE, title)
.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber)
.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount)
.build();
}
/**
* Download a JSON file from a server, parse the content and return the JSON
* object.
*
* @param urlString
* @return
*/
private JSONObject parseUrl(String urlString) {
InputStream is = null;
try {
java.net.URL url = new java.net.URL(urlString);
URLConnection urlConnection = url.openConnection();
is = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(
urlConnection.getInputStream(), "iso-8859-1"));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return new JSONObject(sb.toString());
} catch (Exception e) {
LogHelper.e(TAG, "Failed to parse the json for media list", e);
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// ignore
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class BitmapHelper {
// Bitmap size for album art in media notifications when there are more than 3 playback actions
public static final int MEDIA_ART_SMALL_WIDTH=64;
public static final int MEDIA_ART_SMALL_HEIGHT=64;
// Bitmap size for album art in media notifications when there are no more than 3 playback actions
public static final int MEDIA_ART_BIG_WIDTH=128;
public static final int MEDIA_ART_BIG_HEIGHT=128;
public static final Bitmap scaleBitmap(int scaleFactor, InputStream is) {
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
Bitmap bitmap = BitmapFactory.decodeStream(is, null, bmOptions);
return bitmap;
}
public static final int findScaleFactor(int targetW, int targetH, InputStream is) {
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, bmOptions);
int actualW = bmOptions.outWidth;
int actualH = bmOptions.outHeight;
// Determine how much to scale down the image
return Math.min(actualW/targetW, actualH/targetH);
}
public static final Bitmap fetchAndRescaleBitmap(String uri, int width, int height)
throws IOException {
URL url = new URL(uri);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setDoInput(true);
httpConnection.connect();
InputStream inputStream = httpConnection.getInputStream();
int scaleFactor = findScaleFactor(width, height, inputStream);
httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setDoInput(true);
httpConnection.connect();
inputStream = httpConnection.getInputStream();
Bitmap bitmap = scaleBitmap(scaleFactor, inputStream);
return bitmap;
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice.utils;
import android.util.Log;
public class LogHelper {
public static void v(String tag, Object... messages) {
log(tag, Log.VERBOSE, null, messages);
}
public static void d(String tag, Object... messages) {
log(tag, Log.DEBUG, null, messages);
}
public static void i(String tag, Object... messages) {
log(tag, Log.INFO, null, messages);
}
public static void w(String tag, Object... messages) {
log(tag, Log.WARN, null, messages);
}
public static void w(String tag, Throwable t, Object... messages) {
log(tag, Log.WARN, t, messages);
}
public static void e(String tag, Object... messages) {
log(tag, Log.ERROR, null, messages);
}
public static void e(String tag, Throwable t, Object... messages) {
log(tag, Log.ERROR, t, messages);
}
public static void log(String tag, int level, Throwable t, Object... messages) {
if (messages != null && Log.isLoggable(tag, level)) {
String message;
if (messages.length == 1) {
message = messages[0] == null ? null : messages[0].toString();
} else {
StringBuilder sb = new StringBuilder();
for (Object m: messages) {
sb.append(m);
}
if (t != null) {
sb.append("\n").append(Log.getStackTraceString(t));
}
message = sb.toString();
}
Log.println(level, tag, message);
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice.utils;
import android.media.MediaMetadata;
/**
* Utility class to help on queue related tasks.
*/
public class MediaIDHelper {
private static final String TAG = "MediaIDHelper";
// Media IDs used on browseable items of MediaBrowser
public static final String MEDIA_ID_ROOT = "__ROOT__";
public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__";
public static final String createTrackMediaID(String categoryType, String categoryValue,
MediaMetadata track) {
// MediaIDs are of the form <categoryType>/<categoryValue>|<musicUniqueId>, to make it easy to
// find the category (like genre) that a music was selected from, so we
// can correctly build the playing queue. This is specially useful when
// one music can appear in more than one list, like "by genre -> genre_1"
// and "by artist -> artist_1".
return categoryType + "/" + categoryValue + "|" +
track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
}
public static final String createBrowseCategoryMediaID(String categoryType, String categoryValue) {
return categoryType + "/" + categoryValue;
}
/**
* Extracts unique musicID from the mediaID. mediaID is, by this sample's convention, a
* concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and unique
* musicID. This is necessary so we know where the user selected the music from, when the music
* exists in more than one music list, and thus we are able to correctly build the playing queue.
*
* @param musicID
* @return
*/
public static final String extractMusicIDFromMediaID(String musicID) {
String[] segments = musicID.split("\\|", 2);
return segments.length == 2 ? segments[1] : null;
}
/**
* Extracts category and categoryValue from the mediaID. mediaID is, by this sample's
* convention, a concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and
* mediaID. This is necessary so we know where the user selected the music from, when the music
* exists in more than one music list, and thus we are able to correctly build the playing queue.
*
* @param mediaID
* @return
*/
public static final String[] extractBrowseCategoryFromMediaID(String mediaID) {
if (mediaID.indexOf('|') >= 0) {
mediaID = mediaID.split("\\|")[0];
}
if (mediaID.indexOf('/') == 0) {
return new String[]{mediaID, null};
} else {
return mediaID.split("/", 2);
}
}
public static final String extractBrowseCategoryValueFromMediaID(String mediaID) {
String[] categoryAndValue = extractBrowseCategoryFromMediaID(mediaID);
if (categoryAndValue != null && categoryAndValue.length == 2) {
return categoryAndValue[1];
}
return null;
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.mediabrowserservice.utils;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import com.example.android.mediabrowserservice.model.MusicProvider;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
/**
* Utility class to help on queue related tasks.
*/
public class QueueHelper {
private static final String TAG = "QueueHelper";
public static final List<MediaSession.QueueItem> getPlayingQueue(String mediaId,
MusicProvider musicProvider) {
// extract the category and unique music ID from the media ID:
String[] category = MediaIDHelper.extractBrowseCategoryFromMediaID(mediaId);
// This sample only supports genre category.
if (!category[0].equals(MEDIA_ID_MUSICS_BY_GENRE) || category.length != 2) {
LogHelper.e(TAG, "Could not build a playing queue for this mediaId: ", mediaId);
return null;
}
String categoryValue = category[1];
LogHelper.e(TAG, "Creating playing queue for musics of genre ", categoryValue);
List<MediaSession.QueueItem> queue = convertToQueue(
musicProvider.getMusicsByGenre(categoryValue));
return queue;
}
public static final List<MediaSession.QueueItem> getPlayingQueueFromSearch(String query,
MusicProvider musicProvider) {
LogHelper.e(TAG, "Creating playing queue for musics from search ", query);
return convertToQueue(musicProvider.searchMusics(query));
}
public static final int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
String mediaId) {
int index = 0;
for (MediaSession.QueueItem item: queue) {
if (mediaId.equals(item.getDescription().getMediaId())) {
return index;
}
index++;
}
return -1;
}
public static final int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
long queueId) {
int index = 0;
for (MediaSession.QueueItem item: queue) {
if (queueId == item.getQueueId()) {
return index;
}
index++;
}
return -1;
}
private static final List<MediaSession.QueueItem> convertToQueue(
Iterable<MediaMetadata> tracks) {
List<MediaSession.QueueItem> queue = new ArrayList<>();
int count = 0;
for (MediaMetadata track : tracks) {
// We don't expect queues to change after created, so we use the item index as the
// queueId. Any other number unique in the queue would work.
MediaSession.QueueItem item = new MediaSession.QueueItem(
track.getDescription(), count++);
queue.add(item);
}
return queue;
}
/**
* Create a random queue. For simplicity sake, instead of a random queue, we create a
* queue using the first genre,
*
* @param musicProvider
* @return
*/
public static final List<MediaSession.QueueItem> getRandomQueue(MusicProvider musicProvider) {
Iterator<String> genres = musicProvider.getGenres().iterator();
if (!genres.hasNext()) {
return new ArrayList<>();
}
String genre = genres.next();
Iterable<MediaMetadata> tracks = musicProvider.getMusicsByGenre(genre);
return convertToQueue(tracks);
}
public static final boolean isIndexPlayable(int index, List<MediaSession.QueueItem> queue) {
return (queue != null && index >= 0 && index < queue.size());
}
}