com.achep.acdisplay.ui.components.MediaWidget.java Source code

Java tutorial

Introduction

Here is the source code for com.achep.acdisplay.ui.components.MediaWidget.java

Source

/*
 * 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;
    }

}