| // 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/ui/ash/assistant/assistant_browser_delegate_impl.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_switches.h" |
| #include "ash/public/cpp/assistant/assistant_interface_binder.h" |
| #include "ash/public/cpp/assistant/controller/assistant_interaction_controller.h" |
| #include "ash/public/cpp/network_config_service.h" |
| #include "ash/public/cpp/new_window_delegate.h" |
| #include "base/check_is_test.h" |
| #include "base/command_line.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/strings/string_util.h" |
| #include "base/types/expected.h" |
| #include "base/types/expected_macros.h" |
| #include "chrome/browser/apps/app_service/app_launch_params.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy_ash.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy_factory.h" |
| #include "chrome/browser/ash/assistant/assistant_util.h" |
| #include "chrome/browser/ash/crosapi/browser_util.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/lifetime/termination_notification.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/ui/ash/assistant/assistant_setup.h" |
| #include "chrome/browser/ui/ash/assistant/device_actions_delegate_impl.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chromeos/ash/services/assistant/public/cpp/assistant_browser_delegate.h" |
| #include "chromeos/ash/services/assistant/public/cpp/assistant_service.h" |
| #include "chromeos/ash/services/assistant/public/cpp/features.h" |
| #include "chromeos/ash/services/assistant/public/mojom/assistant_audio_decoder.mojom.h" |
| #include "chromeos/services/assistant/public/shared/constants.h" |
| #include "components/session_manager/core/session_manager.h" |
| #include "components/user_manager/user_manager.h" |
| #include "content/public/browser/audio_service.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/device_service.h" |
| #include "content/public/browser/media_session_service.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "content/public/browser/service_process_host.h" |
| #include "content/public/common/content_switches.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| #if BUILDFLAG(ENABLE_CROS_LIBASSISTANT) |
| #include "chromeos/ash/services/libassistant/public/mojom/service.mojom.h" |
| #endif // BUILDFLAG(ENABLE_CROS_LIBASSISTANT) |
| |
| namespace { |
| |
| Profile* GetActiveUserProfile() { |
| user_manager::User* active_user = |
| user_manager::UserManager::Get()->GetActiveUser(); |
| CHECK(active_user); |
| |
| return ash::ProfileHelper::Get()->GetProfileByUser(active_user); |
| } |
| |
| } // namespace |
| |
| AssistantBrowserDelegateImpl::AssistantBrowserDelegateImpl() { |
| auto* session_manager = session_manager::SessionManager::Get(); |
| // AssistantBrowserDelegateImpl must be created before any user session is |
| // created. Otherwise, it will not get OnUserProfileLoaded notification. |
| DCHECK(session_manager->sessions().empty()); |
| session_manager->AddObserver(this); |
| |
| subscription_ = browser_shutdown::AddAppTerminatingCallback(base::BindOnce( |
| &AssistantBrowserDelegateImpl::OnAppTerminating, base::Unretained(this))); |
| } |
| |
| AssistantBrowserDelegateImpl::~AssistantBrowserDelegateImpl() { |
| session_manager::SessionManager::Get()->RemoveObserver(this); |
| if (identity_manager_) { |
| identity_manager_->RemoveObserver(this); |
| } |
| } |
| |
| void AssistantBrowserDelegateImpl::MaybeInit(Profile* profile) { |
| if (assistant::IsAssistantAllowedForProfile(profile) != |
| ash::assistant::AssistantAllowedState::ALLOWED) { |
| return; |
| } |
| |
| if (!profile_) { |
| profile_ = profile; |
| identity_manager_ = IdentityManagerFactory::GetForProfile(profile_); |
| DCHECK(identity_manager_); |
| identity_manager_->AddObserver(this); |
| } |
| DCHECK_EQ(profile_, profile); |
| |
| if (initialized_) { |
| return; |
| } |
| |
| initialized_ = true; |
| |
| device_actions_ = std::make_unique<DeviceActions>( |
| std::make_unique<DeviceActionsDelegateImpl>()); |
| |
| service_ = std::make_unique<ash::assistant::Service>( |
| profile->GetURLLoaderFactory()->Clone(), |
| IdentityManagerFactory::GetForProfile(profile), profile->GetPrefs()); |
| service_->Init(); |
| |
| assistant_setup_ = std::make_unique<AssistantSetup>(); |
| } |
| |
| void AssistantBrowserDelegateImpl::MaybeStartAssistantOptInFlow() { |
| if (!initialized_) { |
| return; |
| } |
| |
| assistant_setup_->MaybeStartAssistantOptInFlow(); |
| } |
| |
| void AssistantBrowserDelegateImpl::OnAppTerminating() { |
| if (!initialized_) { |
| return; |
| } |
| |
| ash::assistant::AssistantService::Get()->Shutdown(); |
| } |
| |
| void AssistantBrowserDelegateImpl::InitializeNewEntryPointFor( |
| Profile* profile) { |
| CHECK(profile); |
| |
| web_app::WebAppProvider* provider = |
| web_app::WebAppProvider::GetForWebApps(profile); |
| if (!provider) { |
| // `WebAppProvider` is not available if `GetBrowserContextForWebApps` in |
| // `web_app_utils.cc` returns nullptr, e.g., guest session. This is |
| // non-recoverable, i.e., no need to wait and/or re-try. |
| return; |
| } |
| |
| if (profile_for_new_entry_point_) { |
| CHECK_EQ(profile_for_new_entry_point_, profile) |
| << "profile_for_new_entry_point_ is already initialized with a " |
| "different profile. There should be only a single primary profile."; |
| return; |
| } |
| |
| // Profile is set only if `WebAppProvider` is available for the profile. |
| profile_for_new_entry_point_ = profile; |
| |
| // Assistant new entry point is loaded to `WebAppProvider` as an async |
| // operation. We have to wait for the async load before checking if Assistant |
| // new entry point is installed on a device/profile. |
| provider->on_external_managers_synchronized().Post( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantBrowserDelegateImpl::OnExternalManagersSynchronized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void AssistantBrowserDelegateImpl::OnAssistantStatusChanged( |
| ash::assistant::AssistantStatus new_status) { |
| ash::AssistantState::Get()->NotifyStatusChanged(new_status); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestAssistantVolumeControl( |
| mojo::PendingReceiver<ash::mojom::AssistantVolumeControl> receiver) { |
| ash::AssistantInterfaceBinder::GetInstance()->BindVolumeControl( |
| std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestBatteryMonitor( |
| mojo::PendingReceiver<device::mojom::BatteryMonitor> receiver) { |
| content::GetDeviceService().BindBatteryMonitor(std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestWakeLockProvider( |
| mojo::PendingReceiver<device::mojom::WakeLockProvider> receiver) { |
| content::GetDeviceService().BindWakeLockProvider(std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestAudioStreamFactory( |
| mojo::PendingReceiver<media::mojom::AudioStreamFactory> receiver) { |
| content::GetAudioService().BindStreamFactory(std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestAudioDecoderFactory( |
| mojo::PendingReceiver<ash::assistant::mojom::AssistantAudioDecoderFactory> |
| receiver) { |
| content::ServiceProcessHost::Launch( |
| std::move(receiver), |
| content::ServiceProcessHost::Options() |
| .WithDisplayName("Assistant Audio Decoder Service") |
| .Pass()); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestAudioFocusManager( |
| mojo::PendingReceiver<media_session::mojom::AudioFocusManager> receiver) { |
| content::GetMediaSessionService().BindAudioFocusManager(std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestMediaControllerManager( |
| mojo::PendingReceiver<media_session::mojom::MediaControllerManager> |
| receiver) { |
| content::GetMediaSessionService().BindMediaControllerManager( |
| std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::RequestNetworkConfig( |
| mojo::PendingReceiver<chromeos::network_config::mojom::CrosNetworkConfig> |
| receiver) { |
| ash::GetNetworkConfigService(std::move(receiver)); |
| } |
| |
| void AssistantBrowserDelegateImpl::OpenUrl(GURL url) { |
| // The new tab should be opened with a user activation since the user |
| // interacted with the Assistant to open the url. |in_background| describes |
| // the relationship between |url| and Assistant UI, not the browser. As |
| // such, the browser will always be instructed to open |url| in a new |
| // browser tab and Assistant UI state will be updated downstream to respect |
| // |in_background|. |
| ash::NewWindowDelegate::GetPrimary()->OpenUrl( |
| url, ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction, |
| ash::NewWindowDelegate::Disposition::kNewForegroundTab); |
| } |
| |
| base::expected<const web_app::WebApp*, |
| ash::assistant::AssistantBrowserDelegate::Error> |
| AssistantBrowserDelegateImpl::ResolveNewEntryPointIfEligible() { |
| if (!profile_for_new_entry_point_) { |
| return base::unexpected( |
| ash::assistant::AssistantBrowserDelegate::Error::kProfileNotReady); |
| } |
| |
| if (!on_is_new_entry_point_eligible_ready_.is_signaled()) { |
| return base::unexpected(ash::assistant::AssistantBrowserDelegate::Error:: |
| kWebAppProviderNotReadyToRead); |
| } |
| |
| if (!ash::assistant::features::IsNewEntryPointEnabled()) { |
| return base::unexpected(ash::assistant::AssistantBrowserDelegate::Error:: |
| kNewEntryPointNotEnabled); |
| } |
| |
| web_app::WebAppProvider* provider = |
| web_app::WebAppProvider::GetForWebApps(profile_for_new_entry_point_); |
| CHECK(provider) << "WebAppProvider must be available if " |
| "on_is_new_entry_point_eligible_ready_ is signaled"; |
| |
| std::string app_id = entry_point_id_for_testing_.empty() |
| ? chromeos::assistant::kEntryPointId |
| : entry_point_id_for_testing_; |
| const web_app::WebApp* web_app = |
| provider->registrar_unsafe().GetAppById(app_id); |
| if (!web_app) { |
| return base::unexpected(ash::assistant::AssistantBrowserDelegate::Error:: |
| kNewEntryPointNotFound); |
| } |
| |
| return web_app; |
| } |
| |
| void AssistantBrowserDelegateImpl::OnExternalManagersSynchronized() { |
| on_is_new_entry_point_eligible_ready_.Signal(); |
| } |
| |
| base::expected<bool, ash::assistant::AssistantBrowserDelegate::Error> |
| AssistantBrowserDelegateImpl::IsNewEntryPointEligibleForPrimaryProfile() { |
| // TODO(crbug.com/382561528): add metrics for has_value and error. |
| base::expected<const web_app::WebApp*, |
| ash::assistant::AssistantBrowserDelegate::Error> |
| maybe_web_app = ResolveNewEntryPointIfEligible(); |
| if (maybe_web_app.has_value()) { |
| return true; |
| } |
| |
| auto non_transient_error = |
| base::MakeFixedFlatSet<ash::assistant::AssistantBrowserDelegate::Error>( |
| {ash::assistant::AssistantBrowserDelegate::Error:: |
| kNewEntryPointNotEnabled, |
| ash::assistant::AssistantBrowserDelegate::Error:: |
| kNewEntryPointNotFound}); |
| if (non_transient_error.contains(maybe_web_app.error())) { |
| return false; |
| } |
| |
| return base::unexpected(maybe_web_app.error()); |
| } |
| |
| void AssistantBrowserDelegateImpl::OpenNewEntryPoint() { |
| ASSIGN_OR_RETURN(const web_app::WebApp* web_app, |
| ResolveNewEntryPointIfEligible(), [](auto) {}); |
| CHECK(profile_for_new_entry_point_); |
| |
| // Check if the app is already running. If it is, bring the window to front. |
| for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) { |
| if ((browser->is_type_app() || browser->is_type_app_popup()) && |
| web_app->app_id() == |
| web_app::GetAppIdFromApplicationName(browser->app_name()) && |
| profile_for_new_entry_point_ == browser->profile()) { |
| browser->window()->Show(); |
| return; |
| } |
| } |
| |
| apps::AppServiceProxyFactory::GetForProfile(profile_for_new_entry_point_) |
| ->LaunchAppWithParams(apps::AppLaunchParams( |
| web_app->app_id(), apps::LaunchContainer::kLaunchContainerWindow, |
| WindowOpenDisposition::NEW_WINDOW, |
| // TODO(xiaohuic): maybe add new source |
| apps::LaunchSource::kUnknown)); |
| } |
| |
| #if BUILDFLAG(ENABLE_CROS_LIBASSISTANT) |
| void AssistantBrowserDelegateImpl::RequestLibassistantService( |
| mojo::PendingReceiver<ash::libassistant::mojom::LibassistantService> |
| receiver) { |
| content::ServiceProcessHost::Launch< |
| ash::libassistant::mojom::LibassistantService>( |
| std::move(receiver), content::ServiceProcessHost::Options() |
| .WithDisplayName("Libassistant Service") |
| .Pass()); |
| } |
| #endif // BUILDFLAG(ENABLE_CROS_LIBASSISTANT) |
| |
| void AssistantBrowserDelegateImpl::OverrideEntryPointIdForTesting( |
| const std::string& test_entry_point_id) { |
| CHECK_IS_TEST(); |
| entry_point_id_for_testing_ = test_entry_point_id; |
| } |
| |
| void AssistantBrowserDelegateImpl::OnExtendedAccountInfoUpdated( |
| const AccountInfo& info) { |
| if (initialized_) { |
| return; |
| } |
| |
| MaybeInit(profile_); |
| } |
| |
| void AssistantBrowserDelegateImpl::OnUserProfileLoaded( |
| const AccountId& account_id) { |
| if (!assistant_state_observation_.IsObserving() && !initialized_ && |
| ash::AssistantState::Get()) { |
| assistant_state_observation_.Observe(ash::AssistantState::Get()); |
| } |
| } |
| |
| void AssistantBrowserDelegateImpl::OnUserSessionStarted(bool is_primary_user) { |
| if (is_primary_user) { |
| InitializeNewEntryPointFor(GetActiveUserProfile()); |
| } |
| |
| if (ash::features::IsOobeSkipAssistantEnabled()) { |
| return; |
| } |
| |
| // Disable the handling for browser tests to prevent the Assistant being |
| // enabled unexpectedly. |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| if (is_primary_user && !ash::switches::ShouldSkipOobePostLogin() && |
| !command_line->HasSwitch(switches::kBrowserTest)) { |
| MaybeStartAssistantOptInFlow(); |
| } |
| } |
| |
| void AssistantBrowserDelegateImpl::OnAssistantFeatureAllowedChanged( |
| ash::assistant::AssistantAllowedState allowed_state) { |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| |
| MaybeInit(profile); |
| } |