content: /*
   * Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
   *
   * This program 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 2
   * of the License, or (at your option) any later version.
   *
   * This program 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 this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   * MA  02110-1301, USA.
   */
  package com.achep.acdisplay.ui.components;
  
  import android.annotation.SuppressLint;
  import android.content.res.ColorStateList;
  import android.content.res.Resources;
  import android.graphics.Bitmap;
  import android.graphics.Color;
  import android.graphics.PorterDuff;
  import android.graphics.drawable.Drawable;
  import android.os.AsyncTask;
  import android.os.Handler;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import android.support.v4.media.session.PlaybackStateCompat;
  import androidx.palette.graphics.Palette;
  import android.transition.TransitionManager;
  import android.util.Log;
  import android.view.LayoutInflater;
  import android.view.View;
  import android.view.ViewGroup;
  import android.widget.ImageButton;
  import android.widget.ImageView;
  import android.widget.SeekBar;
  import android.widget.TextView;
  
  import com.achep.acdisplay.Atomic;
  import com.achep.acdisplay.Config;
  import com.achep.acdisplay.R;
  import com.achep.acdisplay.graphics.BackgroundFactory;
  import com.achep.acdisplay.services.media.MediaController2;
  import com.achep.acdisplay.services.media.Metadata;
  import com.achep.acdisplay.ui.fragments.AcDisplayFragment;
  import com.achep.base.Device;
  import com.achep.base.tests.Check;
  import com.achep.base.ui.drawables.PlayPauseDrawable;
  import com.achep.base.ui.drawables.RippleDrawable2;
  import com.achep.base.utils.Operator;
  import com.achep.base.utils.ResUtils;
  import com.achep.base.utils.RippleUtils;
  import com.achep.base.utils.ViewUtils;
  
  import static com.achep.base.Build.DEBUG;
  
  /**
   * Media widget for {@link com.achep.acdisplay.ui.fragments.AcDisplayFragment} that provides
   * basic media controls and has a nice skin.
   *
   * @author Artem Chepurnoy
   */
  public class MediaWidget extends Widget implements
          MediaController2.MediaListener,
          View.OnClickListener,
          View.OnLongClickListener, SeekBar.OnSeekBarChangeListener {
  
      private static final String TAG = "MediaWidget";
  
      private final MediaController2 mMediaController;
      private final PlayPauseDrawable mPlayPauseDrawable;
      private final Drawable mWarningDrawable;
  
      private ImageView mArtworkView;
      private TextView mTitleView;
      private TextView mSubtitleView;
      private ImageButton mButtonPrevious;
      private ImageButton mButtonPlayPause;
      private ImageButton mButtonNext;
      // Seek
      private ViewGroup mSeekLayout;
      private TextView mPositionText;
      private TextView mDurationText;
      private SeekBar mSeekBar;
  
      private boolean mIdle;
  
      private int mArtworkColor = Color.WHITE;
      private Bitmap mArtwork;
      private Bitmap mArtworkBackground;
      private AsyncTask<Bitmap, Void, Palette> mPaletteWorker;
      private AsyncTask<Void, Void, Bitmap> mBackgroundWorker;
  
      private final Palette.PaletteAsyncListener mPaletteCallback =
              new Palette.PaletteAsyncListener() {
                  @Override
                  public void onGenerated(@NonNull Palette palette) {
                      mArtworkColor = palette.getVibrantColor(Color.WHITE);
                      updatePlayPauseButtonColor(mArtworkColor);
                      updateSeekBarColor(mArtworkColor);
                  }
              };
  
      private final BackgroundFactory.BackgroundAsyncListener mBackgroundCallback =
              new BackgroundFactory.BackgroundAsyncListener() {
                  @Override
                  public void onGenerated(@NonNull Bitmap bitmap) {
                      mArtworkBackground = bitmap;
                      populateBackground();
                  }
              };
  
      private final Atomic.Callback mSeekAtomicCallback = new Atomic.Callback() {
  
          private static final int REFRESH_RATE = 1000; // ms.
  
          @NonNull
          private final Handler mHandler = new Handler();
          @NonNull
          private final Runnable mRunnable = new Runnable() {
              @Override
              public void run() {
                  if (mSeekBarTracking) {
                      // FIXME: Get rid of this workaround by implementing the states of
                      // AcDisplay fragment.
                      mCallback.requestTimeoutRestart(MediaWidget.this);
                  } else {
                      // Update the seek bar.
                      long position = mMediaController.getPlaybackPosition();
                      long duration = mMediaController.getMetadata().duration;
                      Check.getInstance().isTrue(duration > 0);
                      float ratio = (float) ((double) position / duration);
                      float progress = mSeekBar.getMax() * ratio;
                      mSeekBar.setProgress(Math.round(progress));
                      // Update the playback position text.
                      mPositionText.setText(formatTime(position));
                  }
                  // Refresh schedule.
                  if (mSeekUiAtomic.isRunning()) {
                      if (DEBUG) Log.d(TAG, "Seek bar refresh tick.");
                      mHandler.postDelayed(this, REFRESH_RATE);
                  }
              }
          };
  
          @Override
          public void onStart(Object... objects) {
              mHandler.post(mRunnable);
              mSeekLayout.setVisibility(View.VISIBLE);
          }
  
          @Override
          public void onStop(Object... objects) {
              mHandler.removeCallbacks(mRunnable);
              mSeekLayout.setVisibility(View.GONE);
              // Workaround for a bug with the transition manager,
              // which causes seek layout to be semi-transparent,
              // but not gone.
              mSeekLayout.postDelayed(new Runnable() {
                  @Override
                  public void run() {
                      mSeekLayout.setVisibility(View.VISIBLE);
                      mSeekLayout.setVisibility(View.GONE);
                      mSeekLayout.postInvalidate();
                  }
              }, 80);
          }
      };
  
      private final Atomic mSeekUiAtomic = new Atomic(mSeekAtomicCallback, "MediaWidget:SeekBar");
      private boolean mSeekBarTracking;
  
      public MediaWidget(@NonNull Callback callback, @NonNull AcDisplayFragment fragment) {
          super(callback, fragment);
          mMediaController = fragment.getMediaController2();
  
          Resources res = fragment.getResources();
          mPlayPauseDrawable = new PlayPauseDrawable();
          mPlayPauseDrawable.setSize(res.getDimensionPixelSize(R.dimen.media_btn_actual_size));
          mWarningDrawable = ResUtils.getDrawable(fragment.getActivity(), R.drawable.ic_action_warning_white);
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public boolean isHomeWidget() {
          return true;
      }
  
      @Override
      public void onViewAttached() {
          super.onViewAttached();
          mIdle = false;
          mMediaController.registerListener(this);
          onMetadataChanged(mMediaController.getMetadata());
          onPlaybackStateChanged(mMediaController.getPlaybackState());
          mIdle = true;
      }
  
      @Override
      public void onViewDetached() {
          mMediaController.unregisterListener(this);
          mSeekUiAtomic.stop();
          super.onViewDetached();
      }
  
      @Override
      public void onStop() {
          mSeekUiAtomic.stop();
          super.onStop();
      }
  
      @Override
      public void onMetadataChanged(@NonNull Metadata metadata) {
          populateMetadata();
          final Bitmap bitmap = metadata.bitmap;
  
          // Check if artwork are equals. If so, then we don't need to
          // generate everything from the beginning.
          if (mArtwork == bitmap || mArtwork != null && mArtwork.sameAs(bitmap)) {
              return;
          }
  
          mArtwork = bitmap;
          mArtworkBackground = null;
  
          com.achep.base.async.AsyncTask.stop(mPaletteWorker);
          com.achep.base.async.AsyncTask.stop(mBackgroundWorker);
          updatePlayPauseButtonColor(Color.WHITE); // Reset color
          updateSeekBarColor(Color.WHITE); // Reset color
  
          if (bitmap != null) {
              // TODO: Load the vibrant color only.
              mArtworkColor = Color.WHITE;
              mPaletteWorker = new Palette.Builder(bitmap)
                      .maximumColorCount(16)
                      .generate(mPaletteCallback);
  
              int dynamicBgMode = getConfig().getDynamicBackgroundMode();
              if (Operator.bitAnd(dynamicBgMode, getBackgroundMask())) {
                  mBackgroundWorker = BackgroundFactory.generateAsync(bitmap, mBackgroundCallback);
                  return; // Do not reset the background.
              }
          } else {
              mPaletteWorker = null;
          }
  
          mBackgroundWorker = null;
          populateBackground();
      }
  
      @Override
      public void onPlaybackStateChanged(int state) {
  
          // Making transformation rule for the warning icon is too
          // much overkill for me.
          if (state == PlaybackStateCompat.STATE_ERROR) {
              mButtonPlayPause.setImageDrawable(mWarningDrawable);
          } else {
              mButtonPlayPause.setImageDrawable(mPlayPauseDrawable);
          }
  
          if (DEBUG) Log.d(TAG, "Playback state is " + state);
  
          final int imageDescId;
          switch (state) {
              case PlaybackStateCompat.STATE_ERROR:
                  imageDescId = R.string.media_play_description;
                  break;
              case PlaybackStateCompat.STATE_PLAYING:
                  mPlayPauseDrawable.transformToPause();
                  imageDescId = R.string.media_pause_description;
                  break;
              case PlaybackStateCompat.STATE_BUFFERING:
              case PlaybackStateCompat.STATE_STOPPED:
                  mPlayPauseDrawable.transformToStop();
                  imageDescId = R.string.media_stop_description;
                  break;
              case PlaybackStateCompat.STATE_PAUSED:
              default:
                  mPlayPauseDrawable.transformToPlay();
                  imageDescId = R.string.media_play_description;
                  break;
          }
  
          mButtonPlayPause.setContentDescription(getFragment().getString(imageDescId));
      }
  
      /**
       * Updates the content of the view to latest metadata
       * provided by {@link com.achep.acdisplay.services.media.MediaController2#getMetadata()}.
       */
      private void populateMetadata() {
          if (mIdle) {
              ViewGroup vg = getView();
              if (Device.hasKitKatApi() && vg.isLaidOut() && getFragment().isAnimatable()) {
                  TransitionManager.beginDelayedTransition(vg);
              }
          }
  
          Metadata metadata = mMediaController.getMetadata();
          ViewUtils.safelySetText(mTitleView, metadata.title);
          ViewUtils.safelySetText(mSubtitleView, metadata.subtitle);
          mDurationText.setText(formatTime(metadata.duration));
          mSeekUiAtomic.stop();
          mSeekBar.setMax(Math.min(100, (int) (metadata.duration / 1000L)));
  
          if (mArtworkView != null) {
              mArtworkView.setImageBitmap(metadata.bitmap);
          }
      }
  
      /**
       * Requests host to update dynamic background.
       *
       * @see #getBackground()
       * @see #getBackgroundMask()
       */
      private void populateBackground() {
          if (isViewAttached()) {
              mCallback.requestBackgroundUpdate(this);
          }
      }
  
      @SuppressLint("NewApi")
      private void updatePlayPauseButtonColor(int color) {
          if (Device.hasLollipopApi()) {
              RippleDrawable2 rippleDrawable = (RippleDrawable2) mButtonPlayPause.getBackground();
              rippleDrawable.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
          } else {
              RippleUtils.makeFor(ColorStateList.valueOf(color), false, mButtonPlayPause);
          }
      }
  
      private void updateSeekBarColor(int color) {
          mSeekBar.getProgressDrawable().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
      }
  
      /**
       * {@inheritDoc}
       */
      @Nullable
      @Override
      public Bitmap getBackground() {
          return mArtworkBackground == null
                  ? mArtwork
                  : mArtworkBackground;
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public int getBackgroundMask() {
          return Config.DYNAMIC_BG_ARTWORK_MASK;
      }
  
      @Override
      protected ViewGroup onCreateView(
              @NonNull LayoutInflater inflater,
              @NonNull ViewGroup container,
              @Nullable ViewGroup sceneView) {
          boolean initialize = sceneView == null;
          if (initialize) {
              sceneView = (ViewGroup) inflater.inflate(R.layout.acdisplay_scene_music, container, false);
              assert sceneView != null;
          }
  
          mArtworkView = (ImageView) sceneView.findViewById(R.id.artwork);
          ViewGroup infoLayout = (ViewGroup) sceneView.findViewById(R.id.metadata);
          mTitleView = (TextView) infoLayout.findViewById(R.id.media_title);
          mSubtitleView = (TextView) infoLayout.findViewById(R.id.media_subtitle);
          mButtonPrevious = (ImageButton) sceneView.findViewById(R.id.previous);
          mButtonPlayPause = (ImageButton) sceneView.findViewById(R.id.play);
          mButtonNext = (ImageButton) sceneView.findViewById(R.id.next);
          mSeekLayout = (ViewGroup) sceneView.findViewById(R.id.seek_layout);
          mSeekBar = (SeekBar) mSeekLayout.findViewById(R.id.seek_bar);
          mPositionText = (TextView) mSeekLayout.findViewById(R.id.playback_position);
          mDurationText = (TextView) mSeekLayout.findViewById(R.id.duration);
  
          if (!initialize) {
              return sceneView;
          }
  
          mSeekBar.setOnSeekBarChangeListener(this);
          mButtonPrevious.setOnClickListener(this);
          mButtonPlayPause.setImageDrawable(mPlayPauseDrawable);
          mButtonPlayPause.setOnClickListener(this);
          mButtonPlayPause.setOnLongClickListener(this);
          mButtonNext.setOnClickListener(this);
  
          // Show the seek-panel on long click.
          infoLayout.setOnLongClickListener(new View.OnLongClickListener() {
              @Override
              public boolean onLongClick(View v) {
                  // Don't allow seeking on a weird song.
                  if (mMediaController.getMetadata().duration <= 0
                          || mMediaController.getPlaybackPosition() < 0) {
                      if (mSeekUiAtomic.isRunning()) {
                          toggleSeekUiVisibility();
                          return true;
                      }
                      return false;
                  }
  
                  toggleSeekUiVisibility();
                  return true;
              }
  
              private void toggleSeekUiVisibility() {
                  ViewGroup vg = getView();
                  if (Device.hasKitKatApi() && vg.isLaidOut() && getFragment().isAnimatable()) {
                      TransitionManager.beginDelayedTransition(vg);
                  }
                  mSeekUiAtomic.react(!mSeekUiAtomic.isRunning());
                  mCallback.requestTimeoutRestart(MediaWidget.this);
              }
          });
  
          if (Device.hasLollipopApi()) {
              // FIXME: Ripple doesn't work if the background is set (masked ripple works fine, but ugly).
              // Apply our own ripple drawable with slightly extended abilities, such
              // as setting color filter.
              ColorStateList csl = container.getResources().getColorStateList(R.color.ripple_dark);
              mButtonPlayPause.setBackground(new RippleDrawable2(csl, null, null));
          } else {
              RippleUtils.makeFor(false, true,
                      mButtonNext,
                      mButtonPlayPause,
                      mButtonPrevious);
          }
  
          updatePlayPauseButtonColor(mArtworkColor);
          updateSeekBarColor(mArtworkColor);
  
          return sceneView;
      }
  
      @Override
      public void onClick(@NonNull View v) {
          int action;
          if (v == mButtonPrevious) {
              action = MediaController2.ACTION_SKIP_TO_PREVIOUS;
          } else if (v == mButtonPlayPause) {
              action = MediaController2.ACTION_PLAY_PAUSE;
          } else if (v == mButtonNext) {
              action = MediaController2.ACTION_SKIP_TO_NEXT;
          } else {
              Log.wtf(TAG, "Received click event from an unknown view.");
              return;
          }
  
          mMediaController.sendMediaAction(action);
          mCallback.requestTimeoutRestart(this);
      }
  
      @Override
      public boolean onLongClick(@NonNull View v) {
          if (v == mButtonPlayPause) {
              mMediaController.sendMediaAction(MediaController2.ACTION_STOP);
          } else {
              Log.wtf(TAG, "Received long-click event from an unknown view.");
              return false;
          }
  
          mCallback.requestTimeoutRestart(this);
          return true;
      }
  
      //-- SEEKING SONGS --------------------------------------------------------
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
          if (fromUser) {
              final long position = getPlaybackSeekPosition();
              mPositionText.setText(formatTime(position));
          }
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onStartTrackingTouch(SeekBar seekBar) {
          mSeekBarTracking = true;
          mCallback.requestTimeoutRestart(MediaWidget.this);
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onStopTrackingTouch(SeekBar seekBar) {
          if (mSeekBarTracking) {
              final long position = getPlaybackSeekPosition();
              if (DEBUG) Log.d(TAG, "Seeking to " + position + " of "
                      + mMediaController.getMetadata().duration);
              mMediaController.seekTo(position);
          }
          mSeekBarTracking = false;
      }
  
      private long getPlaybackSeekPosition() {
          double pos = mSeekBar.getProgress();
          double max = mSeekBar.getMax();
          double ratio = pos / max;
          long duration = mMediaController.getMetadata().duration;
          return (long) Math.ceil(duration * ratio);
      }
  
      @NonNull
      private String formatTime(long time) {
          time /= 1000L; // get rid of millis.
          int s = (int) (time % 60L);
          int m = (int) (time / 60L);
          return formatNumber(m) + ":" + formatNumber(s);
      }
  
      @NonNull
      private String formatNumber(int a) {
          String str = Integer.toString(a);
          return a > 9 ? str : "0" + str;
      }
  
  }
  