| // Copyright 2018 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/web_applications/web_app_tab_helper.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| |
| #include "base/check_is_test.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/unguessable_token.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/task_manager/web_contents_tags.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/browser/web_applications/manifest_update_manager.h" |
| #include "chrome/browser/web_applications/os_integration/os_integration_manager.h" |
| #include "chrome/browser/web_applications/policy/web_app_policy_manager.h" |
| #include "chrome/browser/web_applications/proto/web_app_install_state.pb.h" |
| #include "chrome/browser/web_applications/web_app_audio_focus_id_map.h" |
| #include "chrome/browser/web_applications/web_app_filter.h" |
| #include "chrome/browser/web_applications/web_app_launch_queue_delegate_impl.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/browser/web_applications/web_app_ui_manager.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/page_load_metrics/browser/metrics_web_contents_observer.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "components/webapps/browser/launch_queue/launch_queue.h" |
| #include "content/public/browser/media_session.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/site_instance.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h" |
| #include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "chrome/browser/web_applications/os_integration/mac/web_app_shortcut_mac.h" |
| #endif |
| |
| namespace web_app { |
| |
| // static |
| void WebAppTabHelper::Create(tabs::TabInterface* tab, |
| content::WebContents* contents) { |
| // In the event when a tab is moved from a normal browser window to an app |
| // window, or vise versa, we want to keep the state on WebAppTabHelper. |
| auto* tab_helper = WebAppTabHelper::FromWebContents(contents); |
| if (tab->GetContents() == contents && tab_helper) { |
| tab_helper->SubscribeToTabState(tab); |
| return; |
| } |
| |
| // If on the other hand this is a tab-discard, we let the old tab's |
| // WebAppTabHelper be destroyed at its normal timing. This is because the |
| // current implementation of WebAppMetrics relies on the assumption that |
| // discarded WebContents are still usable. |
| // This will become a moot point once https://crbug.com/347770670 is fixed, as |
| // discarding will no longer change the WebContents. |
| |
| auto helper = std::make_unique<WebAppTabHelper>(tab, contents); |
| helper->SubscribeToTabState(tab); |
| contents->SetUserData(UserDataKey(), std::move(helper)); |
| } |
| |
| // static |
| const webapps::AppId* WebAppTabHelper::GetAppId( |
| const content::WebContents* web_contents) { |
| auto* tab_helper = WebAppTabHelper::FromWebContents(web_contents); |
| return tab_helper && tab_helper->app_id_.has_value() |
| ? &tab_helper->app_id_.value() |
| : nullptr; |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| std::optional<webapps::AppId> |
| WebAppTabHelper::GetAppIdForNotificationAttribution( |
| content::WebContents* web_contents) { |
| if (!UseNotificationAttributionForWebAppShims()) { |
| return std::nullopt; |
| } |
| const webapps::AppId* app_id = GetAppId(web_contents); |
| if (!app_id) { |
| return std::nullopt; |
| } |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| WebAppProvider* web_app_provider = WebAppProvider::GetForWebApps(profile); |
| if (!web_app_provider || |
| web_app_provider->registrar_unsafe().GetInstallState(*app_id) != |
| proto::INSTALLED_WITH_OS_INTEGRATION) { |
| return std::nullopt; |
| } |
| // Default apps are locally installed but unless an app shim has been created |
| // for them should not get attributed notifications. |
| if (!AppShimRegistry::Get()->IsAppInstalledInProfile(*app_id, |
| profile->GetPath())) { |
| return std::nullopt; |
| } |
| return *app_id; |
| } |
| #endif |
| |
| WebAppTabHelper::~WebAppTabHelper() = default; |
| |
| const base::UnguessableToken& WebAppTabHelper::GetAudioFocusGroupIdForTesting() |
| const { |
| return audio_focus_group_id_; |
| } |
| |
| webapps::LaunchQueue& WebAppTabHelper::EnsureLaunchQueue() { |
| if (!launch_queue_) { |
| std::unique_ptr<webapps::LaunchQueueDelegate> delegate = |
| std::make_unique<LaunchQueueDelegateImpl>( |
| provider_->registrar_unsafe()); |
| launch_queue_ = std::make_unique<webapps::LaunchQueue>(web_contents(), |
| std::move(delegate)); |
| } |
| return *launch_queue_; |
| } |
| |
| void WebAppTabHelper::SetState(std::optional<webapps::AppId> app_id, |
| std::optional<webapps::AppId> window_app_id) { |
| // Empty string should not be used to indicate "no app ID". |
| DCHECK(!app_id || !app_id->empty()); |
| |
| // If the app_id is changing, then it should exist in the database. |
| DCHECK(app_id_ == app_id || !app_id || |
| provider_->registrar_unsafe().IsInstallState( |
| *app_id, {proto::InstallState::SUGGESTED_FROM_ANOTHER_DEVICE, |
| proto::InstallState::INSTALLED_WITHOUT_OS_INTEGRATION, |
| proto::InstallState::INSTALLED_WITH_OS_INTEGRATION}) || |
| provider_->registrar_unsafe().IsUninstalling(*app_id)); |
| |
| if (app_id_ == app_id && window_app_id_ == window_app_id) { |
| // This can be triggered for navigations that are happening in the same app |
| // window, like if a navigation is captured in an open window causing a page |
| // load to happen. Record the `UseCounter` there as well, as that is |
| // treated as an app launch. |
| ScheduleManifestAppliedUseCounter(); |
| return; |
| } |
| |
| std::optional<webapps::AppId> previous_app_id = std::move(app_id_); |
| app_id_ = std::move(app_id); |
| window_app_id_ = std::move(window_app_id); |
| |
| if (previous_app_id != app_id_) { |
| OnAssociatedAppChanged(previous_app_id, app_id_); |
| } |
| UpdateAudioFocusGroupId(); |
| ScheduleManifestAppliedUseCounter(); |
| } |
| |
| void WebAppTabHelper::SetAppId(std::optional<webapps::AppId> app_id) { |
| SetState(std::move(app_id), window_app_id_); |
| } |
| |
| void WebAppTabHelper::SetIsInAppWindow( |
| std::optional<webapps::AppId> window_app_id) { |
| SetState(app_id(), std::move(window_app_id)); |
| } |
| |
| void WebAppTabHelper::SetCallbackToRunOnTabChanges(base::OnceClosure callback) { |
| on_tab_details_changed_callback_ = std::move(callback); |
| } |
| |
| void WebAppTabHelper::OnTabBackgrounded(tabs::TabInterface*) { |
| MaybeNotifyTabChanged(); |
| } |
| |
| void WebAppTabHelper::OnTabDetached(tabs::TabInterface* tab_interface, |
| tabs::TabInterface::DetachReason) { |
| MaybeNotifyTabChanged(); |
| } |
| |
| void WebAppTabHelper::ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (navigation_handle->IsInPrimaryMainFrame()) { |
| const GURL& url = navigation_handle->GetURL(); |
| SetAppId(provider_->registrar_unsafe().FindBestAppWithUrlInScope( |
| url, web_app::WebAppFilter::InstalledInChrome())); |
| } |
| |
| // If navigating to a Web App (including navigation in sub frames), let |
| // `WebAppUiManager` observers perform tab-secific setup for navigations in |
| // Web Apps. |
| if (app_id_.has_value()) { |
| provider_->ui_manager().NotifyReadyToCommitNavigation(app_id_.value(), |
| navigation_handle); |
| } |
| } |
| |
| void WebAppTabHelper::PrimaryPageChanged(content::Page& page) { |
| // This method is invoked whenever primary page of a WebContents |
| // (WebContents::GetPrimaryPage()) changes to `page`. This happens in one of |
| // the following cases: |
| // 1) when the current RenderFrameHost in the primary main frame changes after |
| // a navigation. |
| // 2) when the current RenderFrameHost in the primary main frame is |
| // reinitialized after a crash. |
| // 3) when a cross-document navigation commits in the current RenderFrameHost |
| // of the primary main frame. |
| // |
| // The new primary page might either be a brand new one (if the committed |
| // navigation created a new document in the primary main frame) or an existing |
| // one (back-forward cache restore or prerendering activation). |
| // |
| // This notification is not dispatched for changes of pages in the non-primary |
| // frame trees (prerendering, fenced frames) and when the primary page is |
| // destroyed (e.g., when closing a tab). |
| // |
| // See the declaration of WebContentsObserver::PrimaryPageChanged for more |
| // information. |
| provider_->manifest_update_manager().MaybeUpdate( |
| page.GetMainDocument().GetLastCommittedURL(), app_id_, web_contents()); |
| |
| ReinstallPlaceholderAppIfNecessary( |
| page.GetMainDocument().GetLastCommittedURL()); |
| } |
| |
| void WebAppTabHelper::DidFinishLoad(content::RenderFrameHost* render_frame_host, |
| const GURL& validated_url) { |
| can_record_manifest_applied_ = true; |
| MaybeRecordManifestAppliedUseCounter(); |
| } |
| |
| void WebAppTabHelper::FlushLaunchQueueForTesting() const { |
| if (!launch_queue_) { |
| return; |
| } |
| launch_queue_->FlushForTesting(); // IN-TEST |
| } |
| |
| WebAppTabHelper::WebAppTabHelper(tabs::TabInterface* tab, |
| content::WebContents* contents) |
| : content::WebContentsUserData<WebAppTabHelper>(*contents), |
| content::WebContentsObserver(contents) { |
| CHECK(AreWebAppsEnabled(tab->GetBrowserWindowInterface()->GetProfile())); |
| provider_ = WebAppProvider::GetForLocalAppsUnchecked( |
| tab->GetBrowserWindowInterface()->GetProfile()); |
| CHECK(provider_); |
| observation_.Observe(&provider_->install_manager()); |
| SetState(provider_->registrar_unsafe().FindBestAppWithUrlInScope( |
| contents->GetLastCommittedURL(), |
| web_app::WebAppFilter::InstalledInChrome()), |
| /*window_app_id=*/std::nullopt); |
| } |
| |
| bool WebAppTabHelper::CanBeUsedForFocusExisting() const { |
| constexpr std::array<std::string_view, 3> |
| kMimeTypesWithExpectedLaunchConsumer = { |
| "text/html", |
| "text/xhtml+xml", |
| "application/xhtml+xml", |
| }; |
| |
| const std::string& mime_type = web_contents()->GetContentsMimeType(); |
| for (std::string_view allowed_mime_type : |
| kMimeTypesWithExpectedLaunchConsumer) { |
| if (mime_type == allowed_mime_type) { |
| return true; |
| } |
| } |
| |
| const network::mojom::URLResponseHead* response_head = |
| web_contents()->GetPrimaryMainFrame()->GetLastResponseHead(); |
| if (response_head) { |
| for (std::string_view allowed_mime_type : |
| kMimeTypesWithExpectedLaunchConsumer) { |
| if (response_head->mime_type == allowed_mime_type) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| void WebAppTabHelper::OnWebAppInstalled( |
| const webapps::AppId& installed_app_id) { |
| // Check if current web_contents url is in scope for the newly installed app. |
| std::optional<webapps::AppId> app_id = |
| provider_->registrar_unsafe().FindBestAppWithUrlInScope( |
| web_contents()->GetLastCommittedURL(), |
| web_app::WebAppFilter::InstalledInChrome()); |
| if (app_id == installed_app_id) { |
| SetAppId(app_id); |
| } |
| } |
| |
| void WebAppTabHelper::OnWebAppWillBeUninstalled( |
| const webapps::AppId& uninstalled_app_id) { |
| if (app_id_ == uninstalled_app_id) { |
| SetAppId(std::nullopt); |
| } |
| } |
| |
| void WebAppTabHelper::OnWebAppInstallManagerDestroyed() { |
| observation_.Reset(); |
| SetAppId(std::nullopt); |
| } |
| |
| void WebAppTabHelper::OnAssociatedAppChanged( |
| const std::optional<webapps::AppId>& previous_app_id, |
| const std::optional<webapps::AppId>& new_app_id) { |
| // Tag WebContents for Task Manager. |
| // cases to consider: |
| // 1. non-app -> app (association added) |
| // 2. non-app -> non-app |
| // 3. app -> app (association changed) |
| // 4. app -> non-app (association removed) |
| |
| if (new_app_id.has_value() && !new_app_id->empty()) { |
| // case 1 & 3: |
| // WebContents could already be tagged with TabContentsTag or WebAppTag, |
| // therefore we want to clear it. |
| task_manager::WebContentsTags::ClearTag(web_contents()); |
| task_manager::WebContentsTags::CreateForWebApp( |
| web_contents(), new_app_id.value(), |
| provider_->registrar_unsafe().IsIsolated(new_app_id.value())); |
| } else { |
| // case 4: |
| if (previous_app_id.has_value() && !previous_app_id->empty()) { |
| // remove WebAppTag, add TabContentsTag. |
| task_manager::WebContentsTags::ClearTag(web_contents()); |
| task_manager::WebContentsTags::CreateForTabContents(web_contents()); |
| } |
| // case 2: do nothing |
| } |
| } |
| |
| void WebAppTabHelper::UpdateAudioFocusGroupId() { |
| // TODO(https://crbug.com/378970240): Perhaps check that these values are |
| // equal. |
| if (app_id_.has_value() && window_app_id_.has_value()) { |
| audio_focus_group_id_ = |
| provider_->audio_focus_id_map().CreateOrGetIdForApp(app_id_.value()); |
| } else { |
| audio_focus_group_id_ = base::UnguessableToken::Null(); |
| } |
| |
| // There is no need to trigger creation of a MediaSession if we'd merely be |
| // resetting the audo focus group id as that is the default state. Skipping |
| // creating a MediaSession when not needed also helps with some (unit) tests |
| // where creating a MediaSession can trigger other subsystems in ways that |
| // the test might not be setup for (for example lack of a real io thread for |
| // the mdns service). |
| if (audio_focus_group_id_ == base::UnguessableToken::Null() && |
| !content::MediaSession::GetIfExists(web_contents())) { |
| return; |
| } |
| content::MediaSession::Get(web_contents()) |
| ->SetAudioFocusGroupId(audio_focus_group_id_); |
| } |
| |
| void WebAppTabHelper::ReinstallPlaceholderAppIfNecessary(const GURL& url) { |
| provider_->policy_manager().ReinstallPlaceholderAppIfNecessary( |
| url, base::DoNothing()); |
| } |
| |
| void WebAppTabHelper::SubscribeToTabState(tabs::TabInterface* tab_interface) { |
| tab_subscriptions_.clear(); |
| CHECK(tab_interface); |
| tab_subscriptions_.push_back( |
| tab_interface->RegisterWillDeactivate(base::BindRepeating( |
| &WebAppTabHelper::OnTabBackgrounded, weak_factory_.GetWeakPtr()))); |
| tab_subscriptions_.push_back( |
| tab_interface->RegisterWillDetach(base::BindRepeating( |
| &WebAppTabHelper::OnTabDetached, weak_factory_.GetWeakPtr()))); |
| } |
| |
| void WebAppTabHelper::MaybeNotifyTabChanged() { |
| if (on_tab_details_changed_callback_) { |
| std::move(on_tab_details_changed_callback_).Run(); |
| } |
| } |
| |
| void WebAppTabHelper::ScheduleManifestAppliedUseCounter() { |
| bool should_measure_use_counter_for_standalone_launch = |
| app_id_.has_value() && app_id_ == window_app_id_ && |
| !provider_->registrar_unsafe().GetAppManifestUrl(*app_id_).is_empty(); |
| if (!should_measure_use_counter_for_standalone_launch) { |
| return; |
| } |
| meaure_manifest_applied_use_counter_ = true; |
| MaybeRecordManifestAppliedUseCounter(); |
| } |
| |
| void WebAppTabHelper::MaybeRecordManifestAppliedUseCounter() { |
| if (!meaure_manifest_applied_use_counter_ || !can_record_manifest_applied_) { |
| return; |
| } |
| |
| page_load_metrics::MetricsWebContentsObserver::RecordFeatureUsage( |
| web_contents()->GetPrimaryMainFrame(), |
| blink::mojom::WebFeature::kInstalledManifestApplied); |
| meaure_manifest_applied_use_counter_ = false; |
| can_record_manifest_applied_ = false; |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(WebAppTabHelper); |
| |
| } // namespace web_app |