| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/ash/chrome_new_window_client.h" |
| #include "base/memory/raw_ptr.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "apps/launcher.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/app_list/internal_app_id_constants.h" |
| #include "ash/public/cpp/keyboard_shortcut_viewer.h" |
| #include "ash/public/cpp/shelf_model.h" |
| #include "ash/public/cpp/shelf_types.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy_factory.h" |
| #include "chrome/browser/apps/app_service/intent_util.h" |
| #include "chrome/browser/apps/app_service/launch_utils.h" |
| #include "chrome/browser/apps/app_service/metrics/app_service_metrics.h" |
| #include "chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.h" |
| #include "chrome/browser/ash/apps/apk_web_app_service.h" |
| #include "chrome/browser/ash/arc/arc_util.h" |
| #include "chrome/browser/ash/file_manager/app_id.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/file_manager/url_util.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/ash/system_web_apps/system_web_app_manager.h" |
| #include "chrome/browser/ash/web_applications/calculator_app/calculator_app_utils.h" |
| #include "chrome/browser/ash/web_applications/camera_app/chrome_camera_app_ui_delegate.h" |
| #include "chrome/browser/chromeos/arc/arc_web_contents_data.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/prefs/incognito_mode_prefs.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/sessions/tab_restore_service_factory.h" |
| #include "chrome/browser/ui/ash/multi_user/multi_user_util.h" |
| #include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h" |
| #include "chrome/browser/ui/ash/shelf/app_window_base.h" |
| #include "chrome/browser/ui/ash/shelf/app_window_shelf_item_controller.h" |
| #include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h" |
| #include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/extensions/app_launch_params.h" |
| #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h" |
| #include "chrome/browser/ui/settings_window_manager_chromeos.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/webui/chrome_web_contents_handler.h" |
| #include "chrome/browser/ui/webui/settings/chromeos/constants/routes.mojom.h" |
| #include "chrome/browser/ui/webui/tab_strip/tab_strip_ui_util.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_id.h" |
| #include "chrome/browser/web_applications/web_app_id_constants.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "components/arc/intent_helper/arc_intent_helper_bridge.h" |
| #include "components/services/app_service/public/cpp/app_launch_util.h" |
| #include "components/services/app_service/public/cpp/app_types.h" |
| #include "components/services/app_service/public/cpp/intent_util.h" |
| #include "components/services/app_service/public/cpp/types_util.h" |
| #include "components/sessions/core/tab_restore_service.h" |
| #include "components/sessions/core/tab_restore_service_observer.h" |
| #include "components/url_formatter/url_fixer.h" |
| #include "components/user_manager/user.h" |
| #include "components/user_manager/user_manager.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension.h" |
| #include "third_party/blink/public/mojom/navigation/was_activated_option.mojom.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/dragdrop/os_exchange_data.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/base/window_open_disposition.h" |
| #include "url/url_constants.h" |
| |
| namespace { |
| |
| void RestoreTabUsingProfile(Profile* profile) { |
| sessions::TabRestoreService* service = |
| TabRestoreServiceFactory::GetForProfile(profile); |
| service->RestoreMostRecentEntry(nullptr); |
| } |
| |
| bool IsIncognitoAllowed() { |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| return profile && IncognitoModePrefs::IsIncognitoAllowed(profile); |
| } |
| |
| // Returns URL path and query without the "/" prefix. For example, for the URL |
| // "chrome://settings/networks/?type=WiFi" returns "networks/?type=WiFi". |
| std::string GetPathAndQuery(const GURL& url) { |
| std::string result = url.path(); |
| if (!result.empty() && result[0] == '/') |
| result.erase(0, 1); |
| if (url.has_query()) { |
| result += '?'; |
| result += url.query(); |
| } |
| return result; |
| } |
| |
| chrome::FeedbackSource MapToChromeSource( |
| ash::NewWindowDelegate::FeedbackSource source) { |
| switch (source) { |
| case ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceAsh: |
| return chrome::FeedbackSource::kFeedbackSourceAsh; |
| case ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceAssistant: |
| return chrome::FeedbackSource::kFeedbackSourceAssistant; |
| case ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceQuickAnswers: |
| return chrome::FeedbackSource::kFeedbackSourceQuickAnswers; |
| case ash::NewWindowDelegate::FeedbackSource:: |
| kFeedbackSourceChannelIndicator: |
| return chrome::FeedbackSource::kFeedbackSourceChannelIndicator; |
| } |
| } |
| |
| // When the Files SWA is enabled: Open Files SWA. |
| // Returns true if it opens the SWA. |
| // `target_directory` is optional, if provided it opens the Files SWA in the |
| // given directory, instead of the default directory. |
| bool OpenFilesSwa(Profile* const profile, |
| base::FilePath target_directory = {}) { |
| GURL directory_url; |
| if (!target_directory.empty() && |
| !file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl( |
| profile, target_directory, file_manager::util::GetFileManagerURL(), |
| &directory_url)) { |
| LOG(WARNING) << "Failed to convert the path to FileSystemURL: " |
| << target_directory << " using the default directory"; |
| } |
| |
| std::u16string title; |
| ui::SelectFileDialog::FileTypeInfo file_type_info; |
| file_type_info.allowed_paths = |
| ui::SelectFileDialog::FileTypeInfo::ANY_PATH_OR_URL; |
| GURL files_swa_url = |
| ::file_manager::util::GetFileManagerMainPageUrlWithParams( |
| ui::SelectFileDialog::SELECT_NONE, title, |
| /*current_directory_url=*/directory_url, |
| /*selection_url=*/{}, |
| /*target_name=*/{}, &file_type_info, |
| /*file_type_index=*/0, |
| /*search_query=*/{}, |
| /*show_android_picker_apps=*/false, |
| /*volume_filter=*/{}); |
| |
| ash::SystemAppLaunchParams params; |
| params.url = files_swa_url; |
| ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::FILE_MANAGER, |
| params); |
| return true; |
| } |
| |
| } // namespace |
| |
| ChromeNewWindowClient::ChromeNewWindowClient() { |
| arc::ArcIntentHelperBridge::SetControlCameraAppDelegate(this); |
| } |
| |
| ChromeNewWindowClient::~ChromeNewWindowClient() { |
| arc::ArcIntentHelperBridge::SetControlCameraAppDelegate(nullptr); |
| } |
| |
| // static |
| ChromeNewWindowClient* ChromeNewWindowClient::Get() { |
| return static_cast<ChromeNewWindowClient*>( |
| ash::NewWindowDelegate::GetInstance()); |
| } |
| |
| // TabRestoreHelper is used to restore a tab. In particular when the user |
| // attempts to a restore a tab if the TabRestoreService hasn't finished loading |
| // this waits for it. Once the TabRestoreService finishes loading the tab is |
| // restored. |
| class ChromeNewWindowClient::TabRestoreHelper |
| : public sessions::TabRestoreServiceObserver { |
| public: |
| TabRestoreHelper(ChromeNewWindowClient* delegate, |
| Profile* profile, |
| sessions::TabRestoreService* service) |
| : delegate_(delegate), profile_(profile), tab_restore_service_(service) { |
| tab_restore_service_->AddObserver(this); |
| } |
| |
| TabRestoreHelper(const TabRestoreHelper&) = delete; |
| TabRestoreHelper& operator=(const TabRestoreHelper&) = delete; |
| |
| ~TabRestoreHelper() override { tab_restore_service_->RemoveObserver(this); } |
| |
| sessions::TabRestoreService* tab_restore_service() { |
| return tab_restore_service_; |
| } |
| |
| void TabRestoreServiceChanged(sessions::TabRestoreService* service) override { |
| } |
| |
| void TabRestoreServiceDestroyed( |
| sessions::TabRestoreService* service) override { |
| // This destroys us. |
| delegate_->tab_restore_helper_.reset(); |
| } |
| |
| void TabRestoreServiceLoaded(sessions::TabRestoreService* service) override { |
| RestoreTabUsingProfile(profile_); |
| // This destroys us. |
| delegate_->tab_restore_helper_.reset(); |
| } |
| |
| private: |
| raw_ptr<ChromeNewWindowClient, ExperimentalAsh> delegate_; |
| raw_ptr<Profile, ExperimentalAsh> profile_; |
| raw_ptr<sessions::TabRestoreService, ExperimentalAsh> tab_restore_service_; |
| }; |
| |
| void ChromeNewWindowClient::NewTab() { |
| Browser* browser = chrome::FindBrowserWithActiveWindow(); |
| if (browser && browser->is_type_normal()) { |
| chrome::NewTab(browser); |
| return; |
| } |
| |
| // Display a browser, setting the focus to the location bar after it is shown. |
| { |
| chrome::ScopedTabbedBrowserDisplayer displayer( |
| ProfileManager::GetActiveUserProfile()); |
| browser = displayer.browser(); |
| chrome::NewTab(browser); |
| } |
| |
| browser->SetFocusToLocationBar(); |
| } |
| |
| void ChromeNewWindowClient::NewWindow(bool is_incognito, |
| bool should_trigger_session_restore) { |
| if (is_incognito && !IsIncognitoAllowed()) |
| return; |
| |
| Browser* browser = chrome::FindBrowserWithActiveWindow(); |
| Profile* profile = (browser && browser->profile()) |
| ? browser->profile()->GetOriginalProfile() |
| : ProfileManager::GetActiveUserProfile(); |
| chrome::NewEmptyWindow( |
| is_incognito ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/true) |
| : profile, |
| should_trigger_session_restore); |
| } |
| |
| void ChromeNewWindowClient::NewWindowForDetachingTab( |
| aura::Window* source_window, |
| const ui::OSExchangeData& drop_data, |
| NewWindowForDetachingTabCallback closure) { |
| DCHECK(ash::features::IsWebUITabStripTabDragIntegrationEnabled()); |
| |
| BrowserView* source_view = BrowserView::GetBrowserViewForNativeWindow( |
| source_window->GetToplevelWindow()); |
| if (!source_view) { |
| std::move(closure).Run(/*new_window=*/nullptr); |
| return; |
| } |
| |
| Browser::CreateParams params = source_view->browser()->create_params(); |
| params.user_gesture = true; |
| params.initial_show_state = ui::SHOW_STATE_DEFAULT; |
| Browser* browser = Browser::Create(params); |
| if (!browser) { |
| std::move(closure).Run(/*new_window=*/nullptr); |
| return; |
| } |
| |
| if (!tab_strip_ui::DropTabsInNewBrowser(browser, drop_data)) { |
| browser->window()->Close(); |
| std::move(closure).Run(/*new_window=*/nullptr); |
| return; |
| } |
| |
| // TODO(https://crbug.com/1069869): evaluate whether the above |
| // failures can happen in valid states, and if so whether we need to |
| // reflect failure in UX. |
| |
| // TODO(crbug.com/1225667): Loosen restriction for SplitViewController to be |
| // able to snap a window without calling Show(). It will simplify the logic |
| // without having to set and clear ash::kIsDraggingTabsKey by calling Show() |
| // after snapping the window to the right place. |
| |
| // We need to mark the newly created window with |ash::kIsDraggingTabsKey| |
| // and clear it afterwards in order to prevent |
| // SplitViewController::AutoSnapController from snapping it on Show(). |
| aura::Window* window = browser->window()->GetNativeWindow(); |
| window->SetProperty(ash::kIsDraggingTabsKey, true); |
| browser->window()->Show(); |
| window->ClearProperty(ash::kIsDraggingTabsKey); |
| std::move(closure).Run(window); |
| } |
| |
| namespace { |
| WindowOpenDisposition ToWindowOpenDisposition( |
| ash::NewWindowDelegate::Disposition disposition) { |
| switch (disposition) { |
| case ash::NewWindowDelegate::Disposition::kNewForegroundTab: |
| return WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| case ash::NewWindowDelegate::Disposition::kNewWindow: |
| return WindowOpenDisposition::NEW_WINDOW; |
| case ash::NewWindowDelegate::Disposition::kOffTheRecord: |
| return WindowOpenDisposition::OFF_THE_RECORD; |
| case ash::NewWindowDelegate::Disposition::kSwitchToTab: |
| return WindowOpenDisposition::SWITCH_TO_TAB; |
| } |
| } |
| } // namespace |
| |
| void ChromeNewWindowClient::OpenUrl(const GURL& url, |
| OpenUrlFrom from, |
| Disposition disposition) { |
| // Opens a URL in a new tab. If the URL is for a chrome://settings page, |
| // opens settings in a new window. |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| if ((url.SchemeIs(url::kAboutScheme) || |
| url.SchemeIs(content::kChromeUIScheme))) { |
| // Show browser settings (e.g. chrome://settings). This may open in a window |
| // or a tab depending on feature SplitSettings. |
| if (url.host() == chrome::kChromeUISettingsHost) { |
| std::string sub_page = GetPathAndQuery(url); |
| chrome::ShowSettingsSubPageForProfile(profile, sub_page); |
| return; |
| } |
| // OS settings are shown in a window. |
| if (url.host() == chrome::kChromeUIOSSettingsHost) { |
| std::string sub_page = GetPathAndQuery(url); |
| chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile, |
| sub_page); |
| return; |
| } |
| } |
| |
| NavigateParams navigate_params( |
| profile, url, |
| ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_FROM_API)); |
| navigate_params.disposition = ToWindowOpenDisposition(disposition); |
| |
| // If the |from| is kUserInteraction, then the page will load with a user |
| // activation. This means it will be able to autoplay media without |
| // restriction. |
| if (from == OpenUrlFrom::kUserInteraction) |
| navigate_params.was_activated = blink::mojom::WasActivatedOption::kYes; |
| |
| Navigate(&navigate_params); |
| |
| if (navigate_params.browser) { |
| // The browser window might be on another user's desktop, and hence not |
| // visible. Ensure the browser becomes visible on this user's desktop. |
| multi_user_util::MoveWindowToCurrentDesktop( |
| navigate_params.browser->window()->GetNativeWindow()); |
| } |
| |
| auto* tab = navigate_params.navigated_or_inserted_contents; |
| if (from == OpenUrlFrom::kArc && tab) { |
| // Add a flag to remember this tab originated in the ARC context. |
| tab->SetUserData(&arc::ArcWebContentsData::kArcTransitionFlag, |
| std::make_unique<arc::ArcWebContentsData>(tab)); |
| |
| apps::IntentHandlingMetrics::RecordOpenBrowserMetrics( |
| apps::IntentHandlingMetrics::AppType::kArc); |
| } |
| } |
| |
| void ChromeNewWindowClient::OpenCalculator() { |
| Profile* const profile = ProfileManager::GetActiveUserProfile(); |
| apps::AppServiceProxy* proxy = |
| apps::AppServiceProxyFactory::GetForProfile(profile); |
| DCHECK(proxy); |
| proxy->Launch(ash::calculator_app::GetInstalledCalculatorAppId(profile), |
| ui::EF_NONE, apps::LaunchSource::kFromKeyboard); |
| } |
| |
| void ChromeNewWindowClient::OpenFileManager() { |
| Profile* const profile = ProfileManager::GetActiveUserProfile(); |
| if (OpenFilesSwa(profile)) { |
| return; |
| } |
| |
| apps::AppServiceProxy* proxy = |
| apps::AppServiceProxyFactory::GetForProfile(profile); |
| DCHECK(proxy); |
| |
| auto launch_files_app = [proxy](const apps::AppUpdate& update) { |
| if (update.Readiness() != apps::Readiness::kReady) { |
| LOG(WARNING) |
| << "Couldn't launch Files app because it isn't ready, readiness: " |
| << static_cast<int>(update.Readiness()); |
| return; |
| } |
| |
| proxy->Launch(update.AppId(), |
| apps::GetEventFlags(WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| /*prefer_container=*/true), |
| apps::LaunchSource::kFromKeyboard); |
| }; |
| |
| bool result = proxy->AppRegistryCache().ForOneApp( |
| file_manager::kFileManagerAppId, std::move(launch_files_app)); |
| DCHECK(result); |
| } |
| |
| void ChromeNewWindowClient::OpenDownloadsFolder() { |
| Profile* const profile = ProfileManager::GetActiveUserProfile(); |
| base::FilePath target_directory = |
| file_manager::util::GetDownloadsFolderForProfile(profile); |
| if (OpenFilesSwa(profile, target_directory)) { |
| return; |
| } |
| |
| apps::AppServiceProxy* proxy = |
| apps::AppServiceProxyFactory::GetForProfile(profile); |
| auto downloads_path = |
| file_manager::util::GetDownloadsFolderForProfile(profile); |
| DCHECK(proxy); |
| |
| auto launch_files_app = [proxy, |
| downloads_path](const apps::AppUpdate& update) { |
| if (update.Readiness() != apps::Readiness::kReady) { |
| LOG(WARNING) |
| << "Couldn't launch Files app because it isn't ready, readiness: " |
| << static_cast<int>(update.Readiness()); |
| return; |
| } |
| |
| std::vector<base::FilePath> launch_files; |
| launch_files.push_back(downloads_path); |
| proxy->LaunchAppWithFiles( |
| update.AppId(), |
| apps::GetEventFlags(WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| /*prefer_container=*/true), |
| apps::LaunchSource::kFromKeyboard, std::move(launch_files)); |
| }; |
| |
| bool result = proxy->AppRegistryCache().ForOneApp( |
| file_manager::kFileManagerAppId, launch_files_app); |
| DCHECK(result); |
| } |
| |
| void ChromeNewWindowClient::OpenCrosh() { |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::CROSH); |
| } |
| |
| void ChromeNewWindowClient::OpenGetHelp() { |
| Profile* const profile = ProfileManager::GetActiveUserProfile(); |
| chrome::ShowHelpForProfile(profile, chrome::HELP_SOURCE_KEYBOARD); |
| } |
| |
| void ChromeNewWindowClient::RestoreTab() { |
| if (tab_restore_helper_.get()) { |
| DCHECK(!tab_restore_helper_->tab_restore_service()->IsLoaded()); |
| return; |
| } |
| |
| Browser* browser = chrome::FindBrowserWithActiveWindow(); |
| Profile* profile = browser ? browser->profile() : nullptr; |
| if (!profile) |
| profile = ProfileManager::GetActiveUserProfile(); |
| if (profile->IsOffTheRecord()) |
| return; |
| sessions::TabRestoreService* service = |
| TabRestoreServiceFactory::GetForProfile(profile); |
| if (!service) |
| return; |
| |
| if (service->IsLoaded()) { |
| RestoreTabUsingProfile(profile); |
| } else { |
| tab_restore_helper_ = |
| std::make_unique<TabRestoreHelper>(this, profile, service); |
| service->LoadTabsFromLastSession(); |
| } |
| } |
| |
| void ChromeNewWindowClient::ShowKeyboardShortcutViewer() { |
| if (ash::features::ShouldOnlyShowNewShortcutApp()) { |
| ShowShortcutCustomizationApp(); |
| return; |
| } |
| ash::ToggleKeyboardShortcutViewer(); |
| } |
| |
| void ChromeNewWindowClient::ShowShortcutCustomizationApp() { |
| chrome::ShowShortcutCustomizationApp(ProfileManager::GetActiveUserProfile()); |
| } |
| |
| void ChromeNewWindowClient::ShowTaskManager() { |
| chrome::OpenTaskManager(nullptr); |
| } |
| |
| void ChromeNewWindowClient::OpenDiagnostics() { |
| chrome::ShowDiagnosticsApp(ProfileManager::GetActiveUserProfile()); |
| } |
| |
| void ChromeNewWindowClient::OpenFeedbackPage( |
| FeedbackSource source, |
| const std::string& description_template) { |
| chrome::OpenFeedbackDialog(chrome::FindBrowserWithActiveWindow(), |
| MapToChromeSource(source), description_template); |
| } |
| |
| void ChromeNewWindowClient::OpenPersonalizationHub() { |
| Profile* const profile = ProfileManager::GetActiveUserProfile(); |
| ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::PERSONALIZATION); |
| } |
| |
| void ChromeNewWindowClient::LaunchCameraApp(const std::string& queries, |
| int32_t task_id) { |
| DCHECK(IsCameraAppEnabled()); |
| ChromeCameraAppUIDelegate::CameraAppDialog::ShowIntent( |
| queries, arc::GetArcWindow(task_id)); |
| apps::RecordAppLaunch(web_app::kCameraAppId, apps::LaunchSource::kFromArc); |
| } |
| |
| void ChromeNewWindowClient::CloseCameraApp() { |
| const ash::ShelfID shelf_id(web_app::kCameraAppId); |
| AppWindowShelfItemController* const app_controller = |
| ChromeShelfController::instance() |
| ->shelf_model() |
| ->GetAppWindowShelfItemController(shelf_id); |
| if (!app_controller) |
| return; |
| |
| DCHECK_LE(app_controller->window_count(), 1lu); |
| if (app_controller->window_count() > 0) |
| app_controller->windows().front()->Close(); |
| } |
| |
| bool ChromeNewWindowClient::IsCameraAppEnabled() { |
| Profile* const profile = ProfileManager::GetActiveUserProfile(); |
| auto* swa_manager = ash::SystemWebAppManager::Get(profile); |
| return swa_manager && |
| swa_manager->IsAppEnabled(ash::SystemWebAppType::CAMERA); |
| } |