Files
brave-core/browser/ui/speedreader/speedreader_tab_helper.cc
T
Sangwoo Ko 263959fa7f [Containers] Show PageActionView for partitioned storage tab in location bar (#34248)
* Show PageActionView for partitioned storage tab in location bar

When the current tab uses a Brave container (partitioned storage), show the
page action in the location bar with the container's icon, name, and
background color from ContainerModel.

- Register kPartitionedStorageActionIconType and enable it in the location
    bar only when ENABLE_CONTAINERS and the Containers feature are on.
- Add PartitionedStoragePageActionController that resolves the tab's
    container via GetContainerIdForWebContents and ContainerModel, and drives
    the generic PageActionView (Show/Hide, OverrideImage/Text, etc.); the
    action is hidden when the tab has no container.
2026-03-04 06:49:08 +09:00

643 lines
21 KiB
C++

/* Copyright (c) 2020 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/ui/speedreader/speedreader_tab_helper.h"
#include <initializer_list>
#include <string>
#include <string_view>
#include <utility>
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "brave/browser/brave_browser_process.h"
#include "brave/browser/speedreader/page_distiller.h"
#include "brave/browser/speedreader/speedreader_service_factory.h"
#include "brave/browser/ui/speedreader/speedreader_bubble_view.h"
#include "brave/components/speedreader/common/features.h"
#include "brave/components/speedreader/speedreader_extended_info_handler.h"
#include "brave/components/speedreader/speedreader_rewriter_service.h"
#include "brave/components/speedreader/speedreader_service.h"
#include "brave/components/speedreader/speedreader_util.h"
#include "brave/components/speedreader/tts_player.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "components/dom_distiller/content/browser/distillable_page_utils.h"
#include "components/grit/brave_components_resources.h"
#include "components/grit/brave_components_strings.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/reload_type.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#if !BUILDFLAG(IS_ANDROID)
#include "brave/browser/ui/brave_browser_window.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#else
#include "ui/android/view_android.h"
#include "ui/events/android/gesture_event_android.h"
#include "ui/events/android/gesture_event_type.h"
#endif
namespace speedreader {
std::u16string GetSpeedreaderData(
std::initializer_list<std::pair<std::string_view, int>> resources) {
base::DictValue sr_data;
sr_data.Set("ttsEnabled", features::kSpeedreaderTTS.Get());
for (const auto& r : resources) {
sr_data.Set(r.first, l10n_util::GetStringUTF16(r.second));
}
return base::StrCat(
{u"speedreaderData = ",
base::UTF8ToUTF16(base::WriteJson(sr_data).value_or("{}"))});
}
SpeedreaderTabHelper::SpeedreaderTabHelper(
content::WebContents* web_contents,
SpeedreaderService& speedreader_service,
SpeedreaderRewriterService* rewriter_service)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<SpeedreaderTabHelper>(*web_contents),
PageDistiller(web_contents),
speedreader_service_(speedreader_service),
rewriter_service_(rewriter_service) {
dom_distiller::AddObserver(web_contents, this);
speedreader_service_observation_.Observe(&speedreader_service_.get());
tts_player_observation_.Observe(speedreader::TtsPlayer::GetInstance());
is_visible_ = web_contents->GetVisibility() != content::Visibility::HIDDEN;
}
SpeedreaderTabHelper::~SpeedreaderTabHelper() {
DCHECK(!speedreader_bubble_);
DCHECK(!speedreader_service_observation_.IsObserving());
}
// static
void SpeedreaderTabHelper::MaybeCreateForWebContents(
content::WebContents* contents) {
if (!base::FeatureList::IsEnabled(features::kSpeedreaderFeature)) {
return;
}
// Check if speedreader is available (feature enabled and not disabled by
// policy)
auto* speedreader_service = SpeedreaderServiceFactory::GetForBrowserContext(
contents->GetBrowserContext());
if (!speedreader_service) {
return;
}
auto* rewriter_service =
g_brave_browser_process->speedreader_rewriter_service();
if (!rewriter_service) {
CHECK_IS_TEST();
return;
}
SpeedreaderTabHelper::CreateForWebContents(contents, *speedreader_service,
rewriter_service);
}
// static
void SpeedreaderTabHelper::BindSpeedreaderHost(
mojo::PendingAssociatedReceiver<mojom::SpeedreaderHost> receiver,
content::RenderFrameHost* rfh) {
auto* sender = content::WebContents::FromRenderFrameHost(rfh);
if (!sender) {
return;
}
auto* tab_helper = SpeedreaderTabHelper::FromWebContents(sender);
if (!tab_helper) {
return;
}
tab_helper->BindReceiver(std::move(receiver));
}
// static
PageDistiller* SpeedreaderTabHelper::GetPageDistiller(
content::WebContents* contents) {
auto* tab_helper = SpeedreaderTabHelper::FromWebContents(contents);
if (!tab_helper) {
return nullptr;
}
return tab_helper;
}
void SpeedreaderTabHelper::BindReceiver(
mojo::PendingAssociatedReceiver<mojom::SpeedreaderHost> receiver) {
receiver_.reset();
receiver_.Bind(std::move(receiver));
}
base::WeakPtr<SpeedreaderTabHelper> SpeedreaderTabHelper::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void SpeedreaderTabHelper::ProcessIconClick() {
if (DistillStates::IsViewOriginal(distill_state_)) {
const auto& vo = std::get<DistillStates::ViewOriginal>(distill_state_);
if (!vo.was_auto_distilled ||
!speedreader_service_->IsAllowedForSite(web_contents())) {
GetDistilledHTML(
base::BindOnce(&SpeedreaderTabHelper::OnGetDocumentSource,
weak_factory_.GetWeakPtr()));
} else {
TransitStateTo(DistillStates::Distilling(
DistillStates::Distilling::Reason::kAutomatic));
}
} else if (DistillStates::IsDistilled(distill_state_)) {
OnShowOriginalPage();
}
}
SpeedreaderBubbleView* SpeedreaderTabHelper::speedreader_bubble_view() const {
return speedreader_bubble_;
}
bool SpeedreaderTabHelper::MaybeUpdateCachedState(
content::NavigationHandle* handle) {
auto* entry = handle->GetNavigationEntry();
if (!entry || (handle->GetRestoreType() != content::RestoreType::kRestored &&
!handle->IsServedFromBackForwardCache())) {
return false;
}
const DistillState state = SpeedreaderExtendedInfoHandler::GetCachedMode(
entry, &speedreader_service_.get());
if (DistillStates::IsDistilled(state)) {
if (handle->IsServedFromBackForwardCache() ||
DistillStates::IsDistilledAutomatically(state)) {
distill_state_ = state;
return true;
}
}
SpeedreaderExtendedInfoHandler::ClearPersistedData(entry);
return false;
}
void SpeedreaderTabHelper::OnBubbleClosed() {
speedreader_bubble_ = nullptr;
UpdateUI();
for (auto& o : observers_) {
o.OnTuneBubbleClosed();
}
}
void SpeedreaderTabHelper::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void SpeedreaderTabHelper::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void SpeedreaderTabHelper::ShowSpeedreaderBubble(
SpeedreaderBubbleLocation location) {
#if !BUILDFLAG(IS_ANDROID)
if (auto* browser_window = BraveBrowserWindow::From(
BrowserWindow::FindBrowserWindowWithWebContents(web_contents()))) {
speedreader_bubble_ = browser_window->ShowSpeedreaderBubble(this, location);
}
#endif
}
void SpeedreaderTabHelper::HideSpeedreaderBubble() {
if (speedreader_bubble_) {
speedreader_bubble_->Hide();
speedreader_bubble_ = nullptr;
}
}
void SpeedreaderTabHelper::OnShowOriginalPage() {
if (!DistillStates::IsDistilled(distill_state_)) {
return;
}
TransitStateTo(DistillStates::ViewOriginal());
}
void SpeedreaderTabHelper::OnTtsPlayPause(int paragraph_index) {
auto& tts_controller =
speedreader::TtsPlayer::GetInstance()->GetControllerFor(web_contents());
if (tts_controller.IsPlaying() &&
tts_controller.IsPlayingRequestedWebContents(paragraph_index)) {
tts_controller.Pause();
} else {
tts_controller.Play(paragraph_index);
}
}
void SpeedreaderTabHelper::OnToolbarStateChanged(mojom::MainButtonType button) {
switch (button) {
case mojom::MainButtonType::None:
SetDocumentAttribute("data-toolbar-button", "");
break;
case mojom::MainButtonType::Tune:
SetDocumentAttribute("data-toolbar-button", "tune");
break;
case mojom::MainButtonType::Appearance:
SetDocumentAttribute("data-toolbar-button", "appearance");
break;
case mojom::MainButtonType::TextToSpeech:
SetDocumentAttribute("data-toolbar-button", "tts");
break;
case mojom::MainButtonType::AI:
SetDocumentAttribute("data-toolbar-button", "ai");
break;
}
}
void SpeedreaderTabHelper::ClearPersistedData() {
if (auto* entry = web_contents()->GetController().GetLastCommittedEntry()) {
SpeedreaderExtendedInfoHandler::ClearPersistedData(entry);
}
}
void SpeedreaderTabHelper::ReloadContents() {
web_contents()->GetController().Reload(content::ReloadType::NORMAL, false);
}
void SpeedreaderTabHelper::ProcessNavigation(
content::NavigationHandle* navigation_handle,
bool finish_navigation) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument() ||
MaybeUpdateCachedState(navigation_handle)) {
UpdateUI();
return;
}
if (finish_navigation) {
if (navigation_handle->IsErrorPage() ||
web_contents()->GetPrimaryMainFrame()->IsErrorDocument()) {
TransitStateTo(
DistillStates::ViewOriginal(
DistillStates::ViewOriginal::Reason::kNotDistillable, false),
true);
}
return;
}
if (DistillStates::IsDistilling(distill_state_)) {
// State will be determined in OnDistillComplete.
return;
}
if (DistillStates::IsDistillReverting(distill_state_)) {
TransitStateTo(DistillStates::ViewOriginal(), true);
return;
}
auto* nav_entry = navigation_handle->GetNavigationEntry();
const bool url_looks_readable =
nav_entry && nav_entry->GetVirtualURL().SchemeIsHTTPOrHTTPS() &&
rewriter_service_->URLLooksReadable(navigation_handle->GetURL());
const bool enabled_for_site =
speedreader_service_->IsAllowedForSite(navigation_handle->GetURL());
const auto reason =
url_looks_readable ? DistillStates::ViewOriginal::Reason::kNone
: DistillStates::ViewOriginal::Reason::kNotDistillable;
TransitStateTo(DistillStates::DistillReverting(reason, false), true);
TransitStateTo(DistillStates::ViewOriginal(), true);
if (enabled_for_site) {
// Check if url is pointed to the homepage, basically these pages aren't
// readable. We've got the same check in speedreader::IsURLLooksReadable
const bool homepage = !navigation_handle->GetURL().has_path() ||
navigation_handle->GetURL().path() == "/";
// Enable speedreader if the user explicitly enabled speedreader on the
// site.
const bool explicit_enabled_for_size =
!homepage && features::kSpeedreaderExplicitPref.Get() &&
speedreader_service_->IsEnabledForSite(navigation_handle->GetURL());
if (url_looks_readable || explicit_enabled_for_size) {
// Speedreader enabled for this page.
TransitStateTo(DistillStates::Distilling(
DistillStates::Distilling::Reason::kAutomatic),
true);
}
}
}
void SpeedreaderTabHelper::UpdateUI() {
if (DistillStates::IsDistilled(distill_state_)) {
UpdateState(State::kDistilled);
} else if (DistillStates::IsDistillable(distill_state_)) {
UpdateState(State::kDistillable);
} else {
UpdateState(State::kNotDistillable);
}
if (!is_visible_) {
return;
}
#if !BUILDFLAG(IS_ANDROID)
if (auto* browser_window = BraveBrowserWindow::From(
BrowserWindow::FindBrowserWindowWithWebContents(web_contents()))) {
browser_window->UpdateReaderModeToolbar();
browser_window->UpdatePageActionIcon(brave::kSpeedreaderPageActionIconType);
}
#endif
}
void SpeedreaderTabHelper::ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame()) {
return;
}
const bool is_distilled = DistillStates::IsDistilled(PageDistillState());
blink::web_pref::WebPreferences prefs =
web_contents()->GetOrCreateWebPreferences();
if (prefs.page_in_reader_mode != is_distilled) {
prefs.page_in_reader_mode = DistillStates::IsDistilled(PageDistillState());
web_contents()->SetWebPreferences(prefs);
}
}
void SpeedreaderTabHelper::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
ProcessNavigation(navigation_handle);
}
void SpeedreaderTabHelper::DidRedirectNavigation(
content::NavigationHandle* navigation_handle) {
ProcessNavigation(navigation_handle);
}
void SpeedreaderTabHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
ProcessNavigation(navigation_handle, true);
}
void SpeedreaderTabHelper::DidStopLoading() {
auto* entry = web_contents()->GetController().GetLastCommittedEntry();
if (entry) {
SpeedreaderExtendedInfoHandler::PersistMode(entry, distill_state_);
}
}
void SpeedreaderTabHelper::DOMContentLoaded(
content::RenderFrameHost* render_frame_host) {
if (!render_frame_host->IsInPrimaryMainFrame() ||
!DistillStates::IsDistilled(distill_state_)) {
return;
}
UpdateUI();
static base::NoDestructor<std::u16string> kSpeedreaderData(GetSpeedreaderData(
{{"showOriginalLinkText", IDS_READER_MODE_SHOW_ORIGINAL_PAGE_LINK},
{"minutesText", IDS_READER_MODE_MINUTES_TEXT},
#if defined(IDS_READER_MODE_TEXT_TO_SPEECH_PLAY_PAUSE)
{"playButtonTitle", IDS_READER_MODE_TEXT_TO_SPEECH_PLAY_PAUSE}
#endif
}));
static base::NoDestructor<std::u16string> kJsScript(base::UTF8ToUTF16(
ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_SPEEDREADER_JS_DESKTOP)));
static base::NoDestructor<std::u16string> kLoadScript(*kSpeedreaderData +
*kJsScript);
render_frame_host->ExecuteJavaScriptInIsolatedWorld(
*kLoadScript, base::DoNothing(), ISOLATED_WORLD_ID_BRAVE_INTERNAL);
for (auto& o : observers_) {
o.OnContentsReady();
}
}
void SpeedreaderTabHelper::OnVisibilityChanged(content::Visibility visibility) {
is_visible_ = visibility != content::Visibility::HIDDEN;
UpdateUI();
}
void SpeedreaderTabHelper::WebContentsDestroyed() {
speedreader_service_observation_.Reset();
tts_player_observation_.Reset();
dom_distiller::RemoveObserver(web_contents(), this);
SetWebContents(nullptr);
HideSpeedreaderBubble();
}
bool SpeedreaderTabHelper::IsPageDistillationAllowed() {
return DistillStates::IsDistilling(distill_state_) ||
DistillStates::IsDistilled(distill_state_);
}
bool SpeedreaderTabHelper::IsPageContentPresent() {
return !single_show_content_.empty();
}
std::string SpeedreaderTabHelper::TakePageContent() {
return std::move(single_show_content_);
}
void SpeedreaderTabHelper::OnDistillComplete(DistillationResult result) {
// Perform a state transition
Transit(distill_state_, DistillStates::Distilled(result));
if (result == DistillationResult::kSuccess) {
speedreader_service_->metrics().RecordPageView();
}
}
void SpeedreaderTabHelper::OnDistilledDocumentSent() {
UpdateUI();
#if BUILDFLAG(IS_ANDROID)
// Attempt to reset page scale after a successful distillation.
// This is done by mocking a pinch gesture on Android,
// see chrome/android/java/src/org/chromium/chrome/browser/ZoomController.java
// and ui/android/event_forwarder.cc
if (DistillStates::IsDistilled(distill_state_)) {
ui::ViewAndroid* view = web_contents()->GetNativeView();
int64_t time_ms = base::TimeTicks::Now().ToUptimeMillis();
SendGestureEvent(view, ui::GESTURE_EVENT_TYPE_PINCH_BEGIN, time_ms, 0.f);
SendGestureEvent(view, ui::GESTURE_EVENT_TYPE_PINCH_BY, time_ms, -1.f);
SendGestureEvent(view, ui::GESTURE_EVENT_TYPE_PINCH_END, time_ms, 0.f);
}
#endif
}
void SpeedreaderTabHelper::OnReadingStart(content::WebContents* web_contents) {
if (!speedreader::DistillStates::IsDistilled(distill_state_)) {
return;
}
static constexpr char16_t kReading[] =
uR"js( speedreaderUtils.setTtsReadingState($1) )js";
const auto script = base::ReplaceStringPlaceholders(
kReading, (web_contents == this->web_contents()) ? u"true" : u"false",
nullptr);
this->web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptInIsolatedWorld(
script, base::DoNothing(), ISOLATED_WORLD_ID_BRAVE_INTERNAL);
}
void SpeedreaderTabHelper::OnReadingStop(content::WebContents* web_contents) {
OnReadingStart(nullptr);
}
void SpeedreaderTabHelper::OnReadingProgress(content::WebContents* web_contents,
int paragraph_index,
int char_index,
int length) {
if (!speedreader::DistillStates::IsDistilled(distill_state_) ||
web_contents != this->web_contents()) {
return;
}
static constexpr char16_t kHighlight[] =
uR"js( speedreaderUtils.highlightText($1, $2, $3) )js";
const auto script = base::ReplaceStringPlaceholders(
kHighlight,
{base::NumberToString16(paragraph_index),
base::NumberToString16(char_index), base::NumberToString16(length)},
nullptr);
this->web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptInIsolatedWorld(
script, base::DoNothing(), ISOLATED_WORLD_ID_BRAVE_INTERNAL);
}
void SpeedreaderTabHelper::OnSiteEnableSettingChanged(
content::WebContents* site,
bool enabled_on_site) {
if (site != web_contents()) {
return;
}
if (enabled_on_site) {
TransitStateTo(DistillStates::Distilling(
DistillStates::Distilled::Reason::kAutomatic));
} else {
TransitStateTo(DistillStates::ViewOriginal());
}
HideSpeedreaderBubble();
}
void SpeedreaderTabHelper::OnAllSitesEnableSettingChanged(
bool enabled_on_all_sites) {
if (!is_visible_) {
return;
}
OnSiteEnableSettingChanged(
web_contents(), speedreader_service_->IsAllowedForSite(web_contents()));
}
void SpeedreaderTabHelper::OnAppearanceSettingsChanged(
const mojom::AppearanceSettings& view_settings) {
if (!speedreader::DistillStates::IsDistilled(distill_state_)) {
return;
}
SetDocumentAttribute("data-theme", speedreader_service_->GetThemeName());
SetDocumentAttribute("data-font-family",
speedreader_service_->GetFontFamilyName());
SetDocumentAttribute("data-font-size",
speedreader_service_->GetFontSizeName());
SetDocumentAttribute("data-column-width",
speedreader_service_->GetColumnWidth());
}
void SpeedreaderTabHelper::OnResult(
const dom_distiller::DistillabilityResult& result) {
if (DistillStates::IsNotDistillable(distill_state_) &&
result.is_distillable) {
TransitStateTo(DistillStates::DistillReverting(
DistillStates::DistillReverting::Reason::kNone, false));
TransitStateTo(DistillStates::ViewOriginal());
}
}
void SpeedreaderTabHelper::SetDocumentAttribute(const std::string& attribute,
const std::string& value) {
static constexpr char16_t kSetAttribute[] =
uR"js(
(function() {
const attribute = '$1'
const value = '$2'
if (value == '') {
document?.documentElement?.removeAttribute(attribute)
} else {
document?.documentElement?.setAttribute(attribute, value)
}
})();
)js";
const auto script = base::ReplaceStringPlaceholders(
kSetAttribute, {base::UTF8ToUTF16(attribute), base::UTF8ToUTF16(value)},
nullptr);
web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptInIsolatedWorld(
script, base::DoNothing(), ISOLATED_WORLD_ID_BRAVE_INTERNAL);
}
void SpeedreaderTabHelper::OnGetDocumentSource(bool success, std::string html) {
if (!success || html.empty()) {
TransitStateTo(DistillStates::DistillReverting(
DistillStates::DistillReverting::Reason::kError, false));
TransitStateTo(DistillStates::ViewOriginal());
return;
}
single_show_content_ = std::move(html);
TransitStateTo(
DistillStates::Distilling(DistillStates::Distilling::Reason::kManual));
}
void SpeedreaderTabHelper::TransitStateTo(const DistillState& desired_state,
bool no_reload) {
if (Transit(distill_state_, desired_state) && !no_reload) {
ClearPersistedData();
ReloadContents();
}
UpdateUI();
}
#if BUILDFLAG(IS_ANDROID)
bool SpeedreaderTabHelper::SendGestureEvent(ui::ViewAndroid* view,
int type,
int64_t time_ms,
float scale) {
float dip_scale = view->GetDipScale();
auto size = view->GetSizeDIPs();
float x = size.width() / 2;
float y = size.height() / 2;
gfx::PointF root_location =
ScalePoint(view->GetLocationOnScreen(x, y), 1.f / dip_scale);
return view->OnGestureEvent(ui::GestureEventAndroid(
type, gfx::PointF(x / dip_scale, y / dip_scale), root_location, time_ms,
/*source*/ ui::GestureDeviceType::DEVICE_TOUCHSCREEN, scale, 0, 0, 0, 0,
/*target_viewport*/ false, /*synthetic_scroll*/ false,
/*prevent_boosting*/ false));
}
#endif
WEB_CONTENTS_USER_DATA_KEY_IMPL(SpeedreaderTabHelper);
} // namespace speedreader