chromium/ash/system/video_conference/bubble/vc_tile_ui_controller.cc

// 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 "ash/system/video_conference/bubble/vc_tile_ui_controller.h"

#include <optional>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_utils.h"
#include "base/barrier_callback.h"
#include "base/containers/flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"
#include "chromeos/utils/haptics_util.h"
#include "third_party/cros_system_api/dbus/dlcservice/dbus-constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/events/devices/haptic_touchpad_effects.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/label.h"

namespace ash::video_conference {

VcTileUiController::VcTileUiController(const VcHostedEffect* effect)
    : effect_(effect->get_weak_ptr()) {
  effect_id_ = effect->id();
  effect_state_ = effect->GetWeakState(/*index=*/0);
  effect_state_label_for_debug_ = effect_state_->label_text();
  auto* dlc_service_client = DlcserviceClient::Get();
  if (!dlc_service_client) {
    // `dlc_service_client` may not exist in tests.
    return;
  }
  dlc_service_client->AddObserver(this);
}

VcTileUiController::~VcTileUiController() {
  auto* dlc_service_client = DlcserviceClient::Get();
  if (!dlc_service_client) {
    // `dlc_service_client` may not exist in tests.
    return;
  }
  dlc_service_client->RemoveObserver(this);
}

std::unique_ptr<FeatureTile> VcTileUiController::CreateTile() {
  auto tile = std::make_unique<FeatureTile>(
      base::BindRepeating(&VcTileUiController::OnPressed,
                          weak_ptr_factory_.GetWeakPtr()),
      /*is_togglable=*/true, FeatureTile::TileType::kCompact);
  tile_ = tile->GetWeakPtr();

  // Set up view ids for the tile and its children.
  tile->SetID(BubbleViewID::kToggleEffectsButton);
  tile->label()->SetID(BubbleViewID::kToggleEffectLabel);
  tile->icon_button()->SetID(BubbleViewID::kToggleEffectIcon);

  // Set up the initial state of the tile, including elements like label, icon,
  // and colors based on toggle state.
  tile->SetLabel(effect_state_ ? effect_state_->label_text()
                               : std::u16string());
  if (effect_state_) {
    tile->SetVectorIcon(*effect_state_->icon());
  }
  tile->SetForegroundColorId(cros_tokens::kCrosSysOnSurface);
  std::optional<int> current_state =
      effect_ ? effect_->get_state_callback().Run() : std::nullopt;
  if (current_state.has_value()) {
    tile->SetToggled(current_state.value() != 0);
  }
  UpdateTooltip();

  // Set the initial download state of the tile. Future changes to the tile's
  // download state will occur if/when the tile's associated DLCs update.
  dlc_ids_ = VideoConferenceTrayController::Get()
                 ->GetEffectsManager()
                 .GetDlcIdsForEffectId(effect_id_);
  UpdateDlcDownloadUi();

  return tile;
}

void VcTileUiController::OnDlcStateChanged(
    const dlcservice::DlcState& dlc_state) {
  if (!base::Contains(dlc_ids_, dlc_state.id())) {
    return;
  }

  UpdateDlcDownloadUi();
}

void VcTileUiController::OnPressed(const ui::Event& event) {
  if (!effect_state_ || !tile_) {
    return;
  }

  // Execute the associated tile's callback.
  views::Button::PressedCallback(effect_state_->button_callback()).Run(event);

  // Set the toggled state.
  bool toggled = !tile_->IsToggled();
  tile_->SetToggled(toggled);

  // Track UMA metrics about the toggled state.
  TrackToggleUMA(toggled);

  // Play a "toggled-on" or "toggled-off" haptic effect, depending on the toggle
  // state.
  PlayToggleHaptic(toggled);

  // Update properties about the associated tile that change when the toggle
  // state changes, e.g. colors and tooltip text.
  tile_->UpdateColors();
  UpdateTooltip();
}

void VcTileUiController::TrackToggleUMA(bool target_toggle_state) {
  base::UmaHistogramBoolean(
      video_conference_utils::GetEffectHistogramNameForClick(effect_id_),
      target_toggle_state);
}

void VcTileUiController::PlayToggleHaptic(bool target_toggle_state) {
  chromeos::haptics_util::PlayHapticToggleEffect(
      target_toggle_state, ui::HapticTouchpadEffectStrength::kMedium);
}

VcTileUiController::DlcDownloadStateRequest::DlcDownloadStateRequest(
    const base::flat_set<std::string>& dlc_ids,
    base::OnceCallback<void(FeatureTile::DownloadState download_state,
                            int progress)> set_progress_callback)
    : set_progress_callback_(std::move(set_progress_callback)) {
  if (dlc_ids.empty()) {
    std::move(set_progress_callback_)
        .Run(FeatureTile::DownloadState::kNone, /*progress=*/0);
    return;
  }

  // Multiple DLCs can be managed by one tile, and their states will be
  // delivered individually.
  const auto merge_callback = base::BarrierCallback<DlcDownloadState>(
      dlc_ids.size(),
      base::BindOnce(
          &VcTileUiController::DlcDownloadStateRequest::OnAllDlcStatesRetrieved,
          weak_ptr_factory_.GetWeakPtr()));
  for (const std::string& dlc_id : dlc_ids) {
    DlcserviceClient::Get()->GetDlcState(
        dlc_id,
        base::BindOnce(&DlcDownloadStateRequest::OnDlcStateRetrieved,
                       weak_ptr_factory_.GetWeakPtr(), dlc_id, merge_callback));
  }
}

VcTileUiController::DlcDownloadStateRequest::~DlcDownloadStateRequest() {}

void VcTileUiController::DlcDownloadStateRequest::OnDlcStateRetrieved(
    std::string dlc_id,
    base::OnceCallback<void(DlcDownloadState)> merge_callback,
    std::string_view error,
    const dlcservice::DlcState& dlc_state) {
  std::move(merge_callback)
      .Run({std::move(dlc_id), std::string(error), dlc_state});
}

void VcTileUiController::DlcDownloadStateRequest::OnAllDlcStatesRetrieved(
    std::vector<DlcDownloadState> dlc_download_states) {
  // Check for errors.
  for (const DlcDownloadState& dlc_download_state : dlc_download_states) {
    if (dlc_download_state.error_code != dlcservice::kErrorNone) {
      std::move(set_progress_callback_)
          .Run(FeatureTile::DownloadState::kError, /*progress=*/0);
      return;
    }
  }

  // Check for in progress downloads.
  bool fully_installed = true;
  for (const DlcDownloadState& dlc_download_state : dlc_download_states) {
    if (dlc_download_state.dlc_state.state() !=
        dlcservice::DlcState::State::DlcState_State_INSTALLED) {
      fully_installed = false;
      break;
    }
  }
  if (fully_installed) {
    std::move(set_progress_callback_)
        .Run(FeatureTile::DownloadState::kDownloaded, /*progress=*/0);
    return;
  }

  // One or more DLCs is still downloading. Calculate the overall download
  // progress as the average of each DLC's download progress, weighted evenly.
  double progress = 0;
  for (const DlcDownloadState& dlc_download_state : dlc_download_states) {
    progress += dlc_download_state.dlc_state.progress();
  }
  progress /= dlc_download_states.size();
  std::move(set_progress_callback_)
      .Run(FeatureTile::DownloadState::kDownloading,
           /*progress=*/static_cast<int>(base::ClampFloor(progress * 100)));
}

void VcTileUiController::UpdateDlcDownloadUi() {
  if (!tile_) {
    return;
  }

  dlc_download_state_request_ = std::make_unique<DlcDownloadStateRequest>(
      dlc_ids_,
      // `base::Unretained` is safe because `DlcDownloadStateRequest` is
      // outlived by this, and `DlcDownloadStateRequest` maintains ownership of
      // the callback.
      base::BindOnce(&VcTileUiController::OnDlcDownloadStateFetched,
                     base::Unretained(this)));
}

void VcTileUiController::OnDlcDownloadStateFetched(
    FeatureTile::DownloadState download_state,
    int progress) {
  dlc_download_state_request_.reset();

  if (!tile_) {
    return;
  }

  CHECK(effect_state_)
      << "DLC State retrieved, but `effect_state_` is no longer valid for: "
      << effect_state_label_for_debug_;

  VideoConferenceTrayController::Get()->OnDlcDownloadStateFetched(
      /*add_warning=*/download_state == FeatureTile::DownloadState::kError,
      effect_state_->label_text());

  tile_->SetDownloadState(download_state, progress);
}

void VcTileUiController::UpdateTooltip() {
  if (!effect_state_ || !tile_) {
    return;
  }
  tile_->SetTooltipText(l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_TOGGLE_BUTTON_TOOLTIP,
      l10n_util::GetStringUTF16(effect_state_->accessible_name_id()),
      l10n_util::GetStringUTF16(
          tile_->IsToggled() ? VIDEO_CONFERENCE_TOGGLE_BUTTON_STATE_ON
                             : VIDEO_CONFERENCE_TOGGLE_BUTTON_STATE_OFF)));
}

}  // namespace ash::video_conference