chromium/chrome/browser/feed/android/web_feed_bridge.cc

// 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.

#include <optional>

#include "base/android/callback_android.h"
#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/task/cancelable_task_tracker.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/feed/feed_service_factory.h"
#include "chrome/browser/feed/web_feed_page_information_fetcher.h"
#include "chrome/browser/feed/web_feed_util.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/country_codes/country_codes.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/public/feed_service.h"
#include "components/feed/core/v2/public/types.h"
#include "components/feed/core/v2/public/web_feed_subscriptions.h"
#include "components/feed/feed_feature_list.h"
#include "components/feed/mojom/rss_link_reader.mojom.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/variations/service/variations_service.h"
#include "url/android/gurl_android.h"
#include "url/origin.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/browser/feed/android/jni_headers/WebFeedBridge_jni.h"

class Profile;

namespace feed {

using PageInformation = WebFeedPageInformationFetcher::PageInformation;

namespace {

base::CancelableTaskTracker& TaskTracker() {
  static base::NoDestructor<base::CancelableTaskTracker> task_tracker;
  return *task_tracker;
}

PageInformation ToNativePageInformation(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& pageInfo) {

  PageInformation result;
  result.url = url::GURLAndroid::ToNativeGURL(
      env, Java_WebFeedPageInformation_getUrl(env, pageInfo));
  TabAndroid* tab = TabAndroid::GetNativeTab(
      env, Java_WebFeedPageInformation_getTab(env, pageInfo));
  result.web_contents = tab ? tab->web_contents() : nullptr;
  return result;
}

std::string ToNativeWebFeedId(
    JNIEnv* env,
    const base::android::JavaRef<jbyteArray>& j_web_feed_id) {
  std::string result;
  base::android::JavaByteArrayToString(env, j_web_feed_id, &result);
  return result;
}

base::android::ScopedJavaLocalRef<jbyteArray> ToJavaWebFeedId(
    JNIEnv* env,
    const std::string& web_feed_id) {
  return base::android::ToJavaByteArray(env, web_feed_id);
}

WebFeedSubscriptions* GetSubscriptions() {
  Profile* profile = ProfileManager::GetLastUsedProfile();
  if (!profile)
    return nullptr;
  return GetSubscriptionsForProfile(profile);
}

FeedApi* GetStream() {
  Profile* profile = ProfileManager::GetLastUsedProfile();
  FeedService* service = FeedServiceFactory::GetForBrowserContext(profile);
  if (!service)
    return nullptr;
  return service->GetStream();
}

// ToJava functions convert C++ types to Java. Used in `AdaptCallbackForJava`.

bool ToJava(JNIEnv* env, WebFeedSubscriptions::RefreshResult value) {
  return value.success;
}

base::android::ScopedJavaLocalRef<jobject> ToJava(
    JNIEnv* env,
    const WebFeedMetadata& metadata) {
  return Java_WebFeedMetadata_Constructor(
      env, ToJavaWebFeedId(env, metadata.web_feed_id),
      base::android::ConvertUTF8ToJavaString(env, metadata.title),
      url::GURLAndroid::FromNativeGURL(env, metadata.publisher_url),
      static_cast<int>(metadata.subscription_status),
      static_cast<int>(metadata.availability_status), metadata.is_recommended,
      url::GURLAndroid::FromNativeGURL(env, metadata.favicon_url));
}

base::android::ScopedJavaLocalRef<jobject> ToJava(
    JNIEnv* env,
    const WebFeedSubscriptions::FollowWebFeedResult& result) {
  return Java_FollowResults_Constructor(env,
                                        static_cast<int>(result.request_status),
                                        ToJava(env, result.web_feed_metadata));
}

base::android::ScopedJavaLocalRef<jobject> ToJava(
    JNIEnv* env,
    const WebFeedSubscriptions::UnfollowWebFeedResult& result) {
  return Java_UnfollowResults_Constructor(
      env, static_cast<int>(result.request_status));
}

base::android::ScopedJavaLocalRef<jobject> ToJava(
    JNIEnv* env,
    std::vector<WebFeedMetadata> metadata_list) {
  std::vector<base::android::ScopedJavaLocalRef<jobject>> j_metadata_list;
  for (const WebFeedMetadata& metadata : metadata_list) {
    j_metadata_list.push_back(ToJava(env, metadata));
  }
  return base::android::ToJavaArrayOfObjects(env, j_metadata_list);
}

base::android::ScopedJavaLocalRef<jobject> ToJava(
    JNIEnv* env,
    const WebFeedSubscriptions::QueryWebFeedResult& result) {
  return Java_QueryResult_Constructor(
      env, base::android::ConvertUTF8ToJavaString(env, result.web_feed_id),
      base::android::ConvertUTF8ToJavaString(env, result.title),
      base::android::ConvertUTF8ToJavaString(env, result.url));
}

base::android::ScopedJavaLocalRef<jobject> ToJava(
    JNIEnv* env,
    history::DailyVisitsResult result) {
  return base::android::ToJavaIntArray(
      env, std::vector<int>({result.total_visits, result.days_with_visits}));
}

base::OnceCallback<void(WebFeedMetadata)> AdaptWebFeedMetadataCallback(
    const base::android::JavaParamRef<jobject>& callback) {
  auto adaptor = [](const base::android::JavaRef<jobject>& callback,
                    WebFeedMetadata metadata) {
    JNIEnv* env = base::android::AttachCurrentThread();
    base::android::RunObjectCallbackAndroid(callback, ToJava(env, metadata));
  };

  return base::BindOnce(adaptor,
                        base::android::ScopedJavaGlobalRef<jobject>(callback));
}

base::OnceCallback<void(WebFeedSubscriptions::QueryWebFeedResult)>
AdaptQueryWebFeedResultCallback(
    const base::android::JavaParamRef<jobject>& callback) {
  auto adaptor = [](const base::android::JavaRef<jobject>& callback,
                    WebFeedSubscriptions::QueryWebFeedResult result) {
    JNIEnv* env = base::android::AttachCurrentThread();
    base::android::RunObjectCallbackAndroid(callback, ToJava(env, result));
  };

  return base::BindOnce(adaptor,
                        base::android::ScopedJavaGlobalRef<jobject>(callback));
}

void RunJavaCallback(const base::android::JavaRef<jobject>& callback,
                     const base::android::JavaRef<jobject>& arg) {
  base::android::RunObjectCallbackAndroid(callback, arg);
}
void RunJavaCallback(const base::android::JavaRef<jobject>& callback,
                     bool arg) {
  base::android::RunBooleanCallbackAndroid(callback, arg);
}

template <typename T>
base::OnceCallback<void(T)> AdaptCallbackForJava(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& callback) {
  auto adaptor = [](const base::android::JavaRef<jobject>& callback, T result) {
    JNIEnv* env = base::android::AttachCurrentThread();
    RunJavaCallback(callback, ToJava(env, std::move(result)));
  };

  return base::BindOnce(adaptor,
                        base::android::ScopedJavaGlobalRef<jobject>(callback));
}

}  // namespace

static void JNI_WebFeedBridge_FollowWebFeed(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& pageInfo,
    jint change_reason,
    const base::android::JavaParamRef<jobject>& j_callback) {
  auto callback =
      AdaptCallbackForJava<WebFeedSubscriptions::FollowWebFeedResult>(
          env, j_callback);

  PageInformation page_info = ToNativePageInformation(env, pageInfo);
  // Make sure web_contents is not NULL since the user might navigate away from
  // the current tab that is requested to follow.
  if (!page_info.web_contents) {
    std::move(callback).Run({});
    return;
  }

  FollowWebFeed(
      page_info.web_contents,
      static_cast<feedwire::webfeed::WebFeedChangeReason>(change_reason),
      std::move(callback));
}

static jboolean JNI_WebFeedBridge_IsCormorantEnabledForLocale(JNIEnv* env) {
  return JNI_WebFeedBridge_IsWebFeedEnabled(env);
}

static jboolean JNI_WebFeedBridge_IsWebFeedEnabled(JNIEnv* env) {
  return feed::IsWebFeedEnabledForLocale(FeedServiceFactory::GetCountry());
}

static void JNI_WebFeedBridge_FollowWebFeedById(
    JNIEnv* env,
    const base::android::JavaParamRef<jbyteArray>& webFeedId,
    jboolean is_durable,
    jint change_reason,
    const base::android::JavaParamRef<jobject>& j_callback) {
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  auto callback =
      AdaptCallbackForJava<WebFeedSubscriptions::FollowWebFeedResult>(
          env, j_callback);
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->FollowWebFeed(
      ToNativeWebFeedId(env, webFeedId),
      /*is_durable_request=*/is_durable,
      static_cast<feedwire::webfeed::WebFeedChangeReason>(change_reason),
      std::move(callback));
}

static void JNI_WebFeedBridge_UnfollowWebFeed(
    JNIEnv* env,
    const base::android::JavaParamRef<jbyteArray>& webFeedId,
    jboolean is_durable,
    jint change_reason,
    const base::android::JavaParamRef<jobject>& j_callback) {
  auto callback =
      AdaptCallbackForJava<WebFeedSubscriptions::UnfollowWebFeedResult>(
          env, j_callback);
  UnfollowWebFeed(
      ToNativeWebFeedId(env, webFeedId),
      /*is_durable_request=*/is_durable,
      static_cast<feedwire::webfeed::WebFeedChangeReason>(change_reason),
      std::move(callback));
}

static void JNI_WebFeedBridge_FindWebFeedInfoForPage(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& pageInfo,
    const int reason,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(WebFeedMetadata)> callback =
      AdaptCallbackForJava<WebFeedMetadata>(env, j_callback);

  PageInformation page_info = ToNativePageInformation(env, pageInfo);
  // Make sure web_contents is not NULL since the user might navigate away from
  // the current tab that is requested to find info.
  if (!page_info.web_contents) {
    std::move(callback).Run({});
    return;
  }
  FindWebFeedInfoForPage(
      page_info.web_contents,
      static_cast<WebFeedPageInformationRequestReason>(reason),
      std::move(callback));
}

static void JNI_WebFeedBridge_FindWebFeedInfoForWebFeedId(
    JNIEnv* env,
    const base::android::JavaParamRef<jbyteArray>& webFeedId,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(WebFeedMetadata)> callback =
      AdaptWebFeedMetadataCallback(j_callback);
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->FindWebFeedInfoForWebFeedId(ToNativeWebFeedId(env, webFeedId),
                                             std::move(callback));
}

static void JNI_WebFeedBridge_GetAllSubscriptions(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(std::vector<WebFeedMetadata>)> callback =
      AdaptCallbackForJava<std::vector<WebFeedMetadata>>(env, j_callback);
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->GetAllSubscriptions(std::move(callback));
}

static void JNI_WebFeedBridge_RefreshSubscriptions(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(WebFeedSubscriptions::RefreshResult)> callback =
      AdaptCallbackForJava<WebFeedSubscriptions::RefreshResult>(env,
                                                                j_callback);
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->RefreshSubscriptions(std::move(callback));
}

static void JNI_WebFeedBridge_RefreshRecommendedFeeds(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(WebFeedSubscriptions::RefreshResult)> callback =
      AdaptCallbackForJava<WebFeedSubscriptions::RefreshResult>(env,
                                                                j_callback);
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->RefreshRecommendedFeeds(std::move(callback));
}

static void JNI_WebFeedBridge_GetRecentVisitCountsToHost(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& j_url,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(history::DailyVisitsResult)> callback =
      AdaptCallbackForJava<history::DailyVisitsResult>(env, j_callback);

  Profile* profile = ProfileManager::GetLastUsedProfile();
  history::HistoryService* history_service = nullptr;
  if (profile) {
    history_service = HistoryServiceFactory::GetForProfile(
        profile, ServiceAccessType::IMPLICIT_ACCESS);
  }
  if (!history_service) {
    std::move(callback).Run({});
    return;
  }

  // Ignore any visits within the last hour so that we do not count the current
  // visit to the page.
  auto end_time = base::Time::Now() - base::Hours(1);
  auto begin_time =
      base::Time::Now() -
      base::Days(GetFeedConfig().webfeed_accelerator_recent_visit_history_days);
  history_service->GetDailyVisitsToOrigin(
      url::Origin::Create(url::GURLAndroid::ToNativeGURL(env, j_url)),
      begin_time, end_time, std::move(callback), &TaskTracker());
}

static void JNI_WebFeedBridge_IncrementFollowedFromWebPageMenuCount(
    JNIEnv* env) {
  FeedApi* stream = GetStream();
  if (!stream)
    return;

  stream->IncrementFollowedFromWebPageMenuCount();
}

static void JNI_WebFeedBridge_QueryWebFeed(
    JNIEnv* env,
    const base::android::JavaParamRef<jstring>& url,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(WebFeedSubscriptions::QueryWebFeedResult)> callback =
      AdaptQueryWebFeedResultCallback(j_callback);
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->QueryWebFeed(
      GURL(base::android::ConvertJavaStringToUTF8(env, url)),
      std::move(callback));
}

static void JNI_WebFeedBridge_QueryWebFeedId(
    JNIEnv* env,
    const base::android::JavaParamRef<jstring>& id,
    const base::android::JavaParamRef<jobject>& j_callback) {
  base::OnceCallback<void(WebFeedSubscriptions::QueryWebFeedResult)> callback =
      AdaptQueryWebFeedResultCallback(j_callback);
  WebFeedSubscriptions* subscriptions = GetSubscriptions();
  if (!subscriptions) {
    std::move(callback).Run({});
    return;
  }
  subscriptions->QueryWebFeedId(base::android::ConvertJavaStringToUTF8(env, id),
                                std::move(callback));
}
}  // namespace feed