chromium/ash/quick_pair/repository/fast_pair/device_image_store.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/quick_pair/repository/fast_pair/device_image_store.h"

#include "ash/quick_pair/proto/fastpair.pb.h"
#include "ash/quick_pair/proto/fastpair_data.pb.h"
#include "ash/quick_pair/repository/fast_pair/fast_pair_image_decoder.h"
#include "ash/shell.h"
#include "base/values.h"
#include "chromeos/ash/services/bluetooth_config/public/cpp/device_image_info.h"
#include "components/cross_device/logging/logging.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "ui/base/webui/web_ui_util.h"
#include "url/gurl.h"

namespace ash::quick_pair {

// Alias DeviceImageInfo for convenience.
using bluetooth_config::DeviceImageInfo;

// static
constexpr char DeviceImageStore::kDeviceImageStorePref[];

// static
void DeviceImageStore::RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(kDeviceImageStorePref);
}

DeviceImageStore::DeviceImageStore(FastPairImageDecoder* image_decoder)
    : image_decoder_(image_decoder) {}

DeviceImageStore::~DeviceImageStore() = default;

void DeviceImageStore::FetchDeviceImages(
    const std::string& model_id,
    DeviceMetadata* device_metadata,
    FetchDeviceImagesCallback on_images_saved_callback) {
  DCHECK(device_metadata);
  DeviceImageInfo& images = model_id_to_images_[model_id];

  if (images.default_image().empty() && !device_metadata->image().IsEmpty()) {
    SaveImageAsBase64(model_id, DeviceImageType::kDefault,
                      on_images_saved_callback, device_metadata->image());
  } else {
    on_images_saved_callback.Run(std::make_pair(
        DeviceImageType::kDefault, FetchDeviceImagesResult::kSkipped));
  }

  nearby::fastpair::TrueWirelessHeadsetImages true_wireless_images =
      device_metadata->GetDetails().true_wireless_images();

  if (images.left_bud_image().empty() &&
      true_wireless_images.has_left_bud_url()) {
    DecodeImage(model_id, DeviceImageType::kLeftBud,
                true_wireless_images.left_bud_url(), on_images_saved_callback);
  } else {
    on_images_saved_callback.Run(std::make_pair(
        DeviceImageType::kLeftBud, FetchDeviceImagesResult::kSkipped));
  }

  if (images.right_bud_image().empty() &&
      true_wireless_images.has_right_bud_url()) {
    DecodeImage(model_id, DeviceImageType::kRightBud,
                true_wireless_images.right_bud_url(), on_images_saved_callback);
  } else {
    on_images_saved_callback.Run(std::make_pair(
        DeviceImageType::kRightBud, FetchDeviceImagesResult::kSkipped));
  }

  if (images.case_image().empty() && true_wireless_images.has_case_url()) {
    DecodeImage(model_id, DeviceImageType::kCase,
                true_wireless_images.case_url(), on_images_saved_callback);
  } else {
    on_images_saved_callback.Run(std::make_pair(
        DeviceImageType::kCase, FetchDeviceImagesResult::kSkipped));
  }
}

bool DeviceImageStore::PersistDeviceImages(const std::string& model_id) {
  DeviceImageInfo& images = model_id_to_images_[model_id];

  if (!DeviceImageInfoHasImages(images)) {
    CD_LOG(WARNING, Feature::FP)
        << __func__
        << ": Attempted to persist non-existent images for model ID: " +
               model_id;
    return false;
  }
  PrefService* local_state = Shell::Get()->local_state();
  if (!local_state) {
    CD_LOG(WARNING, Feature::FP)
        << __func__ << ": No shell local state available.";
    return false;
  }
  ScopedDictPrefUpdate device_image_store(local_state, kDeviceImageStorePref);
  // TODO(dclasson): Once we add TrueWireless support, need to modify this to
  // merge new & persisted images objects.
  if (!device_image_store->Set(model_id, images.ToDictionaryValue())) {
    CD_LOG(WARNING, Feature::FP)
        << __func__
        << ": Failed to persist images to prefs for model ID: " + model_id;
    return false;
  }
  return true;
}

bool DeviceImageStore::EvictDeviceImages(const std::string& model_id) {
  PrefService* local_state = Shell::Get()->local_state();
  if (!local_state) {
    CD_LOG(WARNING, Feature::FP)
        << __func__ << ": No shell local state available.";
    return false;
  }
  ScopedDictPrefUpdate device_image_store(local_state, kDeviceImageStorePref);
  if (!device_image_store->Remove(model_id)) {
    CD_LOG(WARNING, Feature::FP)
        << __func__
        << ": Failed to evict images from prefs for model ID: " + model_id;
    return false;
  }
  return true;
}

std::optional<DeviceImageInfo> DeviceImageStore::GetImagesForDeviceModel(
    const std::string& model_id) {
  // Lazily load saved images from prefs the first time we get an image.
  if (!loaded_images_from_prefs_) {
    loaded_images_from_prefs_ = true;
    LoadPersistedImagesFromPrefs();
  }

  DeviceImageInfo& images = model_id_to_images_[model_id];

  if (!DeviceImageInfoHasImages(images)) {
    return std::nullopt;
  }
  return images;
}

void DeviceImageStore::LoadPersistedImagesFromPrefs() {
  PrefService* local_state = Shell::Get()->local_state();
  if (!local_state) {
    CD_LOG(WARNING, Feature::FP)
        << __func__ << ": No shell local state available.";
    return;
  }
  const base::Value::Dict& device_image_store =
      local_state->GetDict(kDeviceImageStorePref);
  for (auto [model_id, image_dict] : device_image_store) {
    std::optional<DeviceImageInfo> images;
    if (image_dict.is_dict()) {
      images = DeviceImageInfo::FromDictionaryValue(image_dict.GetDict());
    }
    if (!images) {
      CD_LOG(WARNING, Feature::FP)
          << __func__ << ": Failed to load persisted images from prefs.";
      continue;
    }
    model_id_to_images_[model_id] = images.value();
  }
}

bool DeviceImageStore::DeviceImageInfoHasImages(
    const DeviceImageInfo& images) const {
  return !images.default_image_.empty() || !images.left_bud_image_.empty() ||
         !images.right_bud_image_.empty() || !images.case_image_.empty();
}

void DeviceImageStore::DecodeImage(
    const std::string& model_id,
    DeviceImageType image_type,
    const std::string& image_url,
    FetchDeviceImagesCallback on_images_saved_callback) {
  DCHECK(image_decoder_);
  image_decoder_->DecodeImageFromUrl(
      GURL(image_url), /*resize_to_notification_size=*/true,
      base::BindOnce(&DeviceImageStore::SaveImageAsBase64,
                     weak_ptr_factory_.GetWeakPtr(), model_id, image_type,
                     std::move(on_images_saved_callback)));
}

void DeviceImageStore::SaveImageAsBase64(
    const std::string& model_id,
    DeviceImageType image_type,
    FetchDeviceImagesCallback on_images_saved_callback,
    gfx::Image image) {
  if (image.IsEmpty()) {
    CD_LOG(WARNING, Feature::FP)
        << __func__ << ": Failed to fetch device image.";
    std::move(on_images_saved_callback)
        .Run(std::make_pair(image_type, FetchDeviceImagesResult::kFailure));
    return;
  }

  // Encode the image as a base64 data URL.
  std::string encoded_image = webui::GetBitmapDataUrl(image.AsBitmap());

  // Save the image in the correct path.
  if (image_type == DeviceImageType::kDefault) {
    model_id_to_images_[model_id].default_image_ = encoded_image;
  } else if (image_type == DeviceImageType::kLeftBud) {
    model_id_to_images_[model_id].left_bud_image_ = encoded_image;
  } else if (image_type == DeviceImageType::kRightBud) {
    model_id_to_images_[model_id].right_bud_image_ = encoded_image;
  } else if (image_type == DeviceImageType::kCase) {
    model_id_to_images_[model_id].case_image_ = encoded_image;
  } else {
    CD_LOG(WARNING, Feature::FP)
        << __func__ << ": Can't save device image to invalid image field.";
    std::move(on_images_saved_callback)
        .Run(std::make_pair(DeviceImageType::kNotSupportedType,
                            FetchDeviceImagesResult::kFailure));
    return;
  }

  // Once successfully saved, return success.
  std::move(on_images_saved_callback)
      .Run(std::make_pair(image_type, FetchDeviceImagesResult::kSuccess));
}

void DeviceImageStore::RefreshCacheForTest() {
  CD_LOG(INFO, Feature::FP) << __func__;
  model_id_to_images_.clear();
  LoadPersistedImagesFromPrefs();
}

}  // namespace ash::quick_pair