chromium/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillImageFetcher.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.autofill;

import android.content.Context;
import android.graphics.Bitmap;

import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;

import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.autofill.AutofillUiUtils.CardIconSize;
import org.chromium.chrome.browser.autofill.AutofillUiUtils.CardIconSpecs;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.components.embedder_support.simple_factory_key.SimpleFactoryKeyHandle;
import org.chromium.components.image_fetcher.ImageFetcher;
import org.chromium.components.image_fetcher.ImageFetcherConfig;
import org.chromium.components.image_fetcher.ImageFetcherFactory;
import org.chromium.url.GURL;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/** Fetches, and caches credit card art images. */
public class AutofillImageFetcher {
    private final Map<String, Bitmap> mImagesCache = new HashMap<>();
    private ImageFetcher mImageFetcher;

    @CalledByNative
    private static AutofillImageFetcher create(SimpleFactoryKeyHandle key) {
        return new AutofillImageFetcher(
                ImageFetcherFactory.createImageFetcher(ImageFetcherConfig.DISK_CACHE_ONLY, key));
    }

    AutofillImageFetcher(ImageFetcher imageFetcher) {
        mImageFetcher = imageFetcher;
    }

    /**
     * Fetches images for the passed in URLs and stores them in cache.
     *
     * @param urls The URLs to fetch the images.
     */
    @CalledByNative
    void prefetchImages(@JniType("base::span<const GURL>") GURL[] urls) {
        Context context = ContextUtils.getApplicationContext();

        for (GURL url : urls) {
            // Credit card art images are shown in 2 different sizes depending on the surface.
            // Prefetch and cache images in both sizes.
            for (@CardIconSize int size : new int[] {CardIconSize.SMALL, CardIconSize.LARGE}) {
                CardIconSpecs cardIconSpecs = CardIconSpecs.create(context, size);
                fetchImage(url, cardIconSpecs);
            }
        }
    }

    /**
     * Returns the required image if it exists in the image cache. If not, makes a call to fetch and
     * cache the image for next time.
     *
     * @param url The URL of the image.
     * @param cardIconSpecs The sizing specifications for the image.
     * @return Bitmap image for the passed in URL if it exists in cache, an empty object otherwise.
     */
    Optional<Bitmap> getImageIfAvailable(GURL url, CardIconSpecs cardIconSpecs) {
        GURL urlToCache =
                AutofillUiUtils.getCreditCardIconUrlWithParams(
                        url, cardIconSpecs.getWidth(), cardIconSpecs.getHeight());
        // If the card art image exists in the cache, return it.
        if (mImagesCache.containsKey(urlToCache.getSpec())) {
            return Optional.of(mImagesCache.get(urlToCache.getSpec()));
        }

        // If not, fetch the image from the server, and cache for next time. Return empty object.
        fetchImage(url, cardIconSpecs);
        return Optional.empty();
    }

    /**
     * Fetches image for the given URL.
     *
     * @param url The URL to fetch the image.
     */
    private void fetchImage(GURL url, CardIconSpecs cardIconSpecs) {
        if (!url.isValid()) {
            return;
        }

        // The Capital One icon for virtual cards is available in a single size via a static
        // URL. Cache this image at different sizes so it can be used by different surfaces.
        GURL urlToCache =
                AutofillUiUtils.getCreditCardIconUrlWithParams(
                        url, cardIconSpecs.getWidth(), cardIconSpecs.getHeight());
        GURL urlToFetch =
                url.getSpec().equals(AutofillUiUtils.CAPITAL_ONE_ICON_URL) ? url : urlToCache;

        // If the image already exists in the cache, return.
        if (mImagesCache.containsKey(urlToCache.getSpec())) {
            return;
        }

        ImageFetcher.Params params =
                ImageFetcher.Params.create(
                        urlToFetch.getSpec(), ImageFetcher.AUTOFILL_CARD_ART_UMA_CLIENT_NAME);
        mImageFetcher.fetchImage(
                params, bitmap -> treatAndCacheImage(bitmap, urlToCache, cardIconSpecs));
    }

    private void treatAndCacheImage(Bitmap bitmap, GURL urlToCache, CardIconSpecs cardIconSpecs) {
        RecordHistogram.recordBooleanHistogram("Autofill.ImageFetcher.Result", bitmap != null);

        // If the image fetching was unsuccessful, silently return.
        if (bitmap == null) {
            return;
        }

        // When adding new sizes for card icons, check if the corner radius needs to be added as
        // a suffix for caching (crbug.com/1431283).
        mImagesCache.put(
                urlToCache.getSpec(),
                AutofillUiUtils.resizeAndAddRoundedCornersAndGreyBorder(
                        bitmap,
                        cardIconSpecs,
                        ChromeFeatureList.isEnabled(
                                ChromeFeatureList
                                        .AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES)));
    }

    /**
     * Add an image to the in-memory cache of images.
     *
     * @param url The URL that should be used as the key for the cache.
     * @param bitmap The actual image bitmap that should be cached.
     * @param cardIconSpecs The {@link CardIconSpecs} for the bitmap. This is used for generating
     *     the URL params.
     */
    public void addImageToCacheForTesting(GURL url, Bitmap bitmap, CardIconSpecs cardIconSpecs) {
        GURL urlToCache =
                AutofillUiUtils.getCreditCardIconUrlWithParams(
                        url, cardIconSpecs.getWidth(), cardIconSpecs.getHeight());
        mImagesCache.put(urlToCache.getSpec(), bitmap);
    }

    Map<String, Bitmap> getCachedImagesForTesting() {
        return mImagesCache;
    }

    /** Clears the in-memory cache of images. */
    public void clearCachedImagesForTesting() {
        mImagesCache.clear();
    }
}