content: /*
   * 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.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 androidx.annotation.NonNull;
  import androidx.annotation.RequiresApi;
  import androidx.core.app.NotificationCompat;
  import androidx.core.app.NotificationManagerCompat;
  import android.support.v4.media.MediaDescriptionCompat;
  import android.support.v4.media.MediaMetadataCompat;
  import androidx.media.app.NotificationCompat.MediaStyle;
  import androidx.media.session.MediaButtonReceiver;
  import android.support.v4.media.session.MediaControllerCompat;
  import android.support.v4.media.session.MediaSessionCompat;
  import android.support.v4.media.session.PlaybackStateCompat;
  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 static final String CHANNEL_ID = "media_playback_channel";
      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);
                  }
              }
          }
      }
  
      @RequiresApi(Build.VERSION_CODES.O)
      private void createChannel() {
          NotificationManager mNotificationManager = (NotificationManager) mService.
              getSystemService(Context.NOTIFICATION_SERVICE);
          // The user-visible name of the channel.
          String name = "Media Playback";
          // The user-visible description of the channel.
          String description = "Used for playing songs";
          int importance = NotificationManager.IMPORTANCE_LOW;
          NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
          // Configure the notification channel.
          mChannel.setDescription(description);
          mChannel.setShowBadge(false);
          mChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
          mNotificationManager.createNotificationChannel(mChannel);
      }
  
  
      private Notification createNotification() {
          if (mMetadata == null || mPlaybackState == null) {
              return null;
          }
  
          NotificationCompat.Builder notificationBuilder =
              new NotificationCompat.Builder(mService, CHANNEL_ID);
          notificationBuilder.addAction(R.drawable.ic_skip_previous_36dp,
              mService.getString(R.string.label_previous), mPreviousIntent);
  
          addPlayPauseAction(notificationBuilder);
  
          notificationBuilder.addAction(R.drawable.ic_skip_next_36dp,
              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);
          }
  
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
              createChannel();
          }
  
          notificationBuilder
              .setStyle(
                  new MediaStyle()
                      .setMediaSession(mSessionToken)
                      .setShowActionsInCompactView(1)
                      .setShowCancelButton(true)
                      .setCancelButtonIntent(
                          MediaButtonReceiver.buildMediaButtonPendingIntent(
                              mService, PlaybackStateCompat.ACTION_STOP)))
              .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 = R.drawable.ic_pause_36dp;
              intent = mPauseIntent;
          } else {
              label = mService.getString(R.string.label_play);
              icon = R.drawable.ic_play_36dp;
              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);
          }
      }
  }
  