// Copyright 2023 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/link_capturing/chromeos_link_capturing_delegate.h"
#include <optional>
#include <string_view>
#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
#include "base/auto_reset.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/values_equivalent.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.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/launch_utils.h"
#include "chrome/browser/apps/link_capturing/link_capturing_features.h"
#include "chrome/browser/apps/link_capturing/link_capturing_tab_data.h"
#include "chrome/browser/apps/link_capturing/metrics/intent_handling_metrics.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
namespace apps {
namespace {
// Usually we want to only capture navigations from clicking a link. For a
// subset of apps, we want to capture typing into the omnibox as well.
bool ShouldOnlyCaptureLinks(const std::vector<std::string>& app_ids) {
return !base::Contains(app_ids, ash::kChromeUIUntrustedProjectorSwaAppId);
}
bool IsSystemWebApp(Profile* profile, const std::string& app_id) {
bool is_system_web_app = false;
apps::AppServiceProxyFactory::GetForProfile(profile)
->AppRegistryCache()
.ForOneApp(app_id, [&is_system_web_app](const apps::AppUpdate& update) {
if (update.InstallReason() == apps::InstallReason::kSystem) {
is_system_web_app = true;
}
});
return is_system_web_app;
}
// This function redirects an external untrusted |url| to a privileged trusted
// one for SWAs, if applicable.
GURL RedirectUrlIfSwa(Profile* profile,
const std::string& app_id,
const GURL& url,
const base::TickClock* clock) {
if (!IsSystemWebApp(profile, app_id)) {
return url;
}
// Projector:
if (app_id == ash::kChromeUIUntrustedProjectorSwaAppId &&
url.GetWithEmptyPath() == GURL(ash::kChromeUIUntrustedProjectorPwaUrl)) {
std::string override_url = ash::kChromeUIUntrustedProjectorUrl;
if (url.path().length() > 1) {
override_url += url.path().substr(1);
}
std::stringstream ss;
// Since ChromeOS doesn't reload an app if the URL doesn't change, the line
// below appends a unique timestamp to the URL to force a reload.
// TODO(b/211787536): Remove the timestamp after we update the trusted URL
// to match the user's navigations through the post message api.
ss << override_url << "?timestamp=" << clock->NowTicks();
if (url.has_query()) {
ss << '&' << url.query();
}
GURL result(ss.str());
DCHECK(result.is_valid());
return result;
}
// Add redirects for other SWAs above this line.
// No matching SWAs found, returning original url.
return url;
}
IntentHandlingMetrics::Platform GetMetricsPlatform(AppType app_type) {
switch (app_type) {
case AppType::kArc:
return IntentHandlingMetrics::Platform::ARC;
case AppType::kWeb:
case AppType::kSystemWeb:
return IntentHandlingMetrics::Platform::PWA;
case AppType::kUnknown:
case AppType::kBuiltIn:
case AppType::kCrostini:
case AppType::kChromeApp:
case AppType::kPluginVm:
case AppType::kStandaloneBrowser:
case AppType::kRemote:
case AppType::kBorealis:
case AppType::kStandaloneBrowserChromeApp:
case AppType::kExtension:
case AppType::kStandaloneBrowserExtension:
case AppType::kBruschetta:
NOTREACHED_IN_MIGRATION();
return IntentHandlingMetrics::Platform::ARC;
}
}
void LaunchApp(base::WeakPtr<AppServiceProxy> proxy,
const std::string& app_id,
int32_t event_flags,
GURL url,
LaunchSource launch_source,
WindowInfoPtr window_info,
AppType app_type,
base::OnceClosure callback) {
if (!proxy) {
std::move(callback).Run();
return;
}
proxy->LaunchAppWithUrl(
app_id, event_flags, url, launch_source, std::move(window_info),
base::IgnoreArgs<LaunchResult&&>(std::move(callback)));
IntentHandlingMetrics::RecordPreferredAppLinkClickMetrics(
GetMetricsPlatform(app_type));
}
// Used to create a unique timestamped URL to force reload apps.
// Points to the base::DefaultTickClock by default.
static const base::TickClock*& GetTickClock() {
static const base::TickClock* g_clock = base::DefaultTickClock::GetInstance();
return g_clock;
}
} // namespace
// static
std::optional<std::string> ChromeOsLinkCapturingDelegate::GetLaunchAppId(
const AppIdsToLaunchForUrl& app_ids_to_launch,
bool is_navigation_from_link) {
if (app_ids_to_launch.candidates.empty()) {
return std::nullopt;
}
if (ShouldOnlyCaptureLinks(app_ids_to_launch.candidates) &&
!is_navigation_from_link) {
return std::nullopt;
}
if (app_ids_to_launch.preferred) {
return app_ids_to_launch.preferred;
}
return std::nullopt;
}
// static
base::AutoReset<const base::TickClock*>
ChromeOsLinkCapturingDelegate::SetClockForTesting(
const base::TickClock* tick_clock) {
return base::AutoReset<const base::TickClock*>(&GetTickClock(), tick_clock);
}
ChromeOsLinkCapturingDelegate::ChromeOsLinkCapturingDelegate() = default;
ChromeOsLinkCapturingDelegate::~ChromeOsLinkCapturingDelegate() = default;
bool ChromeOsLinkCapturingDelegate::ShouldCancelThrottleCreation(
content::NavigationHandle* handle) {
content::WebContents* web_contents = handle->GetWebContents();
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
return !AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile);
}
std::optional<apps::LinkCapturingNavigationThrottle::LaunchCallback>
ChromeOsLinkCapturingDelegate::CreateLinkCaptureLaunchClosure(
Profile* profile,
content::WebContents* web_contents,
const GURL& url,
bool is_navigation_from_link) {
AppServiceProxy* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
AppIdsToLaunchForUrl app_ids_to_launch = FindAppIdsToLaunchForUrl(proxy, url);
std::optional<std::string> launch_app_id =
GetLaunchAppId(app_ids_to_launch, is_navigation_from_link);
if (!launch_app_id) {
return std::nullopt;
}
// Only automatically launch supported app types.
AppType app_type = proxy->AppRegistryCache().GetAppType(*launch_app_id);
if (app_type != AppType::kArc && app_type != AppType::kWeb &&
!IsSystemWebApp(profile, *launch_app_id)) {
return std::nullopt;
}
// Don't capture if already inside the target app scope.
// TODO(b/313518305): Query App Service intent filters instead, so that this
// check also covers ARC apps.
if (app_type == AppType::kWeb &&
base::ValuesEquivalent(web_app::WebAppTabHelper::GetAppId(web_contents),
&launch_app_id.value())) {
return std::nullopt;
}
// Don't capture if already inside a Web App window for the target app. If the
// previous early return didn't trigger, this means we are in an app window
// but out of scope of the original app, and navigating will put us back in
// scope.
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebApps(profile);
if (provider && base::ValuesEquivalent(
provider->ui_manager().GetAppIdForWindow(web_contents),
&launch_app_id.value())) {
return std::nullopt;
}
auto launch_source = is_navigation_from_link ? LaunchSource::kFromLink
: LaunchSource::kFromOmnibox;
GURL redirected_url =
RedirectUrlIfSwa(profile, *launch_app_id, url, GetTickClock());
// Note: The launch can occur after this object is destroyed, so bind to a
// static function.
return base::BindOnce(
&LaunchApp, proxy->GetWeakPtr(), *launch_app_id,
GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
/*prefer_container=*/true),
redirected_url, launch_source,
std::make_unique<WindowInfo>(display::kDefaultDisplayId), app_type);
}
} // namespace apps