chromium/chrome/browser/ui/webui/ash/settings/pages/bluetooth/fast_pair_saved_devices_handler.cc

// Copyright 2022 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/ui/webui/ash/settings/pages/bluetooth/fast_pair_saved_devices_handler.h"

#include "ash/quick_pair/common/fast_pair/fast_pair_metrics.h"
#include "ash/quick_pair/repository/fast_pair/fast_pair_image_decoder_impl.h"
#include "ash/quick_pair/repository/fast_pair_repository.h"
#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "components/cross_device/logging/logging.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"

namespace {

const char kLoadSavedDevicePage[] = "loadSavedDevicePage";
const char kRemoveSavedDevice[] = "removeSavedDevice";
const char kOptInStatusMessage[] = "fast-pair-saved-devices-opt-in-status";
const char kSavedDevicesListMessage[] = "fast-pair-saved-devices-list";

std::string EncodeKey(const std::string& decoded_key) {
  return base::HexEncode(
      std::vector<uint8_t>(decoded_key.begin(), decoded_key.end()));
}

// Keys in the JSON representation of a SavedDevice
const char kSavedDeviceNameKey[] = "name";
const char kSavedDeviceImageUrlKey[] = "imageUrl";
const char kSavedDeviceAccountKeyKey[] = "accountKey";

// Converts |device| to a raw dictionary value used as a JSON
// argument to JavaScript functions.
base::Value::Dict SavedDeviceToDictionary(const std::string& device_name,
                                          const std::string& image_url,
                                          const std::string account_key) {
  return base::Value::Dict()
      .Set(kSavedDeviceNameKey, device_name)
      .Set(kSavedDeviceImageUrlKey, image_url)
      .Set(kSavedDeviceAccountKeyKey, EncodeKey(account_key));
}

}  // namespace

namespace ash::settings {

FastPairSavedDevicesHandler::FastPairSavedDevicesHandler()
    : image_decoder_(std::make_unique<quick_pair::FastPairImageDecoderImpl>()) {
}

FastPairSavedDevicesHandler::FastPairSavedDevicesHandler(
    std::unique_ptr<quick_pair::FastPairImageDecoder> image_decoder)
    : image_decoder_(std::move(image_decoder)) {}

FastPairSavedDevicesHandler::~FastPairSavedDevicesHandler() = default;

void FastPairSavedDevicesHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      kLoadSavedDevicePage,
      base::BindRepeating(
          &FastPairSavedDevicesHandler::HandleLoadSavedDevicePage,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      kRemoveSavedDevice,
      base::BindRepeating(&FastPairSavedDevicesHandler::HandleRemoveSavedDevice,
                          base::Unretained(this)));
}

void FastPairSavedDevicesHandler::HandleRemoveSavedDevice(
    const base::Value::List& args) {
  CD_LOG(VERBOSE, Feature::FP) << __func__;
  std::vector<uint8_t> account_key;
  base::HexStringToBytes(args[0].GetString(), &account_key);
  quick_pair::FastPairRepository::Get()->DeleteAssociatedDeviceByAccountKey(
      account_key,
      base::BindOnce(&FastPairSavedDevicesHandler::OnSavedDeviceDeleted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FastPairSavedDevicesHandler::OnSavedDeviceDeleted(bool success) {
  CD_LOG(INFO, Feature::FP)
      << __func__ << ": " << (success ? "success" : "failed");
  quick_pair::RecordSavedDevicesRemoveResult(/*success=*/success);
}

void FastPairSavedDevicesHandler::HandleLoadSavedDevicePage(
    const base::Value::List& args) {
  CD_LOG(VERBOSE, Feature::FP) << __func__;
  AllowJavascript();

  // If the page is already loading, we ignore any new requests to load the
  // page.
  if (loading_saved_device_page_) {
    return;
  }

  loading_saved_device_page_ = true;
  loading_start_time_ = base::TimeTicks::Now();
  quick_pair::FastPairRepository::Get()->GetSavedDevices(
      base::BindOnce(&FastPairSavedDevicesHandler::OnGetSavedDevices,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FastPairSavedDevicesHandler::OnGetSavedDevices(
    nearby::fastpair::OptInStatus status,
    std::vector<nearby::fastpair::FastPairDevice> devices) {
  CD_LOG(VERBOSE, Feature::FP) << __func__;

  // The JavaScript WebUI layer needs an enum to stay in sync with the
  // nearby::fastpair::OptInStatus enum from ash/quick_pair/proto/enums.proto
  // to properly handle this message.
  FireWebUIListener(kOptInStatusMessage, base::Value(static_cast<int>(status)));
  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": Sending opt-in status of " << status;

  // If the device list is empty, we communicate it to the settings page to
  // stop the loading UX screen.
  if (devices.empty()) {
    CD_LOG(VERBOSE, Feature::FP)
        << __func__ << ": No devices saved to the user's account";
    base::Value::List saved_devices_list;
    quick_pair::RecordSavedDevicesCount(
        /*num_devices=*/saved_devices_list.size());
    FireWebUIListener(kSavedDevicesListMessage, saved_devices_list);
    loading_saved_device_page_ = false;
    base::TimeDelta total_load_time =
        base::TimeTicks::Now() - loading_start_time_;
    quick_pair::RecordSavedDevicesTotalUxLoadTime(total_load_time);
    return;
  }

  // The difference between a nearby::fastpair::FastPairDevice and a
  // nearby::fastpair::StoredDiscoveryItem is that although they represent the
  // same devices saved to a user's account, the FastPairDevice contains the
  // raw byte array with the device name and the image bytes, which we will
  // parse into a StoredDiscoveryItem to access the image bytes as strings. We
  // create a flat set of these strings in the case that the
  // user has the same device multiple times, we only decode the image bytes
  // once.
  devices_ = devices;
  base::flat_set<std::string> image_byte_strings;
  for (const auto& device : devices) {
    nearby::fastpair::StoredDiscoveryItem item;
    if (item.ParseFromString(device.discovery_item_bytes()) &&
        item.has_icon_png()) {
      image_byte_strings.insert(item.icon_png());
    }
  }

  if (image_byte_strings.empty()) {
    CD_LOG(VERBOSE, Feature::FP) << __func__ << ": no device images";
    DecodingUrlsFinished();
    return;
  }

  // Image decoding occurs asynchronously in a separate process, so we use
  // a AtomicRefCounter to keep track of the pending tasks remaining, and once
  // they complete, we can continue parsing the saved device data to communicate
  // with the settings page.
  pending_decoding_tasks_count_ =
      std::make_unique<base::AtomicRefCount>(image_byte_strings.size());
  for (std::string image_byte_string : image_byte_strings) {
    image_decoder_->DecodeImage(
        std::vector<uint8_t>(image_byte_string.begin(),
                             image_byte_string.end()),
        /*resize_to_notification_size=*/false,
        base::BindOnce(&FastPairSavedDevicesHandler::SaveImageAsBase64,
                       weak_ptr_factory_.GetWeakPtr(), image_byte_string));
  }
}

void FastPairSavedDevicesHandler::SaveImageAsBase64(
    const std::string& image_byte_string,
    gfx::Image image) {
  if (!image.IsEmpty()) {
    std::string encoded_image = webui::GetBitmapDataUrl(image.AsBitmap());
    image_byte_string_to_encoded_url_map_[image_byte_string] = encoded_image;
  }

  // Even if the image is empty, we want to decrement our task counter to
  // handle when the tasks are completed, and ultimately communicate the
  // list to the settings page. If we don't have an image, we will
  // send the settings page a null url, and it is up to the settings page
  // to handle this case as needed.
  if (!pending_decoding_tasks_count_->Decrement()) {
    DecodingUrlsFinished();
  }
}

void FastPairSavedDevicesHandler::DecodingUrlsFinished() {
  CD_LOG(VERBOSE, Feature::FP) << __func__;

  // We initialize a list of the saved devices that we will parse with the
  // decoded urls we have.
  base::Value::List saved_devices_list;
  saved_devices_list.reserve(devices_.size());

  // |nearby::fastpair::StoredDiscoveryItem| contains information about
  // the device name, |image_byte_string_to_encoded_url_map_| contains the
  // base64 encoded urls for the images, and |nearby::fastpair::FastPairDevice|
  // contains the account key. Here, we reconcile this data for each device
  // and convert it to dictionary, to be communicated to the settings page.
  for (const auto& device : devices_) {
    // If the device does not have an account key, then it was not properly
    // saved to the user's account, and we ignore these devices. Android
    // has devices still in Footprints but marked as deleted by removing the
    // account key, so it is not expected for all of these devices to have
    // account keys.
    if (!device.has_account_key()) {
      continue;
    }

    std::string account_key = device.account_key();
    std::string image_url = "";
    nearby::fastpair::StoredDiscoveryItem item;
    if (item.ParseFromString(device.discovery_item_bytes()) &&
        item.has_icon_png() &&
        base::Contains(image_byte_string_to_encoded_url_map_,
                       item.icon_png())) {
      image_url = image_byte_string_to_encoded_url_map_[item.icon_png()];
    }

    saved_devices_list.Append(SavedDeviceToDictionary(
        /*device_name=*/item.has_title() ? item.title() : "",
        /*image_url=*/image_url,
        /*account_key=*/account_key));
  }

  quick_pair::RecordSavedDevicesCount(
      /*num_devices=*/saved_devices_list.size());
  FireWebUIListener(kSavedDevicesListMessage, saved_devices_list);
  CD_LOG(VERBOSE, Feature::FP) << __func__ << ": Sending device list";
  base::TimeDelta total_load_time =
      base::TimeTicks::Now() - loading_start_time_;
  quick_pair::RecordSavedDevicesTotalUxLoadTime(total_load_time);

  // We reset the state here for another page load that may happened while
  // chrome://os-settings is open, since our decoding tasks are completed.
  devices_.clear();
  image_byte_string_to_encoded_url_map_.clear();
  pending_decoding_tasks_count_.reset();
  loading_saved_device_page_ = false;
}

void FastPairSavedDevicesHandler::OnJavascriptAllowed() {}

void FastPairSavedDevicesHandler::OnJavascriptDisallowed() {}

}  // namespace ash::settings