chromium/ash/system/network/network_list_view_controller_impl.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 "ash/system/network/network_list_view_controller_impl.h"

#include <memory>
#include <vector>

#include "ash/ash_element_identifiers.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/bluetooth_config_service.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/typography.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/network/network_detailed_network_view_impl.h"
#include "ash/system/network/network_list_mobile_header_view.h"
#include "ash/system/network/network_list_network_header_view.h"
#include "ash/system/network/network_list_network_item_view.h"
#include "ash/system/network/network_utils.h"
#include "ash/system/network/tray_network_state_model.h"
#include "ash/system/tray/tray_info_label.h"
#include "ash/system/tray/tri_view.h"
#include "base/timer/timer.h"
#include "chromeos/ash/components/dbus/hermes/hermes_manager_client.h"
#include "chromeos/ash/services/bluetooth_config/public/cpp/cros_bluetooth_config_util.h"
#include "chromeos/ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "chromeos/services/network_config/public/mojom/cros_network_config.mojom.h"
#include "chromeos/services/network_config/public/mojom/network_types.mojom-shared.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"

namespace ash {

namespace {

using bluetooth_config::IsBluetoothEnabledOrEnabling;
using bluetooth_config::mojom::BluetoothSystemPropertiesPtr;
using bluetooth_config::mojom::BluetoothSystemState;
using ::chromeos::network_config::NetworkTypeMatchesType;
using ::chromeos::network_config::StateIsConnected;
using ::chromeos::network_config::mojom::DeviceStateProperties;
using ::chromeos::network_config::mojom::DeviceStateType;
using ::chromeos::network_config::mojom::FilterType;
using ::chromeos::network_config::mojom::GlobalPolicy;
using ::chromeos::network_config::mojom::ManagedPropertiesPtr;
using ::chromeos::network_config::mojom::NetworkFilter;
using ::chromeos::network_config::mojom::NetworkStateProperties;
using ::chromeos::network_config::mojom::NetworkStatePropertiesPtr;
using ::chromeos::network_config::mojom::NetworkType;
using ::chromeos::network_config::mojom::OncSource;
using ::chromeos::network_config::mojom::ProxyMode;

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

constexpr auto kWifiGroupLabelPadding = gfx::Insets::TLBR(8, 22, 8, 4);

// 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());
  }
}

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

bool IsCellularDeviceInhibited() {
  const DeviceStateProperties* cellular_device =
      Shell::Get()->system_tray_model()->network_state_model()->GetDevice(
          NetworkType::kCellular);
  if (!cellular_device) {
    return false;
  }
  return cellular_device->inhibit_reason !=
         chromeos::network_config::mojom::InhibitReason::kNotInhibited;
}

bool IsCellularDeviceFlashing() {
  const DeviceStateProperties* cellular_device =
      Shell::Get()->system_tray_model()->network_state_model()->GetDevice(
          NetworkType::kCellular);
  if (!cellular_device) {
    return false;
  }
  return cellular_device->is_flashing;
}

bool IsESimSupported() {
  const DeviceStateProperties* cellular_device =
      Shell::Get()->system_tray_model()->network_state_model()->GetDevice(
          NetworkType::kCellular);

  if (!cellular_device || !cellular_device->sim_infos) {
    return false;
  }

  // Check both the SIM slot infos and the number of EUICCs because the former
  // comes from Shill and the latter from Hermes, and so there may be instances
  // where one may be true while they other isn't.
  if (HermesManagerClient::Get() &&
      HermesManagerClient::Get()->GetAvailableEuiccs().empty()) {
    return false;
  }
  for (const auto& sim_info : *cellular_device->sim_infos) {
    if (!sim_info->eid.empty()) {
      return true;
    }
  }
  return false;
}

bool IsCellularSimLocked() {
  const DeviceStateProperties* cellular_device =
      Shell::Get()->system_tray_model()->network_state_model()->GetDevice(
          NetworkType::kCellular);
  return cellular_device &&
         !cellular_device->sim_lock_status->lock_type.empty();
}

NetworkType GetMobileSectionNetworkType() {
  if (features::IsInstantHotspotRebrandEnabled()) {
    return NetworkType::kCellular;
  }
  return NetworkType::kMobile;
}

}  // namespace

NetworkListViewControllerImpl::NetworkListViewControllerImpl(
    NetworkDetailedNetworkView* network_detailed_network_view)
    : model_(Shell::Get()->system_tray_model()->network_state_model()),
      network_detailed_network_view_(network_detailed_network_view) {
  DCHECK(network_detailed_network_view_);
  Shell::Get()->system_tray_model()->network_state_model()->AddObserver(this);

  GetBluetoothConfigService(
      remote_cros_bluetooth_config_.BindNewPipeAndPassReceiver());
  remote_cros_bluetooth_config_->ObserveSystemProperties(
      cros_system_properties_observer_receiver_.BindNewPipeAndPassRemote());

  if (features::IsInstantHotspotRebrandEnabled()) {
    Shell::Get()->shell_delegate()->BindMultiDeviceSetup(
        multidevice_setup_remote_.BindNewPipeAndPassReceiver());

    multidevice_setup_remote_->AddHostStatusObserver(
        host_status_observer_receiver_.BindNewPipeAndPassRemote());

    multidevice_setup_remote_->GetHostStatus(
        base::BindOnce(&NetworkListViewControllerImpl::OnHostStatusChanged,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  GetNetworkStateList();
}

NetworkListViewControllerImpl::~NetworkListViewControllerImpl() {
  Shell::Get()->system_tray_model()->network_state_model()->RemoveObserver(
      this);
}

void NetworkListViewControllerImpl::ActiveNetworkStateChanged() {
  GetNetworkStateList();
}

void NetworkListViewControllerImpl::NetworkListChanged() {
  GetNetworkStateList();
}

void NetworkListViewControllerImpl::GlobalPolicyChanged() {
  UpdateMobileSection();
  GetNetworkStateList();
}

void NetworkListViewControllerImpl::OnPropertiesUpdated(
    BluetoothSystemPropertiesPtr properties) {
  if (bluetooth_system_state_ == properties->system_state) {
    return;
  }

  bluetooth_system_state_ = properties->system_state;
  if (features::IsInstantHotspotRebrandEnabled()) {
    UpdateTetherHostsSection();
  } else {
    UpdateMobileSection();
  }
}

void NetworkListViewControllerImpl::GetNetworkStateList() {
  model()->cros_network_config()->GetNetworkStateList(
      NetworkFilter::New(FilterType::kVisible, NetworkType::kAll,
                         chromeos::network_config::mojom::kNoLimit),
      base::BindOnce(&NetworkListViewControllerImpl::OnGetNetworkStateList,
                     weak_ptr_factory_.GetWeakPtr()));
}

void NetworkListViewControllerImpl::OnHostStatusChanged(
    multidevice_setup::mojom::HostStatus host_status,
    const std::optional<multidevice::RemoteDevice>& host_device) {
  has_phone_eligible_for_setup_ =
      (host_status ==
       multidevice_setup::mojom::HostStatus::kEligibleHostExistsButNoHostSet);
  GetNetworkStateList();
}

void NetworkListViewControllerImpl::OnGetNetworkStateList(
    std::vector<NetworkStatePropertiesPtr> networks) {
  int old_position = network_detailed_network_view()->GetScrollPosition();

  // Indicates the current position a view will be added to in
  // `NetworkDetailedNetworkView` scroll list.
  size_t index = 0;

  // Store current views in `previous_network_views`, views which have
  // a corresponding network in `networks` will be added back to
  // `network_id_to_view_map_` any remaining views in `previous_network_views`
  // would be deleted.
  NetworkIdToViewMap previous_network_views =
      std::move(network_id_to_view_map_);
  network_id_to_view_map_.clear();

  UpdateNetworkTypeExistence(networks);

  network_detailed_network_view()->ReorderFirstListView(index++);

  // The warning message entry and the ethernet entry are placed in the
  // `network_detailed_network_view()`'s `first_list_view_`. Here this index is
  // used to indicate the current position a entry will be added to or reordered
  // in the `first_list_view_`.
  size_t first_list_item_index = 0;

  // Show a warning that the connection might be monitored if connected to a
  // VPN or if the default network has a proxy installed.
  first_list_item_index =
      ShowConnectionWarningIfNetworkMonitored(first_list_item_index);

  // Show Ethernet section first.
  first_list_item_index = CreateItemViewsIfMissingAndReorder(
      NetworkType::kEthernet, first_list_item_index, networks,
      &previous_network_views);

  if (ShouldMobileDataSectionBeShown()) {
    if (!mobile_header_view_) {
      mobile_header_view_ =
          network_detailed_network_view()->AddMobileSectionHeader();

      // Mobile toggle state is set here to avoid toggle animating on/off when
      // detailed view is opened for the first time. Enabled state will be
      // updated in subsequent calls.
      mobile_header_view_->SetToggleState(
          /*enabled=*/false,
          /*is_on=*/is_mobile_network_enabled_, /*animate_toggle=*/false);
    }

    UpdateMobileSection();
    network_detailed_network_view()->ReorderMobileTopContainer(index++);

    size_t mobile_item_index = 0;
    mobile_item_index = CreateItemViewsIfMissingAndReorder(
        GetMobileSectionNetworkType(), mobile_item_index, networks,
        &previous_network_views);

    // Add mobile status message to NetworkDetailedNetworkView's
    // `mobile_network_list_view_` if it exist.
    if (mobile_status_message_) {
      network_detailed_network_view()
          ->GetNetworkList(GetMobileSectionNetworkType())
          ->ReorderChildView(mobile_status_message_, mobile_item_index++);
    }

    if (ShouldAddESimEntry()) {
      mobile_item_index = CreateConfigureNetworkEntry(
          &add_esim_entry_, GetMobileSectionNetworkType(), mobile_item_index);
      add_esim_entry_->SetProperty(views::kElementIdentifierKey,
                                   kNetworkAddEsimElementId);
    } else {
      RemoveAndResetViewIfExists(&add_esim_entry_);
    }

    network_detailed_network_view()->ReorderMobileListView(index++);
  } else {
    RemoveAndResetViewIfExists(&mobile_header_view_);
  }

  if (features::IsInstantHotspotRebrandEnabled()) {
    if (ShouldTetherHostsSectionBeShown()) {
      if (!tether_hosts_header_view_) {
        tether_hosts_header_view_ =
            network_detailed_network_view()->AddTetherHostsSectionHeader(
                base::BindRepeating(
                    &NetworkListViewControllerImpl::UpdateTetherHostsSection,
                    weak_ptr_factory_.GetWeakPtr()));
      }

      UpdateTetherHostsSection();
      tether_hosts_header_view_->parent()->ReorderChildView(
          tether_hosts_header_view_, index++);

      size_t tether_item_index = 0;

      if (has_phone_eligible_for_setup_) {
        // This does not remote tether_hosts_status_message_, as
        // UpdateTetherHostsSection() ensures tether_hosts_status_message_ does
        // not exist if has_phone_eligible_for_setup_ is true
        tether_item_index = CreateConfigureNetworkEntry(
            &set_up_cross_device_suite_entry_, NetworkType::kTether,
            tether_item_index);
      } else {
        RemoveAndResetViewIfExists(&set_up_cross_device_suite_entry_);
        tether_item_index = CreateItemViewsIfMissingAndReorder(
            NetworkType::kTether, tether_item_index, networks,
            &previous_network_views);

        // Add tether hosts status message to NetworkDetailedNetworkView's
        // `tether_hosts_network_list_view_` if it exist.
        if (tether_hosts_status_message_) {
          network_detailed_network_view()
              ->GetNetworkList(NetworkType::kTether)
              ->ReorderChildView(tether_hosts_status_message_,
                                 tether_item_index);
        }
      }
      network_detailed_network_view()->ReorderTetherHostsListView(index++);
    } else {
      RemoveAndResetViewIfExists(&tether_hosts_header_view_);
    }
  }

  UpdateWifiSection();
  network_detailed_network_view()->ReorderNetworkTopContainer(index++);

  size_t network_item_index = 0;
  // The wifi networks are grouped into known and unknown groups.
  std::vector<NetworkStatePropertiesPtr> known_networks;
  std::vector<NetworkStatePropertiesPtr> unknown_networks;
  for (NetworkStatePropertiesPtr& network : networks) {
    if (network->source != OncSource::kNone &&
        NetworkTypeMatchesType(network->type, NetworkType::kWiFi)) {
      known_networks.push_back(std::move(network));
    } else if (NetworkTypeMatchesType(network->type, NetworkType::kWiFi)) {
      unknown_networks.push_back(std::move(network));
    }
  }
  if (!known_networks.empty()) {
    network_item_index =
        CreateWifiGroupHeader(network_item_index, /*is_known=*/true);
    network_item_index = CreateItemViewsIfMissingAndReorder(
        NetworkType::kWiFi, network_item_index, known_networks,
        &previous_network_views);
  } else {
    RemoveAndResetViewIfExists(&known_header_);
  }
  if (!unknown_networks.empty()) {
    network_item_index =
        CreateWifiGroupHeader(network_item_index, /*is_known=*/false);
    network_item_index = CreateItemViewsIfMissingAndReorder(
        NetworkType::kWiFi, network_item_index, unknown_networks,
        &previous_network_views);
  } else {
    RemoveAndResetViewIfExists(&unknown_header_);
  }
  if (is_wifi_enabled_) {
    network_item_index = CreateConfigureNetworkEntry(
        &join_wifi_entry_, NetworkType::kWiFi, network_item_index);
  } else {
    RemoveAndResetViewIfExists(&join_wifi_entry_);
  }
  network_detailed_network_view()->ReorderNetworkListView(index++);

  if (wifi_status_message_) {
    network_detailed_network_view()
        ->GetNetworkList(NetworkType::kWiFi)
        ->ReorderChildView(wifi_status_message_, network_item_index++);
  }

  UpdateScanningBarAndTimer();

  // Remaining views in `previous_network_views` are no longer needed
  // and should be deleted.
  for (const auto& id_and_view : previous_network_views) {
    auto* parent = id_and_view.second->parent();
    parent->RemoveChildViewT(id_and_view.second);
  }

  FocusLastSelectedView();
  network_detailed_network_view()->NotifyNetworkListChanged();

  // Resets the scrolling position to the old position to avoid and position
  // change during reordering the child views in the list.
  network_detailed_network_view()->ScrollToPosition(old_position);
}

void NetworkListViewControllerImpl::UpdateNetworkTypeExistence(
    const std::vector<NetworkStatePropertiesPtr>& networks) {
  has_cellular_networks_ = false;
  has_wifi_networks_ = false;
  has_tether_networks_ = false;
  connected_vpn_guid_ = std::string();

  for (auto& network : networks) {
    if (NetworkTypeMatchesType(network->type, NetworkType::kCellular)) {
      has_cellular_networks_ = true;
    } else if (NetworkTypeMatchesType(network->type, NetworkType::kWiFi)) {
      has_wifi_networks_ = true;
    } else if (NetworkTypeMatchesType(network->type, NetworkType::kTether)) {
      has_tether_networks_ = true;
    } else if (NetworkTypeMatchesType(network->type, NetworkType::kVPN) &&
               StateIsConnected(network->connection_state)) {
      connected_vpn_guid_ = network->guid;
    }
  }

  is_mobile_network_enabled_ =
      model()->GetDeviceState(NetworkType::kCellular) ==
          DeviceStateType::kEnabled ||
      model()->GetDeviceState(NetworkType::kTether) ==
          DeviceStateType::kEnabled;

  is_wifi_enabled_ =
      model()->GetDeviceState(NetworkType::kWiFi) == DeviceStateType::kEnabled;
}

size_t NetworkListViewControllerImpl::ShowConnectionWarningIfNetworkMonitored(
    size_t index) {
  const NetworkStateProperties* default_network = model()->default_network();
  const GlobalPolicy* global_policy = model()->global_policy();
  bool using_proxy =
      default_network && default_network->proxy_mode != ProxyMode::kDirect;
  bool enterprise_monitored_web_requests =
      global_policy && (global_policy->dns_queries_monitored ||
                        global_policy->report_xdr_events_enabled);

  if (!connected_vpn_guid_.empty() || using_proxy ||
      enterprise_monitored_web_requests) {
    if (!connection_warning_) {
      ShowConnectionWarning(
          /*show_managed_icon=*/enterprise_monitored_web_requests);
    }
    // If the warning is already shown check if the label needs to be updated
    // with a different message.
    else if (connection_warning_label_->GetText() != GenerateLabelText()) {
      connection_warning_label_->SetText(GenerateLabelText());
    }

    // The initial request to add a connection warning icon is missing
    // information about the management status of VPN and proxy configurations.
    // This call will request the managed network properties of the default
    // network and the VPN network (if one is active).
    if (!enterprise_monitored_web_requests) {
      MaybeShowConnectionWarningManagedIcon(using_proxy);
    }

    // The warning messages are shown in the ethernet section.
    network_detailed_network_view()
        ->GetNetworkList(NetworkType::kEthernet)
        ->ReorderChildView(connection_warning_, index++);
  } else if (connected_vpn_guid_.empty() && !using_proxy) {
    HideConnectionWarning();
    network_detailed_network_view()->MaybeRemoveFirstListView();
  }

  return index;
}

void NetworkListViewControllerImpl::MaybeShowConnectionWarningManagedIcon(
    bool using_proxy) {
  // If the proxy is set, check if it's a managed setting.
  const NetworkStateProperties* default_network = model()->default_network();
  if (using_proxy && default_network) {
    model()->cros_network_config()->GetManagedProperties(
        default_network->guid,
        base::BindOnce(
            &NetworkListViewControllerImpl::OnGetManagedPropertiesResult,
            weak_ptr_factory_.GetWeakPtr(), default_network->guid));
  } else {
    is_proxy_managed_ = false;
  }

  // If the vpn is set, check if it's a managed setting.
  if (!connected_vpn_guid_.empty()) {
    model()->cros_network_config()->GetManagedProperties(
        connected_vpn_guid_,
        base::BindOnce(
            &NetworkListViewControllerImpl::OnGetManagedPropertiesResult,
            weak_ptr_factory_.GetWeakPtr(), connected_vpn_guid_));
  } else {
    is_vpn_managed_ = false;
  }
}

bool NetworkListViewControllerImpl::ShouldAddESimEntry() const {
  const bool is_add_esim_enabled = is_mobile_network_enabled_ &&
                                   !IsCellularDeviceInhibited() &&
                                   !IsCellularDeviceFlashing();

  bool is_add_esim_visible = IsESimSupported();
  const GlobalPolicy* global_policy = model()->global_policy();

  // Adding new cellular networks is disallowed when only policy cellular
  // networks are allowed by admin.
  if (!global_policy || global_policy->allow_only_policy_cellular_networks) {
    is_add_esim_visible = false;
  }
  // The entry navigates to Settings, only add it if this can occur.
  return is_add_esim_enabled && is_add_esim_visible &&
         TrayPopupUtils::CanOpenWebUISettings();
}

void NetworkListViewControllerImpl::OnGetManagedPropertiesResult(
    const std::string& guid,
    ManagedPropertiesPtr properties) {
  // Bail out early if no connection warning is being shown.
  // This could happen if the connection warning is hidden while the async
  // GetManagedProperties step is in progress.
  if (!connection_warning_) {
    return;
  }

  // Check if the proxy is managed.
  const NetworkStateProperties* default_network = model()->default_network();
  if (default_network && default_network->guid == guid) {
    is_proxy_managed_ =
        properties && properties->proxy_settings &&
        properties->proxy_settings->type->policy_source !=
            chromeos::network_config::mojom::PolicySource::kNone;
  }
  // Check if the VPN is managed.
  if (guid == connected_vpn_guid_) {
    // TODO(b/261009968): Add check for managed WireGuard settings.
    is_vpn_managed_ =
        properties && properties->type_properties->is_vpn() &&
        properties->type_properties->get_vpn()->host &&
        properties->type_properties->get_vpn()->host->policy_source !=
            chromeos::network_config::mojom::PolicySource::kNone;
  }

  if (is_proxy_managed_ || is_vpn_managed_) {
    SetConnectionWarningIcon(connection_warning_, /*use_managed_icon=*/true);
  }
}

void NetworkListViewControllerImpl::SetConnectionWarningIcon(
    TriView* parent,
    bool use_managed_icon) {
  DCHECK(parent) << "The connection warning parent view should not be null";
  int newIconId = static_cast<int>(
      use_managed_icon
          ? NetworkListViewControllerViewChildId::kConnectionWarningManagedIcon
          : NetworkListViewControllerViewChildId::kConnectionWarningSystemIcon);

  if (connection_warning_icon_ &&
      connection_warning_icon_->GetID() == newIconId) {
    // The view is already showing the correct icon.
    return;
  }

  // Remove the previous icon if set.
  RemoveAndResetViewIfExists(&connection_warning_icon_);

  // Set 'info' icon on left side.
  std::unique_ptr<views::ImageView> image_view = base::WrapUnique(
      TrayPopupUtils::CreateMainImageView(/*use_wide_layout=*/false));
  image_view->SetImage(gfx::CreateVectorIcon(
      use_managed_icon ? kSystemTrayManagedIcon : kSystemMenuInfoIcon,
      AshColorProvider::Get()->GetContentLayerColor(
          AshColorProvider::ContentLayerType::kIconColorPrimary)));
  image_view->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
  image_view->SetID(newIconId);
  connection_warning_icon_ = image_view.get();
  parent->AddView(TriView::Container::START, image_view.release());
}

bool NetworkListViewControllerImpl::ShouldMobileDataSectionBeShown() {
  // The section should always be shown if Cellular networks are available.
  if (model()->GetDeviceState(NetworkType::kCellular) !=
      DeviceStateType::kUnavailable) {
    return true;
  }

  if (features::IsInstantHotspotRebrandEnabled()) {
    return false;
  }

  const DeviceStateType tether_state =
      model()->GetDeviceState(NetworkType::kTether);

  // Hide the section if both Cellular and Tether are UNAVAILABLE.
  if (tether_state == DeviceStateType::kUnavailable) {
    return false;
  }

  // Hide the section if Tether is PROHIBITED.
  if (tether_state == DeviceStateType::kProhibited) {
    return false;
  }

  // Secondary users cannot enable Bluetooth, and Tether is only UNINITIALIZED
  // if Bluetooth is disabled. Hide the section in this case.
  if (tether_state == DeviceStateType::kUninitialized && IsSecondaryUser()) {
    return false;
  }

  return true;
}

bool NetworkListViewControllerImpl::ShouldTetherHostsSectionBeShown() {
  // The section should never be shown if the feature flag is disabled.
  if (!features::IsInstantHotspotRebrandEnabled()) {
    return false;
  }

  // The Tether Hosts section should always be shown if there is an eligible
  // device which has not been set up yet.
  if (has_phone_eligible_for_setup_) {
    return true;
  }

  const DeviceStateType tether_state =
      model()->GetDeviceState(NetworkType::kTether);

  // Hide the section if Tether is Unavailable, Prohibited, or Disabled.
  if (tether_state == DeviceStateType::kUnavailable ||
      tether_state == DeviceStateType::kProhibited ||
      tether_state == DeviceStateType::kDisabled) {
    return false;
  }

  // Secondary users cannot enable Bluetooth, and Tether is only UNINITIALIZED
  // if Bluetooth is disabled. Hide the section in this case.
  if (tether_state == DeviceStateType::kUninitialized && IsSecondaryUser()) {
    return false;
  }

  return true;
}

size_t NetworkListViewControllerImpl::CreateWifiGroupHeader(
    size_t index,
    const bool is_known) {
  // If the headers are already created, reorder the child views and return.
  if (is_known && known_header_) {
    network_detailed_network_view()
        ->GetNetworkList(NetworkType::kWiFi)
        ->ReorderChildView(known_header_, index++);
    return index;
  }
  if (!is_known && unknown_header_) {
    network_detailed_network_view()
        ->GetNetworkList(NetworkType::kWiFi)
        ->ReorderChildView(unknown_header_, index++);
    return index;
  }

  auto header = std::make_unique<views::Label>();
  header->SetText(l10n_util::GetStringUTF16(
      is_known ? IDS_ASH_QUICK_SETTINGS_KNOWN_NETWORKS
               : IDS_ASH_QUICK_SETTINGS_UNKNOWN_NETWORKS));
  header->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_TO_HEAD);
  header->SetBorder(views::CreateEmptyBorder(kWifiGroupLabelPadding));
  header->SetEnabledColorId(cros_tokens::kCrosSysOnSurfaceVariant);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2, *header);

  if (is_known) {
    known_header_ = network_detailed_network_view()
                        ->GetNetworkList(NetworkType::kWiFi)
                        ->AddChildViewAt(std::move(header), index++);
    return index;
  }

  unknown_header_ = network_detailed_network_view()
                        ->GetNetworkList(NetworkType::kWiFi)
                        ->AddChildViewAt(std::move(header), index++);
  return index;
}

size_t NetworkListViewControllerImpl::CreateConfigureNetworkEntry(
    raw_ptr<HoverHighlightView>* configure_network_entry_ptr,
    NetworkType type,
    size_t index) {
  if (*configure_network_entry_ptr) {
    network_detailed_network_view()->GetNetworkList(type)->ReorderChildView(
        *configure_network_entry_ptr, index++);
    return index;
  }

  *configure_network_entry_ptr =
      network_detailed_network_view()->AddConfigureNetworkEntry(type);
  return index++;
}

void NetworkListViewControllerImpl::UpdateMobileSection() {
  if (!mobile_header_view_) {
    return;
  }
  UpdateMobileToggleAndSetStatusMessage();
}

void NetworkListViewControllerImpl::UpdateTetherHostsSection() {
  CHECK(features::IsInstantHotspotRebrandEnabled());
  if (!tether_hosts_header_view_) {
    return;
  }

  network_detailed_network_view()->UpdateTetherHostsStatus(
      tether_hosts_header_view_->is_expanded());

  if (!IsBluetoothEnabledOrEnabling(bluetooth_system_state_) &&
      !has_phone_eligible_for_setup_) {
    CreateInfoLabelIfMissingAndUpdate(
        IDS_ASH_STATUS_TRAY_NETWORK_TETHER_NO_BLUETOOTH,
        &tether_hosts_status_message_);
    return;
  }

  if (!has_tether_networks_ && !has_phone_eligible_for_setup_) {
    CreateInfoLabelIfMissingAndUpdate(
        IDS_ASH_STATUS_TRAY_NETWORK_NO_TETHER_DEVICES_FOUND,
        &tether_hosts_status_message_);
    return;
  }

  RemoveAndResetViewIfExists(&tether_hosts_status_message_);
}

void NetworkListViewControllerImpl::UpdateWifiSection() {
  if (!wifi_header_view_) {
    wifi_header_view_ = network_detailed_network_view()->AddWifiSectionHeader();

    // WiFi toggle state is set here to avoid toggle animating on/off when
    // detailed view is opened for the first time. Enabled state will be
    // updated in subsequent calls.
    wifi_header_view_->SetToggleState(/*enabled=*/false,
                                      /*is_on=*/is_wifi_enabled_,
                                      /*animate_toggle=*/false);
  }

  wifi_header_view_->SetToggleVisibility(/*visible=*/true);
  wifi_header_view_->SetToggleState(/*enabled=*/true,
                                    /*is_on=*/is_wifi_enabled_,
                                    /*animate_toggle=*/true);

  network_detailed_network_view()->UpdateWifiStatus(is_wifi_enabled_);

  if (!is_wifi_enabled_) {
    return;
  }

  if (!has_wifi_networks_) {
    CreateInfoLabelIfMissingAndUpdate(IDS_ASH_STATUS_TRAY_NETWORK_WIFI_ENABLED,
                                      &wifi_status_message_);
  } else {
    RemoveAndResetViewIfExists(&wifi_status_message_);
  }
}

void NetworkListViewControllerImpl::UpdateMobileToggleAndSetStatusMessage() {
  if (!mobile_header_view_) {
    return;
  }

  const DeviceStateType cellular_state =
      model()->GetDeviceState(NetworkType::kCellular);
  const DeviceStateType tether_state =
      model()->GetDeviceState(NetworkType::kTether);

  const bool is_secondary_user = IsSecondaryUser();

  if (cellular_state == DeviceStateType::kUninitialized) {
    CreateInfoLabelIfMissingAndUpdate(IDS_ASH_STATUS_TRAY_INITIALIZING_CELLULAR,
                                      &mobile_status_message_);
    mobile_header_view_->SetToggleState(/*enabled=*/false,
                                        /*is_on=*/false,
                                        /*animate_toggle=*/true);
    // Updates the Mobile status to `true` so that the info label will be
    // visible, although the toggle is off.
    network_detailed_network_view()->UpdateMobileStatus(true);
    return;
  }

  if (cellular_state != DeviceStateType::kUnavailable) {
    if (IsCellularDeviceInhibited()) {
      // When a device is inhibited, it cannot process any new operations. Thus,
      // keep the toggle on to show users that the device is active, but set it
      // to be disabled to make it clear that users cannot update it until it
      // becomes uninhibited.
      mobile_header_view_->SetToggleVisibility(/*visible=*/true);
      mobile_header_view_->SetToggleState(/*enabled=*/false,
                                          /*is_on=*/true,
                                          /*animate_toggle=*/true);
      network_detailed_network_view()->UpdateMobileStatus(true);

      RemoveAndResetViewIfExists(&mobile_status_message_);
      return;
    }

    if (IsCellularDeviceFlashing()) {
      // When a device is flashing, it cannot process any new operations. Thus,
      // keep the toggle on to show users that the device is active, but set it
      // to be disabled to make it clear that users cannot update it until
      // operation completes.
      CreateInfoLabelIfMissingAndUpdate(IDS_ASH_STATUS_TRAY_UPDATING,
                                        &mobile_status_message_);

      mobile_header_view_->SetToggleVisibility(/*visible=*/true);
      mobile_header_view_->SetToggleState(/*enabled=*/false,
                                          /*is_on=*/true,
                                          /*animate_toggle=*/true);
      network_detailed_network_view()->UpdateMobileStatus(true);

      return;
    }

    const bool cellular_enabled = cellular_state == DeviceStateType::kEnabled;

    // The toggle will never be enabled for secondary users.
    bool toggle_enabled = !is_secondary_user;

    // The toggle will never be enabled during cellular state transitions.
    toggle_enabled &=
        cellular_enabled || cellular_state == DeviceStateType::kDisabled;

    // The toggle will never be enabled if the device is SIM locked and we
    // cannot open the Settings UI.
    toggle_enabled &=
        cellular_enabled ||
        Shell::Get()->session_controller()->ShouldEnableSettings() ||
        !IsCellularSimLocked();

    mobile_header_view_->SetToggleVisibility(/*visibility=*/true);
    mobile_header_view_->SetToggleState(/*enabled=*/toggle_enabled,
                                        /*is_on=*/cellular_enabled,
                                        /*animate_toggle=*/true);

    if (cellular_state == DeviceStateType::kDisabling) {
      network_detailed_network_view()->UpdateMobileStatus(true);
      CreateInfoLabelIfMissingAndUpdate(
          IDS_ASH_STATUS_TRAY_NETWORK_MOBILE_DISABLING,
          &mobile_status_message_);
      return;
    }

    network_detailed_network_view()->UpdateMobileStatus(cellular_enabled);

    if (cellular_enabled) {
      if (has_cellular_networks_ ||
          (has_tether_networks_ &&
           !features::IsInstantHotspotRebrandEnabled())) {
        RemoveAndResetViewIfExists(&mobile_status_message_);
        return;
      }

      CreateInfoLabelIfMissingAndUpdate(IDS_ASH_STATUS_TRAY_NO_MOBILE_NETWORKS,
                                        &mobile_status_message_);
      return;
    }
    return;
  }

  // When Cellular is not available, always show the toggle.
  mobile_header_view_->SetToggleVisibility(/*visibility=*/true);

  // Otherwise, toggle state and status message reflect Tether.
  if (tether_state == DeviceStateType::kUninitialized) {
    if (bluetooth_system_state_ == BluetoothSystemState::kEnabling) {
      mobile_header_view_->SetToggleState(/*enabled=*/false,
                                          /*is_on=*/true,
                                          /*animate_toggle=*/true);
      CreateInfoLabelIfMissingAndUpdate(
          IDS_ASH_STATUS_TRAY_INITIALIZING_CELLULAR, &mobile_status_message_);
      network_detailed_network_view()->UpdateMobileStatus(true);
      return;
    }
    mobile_header_view_->SetToggleState(
        /*enabled=*/!is_secondary_user, /*is_on=*/false,
        /*animate_toggle=*/true);
    CreateInfoLabelIfMissingAndUpdate(
        IDS_ASH_STATUS_TRAY_ENABLING_MOBILE_ENABLES_BLUETOOTH,
        &mobile_status_message_);
    // Updates the Mobile status to `true` so that the info label will be
    // visible, although the toggle is off.
    network_detailed_network_view()->UpdateMobileStatus(true);
    return;
  }

  const bool tether_enabled = tether_state == DeviceStateType::kEnabled;

  // Ensure that the toggle state and status message match the tether state.
  mobile_header_view_->SetToggleState(/*enabled=*/!is_secondary_user,
                                      /*is_on=*/tether_enabled,
                                      /*animate_toggle=*/true);
  network_detailed_network_view()->UpdateMobileStatus(tether_enabled);

  if (tether_enabled && !has_tether_networks_ && !has_cellular_networks_) {
    CreateInfoLabelIfMissingAndUpdate(
        IDS_ASH_STATUS_TRAY_NO_MOBILE_DEVICES_FOUND, &mobile_status_message_);
    return;
  }

  RemoveAndResetViewIfExists(&mobile_status_message_);
}

void NetworkListViewControllerImpl::CreateInfoLabelIfMissingAndUpdate(
    int message_id,
    raw_ptr<TrayInfoLabel>* info_label_ptr) {
  DCHECK(message_id);
  DCHECK(info_label_ptr);

  TrayInfoLabel* info_label = *info_label_ptr;

  if (info_label) {
    info_label->Update(message_id);
    return;
  }

  std::unique_ptr<TrayInfoLabel> info =
      std::make_unique<TrayInfoLabel>(message_id);

  if (info_label_ptr == &mobile_status_message_) {
    info->SetID(static_cast<int>(
        NetworkListViewControllerViewChildId::kMobileStatusMessage));
    *info_label_ptr = network_detailed_network_view()
                          ->GetNetworkList(GetMobileSectionNetworkType())
                          ->AddChildView(std::move(info));
  } else if (info_label_ptr == &wifi_status_message_) {
    info->SetID(static_cast<int>(
        NetworkListViewControllerViewChildId::kWifiStatusMessage));
    *info_label_ptr = network_detailed_network_view()
                          ->GetNetworkList(NetworkType::kWiFi)
                          ->AddChildView(std::move(info));
  } else if (info_label_ptr == &tether_hosts_status_message_) {
    info->SetID(static_cast<int>(
        NetworkListViewControllerViewChildId::kTetherHostsStatusMessage));
    *info_label_ptr = network_detailed_network_view()
                          ->GetNetworkList(NetworkType::kTether)
                          ->AddChildView(std::move(info));
  } else {
    NOTREACHED();
  }
}

size_t NetworkListViewControllerImpl::CreateItemViewsIfMissingAndReorder(
    NetworkType type,
    size_t index,
    std::vector<NetworkStatePropertiesPtr>& networks,
    NetworkIdToViewMap* previous_views) {
  NetworkIdToViewMap id_to_view_map;
  NetworkListNetworkItemView* network_view = nullptr;

  for (const auto& network : networks) {
    if (!NetworkTypeMatchesType(network->type, type)) {
      continue;
    }

    const std::string& network_id = network->guid;
    auto it = previous_views->find(network_id);
    if (it == previous_views->end()) {
      network_view = network_detailed_network_view()->AddNetworkListItem(type);
    } else {
      network_view = it->second;
      previous_views->erase(it);
    }
    network_id_to_view_map_.emplace(network_id, network_view);

    network_view->UpdateViewForNetwork(network);
    network_detailed_network_view()->GetNetworkList(type)->ReorderChildView(
        network_view, index);
    network_view->SetEnabled(!IsNetworkDisabled(network));

    // Increment `index` since this position was taken by `network_view`.
    index++;
  }

  return index;
}

std::u16string NetworkListViewControllerImpl::GenerateLabelText() {
  const bool using_proxy =
      model()->default_network() &&
      model()->default_network()->proxy_mode != ProxyMode::kDirect;

  // If the device is not managed then the high risk disclosure should be shown.
  // VPN connections always show a high risk disclosure, regardless of the
  // management state.
  if ((using_proxy && !is_proxy_managed_) || !connected_vpn_guid_.empty()) {
    return l10n_util::GetStringUTF16(
        IDS_ASH_STATUS_TRAY_NETWORK_MONITORED_WARNING);
  }

  // If XDR reporting is enabled the high risk disclosure should be shown.
  if (model()->global_policy()->report_xdr_events_enabled) {
    return l10n_util::GetStringUTF16(
        IDS_ASH_STATUS_TRAY_NETWORK_MONITORED_WARNING);
  }

  return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_MANAGED_WARNING);
}

void NetworkListViewControllerImpl::ShowConnectionWarning(
    bool show_managed_icon) {
  // Set up layout and apply sticky row property.
  std::unique_ptr<TriView> connection_warning(
      TrayPopupUtils::CreateDefaultRowView(/*use_wide_layout=*/false));
  TrayPopupUtils::ConfigureHeader(connection_warning.get());

  SetConnectionWarningIcon(connection_warning.get(),
                           /*use_managed_icon=*/show_managed_icon);

  // Set message label in middle of row.
  std::unique_ptr<views::Label> label =
      base::WrapUnique(TrayPopupUtils::CreateDefaultLabel());
  label->SetText(GenerateLabelText());
  label->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
  label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kTextColorPrimary));
  label->SetAutoColorReadabilityEnabled(false);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2, *label);
  label->SetID(static_cast<int>(
      NetworkListViewControllerViewChildId::kConnectionWarningLabel));
  connection_warning_label_ = label.get();

  connection_warning->AddView(TriView::Container::CENTER, std::move(label));
  connection_warning->SetContainerBorder(
      TriView::Container::CENTER, views::CreateEmptyBorder(gfx::Insets::TLBR(
                                      0, 0, 0, kTrayPopupLabelRightPadding)));

  // Nothing to the right of the text.
  connection_warning->SetContainerVisible(TriView::Container::END, false);
  connection_warning->SetID(static_cast<int>(
      NetworkListViewControllerViewChildId::kConnectionWarning));
  // The warning messages are shown in the ethernet section.
  connection_warning_ = network_detailed_network_view()
                            ->GetNetworkList(NetworkType::kEthernet)
                            ->AddChildView(std::move(connection_warning));
}

void NetworkListViewControllerImpl::HideConnectionWarning() {
  // If `connection_warning_icon_` or `connection_warning_label_` existed, they
  // must be cleared first because `connection_warning_` owns them.
  RemoveAndResetViewIfExists(&connection_warning_icon_);
  RemoveAndResetViewIfExists(&connection_warning_label_);
  RemoveAndResetViewIfExists(&connection_warning_);
}

void NetworkListViewControllerImpl::UpdateScanningBarAndTimer() {
  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 is_scanning_bar_visible = false;
  if (is_wifi_enabled_) {
    const DeviceStateProperties* wifi = model_->GetDevice(NetworkType::kWiFi);
    const DeviceStateProperties* tether =
        model_->GetDevice(NetworkType::kTether);

    is_scanning_bar_visible =
        (wifi && wifi->scanning) || (tether && tether->scanning);
  }

  network_detailed_network_view()->UpdateScanningBarVisibility(
      /*visible=*/is_scanning_bar_visible);
}

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

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

void NetworkListViewControllerImpl::FocusLastSelectedView() {
  views::View* selected_view = nullptr;
  views::View* parent_view =
      network_detailed_network_view()->GetNetworkList(NetworkType::kAll);
  for (const auto& [network_id, view] : network_id_to_view_map_) {
    // The within_bounds check is necessary when the network list goes beyond
    // the visible area (i.e. scrolling) and the mouse is below the tray pop-up.
    // The items not in view in the tray pop-up keep going down and have
    // View::GetVisibility() == true but they are masked and not seen by the
    // user. When the mouse is below the list where the item would be if the
    // list continued downward, IsMouseHovered() is true and this will trigger
    // an incorrect programmatic scroll if we don't stop it. The bounds check
    // ensures the view is actually visible within the tray pop-up.
    bool within_bounds =
        parent_view->GetBoundsInScreen().Intersects(view->GetBoundsInScreen());
    if (within_bounds && view->IsMouseHovered()) {
      selected_view = view;
      break;
    }
  }

  parent_view->SizeToPreferredSize();
  parent_view->DeprecatedLayoutImmediately();
  if (selected_view) {
    parent_view->ScrollRectToVisible(selected_view->bounds());
  }
}

}  // namespace ash