chromium/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/MenuItem.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.view.LayoutInflater;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.SwitchCompat;

import com.google.android.material.materialswitch.MaterialSwitch;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneShotCallback;
import org.chromium.chrome.browser.readaloud.player.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** MenuItem is a view that can be used for all Read Aloud player menu item variants. */
public class MenuItem extends FrameLayout {
    private static final String TAG = "ReadAloudMenuItem";

    /** Menu item actions that show up as widgets at the end. */
    @IntDef({Action.NONE, Action.EXPAND, Action.RADIO, Action.TOGGLE})
    @Retention(RetentionPolicy.SOURCE)
    @interface Action {
        /** No special action. */
        int NONE = 0;

        /** Show a separator and arrow meaning that clicking will open a submenu. */
        int EXPAND = 1;

        /** Show a radio button. */
        int RADIO = 2;

        /** Show a toggle switch. */
        int TOGGLE = 3;
    }

    private final int mId;
    private final @Action int mActionType;
    private final Menu mMenu;
    private final LinearLayout mLayout;
    private final ObservableSupplier<LinearLayout> mLayoutSupplier;
    private final ImageView mPlayButton;
    private final ProgressBar mPlayButtonSpinner;
    private Callback<Boolean> mToggleHandler;
    private final String mLabel;

    /**
     * @param context Context.
     * @param parentMenu Menu to which this item belongs.
     * @param itemId Menu item's identifying number, to be used for handling clicks.
     * @param iconId Resource ID of an icon drawable. Pass 0 to show no icon.
     * @param label Primary text to show for the item.
     * @param action Extra widget to show at the end.
     */
    public MenuItem(
            Context context,
            Menu parentMenu,
            int itemId,
            int iconId,
            String label,
            @Nullable String header,
            @Action int action) {
        super(context);
        mMenu = parentMenu;
        mId = itemId;
        mActionType = action;
        mLabel = label;
        LayoutInflater inflater = LayoutInflater.from(context);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.readaloud_menu_item, null);
        layout.setOnClickListener(
                (view) -> {
                    onClick();
                });
        mLayout = layout;
        mLayoutSupplier = new ObservableSupplierImpl(mLayout);
        new OneShotCallback<LinearLayout>(mLayoutSupplier, this::onLayoutInflated);
        if (iconId != 0) {
            ImageView icon = layout.findViewById(R.id.icon);
            icon.setImageResource(iconId);
            // Icon is GONE by default.
            icon.setVisibility(View.VISIBLE);
        }
        TextView localeView = layout.findViewById(R.id.item_header);
        if (header == null) {
            localeView.setVisibility(View.GONE);
        } else {
            localeView.setVisibility(View.VISIBLE);
            localeView.setText(header);
        }

        ((TextView) layout.findViewById(R.id.item_label)).setText(label);

        switch (mActionType) {
            case Action.EXPAND:
                View expandView = inflater.inflate(R.layout.expand_arrow_with_separator, null);
                View arrow = expandView.findViewById(R.id.expand_arrow);
                arrow.setClickable(false);
                arrow.setFocusable(false);
                setEndView(layout, expandView);
                break;

            case Action.TOGGLE:
                MaterialSwitch materialSwitch =
                        (MaterialSwitch) inflater.inflate(R.layout.readaloud_toggle_switch, null);
                materialSwitch.setOnCheckedChangeListener(
                        (view, value) -> {
                            onToggle(value);
                        });
                setEndView(layout, materialSwitch);
                break;

            case Action.RADIO:
                RadioButton radioButton =
                        (RadioButton) inflater.inflate(R.layout.readaloud_radio_button, null);
                radioButton.setOnCheckedChangeListener(
                        (view, value) -> {
                            if (value) {
                                onRadioButtonSelected();
                            }
                        });
                setEndView(layout, radioButton);
                break;
            case Action.NONE:
            default:
                break;
        }
        addView(layout);

        mPlayButton = (ImageView) findViewById(R.id.play_button);
        mPlayButton.setContentDescription(
                context.getResources().getString(R.string.readaloud_play) + " " + mLabel);
        mPlayButtonSpinner = (ProgressBar) findViewById(R.id.spinner);
        mPlayButtonSpinner.setContentDescription(
                context.getResources().getString(R.string.readaloud_playback_loading)
                        + " "
                        + mLabel);
    }

    private void onLayoutInflated(LinearLayout layout) {
        // accessibility delegate is only being set for radio and toggle switch item types
        if (mActionType != Action.RADIO && mActionType != Action.TOGGLE) {
            return;
        }
        // To improve the explore-by-touch experience, the button are hidden from accessibility
        // and instead, "checked" or "not checked" is read along with the button's label
        layout.setAccessibilityDelegate(
                new AccessibilityDelegate() {
                    @Override
                    public void onInitializeAccessibilityEvent(
                            View host, AccessibilityEvent event) {
                        super.onInitializeAccessibilityEvent(host, event);
                        if (mActionType == Action.RADIO) {
                            event.setChecked(getRadioButton().isChecked());
                        } else if (mActionType == Action.TOGGLE) {
                            event.setChecked(getToggleSwitch().isChecked());
                        }
                    }

                    @Override
                    public void onInitializeAccessibilityNodeInfo(
                            View host, AccessibilityNodeInfo info) {
                        super.onInitializeAccessibilityNodeInfo(host, info);
                        info.setCheckable(true);
                        if (mActionType == Action.RADIO) {
                            info.setChecked(getRadioButton().isChecked());
                            info.setEnabled(getRadioButton().isEnabled());
                        } else if (mActionType == Action.TOGGLE) {
                            info.setChecked(getToggleSwitch().isChecked());
                            info.setEnabled(getToggleSwitch().isEnabled());
                        }
                    }
                });
    }

    void setToggleHandler(Callback<Boolean> handler) {
        mToggleHandler = handler;
    }

    void addPlayButton() {
        mPlayButton.setVisibility(View.VISIBLE);
        mPlayButton.setOnClickListener(
                (view) -> {
                    mMenu.onPlayButtonClicked(mId);
                });
    }

    void setItemEnabled(boolean enabled) {
        if (mActionType == Action.TOGGLE) {
            getToggleSwitch().setEnabled(enabled);
        }
    }

    void setValue(boolean value) {
        if (mActionType == Action.TOGGLE) {
            getToggleSwitch().setChecked(value);
        } else if (mActionType == Action.RADIO) {
            getRadioButton().setChecked(value);
        }
    }

    void showPlayButtonSpinner() {
        mPlayButton.setVisibility(View.GONE);
        mPlayButtonSpinner.setVisibility(View.VISIBLE);
    }

    void showPlayButton() {
        mPlayButtonSpinner.setVisibility(View.GONE);
        mPlayButton.setVisibility(View.VISIBLE);
    }

    void setPlayButtonStopped() {
        mPlayButton.setImageResource(R.drawable.mini_play_button);
        mPlayButton.setContentDescription(
                getContext().getResources().getString(R.string.readaloud_play) + " " + mLabel);
    }

    void setPlayButtonPlaying() {
        mPlayButton.setImageResource(R.drawable.mini_pause_button);
        mPlayButton.setContentDescription(
                getContext().getResources().getString(R.string.readaloud_pause) + " " + mLabel);
    }

    void setSecondLine(String text) {
        TextView view = mLayout.findViewById(R.id.item_sublabel);
        view.setText(text);
        view.setVisibility(View.VISIBLE);
    }

    private void setEndView(LinearLayout layout, View view) {
        ((FrameLayout) layout.findViewById(R.id.end_view)).addView(view);
    }

    // On click won't be propagated here if the parent layout is not clickable
    private void onClick() {
        assert mMenu != null;
        if (mActionType == Action.RADIO) {
            getRadioButton().toggle();
        } else if (mActionType == Action.TOGGLE) {
            getToggleSwitch().toggle();
        }
        mMenu.onItemClicked(mId);
    }

    private void onRadioButtonSelected() {
        assert mMenu != null;
        mMenu.onRadioButtonSelected(mId);
    }

    private void onToggle(boolean newValue) {
        if (mToggleHandler != null) {
            mToggleHandler.onResult(newValue);
        }
    }

    private SwitchCompat getToggleSwitch() {
        return (SwitchCompat) findViewById(R.id.toggle_switch);
    }

    private RadioButton getRadioButton() {
        return (RadioButton) findViewById(R.id.readaloud_radio_button);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public ObservableSupplierImpl<LinearLayout> getLayoutSupplier() {
        return (ObservableSupplierImpl) mLayoutSupplier;
    }
}