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

import android.content.Context;
import android.view.ViewStub;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.BundleUtils;
import org.chromium.base.ObserverList;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.readaloud.ReadAloudPrefs;
import org.chromium.chrome.browser.readaloud.player.expanded.ExpandedPlayerCoordinator;
import org.chromium.chrome.browser.readaloud.player.mini.MiniPlayerCoordinator;
import org.chromium.chrome.modules.readaloud.Playback;
import org.chromium.chrome.modules.readaloud.PlaybackListener;
import org.chromium.chrome.modules.readaloud.Player;
import org.chromium.ui.modelutil.PropertyModel;

/**
 * Class that controls and coordinates the mini and expanded player UI.
 *
 * <p>The expanded player is a full-width bottom sheet that will completely obscure the mini player
 * if it's showing. Since showing or hiding the mini player requires resizing web contents which is
 * expensive and laggy, we will leave the mini player on screen when the expanded player is shown.
 *
 * <p>States: A. no players shown B. mini player visible C. expanded player open and mini player
 * visible (behind expanded player)
 */
public class PlayerCoordinator implements Player {
    private static final String TAG = "ReadAloudPlayer";
    private final ObserverList<Observer> mObserverList;
    private final PlayerMediator mMediator;
    private final Delegate mDelegate;
    private final MiniPlayerCoordinator mMiniPlayer;
    private final ExpandedPlayerCoordinator mExpandedPlayer;
    private Playback mPlayback;
    private boolean mRestoreMiniPlayer;
    private boolean mRestoreExpandedPlayer;
    private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher;

    // TODO remove internal call and then remove this constructor
    public PlayerCoordinator(
            Context splitContext, ViewStub miniPlayerViewStubm, Delegate delegate) {
        this(delegate);
    }

    public PlayerCoordinator(Delegate delegate) {
        mObserverList = new ObserverList<Observer>();
        PropertyModel model =
                new PropertyModel.Builder(PlayerProperties.ALL_KEYS)
                        // TODO Set voice and highlighting from settings when needed.
                        .with(
                                PlayerProperties.SPEED,
                                ReadAloudPrefs.getSpeed(delegate.getPrefService()))
                        .build();
        // This Context can be used to inflate views from the split.
        Context contextForInflation =
                BundleUtils.createContextForInflation(
                        delegate.getActivity(), "read_aloud_playback");
        mMiniPlayer =
                new MiniPlayerCoordinator(
                        delegate.getActivity(),
                        contextForInflation,
                        model,
                        delegate.getBottomControlsStacker(),
                        delegate.getLayoutManager(),
                        this,
                        delegate.getUserEducationHelper());
        mMediator = new PlayerMediator(/* coordinator= */ this, delegate, model);
        mExpandedPlayer = new ExpandedPlayerCoordinator(contextForInflation, delegate, model);
        mDelegate = delegate;
        mActivityLifecycleDispatcher = delegate.getActivityLifecycleDispatcher();
        if (mActivityLifecycleDispatcher != null) {
            mActivityLifecycleDispatcher.register(mExpandedPlayer);
        }
    }

    @VisibleForTesting
    PlayerCoordinator(
            MiniPlayerCoordinator miniPlayer,
            PlayerMediator mediator,
            Delegate delegate,
            ExpandedPlayerCoordinator player) {
        mObserverList = new ObserverList<Observer>();
        mMiniPlayer = miniPlayer;
        mMediator = mediator;
        mDelegate = delegate;
        mExpandedPlayer = player;
        mActivityLifecycleDispatcher = delegate.getActivityLifecycleDispatcher();
        if (mActivityLifecycleDispatcher != null) {
            mActivityLifecycleDispatcher.register(mExpandedPlayer);
        }
    }

    @Override
    public void addObserver(Observer observer) {
        mObserverList.addObserver(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        mObserverList.removeObserver(observer);
    }

    @Override
    public void destroy() {
        dismissPlayers();
        mMediator.destroy();
        mMiniPlayer.destroy();
        if (mActivityLifecycleDispatcher != null) {
            mActivityLifecycleDispatcher.unregister(mExpandedPlayer);
        }
    }

    @Override
    public void playTabRequested() {
        mMediator.setPlayback(null);
        mMediator.setPlaybackState(PlaybackListener.State.BUFFERING);
        if (!mExpandedPlayer.anySheetShowing()) {
            mMiniPlayer.show(/* animate= */ true);
        }
    }

    @Override
    public void playbackReady(Playback playback, @PlaybackListener.State int currentPlaybackState) {
        mMediator.setPlayback(playback);
        mMediator.setPlaybackState(currentPlaybackState);
        mPlayback = playback;
    }

    @Override
    public void playbackFailed() {
        mMediator.setPlayback(null);
        mMediator.setPlaybackState(PlaybackListener.State.ERROR);
    }

    @Override
    public void recordPlaybackDuration() {
        mMediator.recordPlaybackDuration();
    }

    /** Show expanded player. */
    void expand() {
        if (mDelegate.getProfile() != null) {
            TrackerFactory.getTrackerForProfile(mDelegate.getProfile())
                    .notifyEvent("read_aloud_expanded_player_shown");
        }
        mExpandedPlayer.show();
        mMiniPlayer.dismiss(/* animate= */ false);
    }

    @Override
    public void restoreMiniPlayer() {
        mMiniPlayer.show(/* animate= */ true);
        mMediator.setHiddenAndPlaying(false);
    }

    @Override
    public void dismissPlayers() {
        // Resetting the state. We can do it unconditionally because this UI is only
        // dismissed when stopping the playback.
        mMediator.setPlayback(null);
        mMediator.setPlaybackState(PlaybackListener.State.STOPPED);
        mMiniPlayer.dismiss(/* animate= */ true);
        mExpandedPlayer.dismiss();
        mMediator.setHiddenAndPlaying(false);
    }

    @Override
    public void hideMiniPlayer() {
        int miniPlayerVisibility = mMiniPlayer.getVisibility();
        if (miniPlayerVisibility == VisibilityState.SHOWING
                || miniPlayerVisibility == VisibilityState.VISIBLE) {
            mMiniPlayer.dismiss(/* animate= */ true);
            mMediator.setHiddenAndPlaying(true);
        }
    }

    /** Collapses the expanded player and shows mini player */
    void hideExpandedPlayer() {
        mExpandedPlayer.dismiss(true);
    }

    @Override
    public void hidePlayers() {
        int expandedSheetVisibility = mExpandedPlayer.getVisibility();
        int miniPlayerVisibility = mMiniPlayer.getVisibility();
        if (expandedSheetVisibility == VisibilityState.SHOWING
                || expandedSheetVisibility == VisibilityState.VISIBLE) {
            mRestoreExpandedPlayer = true;
            mRestoreMiniPlayer = false;
            mExpandedPlayer.dismiss();
        } else if (miniPlayerVisibility == VisibilityState.SHOWING
                || miniPlayerVisibility == VisibilityState.VISIBLE) {
            mRestoreMiniPlayer = true;
            mRestoreExpandedPlayer = false;
            mMiniPlayer.dismiss(/* animate= */ true);
        }

        mMediator.setHiddenAndPlaying(true);
    }

    @Override
    public void restorePlayers() {
        if (mRestoreMiniPlayer) {
            restoreMiniPlayer();
            mRestoreMiniPlayer = false;
        } else if (mRestoreExpandedPlayer) {
            mExpandedPlayer.show();
            mRestoreExpandedPlayer = false;
            mMediator.setHiddenAndPlaying(false);
        }
    }

    @Override
    public void onScreenStatusChanged(boolean isScreenLocked) {
        mMediator.onScreenStatusChanged(isScreenLocked);
    }

    /** To be called when the close button is clicked. */
    void closeClicked() {
        for (Observer o : mObserverList) {
            o.onRequestClosePlayers();
        }
    }

    void voiceMenuClosed() {
        for (Observer o : mObserverList) {
            o.onVoiceMenuClosed();
        }
    }

    // Called by mini player when it finishes showing.
    public void onMiniPlayerShown() {
        for (Observer o : mObserverList) {
            o.onMiniPlayerShown();
        }
    }
}