Java tutorial
/* * Copyright (c) 2014 Amahi * * This file is part of Amahi. * * Amahi is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Amahi is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Amahi. If not, see <http ://www.gnu.org/licenses/>. */ package org.amahi.anywhere.util; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.RemoteException; import android.support.annotation.NonNull; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v7.app.NotificationCompat; import android.util.Log; import org.amahi.anywhere.R; import org.amahi.anywhere.service.AudioService; /** * Keeps track of a notification and updates it automatically for a given * MediaSession. Maintaining a visible notification (usually) guarantees that the music service * won't be killed during playback. */ public class MediaNotificationManager extends BroadcastReceiver { public static final String ACTION_PAUSE = "org.amahi.anywhere.pause"; public static final String ACTION_PLAY = "org.amahi.anywhere.play"; public static final String ACTION_PREV = "org.amahi.anywhere.prev"; public static final String ACTION_NEXT = "org.amahi.anywhere.next"; private static final int NOTIFICATION_ID = 412; private static final int REQUEST_CODE = 100; private static final String TAG = "notification_manager"; private final AudioService mService; private final NotificationManagerCompat mNotificationManager; private final PendingIntent mPauseIntent; private final PendingIntent mPlayIntent; private final PendingIntent mPreviousIntent; private final PendingIntent mNextIntent; private MediaSessionCompat.Token mSessionToken; private MediaControllerCompat mController; private MediaControllerCompat.TransportControls mTransportControls; private PlaybackStateCompat mPlaybackState; private MediaMetadataCompat mMetadata; private boolean mStarted = false; private final MediaControllerCompat.Callback mCb = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) { mPlaybackState = state; Log.d(TAG, "Received new playback state"); if (state.getState() == PlaybackStateCompat.STATE_STOPPED || state.getState() == PlaybackStateCompat.STATE_NONE) { stopNotification(); } else { Notification notification = createNotification(); if (notification != null) { mNotificationManager.notify(NOTIFICATION_ID, notification); } if (state.getState() == PlaybackStateCompat.STATE_PAUSED) { pauseNotification(); } } } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { mMetadata = metadata; Log.d(TAG, "Received new metadata"); Notification notification = createNotification(); if (notification != null) { mNotificationManager.notify(NOTIFICATION_ID, notification); } } @Override public void onSessionDestroyed() { super.onSessionDestroyed(); Log.d(TAG, "Session was destroyed, resetting to the new session token"); try { updateSessionToken(); } catch (RemoteException e) { Log.e(TAG, "could not connect media controller", e); } } }; public MediaNotificationManager(AudioService service) throws RemoteException { mService = service; updateSessionToken(); mNotificationManager = NotificationManagerCompat.from(service); String pkg = mService.getPackageName(); mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); // Cancel all notifications to handle the case where the Service was killed and // restarted by the system. mNotificationManager.cancelAll(); } /** * Posts the notification and starts tracking the session to keep it * updated. The notification will automatically be removed if the session is * destroyed before {@link #stopNotification} is called. */ public void startNotification() { if (!mStarted) { mMetadata = mController.getMetadata(); mPlaybackState = mController.getPlaybackState(); // The notification must be updated after setting started to true Notification notification = createNotification(); if (notification != null) { mController.registerCallback(mCb); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_NEXT); filter.addAction(ACTION_PAUSE); filter.addAction(ACTION_PLAY); filter.addAction(ACTION_PREV); mService.registerReceiver(this, filter); mService.startForeground(NOTIFICATION_ID, notification); mStarted = true; } } } /** * Removes the notification and stops tracking the session. If the session * was destroyed this has no effect. */ public void stopNotification() { if (mStarted) { mStarted = false; mController.unregisterCallback(mCb); try { mNotificationManager.cancel(NOTIFICATION_ID); mService.unregisterReceiver(this); } catch (IllegalArgumentException ex) { // ignore if the receiver is not registered. } mService.stopForeground(true); } } private void pauseNotification() { if (mStarted) { mService.stopForeground(false); } } @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); switch (action) { case ACTION_PAUSE: mTransportControls.pause(); pauseNotification(); break; case ACTION_PLAY: mTransportControls.play(); break; case ACTION_NEXT: mTransportControls.skipToNext(); break; case ACTION_PREV: mTransportControls.skipToPrevious(); break; default: Log.w(TAG, "Unknown intent ignored. Action=" + action); } } /** * Update the state based on a change on the session token. Called either when * we are running for the first time or when the media session owner has destroyed the session * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) */ private void updateSessionToken() throws RemoteException { MediaSessionCompat.Token freshToken = mService.getSessionToken(); if (mSessionToken == null && freshToken != null || mSessionToken != null && !mSessionToken.equals(freshToken)) { if (mController != null) { mController.unregisterCallback(mCb); } mSessionToken = freshToken; if (mSessionToken != null) { mController = new MediaControllerCompat(mService, mSessionToken); mTransportControls = mController.getTransportControls(); if (mStarted) { mController.registerCallback(mCb); } } } } private Notification createNotification() { Log.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); if (mMetadata == null || mPlaybackState == null) { return null; } NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mService); notificationBuilder.addAction(android.R.drawable.ic_media_previous, mService.getString(R.string.label_previous), mPreviousIntent); addPlayPauseAction(notificationBuilder); notificationBuilder.addAction(android.R.drawable.ic_media_next, mService.getString(R.string.label_next), mNextIntent); MediaDescriptionCompat description = mMetadata.getDescription(); Bitmap audioAlbumArt = mMetadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART); if (audioAlbumArt == null) { // use a placeholder art while the remote art is being downloaded audioAlbumArt = BitmapFactory.decodeResource(mService.getResources(), R.drawable.default_audiotrack); } notificationBuilder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(1) // show only play/pause in compact view .setMediaSession(mSessionToken)).setSmallIcon(getAudioPlayerNotificationIcon()) .setLargeIcon(getAudioPlayerNotificationArtwork(audioAlbumArt)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(mService.createContentIntent()).setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setOngoing(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING); // setNotificationPlaybackState(notificationBuilder); return notificationBuilder.build(); } private int getAudioPlayerNotificationIcon() { return R.drawable.ic_launcher; } private Bitmap getAudioPlayerNotificationArtwork(Bitmap audioAlbumArt) { int iconHeight = (int) mService.getResources().getDimension(android.R.dimen.notification_large_icon_height); int iconWidth = (int) mService.getResources().getDimension(android.R.dimen.notification_large_icon_width); if (audioAlbumArt == null) { return null; } return Bitmap.createScaledBitmap(audioAlbumArt, iconWidth, iconHeight, false); } private void addPlayPauseAction(NotificationCompat.Builder builder) { Log.d(TAG, "updatePlayPauseAction"); String label; int icon; PendingIntent intent; if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) { label = mService.getString(R.string.label_pause); icon = android.R.drawable.ic_media_pause; intent = mPauseIntent; } else { label = mService.getString(R.string.label_play); icon = android.R.drawable.ic_media_play; intent = mPlayIntent; } builder.addAction(new NotificationCompat.Action(icon, label, intent)); } private void setNotificationPlaybackState(NotificationCompat.Builder builder) { if (mPlaybackState == null || !mStarted) { mService.stopForeground(true); return; } if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING && mPlaybackState.getPosition() >= 0) { builder.setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()).setShowWhen(true) .setUsesChronometer(true); } else { builder.setWhen(0).setShowWhen(false).setUsesChronometer(false); } } }