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

import android.content.res.Resources;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.common.primitives.UnsignedLongs;

import org.chromium.base.Callback;
import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.commerce.PriceTrackingUtils;
import org.chromium.chrome.browser.commerce.ShoppingServiceFactory;
import org.chromium.chrome.browser.price_tracking.PriceDropNotificationManagerFactory;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.commerce.core.CommerceSubscription;
import org.chromium.components.commerce.core.CommerceSubscription.UserSeenOffer;
import org.chromium.components.commerce.core.IdentifierType;
import org.chromium.components.commerce.core.ManagementType;
import org.chromium.components.commerce.core.ShoppingService;
import org.chromium.components.commerce.core.SubscriptionType;
import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
import org.chromium.components.power_bookmarks.PowerBookmarkType;
import org.chromium.components.power_bookmarks.ShoppingSpecifics;

import java.util.ArrayList;
import java.util.List;

/** Utilities for use in power bookmarks. */
// TODO(crbug.com/40234642): We should add a JNI layer for the native version of these utilities in
//                price_tracking_utils and use those instead.
public class PowerBookmarkUtils {
    private static Boolean sPriceTrackingEligibleForTesting;
    private static PowerBookmarkMeta sPowerBookmarkMetaForTesting;

    /** Returns whether the given meta is a shopping list item. */
    public static boolean isShoppingListItem(
            ShoppingService shoppingService, PowerBookmarkMeta meta) {
        return shoppingService.isShoppingListEligible()
                && meta != null
                && meta.hasShoppingSpecifics();
    }

    /**
     * Checks if the given tab is price-trackable.
     *
     * @param tab The tab to check for price-tracking eligibility.
     * @return Whether the given tab is eligible for price-tracking.
     */
    public static boolean isPriceTrackingEligible(@Nullable Tab tab) {
        if (tab == null || tab.getWebContents() == null) return false;
        if (sPriceTrackingEligibleForTesting != null) return sPriceTrackingEligibleForTesting;

        Profile profile = tab.getProfile();
        ShoppingService service = ShoppingServiceFactory.getForProfile(profile);
        if (service == null) return false;

        ShoppingService.ProductInfo info = service.getAvailableProductInfoForUrl(tab.getUrl());

        return info != null && info.productClusterId.isPresent();
    }

    /**
     * Unified way to get the associated {@link CommerceSubscription} for a {@link
     * PowerBookmarkMeta}.
     *
     * @param meta The {@link PowerBookmarkMeta} to create the {@link CommerceSubscription} for.
     * @return The {@link CommerceSubsription} for the given {@link PowerBookmarkMeta}
     */
    public static @NonNull CommerceSubscription createCommerceSubscriptionForPowerBookmarkMeta(
            @NonNull PowerBookmarkMeta meta) {
        return createCommerceSubscriptionForShoppingSpecifics(meta.getShoppingSpecifics());
    }

    /**
     * Unified way to get the associated {@link CommerceSubscription} for a {@link
     * ShoppingSpecifics}.
     *
     * @param shoppingSpecifics The {@link ShoppingSpecifics} to create the {@link
     *     CommerceSubscription} for.
     * @return The {@link CommerceSubsription} for the given {@link ShoppingSpecifics}
     */
    public static @NonNull CommerceSubscription createCommerceSubscriptionForShoppingSpecifics(
            @NonNull ShoppingSpecifics shoppingSpecifics) {
        // Use UnsignedLongs to convert ProductClusterId to avoid overflow.
        UserSeenOffer seenOffer =
                new UserSeenOffer(
                        UnsignedLongs.toString(shoppingSpecifics.getOfferId()),
                        shoppingSpecifics.getCurrentPrice().getAmountMicros(),
                        shoppingSpecifics.getCountryCode(),
                        shoppingSpecifics.getLocale());
        return new CommerceSubscription(
                SubscriptionType.PRICE_TRACK,
                IdentifierType.PRODUCT_CLUSTER_ID,
                UnsignedLongs.toString(shoppingSpecifics.getProductClusterId()),
                ManagementType.USER_MANAGED,
                seenOffer);
    }

    /**
     * Checks if the given {@link BookmarkId} is price-tracked.
     *
     * @param bookmarkModel The BookmarkModel used to query bookmarks.
     * @param bookmarkId The BookmarkId to check the price-tracking status of.
     * @param enabled Whether price-tracking should be enabled.
     * @param snackbarManager Manages snackbars, non-null if a message should be sent to alert the
     *     users of price-tracking events.
     * @param resources Used to retrieve resources.
     * @param profile The current profile.
     * @param callback The status callback, may be called multiple times depending if the user
     *     retries on failure.
     */
    public static void setPriceTrackingEnabledWithSnackbars(
            @NonNull BookmarkModel bookmarkModel,
            @Nullable BookmarkId bookmarkId,
            boolean enabled,
            SnackbarManager snackbarManager,
            Resources resources,
            Profile profile,
            Callback<Boolean> callback) {
        // Action to retry the subscription request on failure.
        SnackbarManager.SnackbarController retrySnackbarControllerAction =
                new SnackbarManager.SnackbarController() {
                    @Override
                    public void onAction(Object actionData) {
                        setPriceTrackingEnabledWithSnackbars(
                                bookmarkModel,
                                bookmarkId,
                                enabled,
                                snackbarManager,
                                resources,
                                profile,
                                callback);
                    }
                };
        // Wrapper which shows a snackbar and forwards the result.
        Callback<Boolean> wrapperCallback =
                (success) -> {
                    Snackbar snackbar;
                    if (success) {
                        snackbar =
                                Snackbar.make(
                                        resources.getString(
                                                enabled
                                                        ? R.string.price_tracking_enabled_snackbar
                                                        : R.string
                                                                .price_tracking_disabled_snackbar),
                                        null,
                                        Snackbar.TYPE_NOTIFICATION,
                                        Snackbar.UMA_PRICE_TRACKING_SUCCESS);
                    } else {
                        snackbar =
                                Snackbar.make(
                                                resources.getString(
                                                        R.string.price_tracking_error_snackbar),
                                                retrySnackbarControllerAction,
                                                Snackbar.TYPE_NOTIFICATION,
                                                Snackbar.UMA_PRICE_TRACKING_FAILURE)
                                        .setAction(
                                                resources.getString(
                                                        R.string
                                                                .price_tracking_error_snackbar_action),
                                                null);
                    }
                    snackbar.setSingleLine(false);
                    snackbarManager.showSnackbar(snackbar);
                    callback.onResult(success);
                };
        // Make sure the notification channel is initialized when the user tracks a product.
        // TODO(crbug.com/40245507): Add a SubscriptionsObserver in the PriceDropNotificationManager
        // and initialize the channel there.
        if (enabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            PriceDropNotificationManagerFactory.create(profile).createNotificationChannel();
        }
        PriceTrackingUtils.setPriceTrackingStateForBookmark(
                profile, bookmarkId.getId(), enabled, wrapperCallback);
    }

    /**
     * Gets the power bookmark associated with the given tab.
     *
     * @param bookmarkModel The {@link BookmarkModel} to retrieve bookmark info.
     * @param tab The current {@link Tab} to check.
     * @return The {@link PowerBookmarkMeta} associated with the given tab or null.
     */
    public static @Nullable PowerBookmarkMeta getBookmarkBookmarkMetaForTab(
            @Nullable BookmarkModel bookmarkModel, @Nullable Tab tab) {
        if (bookmarkModel == null || tab == null) return null;

        BookmarkId bookmarkId = bookmarkModel.getUserBookmarkIdForTab(tab);
        if (bookmarkId == null) return null;

        return bookmarkModel.getPowerBookmarkMeta(bookmarkId);
    }

    private static List<BookmarkId> getBookmarkIdsForClusterId(
            Long clusterId, BookmarkModel bookmarkModel) {
        List<BookmarkId> results = new ArrayList<>();
        List<BookmarkId> products = bookmarkModel.getBookmarksOfType(PowerBookmarkType.SHOPPING);
        if (products == null || products.size() == 0) return results;

        for (BookmarkId product : products) {
            PowerBookmarkMeta meta = bookmarkModel.getPowerBookmarkMeta(product);
            if (meta == null || !meta.hasShoppingSpecifics()) continue;

            Long productClusterId = meta.getShoppingSpecifics().getProductClusterId();
            if (productClusterId.equals(clusterId)) {
                results.add(product);
            }
        }

        return results;
    }

    /** Sets the price-tracking eligibility to the test value given. */
    public static void setPriceTrackingEligibleForTesting(@Nullable Boolean enabled) {
        sPriceTrackingEligibleForTesting = enabled;
        ResettersForTesting.register(() -> sPriceTrackingEligibleForTesting = null);
    }

    /** Sets the current page meta to the test value given. */
    public static void setPowerBookmarkMetaForTesting(@Nullable PowerBookmarkMeta meta) {
        sPowerBookmarkMetaForTesting = meta;
        ResettersForTesting.register(() -> sPowerBookmarkMetaForTesting = null);
    }
}