// Copyright 2019 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/web_applications/web_app_ui_manager_impl.h"

#include <algorithm>
#include <map>
#include <memory>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

#include "base/barrier_callback.h"
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/one_shot_event.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/pass_key.h"
#include "build/build_config.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/profiles/profile.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/intent_picker_tab_helper.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/profiles/profile_error_dialog.h"
#include "chrome/browser/ui/unload_controller.h"
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/web_apps/web_app_blocked_migration_infobar_delegate.h"
#include "chrome/browser/ui/views/web_apps/web_app_update_review_dialog.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/web_app_browser_controller.h"
#include "chrome/browser/ui/web_applications/web_app_dialog_utils.h"
#include "chrome/browser/ui/web_applications/web_app_dialogs.h"
#include "chrome/browser/ui/web_applications/web_app_launch_navigation_handle_user_data.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/browser/ui/web_applications/web_app_metrics.h"
#include "chrome/browser/ui/web_applications/web_app_run_on_os_login_notification.h"
#include "chrome/browser/user_education/user_education_service.h"
#include "chrome/browser/user_education/user_education_service_factory.h"
#include "chrome/browser/web_applications/link_capturing_features.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_icon_generator.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_pref_guardrails.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_tab_helper.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/browser/web_applications/web_app_uninstall_dialog_user_options.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/grit/generated_resources.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/services/app_service/public/cpp/app_launch_params.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/tabs/public/tab_interface.h"
#include "components/user_education/common/feature_promo/feature_promo_controller.h"
#include "components/user_education/common/feature_promo/feature_promo_result.h"
#include "components/user_education/common/user_education_data.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/installable/ml_install_operation_tracker.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/clear_site_data_utils.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "extensions/browser/app_sorting.h"
#include "extensions/browser/extension_system.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-shared.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/page_transition_types.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/native_window_tracker/native_window_tracker.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"

#if !BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/apps/link_capturing/enable_link_capturing_infobar_delegate.h"
#include "chrome/browser/infobars/confirm_infobar_creator.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#else
#include "chrome/browser/ui/web_applications/web_app_relaunch_notification.h"
#endif  // !BUILDFLAG(IS_CHROMEOS)

#if !BUILDFLAG(IS_MAC)
#include "ui/aura/window.h"
#endif  // !BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/public/cpp/shelf_model.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/ash/app_list/app_list_syncable_service.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chromeos/ash/components/nonclosable_app_ui/nonclosable_app_ui_utils.h"
#include "components/sync/model/string_ordinal.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_WIN)
#include "base/process/process.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/os_integration/web_app_shortcut.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#endif  // BUILDFLAG(IS_WIN)

namespace base {
class FilePath;
}

namespace web_app {

class AppLock;

namespace {

struct UninstallDialogState {
  IconMetadataFromDisk main_icon_metadata;
  std::vector<SubAppUninstallMetadata> sub_apps;
};

#if BUILDFLAG(IS_WIN)
void UninstallWebAppWithDialogFromStartupSwitch(
    std::unique_ptr<ScopedKeepAlive> scoped_keep_alive,
    const webapps::AppId& app_id,
    WebAppProvider* provider) {
  if (provider->registrar_unsafe().CanUserUninstallWebApp(app_id)) {
    provider->ui_manager().PresentUserUninstallDialog(
        app_id, webapps::WebappUninstallSource::kOsSettings,
        gfx::NativeWindow(),
        base::BindOnce(
            [](std::unique_ptr<ScopedKeepAlive> scoped_keep_alive,
               webapps::UninstallResultCode code) {
              // This ensures that the scoped_keep_alive will be deleted in the
              // next message loop, giving objects like DialogDelegate enough
              // time to shut itself down. See crbug.com/40946953 for more
              // information.
              base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
                  FROM_HERE, std::move(scoped_keep_alive));
            },
            std::move(scoped_keep_alive)));
  } else {
    // This is necessary to remove all OS integrations if the app has
    // been uninstalled.
    SynchronizeOsOptions synchronize_options;
    synchronize_options.force_unregister_os_integration = true;
    provider->scheduler().SynchronizeOsIntegration(
        app_id, base::DoNothingWithBoundArgs(std::move(scoped_keep_alive)),
        synchronize_options);
  }
}

#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_CHROMEOS)
void ShowNonclosableAppToast(const web_app::WebAppRegistrar& registrar,
                             const webapps::AppId& app_id) {
  ash::ShowNonclosableAppToast(app_id, registrar.GetAppShortName(app_id));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace

// static
std::unique_ptr<WebAppUiManager> WebAppUiManager::Create(Profile* profile) {
  return std::make_unique<WebAppUiManagerImpl>(profile);
}

// static
void WebAppUiManager::TriggerInstallNotSupportedDialog(
    content::WebContents* web_contents,
    Profile* profile,
    base::OnceClosure callback) {
  NotSupportedReason reason;
  if (profile->IsGuestSession()) {
    reason = NotSupportedReason::kGuestMode;
  } else if (profile->IsOffTheRecord()) {
    reason = NotSupportedReason::kOffTheRecord;
  } else if (!web_app::IsWebAppInstallByUserPolicyEnabled(profile)) {
    reason = NotSupportedReason::kPolicyDisabled;
  } else {
    NOTREACHED();
  }
  ShowInstallNotSupportedDialog(web_contents, profile, reason,
                                std::move(callback));
}

WebAppUiManagerImpl::WebAppUiManagerImpl(Profile* profile)
    : profile_(profile) {}

WebAppUiManagerImpl::~WebAppUiManagerImpl() = default;

void WebAppUiManagerImpl::Start() {
  DCHECK(!started_);
  started_ = true;

  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this](BrowserWindowInterface* browser_window_interface) {
        if (!IsBrowserForInstalledApp(browser_window_interface)) {
          return true;
        }

        ++num_windows_for_apps_map_[GetAppIdForBrowser(
            browser_window_interface)];
        return true;
      });

  extensions::ExtensionSystem::Get(profile_)->ready().Post(
      FROM_HERE, base::BindOnce(&WebAppUiManagerImpl::OnExtensionSystemReady,
                                weak_ptr_factory_.GetWeakPtr()));

  browser_collection_observation_.Observe(
      GlobalBrowserCollection::GetInstance());
}

void WebAppUiManagerImpl::Shutdown() {
  browser_collection_observation_.Reset();
  started_ = false;
}

WebAppUiManagerImpl* WebAppUiManagerImpl::AsImpl() {
  return this;
}

size_t WebAppUiManagerImpl::GetNumWindowsForApp(const webapps::AppId& app_id) {
  DCHECK(started_);

  auto it = num_windows_for_apps_map_.find(app_id);
  if (it == num_windows_for_apps_map_.end()) {
    return 0;
  }

  return it->second;
}

void WebAppUiManagerImpl::CloseAppWindows(const webapps::AppId& app_id) {
  DCHECK(started_);

  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&app_id](BrowserWindowInterface* browser_window_interface) {
        const auto* const app_controller =
            AppBrowserController::From(browser_window_interface);
        if (app_controller && app_controller->app_id() == app_id) {
          UnloadController* const unload_controller =
              UnloadController::From(browser_window_interface);
          if (unload_controller) {
            unload_controller->set_force_skip_warning_user_on_close(true);
          }
          browser_window_interface->GetWindow()->Close();
        }
        return true;
      });
}

void WebAppUiManagerImpl::NotifyOnAllAppWindowsClosed(
    const webapps::AppId& app_id,
    base::OnceClosure callback) {
  DCHECK(started_);

  const size_t num_windows_for_app = GetNumWindowsForApp(app_id);
  if (num_windows_for_app == 0) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(callback));
    return;
  }

  windows_closed_requests_map_[app_id].push_back(std::move(callback));
}

void WebAppUiManagerImpl::OnExtensionSystemReady() {
  extensions::ExtensionSystem::Get(profile_)
      ->app_sorting()
      ->InitializePageOrdinalMapFromWebApps();
}

bool WebAppUiManagerImpl::CanAddAppToQuickLaunchBar() const {
#if BUILDFLAG(IS_CHROMEOS)
  return true;
#else
  return false;
#endif
}

void WebAppUiManagerImpl::AddAppToQuickLaunchBar(const webapps::AppId& app_id) {
  DCHECK(CanAddAppToQuickLaunchBar());
#if BUILDFLAG(IS_CHROMEOS)
  // ChromeShelfController does not exist in unit tests.
  if (auto* controller = ChromeShelfController::instance()) {
    PinAppWithIDToShelf(app_id);
    controller->UpdateV1AppState(app_id);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
}

bool WebAppUiManagerImpl::IsAppInQuickLaunchBar(
    const webapps::AppId& app_id) const {
  DCHECK(CanAddAppToQuickLaunchBar());
#if BUILDFLAG(IS_CHROMEOS)
  // ChromeShelfController does not exist in unit tests.
  if (auto* controller = ChromeShelfController::instance()) {
    return controller->shelf_model()->IsAppPinned(app_id);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
  return false;
}

bool WebAppUiManagerImpl::IsAppMigrationSuggested(
    BrowserWindowInterface* window) const {
  WebAppBrowserController* controller = WebAppBrowserController::From(window);
  if (!controller) {
    return false;
  }
  return controller->HasPendingMigration();
}

bool WebAppUiManagerImpl::IsAppMigrationDialogShowing(
    BrowserWindowInterface* window) const {
  WebAppBrowserController* controller = WebAppBrowserController::From(window);
  if (!controller) {
    return false;
  }
#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) || \
    BUILDFLAG(IS_MAC)
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(window);
  return browser_view &&
         browser_view->GetProperty(kIsPwaUpdateDialogShowingKey) &&
         controller->HasPendingMigration();
#else
  return false;
#endif
}

bool WebAppUiManagerImpl::CanReparentAppTabToWindow(
    const webapps::AppId& app_id,
    bool shortcut_created,
    content::WebContents* web_contents) const {
  CHECK(web_contents);
  const WebAppTabHelper* tab_helper =
      WebAppTabHelper::FromWebContents(web_contents);
  bool is_in_app_window = false;
  if (tab_helper) {
    // The tab helper doesn't exist in unit tests.
    is_in_app_window = tab_helper->is_in_app_window();
  }
  // App-to-app reparenting is not currently supported.
  if (is_in_app_window) {
    return false;
  }
#if BUILDFLAG(IS_MAC)
  // On macOS it is only possible to reparent the window when the shortcut (app
  // shim) was created. See https://crbug.com/40606764.
  return shortcut_created;
#else
  return true;
#endif
}

BrowserWindowInterface* WebAppUiManagerImpl::ReparentAppTabToWindow(
    content::WebContents* contents,
    const webapps::AppId& app_id,
    bool shortcut_created) {
  DCHECK(CanReparentAppTabToWindow(app_id, shortcut_created, contents));
  // Reparent the tab into an app window immediately.
  return ReparentWebContentsIntoAppBrowser(contents, app_id);
}

BrowserWindowInterface* WebAppUiManagerImpl::ReparentAppTabToWindow(
    content::WebContents* contents,
    const webapps::AppId& app_id,
    base::OnceCallback<void(content::WebContents*)> completion_callback) {
  return ReparentWebContentsIntoAppBrowser(contents, app_id,
                                           std::move(completion_callback));
}

void WebAppUiManagerImpl::ShowWebAppFileLaunchDialog(
    const std::vector<base::FilePath>& file_paths,
    const webapps::AppId& app_id,
    WebAppLaunchAcceptanceCallback launch_callback) {
  ::web_app::ShowWebAppFileLaunchDialog(file_paths, profile_, app_id,
                                        std::move(launch_callback));
}

void WebAppUiManagerImpl::ShowWebAppProtocolLaunchDialog(
    const GURL& protocol_url,
    const webapps::AppId& app_id,
    WebAppLaunchAcceptanceCallback launch_callback) {
  ::web_app::ShowWebAppProtocolLaunchDialog(protocol_url, profile_, app_id,
                                            std::move(launch_callback));
}

void WebAppUiManagerImpl::ShowSubAppsInstallDialog(
    content::WebContents* initiating_web_contents,
    const std::vector<std::unique_ptr<WebAppInstallInfo>>& sub_apps,
    const webapps::AppId& parent_app_id,
    base::OnceCallback<void(bool)> callback) {
  std::string parent_app_name = WebAppProvider::GetForWebApps(profile_)
                                    ->registrar_unsafe()
                                    .GetAppShortName(parent_app_id);
  web_app::ShowSubAppsInstallDialog(initiating_web_contents, sub_apps,
                                    parent_app_name, parent_app_id,
                                    std::move(callback));
}

void WebAppUiManagerImpl::ShowWebAppSettings(const webapps::AppId& app_id) {
  WebAppProvider* provider = WebAppProvider::GetForWebApps(profile_);
  if (!provider) {
    return;
  }

  GURL start_url = provider->registrar_unsafe().GetAppStartUrl(app_id);
  if (!start_url.is_valid()) {
    return;
  }

  chrome::ShowSiteSettings(profile_, start_url);
}

void WebAppUiManagerImpl::LaunchWebApp(apps::AppLaunchParams params,
                                       LaunchWebAppWindowSetting launch_setting,
                                       Profile& profile,
                                       LaunchWebAppDebugValueCallback callback,
                                       WithAppResources& lock) {
  ::web_app::LaunchWebApp(std::move(params), launch_setting, profile, lock,
                          std::move(callback));
}

#if BUILDFLAG(IS_CHROMEOS)
void WebAppUiManagerImpl::MigrateLauncherState(
    const webapps::AppId& from_app_id,
    const webapps::AppId& to_app_id,
    base::OnceClosure callback) {
  auto* app_list_syncable_service =
      app_list::AppListSyncableServiceFactory::GetForProfile(profile_);
  bool to_app_in_shelf =
      app_list_syncable_service->GetPinPosition(to_app_id).IsValid();
  // If the new app is already pinned to the shelf don't transfer UI prefs
  // across as that could cause it to become unpinned.
  if (!to_app_in_shelf) {
    app_list_syncable_service->TransferItemAttributes(from_app_id, to_app_id);
  }
  std::move(callback).Run();
}

void WebAppUiManagerImpl::DisplayRunOnOsLoginNotification(
    const base::flat_map<webapps::AppId, RoolNotificationBehavior>& apps,
    base::WeakPtr<Profile> profile) {
  web_app::DisplayRunOnOsLoginNotification(apps, profile);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

void WebAppUiManagerImpl::NotifyAppRelaunchState(
    const webapps::AppId& placeholder_app_id,
    const webapps::AppId& final_app_id,
    const std::u16string& final_app_name,
    base::WeakPtr<Profile> profile,
    AppRelaunchState relaunch_state) {
#if BUILDFLAG(IS_CHROMEOS)
  web_app::NotifyAppRelaunchState(placeholder_app_id, final_app_id,
                                  final_app_name, std::move(profile),
                                  relaunch_state);
#endif  // BUILDFLAG(IS_CHROMEOS)
}

content::WebContents* WebAppUiManagerImpl::CreateNewTab() {
  NavigateParams params(profile_, GURL(url::kAboutBlankURL),
                        ui::PAGE_TRANSITION_FROM_API);
  base::WeakPtr<content::NavigationHandle> handle = Navigate(&params);
  if (handle) {
    return handle->GetWebContents();
  }
  return nullptr;
}

bool WebAppUiManagerImpl::IsWebContentsActiveTabInBrowser(
    content::WebContents* web_contents) {
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents);
  return browser && browser->GetTabStripModel() &&
         browser->GetTabStripModel()->GetActiveWebContents() == web_contents;
}

void WebAppUiManagerImpl::TriggerInstallDialog(
    content::WebContents* web_contents,
    webapps::WebappInstallSource source,
    InstallCallback callback) {
  web_app::CreateWebAppFromManifest(web_contents, source, std::move(callback));
}

void WebAppUiManagerImpl::TriggerInstallDialogForBackgroundInstall(
    content::WebContents* initiating_web_contents,
    std::unique_ptr<webapps::MlInstallOperationTracker> tracker,
    const GURL& install_url,
    const std::optional<GURL>& manifest_id,
    const GURL& last_committed_url,
    InstallCallback callback) {
  web_app::CreateWebAppForBackgroundInstall(
      initiating_web_contents, std::move(tracker), install_url, manifest_id,
      last_committed_url, std::move(callback));
}

void WebAppUiManagerImpl::TriggerInstallDialogForManifestInstall(
    content::WebContents* initiating_web_contents,
    base::WeakPtr<content::Page> initiating_page,
    std::unique_ptr<webapps::MlInstallOperationTracker> tracker,
    blink::mojom::ManifestPtr manifest,
    const GURL& manifest_url,
    const GURL& requesting_page_url,
    InstallCallback callback) {
  web_app::CreateWebAppForManifestInstall(
      initiating_web_contents, std::move(initiating_page), std::move(tracker),
      std::move(manifest), manifest_url, requesting_page_url,
      std::move(callback));
}

void WebAppUiManagerImpl::TriggerLaunchDialogForBackgroundInstall(
    content::WebContents* initiating_web_contents,
    const webapps::AppId& app_id,
    Profile* profile,
    const std::string& app_name,
    const SkBitmap& icon,
    WebInstallAppLaunchAcceptanceCallback callback) {
  ShowWebInstallAppLaunchDialog(initiating_web_contents, app_id, profile,
                                app_name, icon, std::move(callback));
}

void WebAppUiManagerImpl::PresentUserUninstallDialog(
    const webapps::AppId& app_id,
    webapps::WebappUninstallSource uninstall_source,
    BrowserWindow* parent_window,
    UninstallCompleteCallback callback) {
  PresentUserUninstallDialog(
      app_id, uninstall_source,
      parent_window ? parent_window->GetNativeWindow() : gfx::NativeWindow(),
      std::move(callback), base::DoNothing());
}

void WebAppUiManagerImpl::PresentUserUninstallDialog(
    const webapps::AppId& app_id,
    webapps::WebappUninstallSource uninstall_source,
    gfx::NativeWindow native_window,
    UninstallCompleteCallback callback) {
  PresentUserUninstallDialog(app_id, uninstall_source, native_window,
                             std::move(callback), base::DoNothing());
}

void WebAppUiManagerImpl::PresentUserUninstallDialog(
    const webapps::AppId& app_id,
    webapps::WebappUninstallSource uninstall_source,
    gfx::NativeWindow parent_window,
    UninstallCompleteCallback uninstall_complete_callback,
    UninstallScheduledCallback uninstall_scheduled_callback) {
  std::unique_ptr<ui::NativeWindowTracker> parent_window_tracker;
  if (parent_window) {
    parent_window_tracker = ui::NativeWindowTracker::Create(parent_window);
  }

  if (parent_window && parent_window_tracker->WasNativeWindowDestroyed()) {
    OnUninstallCancelled(std::move(uninstall_complete_callback),
                         std::move(uninstall_scheduled_callback));
    return;
  }

  WebAppProvider* provider = WebAppProvider::GetForWebApps(profile_);
  CHECK(provider);

  auto state =
      base::MakeRefCounted<base::RefCountedData<UninstallDialogState>>();

  std::vector<webapps::AppId> sub_app_ids =
      provider->registrar_unsafe().GetAllSubAppIds(app_id);

  auto barrier = base::BarrierClosure(
      1 + sub_app_ids.size(),
      base::BindOnce(
          [](base::WeakPtr<WebAppUiManagerImpl> ui_manager,
             const webapps::AppId& app_id,
             webapps::WebappUninstallSource uninstall_source,
             gfx::NativeWindow parent_window,
             std::unique_ptr<ui::NativeWindowTracker> parent_window_tracker,
             UninstallCompleteCallback complete_callback,
             UninstallScheduledCallback uninstall_scheduled_callback,
             scoped_refptr<base::RefCountedData<UninstallDialogState>> state) {
            if (ui_manager) {
              ui_manager->OnAllIconsReadForUninstall(
                  app_id, uninstall_source, parent_window,
                  std::move(parent_window_tracker),
                  std::move(complete_callback),
                  std::move(uninstall_scheduled_callback),
                  std::move(state->data.main_icon_metadata),
                  std::move(state->data.sub_apps));
            }
          },
          weak_ptr_factory_.GetWeakPtr(), app_id, uninstall_source,
          parent_window, std::move(parent_window_tracker),
          std::move(uninstall_complete_callback),
          std::move(uninstall_scheduled_callback), state));

  provider->icon_manager().ReadTrustedIconsWithFallbackToManifestIcons(
      app_id,
      provider->registrar_unsafe().GetAppTrustedIconSizesFallbackToUntrusted(
          app_id),
      IconPurpose::ANY,
      base::BindOnce(
          [](scoped_refptr<base::RefCountedData<UninstallDialogState>> state,
             base::OnceClosure done, IconMetadataFromDisk icon_metadata) {
            state->data.main_icon_metadata = std::move(icon_metadata);
            std::move(done).Run();
          },
          state, barrier));

  for (const webapps::AppId& sub_app_id : sub_app_ids) {
    std::u16string sub_app_name = base::UTF8ToUTF16(
        provider->registrar_unsafe().GetAppShortName(sub_app_id));

    provider->icon_manager().ReadTrustedIconsWithFallbackToManifestIcons(
        sub_app_id,
        provider->registrar_unsafe().GetAppTrustedIconSizesFallbackToUntrusted(
            sub_app_id),
        web_app::IconPurpose::ANY,
        base::BindOnce(
            [](scoped_refptr<base::RefCountedData<UninstallDialogState>> state,
               base::OnceClosure done, std::u16string sub_app_name,
               IconMetadataFromDisk sub_app_icon_metadata) {
              state->data.sub_apps.emplace_back(
                  std::move(sub_app_name), std::move(sub_app_icon_metadata));
              std::move(done).Run();
            },
            state, barrier, sub_app_name));
  }
}

void WebAppUiManagerImpl::UninstallAppSilentlyForMigration(
    const webapps::AppId& app_id) {
  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
  proxy->UninstallSilently(app_id, apps::UninstallSource::kMigration);
}

void WebAppUiManagerImpl::ShowProfileErrorDialogForCorruptDB() {
  ::ShowProfileErrorDialog(ProfileErrorType::DB_WEB_APP_DATA,
                           IDS_COULDNT_OPEN_PROFILE_ERROR,
                           "Web App database corruption detected.");
}

void WebAppUiManagerImpl::ShowIntentPicker(
    const GURL& url,
    content::WebContents* web_contents,
    ShowIntentPickerBubbleCallback callback,
    std::optional<webapps::AppId> scoped_app_id) {
  tabs::TabInterface* tab =
      tabs::TabInterface::MaybeGetFromContents(web_contents);
  IntentPickerTabHelper* intent_picker_tab_helper =
      tab ? IntentPickerTabHelper::From(tab) : nullptr;

  if (!intent_picker_tab_helper) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), /*launched=*/false));
    return;
  }
  intent_picker_tab_helper->ShowIntentPickerBubbleOrLaunchApp(
      url, /*always_show =*/true, std::move(callback),
      std::move(scoped_app_id));
}

void WebAppUiManagerImpl::LaunchOrFocusIsolatedWebAppInstaller(
    const base::FilePath& bundle_path) {
  auto it = active_installers_.find(bundle_path);

  if (it == active_installers_.end()) {
    // If no installer exists for the path, we create a new coordinator
    active_installers_[bundle_path] = ::web_app::LaunchIsolatedWebAppInstaller(
        profile_, bundle_path,
        base::BindOnce(&WebAppUiManagerImpl::OnIsolatedWebAppInstallerClosed,
                       weak_ptr_factory_.GetWeakPtr(), bundle_path));
  } else {
    // If an installer already exists for |path|, we focus the existing
    // installer.
    FocusIsolatedWebAppInstaller(it->second);
  }
}

void WebAppUiManagerImpl::OnIsolatedWebAppInstallerClosed(
    base::FilePath bundle_path) {
  auto it = active_installers_.find(bundle_path);
  CHECK(it != active_installers_.end())
      << "Installer with path " << bundle_path
      << " is being closed, but it is not found in the list of active "
         "installers.";
  active_installers_.erase(it);
}

void WebAppUiManagerImpl::MaybeCreateEnableSupportedLinksInfobar(
    content::WebContents* web_contents,
    const std::string& launch_name) {
#if !BUILDFLAG(IS_CHROMEOS)
  std::unique_ptr<apps::EnableLinkCapturingInfoBarDelegate> delegate =
      apps::EnableLinkCapturingInfoBarDelegate::MaybeCreate(web_contents,
                                                            launch_name);
  if (delegate) {
    infobars::ContentInfoBarManager::FromWebContents(web_contents)
        ->AddInfoBar(CreateConfirmInfoBar(std::move(delegate)));
  }
#endif  // !BUILDFLAG(IS_CHROMEOS)
}

void WebAppUiManagerImpl::MaybeCreateWebAppBlockedMigrationInfoBar(
    content::WebContents* web_contents,
    base::OnceClosure on_dismiss_callback) {
  WebAppBlockedMigrationInfoBarDelegate::Create(web_contents,
                                                std::move(on_dismiss_callback));
}

void WebAppUiManagerImpl::MaybeRemoveWebAppBlockedMigrationInfoBar(
    content::WebContents* web_contents) {
  WebAppBlockedMigrationInfoBarDelegate::Remove(web_contents);
}

void WebAppUiManagerImpl::MaybeShowIPHPromoForAppsLaunchedViaLinkCapturing(
    BrowserWindowInterface* browser,
    Profile* profile,
    const std::string& app_id) {
  if (!apps::features::IsNavigationCapturingOnByDefault()) {
    return;
  }

  WebAppProvider* provider = WebAppProvider::GetForWebApps(profile);
  CHECK(provider);

#if BUILDFLAG(IS_CHROMEOS)
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
    return;
  }
  apps::AppServiceProxy* proxy =
      apps::AppServiceProxyFactory::GetForProfile(profile);
  if (!proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id)) {
    return;
  }
#else
  if (!provider->registrar_unsafe().CapturesLinksInScope(app_id)) {
    return;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  BrowserWindowInterface* const app_browser =
      browser ? browser : AppBrowserController::FindForWebApp(*profile, app_id);
  if (!app_browser) {
    return;
  }

  if (WebAppPrefGuardrails::GetForNavigationCapturingIph(
          app_browser->GetProfile()->GetPrefs())
          .IsBlockedByGuardrails(app_id)) {
    return;
  }

  web_app::PostCallbackOnBrowserActivation(
      app_browser, kToolbarAppMenuButtonElementId,
      base::BindOnce(
          &WebAppUiManagerImpl::ShowIPHPromoForAppsLaunchedViaLinkCapturing,
          weak_ptr_factory_.GetWeakPtr(), app_browser, app_id));
}

void WebAppUiManagerImpl::OnBrowserCreated(BrowserWindowInterface* browser) {
  DCHECK(started_);
  if (!IsBrowserForInstalledApp(browser)) {
    return;
  }

  ++num_windows_for_apps_map_[GetAppIdForBrowser(browser)];

#if BUILDFLAG(IS_CHROMEOS)
  // TODO(crbug.com/452120900): TabStripModel auto-unregistered by dtor
  browser->GetTabStripModel()->AddObserver(this);
  browser_close_cancelled_subscriptions_.push_back(
      browser->RegisterBrowserCloseCancelled(
          base::BindRepeating(&WebAppUiManagerImpl::OnBrowserCloseCancelled,
                              base::Unretained(this))));
#endif  // BUILDFLAG(IS_CHROMEOS)
}

void WebAppUiManagerImpl::OnBrowserClosed(BrowserWindowInterface* browser) {
  DCHECK(started_);
  if (!IsBrowserForInstalledApp(browser)) {
    return;
  }

  const auto& app_id = GetAppIdForBrowser(browser);

  size_t& num_windows_for_app = num_windows_for_apps_map_[app_id];
  DCHECK_GT(num_windows_for_app, 0u);
  --num_windows_for_app;

  if (num_windows_for_app > 0) {
    return;
  }

  auto it = windows_closed_requests_map_.find(app_id);
  if (it == windows_closed_requests_map_.end()) {
    return;
  }

  for (auto& callback : it->second) {
    std::move(callback).Run();
  }

  windows_closed_requests_map_.erase(app_id);
}

#if BUILDFLAG(IS_CHROMEOS)
void WebAppUiManagerImpl::OnTabCloseCancelled(const tabs::TabInterface* tab) {
  const content::WebContents* contents = tab->GetContents();
  CHECK(contents);
  const WebAppTabHelper* tab_helper =
      WebAppTabHelper::FromWebContents(contents);
  if (!tab_helper || !tab_helper->window_app_id()) {
    return;
  }

  ShowNonclosableAppToast(
      WebAppProvider::GetForWebApps(profile_)->registrar_unsafe(),
      *tab_helper->window_app_id());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_WIN)
void WebAppUiManagerImpl::UninstallWebAppFromStartupSwitch(
    const webapps::AppId& app_id) {
  WebAppProvider* provider = WebAppProvider::GetForWebApps(profile_);
  // ScopedKeepAlive not only keeps the process from terminating early
  // during uninstall, it also ensures the process will terminate in the next
  // message loop if there are no active browser windows.
  provider->on_registry_ready().Post(
      FROM_HERE, base::BindOnce(&UninstallWebAppWithDialogFromStartupSwitch,
                                std::make_unique<ScopedKeepAlive>(
                                    KeepAliveOrigin::WEB_APP_UNINSTALL,
                                    KeepAliveRestartOption::DISABLED),
                                app_id, provider));
}
#endif  //  BUILDFLAG(IS_WIN)

bool WebAppUiManagerImpl::IsBrowserForInstalledApp(
    const BrowserWindowInterface* browser) const {
  if (browser->GetProfile() != profile_) {
    return false;
  }

  if (!web_app::AppBrowserController::IsWebApp(browser)) {
    return false;
  }

  return true;
}

webapps::AppId WebAppUiManagerImpl::GetAppIdForBrowser(
    const BrowserWindowInterface* browser) const {
  return web_app::AppBrowserController::From(browser)->app_id();
}

void WebAppUiManagerImpl::OnAllIconsReadForUninstall(
    const webapps::AppId& app_id,
    webapps::WebappUninstallSource uninstall_source,
    gfx::NativeWindow parent_window,
    std::unique_ptr<ui::NativeWindowTracker> parent_window_tracker,
    UninstallCompleteCallback complete_callback,
    UninstallScheduledCallback uninstall_scheduled_callback,
    IconMetadataFromDisk icon_metadata,
    std::vector<SubAppUninstallMetadata> sub_apps) {
  if (parent_window && parent_window_tracker->WasNativeWindowDestroyed()) {
    OnUninstallCancelled(std::move(complete_callback),
                         std::move(uninstall_scheduled_callback));
    return;
  }

  // If icon reading returned an empty map because of disk I/O operations
  // failing from the WebAppIconManager side, fallback to generating monogram
  // icons.
  if (icon_metadata.icons_map.empty()) {
    WebAppProvider* provider = WebAppProvider::GetForWebApps(profile_);
    CHECK(provider);
    WebAppRegistrar& registrar = provider->registrar_unsafe();

    // If the app was unregistered asynchronously during icon reading, we cannot
    // construct fallback icons or display the dialog. Cancel uninstallation.
    if (!registrar.GetInstallState(app_id)) {
      OnUninstallCancelled(std::move(complete_callback),
                           std::move(uninstall_scheduled_callback));
      return;
    }

    std::string name_to_use_for_icon = registrar.GetAppShortName(app_id);
    if (name_to_use_for_icon.empty()) {
      name_to_use_for_icon = registrar.GetAppStartUrl(app_id).spec();
    }

    for (const auto& [size, bitmap] :
         GenerateIcons(base::UTF8ToUTF16(name_to_use_for_icon))) {
      icon_metadata.icons_map[size] = bitmap;
    }
  }
  // If sub app icon reading returned an empty map ,
  // fallback to generating monogram icons.
  for (auto& sub_app : sub_apps) {
    if (sub_app.icon_metadata.icons_map.empty()) {
      for (const auto& [size, bitmap] : GenerateIcons(sub_app.app_name)) {
        sub_app.icon_metadata.icons_map[size] = bitmap;
      }
    }
  }

  // Sort sub apps by app name because
  // current order is random due to entries being added
  // to vector after icon reading is done.
  std::sort(sub_apps.begin(), sub_apps.end(), [](const auto& a, const auto& b) {
    return a.app_name < b.app_name;
  });

  ShowWebAppUninstallDialog(
      profile_, app_id, uninstall_source, parent_window,
      std::move(icon_metadata), std::move(sub_apps),
      base::BindOnce(&WebAppUiManagerImpl::ScheduleUninstallIfUserRequested,
                     weak_ptr_factory_.GetWeakPtr(), app_id, uninstall_source,
                     std::move(complete_callback),
                     std::move(uninstall_scheduled_callback)));
}

void WebAppUiManagerImpl::ScheduleUninstallIfUserRequested(
    const webapps::AppId& app_id,
    webapps::WebappUninstallSource uninstall_source,
    UninstallCompleteCallback complete_callback,
    UninstallScheduledCallback uninstall_scheduled_callback,
    web_app::UninstallUserOptions uninstall_options) {
  WebAppProvider* provider = WebAppProvider::GetForWebApps(profile_);
  CHECK(provider);

  const bool uninstall_scheduled =
      uninstall_options.user_wants_uninstall &&
      provider->registrar_unsafe().CanUserUninstallWebApp(app_id);
  std::move(uninstall_scheduled_callback).Run(uninstall_scheduled);
  if (!uninstall_scheduled) {
    std::move(complete_callback).Run(webapps::UninstallResultCode::kCancelled);
    return;
  }

  UninstallCompleteCallback final_callback;
  if (uninstall_options.clear_site_data) {
    CHECK(uninstall_options.user_wants_uninstall);
    const GURL app_start_url =
        provider->registrar_unsafe().GetAppStartUrl(app_id);
    final_callback =
        base::BindOnce(&WebAppUiManagerImpl::ClearWebAppSiteDataIfNeeded,
                       weak_ptr_factory_.GetWeakPtr(), app_start_url,
                       std::move(complete_callback));
  } else {
    final_callback = std::move(complete_callback);
  }

  provider->scheduler().RemoveUserUninstallableManagements(
      app_id, uninstall_source, std::move(final_callback));
}

void WebAppUiManagerImpl::OnUninstallCancelled(
    UninstallCompleteCallback complete_callback,
    UninstallScheduledCallback uninstall_scheduled_callback) {
  std::move(uninstall_scheduled_callback).Run(false);
  std::move(complete_callback).Run(webapps::UninstallResultCode::kCancelled);
}

void WebAppUiManagerImpl::ClearWebAppSiteDataIfNeeded(
    const GURL app_start_url,
    UninstallCompleteCallback uninstall_complete_callback,
    webapps::UninstallResultCode uninstall_code) {
  // This callback should be run at the very end of the uninstallation + site
  // data removal process (if any).
  base::OnceClosure final_uninstall_callback =
      base::BindOnce(std::move(uninstall_complete_callback), uninstall_code);

  // Only clear site data if the uninstallation has succeeded, i.e. either the
  // app has been uninstalled completely, or it was previously uninstalled but
  // some data had been left over.
  if (webapps::UninstallSucceeded(uninstall_code)) {
    content::ClearSiteData(profile_->GetWeakPtr(),
                           /*storage_partition_config=*/std::nullopt,
                           url::Origin::Create(app_start_url),
                           content::ClearSiteDataTypeSet::All(),
                           /*storage_buckets_to_remove=*/{},
                           /*avoid_closing_connections=*/false,
                           /*cookie_partition_key=*/std::nullopt,
                           /*storage_key=*/std::nullopt,
                           /*partitioned_state_allowed_only=*/false,
                           std::move(final_uninstall_callback));
  } else {
    std::move(final_uninstall_callback).Run();
  }
}

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
    BUILDFLAG(IS_CHROMEOS)

const base::Feature& GetPromoFeatureEngagementFromBrowser(
    const BrowserWindowInterface* browser) {
  return web_app::AppBrowserController::IsWebApp(browser)
             ? feature_engagement::kIPHDesktopPWAsLinkCapturingLaunch
             : feature_engagement::kIPHDesktopPWAsLinkCapturingLaunchAppInTab;
}

void WebAppUiManagerImpl::ShowIPHPromoForAppsLaunchedViaLinkCapturing(
    BrowserWindowInterface* browser,
    const webapps::AppId& app_id,
    bool is_activated) {
  if (!browser || !is_activated) {
    return;
  }

  const auto& feature = GetPromoFeatureEngagementFromBrowser(browser);
  user_education::FeaturePromoParams promo_params(feature, app_id);
  promo_params.close_callback =
      base::BindOnce(&WebAppUiManagerImpl::OnIPHPromoResponseForLinkCapturing,
                     weak_ptr_factory_.GetWeakPtr(), browser, app_id);
  promo_params.show_promo_result_callback =
      base::BindOnce([](user_education::FeaturePromoResult result) {
        if (result) {
          base::RecordAction(
              base::UserMetricsAction("LinkCapturingIPHAppBubbleShown"));
        }
      });

  BrowserUserEducationInterface::From(browser)->MaybeShowFeaturePromo(
      std::move(promo_params));

  // This is only needed for IPH bubbles that are anchored to a tab in a
  // browser. App browsers don't require this logic since tab switching and
  // navigating to another page isn't something to worry about in an app
  // window.
  if (&feature ==
      &feature_engagement::kIPHDesktopPWAsLinkCapturingLaunchAppInTab) {
    content::WebContents* const active_contents =
        browser->GetTabStripModel()->GetActiveWebContents();
    if (!active_contents) {
      return;
    }
    WebAppTabHelper* const tab_helper =
        WebAppTabHelper::FromWebContents(active_contents);
    CHECK(tab_helper);
    tab_helper->SetCallbackToRunOnTabChanges(base::BindOnce(
        &WebAppUiManagerImpl::OnTabChangedDuringIph,
        weak_ptr_factory_.GetWeakPtr(), base::Unretained(browser)));
  }
}

void WebAppUiManagerImpl::OnIPHPromoResponseForLinkCapturing(
    BrowserWindowInterface* browser,
    const webapps::AppId& app_id) {
  if (!browser) {
    return;
  }

  const auto* const feature_promo_controller =
      UserEducationServiceFactory::GetForBrowserContext(browser->GetProfile())
          ->GetFeaturePromoController(base::PassKey<WebAppUiManagerImpl>());
  if (!feature_promo_controller) {
    return;
  }

  user_education::FeaturePromoClosedReason close_reason;
  feature_promo_controller->HasPromoBeenDismissed(
      {GetPromoFeatureEngagementFromBrowser(browser), app_id}, &close_reason);
  switch (close_reason) {
    case user_education::FeaturePromoClosedReason::kAction:
      base::RecordAction(
          base::UserMetricsAction("LinkCapturingIPHAppBubbleAccepted"));
      WebAppPrefGuardrails::GetForNavigationCapturingIph(
          browser->GetProfile()->GetPrefs())
          .RecordAccept(app_id);
      break;
    case user_education::FeaturePromoClosedReason::kDismiss:
    case user_education::FeaturePromoClosedReason::kCancel:
    // This is needed if the promo is cancelled automatically by the
    // `WebAppTabHelper`.
    case user_education::FeaturePromoClosedReason::kFeatureEngaged:
      base::RecordAction(
          base::UserMetricsAction("LinkCapturingIPHAppBubbleNotAccepted"));
      WebAppPrefGuardrails::GetForNavigationCapturingIph(
          browser->GetProfile()->GetPrefs())
          .RecordDismiss(app_id, base::Time::Now());
      break;
    default:
      break;
  }
}

void WebAppUiManagerImpl::OnTabChangedDuringIph(
    BrowserWindowInterface* browser) {
  const auto& feature =
      feature_engagement::kIPHDesktopPWAsLinkCapturingLaunchAppInTab;
  auto* const user_education = BrowserUserEducationInterface::From(browser);
  if (user_education->IsFeaturePromoQueued(feature)) {
    user_education->AbortFeaturePromo(feature);
  } else if (user_education->IsFeaturePromoActive(feature)) {
    user_education->NotifyFeaturePromoFeatureUsed(
        feature, FeaturePromoFeatureUsedAction::kClosePromoIfPresent);
  }
}

#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) ||
        // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
void WebAppUiManagerImpl::OnBrowserCloseCancelled(
    BrowserWindowInterface* browser,
    BrowserWindowInterface::ClosingStatus closing_status) {
  DCHECK(started_);
  if (!IsBrowserForInstalledApp(browser) ||
      closing_status !=
          BrowserWindowInterface::ClosingStatus::kDeniedByPolicy) {
    return;
  }

  ShowNonclosableAppToast(
      WebAppProvider::GetForWebApps(profile_)->registrar_unsafe(),
      GetAppIdForBrowser(browser));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

void WebAppUiManagerImpl::NotifyDidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  auto* user_data =
      WebAppLaunchNavigationHandleUserData::GetForNavigationHandle(
          *navigation_handle);
  if (!user_data) {
    return;
  }

  // Enqueue launch params once the navigation has committed and the navigation
  // was successful.
  if (navigation_handle->HasCommitted()) {
    user_data->MaybePerformAppHandlingTasksInWebContents();
  }
}

}  // namespace web_app
