910e62b5创建于 1月15日历史提交
// Copyright 2024 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/devtools/protocol/pwa_handler.h"

#include <vector>

#include "base/check.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/concurrent_callbacks.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/strcat.h"
#include "base/strings/to_string.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/web_applications/isolated_web_apps/install/isolated_web_app_installation_manager.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/os_integration/web_app_file_handler_manager.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/proto/web_app_os_integration_state.pb.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_install_params.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 "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/web_app_id.h"
#include "components/webapps/isolated_web_apps/scheme.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace {

namespace errors {

// Returns a common error when WebApp component is unavailable in the scenario,
// e.g. in incognito mode.
protocol::Response WebAppUnavailable() {
  return protocol::Response::ServerError(
      "Webapps are not available in current profile.");
}

// Returns a common error when the to-be-installed webapp has a different
// manifest id than the required.
protocol::Response InconsistentManifestId(const std::string& in_manifest_id,
                                          const std::string& url_or_appid) {
  return protocol::Response::InvalidRequest(
      base::StrCat({"Expected manifest id ", in_manifest_id,
                    " does not match input url or app id ", url_or_appid}));
}

}  // namespace errors

using FileHandlers =
    std::unique_ptr<protocol::Array<protocol::PWA::FileHandler>>;

base::expected<FileHandlers, protocol::Response> GetFileHandlersFromApp(
    const webapps::AppId app_id,
    const std::string in_manifest_id,
    web_app::AppLock& app_lock,
    base::Value::Dict& debug_value) {
  const web_app::WebApp* web_app = app_lock.registrar().GetAppById(app_id);
  if (web_app == nullptr) {
    return base::unexpected(protocol::Response::InvalidParams(
        base::StrCat({"Unknown web-app manifest id ", in_manifest_id})));
  }

  using protocol::PWA::FileHandler;
  using protocol::PWA::FileHandlerAccept;
  auto file_handlers = std::make_unique<protocol::Array<FileHandler>>();
  for (const auto& input_handler : web_app->current_os_integration_states()
                                       .file_handling()
                                       .file_handlers()) {
    auto accepts = std::make_unique<protocol::Array<FileHandlerAccept>>();
    for (const auto& input_accept : input_handler.accept()) {
      auto file_extensions = std::make_unique<protocol::Array<std::string>>();
      for (const auto& input_file_extension : input_accept.file_extensions()) {
        file_extensions->push_back(input_file_extension);
      }
      accepts->push_back(FileHandlerAccept::Create()
                             .SetMediaType(input_accept.mimetype())
                             .SetFileExtensions(std::move(file_extensions))
                             .Build());
    }
    file_handlers->push_back(FileHandler::Create()
                                 .SetAction(input_handler.action())
                                 .SetAccepts(std::move(accepts))
                                 .SetDisplayName(input_handler.display_name())
                                 .Build());
  }
  return base::ok(std::move(file_handlers));
}

// A shared way to handle the callback of WebAppUiManager::LaunchApp.
base::expected<std::string, protocol::Response> GetTargetIdFromLaunch(
    const std::string& in_manifest_id,
    const std::optional<GURL>& url,
    base::WeakPtr<Browser> browser,
    base::WeakPtr<content::WebContents> web_contents,
    apps::LaunchContainer container) {
  // The callback will always be provided with a valid Browser
  // instance, but the web_contents is associated with the newly
  // opened web app, and it can be used to indicate the success of the
  // launch operation.
  // See web_app::WebAppLaunchUtils::LaunchWebApp() for more
  // information.
  if (web_contents) {
    return content::DevToolsAgentHost::GetOrCreateForTab(web_contents.get())
        ->GetId();
  }
  std::string msg = "Failed to launch " + in_manifest_id;
  if (url) {
    msg += " from url " + url->spec();
  }
  return base::unexpected(protocol::Response::InvalidRequest(msg));
}

void OnWebBundleInstalled(
    std::unique_ptr<PWAHandler::InstallCallback> callback,
    const GURL& manifest_url,
    const GURL& web_bundle_url,
    base::expected<web_app::InstallIsolatedWebAppCommandSuccess, std::string>
        result) {
  if (result.has_value()) {
    std::move(callback)->sendSuccess();
  } else {
    std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
        base::StrCat({"Failed to install ", manifest_url.spec(), " from ",
                      web_bundle_url.spec(), ": ", result.error()})));
  }
}
}  // namespace

PWAHandler::PWAHandler(protocol::UberDispatcher* dispatcher,
                       const std::string& target_id)
    : target_id_(target_id) {
  protocol::PWA::Dispatcher::wire(dispatcher, this);
}

PWAHandler::~PWAHandler() = default;

// TODO(crbug.com/331214986): Consider if the API should allow setting a browser
// context id as the profile id to override the default behavior.
Profile* PWAHandler::GetProfile() const {
  auto host = content::DevToolsAgentHost::GetForId(target_id_);
  if (host && host->GetBrowserContext()) {
    return Profile::FromBrowserContext(host->GetBrowserContext());
  }
  return ProfileManager::GetLastUsedProfile();
}

web_app::WebAppCommandScheduler* PWAHandler::GetScheduler() const {
  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(GetProfile());
  if (provider) {
    return &provider->scheduler();
  }
  return nullptr;
}

content::WebContents* PWAHandler::GetWebContents() const {
  auto host = content::DevToolsAgentHost::GetForId(target_id_);
  if (!host) {
    return nullptr;
  }
  return host->GetWebContents();
}

void PWAHandler::GetOsAppState(
    const std::string& in_manifest_id,
    std::unique_ptr<GetOsAppStateCallback> callback) {
  Profile* profile = GetProfile();
  const webapps::AppId app_id =
      web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
  int badge_count = 0;
  {
    badging::BadgeManager* badge_manager =
        badging::BadgeManagerFactory::GetForProfile(profile);
    CHECK(badge_manager);
    std::optional<badging::BadgeManager::BadgeValue> badge =
        badge_manager->GetBadgeValue(app_id);
    if (badge && *badge) {
      badge_count = base::ClampedNumeric<int32_t>(**badge);
    }
  }
  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(profile);
  if (!provider) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  provider->scheduler().ScheduleCallbackWithResult(
      "PWAHandler::GetOsAppState", web_app::AppLockDescription(app_id),
      base::BindOnce(&GetFileHandlersFromApp, app_id, in_manifest_id),
      base::BindOnce(
          [](int badge_count, std::unique_ptr<GetOsAppStateCallback> callback,
             base::expected<FileHandlers, protocol::Response>&& file_handlers) {
            if (file_handlers.has_value()) {
              std::move(callback)->sendSuccess(
                  badge_count, std::move(file_handlers).value());
            } else {
              std::move(callback)->sendFailure(file_handlers.error());
            }
          },
          badge_count, std::move(callback)),
      base::expected<FileHandlers, protocol::Response>(
          base::unexpected(protocol::Response::ServerError(
              base::StrCat({"web-app is shutting down when querying manifest ",
                            in_manifest_id})))));
}

// Install from the manifest_id only. Require a WebContents.
void PWAHandler::InstallFromManifestId(
    const std::string& in_manifest_id,
    std::unique_ptr<InstallCallback> callback) {
  content::WebContents* contents = GetWebContents();
  if (contents == nullptr) {
    std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
        base::StrCat({"The devtools session has no associated web page when "
                      "installing ",
                      in_manifest_id})));
    return;
  }
  auto* scheduler = GetScheduler();
  if (!scheduler) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  scheduler->FetchManifestAndInstall(
      webapps::WebappInstallSource::DEVTOOLS, contents->GetWeakPtr(),
      base::BindOnce(
          [](const std::string& in_manifest_id,
             base::WeakPtr<web_app::WebAppScreenshotFetcher>,
             content::WebContents* initiator_web_contents,
             std::unique_ptr<web_app::WebAppInstallInfo> web_app_info,
             web_app::WebAppInstallationAcceptanceCallback
                 acceptance_callback) {
            // Returning false here ended up causing the kUserInstallDeclined,
            // so the error message needs to be updated accordingly.
            // TODO(crbug.com/331214986): Modify the DialogCallback to allow
            // clients providing their own error code rather than always
            // returning kUserInstallDeclined. And maybe change it to a more
            // neutral name other than "Dialog" to avoid implying the use of UI.
            const bool manifest_match =
                (web_app_info->manifest_id().spec() == in_manifest_id);
            std::move(acceptance_callback)
                .Run(manifest_match, std::move(web_app_info));
          },
          in_manifest_id),
      base::BindOnce(
          [](const std::string& in_manifest_id,
             std::unique_ptr<InstallCallback> callback,
             const webapps::AppId& app_id, webapps::InstallResultCode code) {
            if (webapps::IsSuccess(code)) {
              std::move(callback)->sendSuccess();
            } else {
              // See the comment above.
              if (code == webapps::InstallResultCode::kUserInstallDeclined) {
                return std::move(callback)->sendFailure(
                    errors::InconsistentManifestId(in_manifest_id, app_id));
              }
              std::move(callback)->sendFailure(
                  protocol::Response::InvalidRequest(
                      base::StrCat({"Failed to install ", in_manifest_id, ": ",
                                    base::ToString(code)})));
            }
          },
          in_manifest_id, std::move(callback)),
      web_app::FallbackBehavior::kCraftedManifestOnly);
}

void PWAHandler::InstallFromUrl(const std::string& in_manifest_id,
                                const std::string& in_install_url_or_bundle_url,
                                std::unique_ptr<InstallCallback> callback) {
  GURL url{in_install_url_or_bundle_url};
  // Technically unnecessary, but let's check it anyway to avoid leaking
  // unexpected schemes.
  if (!url.is_valid()) {
    std::move(callback)->sendFailure(
        protocol::Response::InvalidParams(base::StrCat(
            {"Invalid installUrlOrBundleUrl ", in_install_url_or_bundle_url})));
    return;
  }
  const auto manifest_id_url = GURL{in_manifest_id};
  if (!manifest_id_url.is_valid()) {
    std::move(callback)->sendFailure(protocol::Response::InvalidParams(
        base::StrCat({"Invalid manifestId ", in_manifest_id})));
    return;
  }

  // This is isolated web app.
  if (manifest_id_url.SchemeIs(webapps::kIsolatedAppScheme)) {
    InstallWebBundleFromUrl(manifest_id_url, url, std::move(callback));
    return;
  }

  if (!url.SchemeIsHTTPOrHTTPS()) {
    std::move(callback)->sendFailure(
        protocol::Response::MethodNotFound(base::StrCat(
            {"Installing webapp from url ", in_install_url_or_bundle_url,
             " with scheme [", url.GetScheme(), "] is not supported yet."})));
    return;
  }
  auto* scheduler = GetScheduler();
  if (!scheduler) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  scheduler->FetchInstallInfoFromInstallUrl(
      manifest_id_url, url,
      base::BindOnce(&PWAHandler::InstallFromInstallInfo,
                     weak_ptr_factory_.GetWeakPtr(), in_manifest_id,
                     in_install_url_or_bundle_url, std::move(callback)));
}

void PWAHandler::InstallWebBundleFromUrl(
    const GURL& manifest_url,
    const GURL& web_bundle_url,
    std::unique_ptr<InstallCallback> callback) {
  base::expected<web_package::SignedWebBundleId, std::string>
      expected_bundle_id =
          web_package::SignedWebBundleId::Create(manifest_url.GetHost());

  if (!expected_bundle_id.has_value()) {
    std::move(callback)->sendFailure(protocol::Response::InvalidParams(
        base::StrCat({"manifestId=[", manifest_url.spec(),
                      "] must be a valid signed web bundle id. Parse error: ",
                      expected_bundle_id.error()})));
    return;
  }
  auto& installation_manager =
      web_app::WebAppProvider::GetForWebApps(GetProfile())
          ->isolated_web_app_installation_manager();

  if (web_bundle_url.SchemeIsFile()) {
    base::FilePath file_path;
    if (!net::FileURLToFilePath(web_bundle_url, &file_path)) {
      std::move(callback)->sendFailure(protocol::Response::InvalidParams(
          base::StrCat({"installUrlOrBundleUrl should be a valid file path ",
                        web_bundle_url.spec()})));
      return;
    }

    installation_manager.InstallIsolatedWebAppFromDevModeBundle(
        file_path,
        web_app::IsolatedWebAppInstallationManager::InstallSurface::
            kDevToolsProtocol,
        base::BindOnce(&OnWebBundleInstalled, std::move(callback), manifest_url,
                       web_bundle_url),
        std::move(expected_bundle_id.value()));

  } else if (web_bundle_url.SchemeIsHTTPOrHTTPS()) {
    if (expected_bundle_id->is_for_proxy_mode()) {
      installation_manager.InstallIsolatedWebAppFromDevModeProxy(
          web_bundle_url,
          web_app::IsolatedWebAppInstallationManager::InstallSurface::
              kDevToolsProtocol,
          base::BindOnce(&OnWebBundleInstalled, std::move(callback),
                         manifest_url, web_bundle_url),
          expected_bundle_id.value());
    } else {
      installation_manager.DownloadAndInstallIsolatedWebAppFromDevModeBundle(
          web_bundle_url,
          web_app::IsolatedWebAppInstallationManager::InstallSurface::
              kDevToolsProtocol,
          base::BindOnce(&OnWebBundleInstalled, std::move(callback),
                         manifest_url, web_bundle_url),
          std::move(expected_bundle_id.value()));
    }
  } else {
    std::move(callback)->sendFailure(protocol::Response::MethodNotFound(
        base::StrCat({"Installing webapp from url ", web_bundle_url.spec(),
                      " with scheme [", web_bundle_url.GetScheme(),
                      "] is not supported yet."})));
  }
}

void PWAHandler::InstallFromInstallInfo(
    const std::string& in_manifest_id,
    const std::string& in_install_url_or_bundle_url,
    std::unique_ptr<InstallCallback> callback,
    std::unique_ptr<web_app::WebAppInstallInfo> web_app_info) {
  if (!web_app_info) {
    std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
        base::StrCat({"Couldn't fetch install info for ", in_manifest_id,
                      " from ", in_install_url_or_bundle_url})));
    return;
  }
  if (web_app_info->manifest_id().spec() != in_manifest_id) {
    std::move(callback)->sendFailure(errors::InconsistentManifestId(
        in_manifest_id, in_install_url_or_bundle_url));
    return;
  }
  auto* scheduler = GetScheduler();
  if (!scheduler) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  scheduler->InstallFromInfoWithParams(
      std::move(web_app_info), false, webapps::WebappInstallSource::DEVTOOLS,
      base::BindOnce(
          [](const std::string& in_manifest_id,
             const std::string& in_install_url_or_bundle_url,
             std::unique_ptr<InstallCallback> callback,
             const webapps::AppId& app_id, webapps::InstallResultCode code) {
            if (webapps::IsSuccess(code)) {
              std::move(callback)->sendSuccess();
            } else {
              std::move(callback)->sendFailure(
                  protocol::Response::InvalidRequest(
                      base::StrCat({"Failed to install ", in_manifest_id,
                                    " from ", in_install_url_or_bundle_url,
                                    ": ", base::ToString(code)})));
            }
          },
          in_manifest_id, in_install_url_or_bundle_url, std::move(callback)),
      // TODO(crbug.com/331214986): Create command-line flag to fake all os
      // integration for Chrome.
      web_app::WebAppInstallParams{});
}

void PWAHandler::Install(
    const std::string& in_manifest_id,
    std::optional<std::string> in_install_url_or_bundle_url,
    std::unique_ptr<InstallCallback> callback) {
  if (in_install_url_or_bundle_url) {
    InstallFromUrl(in_manifest_id,
                   std::move(in_install_url_or_bundle_url).value(),
                   std::move(callback));
  } else {
    InstallFromManifestId(in_manifest_id, std::move(callback));
  }
}

void PWAHandler::Uninstall(const std::string& in_manifest_id,
                           std::unique_ptr<UninstallCallback> callback) {
  const webapps::AppId app_id =
      web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
  auto* scheduler = GetScheduler();
  if (!scheduler) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  scheduler->RemoveUserUninstallableManagements(
      app_id, webapps::WebappUninstallSource::kDevtools,
      base::BindOnce(
          [](const std::string& in_manifest_id,
             std::unique_ptr<UninstallCallback> callback,
             webapps::UninstallResultCode result) {
            if (webapps::UninstallSucceeded(result)) {
              std::move(callback)->sendSuccess();
            } else {
              std::move(callback)->sendFailure(
                  protocol::Response::InvalidRequest(
                      base::StrCat({"Failed to uninstall ", in_manifest_id,
                                    ": ", base::ToString(result)})));
            }
          },
          in_manifest_id, std::move(callback)));
}

void PWAHandler::Launch(const std::string& in_manifest_id,
                        std::optional<std::string> in_url,
                        std::unique_ptr<LaunchCallback> callback) {
  const webapps::AppId app_id =
      web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
  const auto url =
      (in_url ? std::optional<GURL>{in_url.value()} : std::nullopt);
  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(GetProfile());
  if (!provider) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  if (url) {
    if (!url->is_valid()) {
      std::move(callback)->sendFailure(protocol::Response::InvalidParams(
          base::StrCat({"Invalid input url ", in_url.value()})));
      return;
    }

    // TODO(crbug.com/338406726): Remove after launches correctly fail when url
    // is out of scope.
    bool is_in_scope =
        provider->registrar_unsafe().IsUrlInAppExtendedScope(*url, app_id);
    if (!is_in_scope) {
      std::move(callback)->sendFailure(
          protocol::Response::InvalidParams(base::StrCat(
              {"Requested url ", url->spec(),
               " is not in the scope of the web app ", in_manifest_id})));
      return;
    }
  }
  provider->scheduler().LaunchApp(
      app_id, url,
      base::BindOnce(
          [](const std::string& in_manifest_id, const std::optional<GURL>& url,
             std::unique_ptr<LaunchCallback> callback,
             base::WeakPtr<Browser> browser,
             base::WeakPtr<content::WebContents> web_contents,
             apps::LaunchContainer container) {
            auto result = GetTargetIdFromLaunch(in_manifest_id, url, browser,
                                                web_contents, container);
            if (result.has_value()) {
              std::move(callback)->sendSuccess(std::move(result).value());
            } else {
              std::move(callback)->sendFailure(std::move(result).error());
            }
          },
          in_manifest_id, url, std::move(callback)));
}

void PWAHandler::LaunchFilesInApp(
    const std::string& in_manifest_id,
    std::unique_ptr<protocol::Array<std::string>> in_files,
    std::unique_ptr<LaunchFilesInAppCallback> callback) {
  const GURL manifest_id = GURL{in_manifest_id};
  const webapps::AppId app_id =
      web_app::GenerateAppIdFromManifestId(manifest_id);
  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(GetProfile());
  if (!provider) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }
  if (!in_files || in_files->empty()) {
    std::move(callback)->sendFailure(
        protocol::Response::InvalidParams(base::StrCat(
            {"PWA.launchFilesInApp needs input files to be opened by web app ",
             in_manifest_id})));
    return;
  }
  std::vector<base::FilePath> files{in_files->size()};
  for (const auto& file : *in_files) {
    // TODO(b/331214986): Support platform-dependent file path encoding.
    // TODO(b/331214986): May want to fail if the files do not exist.
    files.push_back(base::FilePath::FromUTF8Unsafe(file));
  }
  const web_app::WebAppFileHandlerManager::LaunchInfos launch_infos =
      provider->os_integration_manager()
          .file_handler_manager()
          .GetMatchingFileHandlerUrls(app_id, files);
  if (launch_infos.empty()) {
    std::move(callback)->sendFailure(protocol::Response::InvalidParams(
        base::StrCat({"Files are not supported by web app ", in_manifest_id})));
    return;
  }

  base::ConcurrentCallbacks<base::expected<std::string, protocol::Response>>
      concurrent;
  for (const auto& [url, paths] : launch_infos) {
    provider->scheduler().LaunchApp(
        app_id, *base::CommandLine::ForCurrentProcess(),
        /* current_directory */ base::FilePath{},
        /* protocol_handler_launch_url */ std::nullopt,
        /* file_launch_url */ url, paths,
        base::BindOnce(
            [](const std::string& in_manifest_id,
               base::OnceCallback<void(
                   base::expected<std::string, protocol::Response>)> callback,
               base::WeakPtr<Browser> browser,
               base::WeakPtr<content::WebContents> web_contents,
               apps::LaunchContainer container) {
              std::move(callback).Run(
                  GetTargetIdFromLaunch(in_manifest_id, std::optional<GURL>{},
                                        browser, web_contents, container));
            },
            in_manifest_id, concurrent.CreateCallback()));
  }
  std::move(concurrent)
      .Done(base::BindOnce(
          [](std::unique_ptr<LaunchFilesInAppCallback> callback,
             std::vector<base::expected<std::string, protocol::Response>>
                 results) {
            auto ids = std::make_unique<protocol::Array<std::string>>();
            std::string errors;
            for (auto& result : results) {
              if (result.has_value()) {
                ids->push_back(std::move(result).value());
              } else {
                errors += std::move(result).error().Message();
                errors += "; ";
              }
            }
            if (errors.empty()) {
              std::move(callback)->sendSuccess(std::move(ids));
            } else {
              std::move(callback)->sendFailure(
                  protocol::Response::InvalidRequest(std::move(errors)));
            }
          },
          std::move(callback)));
}

protocol::Response PWAHandler::OpenCurrentPageInApp(
    const std::string& in_manifest_id) {
  content::WebContents* contents = GetWebContents();
  if (contents == nullptr) {
    return protocol::Response::InvalidRequest(
        base::StrCat({"The devtools session has no associated web page when "
                      "opening ",
                      in_manifest_id, " in its app."}));
  }
  web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(GetProfile());
  if (!provider) {
    return errors::WebAppUnavailable();
  }

  const webapps::AppId app_id =
      web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
  // Since this logic is only needed on MacOS, for the sake of simplicity the
  // unsafe access of the registrar is fine at the moment instead of wrapping it
  // in a command.
  const bool shortcut_created = [provider, &app_id]() {
    auto state =
        provider->registrar_unsafe().GetAppCurrentOsIntegrationState(app_id);
    if (!state.has_value()) {
      return false;
    }
    return state->has_shortcut();
  }();
  if (!provider->ui_manager().CanReparentAppTabToWindow(
          app_id, shortcut_created, contents)) {
    return protocol::Response::InvalidParams(
        base::StrCat({"The web app ", in_manifest_id,
                      " cannot be opened in its app. Check if the app is "
                      "correctly installed."}));
  }
  Browser* browser = provider->ui_manager().ReparentAppTabToWindow(
      contents, app_id, shortcut_created);
  if (browser == nullptr) {
    return protocol::Response::InvalidRequest(base::StrCat(
        {"The current page ", contents->GetLastCommittedURL().spec(),
         " cannot be opened in the web app ", in_manifest_id}));
  }
  return protocol::Response::Success();
}

void PWAHandler::ChangeAppUserSettings(
    const std::string& in_manifest_id,
    std::optional<bool> in_link_capturing,
    std::optional<protocol::PWA::DisplayMode> in_display_mode,
    std::unique_ptr<ChangeAppUserSettingsCallback> callback) {
  const webapps::AppId app_id =
      web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});

  // Always checks the availability of web app system to ensure the consistency
  // of the API behavior.
  auto* scheduler = GetScheduler();
  if (!scheduler) {
    std::move(callback)->sendFailure(errors::WebAppUnavailable());
    return;
  }

  std::optional<web_app::mojom::UserDisplayMode> user_display_mode{};
  if (in_display_mode) {
    if (in_display_mode.value() == protocol::PWA::DisplayModeEnum::Standalone) {
      user_display_mode = web_app::mojom::UserDisplayMode::kStandalone;
    } else if (in_display_mode.value() ==
               protocol::PWA::DisplayModeEnum::Browser) {
      user_display_mode = web_app::mojom::UserDisplayMode::kBrowser;
    } else {
      std::move(callback)->sendFailure(
          protocol::Response::InvalidParams(base::StrCat(
              {"Unrecognized displayMode ", in_display_mode.value(),
               " when changing user settings of web app ", in_manifest_id})));
      return;
    }
  }

#if BUILDFLAG(IS_CHROMEOS)
  // TODO(crbug.com/339453269): Implement changeUserAppSettings/LinkCapturing on
  // ChromeOS.
  // TL:DR; the ChromeOS uses apps::AppServiceProxyFactory instead, and the
  // SetSupportedLinksPreference would associate all the supported links to the
  // app.
  if (in_link_capturing) {
    std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
        "Changing AppUserSettings/LinkCapturing on ChromeOS is not supported "
        "yet."));
    return;
  }
#endif

  base::ConcurrentCallbacks<std::optional<std::string>> concurrent;
  scheduler->ScheduleCallbackWithResult(
      // TODO(crbug.com/339453269): Find a way to forward the error of the set
      // operation back here.
      "PWAHandler::ChangeAppUserSettings", web_app::AppLockDescription(app_id),
      base::BindOnce(
          [](const webapps::AppId& app_id, web_app::AppLock& app_lock,
             base::Value::Dict& debug_value) -> std::optional<std::string> {
            // Only consider apps that are installed with or without OS
            // integration. Apps coming via sync should not be considered.
            if (app_lock.registrar().IsInstallState(
                    app_id, {web_app::proto::InstallState::
                                 INSTALLED_WITH_OS_INTEGRATION,
                             web_app::proto::InstallState::
                                 INSTALLED_WITHOUT_OS_INTEGRATION})) {
              return std::nullopt;
            }
            return "WebApp is not installed";
          },
          app_id),
      concurrent.CreateCallback(),
      /* result_on_shutdown= */
      std::optional<std::string>{std::in_place,
                                 "WebApp system is shuting down."});
  if (in_link_capturing) {
    scheduler->SetAppCapturesSupportedLinksDisableOverlapping(
        app_id, in_link_capturing.value(),
        base::BindOnce(concurrent.CreateCallback(),
                       std::optional<std::string>{}));
  }
  if (user_display_mode) {
    // TODO(crbug.com/331214986): Create command-line flag to fake all os
    // integration for Chrome.
    scheduler->SetUserDisplayMode(app_id, user_display_mode.value(),
                                  base::BindOnce(concurrent.CreateCallback(),
                                                 std::optional<std::string>{}));
  }

  std::move(concurrent)
      .Done(base::BindOnce(
          [](const std::string& in_manifest_id,
             std::unique_ptr<ChangeAppUserSettingsCallback> callback,
             std::vector<std::optional<std::string>> results) {
            std::string errors;
            for (const auto& result : results) {
              if (result) {
                errors.append(result.value()).append(";");
              }
            }
            if (errors.empty()) {
              std::move(callback)->sendSuccess();
            } else {
              std::move(callback)->sendFailure(
                  protocol::Response::InvalidRequest(base::StrCat(
                      {"Failed to change the user settings of web app ",
                       in_manifest_id, ". ", errors})));
            }
          },
          in_manifest_id, std::move(callback)));
}