chromium/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHeaderMediator.java

// Copyright 2019 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.contextmenu;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.embedder_support.contextmenu.ContextMenuNativeDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.components.favicon.IconType;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;

class ContextMenuHeaderMediator implements View.OnClickListener {
    private PropertyModel mModel;

    private Context mContext;
    private GURL mPlainUrl;

    ContextMenuHeaderMediator(
            Context context,
            PropertyModel model,
            ContextMenuParams params,
            Profile profile,
            ContextMenuNativeDelegate nativeDelegate) {
        mContext = context;
        mPlainUrl = params.getUrl();
        mModel = model;
        mModel.set(ContextMenuHeaderProperties.TITLE_AND_URL_CLICK_LISTENER, this);

        if (params.isImage()) {
            final Resources res = mContext.getResources();
            final int imageMaxSize =
                    res.getDimensionPixelSize(R.dimen.context_menu_header_image_max_size);
            nativeDelegate.retrieveImageForContextMenu(
                    imageMaxSize, imageMaxSize, this::onImageThumbnailRetrieved);
        } else if (!params.isImage() && !params.isVideo()) {
            LargeIconBridge iconBridge = new LargeIconBridge(profile);
            iconBridge.getLargeIconForUrl(
                    mPlainUrl,
                    context.getResources().getDimensionPixelSize(R.dimen.default_favicon_min_size),
                    this::onFaviconAvailable);
        } else if (params.isVideo()) {
            setVideoIcon();
        }
    }

    /**
     * This is called when the thumbnail is fetched and ready to display.
     * @param thumbnail The bitmap received that will be displayed as the header image.
     */
    void onImageThumbnailRetrieved(Bitmap thumbnail) {
        if (thumbnail != null) {
            setHeaderImage(getImageWithCheckerBackground(mContext.getResources(), thumbnail), true);
        }
        // TODO(sinansahin): Handle the case where the retrieval of the thumbnail fails.
    }

    /** See {@link org.chromium.components.favicon.LargeIconBridge#getLargeIconForUrl} */
    private void onFaviconAvailable(
            @Nullable Bitmap icon,
            @ColorInt int fallbackColor,
            boolean isColorDefault,
            @IconType int iconType) {
        // If we didn't get a favicon, generate a monogram instead
        if (icon == null) {
            final RoundedIconGenerator iconGenerator = createRoundedIconGenerator(fallbackColor);
            icon = iconGenerator.generateIconForUrl(mPlainUrl);
            // generateIconForUrl might return null if the URL is empty or the domain cannot be
            // resolved. See https://crbug.com/987101
            // TODO(sinansahin): Handle the case where generating an icon fails.
            if (icon == null) {
                return;
            }
        }

        final int size = mModel.get(ContextMenuHeaderProperties.MONOGRAM_SIZE_PIXEL);

        icon = Bitmap.createScaledBitmap(icon, size, size, true);

        setHeaderImage(icon, false);
    }

    /**
     * This is called when the url text is clicked. So, we can expand or shrink the url here.
     * @param v The url text view.
     */
    @Override
    public void onClick(View v) {
        if (mModel.get(ContextMenuHeaderProperties.URL_MAX_LINES) == Integer.MAX_VALUE) {
            // URL and title should both be expanded.
            assert mModel.get(ContextMenuHeaderProperties.TITLE_MAX_LINES) == Integer.MAX_VALUE;

            final boolean isTitleEmpty =
                    TextUtils.isEmpty(mModel.get(ContextMenuHeaderProperties.TITLE));
            mModel.set(ContextMenuHeaderProperties.URL_MAX_LINES, isTitleEmpty ? 2 : 1);
            final boolean isUrlEmpty =
                    TextUtils.isEmpty(mModel.get(ContextMenuHeaderProperties.URL));
            mModel.set(ContextMenuHeaderProperties.TITLE_MAX_LINES, isUrlEmpty ? 2 : 1);
        } else {
            mModel.set(ContextMenuHeaderProperties.URL_MAX_LINES, Integer.MAX_VALUE);
            mModel.set(ContextMenuHeaderProperties.TITLE_MAX_LINES, Integer.MAX_VALUE);
        }
    }

    /**
     * This adds a checkerboard style background to the image.
     * It is useful for the transparent PNGs.
     * @return The given image with the checkerboard pattern in the background.
     */
    private static Bitmap getImageWithCheckerBackground(Resources res, Bitmap image) {
        // 1. Create a bitmap for the checkerboard pattern.
        Drawable drawable =
                ApiCompatibilityUtils.getDrawable(res, R.drawable.checkerboard_background);
        Bitmap tileBitmap =
                Bitmap.createBitmap(
                        drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight(),
                        Bitmap.Config.ARGB_8888);
        Canvas tileCanvas = new Canvas(tileBitmap);
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        drawable.draw(tileCanvas);

        // 2. Create a BitmapDrawable using the checkerboard pattern bitmap.
        BitmapDrawable bitmapDrawable = new BitmapDrawable(res, tileBitmap);
        bitmapDrawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
        bitmapDrawable.setBounds(0, 0, image.getWidth(), image.getHeight());

        // 3. Create a bitmap-backed canvas for the final image.
        Bitmap bitmap =
                Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        // 4. Paint the checkerboard background into the final canvas
        bitmapDrawable.draw(canvas);

        // 5. Draw the image on top.
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        canvas.drawBitmap(image, new Matrix(), paint);

        return bitmap;
    }

    private void setVideoIcon() {
        Drawable drawable =
                ApiCompatibilityUtils.getDrawable(
                        mContext.getResources(), R.drawable.gm_filled_videocam_24);
        drawable.setColorFilter(
                SemanticColorUtils.getDefaultIconColor(mContext), PorterDuff.Mode.SRC_IN);
        Bitmap bitmap =
                Bitmap.createBitmap(
                        drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight(),
                        Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        setHeaderImage(bitmap, false);
    }

    private void setHeaderImage(Bitmap bitmap, boolean isThumbnail) {
        if (isThumbnail) {
            mModel.set(ContextMenuHeaderProperties.IMAGE, bitmap);
            return;
        }

        mModel.set(ContextMenuHeaderProperties.CIRCLE_BG_VISIBLE, true);
        mModel.set(ContextMenuHeaderProperties.IMAGE, bitmap);
    }

    private RoundedIconGenerator createRoundedIconGenerator(@ColorInt int iconColor) {
        final Resources resources = mContext.getResources();
        final int iconSize =
                resources.getDimensionPixelSize(R.dimen.context_menu_header_monogram_size);
        final int cornerRadius = iconSize / 2;
        final int textSize =
                resources.getDimensionPixelSize(R.dimen.context_menu_header_monogram_text_size);

        return new RoundedIconGenerator(iconSize, iconSize, cornerRadius, iconColor, textSize);
    }
}