diff --git a/browser/brave_local_state_prefs.cc b/browser/brave_local_state_prefs.cc index 7470f6bc5838..9b1a57a6cdc7 100644 --- a/browser/brave_local_state_prefs.cc +++ b/browser/brave_local_state_prefs.cc @@ -56,6 +56,10 @@ #include "chrome/browser/first_run/first_run.h" #endif // !BUILDFLAG(IS_ANDROID) +#if defined(TOOLKIT_VIEWS) +#include "brave/browser/onboarding/onboarding_tab_helper.h" +#endif + #if BUILDFLAG(ENABLE_BRAVE_VPN) #include "brave/components/brave_vpn/common/brave_vpn_utils.h" #endif @@ -124,6 +128,10 @@ void RegisterLocalStatePrefs(PrefRegistrySimple* registry) { whats_new::RegisterLocalStatePrefs(registry); #endif +#if defined(TOOLKIT_VIEWS) + onboarding::RegisterLocalStatePrefs(registry); +#endif + #if BUILDFLAG(ENABLE_CRASH_DIALOG) registry->RegisterBooleanPref(kDontAskForCrashReporting, false); #endif diff --git a/browser/brave_tab_helpers.cc b/browser/brave_tab_helpers.cc index d1331ff026ab..d1e715d329b0 100644 --- a/browser/brave_tab_helpers.cc +++ b/browser/brave_tab_helpers.cc @@ -92,6 +92,10 @@ #include "brave/browser/playlist/playlist_tab_helper.h" #endif +#if defined(TOOLKIT_VIEWS) +#include "brave/browser/onboarding/onboarding_tab_helper.h" +#endif + namespace brave { #if defined(TOOLKIT_VIEWS) @@ -171,6 +175,10 @@ void AttachTabHelpers(content::WebContents* web_contents) { BraveNewsTabHelper::MaybeCreateForWebContents(web_contents); +#if defined(TOOLKIT_VIEWS) + OnboardingTabHelper::MaybeCreateForWebContents(web_contents); +#endif + if (base::FeatureList::IsEnabled(net::features::kBraveEphemeralStorage)) { ephemeral_storage::EphemeralStorageTabHelper::CreateForWebContents( web_contents); diff --git a/browser/onboarding/BUILD.gn b/browser/onboarding/BUILD.gn new file mode 100644 index 000000000000..c1b48aa71431 --- /dev/null +++ b/browser/onboarding/BUILD.gn @@ -0,0 +1,19 @@ +# Copyright (c) 2023 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/. + +source_set("unit_tests") { + testonly = true + + sources = [ "onboarding_unittest.cc" ] + + deps = [ + "//base", + "//brave/browser", + "//chrome/browser", + "//chrome/test:test_support", + "//content/test:test_support", + "//testing/gtest", + ] +} diff --git a/browser/onboarding/domain_map.cc b/browser/onboarding/domain_map.cc new file mode 100644 index 000000000000..1e0bb964b5c5 --- /dev/null +++ b/browser/onboarding/domain_map.cc @@ -0,0 +1,180 @@ +/* Copyright (c) 2023 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/onboarding/domain_map.h" + +#include + +#include "base/containers/fixed_flat_map.h" +#include "base/containers/flat_set.h" +#include "base/strings/string_util.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "url/gurl.h" + +namespace onboarding { + +namespace { + +constexpr auto kDomains = + base::MakeFixedFlatMap({ + {"2mdn.net", "Google"}, + {"admeld.com", "Google"}, + {"admob.com", "Google"}, + {"apture.com", "Google"}, + {"blogger.com", "Google"}, + {"cc-dt.com", "Google"}, + {"crashlytics.com", "Google"}, + {"destinationurl.com", "Google"}, + {"doubleclick.net", "Google"}, + {"ggpht.com", "Google"}, + {"gmail.com", "Google"}, + {"gmodules.com", "Google"}, + {"google-analytics.com", "Google"}, + {"google.ac", "Google"}, + {"google.ad", "Google"}, + {"google.ae", "Google"}, + {"google.al", "Google"}, + {"google.am", "Google"}, + {"google.as", "Google"}, + {"google.at", "Google"}, + {"google.az", "Google"}, + {"google.ba", "Google"}, + {"google.be", "Google"}, + {"google.bf", "Google"}, + {"google.bg", "Google"}, + {"google.bi", "Google"}, + {"google.bj", "Google"}, + {"google.bs", "Google"}, + {"google.bt", "Google"}, + {"google.by", "Google"}, + {"google.ca", "Google"}, + {"google.cat", "Google"}, + {"google.cc", "Google"}, + {"google.cd", "Google"}, + {"google.cf", "Google"}, + {"google.cg", "Google"}, + {"google.ch", "Google"}, + {"google.ci", "Google"}, + {"google.cl", "Google"}, + {"google.cm", "Google"}, + {"google.cn", "Google"}, + {"google.co.ao", "Google"}, + {"google.co.bw", "Google"}, + {"google.co.ck", "Google"}, + {"google.co.cr", "Google"}, + {"google.co.id", "Google"}, + {"google.co.il", "Google"}, + {"google.co.in", "Google"}, + {"google.co.jp", "Google"}, + {"google.co.ke", "Google"}, + {"google.co.kr", "Google"}, + {"google.co.ls", "Google"}, + {"google.co.ma", "Google"}, + {"google.co.mz", "Google"}, + {"google.co.nz", "Google"}, + {"google.co.th", "Google"}, + {"google.co.tz", "Google"}, + {"google.co.ug", "Google"}, + {"google.co.uk", "Google"}, + {"google.co.uz", "Google"}, + {"google.co.ve", "Google"}, + {"google.co.vi", "Google"}, + {"google.co.za", "Google"}, + {"google.co.zm", "Google"}, + {"google.co.zw", "Google"}, + {"google.com", "Google"}, + {"google.com.af", "Google"}, + {"google.com.ag", "Google"}, + {"google.com.ai", "Google"}, + {"google.com.ar", "Google"}, + {"google.com.au", "Google"}, + {"google.com.bd", "Google"}, + {"google.com.bh", "Google"}, + {"google.com.bn", "Google"}, + {"google.com.bo", "Google"}, + {"google.com.br", "Google"}, + {"google.com.bz", "Google"}, + {"google.com.co", "Google"}, + {"google.com.cu", "Google"}, + {"google.com.cy", "Google"}, + {"google.com.do", "Google"}, + {"google.com.ec", "Google"}, + {"google.com.eg", "Google"}, + {"google.com.et", "Google"}, + {"google.com.fj", "Google"}, + {"google.com.gh", "Google"}, + {"google.com.gi", "Google"}, + {"google.com.gt", "Google"}, + {"googletagservices.com", "Google"}, + {"youtube.com", "Google"}, + // Amazon + {"alexa.com", "Amazon"}, + {"alexametrics.com", "Amazon"}, + {"amazon-adsystem.com", "Amazon"}, + {"amazon.ca", "Amazon"}, + {"amazon.co.jp", "Amazon"}, + {"amazon.co.uk", "Amazon"}, + {"amazon.com", "Amazon"}, + {"amazon.de", "Amazon"}, + {"amazon.es", "Amazon"}, + {"amazon.fr", "Amazon"}, + {"amazon.it", "Amazon"}, + {"amazonaws.com", "Amazon"}, + {"assoc-amazon.com", "Amazon"}, + {"cloudfront.net", "Amazon"}, + {"ssl-images-amazon.com", "Amazon"}, + // Facebook + {"apps.fbsbx.com", "Facebook"}, + {"atdmt.com", "Facebook"}, + {"atlassolutions.com", "Facebook"}, + {"facebook.com", "Facebook"}, + {"facebook.de", "Facebook"}, + {"facebook.fr", "Facebook"}, + {"facebook.net", "Facebook"}, + {"fb.com", "Facebook"}, + {"fb.me", "Facebook"}, + {"fbcdn.net", "Facebook"}, + {"fbsbx.com", "Facebook"}, + {"friendfeed.com", "Facebook"}, + {"instagram.com", "Facebook"}, + {"messenger.com", "Facebook"}, + }); + +std::string GetCompanyNameFromGURL(const GURL& url) { + const auto host = net::registry_controlled_domains::GetDomainAndRegistry( + url, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); + + return kDomains.contains(host) ? std::string(kDomains.at(host)) + : std::string(); +} + +} // namespace + +std::pair GetCompanyNamesAndCountFromAdsList( + const std::vector& ads_list) { + std::string company_names; + base::flat_set company_names_set; + int total_count = 0; + + for (const GURL& url : ads_list) { + std::string company_name = GetCompanyNameFromGURL(url); + if (company_name.empty()) { + continue; + } + company_names_set.insert(company_name); + total_count++; + } + + if (!company_names_set.empty()) { + company_names = + base::JoinString(std::vector(company_names_set.begin(), + company_names_set.end()), + ", "); + } + + return {company_names, total_count}; +} + +} // namespace onboarding diff --git a/browser/onboarding/domain_map.h b/browser/onboarding/domain_map.h new file mode 100644 index 000000000000..399d2eee13ce --- /dev/null +++ b/browser/onboarding/domain_map.h @@ -0,0 +1,22 @@ +/* Copyright (c) 2023 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/. */ + +#ifndef BRAVE_BROWSER_ONBOARDING_DOMAIN_MAP_H_ +#define BRAVE_BROWSER_ONBOARDING_DOMAIN_MAP_H_ + +#include +#include +#include + +class GURL; + +namespace onboarding { + +std::pair GetCompanyNamesAndCountFromAdsList( + const std::vector& ads_list); + +} // namespace onboarding + +#endif // BRAVE_BROWSER_ONBOARDING_DOMAIN_MAP_H_ diff --git a/browser/onboarding/onboarding_tab_helper.cc b/browser/onboarding/onboarding_tab_helper.cc new file mode 100644 index 000000000000..e1d8c3437a3c --- /dev/null +++ b/browser/onboarding/onboarding_tab_helper.cc @@ -0,0 +1,206 @@ +/* Copyright (c) 2023 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/onboarding/onboarding_tab_helper.h" + +#include +#include + +#include "base/functional/bind.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/thread_pool.h" +#include "brave/browser/onboarding/domain_map.h" +#include "brave/browser/onboarding/pref_names.h" +#include "brave/browser/ui/brave_browser_window.h" +#include "brave/browser/ui/brave_shields_data_controller.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/first_run/first_run.h" +#include "chrome/browser/ui/browser_finder.h" +#include "components/grit/brave_components_strings.h" +#include "components/permissions/permission_request_manager.h" +#include "components/prefs/pref_registry_simple.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents.h" +#include "ui/base/l10n/l10n_util.h" + +namespace onboarding { +void RegisterLocalStatePrefs(PrefRegistrySimple* registry) { + registry->RegisterTimePref(prefs::kLastShieldsIconHighlightTime, + base::Time()); +} +} // namespace onboarding + +// static +bool OnboardingTabHelper::IsSevenDaysPassedSinceFirstRun() { + base::Time time_first_run = s_sentinel_time_for_testing_.value_or( + first_run::GetFirstRunSentinelCreationTime()); + base::Time time_now = s_time_now_for_testing_.value_or(base::Time::Now()); + + base::Time time_7_days_ago = time_now - base::Days(7); + return time_first_run < time_7_days_ago; +} + +// static +std::optional OnboardingTabHelper::s_time_now_for_testing_; +std::optional OnboardingTabHelper::s_sentinel_time_for_testing_; + +// static +void OnboardingTabHelper::MaybeCreateForWebContents( + content::WebContents* web_contents) { + base::Time last_shields_icon_highlight_time = + g_browser_process->local_state()->GetTime( + onboarding::prefs::kLastShieldsIconHighlightTime); + + // Shields highlight is aleady shown. We use it only once. + if (!last_shields_icon_highlight_time.is_null()) { + return; + } + + if (first_run::IsChromeFirstRun()) { + OnboardingTabHelper::CreateForWebContents(web_contents); + return; + } + + // Don't show highlight if already 7 days passed for non first run. + // OnboardingTabHelper::IsSevenDaysPassedSinceFirstRun() could have IO. + base::ThreadPool::PostTaskAndReplyWithResult( + FROM_HERE, {base::MayBlock()}, + base::BindOnce(&OnboardingTabHelper::IsSevenDaysPassedSinceFirstRun), + base::BindOnce( + [](base::WeakPtr contents, bool passed) { + if (!passed && contents) { + OnboardingTabHelper::CreateForWebContents(contents.get()); + } + }, + web_contents->GetWeakPtr())); +} + +OnboardingTabHelper::OnboardingTabHelper(content::WebContents* web_contents) + : content::WebContentsObserver(web_contents), + content::WebContentsUserData(*web_contents) {} + +OnboardingTabHelper::~OnboardingTabHelper() = default; + +void OnboardingTabHelper::DidStopLoading() { + Browser* browser = chrome::FindBrowserWithTab(web_contents()); + DCHECK(browser); + + if (!browser) { + return; + } + + auto* active_tab_web_contents = + browser->tab_strip_model()->GetActiveWebContents(); + + if (web_contents() != active_tab_web_contents) { + return; + } + + // Show highlight when there is no permission request after loaded. + // If permission requests are existed, don't try to show highlight in this + // loading. + auto* permission_request_manager = + permissions::PermissionRequestManager::FromWebContents(web_contents()); + + // Can be null in unit test. + if (!permission_request_manager) { + return; + } + + if (permission_request_manager->has_pending_requests() || + permission_request_manager->IsRequestInProgress()) { + return; + } + + PerformBraveShieldsChecksAndShowHelpBubble(); +} + +void OnboardingTabHelper::PerformBraveShieldsChecksAndShowHelpBubble() { + auto* shields_data_controller = + brave_shields::BraveShieldsDataController::FromWebContents( + web_contents()); + DCHECK(shields_data_controller); + + if (shields_data_controller->GetBraveShieldsEnabled() && + shields_data_controller->GetTotalBlockedCount() > 0 && + CanHighlightBraveShields()) { + ShowBraveHelpBubbleView(); + } +} + +bool OnboardingTabHelper::CanHighlightBraveShields() { + base::Time last_shields_icon_highlight_time = + g_browser_process->local_state()->GetTime( + onboarding::prefs::kLastShieldsIconHighlightTime); + + // If highlight is shown from other tabs after this tab is created, + // this condition could be met. + if (!last_shields_icon_highlight_time.is_null()) { + return false; + } + + // Show highlight for first run always if it's not shown so far. + if (first_run::IsChromeFirstRun()) { + return true; + } + + // We only show highlight to users that has installed the browser in the + // previous 7 days. + return !IsSevenDaysPassedSinceFirstRun(); +} + +void OnboardingTabHelper::ShowBraveHelpBubbleView() { + Browser* browser = chrome::FindBrowserWithTab(web_contents()); + DCHECK(browser); + if (!browser) { + return; + } + + if (!BraveBrowserWindow::From(browser->window()) + ->ShowBraveHelpBubbleView(GetTextForOnboardingShieldsBubble())) { + return; + } + + g_browser_process->local_state()->SetTime( + onboarding::prefs::kLastShieldsIconHighlightTime, base::Time::Now()); + + CleanUp(); +} + +std::string OnboardingTabHelper::GetTextForOnboardingShieldsBubble() { + auto* shields_data_controller = + brave_shields::BraveShieldsDataController::FromWebContents( + web_contents()); + + if (!shields_data_controller) { + return std::string(); + } + + auto [company_names, total_companies_blocked] = + onboarding::GetCompanyNamesAndCountFromAdsList( + shields_data_controller->GetBlockedAdsList()); + + const int others_blocked = + shields_data_controller->GetTotalBlockedCount() - total_companies_blocked; + + std::vector replacements; + std::string label_text = l10n_util::GetPluralStringFUTF8( + company_names.empty() + ? IDS_BRAVE_SHIELDS_ONBOARDING_LABEL_WITHOUT_COMPANIES + : IDS_BRAVE_SHIELDS_ONBOARDING_LABEL_WITH_COMPANIES, + others_blocked); + if (!company_names.empty()) { + replacements.push_back(company_names); + } + replacements.push_back(shields_data_controller->GetCurrentSiteURL().host()); + + return base::ReplaceStringPlaceholders(label_text, replacements, nullptr); +} + +void OnboardingTabHelper::CleanUp() { + Observe(nullptr); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(OnboardingTabHelper); diff --git a/browser/onboarding/onboarding_tab_helper.h b/browser/onboarding/onboarding_tab_helper.h new file mode 100644 index 000000000000..bee1bfff442a --- /dev/null +++ b/browser/onboarding/onboarding_tab_helper.h @@ -0,0 +1,55 @@ +/* Copyright (c) 2023 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/. */ + +#ifndef BRAVE_BROWSER_ONBOARDING_ONBOARDING_TAB_HELPER_H_ +#define BRAVE_BROWSER_ONBOARDING_ONBOARDING_TAB_HELPER_H_ + +#include +#include + +#include "base/time/time.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +class PrefRegistrySimple; + +namespace onboarding { +void RegisterLocalStatePrefs(PrefRegistrySimple* registry); +} // namespace onboarding + +class OnboardingTabHelper + : public content::WebContentsObserver, + public content::WebContentsUserData { + public: + static void MaybeCreateForWebContents(content::WebContents* contents); + OnboardingTabHelper(const OnboardingTabHelper&) = delete; + OnboardingTabHelper& operator=(const OnboardingTabHelper&) = delete; + ~OnboardingTabHelper() override; + + private: + friend class content::WebContentsUserData; + FRIEND_TEST_ALL_PREFIXES(OnboardingTest, HelperCreationTestForFirstRun); + FRIEND_TEST_ALL_PREFIXES(OnboardingTest, HelperCreationTestForNonFirstRun); + + explicit OnboardingTabHelper(content::WebContents* web_contents); + + // content::WebContentsObserver + void DidStopLoading() override; + + void PerformBraveShieldsChecksAndShowHelpBubble(); + bool CanHighlightBraveShields(); + void ShowBraveHelpBubbleView(); + std::string GetTextForOnboardingShieldsBubble(); + void CleanUp(); + + static bool IsSevenDaysPassedSinceFirstRun(); + + static std::optional s_time_now_for_testing_; + static std::optional s_sentinel_time_for_testing_; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +#endif // BRAVE_BROWSER_ONBOARDING_ONBOARDING_TAB_HELPER_H_ diff --git a/browser/onboarding/onboarding_unittest.cc b/browser/onboarding/onboarding_unittest.cc new file mode 100644 index 000000000000..13e9449fc32f --- /dev/null +++ b/browser/onboarding/onboarding_unittest.cc @@ -0,0 +1,153 @@ +/* Copyright (c) 2023 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 +#include + +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "brave/browser/onboarding/domain_map.h" +#include "brave/browser/onboarding/onboarding_tab_helper.h" +#include "brave/browser/onboarding/pref_names.h" +#include "chrome/browser/first_run/first_run.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/web_contents_tester.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { +constexpr char kTestProfileName[] = "TestProfile"; +} // namespace + +TEST(DomainMapTest, CompanyNameAndCountTest) { + std::vector ads_list = { + GURL("https://www.blogger.com"), + GURL("https://www.youtube.com"), + GURL("https://www.google.com"), + }; + auto result = onboarding::GetCompanyNamesAndCountFromAdsList(ads_list); + EXPECT_EQ(result.first, "Google"); + EXPECT_EQ(result.second, 3); + + ads_list.push_back(GURL("https://www.alexa.com")); + result = onboarding::GetCompanyNamesAndCountFromAdsList(ads_list); + EXPECT_EQ(result.first, "Amazon, Google"); + EXPECT_EQ(result.second, 4); + + ads_list.push_back(GURL("https://www.facebook.com")); + result = onboarding::GetCompanyNamesAndCountFromAdsList(ads_list); + EXPECT_EQ(result.first, "Amazon, Facebook, Google"); + EXPECT_EQ(result.second, 5); + + ads_list.push_back(GURL("https://www.d.com")); + result = onboarding::GetCompanyNamesAndCountFromAdsList(ads_list); + EXPECT_EQ(result.first, "Amazon, Facebook, Google"); + EXPECT_EQ(result.second, 5); + + std::vector unknown_ads_list = { + GURL("https://www.a.com"), + GURL("https://www.b.com"), + GURL("https://www.c.com"), + }; + result = onboarding::GetCompanyNamesAndCountFromAdsList(unknown_ads_list); + EXPECT_EQ(result.first, ""); + EXPECT_EQ(result.second, 0); +} + +class OnboardingTest : public testing::Test { + public: + OnboardingTest() = default; + ~OnboardingTest() override = default; + + void SetUp() override { + TestingBrowserProcess* browser_process = TestingBrowserProcess::GetGlobal(); + profile_manager_ = std::make_unique(browser_process); + ASSERT_TRUE(profile_manager_->SetUp()); + profile_ = profile_manager_->CreateTestingProfile(kTestProfileName); + } + + void TearDown() override { + profile_ = nullptr; + profile_manager_->DeleteTestingProfile(kTestProfileName); + } + + Profile* profile() { return profile_; } + + content::BrowserTaskEnvironment task_environment_; + raw_ptr profile_; + std::unique_ptr profile_manager_; +}; + +TEST_F(OnboardingTest, HelperCreationTestForFirstRun) { + first_run::ResetCachedSentinelDataForTesting(); + base::CommandLine::ForCurrentProcess()->AppendSwitch( + switches::kForceFirstRun); + ASSERT_TRUE(first_run::IsChromeFirstRun()); + + auto web_contents = + content::WebContentsTester::CreateTestWebContents(profile(), nullptr); + ASSERT_TRUE(web_contents); + + // Check helper is created for first run. + OnboardingTabHelper::MaybeCreateForWebContents(web_contents.get()); + auto* helper = OnboardingTabHelper::FromWebContents(web_contents.get()); + EXPECT_TRUE(helper); + + // Even seven days passed durig the first run, helper should be created. + web_contents->RemoveUserData(OnboardingTabHelper::UserDataKey()); + OnboardingTabHelper::s_time_now_for_testing_ = + base::Time::Now() + base::Days(8); + OnboardingTabHelper::MaybeCreateForWebContents(web_contents.get()); + helper = OnboardingTabHelper::FromWebContents(web_contents.get()); + EXPECT_TRUE(helper); + + // Check helper is not created when |kLastShieldsIconHighlightTime| is not + // null. + web_contents->SetUserData(OnboardingTabHelper::UserDataKey(), nullptr); + TestingBrowserProcess::GetGlobal()->local_state()->SetTime( + onboarding::prefs::kLastShieldsIconHighlightTime, base::Time::Now()); + OnboardingTabHelper::MaybeCreateForWebContents(web_contents.get()); + helper = OnboardingTabHelper::FromWebContents(web_contents.get()); + EXPECT_FALSE(helper); +} + +TEST_F(OnboardingTest, HelperCreationTestForNonFirstRun) { + // Create sentinel as OnboardingTabHelper::IsSevenDaysPassedSinceFirstRun() + // checks its creation time to know how long it's been since then. + OnboardingTabHelper::s_time_now_for_testing_ = base::Time::Now(); + OnboardingTabHelper::s_sentinel_time_for_testing_ = base::Time::Now(); + first_run::ResetCachedSentinelDataForTesting(); + base::CommandLine::ForCurrentProcess()->AppendSwitch(switches::kNoFirstRun); + ASSERT_FALSE(first_run::IsChromeFirstRun()); + + auto web_contents = + content::WebContentsTester::CreateTestWebContents(profile(), nullptr); + ASSERT_TRUE(web_contents); + + // Check helper is not created when |kLastShieldsIconHighlightTime| is not + // null. + TestingBrowserProcess::GetGlobal()->local_state()->SetTime( + onboarding::prefs::kLastShieldsIconHighlightTime, base::Time::Now()); + OnboardingTabHelper::MaybeCreateForWebContents(web_contents.get()); + auto* tab_helper = OnboardingTabHelper::FromWebContents(web_contents.get()); + EXPECT_FALSE(tab_helper); + + // Check helper is created when |kLastShieldsIconHighlightTime| is null. + TestingBrowserProcess::GetGlobal()->local_state()->SetTime( + onboarding::prefs::kLastShieldsIconHighlightTime, base::Time()); + OnboardingTabHelper::MaybeCreateForWebContents(web_contents.get()); + tab_helper = OnboardingTabHelper::FromWebContents(web_contents.get()); + ASSERT_TRUE(tab_helper->CanHighlightBraveShields()); + + // Check exiting tab doesn't give highlight when 7 days passed. + OnboardingTabHelper::s_time_now_for_testing_ = + base::Time::Now() + base::Days(7); + EXPECT_FALSE(tab_helper->CanHighlightBraveShields()); +} diff --git a/browser/onboarding/pref_names.h b/browser/onboarding/pref_names.h new file mode 100644 index 000000000000..eb4e1380fe22 --- /dev/null +++ b/browser/onboarding/pref_names.h @@ -0,0 +1,16 @@ +/* Copyright (c) 2023 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/. */ + +#ifndef BRAVE_BROWSER_ONBOARDING_PREF_NAMES_H_ +#define BRAVE_BROWSER_ONBOARDING_PREF_NAMES_H_ + +namespace onboarding::prefs { + +constexpr char kLastShieldsIconHighlightTime[] = + "brave.onboarding.last_shields_icon_highlighted_time"; + +} // namespace onboarding::prefs + +#endif // BRAVE_BROWSER_ONBOARDING_PREF_NAMES_H_ diff --git a/browser/onboarding/sources.gni b/browser/onboarding/sources.gni new file mode 100644 index 000000000000..6ed76977db5e --- /dev/null +++ b/browser/onboarding/sources.gni @@ -0,0 +1,24 @@ +# Copyright (c) 2023 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/. + +brave_browser_onboarding_sources = [ + "//brave/browser/onboarding/domain_map.cc", + "//brave/browser/onboarding/domain_map.h", + "//brave/browser/onboarding/onboarding_tab_helper.cc", + "//brave/browser/onboarding/onboarding_tab_helper.h", + "//brave/browser/onboarding/pref_names.h", +] + +brave_browser_onboarding_deps = [ + "//base", + "//brave/browser:browser_process", + "//components/content_settings/core/browser", + "//components/content_settings/core/common", + "//components/permissions", + "//components/pref_registry", + "//components/prefs", + "//content/public/browser", + "//net/base/registry_controlled_domains", +] diff --git a/browser/sources.gni b/browser/sources.gni index ba4bd1e6798b..2e1176198571 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -27,6 +27,7 @@ import("//brave/browser/misc_metrics/sources.gni") import("//brave/browser/new_tab/sources.gni") import("//brave/browser/notifications/sources.gni") import("//brave/browser/ntp_background/sources.gni") +import("//brave/browser/onboarding/sources.gni") import("//brave/browser/perf/sources.gni") import("//brave/browser/permissions/sources.gni") import("//brave/browser/playlist/sources.gni") @@ -353,6 +354,9 @@ if (is_android) { } if (toolkit_views) { + brave_chrome_browser_sources += brave_browser_onboarding_sources + + brave_chrome_browser_deps += brave_browser_onboarding_deps brave_chrome_browser_deps += [ "//brave/browser/ui/sidebar", "//brave/components/sidebar", diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 1d03a92b1a99..7b78c93750ae 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -829,6 +829,10 @@ source_set("ui") { "views/brave_actions/brave_rewards_action_view.h", "views/brave_actions/brave_shields_action_view.cc", "views/brave_actions/brave_shields_action_view.h", + "views/brave_help_bubble/brave_help_bubble_delegate_view.cc", + "views/brave_help_bubble/brave_help_bubble_delegate_view.h", + "views/brave_help_bubble/brave_help_bubble_host_view.cc", + "views/brave_help_bubble/brave_help_bubble_host_view.h", "views/frame/vertical_tab_strip_region_view.cc", "views/frame/vertical_tab_strip_region_view.h", "views/frame/vertical_tab_strip_root_view.cc", diff --git a/browser/ui/brave_browser_window.cc b/browser/ui/brave_browser_window.cc index 15f67347ca51..88836104ade7 100644 --- a/browser/ui/brave_browser_window.cc +++ b/browser/ui/brave_browser_window.cc @@ -35,6 +35,11 @@ void BraveBrowserWindow::ToggleSidebar() {} bool BraveBrowserWindow::HasSelectedURL() const { return false; } + void BraveBrowserWindow::CleanAndCopySelectedURL() {} +bool BraveBrowserWindow::ShowBraveHelpBubbleView(const std::string& text) { + return false; +} + #endif diff --git a/browser/ui/brave_browser_window.h b/browser/ui/brave_browser_window.h index 829869c37a38..f65d5c71847f 100644 --- a/browser/ui/brave_browser_window.h +++ b/browser/ui/brave_browser_window.h @@ -6,6 +6,8 @@ #ifndef BRAVE_BROWSER_UI_BRAVE_BROWSER_WINDOW_H_ #define BRAVE_BROWSER_UI_BRAVE_BROWSER_WINDOW_H_ +#include + #include "brave/components/ai_chat/core/common/buildflags/buildflags.h" #include "brave/components/playlist/common/buildflags/buildflags.h" #include "brave/components/speedreader/common/buildflags/buildflags.h" @@ -55,6 +57,9 @@ class BraveBrowserWindow : public BrowserWindow { virtual void ToggleSidebar(); virtual bool HasSelectedURL() const; virtual void CleanAndCopySelectedURL(); + + // Returns true when bubble is shown. + virtual bool ShowBraveHelpBubbleView(const std::string& text); #endif #if BUILDFLAG(ENABLE_PLAYLIST_WEBUI) diff --git a/browser/ui/views/brave_actions/brave_shields_action_view.cc b/browser/ui/views/brave_actions/brave_shields_action_view.cc index 471e54405520..9e57feec1d28 100644 --- a/browser/ui/views/brave_actions/brave_shields_action_view.cc +++ b/browser/ui/views/brave_actions/brave_shields_action_view.cc @@ -38,10 +38,10 @@ #include "ui/views/controls/button/label_button_border.h" #include "ui/views/controls/highlight_path_generator.h" #include "ui/views/view.h" +#include "ui/views/view_class_properties.h" #include "url/gurl.h" namespace { - constexpr SkColor kBadgeBg = SkColorSetRGB(0x63, 0x64, 0x72); class BraveShieldsActionViewHighlightPathGenerator : public views::HighlightPathGenerator { @@ -59,6 +59,9 @@ class BraveShieldsActionViewHighlightPathGenerator }; } // namespace +DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(BraveShieldsActionView, + kShieldsActionIcon); + BraveShieldsActionView::BraveShieldsActionView(Profile& profile, TabStripModel& tab_strip_model) : LabelButton(base::BindRepeating(&BraveShieldsActionView::ButtonPressed, @@ -75,6 +78,7 @@ BraveShieldsActionView::BraveShieldsActionView(Profile& profile, SetAccessibleName( brave_l10n::GetLocalizedResourceUTF16String(IDS_BRAVE_SHIELDS)); SetHorizontalAlignment(gfx::ALIGN_CENTER); + SetProperty(views::kElementIdentifierKey, kShieldsActionIcon); tab_strip_model_->AddObserver(this); // The MenuButtonController makes sure the panel closes when clicked if the diff --git a/browser/ui/views/brave_actions/brave_shields_action_view.h b/browser/ui/views/brave_actions/brave_shields_action_view.h index d06d80f6487c..883d9a3b8d7a 100644 --- a/browser/ui/views/brave_actions/brave_shields_action_view.h +++ b/browser/ui/views/brave_actions/brave_shields_action_view.h @@ -26,6 +26,7 @@ class BraveShieldsActionView public brave_shields::BraveShieldsDataController::Observer, public TabStripModelObserver { public: + DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kShieldsActionIcon); explicit BraveShieldsActionView(Profile& profile, TabStripModel& tab_strip_model); BraveShieldsActionView(const BraveShieldsActionView&) = delete; diff --git a/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.cc b/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.cc new file mode 100644 index 000000000000..5d5db216850e --- /dev/null +++ b/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.cc @@ -0,0 +1,211 @@ +// Copyright (c) 2023 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/views/brave_help_bubble/brave_help_bubble_delegate_view.h" + +#include + +#include "base/strings/utf_string_conversions.h" +#include "components/grit/brave_components_strings.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/scoped_canvas.h" +#include "ui/views/border.h" +#include "ui/views/bubble/bubble_border.h" +#include "ui/views/bubble/bubble_border_arrow_utils.h" +#include "ui/views/bubble/bubble_frame_view.h" +#include "ui/views/controls/label.h" +#include "ui/views/widget/widget_delegate.h" + +using views::BubbleBorder; +using views::BubbleDialogDelegateView; +using views::BubbleFrameView; +using views::NonClientFrameView; + +namespace { +constexpr SkColor kBgColor = SkColorSetARGB(0xFF, 0x20, 0x4A, 0xE3); + +// This class paints a border with only an arrow, skipping the painting of +// shadow and border around the overall bounds. +class BorderWithArrow : public views::BubbleBorder { + public: + enum BubbleArrowPart { kFill, kBorder }; + + explicit BorderWithArrow(Arrow arrow, ui::ColorId color_id) + : views::BubbleBorder(arrow, + BubbleBorder::Shadow::STANDARD_SHADOW, + color_id) { + set_visible_arrow(true); + } + + BorderWithArrow(const BorderWithArrow&) = delete; + BorderWithArrow& operator=(const BorderWithArrow&) = delete; + ~BorderWithArrow() override = default; + + // views::BubbleBorder: + void Paint(const views::View& view, gfx::Canvas* canvas) override { + PaintVisibleArrow(view, canvas); + } + + private: + // We are copying this function from the upstream code because we only need to + // paint an arrow. + void PaintVisibleArrow(const views::View& view, gfx::Canvas* canvas) { + // The BubbleBorder subclass provides access to the visible_arrow_rect_ + // member only through a test function. + gfx::Point arrow_origin = GetVisibibleArrowRectForTesting().origin(); + + views::View::ConvertPointFromScreen(&view, &arrow_origin); + const gfx::Rect arrow_bounds(arrow_origin, + GetVisibibleArrowRectForTesting().size()); + + // Clip the canvas to a box that's big enough to hold the shadow in every + // dimension but won't overlap the bubble itself. + gfx::ScopedCanvas scoped(canvas); + gfx::Rect clip_rect = arrow_bounds; + const views::BubbleArrowSide side = GetBubbleArrowSide(arrow()); + clip_rect.Inset( + gfx::Insets::TLBR(side == views::BubbleArrowSide::kBottom ? 0 : -2, + side == views::BubbleArrowSide::kRight ? 0 : -2, + side == views::BubbleArrowSide::kTop ? 0 : -2, + side == views::BubbleArrowSide::kLeft ? 0 : -2)); + canvas->ClipRect(clip_rect); + + cc::PaintFlags flags; + flags.setStrokeCap(cc::PaintFlags::kRound_Cap); + + flags.setColor( + view.GetColorProvider()->GetColor(ui::kColorBubbleBorderShadowLarge)); + flags.setStyle(cc::PaintFlags::kStroke_Style); + flags.setStrokeWidth(1.2); + flags.setAntiAlias(true); + canvas->DrawPath( + GetVisibleArrowPath(arrow(), arrow_bounds, BubbleArrowPart::kBorder), + flags); + + flags.setColor(color()); + flags.setStyle(cc::PaintFlags::kFill_Style); + flags.setStrokeWidth(1.0); + flags.setAntiAlias(true); + canvas->DrawPath( + GetVisibleArrowPath(arrow(), arrow_bounds, BubbleArrowPart::kFill), + flags); + } + + SkPath GetVisibleArrowPath(BubbleBorder::Arrow arrow, + const gfx::Rect& bounds, + BubbleArrowPart part) { + constexpr size_t kNumPoints = 4; + gfx::RectF bounds_f(bounds); + SkPoint points[kNumPoints]; + switch (GetBubbleArrowSide(arrow)) { + case views::BubbleArrowSide::kRight: + points[0] = {bounds_f.x(), bounds_f.y()}; + points[1] = {bounds_f.right(), + bounds_f.y() + BubbleBorder::kVisibleArrowRadius - 1}; + points[2] = {bounds_f.right(), + bounds_f.y() + BubbleBorder::kVisibleArrowRadius}; + points[3] = {bounds_f.x(), bounds_f.bottom() - 1}; + break; + case views::BubbleArrowSide::kLeft: + points[0] = {bounds_f.right(), bounds_f.bottom() - 1}; + points[1] = {bounds_f.x(), + bounds_f.y() + BubbleBorder::kVisibleArrowRadius}; + points[2] = {bounds_f.x(), + bounds_f.y() + BubbleBorder::kVisibleArrowRadius - 1}; + points[3] = {bounds_f.right(), bounds_f.y()}; + break; + case views::BubbleArrowSide::kTop: + points[0] = {bounds_f.x(), bounds_f.bottom()}; + points[1] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius - 1, + bounds_f.y()}; + points[2] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius, + bounds_f.y()}; + points[3] = {bounds_f.right() - 1, bounds_f.bottom()}; + break; + case views::BubbleArrowSide::kBottom: + points[0] = {bounds_f.right() - 1, bounds_f.y()}; + points[1] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius, + bounds_f.bottom()}; + points[2] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius - 1, + bounds_f.bottom()}; + points[3] = {bounds_f.x(), bounds_f.y()}; + break; + } + + return SkPath::Polygon(points, kNumPoints, part == BubbleArrowPart::kFill); + } +}; +} // namespace + +BraveHelpBubbleDelegateView::BraveHelpBubbleDelegateView( + View* anchor_view, + const std::string& text) + : BubbleDialogDelegateView(anchor_view, BubbleBorder::Arrow::TOP_CENTER) { + SetButtons(ui::DIALOG_BUTTON_NONE); + set_shadow(BubbleBorder::Shadow::STANDARD_SHADOW); + set_corner_radius(10); + set_color(kBgColor); + SetLayoutManager(std::make_unique( + views::BoxLayout::Orientation::kVertical)); + + views::Label* blocked_trackers_label = + AddChildView(std::make_unique()); + blocked_trackers_label->SetBorder( + views::CreateEmptyBorder(gfx::Insets::TLBR(10, 0, 8, 0))); + SetUpLabel(blocked_trackers_label, base::UTF8ToUTF16(text), 16, + gfx::Font::Weight::SEMIBOLD); + + views::Label* view_label = AddChildView(std::make_unique()); + SetUpLabel(view_label, + l10n_util::GetStringUTF16( + IDS_BRAVE_SHIELDS_ONBOARDING_CLICK_TO_VIEW_LABEL), + 14, gfx::Font::Weight::NORMAL); + + AddChildView(view_label); +} + +BraveHelpBubbleDelegateView::~BraveHelpBubbleDelegateView() = default; + +std::unique_ptr +BraveHelpBubbleDelegateView::CreateNonClientFrameView(views::Widget* widget) { + std::unique_ptr frame = + BubbleDialogDelegateView::CreateNonClientFrameView(widget); + CHECK(frame); + + std::unique_ptr border = + std::make_unique(arrow(), color()); + border->SetColor(color()); + + if (GetParams().round_corners) { + border->SetCornerRadius(GetCornerRadius()); + } + + static_cast(frame.get()) + ->SetBubbleBorder(std::move(border)); + return frame; +} + +void BraveHelpBubbleDelegateView::SetUpLabel(views::Label* label, + const std::u16string& text, + int font_size, + gfx::Font::Weight font_weight) { + label->SetMultiLine(true); + label->SetMaximumWidth(390); + label->SetText(text); + label->SetAutoColorReadabilityEnabled(false); + label->SetEnabledColor(SK_ColorWHITE); + + const auto& font_list = label->font_list(); + label->SetFontList( + font_list.DeriveWithSizeDelta(font_size - font_list.GetFontSize()) + .DeriveWithWeight(font_weight)); + + label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT); +} + +BEGIN_METADATA(BraveHelpBubbleDelegateView, BubbleDialogDelegateView) +END_METADATA diff --git a/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.h b/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.h new file mode 100644 index 000000000000..50122713dbb8 --- /dev/null +++ b/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.h @@ -0,0 +1,37 @@ +// Copyright (c) 2023 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/. + +#ifndef BRAVE_BROWSER_UI_VIEWS_BRAVE_HELP_BUBBLE_BRAVE_HELP_BUBBLE_DELEGATE_VIEW_H_ +#define BRAVE_BROWSER_UI_VIEWS_BRAVE_HELP_BUBBLE_BRAVE_HELP_BUBBLE_DELEGATE_VIEW_H_ + +#include +#include + +#include "ui/base/metadata/metadata_header_macros.h" +#include "ui/views/bubble/bubble_dialog_delegate_view.h" + +class BraveHelpBubbleDelegateView : public views::BubbleDialogDelegateView { + public: + METADATA_HEADER(BraveHelpBubbleDelegateView); + + explicit BraveHelpBubbleDelegateView(View* anchor_view, + const std::string& text); + BraveHelpBubbleDelegateView(const BraveHelpBubbleDelegateView&) = delete; + BraveHelpBubbleDelegateView& operator=(const BraveHelpBubbleDelegateView&) = + delete; + ~BraveHelpBubbleDelegateView() override; + + private: + // views::BubbleDialogDelegate + std::unique_ptr CreateNonClientFrameView( + views::Widget* widget) override; + + void SetUpLabel(views::Label* label, + const std::u16string& text, + int font_size, + gfx::Font::Weight font_weight); +}; + +#endif // BRAVE_BROWSER_UI_VIEWS_BRAVE_HELP_BUBBLE_BRAVE_HELP_BUBBLE_DELEGATE_VIEW_H_ diff --git a/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.cc b/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.cc new file mode 100644 index 000000000000..78e8949e94bd --- /dev/null +++ b/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.cc @@ -0,0 +1,214 @@ +// Copyright (c) 2022 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/views/brave_help_bubble/brave_help_bubble_host_view.h" + +#include "cc/paint/paint_shader.h" +#include "extensions/common/constants.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPoint.h" +#include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/base/ui_base_types.h" +#include "ui/compositor/layer.h" +#include "ui/gfx/animation/animation.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/geometry/transform_util.h" +#include "ui/views/animation/animation_builder.h" +#include "ui/views/bubble/bubble_dialog_delegate_view.h" +#include "ui/views/bubble/bubble_frame_view.h" +#include "ui/views/view_class_properties.h" +#include "ui/views/view_utils.h" + +namespace { +constexpr int kWidth = 60; +constexpr int kHeight = 60; + +// The points here are defined as start and end points of the gradient, +// associated with an entire rect +constexpr SkPoint kPts[] = {{0, 0}, {kWidth, kHeight}}; + +// Colors are ported from Figma, but the order is intentionally flipped for +// proper gradient interpolation. Figma has the order as 1, 2, 3, here it's +// represented as 3, 2, 1 +constexpr SkColor4f kColors[] = {{0.65f, 0.54f, 1, 1}, + {1, 0.09f, 0.57f, 1}, + {0.98f, 0.44f, 0.31f, 1}}; + +// These are positions for each element in kColors +constexpr SkScalar kPositions[] = {0.0, 0.43, 0.93}; + +sk_sp kBraveGradient = + cc::PaintShader::MakeLinearGradient(kPts, + kColors, + kPositions, + std::size(kColors), + SkTileMode::kClamp); + +void SchedulePulsingAnimation(ui::Layer* layer) { + DCHECK(layer); + + constexpr base::TimeDelta kPulsingDuration = base::Milliseconds(1000); + + const gfx::Rect local_bounds(layer->bounds().size()); + views::AnimationBuilder() + .SetPreemptionStrategy( + ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET) + .Repeatedly() + .SetDuration(kPulsingDuration) + .SetTransform(layer, + gfx::GetScaleTransform(local_bounds.CenterPoint(), 0.7f), + gfx::Tween::EASE_IN) + .At(kPulsingDuration) + .SetDuration(kPulsingDuration) + .SetTransform(layer, + gfx::GetScaleTransform(local_bounds.CenterPoint(), 1.0f), + gfx::Tween::EASE_OUT); +} +} // namespace + +BraveHelpBubbleHostView::BraveHelpBubbleHostView() { + // Disable event handling to interact with the underlying element. + SetCanProcessEventsWithinSubtree(false); + SetPaintToLayer(); + layer()->SetFillsBoundsOpaquely(false); + SetSize({kWidth, kHeight}); +} + +BraveHelpBubbleHostView::~BraveHelpBubbleHostView() = default; + +bool BraveHelpBubbleHostView::Show() { + if (help_bubble_) { + return false; + } + + CHECK(tracked_element_ && !text_.empty()); + + auto* brave_help_bubble_delegate_view = + new BraveHelpBubbleDelegateView(this, text_); + help_bubble_ = views::BubbleDialogDelegateView::CreateBubble( + brave_help_bubble_delegate_view); + auto* frame_view = brave_help_bubble_delegate_view->GetBubbleFrameView(); + frame_view->SetDisplayVisibleArrow(true); + bubble_widget_observation_.Observe(help_bubble_); + + // Observes tracked element and host widget(browser frame) to know this host + // view's position update timing. + tracked_view_observation_.Observe(tracked_element_); + host_widget_observation_.Observe(GetWidget()); + + ui::ElementIdentifier id = + tracked_element_->GetProperty(views::kElementIdentifierKey); + CHECK(id); + // Listen activation state to to hide bubble.(ex, hide bubble when button + // clicked) + activated_subscription_ = + ui::ElementTracker::GetElementTracker()->AddElementActivatedCallback( + id, views::ElementTrackerViews::GetContextForView(tracked_element_), + base::BindRepeating( + &BraveHelpBubbleHostView::OnTrackedElementActivated, + weak_factory_.GetWeakPtr())); + + // With this inactive launching, bubble will be hidden after activated. + help_bubble_->ShowInactive(); + + UpdatePosition(); + SetVisible(true); + + if (gfx::Animation::ShouldRenderRichAnimation()) { + SchedulePulsingAnimation(layer()); + } + + return true; +} + +void BraveHelpBubbleHostView::Hide() { + if (!help_bubble_) { + return; + } + + // Closing bubble will make this host view hidden. + help_bubble_->CloseWithReason(views::Widget::ClosedReason::kLostFocus); +} + +void BraveHelpBubbleHostView::UpdatePosition() { + CHECK(tracked_element_); + auto tracked_element_local_center = + tracked_element_->GetLocalBounds().CenterPoint(); + views::View::ConvertPointToScreen(tracked_element_, + &tracked_element_local_center); + auto host_view_origin = views::View::ConvertPointFromScreen( + this->parent(), tracked_element_local_center); + host_view_origin.Offset(-kWidth / 2, -kHeight / 2); + SetPosition(host_view_origin); +} + +void BraveHelpBubbleHostView::OnPaint(gfx::Canvas* canvas) { + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setStyle(cc::PaintFlags::kStroke_Style); + flags.setShader(kBraveGradient); + flags.setStrokeWidth(2.f); + canvas->DrawCircle(GetContentsBounds().CenterPoint(), 27.f, flags); + flags.setStrokeWidth(6.f); + canvas->DrawCircle(GetContentsBounds().CenterPoint(), 20.f, flags); +} + +void BraveHelpBubbleHostView::OnViewBoundsChanged(views::View* observed_view) { + // Update host view position when |tracked_element_| bounds is changed. + CHECK(tracked_element_ == observed_view); + UpdatePosition(); +} + +void BraveHelpBubbleHostView::OnViewIsDeleting(views::View* observed_view) { + CHECK(tracked_element_ == observed_view); + tracked_element_ = nullptr; + tracked_view_observation_.Reset(); +} + +void BraveHelpBubbleHostView::OnViewVisibilityChanged( + views::View* observed_view, + views::View* starting_view) { + // Close help bubble when |tracked_element_| gets hidden. + // It could be in-visible when its ancestor is hidden. + if (!observed_view->GetVisible() || + (starting_view && !starting_view->GetVisible())) { + Hide(); + } +} + +void BraveHelpBubbleHostView::OnWidgetBoundsChanged(views::Widget* widget, + const gfx::Rect&) { + // Only update host view position when host widget(browser frame)'s bounds is + // changed. + if (help_bubble_ == widget) { + return; + } + + UpdatePosition(); +} + +void BraveHelpBubbleHostView::OnWidgetDestroying(views::Widget* widget) { + if (help_bubble_ == widget) { + // Hide this host view when bubble is closed. + help_bubble_ = nullptr; + bubble_widget_observation_.Reset(); + tracked_view_observation_.Reset(); + text_ = std::string(); + activated_subscription_ = {}; + tracked_element_ = nullptr; + + SetVisible(false); + } + + host_widget_observation_.Reset(); +} + +void BraveHelpBubbleHostView::OnTrackedElementActivated( + ui::TrackedElement* element) { + Hide(); +} + +BEGIN_METADATA(BraveHelpBubbleHostView, View) +END_METADATA diff --git a/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.h b/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.h new file mode 100644 index 000000000000..c9cc69ba511e --- /dev/null +++ b/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.h @@ -0,0 +1,67 @@ +// Copyright (c) 2022 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/. + +#ifndef BRAVE_BROWSER_UI_VIEWS_BRAVE_HELP_BUBBLE_BRAVE_HELP_BUBBLE_HOST_VIEW_H_ +#define BRAVE_BROWSER_UI_VIEWS_BRAVE_HELP_BUBBLE_BRAVE_HELP_BUBBLE_HOST_VIEW_H_ + +#include +#include + +#include "brave/browser/ui/views/brave_help_bubble/brave_help_bubble_delegate_view.h" +#include "ui/views/interaction/element_tracker_views.h" +#include "ui/views/view.h" +#include "ui/views/widget/widget_observer.h" + +class BraveHelpBubbleHostView : public views::View, + public views::ViewObserver, + public views::WidgetObserver { + public: + METADATA_HEADER(BraveHelpBubbleHostView); + + BraveHelpBubbleHostView(); + BraveHelpBubbleHostView(const BraveHelpBubbleHostView&) = delete; + BraveHelpBubbleHostView& operator=(const BraveHelpBubbleHostView&) = delete; + ~BraveHelpBubbleHostView() override; + + // Return true when bubble is shown. + bool Show(); + void Hide(); + + void set_text(const std::string& text) { text_ = text; } + void set_tracked_element(views::View* element) { tracked_element_ = element; } + + private: + void UpdatePosition(); + + // views::View: + void OnPaint(gfx::Canvas* canvas) override; + + // views::ViewObserver: + void OnViewBoundsChanged(views::View* observed_view) override; + void OnViewIsDeleting(views::View* observed_view) override; + void OnViewVisibilityChanged(views::View* observed_view, + views::View* starting_view) override; + + // views::WidgetObserver: + void OnWidgetBoundsChanged(views::Widget* widget, const gfx::Rect&) override; + void OnWidgetDestroying(views::Widget* widget) override; + + void OnTrackedElementActivated(ui::TrackedElement* element); + + std::string text_; + raw_ptr tracked_element_ = nullptr; + raw_ptr help_bubble_ = nullptr; + ui::ElementTracker::Subscription activated_subscription_; + base::ScopedObservation + tracked_view_observation_{this}; + base::ScopedObservation + host_widget_observation_{this}; + base::ScopedObservation + bubble_widget_observation_{this}; + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // BRAVE_BROWSER_UI_VIEWS_BRAVE_HELP_BUBBLE_BRAVE_HELP_BUBBLE_HOST_VIEW_H_ diff --git a/browser/ui/views/frame/brave_browser_view.cc b/browser/ui/views/frame/brave_browser_view.cc index 82a00e032142..6ac48d7ba810 100644 --- a/browser/ui/views/frame/brave_browser_view.cc +++ b/browser/ui/views/frame/brave_browser_view.cc @@ -28,6 +28,7 @@ #include "brave/browser/ui/sidebar/sidebar_utils.h" #include "brave/browser/ui/views/brave_actions/brave_actions_container.h" #include "brave/browser/ui/views/brave_actions/brave_shields_action_view.h" +#include "brave/browser/ui/views/brave_help_bubble/brave_help_bubble_host_view.h" #include "brave/browser/ui/views/brave_rewards/tip_panel_bubble_host.h" #include "brave/browser/ui/views/brave_shields/cookie_list_opt_in_bubble_host.h" #include "brave/browser/ui/views/frame/brave_contents_view_util.h" @@ -567,6 +568,7 @@ void BraveBrowserView::CloseWalletBubble() { void BraveBrowserView::AddedToWidget() { BrowserView::AddedToWidget(); + // we must call all new views once BraveBrowserView is added to widget GetBrowserViewLayout()->set_contents_background(contents_background_view_); GetBrowserViewLayout()->set_sidebar_container(sidebar_container_view_); @@ -592,6 +594,27 @@ void BraveBrowserView::AddedToWidget() { } } +bool BraveBrowserView::ShowBraveHelpBubbleView(const std::string& text) { + auto* shields_action_view = + static_cast(GetLocationBarView()) + ->brave_actions_contatiner_view() + ->GetShieldsActionView(); + if (!shields_action_view || !shields_action_view->GetVisible()) { + return false; + } + + // When help bubble is closed, this host view gets hidden. + // For now, this help bubble host view is only used for shield icon, but it + // could be re-used for other icons or views in the future. + if (!brave_help_bubble_host_view_) { + brave_help_bubble_host_view_ = + AddChildView(std::make_unique()); + } + brave_help_bubble_host_view_->set_text(text); + brave_help_bubble_host_view_->set_tracked_element(shields_action_view); + return brave_help_bubble_host_view_->Show(); +} + void BraveBrowserView::LoadAccelerators() { if (base::FeatureList::IsEnabled(commands::features::kBraveCommands)) { auto* accelerator_service = @@ -615,6 +638,11 @@ void BraveBrowserView::OnTabStripModelChanged( // After stopping, current tab cycling, new tab cycling will be started. StopTabCycling(); } + + if (selection.active_tab_changed() && brave_help_bubble_host_view_ && + brave_help_bubble_host_view_->GetVisible()) { + brave_help_bubble_host_view_->Hide(); + } } views::CloseRequestResult BraveBrowserView::OnWindowCloseRequested() { diff --git a/browser/ui/views/frame/brave_browser_view.h b/browser/ui/views/frame/brave_browser_view.h index 852a08c32172..ea3ce3ba45d1 100644 --- a/browser/ui/views/frame/brave_browser_view.h +++ b/browser/ui/views/frame/brave_browser_view.h @@ -53,6 +53,7 @@ class SidebarContainerView; class WalletButton; class ViewShadow; class VerticalTabStripWidgetDelegateView; +class BraveHelpBubbleHostView; class BraveBrowserView : public BrowserView, public commands::AcceleratorService::Observer { @@ -101,6 +102,7 @@ class BraveBrowserView : public BrowserView, vertical_tab_strip_widget_delegate_view() { return vertical_tab_strip_widget_delegate_view_; } + bool ShowBraveHelpBubbleView(const std::string& text) override; // commands::AcceleratorService: void OnAcceleratorsChanged(const commands::Accelerators& changed) override; @@ -157,6 +159,7 @@ class BraveBrowserView : public BrowserView, void UpdateSideBarHorizontalAlignment(); bool closing_confirm_dialog_activated_ = false; + raw_ptr brave_help_bubble_host_view_ = nullptr; raw_ptr sidebar_container_view_ = nullptr; raw_ptr sidebar_separator_view_ = nullptr; raw_ptr contents_background_view_ = nullptr; diff --git a/components/resources/brave_shields_strings.grdp b/components/resources/brave_shields_strings.grdp index 7b4f4f57c4b0..c86f333a472f 100644 --- a/components/resources/brave_shields_strings.grdp +++ b/components/resources/brave_shields_strings.grdp @@ -201,4 +201,23 @@ Wildcards are not allowed for Brave Shields. + + + {BLOCKED_COUNT, plural, + =0 {Brave Shields blocked $1 trackers on $2} + =1 {Brave Shields blocked $1 and 1 other tracker on $2} + other {Brave Shields blocked $1 and {BLOCKED_COUNT} other trackers on $2} + } + + + + {BLOCKED_COUNT, plural, + =1 {Brave Shields blocked 1 tracker on $1} + other {Brave Shields blocked {BLOCKED_COUNT} trackers on $1} + } + + + + Click the Brave icon to view Brave Shields. + diff --git a/test/BUILD.gn b/test/BUILD.gn index 257081d23bde..a39559476890 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -476,6 +476,7 @@ test("brave_unit_tests") { "//brave/browser/ui/views/brave_tooltips/brave_tooltips_unittest.cc", ] deps += [ + "//brave/browser/onboarding:unit_tests", "//brave/browser/ui/sidebar:unit_tests", "//brave/browser/ui/views/brave_ads", "//brave/browser/ui/views/brave_tooltips",