chromium/ash/system/hotspot/hotspot_detailed_view.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/hotspot/hotspot_detailed_view.h"

#include "ash/ash_element_identifiers.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/rounded_container.h"
#include "ash/style/switch.h"
#include "ash/style/typography.h"
#include "ash/system/hotspot/hotspot_icon.h"
#include "ash/system/hotspot/hotspot_icon_animation.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/detailed_view_delegate.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "base/functional/bind.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/view_class_properties.h"

namespace ash {

using hotspot_config::mojom::HotspotAllowStatus;
using hotspot_config::mojom::HotspotInfoPtr;
using hotspot_config::mojom::HotspotState;

namespace {

// Used for setting the insets of broader hotspot entry row.
constexpr auto kToggleRowTriViewInsets = gfx::Insets::VH(8, 24);

bool IsEnabledOrEnabling(HotspotState state) {
  return state == HotspotState::kEnabled || state == HotspotState::kEnabling;
}

bool CanToggleHotspot(HotspotState state, HotspotAllowStatus allow_status) {
  if (state == HotspotState::kDisabling) {
    return false;
  }
  if (state == HotspotState::kEnabling || state == HotspotState::kEnabled) {
    return true;
  }
  return allow_status == HotspotAllowStatus::kAllowed;
}

}  // namespace

HotspotDetailedView::HotspotDetailedView(
    DetailedViewDelegate* detailed_view_delegate,
    Delegate* delegate)
    : TrayDetailedView(detailed_view_delegate), delegate_(delegate) {
  CreateTitleRow(IDS_ASH_STATUS_TRAY_HOTSPOT);
  CreateScrollableList();
  CreateContainer();
}

HotspotDetailedView::~HotspotDetailedView() {
  Shell::Get()->hotspot_icon_animation()->RemoveObserver(this);
}

void HotspotDetailedView::UpdateViewForHotspot(HotspotInfoPtr hotspot_info) {
  if (hotspot_info->state == HotspotState::kEnabling) {
    Shell::Get()->hotspot_icon_animation()->AddObserver(this);
  } else if (state_ == HotspotState::kEnabling) {
    Shell::Get()->hotspot_icon_animation()->RemoveObserver(this);
  }

  if (state_ != hotspot_info->state) {
    state_ = hotspot_info->state;
    UpdateIcon();
  }

  UpdateSubText(hotspot_info);
  allow_status_ = hotspot_info->allow_status;
  UpdateToggleState(hotspot_info->state, hotspot_info->allow_status);
  UpdateExtraIcon(hotspot_info->allow_status);
}

void HotspotDetailedView::HandleViewClicked(views::View* view) {
  // Handle clicks on the on/off toggle row.
  if (view == entry_row_ && CanToggleHotspot(state_, allow_status_)) {
    // The toggle button has the old state, so switch to the opposite state.
    ToggleHotspot(!toggle_->GetIsOn());
    return;
  }
}

void HotspotDetailedView::CreateExtraTitleRowButtons() {
  tri_view()->SetContainerVisible(TriView::Container::END, /*visible=*/true);

  CHECK(!settings_button_);
  settings_button_ = CreateSettingsButton(
      base::BindRepeating(&HotspotDetailedView::OnSettingsClicked,
                          weak_factory_.GetWeakPtr()),
      IDS_ASH_HOTSPOT_DETAILED_VIEW_HOTSPOT_SETTINGS);
  settings_button_->SetState(TrayPopupUtils::CanOpenWebUISettings()
                                 ? views::Button::STATE_NORMAL
                                 : views::Button::STATE_DISABLED);
  settings_button_->SetID(
      static_cast<int>(HotspotDetailedViewChildId::kSettingsButton));
  tri_view()->AddView(TriView::Container::END, settings_button_);
}

void HotspotDetailedView::CreateContainer() {
  row_container_ =
      scroll_content()->AddChildView(std::make_unique<RoundedContainer>(
          RoundedContainer::Behavior::kAllRounded));
  // Ensure the HoverHighlightView ink drop fills the whole container.
  row_container_->SetBorderInsets(gfx::Insets());

  entry_row_ = row_container_->AddChildView(
      std::make_unique<HoverHighlightView>(/*listener=*/this));
  entry_row_->SetID(static_cast<int>(HotspotDetailedViewChildId::kEntryRow));

  // The icon image and label text depend on whether hotspot is enabled. They
  // are set in UpdateViewForHotspot().
  auto hotspot_icon = std::make_unique<views::ImageView>();
  hotspot_icon->SetID(
      static_cast<int>(HotspotDetailedViewChildId::kHotspotIcon));
  hotspot_icon->SetImage(ui::ImageModel::FromVectorIcon(
      kHotspotOffIcon, cros_tokens::kCrosSysOnSurface));
  hotspot_icon_ = hotspot_icon.get();
  entry_row_->AddViewAndLabel(std::move(hotspot_icon), u"");
  const std::u16string text_label = l10n_util::GetStringFUTF16(
      IDS_ASH_HOTSPOT_DETAILED_VIEW_TITLE, ui::GetChromeOSDeviceName());
  entry_row_->text_label()->SetText(text_label);
  entry_row_->text_label()->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton1,
                                        *entry_row_->text_label());
  entry_row_->GetViewAccessibility().SetName(text_label);

  auto toggle = std::make_unique<Switch>(base::BindRepeating(
      &HotspotDetailedView::OnToggleClicked, weak_factory_.GetWeakPtr()));
  toggle->GetViewAccessibility().SetName(l10n_util::GetStringUTF16(
      IDS_ASH_HOTSPOT_DETAILED_VIEW_TOGGLE_A11Y_TEXT));
  toggle->SetID(static_cast<int>(HotspotDetailedViewChildId::kToggle));
  toggle->SetProperty(views::kElementIdentifierKey,
                      kHotspotDetailedViewToggleElementId);
  toggle_ = toggle.get();
  entry_row_->AddRightView(toggle.release());

  auto extra_icon = std::make_unique<views::ImageView>();
  extra_icon->SetVisible(false);
  extra_icon->SetID(static_cast<int>(HotspotDetailedViewChildId::kExtraIcon));
  extra_icon_ = extra_icon.get();
  entry_row_->AddAdditionalRightView(extra_icon.release());

  // Allow the row to be taller than a typical tray menu item.
  entry_row_->SetExpandable(true);
  entry_row_->tri_view()->SetInsets(kToggleRowTriViewInsets);
}

void HotspotDetailedView::OnSettingsClicked() {
  CloseBubble();  // Delete `this`.
  Shell::Get()->system_tray_model()->client()->ShowHotspotSubpage();
}

void HotspotDetailedView::OnToggleClicked() {
  // The toggle button already has the new state after a click.
  ToggleHotspot(toggle_->GetIsOn());
}

void HotspotDetailedView::ToggleHotspot(bool new_state) {
  delegate_->OnToggleClicked(new_state);
}

void HotspotDetailedView::HotspotIconChanged() {
  UpdateIcon();
}

void HotspotDetailedView::UpdateIcon() {
  hotspot_icon_->SetImage(ui::ImageModel::FromVectorIcon(
      hotspot_icon::GetIconForHotspot(state_), cros_tokens::kCrosSysOnSurface));
}

void HotspotDetailedView::UpdateToggleState(
    const HotspotState& state,
    const HotspotAllowStatus& allow_status) {
  toggle_->SetEnabled(CanToggleHotspot(state, allow_status));
  const bool is_enabled_or_enabling = IsEnabledOrEnabling(state);
  toggle_->SetIsOn(is_enabled_or_enabling);
  entry_row_->SetAccessibilityState(
      is_enabled_or_enabling
          ? HoverHighlightView::AccessibilityState::CHECKED_CHECKBOX
          : HoverHighlightView::AccessibilityState::UNCHECKED_CHECKBOX);
}

void HotspotDetailedView::UpdateSubText(const HotspotInfoPtr& hotspot_info) {
  std::u16string sub_text;
  switch (hotspot_info->state) {
    case HotspotState::kEnabled: {
      uint32_t client_count = hotspot_info->client_count;
      if (client_count == 0) {
        sub_text = l10n_util::GetStringUTF16(
            IDS_ASH_HOTSPOT_DETAILED_VIEW_ON_NO_CONNECTED_DEVICES);
      } else if (client_count == 1) {
        sub_text = l10n_util::GetStringUTF16(
            IDS_ASH_HOTSPOT_ON_MESSAGE_ONE_CONNECTED_DEVICE);
      } else {
        sub_text = l10n_util::GetStringFUTF16(
            IDS_ASH_HOTSPOT_ON_MESSAGE_MULTIPLE_CONNECTED_DEVICES,
            base::NumberToString16(client_count));
      }
      break;
    }
    case HotspotState::kDisabled: {
      const HotspotAllowStatus allow_status = hotspot_info->allow_status;
      if (allow_status == HotspotAllowStatus::kDisallowedNoMobileData) {
        sub_text = l10n_util::GetStringUTF16(
            IDS_ASH_HOTSPOT_DETAILED_VIEW_SUBLABEL_NO_MOBILE_DATA);
      }
      break;
    }
    case HotspotState::kEnabling:
      sub_text = l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_HOTSPOT_STATUS_ENABLING);
      break;
    case HotspotState::kDisabling:
      sub_text = l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_HOTSPOT_STATUS_DISABLING);
      break;
  }

  if (!sub_text.empty()) {
    entry_row_->SetSubText(sub_text);
    entry_row_->sub_text_label()->SetVisible(true);
    entry_row_->GetViewAccessibility().SetDescription(sub_text);
    if (hotspot_info->state != HotspotState::kEnabled) {
      // If hotspot is not enabled, no need to set primary color for the status
      // sublabel text.
      return;
    }
    // Set color for the subtext that shows hotspot is connected.
    entry_row_->sub_text_label()->SetEnabledColorId(
        cros_tokens::kCrosSysPositive);
    TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosAnnotation1,
                                          *entry_row_->sub_text_label());
    return;
  }
  // If no subtext is set, previous subtext should be hidden.
  if (entry_row_->sub_text_label()) {
    entry_row_->sub_text_label()->SetVisible(false);
    entry_row_->GetViewAccessibility().SetDescription(
        std::u16string(),
        ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty);
  }
}

void HotspotDetailedView::UpdateExtraIcon(
    const HotspotAllowStatus& allow_status) {
  if (allow_status == HotspotAllowStatus::kAllowed ||
      allow_status == HotspotAllowStatus::kDisallowedNoMobileData) {
    extra_icon_->SetVisible(false);
    return;
  }

  extra_icon_->SetVisible(true);
  bool use_managed_icon =
      allow_status == HotspotAllowStatus::kDisallowedByPolicy;
  extra_icon_->SetImage(ui::ImageModel::FromVectorIcon(
      use_managed_icon ? kSystemTrayManagedIcon : kUnifiedMenuInfoIcon,
      kColorAshIconColorPrimary));
  const std::u16string tooltip = l10n_util::GetStringUTF16(
      use_managed_icon
          ? IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_PROHIBITED_BY_POLICY
          : IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_MOBILE_DATA_NOT_SUPPORTED);
  extra_icon_->SetFocusBehavior(FocusBehavior::ALWAYS);
  extra_icon_->SetTooltipText(tooltip);
  extra_icon_->GetViewAccessibility().SetName(tooltip);
}

BEGIN_METADATA(HotspotDetailedView)
END_METADATA

}  // namespace ash