| // 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 |