| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/tab_resumption/visited_url_ranking_backend.h" |
| |
| #include <map> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/android/jni_string.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/sync/session_sync_service_factory.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/visited_url_ranking/visited_url_ranking_service_factory.h" |
| #include "components/sync/base/data_type.h" |
| #include "components/sync/service/sync_service.h" |
| #include "components/sync_sessions/session_sync_service.h" |
| #include "components/visited_url_ranking/public/fetch_options.h" |
| #include "components/visited_url_ranking/public/url_visit.h" |
| #include "components/visited_url_ranking/public/url_visit_util.h" |
| #include "url/android/gurl_android.h" |
| |
| // Must come after all headers that specialize FromJniType() / ToJniType(). |
| #include "chrome/browser/tab_resumption/jni_headers/VisitedUrlRankingBackend_jni.h" |
| |
| using tab_resumption::jni::SuggestionEntryType; |
| |
| namespace { |
| |
| using Source = visited_url_ranking::URLVisit::Source; |
| using FetchSources = |
| base::EnumSet<Source, Source::kNotApplicable, Source::kForeign>; |
| using tab_resumption::jni::Java_VisitedUrlRankingBackend_addSuggestionEntry; |
| using tab_resumption::jni::Java_VisitedUrlRankingBackend_onSuggestions; |
| using tab_resumption::jni::VisitedUrlRankingBackend; |
| |
| using visited_url_ranking::Config; |
| using visited_url_ranking::Fetcher; |
| using visited_url_ranking::FetchOptions; |
| using visited_url_ranking::ResultStatus; |
| using visited_url_ranking::ScoredURLUserAction; |
| using visited_url_ranking::URLVisitAggregate; |
| using visited_url_ranking::URLVisitAggregatesTransformType; |
| using visited_url_ranking::VisitedURLRankingService; |
| |
| // Must match Java Tab.INVALID_TAB_ID. |
| static constexpr int kInvalidTabId = -1; |
| |
| // FetchOptions::CreateDefaultFetchOptionsForTabResumption() specifies data |
| // sources that are currently unavailable. This function returns a simplified |
| // FetchOptions instance. |
| FetchOptions CreateFetchOptionsForTabResumption(base::Time current_time, |
| bool fetch_local_tab, |
| bool fetch_history) { |
| FetchOptions::URLTypeSet expected_types = { |
| FetchOptions::URLType::kActiveRemoteTab}; |
| if (fetch_local_tab) { |
| expected_types.Put(FetchOptions::URLType::kLocalVisit); |
| } |
| if (fetch_history) { |
| expected_types.Put(FetchOptions::URLType::kLocalVisit); |
| expected_types.Put(FetchOptions::URLType::kRemoteVisit); |
| expected_types.Put(FetchOptions::URLType::kCCTVisit); |
| } |
| return FetchOptions::CreateFetchOptionsForTabResumption(expected_types); |
| } |
| |
| // Class to manage tab resumption fetch and rank flow, containing required |
| // parameters and states |
| class FetchAndRankFlow : public base::RefCounted<FetchAndRankFlow> { |
| public: |
| friend base::RefCounted<FetchAndRankFlow>; |
| |
| FetchAndRankFlow(Profile* profile, |
| JNIEnv* env, |
| jni_zero::ScopedJavaGlobalRef<jobject> jobj, |
| base::Time current_time, |
| bool fetch_local_tabs, |
| bool fetch_history, |
| jni_zero::ScopedJavaGlobalRef<jobject> j_suggestions, |
| jni_zero::ScopedJavaGlobalRef<jobject> j_callback) |
| : ranking_service_( |
| visited_url_ranking::VisitedURLRankingServiceFactory::GetInstance() |
| ->GetForProfile(profile)), |
| env_(env), |
| jobj_(jobj), |
| j_suggestions_(j_suggestions), |
| j_callback_(j_callback), |
| fetch_options_(CreateFetchOptionsForTabResumption(current_time, |
| fetch_local_tabs, |
| fetch_history)), |
| config_({.key = visited_url_ranking::kTabResumptionRankerKey}) {} |
| |
| void RunFlow() { |
| ranking_service_->FetchURLVisitAggregates( |
| fetch_options_, |
| base::BindOnce(&FetchAndRankFlow::OnFetched, base::RetainedRef(this))); |
| } |
| |
| private: |
| ~FetchAndRankFlow() = default; |
| |
| // Continuing after RunFlow()'s call to FetchURLVisitAggregates(). |
| void OnFetched(ResultStatus status, |
| std::vector<URLVisitAggregate> aggregates) { |
| if (status != ResultStatus::kSuccess) { |
| Java_VisitedUrlRankingBackend_onSuggestions(env_, j_suggestions_, |
| j_callback_); |
| return; |
| } |
| |
| ranking_service_->RankURLVisitAggregates( |
| config_, std::move(aggregates), |
| base::BindOnce(&FetchAndRankFlow::OnRanked, base::RetainedRef(this))); |
| } |
| |
| // Continuing after OnFetched()'s call to RankVisitAggregates(). |
| void OnRanked(ResultStatus status, |
| std::vector<URLVisitAggregate> aggregates) { |
| if (status != ResultStatus::kSuccess) { |
| Java_VisitedUrlRankingBackend_onSuggestions(env_, j_suggestions_, |
| j_callback_); |
| return; |
| } |
| |
| ranking_service_->DecorateURLVisitAggregates( |
| {}, std::move(aggregates), |
| base::BindOnce(&FetchAndRankFlow::PassResults, |
| base::RetainedRef(this))); |
| } |
| |
| // Translates results to Java objects and passes results to |j_callback_|. |
| void PassResults(visited_url_ranking::ResultStatus status, |
| std::vector<URLVisitAggregate> aggregates) { |
| for (const URLVisitAggregate& aggregate : aggregates) { |
| // TODO(crbug.com/337858147): Choose representative member. For now, just |
| // take the first one. |
| if (aggregate.fetcher_data_map.empty()) { |
| continue; |
| } |
| auto decoration = |
| !aggregate.decorations.empty() |
| ? base::android::ConvertUTF16ToJavaString( |
| env_, |
| visited_url_ranking::GetMostRelevantDecoration(aggregate) |
| .GetDisplayString()) |
| : nullptr; |
| const auto& fetcher_entry = *aggregate.fetcher_data_map.begin(); |
| std::visit( |
| visited_url_ranking::URLVisitVariantHelper{ |
| [&](const URLVisitAggregate::TabData& tab_data) { |
| bool is_local_tab = |
| (tab_data.last_active_tab.session_tag == std::nullopt); |
| Java_VisitedUrlRankingBackend_addSuggestionEntry( |
| env_, jobj_, |
| JniIntWrapper(static_cast<int>( |
| is_local_tab ? SuggestionEntryType::kLocalTab |
| : SuggestionEntryType::kForeignTab)), |
| base::android::ConvertUTF8ToJavaString( |
| env_, |
| tab_data.last_active_tab.session_name.value_or("?")), |
| url::GURLAndroid::FromNativeGURL( |
| env_, tab_data.last_active_tab.visit.url), |
| base::android::ConvertUTF16ToJavaString( |
| env_, tab_data.last_active_tab.visit.title), |
| tab_data.last_active.InMillisecondsSinceUnixEpoch(), |
| is_local_tab ? tab_data.last_active_tab.id : kInvalidTabId, |
| base::android::ConvertUTF8ToJavaString(env_, |
| aggregate.url_key), |
| aggregate.request_id.is_null() |
| ? -1LL |
| : aggregate.request_id.GetUnsafeValue(), |
| nullptr, decoration, !is_local_tab, j_suggestions_); |
| }, |
| [&](const URLVisitAggregate::HistoryData& history_data) { |
| bool need_match_local_tab = |
| history_data.last_visited.context_annotations.on_visit |
| .browser_type == |
| history::VisitContextAnnotations::BrowserType::kTabbed; |
| Java_VisitedUrlRankingBackend_addSuggestionEntry( |
| env_, jobj_, |
| JniIntWrapper( |
| static_cast<int>(SuggestionEntryType::kHistory)), |
| base::android::ConvertUTF8ToJavaString(env_, "?"), |
| url::GURLAndroid::FromNativeGURL( |
| env_, history_data.last_visited.url_row.url()), |
| base::android::ConvertUTF16ToJavaString( |
| env_, history_data.last_visited.url_row.title()), |
| history_data.last_visited.visit_row.visit_time |
| .InMillisecondsSinceUnixEpoch(), |
| kInvalidTabId, |
| base::android::ConvertUTF8ToJavaString(env_, |
| aggregate.url_key), |
| aggregate.request_id.is_null() |
| ? -1LL |
| : aggregate.request_id.GetUnsafeValue(), |
| history_data.last_app_id |
| ? base::android::ConvertUTF8ToJavaString( |
| env_, *history_data.last_app_id) |
| : nullptr, |
| decoration, need_match_local_tab, j_suggestions_); |
| }}, |
| fetcher_entry.second); |
| } |
| |
| Java_VisitedUrlRankingBackend_onSuggestions(env_, j_suggestions_, |
| j_callback_); |
| } |
| |
| private: |
| raw_ptr<visited_url_ranking::VisitedURLRankingService> ranking_service_; |
| raw_ptr<JNIEnv> env_; |
| jni_zero::ScopedJavaGlobalRef<jobject> jobj_; |
| jni_zero::ScopedJavaGlobalRef<jobject> j_suggestions_; |
| jni_zero::ScopedJavaGlobalRef<jobject> j_callback_; |
| const FetchOptions fetch_options_; |
| const Config config_; |
| }; |
| |
| } // namespace |
| |
| namespace tab_resumption::jni { |
| |
| static jlong JNI_VisitedUrlRankingBackend_Init( |
| JNIEnv* env, |
| const jni_zero::JavaParamRef<jobject>& jobj, |
| Profile* profile) { |
| return reinterpret_cast<intptr_t>( |
| new VisitedUrlRankingBackend(jobj, profile)); |
| } |
| |
| VisitedUrlRankingBackend::VisitedUrlRankingBackend( |
| const jni_zero::JavaRef<jobject>& jobj, |
| Profile* profile) |
| : jobj_(jni_zero::ScopedJavaGlobalRef<jobject>(jobj)), profile_(profile) { |
| sync_sessions::SessionSyncService* session_sync_service = |
| SessionSyncServiceFactory::GetInstance()->GetForProfile(profile_); |
| |
| // SessionSyncService can be null in tests. |
| if (session_sync_service) { |
| // base::Unretained() is safe below because the subscription itself is a |
| // class member field and handles destruction well. |
| foreign_session_updated_subscription_ = |
| session_sync_service->SubscribeToForeignSessionsChanged( |
| base::BindRepeating(&VisitedUrlRankingBackend::OnRefresh, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| VisitedUrlRankingBackend::~VisitedUrlRankingBackend() = default; |
| |
| void VisitedUrlRankingBackend::Destroy(JNIEnv* env) { |
| jobj_ = nullptr; |
| delete this; |
| } |
| |
| void VisitedUrlRankingBackend::TriggerUpdate(JNIEnv* env) { |
| syncer::SyncService* service = SyncServiceFactory::GetForProfile(profile_); |
| if (!service) { |
| return; |
| } |
| |
| service->TriggerRefresh({syncer::SESSIONS}); |
| } |
| |
| void VisitedUrlRankingBackend::GetRankedSuggestions( |
| JNIEnv* env, |
| jlong current_time_ms, |
| jboolean fetch_local_tabs, |
| jboolean fetch_history, |
| const jni_zero::JavaParamRef<jobject>& suggestions, |
| const jni_zero::JavaParamRef<jobject>& callback) { |
| jni_zero::ScopedJavaGlobalRef<jobject> j_suggestions(env, suggestions); |
| jni_zero::ScopedJavaGlobalRef<jobject> j_callback(env, callback); |
| |
| auto current_time = |
| base::Time::FromMillisecondsSinceUnixEpoch(current_time_ms); |
| scoped_refptr<FetchAndRankFlow> flow = base::MakeRefCounted<FetchAndRankFlow>( |
| profile_, env, jobj_, current_time, fetch_local_tabs, fetch_history, |
| j_suggestions, j_callback); |
| |
| flow->RunFlow(); |
| } |
| |
| void VisitedUrlRankingBackend::RecordAction(JNIEnv* env, |
| jint scored_url_user_action, |
| jstring visit_id, |
| jlong visit_request_id) { |
| visited_url_ranking::VisitedURLRankingService* ranking_service = |
| visited_url_ranking::VisitedURLRankingServiceFactory::GetInstance() |
| ->GetForProfile(profile_); |
| if (!ranking_service) { |
| return; |
| } |
| ranking_service->RecordAction( |
| static_cast<ScoredURLUserAction>(scored_url_user_action), |
| base::android::ConvertJavaStringToUTF8(env, visit_id), |
| segmentation_platform::TrainingRequestId::FromUnsafeValue( |
| visit_request_id)); |
| } |
| |
| void VisitedUrlRankingBackend::OnRefresh() { |
| JNIEnv* env = jni_zero::AttachCurrentThread(); |
| Java_VisitedUrlRankingBackend_onRefresh(env, jobj_); |
| } |
| |
| } // namespace tab_resumption::jni |