Java tutorial
/* * 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 nuclei.media; 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.os.Build; 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 io.nuclei.R; import nuclei.ui.util.ViewUtil; import nuclei.logs.Log; import nuclei.logs.Logs; /** * 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 { static final Log LOG = Logs.newLog(MediaNotificationManager.class); private static final int NOTIFICATION_ID = 412; private static final int REQUEST_CODE = 100; public static final String ACTION_PAUSE = "nuclei.PAUSE"; public static final String ACTION_PLAY = "nuclei.PLAY"; public static final String ACTION_PREV = "nuclei.PREV"; public static final String ACTION_NEXT = "nuclei.NEXT"; public static final String ACTION_STOP_CASTING = "nuclei.STOP_CAST"; public static final String ACTION_CANCEL = "nuclei.CANCEL"; private final MediaService mService; private MediaSessionCompat.Token mSessionToken; private MediaControllerCompat mController; private MediaControllerCompat.TransportControls mTransportControls; PlaybackStateCompat mPlaybackState; MediaMetadataCompat mMetadata; final NotificationManagerCompat mNotificationManager; private final PendingIntent mPauseIntent; private final PendingIntent mPlayIntent; private final PendingIntent mPreviousIntent; private final PendingIntent mNextIntent; private final PendingIntent mCancelIntent; private final PendingIntent mStopCastIntent; private final int mNotificationColor; private boolean mStarted = false; public MediaNotificationManager(MediaService service) throws RemoteException { mService = service; updateSessionToken(); mNotificationColor = ViewUtil.getThemeAttrColor(mService, R.attr.colorPrimary); 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); mStopCastIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, new Intent(ACTION_STOP_CASTING).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); mCancelIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, new Intent(ACTION_CANCEL).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); filter.addAction(ACTION_STOP_CASTING); filter.addAction(ACTION_CANCEL); mService.registerReceiver(this, filter); mService.startForeground(NOTIFICATION_ID, notification); mStarted = true; } } } /** * Removes the notification and stops tracking the session. If the session * was destroyed this has no effect. */ public void stopNotification() { if (mStarted) { mStarted = false; mController.unregisterCallback(mCb); try { mNotificationManager.cancel(NOTIFICATION_ID); mService.unregisterReceiver(this); } catch (IllegalArgumentException ex) { // ignore if the receiver is not registered. } mService.stopForeground(true); } } @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); LOG.d("Received intent with action " + action); switch (action) { case ACTION_PAUSE: mTransportControls.pause(); break; case ACTION_PLAY: mTransportControls.play(); break; case ACTION_NEXT: mTransportControls.skipToNext(); break; case ACTION_PREV: mTransportControls.skipToPrevious(); break; case ACTION_STOP_CASTING: Intent i = new Intent(context, MediaService.class); i.setAction(MediaService.ACTION_CMD); i.putExtra(MediaService.CMD_NAME, MediaService.CMD_STOP_CASTING); mService.startService(i); break; case ACTION_CANCEL: mTransportControls.stop(); break; default: if (LOG.isLoggable(Log.WARN)) LOG.w("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()}) */ 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 PendingIntent createContentIntent(MediaDescriptionCompat description) { try { MediaId id = MediaProvider.getInstance().getMediaId(description.getMediaId()); Intent openUI = new Intent(mService, id.type == MediaId.TYPE_AUDIO ? Configuration.AUDIO_ACTIVITY : Configuration.VIDEO_ACTIVITY); openUI.putExtra(MediaService.MEDIA_ID, description.getMediaId()); openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return PendingIntent.getActivity(mService, REQUEST_CODE, openUI, PendingIntent.FLAG_CANCEL_CURRENT); } catch (Exception err) { throw new RuntimeException(err); } } private final MediaControllerCompat.Callback mCb = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) { mPlaybackState = state; LOG.d("Received new playback state ", 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); } } } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { mMetadata = metadata; LOG.d("Received new metadata ", metadata); Notification notification = createNotification(); if (notification != null) { mNotificationManager.notify(NOTIFICATION_ID, notification); } } @Override public void onSessionDestroyed() { super.onSessionDestroyed(); LOG.d("Session was destroyed, resetting to the new session token"); try { updateSessionToken(); } catch (RemoteException e) { LOG.e("could not connect media controller", e); } } }; Notification createNotification() { LOG.d("updateNotificationMetadata. mMetadata=", mMetadata); if (mMetadata == null || mPlaybackState == null) { return null; } NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mService); int playPauseButtonPosition = 0; // If skip to previous action is enabled if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { notificationBuilder.addAction(ResourceProvider.getInstance().getDrawable(ResourceProvider.PREVIOUS), ResourceProvider.getInstance().getString(ResourceProvider.PREVIOUS), mPreviousIntent); // If there is a "skip to previous" button, the play/pause button will // be the second one. We need to keep track of it, because the MediaStyle notification // requires to specify the index of the buttons (actions) that should be visible // when in compact view. playPauseButtonPosition = 1; } addPlayPauseAction(notificationBuilder); // If skip to next action is enabled if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { notificationBuilder.addAction(ResourceProvider.getInstance().getDrawable(ResourceProvider.NEXT), ResourceProvider.getInstance().getString(ResourceProvider.NEXT), mNextIntent); } MediaDescriptionCompat description = mMetadata.getDescription(); Bitmap bitmap = description.getIconBitmap(); if (bitmap != null && bitmap.isRecycled()) bitmap = null; if (bitmap == null) { bitmap = mMetadata.getBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { notificationBuilder .setStyle( new NotificationCompat.MediaStyle().setShowActionsInCompactView(playPauseButtonPosition) // show only play/pause in compact view .setShowCancelButton(true).setCancelButtonIntent(mCancelIntent) .setMediaSession(mSessionToken)) .setContentIntent(createContentIntent(description)); } notificationBuilder.setColor(mNotificationColor).setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setUsesChronometer(true).setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setSmallIcon(ResourceProvider.getInstance().getDrawable(ResourceProvider.ICON_SMALL)) .setLargeIcon(bitmap); if (mController != null && mController.getExtras() != null) { String castName = mController.getExtras().getString(MediaService.EXTRA_CONNECTED_CAST); if (castName != null) { CharSequence castInfo = ResourceProvider.getInstance().getString(ResourceProvider.CASTING_TO_DEVICE, castName); notificationBuilder.setSubText(castInfo); notificationBuilder.addAction( ResourceProvider.getInstance().getDrawable(ResourceProvider.ICON_CLOSE), ResourceProvider.getInstance().getString(ResourceProvider.STOP_CASTING), mStopCastIntent); } } setNotificationPlaybackState(notificationBuilder); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) notificationBuilder.mActions.clear(); return notificationBuilder.build(); } private void addPlayPauseAction(NotificationCompat.Builder builder) { LOG.d("updatePlayPauseAction"); CharSequence label; int icon; PendingIntent intent; if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) { label = ResourceProvider.getInstance().getString(ResourceProvider.PAUSE); icon = ResourceProvider.getInstance().getDrawable(ResourceProvider.PAUSE); intent = mPauseIntent; } else { label = ResourceProvider.getInstance().getString(ResourceProvider.PLAY); icon = ResourceProvider.getInstance().getDrawable(ResourceProvider.PLAY); intent = mPlayIntent; } builder.addAction(new NotificationCompat.Action(icon, label, intent)); } private static final int MILLI = 1000; private void setNotificationPlaybackState(NotificationCompat.Builder builder) { LOG.d("updateNotificationPlaybackState. mPlaybackState=", mPlaybackState); if (mPlaybackState == null || !mStarted) { LOG.d("updateNotificationPlaybackState. cancelling notification!"); mService.stopForeground(true); return; } if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING && mPlaybackState.getPosition() >= 0) { LOG.d("updateNotificationPlaybackState. updating playback position to ", (System.currentTimeMillis() - mPlaybackState.getPosition()) / MILLI, " seconds"); builder.setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()).setShowWhen(true) .setUsesChronometer(true); } else { LOG.d("updateNotificationPlaybackState. hiding playback position"); builder.setWhen(0).setShowWhen(false).setUsesChronometer(false); } // Make sure that the notification can be dismissed by the user when we are not playing: builder.setOngoing(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING); } }