blob: dd1963daa8660ec856265cf25d487526a872e0b2 [file] [log] [blame]
// 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/web_applications/web_app_dialog_utils.h"
#include <memory>
#include <utility>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/web_app_dialogs.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app_command_manager.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_helpers.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_install_manager.h"
#include "chrome/browser/web_applications/web_app_install_params.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "components/webapps/browser/banners/app_banner_manager.h"
#include "components/webapps/browser/banners/web_app_banner_data.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/browser/installable/installable_data.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/installable/ml_install_operation_tracker.h"
#include "components/webapps/browser/installable/ml_installability_promoter.h"
#include "content/public/browser/navigation_entry.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/metrics/structured/event_logging_features.h"
// TODO(crbug.com/1125897): Enable gn check once it handles conditional includes
#include "components/metrics/structured/structured_events.h" // nogncheck
#include "components/metrics/structured/structured_metrics_client.h" // nogncheck
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ui/webui/ash/app_install/app_install_dialog.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/webui/web_ui_util.h"
#endif
namespace web_app {
namespace {
#if BUILDFLAG(IS_CHROMEOS)
namespace cros_events = metrics::structured::events::v2::cr_os_events;
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Returns the first icon larger than `kIconSize` from `manifest_icons`. If none
// exist, returns the largest icon. Returns an empty IconInfo if there are no
// icons.
// TODO(crbug.com/40283709): This function assumes manifest_icons is sorted,
// which it may not be. Icon purpose also needs to be considered.
apps::IconInfo GetIcon(const std::vector<apps::IconInfo>& manifest_icons) {
for (const auto& icon_info : manifest_icons) {
if (icon_info.square_size_px > ash::app_install::kIconSize) {
return icon_info;
}
}
return apps::IconInfo();
}
void OnManifestFetchedShowCrosDialog(
Profile* profile,
base::WeakPtr<ash::app_install::AppInstallDialog> dialog_handle,
std::vector<webapps::Screenshot> screenshots,
content::WebContents* initiator_web_contents,
std::unique_ptr<WebAppInstallInfo> web_app_info,
WebAppInstallationAcceptanceCallback web_app_acceptance_callback) {
web_app_info->user_display_mode = mojom::UserDisplayMode::kStandalone;
apps::IconInfo icon = GetIcon(web_app_info->manifest_icons);
std::vector<ash::app_install::mojom::ScreenshotPtr> dialog_screenshots;
for (const auto& screenshot : screenshots) {
auto dialog_screenshot = ash::app_install::mojom::Screenshot::New();
dialog_screenshot->url = GURL(webui::GetBitmapDataUrl(screenshot.image));
dialog_screenshot->size =
gfx::Size(screenshot.image.width(), screenshot.image.height());
dialog_screenshots.push_back(std::move(dialog_screenshot));
}
dialog_handle->ShowApp(
profile, initiator_web_contents->GetTopLevelNativeWindow(),
apps::PackageId(apps::PackageType::kWeb,
web_app_info->manifest_id.spec()),
base::UTF16ToUTF8(web_app_info->title),
web_app_info->start_url.GetWithEmptyPath(),
base::UTF16ToUTF8(web_app_info->description), icon.url,
icon.square_size_px.has_value() ? icon.square_size_px.value() : 0,
icon.purpose == apps::IconInfo::Purpose::kMaskable,
std::move(dialog_screenshots),
base::BindOnce(
[](std::unique_ptr<WebAppInstallInfo> web_app_info,
WebAppInstallationAcceptanceCallback web_app_acceptance_callback,
bool dialog_accepted) {
std::move(web_app_acceptance_callback)
.Run(dialog_accepted, std::move(web_app_info));
},
std::move(web_app_info), std::move(web_app_acceptance_callback)));
}
void OnWebAppInstalledFromCrosDialog(
base::WeakPtr<ash::app_install::AppInstallDialog> dialog_handle,
WebAppInstalledCallback installed_callback,
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
if (webapps::IsSuccess(code)) {
dialog_handle->SetInstallSucceeded();
} else {
// If we receive an error code, there's a chance the dialog was never shown,
// so we need to clean it up to avoid a memory leak.
dialog_handle->CleanUpDialogIfNotShown();
// TODO(b/40283709): Pass a callback to retry the install.
dialog_handle->SetInstallFailed(base::DoNothing());
}
std::move(installed_callback).Run(app_id, code);
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
void OnWebAppInstallShowInstallDialog(
WebAppInstallFlow flow,
webapps::WebappInstallSource install_source,
PwaInProductHelpState iph_state,
std::unique_ptr<webapps::MlInstallOperationTracker> install_tracker,
std::vector<webapps::Screenshot> screenshots,
content::WebContents* initiator_web_contents,
std::unique_ptr<WebAppInstallInfo> web_app_info,
WebAppInstallationAcceptanceCallback web_app_acceptance_callback) {
DCHECK(web_app_info);
switch (flow) {
case WebAppInstallFlow::kInstallSite:
web_app_info->user_display_mode = mojom::UserDisplayMode::kStandalone;
#if BUILDFLAG(IS_CHROMEOS)
if (base::FeatureList::IsEnabled(
metrics::structured::kAppDiscoveryLogging) &&
install_source == webapps::WebappInstallSource::MENU_BROWSER_TAB) {
webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(web_app_info->manifest_id);
metrics::structured::StructuredMetricsClient::Record(std::move(
cros_events::AppDiscovery_Browser_ClickInstallAppFromMenu()
.SetAppId(app_id)));
}
#endif
if (!screenshots.empty()) {
ShowWebAppDetailedInstallDialog(
initiator_web_contents, std::move(web_app_info),
std::move(install_tracker), std::move(web_app_acceptance_callback),
std::move(screenshots), iph_state);
return;
} else if (base::FeatureList::IsEnabled(
features::kWebAppUniversalInstall) &&
web_app_info->is_diy_app) {
ShowDiyAppInstallDialog(initiator_web_contents, std::move(web_app_info),
std::move(install_tracker),
std::move(web_app_acceptance_callback),
iph_state);
return;
} else {
ShowSimpleInstallDialogForWebApps(
initiator_web_contents, std::move(web_app_info),
std::move(install_tracker), std::move(web_app_acceptance_callback),
iph_state);
return;
}
case WebAppInstallFlow::kCreateShortcut:
#if BUILDFLAG(IS_CHROMEOS)
if (base::FeatureList::IsEnabled(
metrics::structured::kAppDiscoveryLogging)) {
webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(web_app_info->manifest_id);
metrics::structured::StructuredMetricsClient::Record(std::move(
cros_events::AppDiscovery_Browser_CreateShortcut().SetAppId(
app_id)));
}
#endif
ShowCreateShortcutDialog(initiator_web_contents, std::move(web_app_info),
std::move(install_tracker),
std::move(web_app_acceptance_callback));
return;
case WebAppInstallFlow::kUnknown:
NOTREACHED();
}
NOTREACHED();
}
WebAppInstalledCallback& GetInstalledCallbackForTesting() {
static base::NoDestructor<WebAppInstalledCallback> instance;
return *instance;
}
void OnWebAppInstalled(WebAppInstalledCallback callback,
const webapps::AppId& installed_app_id,
webapps::InstallResultCode code) {
if (GetInstalledCallbackForTesting())
std::move(GetInstalledCallbackForTesting()).Run(installed_app_id, code);
std::move(callback).Run(installed_app_id, code);
}
} // namespace
bool CanCreateWebApp(const Browser* browser) {
// Check whether user is allowed to install web app.
if (!WebAppProvider::GetForWebApps(browser->profile()) ||
!AreWebAppsUserInstallable(browser->profile()))
return false;
// Check whether we're able to install the current page as an app.
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
if (!IsValidWebAppUrl(web_contents->GetLastCommittedURL()) ||
web_contents->IsCrashed()) {
return false;
}
content::NavigationEntry* entry =
web_contents->GetController().GetLastCommittedEntry();
if (entry && entry->GetPageType() == content::PAGE_TYPE_ERROR)
return false;
return true;
}
bool CanPopOutWebApp(Profile* profile) {
return AreWebAppsEnabled(profile) && !profile->IsGuestSession() &&
!profile->IsOffTheRecord();
}
void CreateWebAppFromCurrentWebContents(Browser* browser,
WebAppInstallFlow flow) {
DCHECK(CanCreateWebApp(browser));
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
auto* provider = WebAppProvider::GetForWebContents(web_contents);
DCHECK(provider);
webapps::MLInstallabilityPromoter* promoter =
webapps::MLInstallabilityPromoter::FromWebContents(web_contents);
CHECK(promoter);
if (promoter->HasCurrentInstall()) {
return;
}
if (provider->command_manager().IsInstallingForWebContents(web_contents)) {
return;
}
webapps::AppBannerManager* app_banner_manager =
webapps::AppBannerManager::FromWebContents(web_contents);
if (!app_banner_manager) {
return;
}
std::optional<webapps::WebAppBannerData> data =
app_banner_manager->GetCurrentWebAppBannerData();
webapps::WebappInstallSource install_source =
webapps::InstallableMetrics::GetInstallSource(
web_contents, flow == WebAppInstallFlow::kCreateShortcut
? webapps::InstallTrigger::CREATE_SHORTCUT
: webapps::InstallTrigger::MENU);
std::unique_ptr<webapps::MlInstallOperationTracker> install_tracker =
promoter->RegisterCurrentInstallForWebContents(install_source);
WebAppInstalledCallback callback = base::DoNothing();
// Appropriately set the fallback behavior to distinguish installation of DIY
// apps with the create shortcut flow.
FallbackBehavior fallback_behavior =
flow == WebAppInstallFlow::kCreateShortcut
? FallbackBehavior::kAllowFallbackDataAlways
: FallbackBehavior::kUseFallbackInfoWhenNotInstallable;
// TODO(b/307145346): Eventually, this should also be primary install for
// Lacros.
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (base::FeatureList::IsEnabled(
chromeos::features::kCrosOmniboxInstallDialog)) {
base::WeakPtr<ash::app_install::AppInstallDialog> dialog_handle =
ash::app_install::AppInstallDialog::CreateDialog();
provider->scheduler().FetchManifestAndInstall(
install_source, web_contents->GetWeakPtr(),
base::BindOnce(OnManifestFetchedShowCrosDialog, browser->profile(),
dialog_handle,
data.has_value() ? std::move(data->screenshots)
: std::vector<webapps::Screenshot>()),
base::BindOnce(OnWebAppInstalledFromCrosDialog, dialog_handle,
std::move(callback)),
fallback_behavior);
return;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
provider->scheduler().FetchManifestAndInstall(
install_source, web_contents->GetWeakPtr(),
base::BindOnce(OnWebAppInstallShowInstallDialog, flow, install_source,
PwaInProductHelpState::kNotShown,
std::move(install_tracker),
data.has_value() ? std::move(data->screenshots)
: std::vector<webapps::Screenshot>()),
base::BindOnce(OnWebAppInstalled, std::move(callback)),
fallback_behavior);
}
bool CreateWebAppFromManifest(content::WebContents* web_contents,
webapps::WebappInstallSource install_source,
WebAppInstalledCallback installed_callback,
PwaInProductHelpState iph_state) {
auto* provider = WebAppProvider::GetForWebContents(web_contents);
if (!provider)
return false;
webapps::MLInstallabilityPromoter* promoter =
webapps::MLInstallabilityPromoter::FromWebContents(web_contents);
if (promoter->HasCurrentInstall()) {
return false;
}
if (provider->command_manager().IsInstallingForWebContents(web_contents)) {
return false;
}
webapps::AppBannerManager* app_banner_manager =
webapps::AppBannerManager::FromWebContents(web_contents);
if (!app_banner_manager) {
return false;
}
std::optional<webapps::WebAppBannerData> data =
app_banner_manager->GetCurrentWebAppBannerData();
std::unique_ptr<webapps::MlInstallOperationTracker> install_tracker =
promoter->RegisterCurrentInstallForWebContents(install_source);
// If the source is from ML, there may not be a manifest, so allow the command
// to use the metadata from the page too.
FallbackBehavior fallback_behavior =
install_source == webapps::WebappInstallSource::ML_PROMOTION
? FallbackBehavior::kUseFallbackInfoWhenNotInstallable
: FallbackBehavior::kCraftedManifestOnly;
// TODO(b/307145346): Eventually, this should also be primary install for
// Lacros.
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (base::FeatureList::IsEnabled(
chromeos::features::kCrosOmniboxInstallDialog)) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
base::WeakPtr<ash::app_install::AppInstallDialog> dialog_handle =
ash::app_install::AppInstallDialog::CreateDialog();
provider->scheduler().FetchManifestAndInstall(
install_source, web_contents->GetWeakPtr(),
base::BindOnce(OnManifestFetchedShowCrosDialog, profile, dialog_handle,
data.has_value() ? std::move(data->screenshots)
: std::vector<webapps::Screenshot>()),
base::BindOnce(OnWebAppInstalledFromCrosDialog, dialog_handle,
std::move(installed_callback)),
fallback_behavior);
return true;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
provider->scheduler().FetchManifestAndInstall(
install_source, web_contents->GetWeakPtr(),
base::BindOnce(OnWebAppInstallShowInstallDialog,
WebAppInstallFlow::kInstallSite, install_source, iph_state,
std::move(install_tracker),
data.has_value() ? std::move(data->screenshots)
: std::vector<webapps::Screenshot>()),
base::BindOnce(OnWebAppInstalled, std::move(installed_callback)),
fallback_behavior);
return true;
}
void SetInstalledCallbackForTesting(WebAppInstalledCallback callback) {
GetInstalledCallbackForTesting() = std::move(callback);
}
} // namespace web_app