chromium/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContent.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.expanded;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.chrome.browser.readaloud.player.Colors;
import org.chromium.chrome.browser.readaloud.player.InteractionHandler;
import org.chromium.chrome.browser.readaloud.player.PlayerProperties;
import org.chromium.chrome.browser.readaloud.player.R;
import org.chromium.chrome.browser.readaloud.player.TouchDelegateUtil;
import org.chromium.chrome.modules.readaloud.PlaybackListener;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.ui.modelutil.PropertyModel;

public class ExpandedPlayerSheetContent implements BottomSheetContent {
    private static final String TAG = "RAPlayerSheet";
    // Note: if these times need to change, the "back 10" and "forward 10" icons
    // should also be changed.
    private static final int BACK_SECONDS = 10;
    private static final int FORWARD_SECONDS = 10;

    private final Context mContext;
    private final BottomSheetController mBottomSheetController;
    private final PropertyModel mModel;
    private final SeekBar mSeekBar;
    private final ScrollView mScrollView;
    private final LinearLayout mPlayerControls;
    private View mContentView;
    // Effectively final and non null, can be null only in tests
    private OptionsMenuSheetContent mOptionsMenu;
    private SpeedMenuSheetContent mSpeedMenu;
    private TextView mSpeedButton;
    private boolean mHighlightingEnabled;
    private boolean mHighlightingSupported;

    private LinearLayout mNormalLayout;
    private LinearLayout mErrorLayout;

    public ExpandedPlayerSheetContent(
            Context context, BottomSheetController bottomSheetController, PropertyModel model) {
        this(
                context,
                bottomSheetController,
                LayoutInflater.from(context)
                        .inflate(R.layout.readaloud_expanded_player_layout, null),
                model);
        mOptionsMenu =
                new OptionsMenuSheetContent(
                        mContext, /* parent= */ this, mBottomSheetController, mModel);
        mSpeedMenu =
                new SpeedMenuSheetContent(
                        mContext, /* parent= */ this, mBottomSheetController, mModel);
    }

    @VisibleForTesting
    ExpandedPlayerSheetContent(
            Context context,
            BottomSheetController bottomSheetController,
            View contentView,
            PropertyModel model) {
        mContext = context;
        mBottomSheetController = bottomSheetController;
        mContentView = contentView;
        mModel = model;
        Resources res = mContext.getResources();
        mSpeedButton = (TextView) mContentView.findViewById(R.id.readaloud_playback_speed);
        mContentView
                .findViewById(R.id.readaloud_seek_back_button)
                .setContentDescription(res.getString(R.string.readaloud_replay, BACK_SECONDS));
        mContentView
                .findViewById(R.id.readaloud_seek_forward_button)
                .setContentDescription(res.getString(R.string.readaloud_forward, FORWARD_SECONDS));
        mNormalLayout = (LinearLayout) mContentView.findViewById(R.id.normal_layout);
        mErrorLayout = (LinearLayout) mContentView.findViewById(R.id.error_layout);
        mSeekBar = (SeekBar) mContentView.findViewById(R.id.readaloud_expanded_player_seek_bar);
        mScrollView = (ScrollView) mContentView.findViewById(R.id.scroll_view);

        View publisherButton = mContentView.findViewById(R.id.readaloud_player_publisher_button);
        publisherButton.addOnLayoutChangeListener(
                new View.OnLayoutChangeListener() {
                    @Override
                    public void onLayoutChange(
                            View v,
                            int left,
                            int top,
                            int right,
                            int bottom,
                            int oldLeft,
                            int oldTop,
                            int oldRight,
                            int oldBottom) {
                        TouchDelegateUtil.setBiggerTouchTarget(publisherButton);
                    }
                });

        mSeekBar.setAccessibilityDelegate(
                new View.AccessibilityDelegate() {
                    @Override
                    public void onInitializeAccessibilityEvent(
                            View host, AccessibilityEvent event) {
                        // Drop progress announcements that repeatedly interrupt playback.
                        if (event.getEventType()
                                == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
                            return;
                        }
                        super.onInitializeAccessibilityEvent(host, event);
                    }
                });
        mPlayerControls =
                (LinearLayout) mContentView.findViewById(R.id.readaloud_playback_controls);
        // Apply dynamic colors.
        Colors.setBottomSheetContentBackground(mContentView);
        Colors.setProgressBarColor(mSeekBar);

        onOrientationChange(res.getConfiguration().orientation);
    }

    public void onPlaybackStateChanged(@PlaybackListener.State int state) {
        setPlaying(state == PlaybackListener.State.PLAYING);
        if (state == PlaybackListener.State.ERROR) {
            showOnly(mErrorLayout);
        } else {
            showOnly(mNormalLayout);
        }
    }

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

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

    public void show() {
        mBottomSheetController.requestShowContent(this, /* animate= */ true);
        // Reset scrolling if needed.
        mScrollView.scrollTo(0, 0);
    }

    public void hide() {
        mBottomSheetController.hideContent(this, /* animate= */ true);
    }

    void setTitle(String title) {
        ((TextView) mContentView.findViewById(R.id.readaloud_expanded_player_title)).setText(title);
    }

    void setPublisher(String publisher) {
        ((TextView) mContentView.findViewById(R.id.readaloud_expanded_player_publisher))
                .setText(publisher);
    }

    void setElapsed(Long nanos) {
        ((TextView) mContentView.findViewById(R.id.readaloud_player_time))
                .setText(formatTimeNanos(nanos));
    }

    void setDuration(Long nanos) {
        ((TextView) mContentView.findViewById(R.id.readaloud_player_duration))
                .setText(formatTimeNanos(nanos));
    }

    private static String formatTimeNanos(long nanos) {
        if (nanos <= 0) {
            return DateUtils.formatElapsedTime(0);
        }
        final long nanosPerSecond = 1_000_000_000L;
        long seconds = nanos / nanosPerSecond;
        return DateUtils.formatElapsedTime(seconds);
    }

    void setInteractionHandler(InteractionHandler handler) {
        setOnClickListener(R.id.readaloud_play_pause_button, handler::onPlayPauseClick);
        setOnClickListener(R.id.readaloud_seek_back_button, handler::onSeekBackClick);
        setOnClickListener(R.id.readaloud_seek_forward_button, handler::onSeekForwardClick);
        setOnClickListener(R.id.readaloud_expanded_player_publisher, handler::onPublisherClick);
        setOnClickListener(R.id.readaloud_playback_speed, this::showSpeedMenu);
        setOnClickListener(R.id.readaloud_more_button, this::showOptionsMenu);

        SeekBar seekBar =
                (SeekBar) mContentView.findViewById(R.id.readaloud_expanded_player_seek_bar);
        seekBar.setOnSeekBarChangeListener(handler.getSeekBarChangeListener());
        mSpeedMenu.setInteractionHandler(handler);
        mOptionsMenu.setInteractionHandler(handler);
    }

    public void setSpeed(float speed) {
        mModel.set(PlayerProperties.SPEED, speed);
        String speedString = SpeedMenuSheetContent.speedFormatter(speed);
        mSpeedButton.setText(
                mContext.getResources().getString(R.string.readaloud_speed, speedString));
        mSpeedButton.setContentDescription(
                mContext.getResources()
                        .getString(R.string.readaloud_speed_menu_button, speedString));
    }

    void setHighlightingSupported(boolean supported) {
        mOptionsMenu.setHighlightingSupported(supported);
    }

    void setHighlightingEnabled(boolean enabled) {
        mOptionsMenu.setHighlightingEnabled(enabled);
    }

    public void setPlaying(boolean playing) {
        ImageView playButton =
                (ImageView) mContentView.findViewById(R.id.readaloud_play_pause_button);
        // If playing, update to show the pause button.
        if (playing) {
            playButton.setImageResource(R.drawable.pause_button);
            playButton.setContentDescription(
                    mContext.getResources().getString(R.string.readaloud_pause));
        } else {
            playButton.setImageResource(R.drawable.play_button);
            playButton.setContentDescription(
                    mContext.getResources().getString(R.string.readaloud_play));
        }
    }

    /**
     * @param percentProgress out of 1.0
     */
    public void setProgress(float percent) {
        mSeekBar.setProgress((int) (percent * mSeekBar.getMax()), true);
    }

    @Nullable
    OptionsMenuSheetContent getOptionsMenu() {
        return mOptionsMenu;
    }

    public void showOptionsMenu() {
        // set bit saying we're waiting for another sheet
        mModel.set(PlayerProperties.SHOW_MINI_PLAYER_ON_DISMISS, false);
        mBottomSheetController.hideContent(this, /* animate= */ false);
        mBottomSheetController.requestShowContent(mOptionsMenu, /* animate= */ true);
    }

    @Nullable
    VoiceMenu getVoiceMenu() {
        if (mOptionsMenu == null) {
            return null;
        }
        return mOptionsMenu.getVoiceMenu();
    }

    public void notifySheetClosed(BottomSheetContent contentClosed) {
        mOptionsMenu.notifySheetClosed(contentClosed);
        mSpeedMenu.notifySheetClosed(contentClosed);
    }

    public void showSpeedMenu() {
        // set bit saying we're waiting for another sheet
        mModel.set(PlayerProperties.SHOW_MINI_PLAYER_ON_DISMISS, false);
        mBottomSheetController.hideContent(this, /* animate= */ false);
        mBottomSheetController.requestShowContent(mSpeedMenu, /* animate= */ true);
    }

    // BottomSheetContent implementation

    @Override
    public View getContentView() {
        return mContentView;
    }

    @Override
    @Nullable
    public View getToolbarView() {
        return null;
    }

    @Override
    public int getVerticalScrollOffset() {
        return mScrollView.getScrollY();
    }

    @Override
    public void destroy() {}

    @Override
    @ContentPriority
    public int getPriority() {
        // The player is persistent. If another bottom sheet wants to show, this one
        // should hide temporarily.
        return BottomSheetContent.ContentPriority.LOW;
    }

    @Override
    public boolean swipeToDismissEnabled() {
        // The user can dismiss the expanded player by swiping it.
        return true;
    }

    @Override
    public boolean hasCustomLifecycle() {
        // Dismiss if the user navigates the page, switches tabs, or changes layout.
        return false;
    }

    @Override
    public boolean hasCustomScrimLifecycle() {
        // Don't show a scrim when open (gray overlay on page).
        return true;
    }

    @Override
    public int getPeekHeight() {
        // Only full height mode enabled.
        return HeightMode.DISABLED;
    }

    @Override
    public float getHalfHeightRatio() {
        // Only full height mode enabled.
        return HeightMode.DISABLED;
    }

    @Override
    public float getFullHeightRatio() {
        return HeightMode.WRAP_CONTENT;
    }

    @Override
    public int getSheetContentDescriptionStringId() {
        // "'Listen to this page' player."
        // Automatically appended: "Swipe down to close."
        return R.string.readaloud_player_name;
    }

    @Override
    public int getSheetHalfHeightAccessibilityStringId() {
        Log.e(
                TAG,
                "Tried to get half height accessibility string, but half height isn't supported.");
        assert false;
        return 0;
    }

    @Override
    public int getSheetFullHeightAccessibilityStringId() {
        // "Read Aloud player opened at full height."
        return R.string.readaloud_player_opened_at_full_height;
    }

    @Override
    public int getSheetClosedAccessibilityStringId() {
        // "Read Aloud player minimized."
        return R.string.readaloud_player_minimized;
    }

    @Override
    public boolean canSuppressInAnyState() {
        // Always immediately hide if a higher-priority sheet content wants to show.
        return true;
    }

    private void setOnClickListener(int id, Runnable onClick) {
        mContentView
                .findViewById(id)
                .setOnClickListener(
                        (view) -> {
                            onClick.run();
                        });
    }

    /** Customize portraint and landscape mode sheets. Landscape mode layout is more compressed. */
    public void onOrientationChange(int orientation) {
        if (mOptionsMenu != null) {
            mOptionsMenu.onOrientationChange(orientation);
        }
        if (mSpeedMenu != null) {
            mSpeedMenu.onOrientationChange(orientation);
        }
        TextView chromeNowPlaying = mContentView.findViewById(R.id.chrome_now_playing_text);
        ViewGroup.MarginLayoutParams chromeNowPlayingParams =
                (ViewGroup.MarginLayoutParams) chromeNowPlaying.getLayoutParams();

        ViewGroup.LayoutParams errorParams = mErrorLayout.getLayoutParams();
        int bottomPadding = 0;
        int topMargin = 0;
        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
            errorParams.height =
                    mContext.getResources()
                            .getDimensionPixelSize(R.dimen.error_layour_portrait_height);
            bottomPadding =
                    mContext.getResources()
                            .getDimensionPixelSize(R.dimen.readaloud_controls_portrait_padding);
            topMargin =
                    mContext.getResources()
                            .getDimensionPixelSize(R.dimen.readaloud_now_playing_spacing_portrait);

        } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {

            errorParams.height =
                    mContext.getResources()
                            .getDimensionPixelSize(R.dimen.error_layour_landscape_height);
            topMargin =
                    mContext.getResources()
                            .getDimensionPixelSize(R.dimen.readaloud_now_playing_spacing_landscape);
        }
        chromeNowPlayingParams.setMargins(0, topMargin, 0, 0);
        chromeNowPlaying.setLayoutParams(chromeNowPlayingParams);
        mErrorLayout.setLayoutParams(errorParams);
        mPlayerControls.setPadding(0, 0, 0, bottomPadding);
    }

    @VisibleForTesting
    public void setOptionsMenuSheetContent(OptionsMenuSheetContent optionsMenu) {
        mOptionsMenu = optionsMenu;
    }

    @VisibleForTesting
    public void setSpeedMenuSheetContent(SpeedMenuSheetContent speedMenu) {
        mSpeedMenu = speedMenu;
    }
}