chromium/components/commerce/core/android/shopping_service_android.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/commerce/core/android/shopping_service_android.h"

#include <optional>

#include "base/android/callback_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/functional/bind.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/commerce/core/subscriptions/commerce_subscription.h"
#include "url/android/gurl_android.h"
#include "url/gurl.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/commerce/core/android/core_jni/DiscountInfo_jni.h"
#include "components/commerce/core/android/core_jni/ShoppingService_jni.h"

using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::RunBooleanCallbackAndroid;
using base::android::ScopedJavaLocalRef;

namespace commerce {

namespace {

ScopedJavaLocalRef<jobject> ToJavaObject(JNIEnv* env,
                                         const CommerceSubscription& sub) {
  return Java_ShoppingService_createSubscription(
      env, static_cast<int>(sub.type), static_cast<int>(sub.id_type),
      static_cast<int>(sub.management_type),
      ConvertUTF8ToJavaString(env, sub.id));
}

ScopedJavaLocalRef<jobject> ConvertToJavaDiscountInfo(
    JNIEnv* env,
    const DiscountInfo& info) {
  ScopedJavaLocalRef<jobject> terms_and_conditions_java_object =
      info.terms_and_conditions.has_value()
          ? ConvertUTF8ToJavaString(env, info.terms_and_conditions.value())
          : nullptr;
  ScopedJavaLocalRef<jobject> discount_code_java_object =
      info.discount_code.has_value()
          ? ConvertUTF8ToJavaString(env, info.discount_code.value())
          : nullptr;

  return Java_DiscountInfo_Constructor(
      env, static_cast<int>(info.cluster_type), static_cast<int>(info.type),
      ConvertUTF8ToJavaString(env, info.language_code),
      ConvertUTF8ToJavaString(env, info.description_detail),
      terms_and_conditions_java_object,
      ConvertUTF8ToJavaString(env, info.value_in_text),
      discount_code_java_object, info.id, info.is_merchant_wide,
      info.expiry_time_sec, info.offer_id);
}

ScopedJavaLocalRef<jobjectArray> ConvertToJavaDiscountInfos(
    JNIEnv* env,
    const std::vector<DiscountInfo>& info) {
  jclass discount_info_clazz =
      org_chromium_components_commerce_core_DiscountInfo_clazz(env);
  ScopedJavaLocalRef<jobjectArray> discount_info_array(
      env, env->NewObjectArray(info.size(), discount_info_clazz, nullptr));

  for (size_t i = 0; i < info.size(); i++) {
    ScopedJavaLocalRef<jobject> discount_info_java =
        ConvertToJavaDiscountInfo(env, info[i]);
    env->SetObjectArrayElement(discount_info_array.obj(), i,
                               discount_info_java.obj());
  }
  return discount_info_array;
}

}  // namespace

ShoppingServiceAndroid::ShoppingServiceAndroid(ShoppingService* service)
    : shopping_service_(service), weak_ptr_factory_(this) {
  java_ref_.Reset(Java_ShoppingService_create(
      base::android::AttachCurrentThread(), reinterpret_cast<jlong>(this)));
  scoped_subscriptions_observer_.Observe(shopping_service_);
}

ShoppingServiceAndroid::~ShoppingServiceAndroid() {
  Java_ShoppingService_destroy(base::android::AttachCurrentThread(), java_ref_);
}

void ShoppingServiceAndroid::GetProductInfoForUrl(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& j_gurl,
    const JavaParamRef<jobject>& j_callback) {
  CHECK(shopping_service_);

  GURL url = url::GURLAndroid::ToNativeGURL(env, j_gurl);

  shopping_service_->GetProductInfoForUrl(
      url, base::BindOnce(&ShoppingServiceAndroid::HandleProductInfoCallback,
                          weak_ptr_factory_.GetWeakPtr(), env,
                          ScopedJavaGlobalRef<jobject>(j_callback)));
}

ScopedJavaLocalRef<jobject>
ShoppingServiceAndroid::GetAvailableProductInfoForUrl(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& j_gurl) {
  CHECK(shopping_service_);

  GURL url = url::GURLAndroid::ToNativeGURL(env, j_gurl);

  std::optional<ProductInfo> info =
      shopping_service_->GetAvailableProductInfoForUrl(url);

  ScopedJavaLocalRef<jobject> info_java_object(nullptr);
  if (info.has_value()) {
    info_java_object = Java_ShoppingService_createProductInfo(
        env, ConvertUTF8ToJavaString(env, info->title),
        url::GURLAndroid::FromNativeGURL(env, GURL(info->image_url)),
        info->product_cluster_id.has_value(),
        info->product_cluster_id.value_or(0), info->offer_id.has_value(),
        info->offer_id.value_or(0),
        ConvertUTF8ToJavaString(env, info->currency_code), info->amount_micros,
        ConvertUTF8ToJavaString(env, info->country_code),
        info->previous_amount_micros.has_value(),
        info->previous_amount_micros.value_or(0));
  }

  return info_java_object;
}

void ShoppingServiceAndroid::HandleProductInfoCallback(
    JNIEnv* env,
    const ScopedJavaGlobalRef<jobject>& callback,
    const GURL& url,
    const std::optional<const ProductInfo>& info) {
  ScopedJavaLocalRef<jobject> info_java_object(nullptr);
  if (info.has_value()) {
    info_java_object = Java_ShoppingService_createProductInfo(
        env, ConvertUTF8ToJavaString(env, info->title),
        url::GURLAndroid::FromNativeGURL(env, GURL(info->image_url)),
        info->product_cluster_id.has_value(),
        info->product_cluster_id.value_or(0), info->offer_id.has_value(),
        info->offer_id.value_or(0),
        ConvertUTF8ToJavaString(env, info->currency_code), info->amount_micros,
        ConvertUTF8ToJavaString(env, info->country_code),
        info->previous_amount_micros.has_value(),
        info->previous_amount_micros.value_or(0));
  }

  Java_ShoppingService_runProductInfoCallback(
      env, callback, url::GURLAndroid::FromNativeGURL(env, url),
      info_java_object);
}

void ShoppingServiceAndroid::GetMerchantInfoForUrl(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& j_gurl,
    const JavaParamRef<jobject>& j_callback) {
  CHECK(shopping_service_);

  GURL url = url::GURLAndroid::ToNativeGURL(env, j_gurl);

  shopping_service_->GetMerchantInfoForUrl(
      url, base::BindOnce(&ShoppingServiceAndroid::HandleMerchantInfoCallback,
                          weak_ptr_factory_.GetWeakPtr(), env,
                          ScopedJavaGlobalRef<jobject>(j_callback)));
}

void ShoppingServiceAndroid::HandleMerchantInfoCallback(
    JNIEnv* env,
    const ScopedJavaGlobalRef<jobject>& callback,
    const GURL& url,
    std::optional<MerchantInfo> info) {
  ScopedJavaLocalRef<jobject> info_java_object(nullptr);
  if (info.has_value()) {
    info_java_object = Java_ShoppingService_createMerchantInfo(
        env, info->star_rating, info->count_rating,
        url::GURLAndroid::FromNativeGURL(env, GURL(info->details_page_url)),
        info->has_return_policy, info->non_personalized_familiarity_score,
        info->contains_sensitive_content, info->proactive_message_disabled);
  }

  Java_ShoppingService_runMerchantInfoCallback(
      env, callback, url::GURLAndroid::FromNativeGURL(env, url),
      info_java_object);
}

void ShoppingServiceAndroid::GetPriceInsightsInfoForUrl(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& j_gurl,
    const JavaParamRef<jobject>& j_callback) {
  CHECK(shopping_service_);

  GURL url = url::GURLAndroid::ToNativeGURL(env, j_gurl);

  shopping_service_->GetPriceInsightsInfoForUrl(
      url,
      base::BindOnce(&ShoppingServiceAndroid::HandlePriceInsightsInfoCallback,
                     weak_ptr_factory_.GetWeakPtr(), env,
                     ScopedJavaGlobalRef<jobject>(j_callback)));
}

void ShoppingServiceAndroid::HandlePriceInsightsInfoCallback(
    JNIEnv* env,
    const ScopedJavaGlobalRef<jobject>& callback,
    const GURL& url,
    const std::optional<PriceInsightsInfo>& info) {
  ScopedJavaLocalRef<jobject> info_java_object(nullptr);
  if (info.has_value()) {
    ScopedJavaLocalRef<jobject> j_price_points = nullptr;
    for (const auto& point : info->catalog_history_prices) {
      j_price_points = Java_ShoppingService_createPricePointAndAddToList(
          env, j_price_points, ConvertUTF8ToJavaString(env, std::get<0>(point)),
          std::get<1>(point));
    }

    info_java_object = Java_ShoppingService_createPriceInsightsInfo(
        env, info->product_cluster_id.has_value(),
        info->product_cluster_id.value_or(0),
        ConvertUTF8ToJavaString(env, info->currency_code),
        info->typical_low_price_micros.has_value(),
        info->typical_low_price_micros.value_or(0),
        info->typical_high_price_micros.has_value(),
        info->typical_high_price_micros.value_or(0),
        info->catalog_attributes.has_value(),
        ConvertUTF8ToJavaString(env, info->catalog_attributes.value_or("")),
        j_price_points, info->jackpot_url.has_value(),
        url::GURLAndroid::FromNativeGURL(env,
                                         info->jackpot_url.value_or(GURL())),
        static_cast<int>(info->price_bucket), info->has_multiple_catalogs);
  }

  Java_ShoppingService_runPriceInsightsInfoCallback(
      env, callback, url::GURLAndroid::FromNativeGURL(env, url),
      info_java_object);
}

void ShoppingServiceAndroid::GetDiscountInfoForUrl(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& j_gurl,
    const JavaParamRef<jobject>& j_callback) {
  CHECK(shopping_service_);

  GURL url = url::GURLAndroid::ToNativeGURL(env, j_gurl);

  shopping_service_->GetDiscountInfoForUrl(
      {url}, base::BindOnce(&ShoppingServiceAndroid::HandleDiscountInfoCallback,
                            weak_ptr_factory_.GetWeakPtr(), env,
                            ScopedJavaGlobalRef<jobject>(j_callback)));
}

void ShoppingServiceAndroid::HandleDiscountInfoCallback(
    JNIEnv* env,
    const ScopedJavaGlobalRef<jobject>& callback,
    const GURL& url,
    const std::vector<DiscountInfo> info) {
  ScopedJavaLocalRef<jobjectArray> discount_info_array_obj =
      ConvertToJavaDiscountInfos(env, info);
  Java_ShoppingService_runDiscountInfoCallback(
      env, callback, url::GURLAndroid::FromNativeGURL(env, url),
      discount_info_array_obj);
}

void ShoppingServiceAndroid::FetchPriceEmailPref(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  shopping_service_->FetchPriceEmailPref();
}

void ShoppingServiceAndroid::ScheduleSavedProductUpdate(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  shopping_service_->ScheduleSavedProductUpdate();
}

void ShoppingServiceAndroid::Subscribe(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint j_type,
    jint j_id_type,
    jint j_management_type,
    const JavaParamRef<jstring>& j_id,
    const JavaParamRef<jstring>& j_seen_offer_id,
    jlong j_seen_price,
    const JavaParamRef<jstring>& j_seen_country,
    const JavaParamRef<jstring>& j_seen_locale,
    const JavaParamRef<jobject>& j_callback) {
  std::string id = ConvertJavaStringToUTF8(j_id);
  std::string seen_offer_id = ConvertJavaStringToUTF8(j_seen_offer_id);
  std::string seen_country = ConvertJavaStringToUTF8(j_seen_country);
  std::string seen_locale = ConvertJavaStringToUTF8(j_seen_locale);
  CHECK(!id.empty());

  auto user_seen_offer = std::make_optional<UserSeenOffer>(
      seen_offer_id, j_seen_price, seen_country, seen_locale);
  CommerceSubscription sub(SubscriptionType(j_type), IdentifierType(j_id_type),
                           id, ManagementType(j_management_type),
                           kUnknownSubscriptionTimestamp,
                           std::move(user_seen_offer));
  std::unique_ptr<std::vector<CommerceSubscription>> subs =
      std::make_unique<std::vector<CommerceSubscription>>();
  subs->push_back(std::move(sub));

  auto callback = base::BindOnce(&RunBooleanCallbackAndroid,
                                 ScopedJavaGlobalRef<jobject>(j_callback));

  shopping_service_->Subscribe(std::move(subs), std::move(callback));
}

void ShoppingServiceAndroid::Unsubscribe(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint j_type,
    jint j_id_type,
    jint j_management_type,
    const JavaParamRef<jstring>& j_id,
    const JavaParamRef<jobject>& j_callback) {
  std::string id = ConvertJavaStringToUTF8(j_id);
  CHECK(!id.empty());

  CommerceSubscription sub(SubscriptionType(j_type), IdentifierType(j_id_type),
                           id, ManagementType(j_management_type),
                           kUnknownSubscriptionTimestamp, std::nullopt);
  std::unique_ptr<std::vector<CommerceSubscription>> subs =
      std::make_unique<std::vector<CommerceSubscription>>();
  subs->push_back(std::move(sub));

  auto callback = base::BindOnce(&RunBooleanCallbackAndroid,
                                 ScopedJavaGlobalRef<jobject>(j_callback));

  shopping_service_->Unsubscribe(std::move(subs), std::move(callback));
}

void ShoppingServiceAndroid::IsSubscribed(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint j_type,
    jint j_id_type,
    jint j_management_type,
    const JavaParamRef<jstring>& j_id,
    const JavaParamRef<jobject>& j_callback) {
  std::string id = ConvertJavaStringToUTF8(j_id);
  CHECK(!id.empty());

  CommerceSubscription sub(SubscriptionType(j_type), IdentifierType(j_id_type),
                           id, ManagementType(j_management_type),
                           kUnknownSubscriptionTimestamp, std::nullopt);

  shopping_service_->IsSubscribed(
      std::move(sub),
      base::BindOnce(
          [](const ScopedJavaGlobalRef<jobject>& callback, bool is_tracked) {
            base::android::RunBooleanCallbackAndroid(callback, is_tracked);
          },
          ScopedJavaGlobalRef<jobject>(j_callback)));
}

bool ShoppingServiceAndroid::IsSubscribedFromCache(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint j_type,
    jint j_id_type,
    jint j_management_type,
    const JavaParamRef<jstring>& j_id) {
  std::string id = ConvertJavaStringToUTF8(j_id);
  CHECK(!id.empty());

  CommerceSubscription sub(SubscriptionType(j_type), IdentifierType(j_id_type),
                           id, ManagementType(j_management_type),
                           kUnknownSubscriptionTimestamp, std::nullopt);

  return shopping_service_->IsSubscribedFromCache(std::move(sub));
}

void ShoppingServiceAndroid::GetAllPriceTrackedBookmarks(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& j_callback) {
  shopping_service_->GetAllPriceTrackedBookmarks(base::BindOnce(
      [](JNIEnv* env, const ScopedJavaGlobalRef<jobject>& callback,
         std::vector<const bookmarks::BookmarkNode*> tracked_items) {
        std::vector<int64_t> ids;
        for (const bookmarks::BookmarkNode* bookmark : tracked_items) {
          ids.push_back(bookmark->id());
        }
        Java_ShoppingService_runGetAllPriceTrackedBookmarksCallback(
            env, callback, base::android::ToJavaLongArray(env, ids));
      },
      env, ScopedJavaGlobalRef<jobject>(j_callback)));
}

void ShoppingServiceAndroid::OnSubscribe(const CommerceSubscription& sub,
                                         bool succeeded) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_ShoppingService_onSubscribe(env, java_ref_, ToJavaObject(env, sub),
                                   succeeded);
}

void ShoppingServiceAndroid::OnUnsubscribe(const CommerceSubscription& sub,
                                           bool succeeded) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_ShoppingService_onUnsubscribe(env, java_ref_, ToJavaObject(env, sub),
                                     succeeded);
}

bool ShoppingServiceAndroid::IsShoppingListEligible(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  return shopping_service_->IsShoppingListEligible();
}

bool ShoppingServiceAndroid::IsMerchantViewerEnabled(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  return shopping_service_->IsMerchantViewerEnabled();
}

bool ShoppingServiceAndroid::IsCommercePriceTrackingEnabled(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  return shopping_service_->IsCommercePriceTrackingEnabled();
}

bool ShoppingServiceAndroid::IsPriceInsightsEligible(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  return shopping_service_->IsPriceInsightsEligible();
}

bool ShoppingServiceAndroid::IsDiscountEligibleToShowOnNavigation(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  CHECK(shopping_service_);

  return shopping_service_->IsDiscountEligibleToShowOnNavigation();
}

}  // namespace commerce