| // Copyright 2013 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/android/ntp/most_visited_sites_bridge.h" |
| |
| #include <map> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #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/strings/utf_string_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/flags/android/chrome_feature_list.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/ntp_tiles/chrome_most_visited_sites_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/favicon_base/favicon_types.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/ntp_tiles/metrics.h" |
| #include "components/ntp_tiles/most_visited_sites.h" |
| #include "components/ntp_tiles/section_type.h" |
| #include "components/ntp_tiles/tile_source.h" |
| #include "ui/gfx/android/java_bitmap.h" |
| #include "url/android/gurl_android.h" |
| |
| // Must come after all headers that specialize FromJniType() / ToJniType(). |
| #include "chrome/android/chrome_jni_headers/MostVisitedSitesBridge_jni.h" |
| #include "chrome/android/chrome_jni_headers/MostVisitedSites_jni.h" |
| |
| using base::android::AttachCurrentThread; |
| using base::android::ConvertJavaStringToUTF8; |
| using base::android::JavaParamRef; |
| using base::android::ScopedJavaGlobalRef; |
| using base::android::ScopedJavaLocalRef; |
| using ntp_tiles::MostVisitedSites; |
| using ntp_tiles::NTPTilesVector; |
| using ntp_tiles::SectionType; |
| using ntp_tiles::TileSource; |
| using ntp_tiles::TileTitleSource; |
| using ntp_tiles::TileVisualType; |
| |
| namespace { |
| |
| class JavaHomepageClient : public MostVisitedSites::HomepageClient { |
| public: |
| JavaHomepageClient(JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| Profile* profile); |
| |
| JavaHomepageClient(const JavaHomepageClient&) = delete; |
| JavaHomepageClient& operator=(const JavaHomepageClient&) = delete; |
| |
| bool IsHomepageTileEnabled() const override; |
| GURL GetHomepageUrl() const override; |
| void QueryHomepageTitle(TitleCallback title_callback) override; |
| |
| private: |
| void OnTitleEntryFound(TitleCallback title_callback, |
| history::QueryURLResult result); |
| |
| ScopedJavaGlobalRef<jobject> client_; |
| raw_ptr<Profile> profile_; |
| |
| // Used in loading titles. |
| base::CancelableTaskTracker task_tracker_; |
| }; |
| |
| JavaHomepageClient::JavaHomepageClient(JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| Profile* profile) |
| : client_(env, obj), profile_(profile) { |
| DCHECK(profile); |
| } |
| |
| void JavaHomepageClient::QueryHomepageTitle(TitleCallback title_callback) { |
| DCHECK(!title_callback.is_null()); |
| GURL url = GetHomepageUrl(); |
| if (url.is_empty()) { |
| std::move(title_callback).Run(std::nullopt); |
| return; |
| } |
| history::HistoryService* const history_service = |
| HistoryServiceFactory::GetForProfileIfExists( |
| profile_, ServiceAccessType::EXPLICIT_ACCESS); |
| if (!history_service) { |
| std::move(title_callback).Run(std::nullopt); |
| return; |
| } |
| // If the client is destroyed, the tracker will cancel this task automatically |
| // and the callback will not be called. Therefore, base::Unretained works. |
| history_service->QueryURL( |
| url, |
| base::BindOnce(&JavaHomepageClient::OnTitleEntryFound, |
| base::Unretained(this), std::move(title_callback)), |
| &task_tracker_); |
| } |
| |
| void JavaHomepageClient::OnTitleEntryFound(TitleCallback title_callback, |
| history::QueryURLResult result) { |
| if (!result.success) { |
| std::move(title_callback).Run(std::nullopt); |
| return; |
| } |
| std::move(title_callback).Run(result.row.title()); |
| } |
| |
| bool JavaHomepageClient::IsHomepageTileEnabled() const { |
| return Java_HomepageClient_isHomepageTileEnabled(AttachCurrentThread(), |
| client_); |
| } |
| |
| GURL JavaHomepageClient::GetHomepageUrl() const { |
| base::android::ScopedJavaLocalRef<jstring> url = |
| Java_HomepageClient_getHomepageUrl(AttachCurrentThread(), client_); |
| if (url.is_null()) { |
| return GURL(); |
| } |
| return GURL(ConvertJavaStringToUTF8(url)); |
| } |
| |
| } // namespace |
| |
| class MostVisitedSitesBridge::JavaObserver : public MostVisitedSites::Observer { |
| public: |
| JavaObserver(JNIEnv* env, const JavaParamRef<jobject>& obj); |
| |
| JavaObserver(const JavaObserver&) = delete; |
| JavaObserver& operator=(const JavaObserver&) = delete; |
| |
| void OnURLsAvailable( |
| bool is_user_triggered, |
| const std::map<SectionType, NTPTilesVector>& sections) override; |
| |
| void OnIconMadeAvailable(const GURL& site_url) override; |
| |
| private: |
| ScopedJavaGlobalRef<jobject> observer_; |
| }; |
| |
| MostVisitedSitesBridge::JavaObserver::JavaObserver( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj) |
| : observer_(env, obj) {} |
| |
| void MostVisitedSitesBridge::JavaObserver::OnURLsAvailable( |
| bool is_user_triggered, |
| const std::map<SectionType, NTPTilesVector>& sections) { |
| JNIEnv* env = AttachCurrentThread(); |
| std::vector<jni_zero::ScopedJavaLocalRef<jobject>> suggestions; |
| for (const auto& section : sections) { |
| int32_t section_type = static_cast<int32_t>(section.first); |
| const NTPTilesVector& tiles = section.second; |
| for (const auto& tile : tiles) { |
| suggestions.push_back(Java_MostVisitedSitesBridge_makeSiteSuggestion( |
| env, tile.title, tile.url, static_cast<int32_t>(tile.title_source), |
| static_cast<int32_t>(tile.source), section_type)); |
| } |
| } |
| Java_MostVisitedSitesBridge_onURLsAvailable(env, observer_, is_user_triggered, |
| suggestions); |
| } |
| |
| void MostVisitedSitesBridge::JavaObserver::OnIconMadeAvailable( |
| const GURL& site_url) { |
| JNIEnv* env = AttachCurrentThread(); |
| Java_MostVisitedSitesBridge_onIconMadeAvailable(env, observer_, site_url); |
| } |
| |
| MostVisitedSitesBridge::MostVisitedSitesBridge(Profile* profile, |
| bool enable_custom_links) |
| : most_visited_(ChromeMostVisitedSitesFactory::NewForProfile(profile)), |
| profile_(profile) { |
| DCHECK(!profile->IsOffTheRecord()); |
| most_visited_->EnableTileTypes( |
| ntp_tiles::MostVisitedSites::EnableTileTypesOptions().with_custom_links( |
| enable_custom_links)); |
| } |
| |
| MostVisitedSitesBridge::~MostVisitedSitesBridge() = default; |
| |
| void MostVisitedSitesBridge::Destroy(JNIEnv* env) { |
| delete this; |
| } |
| |
| void MostVisitedSitesBridge::OnHomepageStateChanged(JNIEnv* env) { |
| most_visited_->RefreshTiles(); |
| } |
| |
| void MostVisitedSitesBridge::SetHomepageClient( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& j_client) { |
| most_visited_->SetHomepageClient( |
| std::make_unique<JavaHomepageClient>(env, j_client, profile_)); |
| } |
| |
| void MostVisitedSitesBridge::SetObserver( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_observer, |
| jint num_sites) { |
| java_observer_ = std::make_unique<JavaObserver>(env, j_observer); |
| most_visited_->AddMostVisitedURLsObserver(java_observer_.get(), num_sites); |
| } |
| |
| jboolean MostVisitedSitesBridge::AddCustomLinkTo(JNIEnv* env, |
| const std::u16string& name, |
| const GURL& url, |
| jint pos) { |
| return most_visited_->AddCustomLinkTo(url, name, pos); |
| } |
| |
| jboolean MostVisitedSitesBridge::AddCustomLink(JNIEnv* env, |
| const std::u16string& name, |
| const GURL& url) { |
| return most_visited_->AddCustomLink(url, name); |
| } |
| |
| jboolean MostVisitedSitesBridge::AssignCustomLink(JNIEnv* env, |
| const GURL& key_url, |
| const std::u16string& name, |
| const GURL& url) { |
| if (most_visited_->HasCustomLink(key_url)) { |
| // Update existing Custom link. In rare cases (e.g., split-window editing |
| // and/or race conditions) HasCustomLink() can output stale results. But |
| // this is fine, since update / add functions in the backend would still |
| // serialize and make reasonable changes or return error. |
| // If the URL does not change, need to pass empty URL instead. |
| const GURL& url_to_use = (key_url == url) ? GURL() : url; |
| return most_visited_->UpdateCustomLink(key_url, url_to_use, name); |
| } |
| return most_visited_->AddCustomLink(url, name); |
| } |
| |
| jboolean MostVisitedSitesBridge::DeleteCustomLink(JNIEnv* env, |
| const GURL& key_url) { |
| return most_visited_->DeleteCustomLink(key_url); |
| } |
| |
| jboolean MostVisitedSitesBridge::HasCustomLink(JNIEnv* env, |
| const GURL& key_url) { |
| return most_visited_->HasCustomLink(key_url); |
| } |
| |
| jboolean MostVisitedSitesBridge::ReorderCustomLink(JNIEnv* env, |
| const GURL& key_url, |
| jint new_pos) { |
| return most_visited_->ReorderCustomLink(key_url, new_pos); |
| } |
| |
| void MostVisitedSitesBridge::AddOrRemoveBlockedUrl( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_url, |
| jboolean add_url) { |
| GURL url = url::GURLAndroid::ToNativeGURL(env, j_url); |
| most_visited_->AddOrRemoveBlockedUrl(url, add_url); |
| } |
| |
| void MostVisitedSitesBridge::RecordPageImpression( |
| JNIEnv* env, |
| jint jtiles_count) { |
| ntp_tiles::metrics::RecordPageImpression(jtiles_count); |
| } |
| |
| void MostVisitedSitesBridge::RecordTileImpression( |
| JNIEnv* env, |
| jint jindex, |
| jint jvisual_type, |
| jint jicon_type, |
| jint jtitle_source, |
| jint jsource, |
| const JavaParamRef<jobject>& jurl) { |
| GURL url = url::GURLAndroid::ToNativeGURL(env, jurl); |
| TileTitleSource title_source = static_cast<TileTitleSource>(jtitle_source); |
| TileSource source = static_cast<TileSource>(jsource); |
| TileVisualType visual_type = static_cast<TileVisualType>(jvisual_type); |
| favicon_base::IconType icon_type = |
| static_cast<favicon_base::IconType>(jicon_type); |
| |
| ntp_tiles::metrics::RecordTileImpression(ntp_tiles::NTPTileImpression( |
| jindex, source, title_source, visual_type, icon_type, url)); |
| } |
| |
| void MostVisitedSitesBridge::RecordOpenedMostVisitedItem( |
| JNIEnv* env, |
| jint index, |
| jint tile_type, |
| jint title_source, |
| jint source) { |
| ntp_tiles::metrics::RecordTileClick(ntp_tiles::NTPTileImpression( |
| index, static_cast<TileSource>(source), |
| static_cast<TileTitleSource>(title_source), |
| static_cast<TileVisualType>(tile_type), favicon_base::IconType::kInvalid, |
| /*url_for_rappor=*/GURL())); |
| } |
| |
| jdouble MostVisitedSitesBridge::GetSuggestionScore(JNIEnv* env, |
| const GURL& url) { |
| return most_visited_->GetSuggestionScore(url); |
| } |
| |
| static jlong JNI_MostVisitedSitesBridge_Init(JNIEnv* env, |
| Profile* profile, |
| jboolean enable_custom_links) { |
| MostVisitedSitesBridge* most_visited_sites = |
| new MostVisitedSitesBridge(profile, enable_custom_links); |
| return reinterpret_cast<intptr_t>(most_visited_sites); |
| } |