// 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/ash/system_web_apps/system_web_app_ui_utils.h"
#include <string>
#include <utility>
#include "base/check_op.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.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/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/scalable_iph/scalable_iph_factory.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/web_app_helpers.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/webui_url_constants.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/window_open_disposition.h"
#include "ui/display/scoped_display_for_new_windows.h"
namespace ash {
namespace {
// Returns the profile where we should launch System Web Apps into. It returns
// the most appropriate profile for launching, if the provided |profile| is
// unsuitable. It returns nullptr if the we can't find a suitable profile.
Profile* GetProfileForSystemWebAppLaunch(Profile* profile) {
DCHECK(profile);
// We can't launch into certain profiles, and we can't find a suitable
// alternative.
if (profile->IsSystemProfile())
return nullptr;
if (ProfileHelper::IsSigninProfile(profile))
return nullptr;
// For a guest sessions, launch into the primary off-the-record profile, which
// is used for browsing in guest sessions. We do this because the "original"
// profile of the guest session can't create windows.
if (profile->IsGuestSession())
return profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
// We don't support launching SWA in incognito profiles, use the original
// profile if an incognito profile is provided (with the exception of guest
// session, which is implemented with an incognito profile, thus it is handled
// above).
if (profile->IsIncognitoProfile())
return profile->GetOriginalProfile();
// Use the profile provided in other scenarios.
return profile;
}
} // namespace
std::optional<SystemWebAppType> GetSystemWebAppTypeForAppId(
Profile* profile,
const webapps::AppId& app_id) {
auto* swa_manager = SystemWebAppManager::Get(profile);
return swa_manager ? swa_manager->GetSystemAppTypeForAppId(app_id)
: std::nullopt;
}
std::optional<webapps::AppId> GetAppIdForSystemWebApp(
Profile* profile,
SystemWebAppType app_type) {
auto* swa_manager = SystemWebAppManager::Get(profile);
return swa_manager ? swa_manager->GetAppIdForSystemApp(app_type)
: std::nullopt;
}
std::optional<apps::AppLaunchParams> CreateSystemWebAppLaunchParams(
Profile* profile,
SystemWebAppType app_type,
int64_t display_id) {
std::optional<webapps::AppId> app_id =
GetAppIdForSystemWebApp(profile, app_type);
// TODO(calamity): Decide whether to report app launch failure or CHECK fail.
if (!app_id)
return std::nullopt;
auto* provider = SystemWebAppManager::GetWebAppProvider(profile);
DCHECK(provider);
web_app::DisplayMode display_mode =
provider->registrar_unsafe().GetAppEffectiveDisplayMode(app_id.value());
// TODO(crbug.com/40143506): Plumb through better launch sources from
// callsites.
apps::AppLaunchParams params = apps::CreateAppIdLaunchParamsWithEventFlags(
app_id.value(), /*event_flags=*/0,
apps::LaunchSource::kFromChromeInternal, display_id,
/*fallback_container=*/
web_app::ConvertDisplayModeToAppLaunchContainer(display_mode));
return params;
}
SystemAppLaunchParams::SystemAppLaunchParams() = default;
SystemAppLaunchParams::SystemAppLaunchParams(
const SystemAppLaunchParams& params) = default;
SystemAppLaunchParams::~SystemAppLaunchParams() = default;
namespace {
void LaunchSystemWebAppAsyncContinue(Profile* profile_for_launch,
const SystemWebAppType type,
const SystemAppLaunchParams& params,
apps::WindowInfoPtr window_info,
apps::LaunchCallback callback) {
if (profile_for_launch->ShutdownStarted()) {
return;
}
const std::optional<webapps::AppId> app_id =
GetAppIdForSystemWebApp(profile_for_launch, type);
if (!app_id)
return;
auto* app_service =
apps::AppServiceProxyFactory::GetForProfile(profile_for_launch);
DCHECK(app_service);
auto event_flags = apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
/* prefer_container */ false);
if (!params.launch_paths.empty()) {
DCHECK(!params.url.has_value())
<< "Launch URL can't be used with launch_paths.";
app_service->LaunchAppWithFiles(*app_id, event_flags, params.launch_source,
params.launch_paths);
return;
}
if (params.url) {
DCHECK(params.url->is_valid());
app_service->LaunchAppWithUrl(*app_id, event_flags, *params.url,
params.launch_source, std::move(window_info),
std::move(callback));
return;
}
app_service->Launch(*app_id, event_flags, params.launch_source,
std::move(window_info));
}
} // namespace
void LaunchSystemWebAppAsync(Profile* profile,
const SystemWebAppType type,
const SystemAppLaunchParams& params,
apps::WindowInfoPtr window_info,
std::optional<apps::LaunchCallback> callback) {
DCHECK(profile);
// Terminal should be launched with crostini::LaunchTerminal*.
DCHECK(type != SystemWebAppType::TERMINAL);
// Callback is only supported when launching with an URL.
DCHECK(!callback || params.url.has_value());
// TODO(crbug.com/40723875): Implement a confirmation dialog when
// changing to a different profile.
Profile* profile_for_launch = GetProfileForSystemWebAppLaunch(profile);
if (profile_for_launch == nullptr) {
// We can't find a suitable profile to launch. Complain about this so we
// can identify the call site, and ask them to pick the right profile.
// Note that this is fatal in developer builds.
DUMP_WILL_BE_NOTREACHED()
<< "LaunchSystemWebAppAsync is called on a profile that can't launch "
"system web apps. The launch request is ignored. Please check the "
"profile you are using is correct.";
// Early return if we can't find a profile to launch.
return;
}
if (type == SystemWebAppType::PERSONALIZATION &&
profile_for_launch == profile) {
scalable_iph::ScalableIph* scalable_iph =
ScalableIphFactory::GetForBrowserContext(profile_for_launch);
if (scalable_iph) {
scalable_iph->RecordEvent(
scalable_iph::ScalableIph::Event::kOpenPersonalizationApp);
}
}
SystemWebAppManager* manager = SystemWebAppManager::Get(profile_for_launch);
if (!manager) {
return;
}
// Wait for all SWAs to be registered before continuing.
manager->on_apps_synchronized().Post(
FROM_HERE,
base::BindOnce(&LaunchSystemWebAppAsyncContinue, profile_for_launch, type,
params, std::move(window_info),
callback.has_value() ? std::move(callback.value())
: base::DoNothing()));
}
Browser* LaunchSystemWebAppImpl(Profile* profile,
SystemWebAppType app_type,
const GURL& url,
const apps::AppLaunchParams& params) {
// Exit early if we can't create browser windows (e.g. when browser is
// shutting down, or a wrong profile is given).
if (Browser::GetCreationStatusForProfile(profile) !=
Browser::CreationStatus::kOk) {
return nullptr;
}
SystemWebAppManager* swa_manager = SystemWebAppManager::Get(profile);
if (!swa_manager)
return nullptr;
auto* provider = web_app::WebAppProvider::GetForLocalAppsUnchecked(profile);
if (!provider)
return nullptr;
auto* system_app = swa_manager->GetSystemApp(app_type);
#if BUILDFLAG(IS_CHROMEOS)
DCHECK(url.DeprecatedGetOriginAsURL() == provider->registrar_unsafe()
.GetAppLaunchUrl(params.app_id)
.DeprecatedGetOriginAsURL() ||
system_app && system_app->IsUrlInSystemAppScope(url));
#endif
if (!system_app) {
LOG(ERROR) << "Can't find delegate for system app url: " << url
<< " Not launching.";
return nullptr;
}
// Place new windows on the specified display.
display::ScopedDisplayForNewWindows scoped_display(params.display_id);
Browser* browser =
system_app->LaunchAndNavigateSystemWebApp(profile, provider, url, params);
if (!browser) {
return nullptr;
}
// We update web application launch stats (e.g. last launch time), but don't
// record web app launch metrics.
//
// Web app launch metrics reflect user's preference about app launch behavior
// (e.g. open in a tab or open in a window). This information used to make
// decisions about web application UI flow.
//
// Since users can't configure SWA launch behavior, we don't report these
// metrics to avoid skewing web app metrics.
web_app::UpdateLaunchStats(browser->tab_strip_model()->GetActiveWebContents(),
params.app_id, url);
// LaunchSystemWebAppImpl may be called with a profile associated with an
// inactive (background) desktop (e.g. when multiple users are logged in).
// Here we move the newly created browser window (or the existing one on the
// inactive desktop) to the current active (visible) desktop, so the user
// always sees the launched app.
multi_user_util::MoveWindowToCurrentDesktop(
browser->window()->GetNativeWindow());
browser->window()->Show();
return browser;
}
Browser* FindSystemWebAppBrowser(Profile* profile,
SystemWebAppType app_type,
Browser::Type browser_type,
const GURL& url) {
// TODO(calamity): Determine whether, during startup, we need to wait for
// app install and then provide a valid answer here.
std::optional<webapps::AppId> app_id =
GetAppIdForSystemWebApp(profile, app_type);
if (!app_id)
return nullptr;
auto* provider = SystemWebAppManager::GetWebAppProvider(profile);
DCHECK(provider);
if (!provider->registrar_unsafe().IsInstalled(app_id.value()))
return nullptr;
// Look through all the windows, find a browser for this app. Prefer the most
// recently active app window.
for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) {
if (browser->profile() != profile || browser->type() != browser_type ||
browser->is_delete_scheduled()) {
continue;
}
if (web_app::GetAppIdFromApplicationName(browser->app_name()) !=
app_id.value()) {
continue;
}
if (!url.is_empty()) {
// In case a URL is provided, only allow a browser which shows it.
content::WebContents* content =
browser->tab_strip_model()->GetActiveWebContents();
if (!content->GetVisibleURL().EqualsIgnoringRef(url))
continue;
}
return browser;
}
return nullptr;
}
bool IsSystemWebApp(Browser* browser) {
DCHECK(browser);
return browser->app_controller() && browser->app_controller()->system_app();
}
bool IsBrowserForSystemWebApp(Browser* browser, SystemWebAppType type) {
DCHECK(browser);
return browser->app_controller() && browser->app_controller()->system_app() &&
browser->app_controller()->system_app()->GetType() == type;
}
std::optional<SystemWebAppType> GetCapturingSystemAppForURL(Profile* profile,
const GURL& url) {
SystemWebAppManager* swa_manager = SystemWebAppManager::Get(profile);
return swa_manager ? swa_manager->GetCapturingSystemAppForURL(url)
: std::nullopt;
}
gfx::Size GetSystemWebAppMinimumWindowSize(Browser* browser) {
DCHECK(browser);
if (browser->app_controller() && browser->app_controller()->system_app())
return browser->app_controller()->system_app()->GetMinimumWindowSize();
return gfx::Size();
}
} // namespace ash