chromium/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/mini/MiniPlayerLayout.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.readaloud.player.mini;

import static org.chromium.chrome.modules.readaloud.PlaybackListener.State.BUFFERING;
import static org.chromium.chrome.modules.readaloud.PlaybackListener.State.ERROR;
import static org.chromium.chrome.modules.readaloud.PlaybackListener.State.PAUSED;
import static org.chromium.chrome.modules.readaloud.PlaybackListener.State.PLAYING;
import static org.chromium.chrome.modules.readaloud.PlaybackListener.State.STOPPED;
import static org.chromium.chrome.modules.readaloud.PlaybackListener.State.UNKNOWN;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.annotation.ColorInt;

import org.chromium.chrome.browser.readaloud.player.Colors;
import org.chromium.chrome.browser.readaloud.player.InteractionHandler;
import org.chromium.chrome.browser.readaloud.player.R;
import org.chromium.chrome.browser.readaloud.player.TouchDelegateUtil;
import org.chromium.chrome.browser.readaloud.player.VisibilityState;
import org.chromium.chrome.modules.readaloud.PlaybackListener;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.interpolators.Interpolators;

/** Convenience class for manipulating mini player UI layout. */
public class MiniPlayerLayout extends LinearLayout {
    private static final long FADE_DURATION_MS = 300L;
    private static final Interpolator FADE_INTERPOLATOR =
            Interpolators.FAST_OUT_SLOW_IN_INTERPOLATOR;

    private TextView mTitle;
    private TextView mPublisher;
    private ProgressBar mProgressBar;
    private ImageView mPlayPauseView;
    private FrameLayout mBackdrop;
    private View mContents;

    // Layouts related to different playback states.
    private LinearLayout mNormalLayout;
    private LinearLayout mBufferingLayout;
    private LinearLayout mErrorLayout;

    private @PlaybackListener.State int mLastPlaybackState;
    private boolean mEnableAnimations;
    private InteractionHandler mInteractionHandler;
    private ObjectAnimator mAnimator;
    private @VisibilityState int mFinalVisibility;
    private MiniPlayerMediator mMediator;
    private float mFinalOpacity;
    private @ColorInt int mBackgroundColorArgb;
    private int mYOffset;

    /** Constructor for inflating from XML. */
    public MiniPlayerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mFinalVisibility = VisibilityState.GONE;
    }

    void destroy() {
        destroyAnimator();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        // Cache important views.
        mTitle = (TextView) findViewById(R.id.title);
        mPublisher = (TextView) findViewById(R.id.publisher);
        mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
        mPlayPauseView = (ImageView) findViewById(R.id.play_button);

        mBackdrop = (FrameLayout) findViewById(R.id.backdrop);
        mContents = findViewById(R.id.mini_player_container);
        mNormalLayout = (LinearLayout) findViewById(R.id.normal_layout);
        mBufferingLayout = (LinearLayout) findViewById(R.id.buffering_layout);
        mErrorLayout = (LinearLayout) findViewById(R.id.error_layout);

        // Set dynamic colors.
        Context context = getContext();
        mBackgroundColorArgb = Colors.getMiniPlayerBackgroundColor(context);
        Colors.setProgressBarColor(mProgressBar);
        findViewById(R.id.backdrop).setBackgroundColor(mBackgroundColorArgb);
        if (mMediator != null) {
            mMediator.onBackgroundColorUpdated(mBackgroundColorArgb);
        }

        // TODO: Plug in WindowAndroid and use #isWindowOnTablet instead
        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(context)) {
            int paddingPx =
                    context.getResources()
                            .getDimensionPixelSize(R.dimen.readaloud_mini_player_tablet_padding);
            View container = findViewById(R.id.mini_player_container);
            container.setPadding(paddingPx, 0, paddingPx, 0);
        }
        mLastPlaybackState = PlaybackListener.State.UNKNOWN;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        int height = mBackdrop.getHeight();
        if (height == 0) {
            return;
        }

        if (mMediator != null) {
            mMediator.onBackgroundColorUpdated(mBackgroundColorArgb);
            mMediator.onHeightKnown(height);
        }

        // Make the close button touch target bigger.
        TouchDelegateUtil.setBiggerTouchTarget(findViewById(R.id.close_button));
    }

    void changeOpacity(float startValue, float endValue) {
        assert (mMediator != null)
                : "Can't call changeOpacity() before setMediator() which should happen during"
                        + " mediator init.";
        if (endValue == mFinalOpacity) {
            return;
        }
        mFinalOpacity = endValue;
        setAlpha(startValue);

        View nonErrorLayoutContainer = mErrorLayout.getVisibility() == View.GONE ? mContents : null;
        Runnable onFinished =
                endValue == 1f
                        ? (() -> mMediator.onFullOpacityReached(nonErrorLayoutContainer))
                        : mMediator::onZeroOpacityReached;

        if (mEnableAnimations) {
            // TODO: handle case where existing animation is incomplete and needs to be reversed
            destroyAnimator();
            mAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, endValue);
            mAnimator.setDuration(FADE_DURATION_MS);
            mAnimator.setInterpolator(FADE_INTERPOLATOR);
            mAnimator.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            destroyAnimator();
                            onFinished.run();
                        }
                    });
            mAnimator.start();
        } else {
            setAlpha(endValue);
            onFinished.run();
        }
    }

    void enableAnimations(boolean enable) {
        mEnableAnimations = enable;
    }

    void setTitle(String title) {
        mTitle.setText(title);
    }

    void setPublisher(String publisher) {
        mPublisher.setText(publisher);
    }

    /**
     * Set progress bar progress.
     * @param progress Fraction of playback completed in range [0, 1]
     */
    void setProgress(float progress) {
        mProgressBar.setProgress((int) (progress * mProgressBar.getMax()), true);
    }

    /**
     * Set the yOffset of the mini player layout. If yOffset < 0, the view need to shift up from the
     * bottom. It is implemented by applying a bottom margin.
     */
    void setYOffset(int yOffset) {
        if (mYOffset == yOffset) return;

        assert yOffset <= 0;

        mYOffset = -yOffset;
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        mlp.bottomMargin = mYOffset;
        setLayoutParams(mlp);
    }

    void setInteractionHandler(InteractionHandler handler) {
        mInteractionHandler = handler;
        setOnClickListener(R.id.close_button, handler::onCloseClick);
        setOnClickListener(R.id.mini_player_container, handler::onMiniPlayerExpandClick);
        setOnClickListener(R.id.play_button, handler::onPlayPauseClick);
    }

    void setMediator(MiniPlayerMediator mediator) {
        mMediator = mediator;
    }

    void onPlaybackStateChanged(@PlaybackListener.State int state) {
        switch (state) {
                // UNKNOWN is currently the "reset" state and can be treated same as buffering.
            case BUFFERING:
            case UNKNOWN:
                showOnly(mBufferingLayout);
                mProgressBar.setVisibility(View.GONE);
                break;

            case ERROR:
                showOnly(mErrorLayout);
                mProgressBar.setVisibility(View.GONE);
                break;

            case PLAYING:
                if (mLastPlaybackState != PLAYING && mLastPlaybackState != PAUSED) {
                    showOnly(mNormalLayout);
                    mProgressBar.setVisibility(View.VISIBLE);
                }

                mPlayPauseView.setImageResource(R.drawable.mini_pause_button);
                mPlayPauseView.setContentDescription(
                        getResources().getString(R.string.readaloud_pause));
                break;

            case STOPPED:
            case PAUSED:
                // Buffering/unknown and error states have their own views, show back the normal
                // layout if needed
                if (mLastPlaybackState != PLAYING
                        && mLastPlaybackState != PAUSED
                        && mLastPlaybackState != ERROR) {
                    showOnly(mNormalLayout);
                    mProgressBar.setVisibility(View.VISIBLE);
                }
                mPlayPauseView.setImageResource(R.drawable.mini_play_button);
                mPlayPauseView.setContentDescription(
                        getResources().getString(R.string.readaloud_play));
                break;

            default:
                break;
        }
        mLastPlaybackState = state;
    }

    // Show `layout` and hide the other two.
    private void showOnly(LinearLayout layout) {
        setVisibleIfMatch(mNormalLayout, layout);
        setVisibleIfMatch(mBufferingLayout, layout);
        setVisibleIfMatch(mErrorLayout, layout);
    }

    private static void setVisibleIfMatch(LinearLayout a, LinearLayout b) {
        a.setVisibility(a == b ? View.VISIBLE : View.GONE);
    }

    private void setOnClickListener(int id, Runnable handler) {
        findViewById(id)
                .setOnClickListener(
                        (v) -> {
                            handler.run();
                        });
    }

    private void destroyAnimator() {
        if (mAnimator != null) {
            mAnimator.removeAllListeners();
            mAnimator.cancel();
            mAnimator = null;
        }
    }

    ObjectAnimator getAnimatorForTesting() {
        return mAnimator;
    }
}