// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/ash/arc/arc_open_url_delegate_impl.h"
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "ash/components/arc/mojom/intent_helper.mojom.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/check.h"
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_path.h"
#include "base/files/safe_base_name.h"
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/apps/apk_web_app_service.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/fileapi/arc_content_file_system_url_util.h"
#include "chrome/browser/ash/arc/intent_helper/custom_tab_session_impl.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/fileapi/external_file_url_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_arc_tracker.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/browser/webshare/prepare_directory_task.h"
#include "chrome/common/webui_url_constants.h"
#include "components/arc/intent_helper/arc_intent_helper_bridge.h"
#include "components/arc/intent_helper/custom_tab.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "components/user_manager/user_manager.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/base/url_util.h"
#include "storage/browser/file_system/file_system_context.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
#include "url/url_constants.h"
using arc::mojom::ChromePage;
namespace {
ArcOpenUrlDelegateImpl* g_instance = nullptr;
constexpr auto kOSSettingsMap = base::MakeFixedFlatMap<ChromePage,
const char*>({
{ChromePage::ACCOUNTS,
chromeos::settings::mojom::kManageOtherPeopleSubpagePathV2},
{ChromePage::AUDIO, chromeos::settings::mojom::kAudioSubpagePath},
{ChromePage::BLUETOOTH,
chromeos::settings::mojom::kBluetoothDevicesSubpagePath},
{ChromePage::BLUETOOTHDEVICES,
chromeos::settings::mojom::kBluetoothDevicesSubpagePath},
{ChromePage::CUPSPRINTERS,
chromeos::settings::mojom::kPrintingDetailsSubpagePath},
{ChromePage::DATETIME, chromeos::settings::mojom::kDateAndTimeSectionPath},
{ChromePage::DISPLAY, chromeos::settings::mojom::kDisplaySubpagePath},
{ChromePage::GRAPHICSTABLET,
chromeos::settings::mojom::kGraphicsTabletSubpagePath},
{ChromePage::HELP, chromeos::settings::mojom::kAboutChromeOsSectionPath},
{ChromePage::KEYBOARDOVERLAY,
chromeos::settings::mojom::kKeyboardSubpagePath},
{ChromePage::LOCKSCREEN,
chromeos::settings::mojom::kSecurityAndSignInSubpagePathV2},
{ChromePage::MAIN, ""},
{ChromePage::MANAGEACCESSIBILITY,
chromeos::settings::mojom::kAccessibilitySectionPath},
{ChromePage::MANAGEACCESSIBILITYTTS,
chromeos::settings::mojom::kTextToSpeechSubpagePath},
{ChromePage::MULTIDEVICE,
chromeos::settings::mojom::kMultiDeviceSectionPath},
{ChromePage::NETWORKSTYPEVPN,
chromeos::settings::mojom::kVpnDetailsSubpagePath},
{ChromePage::OSLANGUAGESINPUT,
chromeos::settings::mojom::kInputSubpagePath},
{ChromePage::OSLANGUAGESLANGUAGES,
chromeos::settings::mojom::kLanguagesSubpagePath},
{ChromePage::PERDEVICEKEYBOARD,
chromeos::settings::mojom::kPerDeviceKeyboardSubpagePath},
{ChromePage::PERDEVICEMOUSE,
chromeos::settings::mojom::kPerDeviceMouseSubpagePath},
{ChromePage::PERDEVICEPOINTINGSTICK,
chromeos::settings::mojom::kPerDevicePointingStickSubpagePath},
{ChromePage::PERDEVICETOUCHPAD,
chromeos::settings::mojom::kPerDeviceTouchpadSubpagePath},
{ChromePage::POINTEROVERLAY,
chromeos::settings::mojom::kPointersSubpagePath},
{ChromePage::POWER, chromeos::settings::mojom::kPowerSubpagePath},
{ChromePage::PRIVACYHUB, chromeos::settings::mojom::kPrivacyHubSubpagePath},
{ChromePage::SMARTPRIVACY,
chromeos::settings::mojom::kSmartPrivacySubpagePath},
{ChromePage::STORAGE, chromeos::settings::mojom::kStorageSubpagePath},
{ChromePage::WIFI, chromeos::settings::mojom::kWifiNetworksSubpagePath},
});
constexpr auto kBrowserSettingsMap =
base::MakeFixedFlatMap<ChromePage, const char*>({
{ChromePage::APPEARANCE, chrome::kAppearanceSubPage},
{ChromePage::AUTOFILL, chrome::kAutofillSubPage},
{ChromePage::CLEARBROWSERDATA, chrome::kClearBrowserDataSubPage},
{ChromePage::DOWNLOADS, chrome::kDownloadsSubPage},
{ChromePage::LANGUAGES, chrome::kLanguagesSubPage},
{ChromePage::ONSTARTUP, chrome::kOnStartupSubPage},
{ChromePage::PASSWORDS, chrome::kPasswordManagerSubPage},
{ChromePage::PRIVACY, chrome::kPrivacySubPage},
{ChromePage::RESET, chrome::kResetSubPage},
{ChromePage::SEARCH, chrome::kSearchSubPage},
{ChromePage::SYNCSETUP, chrome::kSyncSetupSubPage},
});
constexpr auto kAboutPagesMap =
base::MakeFixedFlatMap<ChromePage, const char*>({
{ChromePage::ABOUTBLANK, url::kAboutBlankURL},
{ChromePage::ABOUTDOWNLOADS, "chrome://downloads/"},
{ChromePage::ABOUTHISTORY, "chrome://history/"},
});
// Converts the given ARC URL to an external file URL to read it via ARC content
// file system when necessary. Otherwise, returns the given URL unchanged.
GURL ConvertArcUrlToExternalFileUrlIfNeeded(const GURL& url) {
if (url.SchemeIs(url::kFileScheme) || url.SchemeIs(url::kContentScheme)) {
// Chrome cannot open this URL. Read the contents via ARC content file
// system with an external file URL.
return arc::ArcUrlToExternalFileUrl(url);
}
return url;
}
// Converts a content:// ARC URL to a file:// URL managed by the FuseBox Moniker
// system. This Moniker file is readable on the Linux filesystem like any other
// file. Returns an empty URL if a Moniker could not be created.
GURL ConvertToMonikerFileUrl(Profile* profile, GURL content_url) {
return ash::ExternalFileURLToFuseboxMonikerFileURL(
profile, arc::ArcUrlToExternalFileUrl(content_url),
/*read_only=*/true, webshare::PrepareDirectoryTask::kSharedFileLifetime);
}
apps::IntentPtr ConvertLaunchIntent(
Profile* profile,
const arc::mojom::LaunchIntentPtr& launch_intent) {
const char* action =
apps_util::ConvertArcToAppServiceIntentAction(launch_intent->action);
auto intent = std::make_unique<apps::Intent>(action ? action : "");
intent->url = launch_intent->data;
intent->mime_type = launch_intent->type;
intent->share_title = launch_intent->extra_subject;
intent->share_text = launch_intent->extra_text;
if (launch_intent->files.has_value() && launch_intent->files->size() > 0) {
std::vector<std::string> mime_types;
for (const auto& file_info : *launch_intent->files) {
GURL moniker_url =
ConvertToMonikerFileUrl(profile, file_info->content_uri);
if (moniker_url.is_empty()) {
// Continue launching the web app, but without any invalid attached
// files.
continue;
}
apps::IntentFilePtr file =
std::make_unique<apps::IntentFile>(moniker_url);
file->mime_type = file_info->type;
file->file_name = file_info->name;
file->file_size = file_info->size;
intent->files.push_back(std::move(file));
mime_types.push_back(file_info->type);
}
// Override the given MIME type based on the files that we're sharing.
intent->mime_type = apps_util::CalculateCommonMimeType(mime_types);
}
return intent;
}
// Finds the best matching web app that can handle the |url|.
std::optional<std::string> FindWebAppForURL(Profile* profile, const GURL& url) {
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
if (!proxy) {
return std::nullopt;
}
std::vector<std::string> app_ids = proxy->GetAppIdsForUrl(
url, /*exclude_browsers=*/true, /*exclude_browser_tab_apps=*/true);
std::string best_match;
size_t best_match_length = 0;
for (const std::string& app_id : app_ids) {
// Among all the matched apps, select a web app with the longest matching
// scope.
size_t match_length = 0;
proxy->AppRegistryCache().ForOneApp(
app_id, [&url, &match_length](const apps::AppUpdate& update) {
if (update.AppType() != apps::AppType::kWeb) {
return;
}
for (const auto& filter : update.IntentFilters()) {
match_length =
std::max(match_length,
apps_util::IntentFilterUrlMatchLength(filter, url));
}
});
if (match_length > best_match_length) {
best_match_length = match_length;
best_match = app_id;
}
}
if (best_match.empty()) {
return std::nullopt;
}
return best_match;
}
} // namespace
ArcOpenUrlDelegateImpl::ArcOpenUrlDelegateImpl() {
arc::ArcIntentHelperBridge::SetOpenUrlDelegate(this);
DCHECK(!g_instance);
g_instance = this;
}
ArcOpenUrlDelegateImpl::~ArcOpenUrlDelegateImpl() {
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
arc::ArcIntentHelperBridge::SetOpenUrlDelegate(nullptr);
}
ArcOpenUrlDelegateImpl* ArcOpenUrlDelegateImpl::GetForTesting() {
return g_instance;
}
void ArcOpenUrlDelegateImpl::OpenUrlFromArc(const GURL& url) {
if (!url.is_valid())
return;
GURL url_to_open = ConvertArcUrlToExternalFileUrlIfNeeded(url);
// If Lacros is enabled, convert externalfile:// url into file:// url
// managed by the FuseBox moniker system because Lacros cannot handle
// externalfile:// urls.
// TODO(crbug.com/1374575): Check if other externalfile:// urls can use the
// same logic. If so, move this code into CrosapiNewWindowDelegate::OpenUrl()
// which is only for Lacros.
if (crosapi::browser_util::IsLacrosEnabled() &&
url_to_open.SchemeIs(content::kExternalFileScheme)) {
Profile* profile = ash::ProfileHelper::Get()->GetProfileByUser(
user_manager::UserManager::Get()->GetPrimaryUser());
// `profile` may be null if sign-in has happened but the profile isn't
// loaded yet.
if (!profile)
return;
url_to_open = ConvertToMonikerFileUrl(profile, url);
}
ash::NewWindowDelegate::GetPrimary()->OpenUrl(
url_to_open, ash::NewWindowDelegate::OpenUrlFrom::kArc,
ash::NewWindowDelegate::Disposition::kNewForegroundTab);
}
void ArcOpenUrlDelegateImpl::OpenWebAppFromArc(const GURL& url) {
DCHECK(url.is_valid() && url.SchemeIs(url::kHttpsScheme));
// Fetch the profile associated with ARC. This method should only be called
// for a |url| which was installed via ARC, and so we want the web app that is
// opened through here to be installed in the profile associated with ARC.
// |user| may be null if sign-in hasn't happened yet
const auto* user = user_manager::UserManager::Get()->GetPrimaryUser();
if (!user)
return;
// `profile` may be null if sign-in has happened but the profile isn't loaded
// yet.
Profile* profile = ash::ProfileHelper::Get()->GetProfileByUser(user);
if (!profile)
return;
std::optional<webapps::AppId> app_id =
web_app::IsWebAppsCrosapiEnabled()
? FindWebAppForURL(profile, url)
: web_app::FindInstalledAppWithUrlInScope(profile, url,
/*window_only=*/true);
if (!app_id) {
OpenUrlFromArc(url);
return;
}
int event_flags = apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
/*prefer_container=*/false);
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
proxy->AppRegistryCache().ForOneApp(
*app_id, [&event_flags](const apps::AppUpdate& update) {
if (update.WindowMode() == apps::WindowMode::kBrowser) {
event_flags =
apps::GetEventFlags(WindowOpenDisposition::NEW_FOREGROUND_TAB,
/*prefer_container=*/false);
}
});
proxy->LaunchAppWithUrl(*app_id, event_flags, url,
apps::LaunchSource::kFromArc);
ash::ApkWebAppService* apk_web_app_service =
ash::ApkWebAppService::Get(profile);
if (!apk_web_app_service ||
!apk_web_app_service->IsWebAppInstalledFromArc(app_id.value())) {
return;
}
ArcAppListPrefs* prefs = ArcAppListPrefs::Get(profile);
if (!prefs)
return;
std::optional<std::string> package_name =
apk_web_app_service->GetPackageNameForWebApp(app_id.value());
if (!package_name.has_value())
return;
ChromeShelfController* chrome_shelf_controller =
ChromeShelfController::instance();
if (!chrome_shelf_controller)
return;
auto* arc_tracker =
chrome_shelf_controller->app_service_app_window_controller()
->app_service_arc_tracker();
if (!arc_tracker)
return;
for (const auto& id : prefs->GetAppsForPackage(package_name.value()))
arc_tracker->CloseWindows(id);
}
void ArcOpenUrlDelegateImpl::OpenArcCustomTab(
const GURL& url,
int32_t task_id,
arc::mojom::IntentHelperHost::OnOpenCustomTabCallback callback) {
GURL url_to_open = ConvertArcUrlToExternalFileUrlIfNeeded(url);
Profile* profile = ProfileManager::GetActiveUserProfile();
aura::Window* arc_window = arc::GetArcWindow(task_id);
if (!arc_window) {
std::move(callback).Run(mojo::NullRemote());
return;
}
auto custom_tab = std::make_unique<arc::CustomTab>(arc_window);
auto web_contents = arc::CreateArcCustomTabWebContents(profile, url);
// |custom_tab_browser| will be destroyed when its tab strip becomes empty,
// either due to the user opening the custom tab page in a tabbed browser or
// because of the CustomTabSessionImpl object getting destroyed.
Browser::CreateParams params(Browser::TYPE_CUSTOM_TAB, profile,
/*user_gesture=*/true);
params.omit_from_session_restore = true;
auto* custom_tab_browser = Browser::Create(params);
custom_tab_browser->tab_strip_model()->AppendWebContents(
std::move(web_contents), /* foreground= */ true);
// TODO(crbug.com/41454219): Remove this temporary conversion to InterfacePtr
// once OnOpenCustomTab from //ash/components/arc/mojom/intent_helper.mojom
// could take pending_remote directly. Refer to crrev.com/c/1868870.
auto custom_tab_remote(
CustomTabSessionImpl::Create(std::move(custom_tab), custom_tab_browser));
std::move(callback).Run(std::move(custom_tab_remote));
}
void ArcOpenUrlDelegateImpl::OpenChromePageFromArc(ChromePage page) {
if (auto it = kOSSettingsMap.find(page); it != kOSSettingsMap.end()) {
Profile* profile = ProfileManager::GetActiveUserProfile();
std::string sub_page = it->second;
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile,
sub_page);
return;
}
if (auto it = kBrowserSettingsMap.find(page);
it != kBrowserSettingsMap.end()) {
OpenUrlFromArc(GURL(chrome::kChromeUISettingsURL).Resolve(it->second));
return;
}
if (auto it = kAboutPagesMap.find(page); it != kAboutPagesMap.end()) {
OpenUrlFromArc(GURL(it->second));
return;
}
NOTREACHED_IN_MIGRATION();
}
void ArcOpenUrlDelegateImpl::OpenAppWithIntent(
const GURL& start_url,
arc::mojom::LaunchIntentPtr arc_intent) {
DCHECK(start_url.is_valid());
DCHECK(start_url.SchemeIs(url::kHttpsScheme) || net::IsLocalhost(start_url));
// Fetch the profile associated with ARC. This method should only be called
// for a |url| which was installed via ARC, and so we want the web app that is
// opened through here to be installed in the profile associated with ARC.
const auto* user = user_manager::UserManager::Get()->GetPrimaryUser();
DCHECK(user);
// |profile| may be null if sign-in has happened but the profile isn't loaded
// yet.
Profile* profile = ash::ProfileHelper::Get()->GetProfileByUser(user);
if (!profile)
return;
webapps::AppId app_id =
web_app::GenerateAppId(/*manifest_id=*/std::nullopt, start_url);
bool app_installed = false;
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
proxy->AppRegistryCache().ForOneApp(
app_id, [&app_installed](const apps::AppUpdate& update) {
app_installed = apps_util::IsInstalled(update.Readiness());
});
if (!app_installed) {
if (arc_intent->data)
OpenUrlFromArc(*arc_intent->data);
return;
}
apps::IntentPtr intent = ConvertLaunchIntent(profile, arc_intent);
auto disposition = WindowOpenDisposition::NEW_WINDOW;
proxy->AppRegistryCache().ForOneApp(
app_id, [&disposition](const apps::AppUpdate& update) {
if (update.WindowMode() == apps::WindowMode::kBrowser) {
disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
}
});
int event_flags = apps::GetEventFlags(disposition,
/*prefer_container=*/false);
proxy->LaunchAppWithIntent(app_id, event_flags, std::move(intent),
apps::LaunchSource::kFromArc, nullptr,
base::DoNothing());
}