/* * 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 android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v7.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; } }