summaryrefslogtreecommitdiffstats
path: root/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java
diff options
context:
space:
mode:
Diffstat (limited to 'samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java')
-rw-r--r--samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java381
1 files changed, 381 insertions, 0 deletions
diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java
new file mode 100644
index 000000000..7b8631a45
--- /dev/null
+++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java
@@ -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();
+ }
+
+}