910e62b5创建于 1月15日历史提交
// Copyright 2023 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/android/webapk/webapk_sync_bridge.h"

#include <memory>
#include <optional>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/browser/android/webapk/webapk_database.h"
#include "chrome/browser/android/webapk/webapk_helpers.h"
#include "chrome/browser/android/webapk/webapk_registry_update.h"
#include "chrome/browser/android/webapk/webapk_restore_task.h"
#include "chrome/browser/android/webapk/webapk_specifics_fetcher.h"
#include "chrome/common/channel_info.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/base/report_unrecoverable_error.h"
#include "components/sync/model/client_tag_based_data_type_processor.h"
#include "components/sync/model/data_type_store.h"
#include "components/sync/model/data_type_store_service.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/web_apk_specifics.pb.h"
#include "components/webapps/browser/android/shortcut_info.h"
#include "components/webapps/browser/android/webapps_icon_utils.h"
#include "components/webapps/common/web_app_id.h"
#include "url/gurl.h"

namespace webapk {

std::unique_ptr<syncer::EntityData> CreateSyncEntityDataFromSpecifics(
    const sync_pb::WebApkSpecifics& app) {
  // The Sync System doesn't allow empty entity_data name.
  CHECK(!app.name().empty());

  auto entity_data = std::make_unique<syncer::EntityData>();
  entity_data->name = app.name();

  *(entity_data->specifics.mutable_web_apk()) = app;
  return entity_data;
}

std::unique_ptr<syncer::EntityData> CreateSyncEntityData(
    const WebApkProto& app) {
  return CreateSyncEntityDataFromSpecifics(app.sync_data());
}

webapps::AppId ManifestIdStrToAppId(const std::string& manifest_id) {
  GURL manifest_id_gurl(manifest_id);
  if (!manifest_id_gurl.is_valid()) {
    LOG(ERROR) << "Invalid manifest_id: " << manifest_id;
    return "";
  }
  return GenerateAppIdFromManifestId(manifest_id_gurl.GetWithoutRef());
}

namespace {

constexpr base::TimeDelta kRecentAppMaxAge = base::Days(30);
constexpr char kSyncedWebApkAdditionHistogramName[] =
    "WebApk.Sync.SyncedWebApkAddition";

const WebApkProto* GetAppById(const Registry& registry,
                              const webapps::AppId& app_id) {
  auto it = registry.find(app_id);
  if (it != registry.end()) {
    return it->second.get();
  }

  return nullptr;
}

WebApkProto* GetAppByIdMutable(const Registry& registry,
                               const webapps::AppId& app_id) {
  return const_cast<WebApkProto*>(GetAppById(registry, app_id));
}

std::unique_ptr<WebApkProto> WebApkProtoFromSpecifics(
    const sync_pb::WebApkSpecifics* app,
    bool installed) {
  std::unique_ptr<WebApkProto> app_proto = std::make_unique<WebApkProto>();
  app_proto->set_is_locally_installed(installed);

  sync_pb::WebApkSpecifics* mutable_specifics = app_proto->mutable_sync_data();
  *mutable_specifics = *app;

  return app_proto;
}

std::unique_ptr<WebApkProto> CloneWebApkProto(const WebApkProto& app) {
  std::unique_ptr<WebApkProto> clone = std::make_unique<WebApkProto>();

  *clone = app;

  sync_pb::WebApkSpecifics* mutable_specifics = clone->mutable_sync_data();
  *mutable_specifics = app.sync_data();

  return clone;
}

// Returns true if the specifics' timestamp is at most kRecentAppMaxAge before
// |time|. In other words, if |time| is Now, then this returns whether the
// specifics is at most kRecentAppMaxAge old.
bool AppWasUsedRecentlyComparedTo(const sync_pb::WebApkSpecifics* specifics,
                                  const base::Time time) {
  base::Time app_last_used = base::Time::FromDeltaSinceWindowsEpoch(
      base::Microseconds(specifics->last_used_time_windows_epoch_micros()));
  return time - app_last_used < kRecentAppMaxAge;
}

// Create a |webapps::ShortcutInfo| from the synced |webapk_specifics|.
// If the data is invalid, returns nullptr.
std::unique_ptr<webapps::ShortcutInfo> CreateShortcutInfoFromSpecifics(
    const sync_pb::WebApkSpecifics& webapk_specifics) {
  GURL start_url(GURL(webapk_specifics.start_url()));
  if (!start_url.is_valid()) {
    return nullptr;
  }
  auto shortcut_info = std::make_unique<webapps::ShortcutInfo>(start_url);
  GURL manifest_id(webapk_specifics.manifest_id());
  if (manifest_id.is_valid()) {
    shortcut_info->manifest_id = manifest_id;
  }
  GURL scope(webapk_specifics.scope());
  if (scope.is_valid()) {
    shortcut_info->scope = scope;
  }
  std::u16string name = base::UTF8ToUTF16(webapk_specifics.name());
  shortcut_info->user_title = name;
  shortcut_info->name = name;
  shortcut_info->short_name = name;
  if (webapk_specifics.icon_infos().size() > 0) {
    shortcut_info->best_primary_icon_url =
        GURL(webapk_specifics.icon_infos(0).url());
    shortcut_info->is_primary_icon_maskable =
        webapk_specifics.icon_infos(0).purpose() ==
            sync_pb::WebApkIconInfo_Purpose_MASKABLE;
  } else {
    // If there is no icon url in sync data, put |start_url| as a place holder
    // for primary icon url. Download icon will fallback to generated icon.
    shortcut_info->best_primary_icon_url = start_url;
  }
  return shortcut_info;
}

// Legacy (pre-manifest-id) WebAPKs can have empty manifest_ids. These, in turn,
// get translated into empty app_ids via ManifestIdStrToAppId(). If we end up
// with an empty app_id, generally we need to abort Sync-handling and ignore
// that WebAPK for Sync purposes.
bool IsLegacyAppId(webapps::AppId app_id) {
  return app_id.empty();
}

}  // anonymous namespace

WebApkSyncBridge::WebApkSyncBridge(
    syncer::DataTypeStoreService* data_type_store_service,
    base::OnceClosure on_initialized)
    : WebApkSyncBridge(
          data_type_store_service,
          std::move(on_initialized),
          std::make_unique<syncer::ClientTagBasedDataTypeProcessor>(
              syncer::WEB_APKS,
              base::BindRepeating(&syncer::ReportUnrecoverableError,
                                  chrome::GetChannel())),
          std::make_unique<base::DefaultClock>(),
          std::make_unique<WebApkSpecificsFetcher>()) {}

WebApkSyncBridge::WebApkSyncBridge(
    syncer::DataTypeStoreService* data_type_store_service,
    base::OnceClosure on_initialized,
    std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
    std::unique_ptr<base::Clock> clock,
    std::unique_ptr<AbstractWebApkSpecificsFetcher> specifics_fetcher)
    : syncer::DataTypeSyncBridge(std::move(change_processor)),
      database_(
          data_type_store_service,
          base::BindRepeating(&WebApkSyncBridge::ReportErrorToChangeProcessor,
                              base::Unretained(this))),
      clock_(std::move(clock)),
      webapk_specifics_fetcher_(std::move(specifics_fetcher)) {
  database_.OpenDatabase(base::BindOnce(&WebApkSyncBridge::OnDatabaseOpened,
                                        weak_ptr_factory_.GetWeakPtr(),
                                        std::move(on_initialized)));
}

WebApkSyncBridge::~WebApkSyncBridge() = default;

void WebApkSyncBridge::ReportErrorToChangeProcessor(
    const syncer::ModelError& error) {
  change_processor()->ReportError(error);
}

void WebApkSyncBridge::OnDatabaseOpened(
    base::OnceClosure callback,
    Registry registry,
    std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
  DCHECK(database_.is_opened());

  // Provide sync metadata to the processor _before_ any local changes occur.
  change_processor()->ModelReadyToSync(std::move(metadata_batch));

  registry_ = std::move(registry);
  std::move(callback).Run();
  for (auto& task : init_done_callback_) {
    std::move(task).Run(/* initialized= */ true);
  }
}

std::unique_ptr<syncer::MetadataChangeList>
WebApkSyncBridge::CreateMetadataChangeList() {
  return syncer::DataTypeStore::WriteBatch::CreateMetadataChangeList();
}

bool WebApkSyncBridge::AppWasUsedRecently(
    const sync_pb::WebApkSpecifics* specifics) const {
  return AppWasUsedRecentlyComparedTo(specifics, clock_->Now());
}

void WebApkSyncBridge::OnDataWritten(CommitCallback callback, bool success) {
  if (!success) {
    DLOG(ERROR) << "WebApkSyncBridge commit failed";
  }

  base::UmaHistogramBoolean("WebApk.Database.WriteResult", success);
  std::move(callback).Run(success);
}

void WebApkSyncBridge::ApplyIncrementalSyncChangesToRegistry(
    std::unique_ptr<RegistryUpdateData> update_data) {
  if (update_data->isEmpty()) {
    return;
  }

  for (auto& app : update_data->apps_to_create) {
    webapps::AppId app_id =
        ManifestIdStrToAppId(app->sync_data().manifest_id());
    if (IsLegacyAppId(app_id)) {
      continue;
    }

    auto it = registry_.find(app_id);
    if (it != registry_.end()) {
      registry_.erase(it);
    }
    registry_.emplace(std::move(app_id), std::move(app));
  }

  for (const webapps::AppId& app_id : update_data->apps_to_delete) {
    auto it = registry_.find(app_id);
    CHECK(it != registry_.end());
    registry_.erase(it);
  }
}

bool WebApkSyncBridge::SyncDataContainsNewApps(
    const std::vector<std::unique_ptr<sync_pb::WebApkSpecifics>>&
        installed_apps,
    const syncer::EntityChangeList& sync_changes) const {
  std::set<webapps::AppId> sync_update_from_installed_set;
  for (const std::unique_ptr<sync_pb::WebApkSpecifics>& sync_update :
       installed_apps) {
    webapps::AppId app_id = ManifestIdStrToAppId(sync_update->manifest_id());
    if (!IsLegacyAppId(app_id)) {
      sync_update_from_installed_set.insert(app_id);
    }
  }

  for (const auto& sync_change : sync_changes) {
    if (sync_update_from_installed_set.count(sync_change->storage_key()) != 0) {
      continue;
    }

    if (sync_change->type() == syncer::EntityChange::ACTION_DELETE) {
      continue;
    }

    // There are changes from sync that aren't installed on the device.
    return true;
  }

  return false;
}

std::optional<syncer::ModelError> WebApkSyncBridge::MergeFullSyncData(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_changes) {
  CHECK(change_processor()->IsTrackingMetadata());

  const std::vector<std::unique_ptr<sync_pb::WebApkSpecifics>> installed_apps =
      webapk_specifics_fetcher_->GetWebApkSpecifics();

  WebappRegistry::SetNeedsPwaRestore(
      SyncDataContainsNewApps(installed_apps, entity_changes));

  // Since we're using "account-only" semantics for Transport Mode, we just call
  // through to ApplyIncrementalSyncChanges().
  return ApplyIncrementalSyncChanges(std::move(metadata_change_list),
                                     std::move(entity_changes));
}

void WebApkSyncBridge::RegisterDoneInitializingCallback(
    base::OnceCallback<void(bool)> init_done_callback) {
  if (database_.is_opened()) {
    std::move(init_done_callback).Run(/* initialized= */ true);
    return;
  }

  init_done_callback_.push_back(std::move(init_done_callback));
}

void WebApkSyncBridge::MergeSyncDataForTesting(
    std::vector<std::vector<std::string>> app_vector) {
  CHECK(database_.is_opened());

  std::unique_ptr<syncer::MetadataChangeList> metadata_change_list =
      syncer::DataTypeStore::WriteBatch::CreateMetadataChangeList();
  std::unique_ptr<webapk::RegistryUpdateData> registry_update =
      std::make_unique<webapk::RegistryUpdateData>();

  for (auto const& app : app_vector) {
    std::unique_ptr<sync_pb::WebApkSpecifics> specifics =
        std::make_unique<sync_pb::WebApkSpecifics>();
    specifics->set_start_url(app[0]);
    specifics->set_manifest_id(app[0]);
    specifics->set_name(app[1]);

    const std::string icon_url = app[2];
    const int32_t icon_size_in_px = 256;
    const sync_pb::WebApkIconInfo_Purpose icon_purpose =
        sync_pb::WebApkIconInfo_Purpose_ANY;
    sync_pb::WebApkIconInfo* icon_info = specifics->add_icon_infos();
    icon_info->set_size_in_px(icon_size_in_px);
    icon_info->set_url(icon_url);
    icon_info->set_purpose(icon_purpose);

    specifics->set_last_used_time_windows_epoch_micros(
        base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());

    registry_update->apps_to_create.push_back(
        WebApkProtoFromSpecifics(specifics.get(), false));
  }

  database_.Write(
      *registry_update, std::move(metadata_change_list),
      base::BindOnce(&WebApkSyncBridge::OnDataWritten,
                     weak_ptr_factory_.GetWeakPtr(), base::DoNothing()));

  ApplyIncrementalSyncChangesToRegistry(std::move(registry_update));
}

void WebApkSyncBridge::PrepareRegistryUpdateFromSyncApps(
    const syncer::EntityChangeList& sync_changes,
    RegistryUpdateData* registry_update_from_sync) const {
  for (const auto& sync_change : sync_changes) {
    if (sync_change->type() == syncer::EntityChange::ACTION_DELETE) {
      // There's no need to queue up a deletion if the app doesn't exist in the
      // registry in the first place.
      if (GetAppByIdMutable(registry_, sync_change->storage_key()) != nullptr) {
        registry_update_from_sync->apps_to_delete.push_back(
            sync_change->storage_key());
      }
      continue;
    }

    CHECK(sync_change->data().specifics.has_web_apk());
    registry_update_from_sync->apps_to_create.push_back(
        WebApkProtoFromSpecifics(&sync_change->data().specifics.web_apk(),
                                 false /* installed */));
  }
}

std::optional<syncer::ModelError> WebApkSyncBridge::ApplyIncrementalSyncChanges(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_changes) {
  std::unique_ptr<RegistryUpdateData> registry_update_from_sync =
      std::make_unique<RegistryUpdateData>();
  PrepareRegistryUpdateFromSyncApps(entity_changes,
                                    registry_update_from_sync.get());

  database_.Write(
      *registry_update_from_sync, std::move(metadata_change_list),
      base::BindOnce(&WebApkSyncBridge::OnDataWritten,
                     weak_ptr_factory_.GetWeakPtr(), base::DoNothing()));

  ApplyIncrementalSyncChangesToRegistry(std::move(registry_update_from_sync));

  return std::nullopt;
}

void WebApkSyncBridge::OnWebApkUsed(
    std::unique_ptr<sync_pb::WebApkSpecifics> app_specifics,
    bool is_install) {
  if (!change_processor()->IsTrackingMetadata()) {
    return;
  }

  AddOrModifyAppInSync(
      WebApkProtoFromSpecifics(app_specifics.get(), true /* installed */),
      is_install);
}

void WebApkSyncBridge::OnWebApkUninstalled(const std::string& manifest_id) {
  if (!change_processor()->IsTrackingMetadata()) {
    return;
  }

  webapps::AppId app_id = ManifestIdStrToAppId(manifest_id);
  if (IsLegacyAppId(app_id)) {
    return;
  }
  WebApkProto* app = GetAppByIdMutable(registry_, app_id);

  if (app == nullptr) {
    return;
  }

  if (!AppWasUsedRecently(&app->sync_data())) {
    DeleteAppsFromSync(std::vector<webapps::AppId>{app_id},
                       database_.is_opened());
    return;
  }

  // This updates the registry entry directly, so we don't need to call
  // ApplyIncrementalSyncChangesToRegistry() later.
  app->set_is_locally_installed(false);

  // We don't need to update Sync, since this change only affects the
  // non-Specifics part of the proto.
  std::unique_ptr<RegistryUpdateData> registry_update =
      std::make_unique<RegistryUpdateData>();
  registry_update->apps_to_create.push_back(CloneWebApkProto(*app));

  database_.Write(
      *registry_update,
      syncer::DataTypeStore::WriteBatch::CreateMetadataChangeList(),
      base::BindOnce(&WebApkSyncBridge::OnDataWritten,
                     weak_ptr_factory_.GetWeakPtr(), base::DoNothing()));
}

std::vector<WebApkRestoreData> WebApkSyncBridge::GetRestorableAppsShortcutInfo()
    const {
  std::vector<WebApkRestoreData> results;
  for (auto const& [appId, proto] : registry_) {
    if (!proto->is_locally_installed() &&
        AppWasUsedRecently(&proto->sync_data())) {
      auto restore_info = CreateShortcutInfoFromSpecifics(proto->sync_data());
      if (restore_info) {
        results.emplace_back(appId, std::move(restore_info));
      }
    }
  }
  return results;
}

const WebApkProto* WebApkSyncBridge::GetWebApkByAppId(
    webapps::AppId app_id) const {
  return GetAppById(registry_, app_id);
}

std::unique_ptr<syncer::DataBatch> WebApkSyncBridge::GetDataForCommit(
    StorageKeyList storage_keys) {
  auto data_batch = std::make_unique<syncer::MutableDataBatch>();

  for (const webapps::AppId& app_id : storage_keys) {
    const WebApkProto* app = GetAppById(registry_, app_id);
    if (app) {
      data_batch->Put(app_id, CreateSyncEntityData(*app));
    }
  }

  return data_batch;
}

std::unique_ptr<syncer::DataBatch> WebApkSyncBridge::GetAllDataForDebugging() {
  auto data_batch = std::make_unique<syncer::MutableDataBatch>();

  for (const auto& appListing : registry_) {
    const webapps::AppId app_id = appListing.first;
    const WebApkProto& app = *appListing.second;
    data_batch->Put(app_id, CreateSyncEntityData(app));
  }

  return data_batch;
}

// GetClientTag and GetStorageKey must return the same thing for a given AppId
// as the dPWA implementation in
// chrome/browser/web_applications/web_app_sync_bridge.cc's
// WebAppSyncBridge::GetClientTag().
std::string WebApkSyncBridge::GetClientTag(
    const syncer::EntityData& entity_data) const {
  DCHECK(entity_data.specifics.has_web_apk());

  return ManifestIdStrToAppId(entity_data.specifics.web_apk().manifest_id());
}

std::string WebApkSyncBridge::GetStorageKey(
    const syncer::EntityData& entity_data) const {
  return GetClientTag(entity_data);
}

bool WebApkSyncBridge::IsEntityDataValid(
    const syncer::EntityData& entity_data) const {
  return !entity_data.specifics.web_apk().manifest_id().empty();
}

void WebApkSyncBridge::ApplyDisableSyncChanges(
    std::unique_ptr<syncer::MetadataChangeList> delete_metadata_change_list) {
  database_.DeleteAllDataAndMetadata(base::DoNothing());

  registry_.clear();
}

void WebApkSyncBridge::RemoveOldWebAPKsFromSync(
    int64_t current_time_ms_since_unix_epoch) {
  std::vector<webapps::AppId> app_ids;
  for (const auto& appListing : registry_) {
    const webapps::AppId app_id = appListing.first;
    const WebApkProto& app = *appListing.second;
    if (!AppWasUsedRecentlyComparedTo(
            &app.sync_data(), base::Time::FromMillisecondsSinceUnixEpoch(
                                  current_time_ms_since_unix_epoch))) {
      app_ids.push_back(app_id);
    }
  }

  RegisterDoneInitializingCallback(
      base::BindOnce(&WebApkSyncBridge::DeleteAppsFromSync,
                     weak_ptr_factory_.GetWeakPtr(), app_ids));
}

void WebApkSyncBridge::AddOrModifyAppInSync(std::unique_ptr<WebApkProto> app,
                                            bool is_install) {
  webapps::AppId app_id = ManifestIdStrToAppId(app->sync_data().manifest_id());
  if (IsLegacyAppId(app_id)) {
    return;
  }
  RecordSyncedWebApkAdditionHistogram(is_install, registry_.count(app_id) > 0);

  std::unique_ptr<syncer::EntityData> entity_data =
      CreateSyncEntityDataFromSpecifics(app->sync_data());

  std::unique_ptr<syncer::MetadataChangeList> metadata_change_list =
      syncer::DataTypeStore::WriteBatch::CreateMetadataChangeList();
  change_processor()->Put(app_id, std::move(entity_data),
                          metadata_change_list.get());

  std::unique_ptr<RegistryUpdateData> registry_update =
      std::make_unique<RegistryUpdateData>();
  registry_update->apps_to_create.push_back(std::move(app));

  database_.Write(
      *registry_update, std::move(metadata_change_list),
      base::BindOnce(&WebApkSyncBridge::OnDataWritten,
                     weak_ptr_factory_.GetWeakPtr(), base::DoNothing()));

  ApplyIncrementalSyncChangesToRegistry(std::move(registry_update));
}

void WebApkSyncBridge::DeleteAppsFromSync(
    const std::vector<webapps::AppId>& app_ids,
    bool database_opened) {
  if (app_ids.size() == 0 || !database_opened) {
    return;
  }

  RecordSyncedWebApkRemovalCountHistogram(app_ids.size());

  std::unique_ptr<syncer::MetadataChangeList> metadata_change_list =
      syncer::DataTypeStore::WriteBatch::CreateMetadataChangeList();
  std::unique_ptr<RegistryUpdateData> registry_update =
      std::make_unique<RegistryUpdateData>();

  for (const webapps::AppId& app_id : app_ids) {
    change_processor()->Delete(app_id, syncer::DeletionOrigin::Unspecified(),
                               metadata_change_list.get());
    registry_update->apps_to_delete.push_back(app_id);
  }

  database_.Write(
      *registry_update, std::move(metadata_change_list),
      base::BindOnce(&WebApkSyncBridge::OnDataWritten,
                     weak_ptr_factory_.GetWeakPtr(), base::DoNothing()));

  ApplyIncrementalSyncChangesToRegistry(std::move(registry_update));
}

void WebApkSyncBridge::SetClockForTesting(std::unique_ptr<base::Clock> clock) {
  clock_ = std::move(clock);
}

const Registry& WebApkSyncBridge::GetRegistryForTesting() const {
  return registry_;
}

base::WeakPtr<syncer::DataTypeControllerDelegate>
WebApkSyncBridge::GetDataTypeControllerDelegate() {
  return change_processor()->GetControllerDelegate();
}

void WebApkSyncBridge::RecordSyncedWebApkAdditionHistogram(
    bool is_install,
    bool already_exists_in_sync) const {
  if (is_install && !already_exists_in_sync) {
    base::UmaHistogramEnumeration(
        kSyncedWebApkAdditionHistogramName,
        AddOrModifyType::kNewInstallOnDeviceAndNewAddToSync);
  } else if (is_install && already_exists_in_sync) {
    base::UmaHistogramEnumeration(
        kSyncedWebApkAdditionHistogramName,
        AddOrModifyType::kNewInstallOnDeviceAndModificationToSync);
  } else if (!is_install && !already_exists_in_sync) {
    base::UmaHistogramEnumeration(
        kSyncedWebApkAdditionHistogramName,
        AddOrModifyType::kLaunchOnDeviceAndNewAddToSync);
  } else {
    base::UmaHistogramEnumeration(
        kSyncedWebApkAdditionHistogramName,
        AddOrModifyType::kLaunchOnDeviceAndModificationToSync);
  }
}

void WebApkSyncBridge::RecordSyncedWebApkRemovalCountHistogram(
    int num_web_apks_removed) const {
  base::UmaHistogramExactLinear("WebApk.Sync.SyncedWebApkRemovalCount",
                                num_web_apks_removed, 51 /* max_count */);
}

}  // namespace webapk