blob: 8baa7aa4e95f5399935bdd5d2fd5dde17b20f97d [file] [log] [blame]
// Copyright 2025 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/web_applications/commands/manifest_silent_update_command.h"
#include <array>
#include <initializer_list>
#include <memory>
#include <optional>
#include <ostream>
#include "base/barrier_closure.h"
#include "base/containers/contains.h"
#include "base/functional/callback.h"
#include "base/functional/concurrent_closures.h"
#include "base/i18n/time_formatting.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/clock.h"
#include "base/values.h"
#include "chrome/browser/shortcuts/shortcut_icon_generator.h"
#include "chrome/browser/web_applications/commands/web_app_command.h"
#include "chrome/browser/web_applications/icons/trusted_icon_filter.h"
#include "chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/locks/noop_lock.h"
#include "chrome/browser/web_applications/manifest_update_utils.h"
#include "chrome/browser/web_applications/proto/web_app.pb.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_icon_generator.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_origin_association_manager.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/browser/web_applications/web_contents/web_contents_manager.h"
#include "chrome/common/chrome_features.h"
#include "components/webapps/browser/image_visual_diff.h"
#include "components/webapps/browser/installable/installable_params.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/page.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
namespace web_app {
namespace {
sync_pb::WebAppIconInfo_Purpose ConvertIconPurposeToSyncPurpose(
apps::IconInfo::Purpose purpose) {
switch (purpose) {
case apps::IconInfo::Purpose::kAny:
return sync_pb::WebAppIconInfo_Purpose::WebAppIconInfo_Purpose_ANY;
case apps::IconInfo::Purpose::kMonochrome:
return sync_pb::WebAppIconInfo_Purpose::WebAppIconInfo_Purpose_MONOCHROME;
case apps::IconInfo::Purpose::kMaskable:
return sync_pb::WebAppIconInfo_Purpose::WebAppIconInfo_Purpose_MASKABLE;
}
}
blink::mojom::ManifestImageResource_Purpose
ConvertIconPurposeToManifestImagePurpose(apps::IconInfo::Purpose app_purpose) {
switch (app_purpose) {
case apps::IconInfo::Purpose::kAny:
return blink::mojom::ManifestImageResource_Purpose::ANY;
case apps::IconInfo::Purpose::kMonochrome:
return blink::mojom::ManifestImageResource_Purpose::MONOCHROME;
case apps::IconInfo::Purpose::kMaskable:
return blink::mojom::ManifestImageResource_Purpose::MASKABLE;
}
}
void CopyIconsToPendingUpdateInfo(
const std::vector<apps::IconInfo>& icon_infos,
google::protobuf::RepeatedPtrField<sync_pb::WebAppIconInfo>*
destination_icons) {
for (const auto& icon_info : icon_infos) {
sync_pb::WebAppIconInfo* pending_icon = destination_icons->Add();
pending_icon->set_url(icon_info.url.spec());
sync_pb::WebAppIconInfo_Purpose icon_purpose =
ConvertIconPurposeToSyncPurpose(icon_info.purpose);
pending_icon->set_purpose(icon_purpose);
if (icon_info.square_size_px.has_value()) {
pending_icon->set_size_in_px(icon_info.square_size_px.value());
}
}
}
} // namespace
bool IsAppUpdated(ManifestSilentUpdateCheckResult result) {
switch (result) {
case ManifestSilentUpdateCheckResult::kAppNotInstalled:
case ManifestSilentUpdateCheckResult::kAppUpdateFailedDuringInstall:
case ManifestSilentUpdateCheckResult::kSystemShutdown:
case ManifestSilentUpdateCheckResult::kAppUpToDate:
case ManifestSilentUpdateCheckResult::kIconReadFromDiskFailed:
case ManifestSilentUpdateCheckResult::kWebContentsDestroyed:
case ManifestSilentUpdateCheckResult::kPendingIconWriteToDiskFailed:
case ManifestSilentUpdateCheckResult::kInvalidManifest:
case ManifestSilentUpdateCheckResult::kInvalidPendingUpdateInfo:
case ManifestSilentUpdateCheckResult::kUserNavigated:
case ManifestSilentUpdateCheckResult::kManifestToWebAppInstallInfoError:
return false;
case ManifestSilentUpdateCheckResult::kAppSilentlyUpdated:
case ManifestSilentUpdateCheckResult::kAppOnlyHasSecurityUpdate:
case ManifestSilentUpdateCheckResult::kAppHasNonSecurityAndSecurityChanges:
return true;
}
}
std::ostream& operator<<(std::ostream& os,
ManifestSilentUpdateCommandStage stage) {
switch (stage) {
case ManifestSilentUpdateCommandStage::kNotStarted:
return os << "kNotStarted";
case ManifestSilentUpdateCommandStage::kFetchingNewManifestData:
return os << "kFetchingNewManifestData";
case ManifestSilentUpdateCommandStage::kLoadingExistingManifestData:
return os << "kLoadingExistingManifestData";
case ManifestSilentUpdateCommandStage::kAcquiringAppLock:
return os << "kAcquiringAppLock";
case ManifestSilentUpdateCommandStage::kConstructingWebAppInfo:
return os << "kConstructingWebAppInfo";
case ManifestSilentUpdateCommandStage::kLoadingExistingAndNewManifestIcons:
return os << "kLoadingExistingAndNewManifestIcons";
case ManifestSilentUpdateCommandStage::kComparingManifestData:
return os << "kComparingManifestData";
case ManifestSilentUpdateCommandStage::kFinalizingSilentManifestChanges:
return os << "kFinalizingSilentManifestChanges";
case ManifestSilentUpdateCommandStage::
kWritingPendingUpdateIconBitmapsToDisk:
return os << "kWritingPendingUpdateIconBitmapsToDisk";
}
}
std::ostream& operator<<(std::ostream& os,
ManifestSilentUpdateCheckResult result) {
switch (result) {
case ManifestSilentUpdateCheckResult::kAppNotInstalled:
return os << "kAppNotInstalled";
case ManifestSilentUpdateCheckResult::kAppUpdateFailedDuringInstall:
return os << "kAppUpdateFailedDuringInstall";
case ManifestSilentUpdateCheckResult::kSystemShutdown:
return os << "kSystemShutdown";
case ManifestSilentUpdateCheckResult::kAppSilentlyUpdated:
return os << "kAppSilentlyUpdated";
case ManifestSilentUpdateCheckResult::kAppUpToDate:
return os << "kAppUpToDate";
case ManifestSilentUpdateCheckResult::kIconReadFromDiskFailed:
return os << "kIconReadFromDiskFailed";
case ManifestSilentUpdateCheckResult::kWebContentsDestroyed:
return os << "kWebContentsDestroyed";
case ManifestSilentUpdateCheckResult::kAppOnlyHasSecurityUpdate:
return os << "kAppOnlyHasSecurityUpdate";
case ManifestSilentUpdateCheckResult::kAppHasNonSecurityAndSecurityChanges:
return os << "kAppHasNonSecurityAndSecurityChanges";
case ManifestSilentUpdateCheckResult::kPendingIconWriteToDiskFailed:
return os << "kPendingIconWriteToDiskFailed";
case ManifestSilentUpdateCheckResult::kInvalidManifest:
return os << "kInvalidManifest";
case ManifestSilentUpdateCheckResult::kInvalidPendingUpdateInfo:
return os << "kInvalidPendingUpdateInfo";
case ManifestSilentUpdateCheckResult::kUserNavigated:
return os << "kUserNavigated";
case ManifestSilentUpdateCheckResult::kManifestToWebAppInstallInfoError:
return os << "kManifestToWebAppInstallInfoError";
}
}
ManifestSilentUpdateCommand::ManifestSilentUpdateCommand(
content::WebContents& web_contents,
CompletedCallback callback)
: WebAppCommand<NoopLock, ManifestSilentUpdateCheckResult>(
"ManifestSilentUpdateCommand",
NoopLockDescription(),
base::BindOnce([](ManifestSilentUpdateCheckResult result) {
base::UmaHistogramEnumeration(
"Webapp.Update.ManifestSilentUpdateCheckResult", result);
return result;
}).Then(std::move(callback)),
/*args_for_shutdown=*/
std::make_tuple(ManifestSilentUpdateCheckResult::kSystemShutdown)),
web_contents_(web_contents.GetWeakPtr()) {
Observe(web_contents_.get());
SetStage(ManifestSilentUpdateCommandStage::kNotStarted);
}
ManifestSilentUpdateCommand::~ManifestSilentUpdateCommand() = default;
void ManifestSilentUpdateCommand::PrimaryPageChanged(content::Page& page) {
auto error = ManifestSilentUpdateCheckResult::kUserNavigated;
GetMutableDebugValue().Set(
"primary_page_changed",
page.GetMainDocument().GetLastCommittedURL().possibly_invalid_spec());
if (IsStarted()) {
CompleteCommandAndSelfDestruct(FROM_HERE, error);
return;
}
GetMutableDebugValue().Set("failed_before_start", true);
failed_before_start_ = error;
}
void ManifestSilentUpdateCommand::StartWithLock(
std::unique_ptr<NoopLock> lock) {
lock_ = std::move(lock);
if (failed_before_start_.has_value()) {
CompleteCommandAndSelfDestruct(FROM_HERE, *failed_before_start_);
return;
}
if (IsWebContentsDestroyed()) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kWebContentsDestroyed);
return;
}
data_retriever_ = lock_->web_contents_manager().CreateDataRetriever();
SetStage(ManifestSilentUpdateCommandStage::kFetchingNewManifestData);
webapps::InstallableParams params;
params.valid_primary_icon = true;
params.check_eligibility = true;
params.installable_criteria =
webapps::InstallableCriteria::kValidManifestIgnoreDisplay;
data_retriever_->CheckInstallabilityAndRetrieveManifest(
web_contents_.get(),
base::BindOnce(
&ManifestSilentUpdateCommand::OnManifestFetchedAcquireAppLock,
GetWeakPtr()),
params);
}
bool ManifestSilentUpdateCommand::WebAppComparison::HasNoChanges() const {
return name_equality && primary_icons_equality &&
shortcut_menu_item_infos_equality && other_fields_equality;
}
bool ManifestSilentUpdateCommand::WebAppComparison::IsNameChangeOnly() const {
return !name_equality && primary_icons_equality &&
shortcut_menu_item_infos_equality && other_fields_equality;
}
bool ManifestSilentUpdateCommand::WebAppComparison::
IsSecuritySensitiveChangesOnly() const {
return !name_equality && !primary_icons_equality &&
shortcut_menu_item_infos_equality && other_fields_equality;
}
base::Value::Dict ManifestSilentUpdateCommand::WebAppComparison::ToDict()
const {
return base::Value::Dict()
.Set("name_equality", name_equality)
.Set("primary_icons_equality", primary_icons_equality)
.Set("shortcut_menu_item_infos_equality",
shortcut_menu_item_infos_equality)
.Set("other_fields_equality", other_fields_equality);
}
// static
ManifestSilentUpdateCommand::WebAppComparison
ManifestSilentUpdateCommand::CompareWebApps(
const WebApp& existing_web_app,
const WebAppInstallInfo& new_install_info) {
CHECK_EQ(existing_web_app.manifest_id(), new_install_info.manifest_id());
WebAppComparison diff;
diff.name_equality = [&]() {
std::u16string new_title;
base::TrimWhitespace(new_install_info.title, base::TRIM_ALL, &new_title);
return new_title == base::UTF8ToUTF16(existing_web_app.untranslated_name());
}();
diff.primary_icons_equality =
existing_web_app.trusted_icons() == new_install_info.trusted_icons;
diff.shortcut_menu_item_infos_equality =
existing_web_app.shortcuts_menu_item_infos() ==
new_install_info.shortcuts_menu_item_infos;
diff.other_fields_equality = [&]() {
if (existing_web_app.start_url() != new_install_info.start_url()) {
return false;
}
if (existing_web_app.theme_color() != new_install_info.theme_color) {
return false;
}
if (existing_web_app.scope() != new_install_info.scope) {
return false;
}
if (existing_web_app.display_mode() != new_install_info.display_mode) {
return false;
}
if (existing_web_app.display_mode_override() !=
new_install_info.display_override) {
return false;
}
if (existing_web_app.share_target() != new_install_info.share_target) {
return false;
}
if (existing_web_app.protocol_handlers() !=
new_install_info.protocol_handlers) {
return false;
}
if (existing_web_app.note_taking_new_note_url() !=
new_install_info.note_taking_new_note_url) {
return false;
}
if (existing_web_app.background_color() !=
new_install_info.background_color) {
return false;
}
if (existing_web_app.dark_mode_theme_color() !=
new_install_info.dark_mode_theme_color) {
return false;
}
if (existing_web_app.dark_mode_background_color() !=
new_install_info.dark_mode_background_color) {
return false;
}
if (existing_web_app.launch_handler() != new_install_info.launch_handler) {
return false;
}
if (existing_web_app.permissions_policy() !=
new_install_info.permissions_policy) {
return false;
}
if (existing_web_app.scope_extensions() !=
new_install_info.scope_extensions) {
return false;
}
if (existing_web_app.related_applications() !=
new_install_info.related_applications) {
return false;
}
if (existing_web_app.file_handlers() != new_install_info.file_handlers) {
return false;
}
if (existing_web_app.tab_strip() != new_install_info.tab_strip) {
return false;
}
// Add new manifest properties here to be considered for update.
return true;
}();
return diff;
}
void ManifestSilentUpdateCommand::SetStage(
ManifestSilentUpdateCommandStage stage) {
stage_ = stage;
GetMutableDebugValue().Set("stage", base::ToString(stage));
}
void ManifestSilentUpdateCommand::OnManifestFetchedAcquireAppLock(
blink::mojom::ManifestPtr opt_manifest,
bool valid_manifest_for_web_app,
webapps::InstallableStatusCode installable_status) {
CHECK_EQ(stage_, ManifestSilentUpdateCommandStage::kFetchingNewManifestData);
if (IsWebContentsDestroyed()) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kWebContentsDestroyed);
return;
}
GetMutableDebugValue().Set("installable_status",
base::ToString(installable_status));
if (!opt_manifest) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kInvalidManifest);
return;
}
// Note: These are filtered below as we require a specified start_url and
// name.
bool manifest_is_default = blink::IsDefaultManifest(
*opt_manifest, web_contents_->GetLastCommittedURL());
GetMutableDebugValue().Set("manifest_is_default", manifest_is_default);
GetMutableDebugValue().Set(
"manifest_url", opt_manifest->manifest_url.possibly_invalid_spec());
GetMutableDebugValue().Set("manifest_id",
opt_manifest->id.possibly_invalid_spec());
GetMutableDebugValue().Set("manifest_start_url",
opt_manifest->start_url.possibly_invalid_spec());
if (installable_status != webapps::InstallableStatusCode::NO_ERROR_DETECTED) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kInvalidManifest);
return;
}
if (opt_manifest->icons.empty()) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kInvalidManifest);
return;
}
CHECK(opt_manifest->id.is_valid());
app_id_ = GenerateAppIdFromManifestId(opt_manifest->id);
SetStage(ManifestSilentUpdateCommandStage::kAcquiringAppLock);
app_lock_ = std::make_unique<AppLock>();
command_manager()->lock_manager().UpgradeAndAcquireLock(
std::move(lock_), *app_lock_, {app_id_},
base::BindOnce(
&ManifestSilentUpdateCommand::StartManifestToInstallInfoJob,
weak_factory_.GetWeakPtr(), std::move(opt_manifest)));
}
void ManifestSilentUpdateCommand::StartManifestToInstallInfoJob(
blink::mojom::ManifestPtr opt_manifest) {
CHECK_EQ(stage_, ManifestSilentUpdateCommandStage::kAcquiringAppLock);
CHECK(app_lock_->IsGranted());
if (!app_lock_->registrar().AppMatches(app_id_,
WebAppFilter::InstalledInChrome())) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kAppNotInstalled);
return;
}
WebAppInstallInfoConstructOptions construct_options;
construct_options.fail_all_if_any_fail = true;
construct_options.defer_icon_fetching = true;
construct_options.record_icon_results_on_update = true;
// The `background_installation` and `install_source` fields here don't matter
// because this is not logged anywhere.
SetStage(ManifestSilentUpdateCommandStage::kConstructingWebAppInfo);
manifest_to_install_info_job_ =
ManifestToWebAppInstallInfoJob::CreateAndStart(
*opt_manifest, *data_retriever_.get(),
/*background_installation=*/false,
webapps::WebappInstallSource::MENU_BROWSER_TAB, web_contents_,
[](IconUrlSizeSet&) {}, GetMutableDebugValue(),
base::BindOnce(
&ManifestSilentUpdateCommand::OnWebAppInfoCreatedFromManifest,
GetWeakPtr()),
construct_options);
}
void ManifestSilentUpdateCommand::OnWebAppInfoCreatedFromManifest(
std::unique_ptr<WebAppInstallInfo> install_info) {
CHECK_EQ(stage_, ManifestSilentUpdateCommandStage::kConstructingWebAppInfo);
CHECK(!new_install_info_);
if (IsWebContentsDestroyed()) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kWebContentsDestroyed);
return;
}
if (!install_info) {
CompleteCommandAndSelfDestruct(
FROM_HERE,
ManifestSilentUpdateCheckResult::kManifestToWebAppInstallInfoError);
return;
}
new_install_info_ = std::move(install_info);
// If there are no changes to the manifest metadata (ignoring icon bitmaps),
// exit early.
const WebApp* app = app_lock_->registrar().GetAppById(app_id_);
CHECK(app);
is_trusted_install_ = app->IsPolicyInstalledApp() || app->IsPreinstalledApp();
web_app_diff_ = CompareWebApps(*app, *new_install_info_);
GetMutableDebugValue().Set("web_app_diff", web_app_diff_.ToDict());
if (web_app_diff_.HasNoChanges()) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kAppUpToDate);
return;
}
// After this line, we know that something in the system needs to update.
// If it's only a name change, simply skip to the end to write the pending
// update info.
// Skip the case where the new name is empty - we will pretend it is the same
// and update the rest of the information.
if (web_app_diff_.IsNameChangeOnly() && !is_trusted_install_) {
proto::PendingUpdateInfo update;
update.set_name(base::UTF16ToUTF8(new_install_info_->title));
WritePendingUpdateInfoThenComplete(std::move(update));
return;
}
// Next, we are loading icons from disk and the network.
base::ConcurrentClosures barrier;
// The existing icons always need to be read from disk, as we need to do the
// 10% comparison even if the urls change.
app_lock_->icon_manager().ReadAllIcons(
app_id_, base::BindOnce(&ManifestSilentUpdateCommand::OnAppIconsLoaded,
GetWeakPtr())
.Then(barrier.CreateClosure()));
if (web_app_diff_.shortcut_menu_item_infos_equality) {
// Since the shortcut menu items did not change, load the existing icons
// from **disk** for the silent update (which acts like a re-install).
app_lock_->icon_manager().ReadAllShortcutsMenuIcons(
app_id_,
base::BindOnce(&ManifestSilentUpdateCommand::OnShortcutIconsLoaded,
GetWeakPtr())
.Then(barrier.CreateClosure()));
}
// Meanwhile, skip downloading icons from the network that we know didn't
// change, and thus we'll just use what we have on disk.
IconUrlExtractionOptions icon_fetch_options{
.product_icons = !web_app_diff_.primary_icons_equality,
.shortcut_menu_item_icons =
!web_app_diff_.shortcut_menu_item_infos_equality};
manifest_to_install_info_job_->FetchIcons(
*new_install_info_, *web_contents_, barrier.CreateClosure(),
/*icon_url_modifications=*/std::nullopt, icon_fetch_options);
std::move(barrier).Done(base::BindOnce(
&ManifestSilentUpdateCommand::FinalizeUpdateIfSilentChangesExist,
weak_factory_.GetWeakPtr()));
SetStage(
ManifestSilentUpdateCommandStage::kLoadingExistingAndNewManifestIcons);
}
void ManifestSilentUpdateCommand::FinalizeUpdateIfSilentChangesExist() {
CHECK_EQ(
stage_,
ManifestSilentUpdateCommandStage::kLoadingExistingAndNewManifestIcons);
SetStage(ManifestSilentUpdateCommandStage::kComparingManifestData);
const WebApp* web_app = app_lock_->registrar().GetAppById(app_id_);
silent_update_required_ = !web_app_diff_.other_fields_equality ||
!web_app_diff_.shortcut_menu_item_infos_equality;
GetMutableDebugValue().Set("silent_update_required",
base::ToString(silent_update_required_));
// Copy over any icons that did not have manifest changes, and thus we loaded
// from disk to avoid hitting the network
CHECK(new_install_info_);
if (web_app_diff_.shortcut_menu_item_infos_equality) {
new_install_info_->shortcuts_menu_item_infos =
web_app->shortcuts_menu_item_infos();
new_install_info_->shortcuts_menu_icon_bitmaps =
existing_shortcuts_menu_icon_bitmaps_;
}
if (web_app_diff_.primary_icons_equality) {
new_install_info_->manifest_icons = web_app->manifest_icons();
new_install_info_->trusted_icons = web_app->trusted_icons();
new_install_info_->icon_bitmaps = existing_manifest_icon_bitmaps_;
new_install_info_->trusted_icon_bitmaps = existing_trusted_icon_bitmaps_;
}
// Changes to preinstalled or admin installed web apps are always silently
// applied since they are installed by trusted sources. There should be no
// pending update info saved for these web apps.
if (base::FeatureList::IsEnabled(
features::kSilentPolicyAndDefaultAppUpdating) &&
is_trusted_install_) {
new_install_info_->trusted_icons = new_install_info_->manifest_icons;
new_install_info_->trusted_icon_bitmaps = new_install_info_->icon_bitmaps;
app_lock_->install_finalizer().FinalizeUpdate(
new_install_info_->Clone(),
base::BindOnce(
[](const webapps::AppId& expected_app_id,
const webapps::AppId& app_id, webapps::InstallResultCode code) {
CHECK_EQ(expected_app_id, app_id);
// Transform the install result code to the command result.
if (!IsSuccess(code)) {
return ManifestSilentUpdateCheckResult::
kAppUpdateFailedDuringInstall;
}
return ManifestSilentUpdateCheckResult::kAppSilentlyUpdated;
},
app_id_)
.Then(base::BindOnce(
&ManifestSilentUpdateCommand::CompleteCommandAndSelfDestruct,
GetWeakPtr(), FROM_HERE)));
return;
}
// Both of these cases should have already been handled & exited early.
CHECK(!web_app_diff_.HasNoChanges());
CHECK(!web_app_diff_.IsNameChangeOnly());
std::optional<proto::PendingUpdateInfo> pending_update_info;
if (!web_app_diff_.name_equality) {
pending_update_info = proto::PendingUpdateInfo();
pending_update_info->set_name(base::UTF16ToUTF8(new_install_info_->title));
new_install_info_->title = base::UTF8ToUTF16(web_app->untranslated_name());
}
// Exit early if there are no icon url changes (and only silent update changes
// with possible name changes).
if (web_app_diff_.primary_icons_equality) {
// The case where only the name changes and nothing else is handled before
// fetching icons.
CHECK(silent_update_required_);
app_lock_->install_finalizer().FinalizeUpdate(
new_install_info_->Clone(),
base::BindOnce(&ManifestSilentUpdateCommand::
UpdateFinalizedWritePendingInfoIfNeeded,
GetWeakPtr(), std::move(pending_update_info)));
return;
}
// After this line, the icon urls have changed. Those icons are either stored
// in PendingUpdateInfo if there is amore than 10% diff, or silently updated
// otherwise.
CHECK(!new_install_info_->trusted_icons.empty());
CHECK(!new_install_info_->trusted_icon_bitmaps.empty());
static constexpr int kLogoSizeInDialog = 96;
// Now, fetch the first icon at or larger than `kLogoSizeInDialog` for both
// the old and new icon.
// Our icon generation logic should always generate an icon at this size or
// larger.
SkBitmap old_trusted_icon = [&]() {
std::optional<apps::IconInfo> trusted_icon =
app_lock_->registrar().GetSingleTrustedAppIconForSecuritySurfaces(
app_id_, kLogoSizeInDialog);
// Some apps don't have any icons, and are all generated.
if (!trusted_icon.has_value()) {
return SkBitmap();
}
blink::mojom::ManifestImageResource_Purpose purpose =
ConvertIconPurposeToManifestImagePurpose(trusted_icon->purpose);
auto old_bitmaps_to_use =
existing_trusted_icon_bitmaps_.GetBitmapsForPurpose(purpose);
if (old_bitmaps_to_use.empty()) {
return SkBitmap();
}
auto old_icon_it = old_bitmaps_to_use.lower_bound(kLogoSizeInDialog);
CHECK(old_icon_it != old_bitmaps_to_use.end());
return old_icon_it->second;
}();
apps::IconInfo::Purpose purpose = new_install_info_->trusted_icons[0].purpose;
SkBitmap new_trusted_icon = [&]() {
const std::map<SquareSizePx, SkBitmap>& icons =
new_install_info_->trusted_icon_bitmaps.GetBitmapsForPurpose(
ConvertIconPurposeToManifestImagePurpose(purpose));
auto icon_it = icons.lower_bound(kLogoSizeInDialog);
CHECK(icon_it != icons.end());
return icon_it->second;
}();
// TODO(crbug.com/437379182): HasMoreThanTenPercentImageDiff() should happen
// in a different thread.
// Case: The icons are being set in the PendingUpdateInfo to be updated later.
if (old_trusted_icon.empty() ||
HasMoreThanTenPercentImageDiff(&old_trusted_icon, &new_trusted_icon)) {
if (!pending_update_info.has_value()) {
pending_update_info = proto::PendingUpdateInfo();
}
GetMutableDebugValue().Set("greater_than_ten_percent", true);
CopyIconsToPendingUpdateInfo(new_install_info_->trusted_icons,
pending_update_info->mutable_trusted_icons());
CopyIconsToPendingUpdateInfo(new_install_info_->manifest_icons,
pending_update_info->mutable_manifest_icons());
pending_trusted_icon_bitmaps_ = new_install_info_->trusted_icon_bitmaps;
pending_manifest_icon_bitmaps_ = new_install_info_->icon_bitmaps;
// Reset the security sensitive icons from the ones loaded from disk.
new_install_info_->manifest_icons = web_app->manifest_icons();
new_install_info_->trusted_icons = web_app->trusted_icons();
new_install_info_->icon_bitmaps = existing_manifest_icon_bitmaps_;
new_install_info_->trusted_icon_bitmaps = existing_trusted_icon_bitmaps_;
} else {
// Silent updates are allowed if the icons are less than 10% diff.
silent_update_required_ = true;
GetMutableDebugValue().Set("silent_update_required",
base::ToString(silent_update_required_));
}
if (silent_update_required_) {
app_lock_->install_finalizer().FinalizeUpdate(
new_install_info_->Clone(),
base::BindOnce(&ManifestSilentUpdateCommand::
UpdateFinalizedWritePendingInfoIfNeeded,
GetWeakPtr(), std::move(pending_update_info)));
} else {
// If there is no silent update, that means it MUST be pending update.
CHECK(pending_update_info);
UpdateFinalizedWritePendingInfoIfNeeded(
std::move(pending_update_info), app_id_,
webapps::InstallResultCode::kSuccessAlreadyInstalled);
}
}
void ManifestSilentUpdateCommand::UpdateFinalizedWritePendingInfoIfNeeded(
std::optional<proto::PendingUpdateInfo> pending_update_info,
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
CHECK_EQ(stage_, ManifestSilentUpdateCommandStage::kComparingManifestData);
SetStage(ManifestSilentUpdateCommandStage::kFinalizingSilentManifestChanges);
GetMutableDebugValue().Set("silent_update_install_code",
base::ToString(code));
if (!IsSuccess(code)) {
CompleteCommandAndSelfDestruct(
FROM_HERE,
ManifestSilentUpdateCheckResult::kAppUpdateFailedDuringInstall);
return;
}
CHECK_EQ(app_id_, app_id);
CHECK_EQ(code, webapps::InstallResultCode::kSuccessAlreadyInstalled);
if (!pending_update_info.has_value()) {
CompleteCommandAndSelfDestruct(
FROM_HERE, ManifestSilentUpdateCheckResult::kAppSilentlyUpdated);
return;
}
// Update the web app with non-security sensitive changes and store security
// sensitive changes to pending update info.
WritePendingUpdateInfoThenComplete(std::move(*pending_update_info));
}
void ManifestSilentUpdateCommand::WritePendingUpdateInfoThenComplete(
proto::PendingUpdateInfo pending_update) {
// Evaluate before `pending_update` is std::move'd.
bool has_pending_icons_to_write = !pending_update.trusted_icons().empty();
{
web_app::ScopedRegistryUpdate update =
app_lock_->sync_bridge().BeginUpdate();
web_app::WebApp* app_to_update = update->UpdateApp(app_id_);
// Record if we are adding a pending update if there wasn't one before, so
// can correctly notify observers only if there was a change.
pending_updated_added_ = !app_to_update->pending_update_info().has_value();
CHECK(app_to_update);
app_to_update->SetPendingUpdateInfo(std::move(pending_update));
}
if (!has_pending_icons_to_write) {
CompleteCommandAndSelfDestruct(
FROM_HERE,
silent_update_required_
? ManifestSilentUpdateCheckResult::
kAppHasNonSecurityAndSecurityChanges
: ManifestSilentUpdateCheckResult::kAppOnlyHasSecurityUpdate);
return;
}
CHECK(!pending_trusted_icon_bitmaps_.empty());
CHECK(!pending_manifest_icon_bitmaps_.empty());
// Write the pending trusted and pending manifest icon bitmaps to disk.
SetStage(
ManifestSilentUpdateCommandStage::kWritingPendingUpdateIconBitmapsToDisk);
app_lock_->icon_manager().WritePendingIconData(
app_id_, std::move(pending_trusted_icon_bitmaps_),
std::move(pending_manifest_icon_bitmaps_),
base::BindOnce(
[](bool silent_update_required, bool bitmaps_write_success) {
if (!bitmaps_write_success) {
return ManifestSilentUpdateCheckResult::
kPendingIconWriteToDiskFailed;
}
if (silent_update_required) {
return ManifestSilentUpdateCheckResult::
kAppHasNonSecurityAndSecurityChanges;
}
return ManifestSilentUpdateCheckResult::kAppOnlyHasSecurityUpdate;
},
silent_update_required_)
.Then(base::BindOnce(
&ManifestSilentUpdateCommand::CompleteCommandAndSelfDestruct,
GetWeakPtr(), FROM_HERE)));
}
void ManifestSilentUpdateCommand::CompleteCommandAndSelfDestruct(
base::Location location,
ManifestSilentUpdateCheckResult check_result) {
GetMutableDebugValue().Set("result", base::ToString(check_result));
Observe(nullptr);
bool record_update;
CommandResult command_result;
switch (check_result) {
case ManifestSilentUpdateCheckResult::kAppSilentlyUpdated:
case ManifestSilentUpdateCheckResult::kAppHasNonSecurityAndSecurityChanges:
record_update = true;
command_result = CommandResult::kSuccess;
break;
case ManifestSilentUpdateCheckResult::kAppUpToDate:
case ManifestSilentUpdateCheckResult::kAppOnlyHasSecurityUpdate:
case ManifestSilentUpdateCheckResult::kAppNotInstalled:
case ManifestSilentUpdateCheckResult::kWebContentsDestroyed:
case ManifestSilentUpdateCheckResult::kIconReadFromDiskFailed:
case ManifestSilentUpdateCheckResult::kPendingIconWriteToDiskFailed:
case ManifestSilentUpdateCheckResult::kInvalidManifest:
case ManifestSilentUpdateCheckResult::kUserNavigated:
record_update = false;
command_result = CommandResult::kSuccess;
break;
case ManifestSilentUpdateCheckResult::kAppUpdateFailedDuringInstall:
case ManifestSilentUpdateCheckResult::kInvalidPendingUpdateInfo:
case ManifestSilentUpdateCheckResult::kManifestToWebAppInstallInfoError:
record_update = false;
command_result = CommandResult::kFailure;
break;
case ManifestSilentUpdateCheckResult::kSystemShutdown:
NOTREACHED() << "The value should only be specified in the constructor "
"and never given to this method.";
}
if (record_update && app_lock_) {
app_lock_->sync_bridge().SetAppManifestUpdateTime(app_id_,
app_lock_->clock().Now());
}
if (pending_updated_added_) {
app_lock_->registrar().NotifyPendingUpdateInfoChanged(
app_id_, /*pending_update_available=*/true,
base::PassKey<ManifestSilentUpdateCommand>());
}
CompleteAndSelfDestruct(command_result, check_result, location);
}
bool ManifestSilentUpdateCommand::IsWebContentsDestroyed() {
return !web_contents_ || web_contents_->IsBeingDestroyed();
}
void ManifestSilentUpdateCommand::OnAppIconsLoaded(
WebAppIconManager::WebAppBitmaps icon_bitmaps) {
existing_manifest_icon_bitmaps_ = std::move(icon_bitmaps.manifest_icons);
existing_trusted_icon_bitmaps_ = std::move(icon_bitmaps.trusted_icons);
}
void ManifestSilentUpdateCommand::OnShortcutIconsLoaded(
ShortcutsMenuIconBitmaps shortcuts_menu_icon_bitmaps) {
existing_shortcuts_menu_icon_bitmaps_ =
std::move(shortcuts_menu_icon_bitmaps);
}
} // namespace web_app