chromium/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebApkIconNameUpdateDialogTest.java

// Copyright 2021 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.webapps;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.R;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;

/** Test for the dialog warning that WebApks have an updated name/icon. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class WebApkIconNameUpdateDialogTest {
    // A callback that fires when an action is taken in a dialog.
    private final CallbackHelper mOnActionCallback = new CallbackHelper();
    private final MockModalDialogManager mDialogManager = new MockModalDialogManager();

    // The length of the explanation header when icon updates are requested.
    private static final int MESSAGE_HEADER_LENGTH = 88;

    private Integer mLastDismissalCause;

    private static class DialogParams {
        public static DialogParams createDefault() {
            DialogParams dialogParams = new DialogParams();
            dialogParams.iconChanged = false;
            dialogParams.expectIconShownAnyway = false;
            dialogParams.bitmapBefore = null;
            dialogParams.bitmapAfter = null;
            dialogParams.shortNameChanged = false;
            dialogParams.expectShortNameShownAnyway = false;
            dialogParams.shortNameBefore = "";
            dialogParams.shortNameAfter = "";
            dialogParams.nameChanged = false;
            dialogParams.expectNameHiddenAnyway = false;
            dialogParams.nameBefore = "";
            dialogParams.nameAfter = "";
            dialogParams.hasExplanationString = true;
            return dialogParams;
        }

        public boolean iconChanged;
        public boolean expectIconShownAnyway;
        public Bitmap bitmapBefore;
        public Bitmap bitmapAfter;
        public boolean shortNameChanged;
        public boolean expectShortNameShownAnyway;
        public String shortNameBefore;
        public String shortNameAfter;
        public boolean nameChanged;
        public boolean expectNameHiddenAnyway;
        public String nameBefore;
        public String nameAfter;
        public boolean hasExplanationString;
    }

    private static class MockModalDialogManager extends ModalDialogManager {
        private PropertyModel mCurrentDialogModel;

        public MockModalDialogManager() {
            super(Mockito.mock(Presenter.class), 0);
        }

        @Override
        public void showDialog(PropertyModel model, int dialogType) {
            mCurrentDialogModel = model;
        }

        public PropertyModel getCurrentDialogModel() {
            return mCurrentDialogModel;
        }

        public void dismissCurrentDialog(int dismissalCause) {
            mCurrentDialogModel
                    .get(ModalDialogProperties.CONTROLLER)
                    .onDismiss(mCurrentDialogModel, dismissalCause);
        }
    }

    /** Generates a 320x320 test single-colored bitmap. */
    private static Bitmap generateTestBitmap(int color) {
        int width = 320;
        int height = 320;
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPaint(paint);
        return bitmap;
    }

    private View getDialogCustomView() {
        return mDialogManager.getCurrentDialogModel().get(ModalDialogProperties.CUSTOM_VIEW);
    }

    private CharSequence getDialogHeaderView() {
        return mDialogManager
                .getCurrentDialogModel()
                .get(ModalDialogProperties.MESSAGE_PARAGRAPH_1);
    }

    private String getDialogTitle() {
        return mDialogManager.getCurrentDialogModel().get(ModalDialogProperties.TITLE).toString();
    }

    private Bitmap getUpdateDialogBitmap(int resId) {
        ImageView imageView = getDialogCustomView().findViewById(resId);
        if (imageView.getVisibility() != View.VISIBLE) return null;
        return ((BitmapDrawable) imageView.getDrawable()).getBitmap();
    }

    private String getUpdateDialogAppNameLabel(int resId) {
        TextView textView = getDialogCustomView().findViewById(resId);
        if (textView.getVisibility() != View.VISIBLE) return null;

        return textView.getText().toString();
    }

    private String getUpdateDialogHeaderLabel() {
        return getDialogHeaderView().toString();
    }

    private void onUpdateDialogResult(Integer dialogDismissalCause) {
        mLastDismissalCause = dialogDismissalCause;
        mOnActionCallback.notifyCalled();
    }

    private void onAbuseDialogResult() {
        mOnActionCallback.notifyCalled();
    }

    public void verifyValues(boolean clickAccept, DialogParams dialogParams) {
        int callCount = mOnActionCallback.getCallCount();

        WebApkIconNameUpdateDialog dialog = new WebApkIconNameUpdateDialog();
        // Applying a theme overlay because the context is used to show the dialog, which needs some
        // color attributes to inflate the views.
        Context context =
                new ContextThemeWrapper(
                        ApplicationProvider.getApplicationContext(),
                        R.style.Theme_BrowserUI_DayNight);

        dialog.show(
                context,
                mDialogManager,
                /* packageName= */ "",
                dialogParams.iconChanged,
                dialogParams.shortNameChanged,
                dialogParams.nameChanged,
                dialogParams.shortNameBefore,
                dialogParams.shortNameAfter,
                dialogParams.nameBefore,
                dialogParams.nameAfter,
                dialogParams.bitmapBefore,
                dialogParams.bitmapAfter,
                false,
                false,
                this::onUpdateDialogResult);

        // Verify the short name labels visibility status.
        Assert.assertEquals(
                dialogParams.shortNameChanged || dialogParams.expectShortNameShownAnyway
                        ? dialogParams.shortNameBefore
                        : null,
                getUpdateDialogAppNameLabel(R.id.short_app_name_old));
        Assert.assertEquals(
                dialogParams.shortNameChanged || dialogParams.expectShortNameShownAnyway
                        ? dialogParams.shortNameAfter
                        : null,
                getUpdateDialogAppNameLabel(R.id.short_app_name_new));

        // Verify the app name label visibility status.
        Assert.assertEquals(
                dialogParams.nameChanged && !dialogParams.expectNameHiddenAnyway
                        ? dialogParams.nameBefore
                        : null,
                getUpdateDialogAppNameLabel(R.id.app_name_old));
        Assert.assertEquals(
                dialogParams.nameChanged && !dialogParams.expectNameHiddenAnyway
                        ? dialogParams.nameAfter
                        : null,
                getUpdateDialogAppNameLabel(R.id.app_name_new));

        // Verify the icons visibility status.
        Assert.assertEquals(
                dialogParams.iconChanged || dialogParams.expectIconShownAnyway
                        ? dialogParams.bitmapBefore
                        : null,
                getUpdateDialogBitmap(R.id.app_icon_old));
        Assert.assertEquals(
                dialogParams.iconChanged || dialogParams.expectIconShownAnyway
                        ? dialogParams.bitmapAfter
                        : null,
                getUpdateDialogBitmap(R.id.app_icon_new));

        mDialogManager.dismissCurrentDialog(
                clickAccept
                        ? DialogDismissalCause.POSITIVE_BUTTON_CLICKED
                        : DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
        Assert.assertEquals(callCount + 1, mOnActionCallback.getCallCount());

        Assert.assertEquals(
                clickAccept
                        ? (Integer) DialogDismissalCause.POSITIVE_BUTTON_CLICKED
                        : (Integer) DialogDismissalCause.NEGATIVE_BUTTON_CLICKED,
                mLastDismissalCause);

        Assert.assertEquals(
                (dialogParams.hasExplanationString ? MESSAGE_HEADER_LENGTH : 0),
                getUpdateDialogHeaderLabel().length());
    }

    public void verifyReportAbuseValues(
            boolean clickAccept, String shortAppName, String expectedTitle) throws Exception {
        int callCount = mOnActionCallback.getCallCount();

        // Applying a theme overlay because the context is used to show the dialog, which needs some
        // color attributes to inflate the views.
        Context context =
                new ContextThemeWrapper(
                        ApplicationProvider.getApplicationContext(),
                        R.style.Theme_BrowserUI_DayNight);
        WebApkUpdateReportAbuseDialog dialog =
                new WebApkUpdateReportAbuseDialog(
                        context,
                        mDialogManager,
                        /* packageName= */ "",
                        shortAppName,
                        /* showAbuseCheckbox= */ true,
                        this::onAbuseDialogResult);
        dialog.show();

        Assert.assertEquals(expectedTitle, getDialogTitle());

        mDialogManager.dismissCurrentDialog(
                clickAccept
                        ? DialogDismissalCause.POSITIVE_BUTTON_CLICKED
                        : DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
        // Pressing Cancel on the Abuse dialog does not activate the dismiss-parent callback
        // (because the parent dialog is not supposed to dismiss).
        Assert.assertEquals(callCount + (clickAccept ? 1 : 0), mOnActionCallback.getCallCount());
    }

    @Test
    @SmallTest
    @Feature({"Webapps"})
    public void testCombinations() throws Throwable {
        Bitmap blue = generateTestBitmap(Color.BLUE);
        Bitmap red = generateTestBitmap(Color.RED);

        // Test only icons changing.
        DialogParams dialogParams = DialogParams.createDefault();
        dialogParams.iconChanged = true;
        dialogParams.bitmapBefore = blue;
        dialogParams.bitmapAfter = red;
        // When only the icon changes, the short name is shown (as unchanged) to provide context.
        dialogParams.expectShortNameShownAnyway = true;
        verifyValues(/* clickAccept= */ true, dialogParams);

        // Test only short name changing.
        dialogParams = DialogParams.createDefault();
        dialogParams.shortNameChanged = true;
        dialogParams.shortNameBefore = "short1";
        dialogParams.shortNameAfter = "short2";
        // When only the short name changes, the icon is shown (as unchanged) to provide context and
        // the explanation string is dropped.
        dialogParams.expectIconShownAnyway = true;
        dialogParams.hasExplanationString = false;
        verifyValues(/* clickAccept= */ true, dialogParams);

        // Test only long name changing.
        dialogParams = DialogParams.createDefault();
        dialogParams.nameChanged = true;
        dialogParams.nameBefore = "name1";
        dialogParams.nameAfter = "name2";
        // Icons always show, even if unchanged.
        dialogParams.expectIconShownAnyway = true;
        dialogParams.hasExplanationString = false;
        verifyValues(/* clickAccept= */ true, dialogParams);

        // Test only short name and icon changing.
        dialogParams = DialogParams.createDefault();
        dialogParams.iconChanged = true;
        dialogParams.bitmapBefore = blue;
        dialogParams.bitmapAfter = red;
        dialogParams.shortNameChanged = true;
        dialogParams.shortNameBefore = "short1";
        dialogParams.shortNameAfter = "short2";
        verifyValues(/* clickAccept= */ true, dialogParams);

        // Test only name and icon changing.
        dialogParams = DialogParams.createDefault();
        dialogParams.iconChanged = true;
        dialogParams.bitmapBefore = blue;
        dialogParams.bitmapAfter = red;
        dialogParams.nameChanged = true;
        dialogParams.nameBefore = "name1";
        dialogParams.nameAfter = "name2";
        verifyValues(/* clickAccept= */ true, dialogParams);

        // Test all values changing.
        dialogParams = DialogParams.createDefault();
        dialogParams.iconChanged = true;
        dialogParams.bitmapBefore = blue;
        dialogParams.bitmapAfter = red;
        dialogParams.shortNameChanged = true;
        dialogParams.shortNameBefore = "short1";
        dialogParams.shortNameAfter = "short2";
        dialogParams.nameChanged = true;
        dialogParams.nameBefore = "name1";
        dialogParams.nameAfter = "name2";
        verifyValues(/* clickAccept= */ true, dialogParams);

        // Test all values changing, but dialog gets canceled.
        dialogParams = DialogParams.createDefault();
        dialogParams.iconChanged = true;
        dialogParams.bitmapBefore = blue;
        dialogParams.bitmapAfter = red;
        dialogParams.shortNameChanged = true;
        dialogParams.shortNameBefore = "short1";
        dialogParams.shortNameAfter = "short2";
        dialogParams.nameChanged = true;
        dialogParams.nameBefore = "name1";
        dialogParams.nameAfter = "name2";
        verifyValues(/* clickAccept= */ false, dialogParams);

        // Don't show duplicate name labels (identical change in both name and short name).
        dialogParams = DialogParams.createDefault();
        dialogParams.iconChanged = true;
        dialogParams.bitmapBefore = blue;
        dialogParams.bitmapAfter = red;
        dialogParams.shortNameChanged = true;
        dialogParams.shortNameBefore = "before";
        dialogParams.shortNameAfter = "after";
        dialogParams.nameChanged = true;
        dialogParams.nameBefore = "before";
        dialogParams.nameAfter = "after";
        dialogParams.expectNameHiddenAnyway = true;
        verifyValues(/* clickAccept= */ true, dialogParams);
    }

    @Test
    @SmallTest
    @Feature({"Webapps"})
    public void testReportAbuse() throws Throwable {
        // Make sure the dialog shows the right values.
        verifyReportAbuseValues(/* clickAccept= */ true, "short", "Uninstall 'short'?");

        // Make sure Canceling the dialog does the right thing.
        verifyReportAbuseValues(/* clickAccept= */ false, "short", "Uninstall 'short'?");
    }
}