chromium/chrome/browser/apps/app_shim/web_app_shim_manager_delegate_mac.cc

// Copyright 2020 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/apps/app_shim/web_app_shim_manager_delegate_mac.h"

#include <algorithm>

#include "base/barrier_closure.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "chrome/browser/apps/app_service/app_launch_params.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/browser_app_launcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_applications/web_app_dialogs.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/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_constants.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_utils.h"
#include "chrome/common/chrome_switches.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "net/base/filename_util.h"
#include "third_party/blink/public/common/custom_handlers/protocol_handler_utils.h"

namespace web_app {

namespace {

// Testing hook for BrowserAppLauncher::LaunchAppWithParams
web_app::BrowserAppLauncherForTesting& GetBrowserAppLauncherForTesting() {
  static base::NoDestructor<web_app::BrowserAppLauncherForTesting> instance;
  return *instance;
}

// Launches the app specified by `params` and `file_launches` in the given
// `profile`.
void LaunchAppWithParams(
    Profile* profile,
    apps::AppLaunchParams params,
    const WebAppFileHandlerManager::LaunchInfos& file_launches,
    base::OnceClosure launch_finished_callback) {
  if (!file_launches.empty()) {
    auto barrier_callback = base::BarrierClosure(
        file_launches.size(), std::move(launch_finished_callback));
    for (const auto& [url, files] : file_launches) {
      apps::AppLaunchParams params_copy(params.app_id, params.container,
                                        params.disposition,
                                        params.launch_source);
      params_copy.override_url = url;
      params_copy.launch_files = files;

      if (GetBrowserAppLauncherForTesting()) {
        GetBrowserAppLauncherForTesting().Run(params_copy);
        barrier_callback.Run();
      } else {
        apps::AppServiceProxyFactory::GetForProfile(profile)
            ->BrowserAppLauncher()
            ->LaunchAppWithParams(
                std::move(params_copy),
                base::IgnoreArgs<content::WebContents*>(barrier_callback));
      }
    }
    return;
  }

  if (GetBrowserAppLauncherForTesting()) {
    GetBrowserAppLauncherForTesting().Run(params);
    std::move(launch_finished_callback).Run();
  } else {
    apps::AppServiceProxyFactory::GetForProfile(profile)
        ->BrowserAppLauncher()
        ->LaunchAppWithParams(std::move(params),
                              base::IgnoreArgs<content::WebContents*>(
                                  std::move(launch_finished_callback)));
  }
}

// Cancels the launch of the app for the given `app_id`, potentially resulting
// in the app shim exiting.
void CancelAppLaunch(Profile* profile, const webapps::AppId& app_id) {
  apps::AppShimManager::Get()->OnAppLaunchCancelled(profile, app_id);
}

// Called after the user's preference has been persisted, and the OS
// has been notified of the change.
void OnPersistUserChoiceCompleted(
    apps::AppLaunchParams params,
    const WebAppFileHandlerManager::LaunchInfos& file_launches,
    Profile* profile,
    base::OnceClosure launch_finished_callback,
    bool allowed) {
  if (allowed) {
    LaunchAppWithParams(profile, std::move(params), file_launches,
                        std::move(launch_finished_callback));
  } else {
    CancelAppLaunch(profile, params.app_id);
    std::move(launch_finished_callback).Run();
  }
}

// Called after the user has dismissed the WebAppProtocolHandlerIntentPicker
// dialog.
void UserChoiceDialogCompleted(
    apps::AppLaunchParams params,
    const WebAppFileHandlerManager::LaunchInfos& file_launches,
    Profile* profile,
    base::OnceClosure launch_finished_callback,
    bool allowed,
    bool remember_user_choice) {
  std::optional<GURL> protocol_url = params.protocol_handler_launch_url;
  const bool is_file_launch = !file_launches.empty();
  webapps::AppId app_id = params.app_id;

  auto persist_done = base::BindOnce(
      &OnPersistUserChoiceCompleted, std::move(params), file_launches, profile,
      std::move(launch_finished_callback), allowed);

  if (remember_user_choice) {
    WebAppProvider* provider = WebAppProvider::GetForWebApps(profile);
    ApiApprovalState approval_state =
        allowed ? ApiApprovalState::kAllowed : ApiApprovalState::kDisallowed;
    if (protocol_url) {
      provider->scheduler().UpdateProtocolHandlerUserApproval(
          app_id, protocol_url->scheme(), approval_state,
          std::move(persist_done));
    } else {
      DCHECK(is_file_launch);
      provider->scheduler().PersistFileHandlersUserChoice(
          app_id, allowed, std::move(persist_done));
    }
  } else {
    std::move(persist_done).Run();
  }
}

}  // namespace

void SetBrowserAppLauncherForTesting(
    const BrowserAppLauncherForTesting& launcher) {
  GetBrowserAppLauncherForTesting() = launcher;
}

WebAppShimManagerDelegate::WebAppShimManagerDelegate(
    std::unique_ptr<apps::AppShimManager::Delegate> fallback_delegate)
    : fallback_delegate_(std::move(fallback_delegate)) {}

WebAppShimManagerDelegate::~WebAppShimManagerDelegate() = default;

bool WebAppShimManagerDelegate::ShowAppWindows(Profile* profile,
                                               const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id))
    return fallback_delegate_->ShowAppWindows(profile, app_id);
  // Non-legacy app windows are handled in AppShimManager.
  return false;
}

void WebAppShimManagerDelegate::CloseAppWindows(Profile* profile,
                                                const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id)) {
    fallback_delegate_->CloseAppWindows(profile, app_id);
    return;
  }
  // This is only used by legacy apps.
  // TODO(crbug.com/40902596): This seems to happen in the wild although
  // though shouldn't be possible. Once legacy apps are no longer supported all
  // this legacy app specific code should get deleted entirely.
  // NOTREACHED();
}

bool WebAppShimManagerDelegate::AppIsInstalled(Profile* profile,
                                               const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id)) {
    return fallback_delegate_->AppIsInstalled(profile, app_id);
  }
  return profile &&
         WebAppProvider::GetForWebApps(profile)->registrar_unsafe().IsInstalled(
             app_id);
}

bool WebAppShimManagerDelegate::AppCanCreateHost(Profile* profile,
                                                 const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id))
    return fallback_delegate_->AppCanCreateHost(profile, app_id);
  // A host is only created for use with RemoteCocoa.
  return AppUsesRemoteCocoa(profile, app_id);
}

bool WebAppShimManagerDelegate::AppUsesRemoteCocoa(
    Profile* profile,
    const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id))
    return fallback_delegate_->AppUsesRemoteCocoa(profile, app_id);
  // All PWAs, and bookmark apps that open in their own window (not in a browser
  // window) can attach to a host.
  if (!profile)
    return false;
  auto& registrar = WebAppProvider::GetForWebApps(profile)->registrar_unsafe();
  return registrar.IsInstalled(app_id) &&
         registrar.GetAppEffectiveDisplayMode(app_id) !=
             web_app::DisplayMode::kBrowser;
}

bool WebAppShimManagerDelegate::AppIsMultiProfile(
    Profile* profile,
    const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id))
    return fallback_delegate_->AppIsMultiProfile(profile, app_id);
  // All PWAs and bookmark apps are multi-profile.
  return AppIsInstalled(profile, app_id);
}

void WebAppShimManagerDelegate::EnableExtension(
    Profile* profile,
    const std::string& extension_id,
    base::OnceCallback<void()> callback) {
  if (UseFallback(profile, extension_id)) {
    fallback_delegate_->EnableExtension(profile, extension_id,
                                        std::move(callback));
    return;
  }
  std::move(callback).Run();
}

void WebAppShimManagerDelegate::LaunchApp(
    Profile* profile,
    const webapps::AppId& app_id,
    const std::vector<base::FilePath>& files,
    const std::vector<GURL>& urls,
    const GURL& override_url,
    chrome::mojom::AppShimLoginItemRestoreState login_item_restore_state,
    base::OnceClosure launch_finished_callback) {
  DCHECK(AppIsInstalled(profile, app_id));
  if (UseFallback(profile, app_id)) {
    fallback_delegate_->LaunchApp(profile, app_id, files, urls, override_url,
                                  login_item_restore_state,
                                  std::move(launch_finished_callback));
    return;
  }
  base::ScopedClosureRunner run_launch_finished(
      std::move(launch_finished_callback));

  DisplayMode effective_display_mode = WebAppProvider::GetForWebApps(profile)
                                           ->registrar_unsafe()
                                           .GetAppEffectiveDisplayMode(app_id);

  apps::LaunchContainer launch_container =
      web_app::ConvertDisplayModeToAppLaunchContainer(effective_display_mode);
  apps::LaunchSource launch_source = apps::LaunchSource::kFromCommandLine;
  if (login_item_restore_state !=
      chrome::mojom::AppShimLoginItemRestoreState::kNone) {
    launch_source = apps::LaunchSource::kFromOsLogin;
  }

  apps::AppLaunchParams params(app_id, launch_container,
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               launch_source);
  // Don't assign `files` to `params.launch_files` until we're sure this is a
  // permitted file launch.
  std::vector<base::FilePath> launch_files = files;
  params.override_url = override_url;

  for (const GURL& url : urls) {
    if (!url.is_valid() || !url.has_scheme()) {
      DLOG(ERROR) << "URL is not valid or does not have a scheme.";
      continue;
    }

    // Convert any file: URLs to a filename that can be passed to the OS.
    // If the conversion succeeds, add to the launch_files vector otherwise,
    // drop the url.
    if (url.SchemeIsFile()) {
      base::FilePath file_path;
      if (net::FileURLToFilePath(url, &file_path)) {
        launch_files.push_back(file_path);
      } else {
        DLOG(ERROR) << "Failed to convert file scheme url to file path.";
      }
      continue;
    }

    if (params.protocol_handler_launch_url.has_value()) {
      DLOG(ERROR) << "Protocol launch URL already set.";
      continue;
    }

    // Validate that the scheme is something that could be registered by the PWA
    // via the manifest.
    if (!blink::IsValidCustomHandlerScheme(
            url.scheme(), blink::ProtocolHandlerSecurityLevel::kStrict)) {
      DLOG(ERROR) << "Protocol is not a valid custom handler scheme.";
      continue;
    }

    params.protocol_handler_launch_url = url;
    params.launch_source = apps::LaunchSource::kFromProtocolHandler;
  }

  WebAppProvider* const provider = WebAppProvider::GetForWebApps(profile);
  WebAppFileHandlerManager::LaunchInfos file_launches;
  if (!params.protocol_handler_launch_url) {
    file_launches = provider->os_integration_manager()
                        .file_handler_manager()
                        .GetMatchingFileHandlerUrls(app_id, launch_files);
  }
  if (GetBrowserAppLauncherForTesting()) {
    LaunchAppWithParams(profile, std::move(params), file_launches,
                        run_launch_finished.Release());
    return;
  }

  if (params.protocol_handler_launch_url.has_value()) {
    GURL protocol_url = params.protocol_handler_launch_url.value();

    // Protocol handlers should prompt the user before launching the app,
    // unless the user has granted or denied permission to this protocol scheme
    // previously.
    web_app::WebAppRegistrar& registrar =
        WebAppProvider::GetForWebApps(profile)->registrar_unsafe();
    if (registrar.IsDisallowedLaunchProtocol(app_id, protocol_url.scheme())) {
      CancelAppLaunch(profile, app_id);
      return;
    }

    if (!registrar.IsAllowedLaunchProtocol(app_id, protocol_url.scheme())) {
      ShowWebAppProtocolLaunchDialog(
          std::move(protocol_url), profile, app_id,
          base::BindOnce(&UserChoiceDialogCompleted, std::move(params),
                         WebAppFileHandlerManager::LaunchInfos(), profile,
                         run_launch_finished.Release()));
      return;
    }
  }

  // If there is no matching file handling URL (such as when the API has been
  // disabled), fall back to a normal app launch.
  if (!file_launches.empty()) {
    const WebApp* web_app = provider->registrar_unsafe().GetAppById(app_id);
    DCHECK(web_app);

    if (web_app->file_handler_approval_state() ==
        ApiApprovalState::kRequiresPrompt) {
      ShowWebAppFileLaunchDialog(
          launch_files, profile, app_id,
          base::BindOnce(&UserChoiceDialogCompleted, std::move(params),
                         file_launches, profile,
                         run_launch_finished.Release()));
      return;
    }

    DCHECK_EQ(ApiApprovalState::kAllowed,
              web_app->file_handler_approval_state());
  }

  LaunchAppWithParams(profile, std::move(params), file_launches,
                      run_launch_finished.Release());
}

void WebAppShimManagerDelegate::LaunchShim(
    Profile* profile,
    const webapps::AppId& app_id,
    web_app::LaunchShimUpdateBehavior update_behavior,
    web_app::ShimLaunchMode launch_mode,
    apps::ShimLaunchedCallback launched_callback,
    apps::ShimTerminatedCallback terminated_callback) {
  DCHECK(AppIsInstalled(profile, app_id));
  if (UseFallback(profile, app_id)) {
    fallback_delegate_->LaunchShim(profile, app_id, update_behavior,
                                   launch_mode, std::move(launched_callback),
                                   std::move(terminated_callback));
    return;
  }
  WebAppProvider::GetForWebApps(profile)
      ->os_integration_manager()
      .GetShortcutInfoForAppFromRegistrar(
          app_id, base::BindOnce(&web_app::LaunchShim, update_behavior,
                                 launch_mode, std::move(launched_callback),
                                 std::move(terminated_callback)));
}

bool WebAppShimManagerDelegate::HasNonBookmarkAppWindowsOpen() {
  if (fallback_delegate_)
    return fallback_delegate_->HasNonBookmarkAppWindowsOpen();
  // PWAs and bookmark apps do not participate in custom app quit behavior.
  return false;
}

bool WebAppShimManagerDelegate::UseFallback(
    Profile* profile,
    const webapps::AppId& app_id) const {
  if (!profile)
    return false;

  // If |app_id| is installed via WebAppProvider, then use |this| as the
  // delegate.
  auto* provider = WebAppProvider::GetForWebApps(profile);
  if (provider->registrar_unsafe().IsInstalled(app_id))
    return false;

  // Use |fallback_delegate_| only if |app_id| is installed for |profile|
  // as an extension.
  return fallback_delegate_->AppIsInstalled(profile, app_id);
}

std::vector<chrome::mojom::ApplicationDockMenuItemPtr>
WebAppShimManagerDelegate::GetAppShortcutsMenuItemInfos(
    Profile* profile,
    const webapps::AppId& app_id) {
  if (UseFallback(profile, app_id))
    return fallback_delegate_->GetAppShortcutsMenuItemInfos(profile, app_id);

  std::vector<chrome::mojom::ApplicationDockMenuItemPtr> dock_menu_items;

  DCHECK(profile);

  auto shortcuts_menu_item_infos = WebAppProvider::GetForWebApps(profile)
                                       ->registrar_unsafe()
                                       .GetAppShortcutsMenuItemInfos(app_id);

  DCHECK_LE(shortcuts_menu_item_infos.size(), kMaxApplicationDockMenuItems);
  for (const auto& shortcuts_menu_item_info : shortcuts_menu_item_infos) {
    auto mojo_item = chrome::mojom::ApplicationDockMenuItem::New();
    mojo_item->name = shortcuts_menu_item_info.name;
    mojo_item->url = shortcuts_menu_item_info.url;
    dock_menu_items.push_back(std::move(mojo_item));
  }

  return dock_menu_items;
}

}  // namespace web_app