blob: 3d68c1de9eeaf838f87ff3843752abfa4fd42af2 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/manifest_update_task.h"
#include <map>
#include <memory>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/components/web_app_install_utils.h"
#include "chrome/browser/web_applications/components/web_app_ui_manager.h"
#include "chrome/browser/web_applications/components/web_application_info.h"
#include "chrome/browser/web_applications/os_integration_manager.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_manager.h"
#include "chrome/browser/web_applications/web_app_installation_utils.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.h"
#include "components/webapps/browser/installable/installable_manager.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "ui/gfx/skia_util.h"
namespace web_app {
struct IconDiff {
public:
IconDiff() = default;
explicit IconDiff(bool changes) { changes_detected = changes; }
IconDiff(const SkBitmap& before_icon, const SkBitmap& after_icon) {
changes_detected = true;
before = before_icon;
after = after_icon;
}
bool changes_detected = false;
SkBitmap before;
SkBitmap after;
};
namespace {
IconDiff HaveIconContentsChanged(
const std::map<SquareSizePx, SkBitmap>& disk_icon_bitmaps,
const std::map<SquareSizePx, SkBitmap>& downloaded_icon_bitmaps) {
if (downloaded_icon_bitmaps.size() != disk_icon_bitmaps.size())
return IconDiff(true);
for (const std::pair<const SquareSizePx, SkBitmap>& entry :
downloaded_icon_bitmaps) {
SquareSizePx size = entry.first;
const SkBitmap& downloaded_bitmap = entry.second;
auto it = disk_icon_bitmaps.find(size);
if (it == disk_icon_bitmaps.end())
return IconDiff(true);
const SkBitmap& disk_bitmap = it->second;
if (!gfx::BitmapsAreEqual(downloaded_bitmap, disk_bitmap))
return IconDiff(disk_bitmap, downloaded_bitmap);
}
return IconDiff(false);
}
IconDiff HaveIconBitmapsChanged(const IconBitmaps& disk_icon_bitmaps,
const IconBitmaps& downloaded_icon_bitmaps) {
IconDiff icon_diff = HaveIconContentsChanged(disk_icon_bitmaps.any,
downloaded_icon_bitmaps.any);
if (icon_diff.changes_detected)
return icon_diff;
icon_diff = HaveIconContentsChanged(disk_icon_bitmaps.maskable,
downloaded_icon_bitmaps.maskable);
if (icon_diff.changes_detected)
return icon_diff;
icon_diff = HaveIconContentsChanged(disk_icon_bitmaps.monochrome,
downloaded_icon_bitmaps.monochrome);
if (icon_diff.changes_detected)
return icon_diff;
return IconDiff(false);
}
// Some apps, such as pre-installed apps, have been vetted and are therefore
// considered safe and permitted to update their names.
bool AllowUnpromptedNameUpdate(const AppId& app_id,
const WebAppRegistrar& registrar) {
const WebApp* web_app = registrar.GetAppById(app_id);
if (!web_app)
return false;
return CanWebAppUpdateIdentity(web_app);
}
// Some apps, such as pre-installed apps, have been vetted and are therefore
// considered safe and permitted to update their icon. For others, the feature
// flag needs to be on.
bool AllowUnpromptedIconUpdate(const AppId& app_id,
const WebAppRegistrar& registrar) {
const WebApp* web_app = registrar.GetAppById(app_id);
if (!web_app)
return false;
return CanWebAppUpdateIdentity(web_app) ||
base::FeatureList::IsEnabled(features::kWebAppManifestIconUpdating);
}
} // namespace
ManifestUpdateTask::ManifestUpdateTask(
const GURL& url,
const AppId& app_id,
content::WebContents* web_contents,
StoppedCallback stopped_callback,
bool hang_for_testing,
const WebAppRegistrar& registrar,
const WebAppIconManager& icon_manager,
WebAppUiManager* ui_manager,
WebAppInstallManager* install_manager,
OsIntegrationManager& os_integration_manager)
: content::WebContentsObserver(web_contents),
registrar_(registrar),
icon_manager_(icon_manager),
ui_manager_(*ui_manager),
install_manager_(*install_manager),
os_integration_manager_(os_integration_manager),
url_(url),
app_id_(app_id),
stopped_callback_(std::move(stopped_callback)),
hang_for_testing_(hang_for_testing) {
// Task starts by waiting for DidFinishLoad() to be called.
stage_ = Stage::kPendingPageLoad;
}
ManifestUpdateTask::~ManifestUpdateTask() {
#if DCHECK_IS_ON()
if (destructor_called_ptr_) {
DCHECK(!(*destructor_called_ptr_));
*destructor_called_ptr_ = true;
}
#endif
}
// content::WebContentsObserver:
void ManifestUpdateTask::DidFinishLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
if (stage_ != Stage::kPendingPageLoad || hang_for_testing_)
return;
if (render_frame_host->GetParent() != nullptr)
return;
stage_ = Stage::kPendingInstallableData;
webapps::InstallableParams params;
params.valid_primary_icon = true;
params.valid_manifest = true;
params.check_webapp_manifest_display = false;
webapps::InstallableManager::FromWebContents(web_contents())
->GetData(params,
base::BindOnce(&ManifestUpdateTask::OnDidGetInstallableData,
AsWeakPtr()));
}
// content::WebContentsObserver:
void ManifestUpdateTask::WebContentsDestroyed() {
switch (stage_) {
case Stage::kPendingPageLoad:
case Stage::kPendingInstallableData:
case Stage::kPendingIconDownload:
case Stage::kPendingIconReadFromDisk:
DestroySelf(ManifestUpdateResult::kWebContentsDestroyed);
return;
case Stage::kPendingAppIdentityCheck:
case Stage::kPendingWindowsClosed:
case Stage::kPendingMaybeReadExistingIcons:
case Stage::kPendingInstallation:
case Stage::kPendingAssociationsUpdate:
// These stages should have stopped listening to the web contents.
NOTREACHED() << static_cast<int>(stage_);
Observe(nullptr);
break;
}
}
void ManifestUpdateTask::OnDidGetInstallableData(
const webapps::InstallableData& data) {
DCHECK_EQ(stage_, Stage::kPendingInstallableData);
if (!data.NoBlockingErrors()) {
DestroySelf(ManifestUpdateResult::kAppNotEligible);
return;
}
web_application_info_.emplace();
UpdateWebAppInfoFromManifest(data.manifest, data.manifest_url,
&web_application_info_.value());
// We cannot allow the app ID to change via the manifest changing. We rely on
// fixed app IDs to determine whether web apps installed in the user sync
// profile has been sync installed across devices. If we allowed the app ID to
// change then the sync system would try to redeploy the old app indefinitely,
// additionally the new app ID would get added to the sync profile. This has
// the potential to flood the user sync profile with an infinite number of
// apps should the site be serving a random start_url on every navigation.
if (app_id_ != GenerateAppId(web_application_info_->manifest_id,
web_application_info_->start_url)) {
DestroySelf(ManifestUpdateResult::kAppIdMismatch);
return;
}
if (IsUpdateNeededForManifest()) {
UpdateAfterWindowsClose();
return;
}
LoadAndCheckIconContents();
}
bool ManifestUpdateTask::IsUpdateNeededForManifest() const {
DCHECK(web_application_info_.has_value());
const WebApp* app = registrar_.GetAppById(app_id_);
DCHECK(app);
// Allows updating start_url and manifest_id when kWebAppEnableManifestId is
// enabled. Both fields are allowed to change as long as the app_id generated
// from them doesn't change.
if (base::FeatureList::IsEnabled(blink::features::kWebAppEnableManifestId)) {
if (web_application_info_->manifest_id != app->manifest_id())
return true;
if (web_application_info_->start_url != app->start_url())
return true;
}
if (web_application_info_->theme_color != app->theme_color())
return true;
if (web_application_info_->scope != app->scope())
return true;
if (web_application_info_->display_mode != app->display_mode())
return true;
if (web_application_info_->display_override != app->display_mode_override())
return true;
// Allow app icon updating for certain apps, or if the existing icons are
// empty - this means the app icon download during install failed.
if (AllowUnpromptedIconUpdate(app_id_, registrar_) &&
web_application_info_->icon_infos != app->icon_infos()) {
return true;
}
if (web_application_info_->shortcuts_menu_item_infos !=
app->shortcuts_menu_item_infos()) {
return true;
}
if (web_application_info_->share_target != app->share_target())
return true;
if (web_application_info_->protocol_handlers != app->protocol_handlers())
return true;
if (web_application_info_->url_handlers != app->url_handlers())
return true;
if (web_application_info_->note_taking_new_note_url !=
app->note_taking_new_note_url()) {
return true;
}
if (web_application_info_->capture_links != app->capture_links())
return true;
if (os_integration_manager_.IsFileHandlingAPIAvailable(app_id_) &&
app->file_handlers() != web_application_info_->file_handlers) {
return true;
}
if (web_application_info_->background_color != app->background_color())
return true;
if (web_application_info_->manifest_url != app->manifest_url())
return true;
if (AllowUnpromptedNameUpdate(app_id_, registrar_) &&
web_application_info_->title != base::UTF8ToUTF16(app->name())) {
return true;
}
if (web_application_info_->launch_handler != app->launch_handler())
return true;
// TODO(crbug.com/1212849): Handle changes to is_storage_isolated.
// TODO(crbug.com/926083): Check more manifest fields.
return false;
}
void ManifestUpdateTask::UpdateAfterWindowsClose() {
DCHECK(stage_ == Stage::kPendingInstallableData ||
stage_ == Stage::kPendingAppIdentityCheck);
stage_ = Stage::kPendingWindowsClosed;
Observe(nullptr);
ui_manager_.NotifyOnAllAppWindowsClosed(
app_id_,
base::BindOnce(&ManifestUpdateTask::OnAllAppWindowsClosed, AsWeakPtr()));
}
void ManifestUpdateTask::LoadAndCheckIconContents() {
DCHECK_EQ(stage_, Stage::kPendingInstallableData);
stage_ = Stage::kPendingIconDownload;
DCHECK(web_application_info_.has_value());
std::vector<GURL> icon_urls =
GetValidIconUrlsToDownload(*web_application_info_);
icon_downloader_.emplace(
web_contents(), std::move(icon_urls),
WebAppIconDownloader::Histogram::kForUpdate,
base::BindOnce(&ManifestUpdateTask::OnIconsDownloaded, AsWeakPtr()));
icon_downloader_->SkipPageFavicons();
icon_downloader_->FailAllIfAnyFail();
icon_downloader_->Start();
}
void ManifestUpdateTask::OnIconsDownloaded(bool success, IconsMap icons_map) {
DCHECK_EQ(stage_, Stage::kPendingIconDownload);
if (!success) {
DestroySelf(ManifestUpdateResult::kIconDownloadFailed);
return;
}
stage_ = Stage::kPendingIconReadFromDisk;
icon_manager_.ReadAllIcons(
app_id_, base::BindOnce(&ManifestUpdateTask::OnAllIconsRead, AsWeakPtr(),
std::move(icons_map)));
}
void ManifestUpdateTask::OnAllIconsRead(IconsMap downloaded_icons_map,
IconBitmaps disk_icon_bitmaps) {
DCHECK_EQ(stage_, Stage::kPendingIconReadFromDisk);
if (disk_icon_bitmaps.empty()) {
DestroySelf(ManifestUpdateResult::kIconReadFromDiskFailed);
return;
}
DCHECK(web_application_info_.has_value());
content::WebContents* web_contents = WebContentsObserver::web_contents();
stage_ = Stage::kPendingAppIdentityCheck;
Observe(nullptr);
PopulateOtherIcons(&web_application_info_.value(), downloaded_icons_map);
if (!AllowUnpromptedNameUpdate(app_id_, registrar_) &&
!AllowUnpromptedIconUpdate(app_id_, registrar_) &&
base::FeatureList::IsEnabled(features::kPwaUpdateDialogForNameAndIcon)) {
// This call populates the |web_application_info_| with all icon bitmap
// data.
// If this data does not match what we already have on disk, then an update
// is necessary.
// TODO(https://crbug.com/1184911): Reuse this data in the web app install
// task.
PopulateProductIcons(&web_application_info_.value(), &downloaded_icons_map);
IconDiff icon_diff = IsUpdateNeededForIconContents(disk_icon_bitmaps);
std::u16string old_title =
base::UTF8ToUTF16(registrar_.GetAppShortName(app_id_));
std::u16string new_title = web_application_info_->title;
bool title_change = old_title != new_title;
if (title_change || icon_diff.changes_detected) {
SkBitmap* before_icon = nullptr;
SkBitmap* after_icon = nullptr;
if (icon_diff.changes_detected) {
before_icon = &icon_diff.before;
after_icon = &icon_diff.after;
} else {
auto it = disk_icon_bitmaps.any.find(web_app::kWebAppIconSmall);
if (it != disk_icon_bitmaps.any.end()) {
before_icon = &it->second;
after_icon = &it->second;
}
}
if (before_icon != nullptr && after_icon != nullptr) {
ui_manager_.ShowWebAppIdentityUpdateDialog(
app_id_, title_change, icon_diff.changes_detected, old_title,
new_title, *before_icon, *after_icon, web_contents,
base::BindOnce(&ManifestUpdateTask::OnPostAppIdentityUpdateCheck,
AsWeakPtr(), std::move(downloaded_icons_map),
std::move(disk_icon_bitmaps)));
return;
}
}
}
// App Identity Warning dialog was not needed, proceed with update check.
OnPostAppIdentityUpdateCheck(downloaded_icons_map, disk_icon_bitmaps,
AppIdentityUpdate::kSkipped);
}
void ManifestUpdateTask::OnPostAppIdentityUpdateCheck(
IconsMap downloaded_icons_map,
IconBitmaps disk_icon_bitmaps,
AppIdentityUpdate app_identity_update_allowed) {
DCHECK_EQ(stage_, Stage::kPendingAppIdentityCheck);
app_identity_update_allowed_ =
app_identity_update_allowed == AppIdentityUpdate::kAllowed;
if (app_identity_update_allowed_) {
UpdateAfterWindowsClose();
return;
}
// Allow app icon updating for certain apps, or if the existing icons are
// empty - this means the app icon download during install failed.
if (AllowUnpromptedIconUpdate(app_id_, registrar_)) {
// When kPwaUpdateDialogForNameAndIcon is enabled, the FilterAndResizeIcons
// call has already been made.
if (!base::FeatureList::IsEnabled(
features::kPwaUpdateDialogForNameAndIcon)) {
// This call populates the |web_application_info_| with all icon bitmap
// data.
// If this data does not match what we already have on disk, then an
// update is necessary.
// TODO(https://crbug.com/1184911): Reuse this data in the web app install
// task.
PopulateProductIcons(&web_application_info_.value(),
&downloaded_icons_map);
}
// TODO: compare in a BEST_EFFORT blocking PostTaskAndReply.
if (IsUpdateNeededForIconContents(disk_icon_bitmaps).changes_detected) {
UpdateAfterWindowsClose();
return;
}
}
icon_manager_.ReadAllShortcutsMenuIcons(
app_id_, base::BindOnce(&ManifestUpdateTask::OnAllShortcutsMenuIconsRead,
AsWeakPtr()));
}
IconDiff ManifestUpdateTask::IsUpdateNeededForIconContents(
const IconBitmaps& disk_icon_bitmaps) const {
DCHECK(web_application_info_.has_value());
return HaveIconBitmapsChanged(disk_icon_bitmaps,
web_application_info_->icon_bitmaps);
}
void ManifestUpdateTask::OnAllShortcutsMenuIconsRead(
ShortcutsMenuIconBitmaps disk_shortcuts_menu_icon_bitmaps) {
DCHECK_EQ(stage_, Stage::kPendingAppIdentityCheck);
DCHECK(web_application_info_.has_value());
if (IsUpdateNeededForShortcutsMenuIconsContents(
disk_shortcuts_menu_icon_bitmaps)) {
UpdateAfterWindowsClose();
return;
}
NoManifestUpdateRequired();
}
bool ManifestUpdateTask::IsUpdateNeededForShortcutsMenuIconsContents(
const ShortcutsMenuIconBitmaps& disk_shortcuts_menu_icon_bitmaps) const {
DCHECK(web_application_info_.has_value());
const ShortcutsMenuIconBitmaps& downloaded_shortcuts_menu_icon_bitmaps =
web_application_info_->shortcuts_menu_icon_bitmaps;
if (downloaded_shortcuts_menu_icon_bitmaps.size() !=
disk_shortcuts_menu_icon_bitmaps.size()) {
return true;
}
for (size_t i = 0; i < downloaded_shortcuts_menu_icon_bitmaps.size(); ++i) {
const IconBitmaps& downloaded_icon_bitmaps =
downloaded_shortcuts_menu_icon_bitmaps[i];
const IconBitmaps& disk_icon_bitmaps = disk_shortcuts_menu_icon_bitmaps[i];
if (HaveIconBitmapsChanged(disk_icon_bitmaps, downloaded_icon_bitmaps)
.changes_detected)
return true;
}
return false;
}
bool ManifestUpdateTask::IsUpdateNeededForWebAppOriginAssociations() const {
// Web app origin association update is tied to the manifest update process.
// If there are url handlers for the current app, associations need to be
// revalidated.
DCHECK(web_application_info_.has_value());
return !web_application_info_->url_handlers.empty();
}
void ManifestUpdateTask::NoManifestUpdateRequired() {
DCHECK_EQ(stage_, Stage::kPendingAppIdentityCheck);
stage_ = Stage::kPendingAssociationsUpdate;
if (!IsUpdateNeededForWebAppOriginAssociations()) {
DestroySelf(ManifestUpdateResult::kAppUpToDate);
return;
}
os_integration_manager_.UpdateUrlHandlers(
app_id_,
base::BindOnce(&ManifestUpdateTask::OnWebAppOriginAssociationsUpdated,
AsWeakPtr()));
}
void ManifestUpdateTask::OnWebAppOriginAssociationsUpdated(bool success) {
DCHECK_EQ(stage_, Stage::kPendingAssociationsUpdate);
success ? DestroySelf(ManifestUpdateResult::kAppAssociationsUpdated)
: DestroySelf(ManifestUpdateResult::kAppAssociationsUpdateFailed);
}
void ManifestUpdateTask::OnAllAppWindowsClosed() {
DCHECK_EQ(stage_, Stage::kPendingWindowsClosed);
DCHECK(web_application_info_.has_value());
if (!AllowUnpromptedNameUpdate(app_id_, registrar_) &&
!app_identity_update_allowed_) {
// The app's name must not change due to an automatic update, except for
// default installed apps (that have been vetted).
// TODO(crbug.com/1088338): Provide a safe way for apps to update their
// name.
web_application_info_->title =
base::UTF8ToUTF16(registrar_.GetAppShortName(app_id_));
}
// Preserve the user's choice of form factor to open the app with.
web_application_info_->user_display_mode =
registrar_.GetAppUserDisplayMode(app_id_);
stage_ = Stage::kPendingMaybeReadExistingIcons;
// If icon updating is disabled, then read the existing icons so they can be
// populated on the WebApplicationInfo.
if (AllowUnpromptedIconUpdate(app_id_, registrar_) ||
app_identity_update_allowed_) {
OnExistingIconsRead(IconBitmaps());
return;
}
icon_manager_.ReadAllIcons(
app_id_,
base::BindOnce(&ManifestUpdateTask::OnExistingIconsRead, AsWeakPtr()));
}
void ManifestUpdateTask::OnExistingIconsRead(IconBitmaps icon_bitmaps) {
DCHECK_EQ(stage_, Stage::kPendingMaybeReadExistingIcons);
bool redownload_app_icons = icon_bitmaps.empty();
if (!redownload_app_icons)
web_application_info_->icon_bitmaps = std::move(icon_bitmaps);
stage_ = Stage::kPendingInstallation;
install_manager_.UpdateWebAppFromInfo(
app_id_, std::make_unique<WebApplicationInfo>(*web_application_info_),
/*redownload_app_icons=*/redownload_app_icons,
base::BindOnce(&ManifestUpdateTask::OnInstallationComplete, AsWeakPtr()));
}
void ManifestUpdateTask::OnInstallationComplete(
const AppId& app_id,
InstallResultCode code) {
DCHECK_EQ(stage_, Stage::kPendingInstallation);
if (!IsSuccess(code)) {
DestroySelf(ManifestUpdateResult::kAppUpdateFailed);
return;
}
DCHECK_EQ(app_id_, app_id);
DCHECK(!IsUpdateNeededForManifest());
DCHECK_EQ(code, InstallResultCode::kSuccessAlreadyInstalled);
DestroySelf(ManifestUpdateResult::kAppUpdated);
}
void ManifestUpdateTask::DestroySelf(ManifestUpdateResult result) {
// Asserts that calling the callback results in |this| getting deleted.
#if DCHECK_IS_ON()
bool destructor_called = false;
destructor_called_ptr_ = &destructor_called;
#endif
std::move(stopped_callback_).Run(*this, result);
#if DCHECK_IS_ON()
DCHECK(destructor_called);
#endif
}
} // namespace web_app