chromium/chrome/browser/password_manager/android/pwd_migration/java/src/org/chromium/chrome/browser/pwd_migration/PasswordMigrationWarningView.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.pwd_migration;

import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;

import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;

import org.chromium.base.Callback;
import org.chromium.chrome.browser.password_manager.PasswordManagerResourceProviderFactory;
import org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningProperties.ScreenType;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;

/**
 * This class is responsible for rendering the bottom sheet that shows the passwords
 * migration warning.
 */
public class PasswordMigrationWarningView implements BottomSheetContent {
    private final BottomSheetController mBottomSheetController;
    private Runnable mOnShowEventListener;
    private Callback<Integer> mDismissHandler;
    private PasswordMigrationWarningOnClickHandler mOnClickHandler;
    private FragmentManager mFragmentManager;
    private final ScrollablePasswordMigrationWarningContent mContentView;
    private Context mContext;
    private String mAccountDisplayName;
    private @ScreenType int mScreenType = ScreenType.NONE;
    private boolean mShouldOfferSync;
    private Runnable mOnResumeExportFlowCallback;
    private boolean mSetFragmentWasCalled;

    private Callback<Throwable> mExceptionReporter;
    private OnSheetClosedCallback mOnSheetClosedCallback;

    /**
     * Interface used to pass a callback that will be called in onSheetClosed to the
     * BottomSheetObservers.
     */
    interface OnSheetClosedCallback {
        /**
         * Called when BottomSheetObserver.onSheetClosed is invoked.
         * @param reason is the cause for the sheet to change its state.
         * @param setFragmentWasCalled indicates that the PasswordMigrationWarningView.setFragment()
         *         method was called.
         */
        public void onSheetClosed(
                @BottomSheetController.StateChangeReason int reason, boolean setFragmentWasCalled);
    }

    private final BottomSheetObserver mBottomSheetObserver =
            new EmptyBottomSheetObserver() {
                @Override
                public void onSheetClosed(@BottomSheetController.StateChangeReason int reason) {
                    super.onSheetClosed(reason);
                    if (mBottomSheetController.getCurrentSheetContent()
                            != PasswordMigrationWarningView.this) {
                        return;
                    }
                    assert mDismissHandler != null;
                    mOnSheetClosedCallback.onSheetClosed(reason, mSetFragmentWasCalled);
                    mDismissHandler.onResult(reason);
                    mBottomSheetController.removeObserver(mBottomSheetObserver);
                }

                @Override
                public void onSheetStateChanged(int newState, int reason) {
                    super.onSheetStateChanged(newState, reason);
                    if (mBottomSheetController.getCurrentSheetContent()
                            != PasswordMigrationWarningView.this) {
                        return;
                    }
                    if (newState != BottomSheetController.SheetState.HIDDEN) return;
                    // This is a fail-safe for cases where onSheetClosed isn't triggered.
                    mDismissHandler.onResult(BottomSheetController.StateChangeReason.NONE);
                    mBottomSheetController.removeObserver(mBottomSheetObserver);
                }

                @Override
                public void onSheetOpened(@StateChangeReason int reason) {
                    if (mBottomSheetController.getCurrentSheetContent()
                                    == PasswordMigrationWarningView.this
                            && mScreenType != ScreenType.NONE
                            && getContentView().isShown()) {
                        setFragment();
                    }
                }
            };

    PasswordMigrationWarningView(
            Context context,
            BottomSheetController bottomSheetController,
            Runnable onResumeExportFlowCallback,
            Callback<Throwable> exceptionReporter,
            OnSheetClosedCallback onSheetClosedCallback) {
        mContext = context;
        mBottomSheetController = bottomSheetController;
        mOnResumeExportFlowCallback = onResumeExportFlowCallback;
        mContentView =
                (ScrollablePasswordMigrationWarningContent)
                        LayoutInflater.from(context).inflate(R.layout.pwd_migration_warning, null);
        mSetFragmentWasCalled = false;
        ImageView sheetHeaderImage =
                mContentView.findViewById(R.id.touch_to_fill_sheet_header_image);
        sheetHeaderImage.setImageDrawable(
                AppCompatResources.getDrawable(
                        context,
                        PasswordManagerResourceProviderFactory.create().getPasswordManagerIcon()));
        mFragmentManager = ((AppCompatActivity) context).getSupportFragmentManager();
        mExceptionReporter = exceptionReporter;
        mOnSheetClosedCallback = onSheetClosedCallback;
    }

    void setOnShowEventListener(Runnable onShowEventListener) {
        mOnShowEventListener = onShowEventListener;
    }

    void setDismissHandler(Callback<Integer> dismissHandler) {
        mDismissHandler = dismissHandler;
    }

    void setOnClickHandler(PasswordMigrationWarningOnClickHandler onClickHandler) {
        mOnClickHandler = onClickHandler;
    }

    boolean setVisible(boolean isVisible) {
        if (!isVisible) {
            mBottomSheetController.hideContent(this, true);
            return true;
        }
        mBottomSheetController.addObserver(mBottomSheetObserver);
        if (!mBottomSheetController.requestShowContent(this, true)) {
            return false;
        }
        return true;
    }

    void setScreen(@ScreenType int screenType) {
        mScreenType = screenType;
        if (mBottomSheetController.getCurrentSheetContent() == PasswordMigrationWarningView.this
                && getContentView().isShown()) {
            setFragment();
        }
        // Makes sure the sheet is fully expanded.
        mBottomSheetController.expandSheet();
    }

    void setShouldOfferSync(boolean shouldOfferSync) {
        mShouldOfferSync = shouldOfferSync;
    }

    private void setFragment() {
        mSetFragmentWasCalled = true;
        assert mScreenType != ScreenType.NONE;
        if (mScreenType == ScreenType.INTRO_SCREEN) {
            String introScreenSubtitle =
                    mContext.getString(R.string.password_migration_warning_subtitle)
                            .replace(
                                    "%1$s",
                                    PasswordMigrationWarningUtil.getChannelString(mContext));
            PasswordMigrationWarningIntroFragment introFragment =
                    new PasswordMigrationWarningIntroFragment(
                            introScreenSubtitle,
                            () -> mOnClickHandler.onAcknowledge(mBottomSheetController),
                            () -> mOnClickHandler.onMoreOptions());
            assert mContentView != null;
            assert mContentView.findViewById(R.id.fragment_container_view) != null;
            replaceFragment(introFragment, mOnShowEventListener);
        } else if (mScreenType == ScreenType.OPTIONS_SCREEN) {
            String exportOptionSubtitle =
                    mContext.getString(R.string.password_migration_warning_password_export_subtitle)
                            .replace(
                                    "%1$s",
                                    PasswordMigrationWarningUtil.getChannelString(mContext));
            PasswordMigrationWarningOptionsFragment optionsFragment =
                    new PasswordMigrationWarningOptionsFragment(
                            exportOptionSubtitle,
                            mShouldOfferSync,
                            mOnClickHandler,
                            () -> mOnClickHandler.onCancel(mBottomSheetController),
                            mAccountDisplayName,
                            mFragmentManager,
                            mOnResumeExportFlowCallback);
            assert mContentView != null;
            assert mContentView.findViewById(R.id.fragment_container_view) != null;
            replaceFragment(optionsFragment, null);
        }
    }

    void runCreateFileOnDiskIntent(Intent intent) {
        Fragment fragment = mFragmentManager.findFragmentById(R.id.fragment_container_view);
        assert fragment instanceof PasswordMigrationWarningOptionsFragment;

        ((PasswordMigrationWarningOptionsFragment) fragment).runCreateFileOnDiskIntent(intent);
    }

    void setAccountDisplayName(String accountDisplayName) {
        mAccountDisplayName = accountDisplayName;
    }

    private void replaceFragment(Fragment newFragment, @Nullable Runnable onCommitted) {
        try {
            mFragmentManager
                    .beginTransaction()
                    .setReorderingAllowed(true)
                    .replace(R.id.fragment_container_view, newFragment)
                    .commitNow();
            if (onCommitted != null) {
                onCommitted.run();
            }
        } catch (IllegalArgumentException exception) {
            mExceptionReporter.onResult(exception);
        }
    }

    private @Px int getDimensionPixelSize(int id) {
        return mContentView.getContext().getResources().getDimensionPixelSize(id);
    }

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

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

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

    @Override
    public void destroy() {}

    @Override
    public int getPriority() {
        return BottomSheetContent.ContentPriority.HIGH;
    }

    @Override
    public boolean swipeToDismissEnabled() {
        return false;
    }

    @Override
    public int getSheetContentDescriptionStringId() {
        return R.string.password_migration_warning_content_description;
    }

    @Override
    public int getSheetHalfHeightAccessibilityStringId() {
        // The sheet doesn't have a half height state.
        assert false;
        return 0;
    }

    @Override
    public int getSheetFullHeightAccessibilityStringId() {
        return R.string.password_migration_warning_content_description;
    }

    @Override
    public int getSheetClosedAccessibilityStringId() {
        return R.string.password_migration_warning_closed;
    }

    @Override
    public float getFullHeightRatio() {
        return 1.f;
    }

    @Override
    public float getHalfHeightRatio() {
        return HeightMode.DISABLED;
    }

    @Override
    public int getPeekHeight() {
        return HeightMode.DISABLED;
    }

    @Override
    public boolean hideOnScroll() {
        return false;
    }
}