chromium/ash/system/network/network_state_list_detailed_view.cc

// Copyright 2012 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/network/network_state_list_detailed_view.h"

#include <algorithm>

#include "ash/public/cpp/system_tray_client.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/network/network_utils.h"
#include "ash/system/network/tray_network_state_model.h"
#include "ash/system/tray/system_menu_button.h"
#include "ash/system/tray/tri_view.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/network/network_connect.h"
#include "chromeos/services/network_config/public/mojom/cros_network_config.mojom.h"
#include "net/base/ip_address.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_manager.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {

using base::UserMetricsAction;
using chromeos::network_config::mojom::ActivationStateType;
using chromeos::network_config::mojom::ConnectionStateType;
using chromeos::network_config::mojom::DeviceStateProperties;
using chromeos::network_config::mojom::DeviceStateType;
using chromeos::network_config::mojom::NetworkStateProperties;
using chromeos::network_config::mojom::NetworkStatePropertiesPtr;
using chromeos::network_config::mojom::NetworkType;

// Delay between scan requests.
constexpr int kRequestScanDelaySeconds = 10;

// 00:00:00:00:00:00 is provided when a device MAC address cannot be retrieved.
constexpr char kMissingMacAddress[] = "00:00:00:00:00:00";

// This margin value is used throughout the bubble:
// - margins inside the border
// - horizontal spacing between bubble border and parent bubble border
// - distance between top of this bubble's border and the bottom of the anchor
//   view (horizontal rule).
constexpr int kBubbleMargin = 8;

// Elevation used for the bubble shadow effect (tiny).
constexpr int kBubbleShadowElevation = 2;

bool IsSecondaryUser() {
  SessionControllerImpl* session_controller =
      Shell::Get()->session_controller();
  return session_controller->IsActiveUserSessionStarted() &&
         !session_controller->IsUserPrimary();
}

bool NetworkTypeIsConfigurable(NetworkType type) {
  switch (type) {
    case NetworkType::kVPN:
    case NetworkType::kWiFi:
      return true;
    case NetworkType::kAll:
    case NetworkType::kCellular:
    case NetworkType::kEthernet:
    case NetworkType::kMobile:
    case NetworkType::kTether:
    case NetworkType::kWireless:
      return false;
  }
  NOTREACHED();
}

}  // namespace

bool CanNetworkConnect(
    chromeos::network_config::mojom::ConnectionStateType connection_state,
    chromeos::network_config::mojom::NetworkType type,
    chromeos::network_config::mojom::ActivationStateType activation_state,
    bool connectable,
    std::string sim_eid) {
  // Network can be connected to if the network is not connected and:
  // * The network is connectable or
  // * The active user is primary and the network is configurable or
  // * The network is cellular and activated
  if (connection_state != ConnectionStateType::kNotConnected) {
    return false;
  }

  // Network cannot be connected to if it is an unactivated eSIM network.
  if (type == NetworkType::kCellular &&
      activation_state == ActivationStateType::kNotActivated &&
      !sim_eid.empty()) {
    return false;
  }

  if (connectable) {
    return true;
  }
  if (!IsSecondaryUser() && NetworkTypeIsConfigurable(type)) {
    return true;
  }
  if (type == NetworkType::kCellular &&
      activation_state == ActivationStateType::kActivated) {
    return true;
  }
  return false;
}

// A bubble which displays network info.
class NetworkStateListDetailedView::InfoBubble
    : public views::BubbleDialogDelegateView {
 public:
  InfoBubble(views::View* anchor,
             views::View* content,
             NetworkStateListDetailedView* detailed_view)
      : views::BubbleDialogDelegateView(anchor, views::BubbleBorder::TOP_RIGHT),
        detailed_view_(detailed_view) {
    SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
    set_margins(gfx::Insets(kBubbleMargin));
    SetArrow(views::BubbleBorder::NONE);
    set_shadow(views::BubbleBorder::NO_SHADOW);
    SetNotifyEnterExitOnChild(true);
    SetLayoutManager(std::make_unique<views::FillLayout>());
    AddChildView(content);
  }

  InfoBubble(const InfoBubble&) = delete;
  InfoBubble& operator=(const InfoBubble&) = delete;

  ~InfoBubble() override {
    // The detailed view can be destructed before info bubble is destructed.
    // Call OnInfoBubbleDestroyed only if the detailed view is live.
    if (detailed_view_) {
      detailed_view_->OnInfoBubbleDestroyed();
    }
  }

  void OnNetworkStateListDetailedViewIsDeleting() { detailed_view_ = nullptr; }

 private:
  // View:
  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override {
    // This bubble should be inset by kBubbleMargin on both left and right
    // relative to the parent bubble.
    const gfx::Size anchor_size = GetAnchorView()->size();
    int contents_width =
        anchor_size.width() - 2 * kBubbleMargin - margins().width();
    return gfx::Size(
        contents_width,
        GetLayoutManager()->GetPreferredHeightForWidth(this, contents_width));
  }

  void OnMouseExited(const ui::MouseEvent& event) override {
    // Like the user switching bubble/menu, hide the bubble when the mouse
    // exits.
    if (detailed_view_) {
      detailed_view_->ResetInfoBubble();
    }
  }

  void OnBeforeBubbleWidgetInit(views::Widget::InitParams* params,
                                views::Widget* widget) const override {
    params->shadow_type = views::Widget::InitParams::ShadowType::kDrop;
    params->shadow_elevation = kBubbleShadowElevation;
    params->name = "NetworkStateListDetailedView::InfoBubble";
  }

  // Not owned.
  raw_ptr<NetworkStateListDetailedView> detailed_view_;
};

//------------------------------------------------------------------------------
// NetworkStateListDetailedView

NetworkStateListDetailedView::NetworkStateListDetailedView(
    DetailedViewDelegate* delegate,
    NetworkDetailedViewListType list_type,
    LoginStatus login)
    : TrayDetailedView(delegate),
      list_type_(list_type),
      login_(login),
      model_(Shell::Get()->system_tray_model()->network_state_model()),
      info_button_(nullptr),
      settings_button_(nullptr),
      info_bubble_(nullptr) {
  OverrideProgressBarAccessibleName(l10n_util::GetStringUTF16(
      IDS_ASH_STATUS_TRAY_NETWORK_PROGRESS_ACCESSIBLE_NAME));
}

NetworkStateListDetailedView::~NetworkStateListDetailedView() {
  model_->RemoveObserver(this);
  if (info_bubble_) {
    info_bubble_->OnNetworkStateListDetailedViewIsDeleting();
  }
  ResetInfoBubble();
}

void NetworkStateListDetailedView::ToggleInfoBubbleForTesting() {
  ToggleInfoBubble();
}

void NetworkStateListDetailedView::Init() {
  CreateScrollableList();
  CreateTitleRow(GetStringIdForNetworkDetailedViewTitleRow(list_type_));

  model_->AddObserver(this);
  Update();

  if (list_type_ == LIST_TYPE_NETWORK && IsWifiEnabled()) {
    ScanAndStartTimer();
  }
}

void NetworkStateListDetailedView::Update() {
  UpdateNetworkList();
  UpdateHeaderButtons();
  UpdateScanningBar();
  DeprecatedLayoutImmediately();
}

void NetworkStateListDetailedView::ActiveNetworkStateChanged() {
  Update();
}

void NetworkStateListDetailedView::NetworkListChanged() {
  Update();
}

void NetworkStateListDetailedView::HandleViewClicked(views::View* view) {
  if (login_ == LoginStatus::LOCKED) {
    return;
  }

  std::string guid;
  if (!IsNetworkEntry(view, &guid)) {
    return;
  }

  model_->cros_network_config()->GetNetworkState(
      guid, base::BindOnce(&NetworkStateListDetailedView::HandleViewClickedImpl,
                           weak_ptr_factory_.GetWeakPtr()));
}

void NetworkStateListDetailedView::HandleViewClickedImpl(
    NetworkStatePropertiesPtr network) {
  if (network) {
    // If the network is locked and is cellular show SIM unlock dialog in OS
    // Settings.
    if (network->type == NetworkType::kCellular &&
        network->type_state->get_cellular()->sim_locked) {
      if (!Shell::Get()->session_controller()->ShouldEnableSettings()) {
        return;
      }
      Shell::Get()->system_tray_model()->client()->ShowSettingsSimUnlock();
      return;
    }

    if (CanNetworkConnect(
            network->connection_state, network->type,
            network->type == NetworkType::kCellular
                ? network->type_state->get_cellular()->activation_state
                : ActivationStateType::kUnknown,
            network->connectable,
            network->type == NetworkType::kCellular
                ? network->type_state->get_cellular()->eid
                : "")) {
      base::RecordAction(
          list_type_ == LIST_TYPE_VPN
              ? UserMetricsAction("StatusArea_VPN_ConnectToNetwork")
              : UserMetricsAction("StatusArea_Network_ConnectConfigured"));
      NetworkConnect::Get()->ConnectToNetworkId(network->guid);
      return;
    }
  }
  // If the network is no longer available or not connectable or configurable,
  // show the Settings UI.
  base::RecordAction(
      list_type_ == LIST_TYPE_VPN
          ? UserMetricsAction("StatusArea_VPN_ConnectionDetails")
          : UserMetricsAction("StatusArea_Network_ConnectionDetails"));
  Shell::Get()->system_tray_model()->client()->ShowNetworkSettings(
      network ? network->guid : std::string());
}

void NetworkStateListDetailedView::CreateExtraTitleRowButtons() {
  if (login_ == LoginStatus::LOCKED) {
    return;
  }

  DCHECK(!info_button_);
  tri_view()->SetContainerVisible(TriView::Container::END, true);

  info_button_ = CreateInfoButton(
      base::BindRepeating(&NetworkStateListDetailedView::ToggleInfoBubble,
                          base::Unretained(this)),
      IDS_ASH_STATUS_TRAY_NETWORK_INFO);
  tri_view()->AddView(TriView::Container::END, info_button_);

  DCHECK(!settings_button_);
  settings_button_ = CreateSettingsButton(
      base::BindRepeating(&NetworkStateListDetailedView::ShowSettings,
                          base::Unretained(this)),
      IDS_ASH_STATUS_TRAY_NETWORK_SETTINGS);
  tri_view()->AddView(TriView::Container::END, settings_button_);
}

void NetworkStateListDetailedView::ShowSettings() {
  base::RecordAction(list_type_ == LIST_TYPE_VPN
                         ? UserMetricsAction("StatusArea_VPN_Settings")
                         : UserMetricsAction("StatusArea_Network_Settings"));
  const std::string guid = model_->default_network()
                               ? model_->default_network()->guid
                               : std::string();

  base::RecordAction(base::UserMetricsAction(
      "ChromeOS.SystemTray.Network.SettingsButtonPressed"));

  // Showing network settings window may close the bubble (and destroy this
  // view). Explicitly request bubble closure here, before showing network
  // settings.
  CloseBubble();  // Deletes |this|.

  SystemTrayClient* system_tray_client =
      Shell::Get()->system_tray_model()->client();
  if (system_tray_client) {
    system_tray_client->ShowNetworkSettings(guid);
  }
}

void NetworkStateListDetailedView::UpdateHeaderButtons() {
  if (settings_button_) {
    if (login_ == LoginStatus::NOT_LOGGED_IN) {
      // When not logged in, only enable the settings button if there is a
      // default (i.e. connected or connecting) network to show settings for.
      settings_button_->SetEnabled(model_->default_network());
    } else {
      // Otherwise, enable if showing settings is allowed. There are situations
      // (supervised user creation flow) when the session is started but UI flow
      // continues within login UI, i.e., no browser window is yet available.
      settings_button_->SetEnabled(
          Shell::Get()->session_controller()->ShouldEnableSettings());
    }
  }
}

void NetworkStateListDetailedView::UpdateScanningBar() {
  if (list_type_ != LIST_TYPE_NETWORK) {
    return;
  }

  bool is_wifi_enabled = IsWifiEnabled();
  if (is_wifi_enabled && !network_scan_repeating_timer_.IsRunning()) {
    ScanAndStartTimer();
  }

  if (!is_wifi_enabled && network_scan_repeating_timer_.IsRunning()) {
    network_scan_repeating_timer_.Stop();
  }

  bool scanning_bar_visible = false;
  if (is_wifi_enabled) {
    const DeviceStateProperties* wifi = model_->GetDevice(NetworkType::kWiFi);
    const DeviceStateProperties* tether =
        model_->GetDevice(NetworkType::kTether);
    scanning_bar_visible =
        (wifi && wifi->scanning) || (tether && tether->scanning);
  }
  ShowProgress(-1, scanning_bar_visible);
}

void NetworkStateListDetailedView::ToggleInfoBubble() {
  if (ResetInfoBubble()) {
    return;
  }

  info_bubble_ = new InfoBubble(tri_view(), CreateNetworkInfoView(), this);
  views::BubbleDialogDelegateView::CreateBubble(info_bubble_)->Show();
  info_bubble_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, false);
}

bool NetworkStateListDetailedView::ResetInfoBubble() {
  if (!info_bubble_) {
    return false;
  }

  info_bubble_->GetWidget()->Close();
  return true;
}

void NetworkStateListDetailedView::OnInfoBubbleDestroyed() {
  info_bubble_ = nullptr;

  // Widget of info bubble is activated while info bubble is shown. To move
  // focus back to the widget of this view, activate it again here.
  GetWidget()->Activate();
}

views::View* NetworkStateListDetailedView::CreateNetworkInfoView() {
  std::string ipv4_address, ipv6_address;
  const NetworkStateProperties* network = model_->default_network();
  const DeviceStateProperties* device =
      network ? model_->GetDevice(network->type) : nullptr;
  if (device) {
    if (device->ipv4_address) {
      ipv4_address = device->ipv4_address->ToString();
    }
    if (device->ipv6_address) {
      ipv6_address = device->ipv6_address->ToString();
    }
  }

  std::string ethernet_address, wifi_address, cellular_address;
  if (list_type_ == LIST_TYPE_NETWORK) {
    const DeviceStateProperties* ethernet =
        model_->GetDevice(NetworkType::kEthernet);
    if (ethernet && ethernet->mac_address) {
      ethernet_address = *ethernet->mac_address;
    }
    const DeviceStateProperties* wifi = model_->GetDevice(NetworkType::kWiFi);
    if (wifi && wifi->mac_address) {
      wifi_address = *wifi->mac_address;
    }
    const DeviceStateProperties* cellular =
        model_->GetDevice(NetworkType::kCellular);
    if (cellular && cellular->mac_address) {
      cellular_address = *cellular->mac_address;
    }
  }

  std::u16string bubble_text;
  auto maybe_add_mac_address = [&bubble_text](const std::string& address,
                                              int ids) {
    if (address.empty() || address == kMissingMacAddress) {
      return;
    }

    if (!bubble_text.empty()) {
      bubble_text += u"\n";
    }

    bubble_text += l10n_util::GetStringFUTF16(ids, base::UTF8ToUTF16(address));
  };

  maybe_add_mac_address(ipv4_address, IDS_ASH_STATUS_TRAY_IP_ADDRESS);
  maybe_add_mac_address(ipv6_address, IDS_ASH_STATUS_TRAY_IPV6_ADDRESS);
  maybe_add_mac_address(ethernet_address, IDS_ASH_STATUS_TRAY_ETHERNET_ADDRESS);
  maybe_add_mac_address(wifi_address, IDS_ASH_STATUS_TRAY_WIFI_ADDRESS);
  maybe_add_mac_address(cellular_address, IDS_ASH_STATUS_TRAY_CELLULAR_ADDRESS);

  // Avoid an empty bubble in the unlikely event that there is no network
  // information at all.
  if (bubble_text.empty()) {
    bubble_text = l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NO_NETWORKS);
  }

  auto* label = new views::Label(bubble_text);
  label->SetMultiLine(true);
  label->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
  label->SetSelectable(true);
  return label;
}

void NetworkStateListDetailedView::ScanAndStartTimer() {
  CallRequestScan();
  network_scan_repeating_timer_.Start(
      FROM_HERE, base::Seconds(kRequestScanDelaySeconds), this,
      &NetworkStateListDetailedView::CallRequestScan);
}

void NetworkStateListDetailedView::CallRequestScan() {
  if (!IsWifiEnabled()) {
    return;
  }

  VLOG(1) << "Requesting Network Scan.";
  model_->cros_network_config()->RequestNetworkScan(NetworkType::kWiFi);
  model_->cros_network_config()->RequestNetworkScan(NetworkType::kTether);
}

bool NetworkStateListDetailedView::IsWifiEnabled() {
  return model_->GetDeviceState(NetworkType::kWiFi) ==
         DeviceStateType::kEnabled;
}

BEGIN_METADATA(NetworkStateListDetailedView)
END_METADATA

}  // namespace ash