Java tutorial
/* * Author: Scott Ware <scoot.software@gmail.com> * Copyright (c) 2015 Scott Ware * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.scooter1556.sms.android.manager; import android.app.Notification; import android.app.NotificationChannel; 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.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.RemoteException; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.ContextCompat; 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.v4.app.NotificationCompat; import android.util.Log; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.transition.Transition; import com.scooter1556.sms.android.R; import com.scooter1556.sms.android.activity.HomeActivity; import com.scooter1556.sms.android.service.MediaService; import static android.os.Build.*; import static android.os.Build.VERSION.*; /** * Manages media notification and updates it automatically for a given MediaSession. */ @RequiresApi(api = VERSION_CODES.O) public class MediaNotificationManager extends BroadcastReceiver { private static final String TAG = "NotificationManager"; private static final String CHANNEL_ID = "com.scooter1556.sms.android.CHANNEL_ID"; private static final int NOTIFICATION_ID = 412; private static final int REQUEST_CODE = 100; public static final String ACTION_PAUSE = "com.scooter1556.sms.android.pause"; public static final String ACTION_PLAY = "com.scooter1556.sms.android.play"; public static final String ACTION_PREV = "com.scooter1556.sms.android.prev"; public static final String ACTION_NEXT = "com.scooter1556.sms.android.next"; public static final String ACTION_STOP = "com.scooter1556.sms.android.stop"; public static final String ACTION_STOP_CASTING = "com.scooter1556.sms.android.stop_cast"; public static final int NOTIFICATION_ICON_SIZE = 200; private final MediaService mediaService; private MediaSessionCompat.Token mediaSessionToken; private MediaControllerCompat mediaController; private MediaControllerCompat.TransportControls transportControls; private PlaybackStateCompat playbackState; private MediaMetadataCompat mediaMetadata; private final NotificationManager notificationManager; private final PendingIntent pauseIntent; private final PendingIntent playIntent; private final PendingIntent previousIntent; private final PendingIntent nextIntent; private final PendingIntent stopIntent; private final PendingIntent stopCastIntent; private boolean started = false; public MediaNotificationManager(MediaService service) throws RemoteException { mediaService = service; updateSessionToken(); notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); String pkg = mediaService.getPackageName(); pauseIntent = PendingIntent.getBroadcast(mediaService, REQUEST_CODE, new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); playIntent = PendingIntent.getBroadcast(mediaService, REQUEST_CODE, new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); previousIntent = PendingIntent.getBroadcast(mediaService, REQUEST_CODE, new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); nextIntent = PendingIntent.getBroadcast(mediaService, REQUEST_CODE, new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); stopIntent = PendingIntent.getBroadcast(mediaService, REQUEST_CODE, new Intent(ACTION_STOP).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); stopCastIntent = PendingIntent.getBroadcast(mediaService, REQUEST_CODE, new Intent(ACTION_STOP_CASTING).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); // Cancel all notifications to handle the case where the service was killed and restarted by the system. notificationManager.cancelAll(); } /** * Posts the notification and starts tracking the session to keep it * updated. The notification will automatically be removed if the session is destroyed. */ public void startNotification() { if (!started) { mediaMetadata = mediaController.getMetadata(); playbackState = mediaController.getPlaybackState(); // The notification must be updated after setting started to true Notification notification = createNotification(); if (notification != null) { mediaController.registerCallback(mediaCallback); 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); mediaService.registerReceiver(this, filter); mediaService.startForeground(NOTIFICATION_ID, notification); started = true; } } } /** * Removes the notification and stops tracking the session. If the session * was destroyed this has no effect. */ public void stopNotification() { if (started) { started = false; mediaController.unregisterCallback(mediaCallback); try { notificationManager.cancel(NOTIFICATION_ID); mediaService.unregisterReceiver(this); } catch (IllegalArgumentException ex) { // Ignore if the receiver is not registered. } mediaService.stopForeground(true); } } @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); switch (action) { case ACTION_PAUSE: transportControls.pause(); break; case ACTION_PLAY: transportControls.play(); break; case ACTION_NEXT: transportControls.skipToNext(); break; case ACTION_PREV: transportControls.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); mediaService.startService(i); break; default: Log.w(TAG, "Unknown action 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. */ private void updateSessionToken() throws RemoteException { MediaSessionCompat.Token newToken = mediaService.getSessionToken(); if (mediaSessionToken == null && newToken != null || mediaSessionToken != null && !mediaSessionToken.equals(newToken)) { if (mediaController != null) { mediaController.unregisterCallback(mediaCallback); } mediaSessionToken = newToken; if (mediaSessionToken != null) { mediaController = new MediaControllerCompat(mediaService, mediaSessionToken); transportControls = mediaController.getTransportControls(); if (started) { mediaController.registerCallback(mediaCallback); } } } } private PendingIntent createContentIntent(MediaDescriptionCompat description) { Intent intent = new Intent(mediaService, HomeActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra(HomeActivity.EXTRA_START_FULLSCREEN, true); if (description != null) { intent.putExtra(HomeActivity.EXTRA_CURRENT_MEDIA_DESCRIPTION, description); } return PendingIntent.getActivity(mediaService, REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT); } private final MediaControllerCompat.Callback mediaCallback = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) { playbackState = state; if (state.getState() == PlaybackStateCompat.STATE_STOPPED || state.getState() == PlaybackStateCompat.STATE_NONE) { stopNotification(); } else { Notification notification = createNotification(); if (notification != null) { notificationManager.notify(NOTIFICATION_ID, notification); } } } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { mediaMetadata = metadata; Notification notification = createNotification(); if (notification != null) { notificationManager.notify(NOTIFICATION_ID, notification); } } @Override public void onSessionDestroyed() { super.onSessionDestroyed(); try { updateSessionToken(); } catch (RemoteException e) { Log.e(TAG, "Could not connect media controller", e); } } }; private Notification createNotification() { Log.d(TAG, "createNotification()"); if (mediaMetadata == null || playbackState == null) { return null; } MediaDescriptionCompat description = mediaMetadata.getDescription(); Bitmap art = BitmapFactory.decodeResource(mediaService.getResources(), R.drawable.ic_placeholder_audio); // Notification channels are only supported on Android O+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel(); } final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mediaService, CHANNEL_ID); final int playPauseButtonPosition = addActions(notificationBuilder); notificationBuilder.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle() // Show only play/pause in compact view .setShowActionsInCompactView(playPauseButtonPosition).setShowCancelButton(true) .setCancelButtonIntent(stopIntent).setMediaSession(mediaSessionToken)).setDeleteIntent(stopIntent) .setColor(ContextCompat.getColor(mediaService, R.color.primary)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC).setSmallIcon(R.drawable.ic_notification) .setOnlyAlertOnce(true).setContentIntent(createContentIntent(description)) .setContentTitle(description.getTitle()).setContentText(description.getSubtitle()) .setLargeIcon(art); if (mediaController != null && mediaController.getExtras() != null) { String castName = mediaController.getExtras().getString(MediaService.EXTRA_CONNECTED_CAST); if (castName != null) { String castInfo = mediaService.getResources().getString(R.string.cast_to_device, castName); notificationBuilder.setSubText(castInfo); notificationBuilder.addAction(R.drawable.ic_clear_black_24dp, mediaService.getString(R.string.cast_stop), stopCastIntent); } } setNotificationPlaybackState(notificationBuilder); if (description.getIconUri() != null) { String url = description.getIconUri().toString(); url = url + "?scale=" + NOTIFICATION_ICON_SIZE; Glide.with(mediaService).asBitmap().load(url) .into(new SimpleTarget<Bitmap>(NOTIFICATION_ICON_SIZE, NOTIFICATION_ICON_SIZE) { @Override public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) { notificationBuilder.setLargeIcon(resource); notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } }); } return notificationBuilder.build(); } private int addActions(final NotificationCompat.Builder notificationBuilder) { int playPauseButtonPosition = 0; // If skip to previous action is enabled if ((playbackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp, mediaService.getString(R.string.label_previous), previousIntent); /* * 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; } // Play or pause button, depending on the current state. final String label; final int icon; final PendingIntent intent; if (playbackState.getState() == PlaybackStateCompat.STATE_PLAYING) { label = mediaService.getString(R.string.label_pause); icon = R.drawable.ic_pause_white_24dp; intent = pauseIntent; } else { label = mediaService.getString(R.string.label_play); icon = R.drawable.ic_play_arrow_white_24dp; intent = playIntent; } notificationBuilder.addAction(new NotificationCompat.Action(icon, label, intent)); // If skip to next action is enabled if ((playbackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, mediaService.getString(R.string.label_next), nextIntent); } return playPauseButtonPosition; } private void setNotificationPlaybackState(NotificationCompat.Builder builder) { if (playbackState == null || !started) { mediaService.stopForeground(true); return; } // Make sure that the notification can be dismissed by the user when we are not playing: builder.setOngoing(playbackState.getState() == PlaybackStateCompat.STATE_PLAYING); } /** * Creates Notification Channel. This is required in Android O+ to display notifications. */ private void createNotificationChannel() { if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) { NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, mediaService.getString(R.string.notification_channel), NotificationManager.IMPORTANCE_LOW); notificationChannel.setDescription(mediaService.getString(R.string.notification_channel_description)); notificationManager.createNotificationChannel(notificationChannel); } } }