chromium/ash/system/bluetooth/bluetooth_device_list_controller_impl.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/system/bluetooth/bluetooth_device_list_controller_impl.h"

#include "ash/strings/grit/ash_strings.h"
#include "ash/system/bluetooth/bluetooth_detailed_view.h"
#include "ash/system/bluetooth/bluetooth_device_list_item_view.h"
#include "base/check.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/view.h"

namespace ash {

namespace {

using bluetooth_config::mojom::PairedBluetoothDevicePropertiesPtr;

// Helper function to remove |*view| from its view hierarchy, delete the view,
// and reset the value of |*view| to be |nullptr|.
template <class T>
void RemoveAndResetViewIfExists(raw_ptr<T>* view) {
  DCHECK(view);

  if (!*view)
    return;

  views::View* parent = (*view)->parent();

  if (parent) {
    parent->RemoveChildViewT(view->ExtractAsDangling());
  }
}

}  // namespace

BluetoothDeviceListControllerImpl::BluetoothDeviceListControllerImpl(
    BluetoothDetailedView* bluetooth_detailed_view)
    : bluetooth_detailed_view_(bluetooth_detailed_view) {}

BluetoothDeviceListControllerImpl::~BluetoothDeviceListControllerImpl() =
    default;

void BluetoothDeviceListControllerImpl::UpdateBluetoothEnabledState(
    bool enabled) {
  if (is_bluetooth_enabled_ && !enabled) {
    device_id_to_view_map_.clear();
    connected_sub_header_ = nullptr;
    no_device_connected_sub_header_ = nullptr;
    previously_connected_sub_header_ = nullptr;
    bluetooth_detailed_view_->device_list()->RemoveAllChildViews();
  }
  is_bluetooth_enabled_ = enabled;
}

void BluetoothDeviceListControllerImpl::UpdateDeviceList(
    const PairedBluetoothDevicePropertiesPtrs& connected,
    const PairedBluetoothDevicePropertiesPtrs& previously_connected) {
  DCHECK(is_bluetooth_enabled_);

  // This function will create views for new devices, re-use views for existing
  // devices, and remove views for devices that no longer exist. To do this, we
  // keep track of all the preexisting views in |previous_views|, removing a
  // view from this map when the corresponding device is found in |connected| or
  // |previously_connected|. Before returning, any view remaining in
  // |previous_views| is no longer needed and is deleted.
  base::flat_map<std::string, BluetoothDeviceListItemView*> previous_views =
      std::move(device_id_to_view_map_);
  device_id_to_view_map_.clear();

  // Since we re-use views when possible, we need to re-order them to match the
  // order of the devices we are provided with. We use |index| to keep track of
  // the next index within the device list where a view should be placed, i.e.
  // all views before |index| are in their final position.
  size_t index = 0;

  // The list of connected devices.
  if (!connected.empty()) {
    connected_sub_header_ = CreateSubHeaderIfMissingAndReorder(
        connected_sub_header_,
        IDS_ASH_STATUS_TRAY_BLUETOOTH_CURRENTLY_CONNECTED_DEVICES, index);

    // Increment |index| since this position was taken by
    // |connected_sub_header_|.
    index++;

    index = CreateViewsIfMissingAndReorder(connected, &previous_views, index);
  } else {
    RemoveAndResetViewIfExists(&connected_sub_header_);
  }

  // The previously connected devices.
  if (!previously_connected.empty()) {
    previously_connected_sub_header_ = CreateSubHeaderIfMissingAndReorder(
        previously_connected_sub_header_,
        IDS_ASH_STATUS_TRAY_BLUETOOTH_PREVIOUSLY_CONNECTED_DEVICES, index);

    // Increment |index| since this position was taken by
    // |previously_connected_sub_header_|.
    index++;

    // Ignore the returned index since we are now done re-ordering the list.
    CreateViewsIfMissingAndReorder(previously_connected, &previous_views,
                                   index);
  } else {
    RemoveAndResetViewIfExists(&previously_connected_sub_header_);
  }

  // The header when there are no connected or previously connected devices.
  if (device_id_to_view_map_.empty()) {
    no_device_connected_sub_header_ = CreateSubHeaderIfMissingAndReorder(
        no_device_connected_sub_header_,
        IDS_ASH_STATUS_TRAY_BLUETOOTH_NO_DEVICE_CONNECTED, index);
  } else {
    RemoveAndResetViewIfExists(&no_device_connected_sub_header_);
  }

  for (const auto& id_and_view : previous_views) {
    bluetooth_detailed_view_->device_list()->RemoveChildViewT(
        id_and_view.second);
  }
  bluetooth_detailed_view_->NotifyDeviceListChanged();
}

views::View*
BluetoothDeviceListControllerImpl::CreateSubHeaderIfMissingAndReorder(
    views::View* sub_header,
    int text_id,
    size_t index) {
  if (!sub_header) {
    sub_header = bluetooth_detailed_view_->AddDeviceListSubHeader(
        gfx::kNoneIcon, text_id);
  }
  bluetooth_detailed_view_->device_list()->ReorderChildView(sub_header, index);
  return sub_header;
}

size_t BluetoothDeviceListControllerImpl::CreateViewsIfMissingAndReorder(
    const PairedBluetoothDevicePropertiesPtrs& device_property_list,
    base::flat_map<std::string, BluetoothDeviceListItemView*>* previous_views,
    size_t index) {
  DCHECK(previous_views);

  BluetoothDeviceListItemView* device_view = nullptr;

  const size_t device_count = device_property_list.size();

  for (size_t i = 0; i < device_count; ++i) {
    const PairedBluetoothDevicePropertiesPtr& device_properties =
        device_property_list.at(i);
    const std::string& device_id = device_properties->device_properties->id;
    auto it = previous_views->find(device_id);

    if (it == previous_views->end()) {
      device_view = bluetooth_detailed_view_->AddDeviceListItem();
    } else {
      device_view = it->second;
      previous_views->erase(it);
    }
    device_id_to_view_map_.emplace(device_id, device_view);

    device_view->UpdateDeviceProperties(i, device_count, device_properties);
    bluetooth_detailed_view_->device_list()->ReorderChildView(device_view,
                                                              index);

    // Increment |index| since this position was taken by |device_view|.
    index++;
  }
  return index;
}

}  // namespace ash