blob: 4d4e422769c225c9fe3d2f0600be2c01bc40c0ac [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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.h"
#include <memory>
#include <utility>
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/network/network_icon.h"
#include "ash/system/network/network_icon_animation.h"
#include "ash/system/network/network_info.h"
#include "ash/system/network/network_section_header_view.h"
#include "ash/system/network/network_state_list_detailed_view.h"
#include "ash/system/power/power_status.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_info_label.h"
#include "ash/system/tray/tray_popup_item_style.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tri_view.h"
#include "base/i18n/number_formatting.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/network/network_handler.h"
#include "chromeos/network/proxy/ui_proxy_config_service.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "chromeos/services/network_config/public/mojom/constants.mojom.h"
#include "components/device_event_log/device_event_log.h"
#include "services/service_manager/public/cpp/connector.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
using chromeos::network_config::NetworkTypeMatchesType;
using chromeos::network_config::StateIsConnected;
using chromeos::network_config::mojom::ActivationStateType;
using chromeos::network_config::mojom::ConnectionStateType;
using chromeos::network_config::mojom::DeviceStatePropertiesPtr;
using chromeos::network_config::mojom::DeviceStateType;
using chromeos::network_config::mojom::FilterType;
using chromeos::network_config::mojom::NetworkFilter;
using chromeos::network_config::mojom::NetworkStatePropertiesPtr;
using chromeos::network_config::mojom::NetworkType;
using chromeos::network_config::mojom::ONCSource;
namespace ash {
namespace tray {
namespace {
const int kMobileNetworkBatteryIconSize = 14;
const int kPowerStatusPaddingRight = 10;
bool IsSecondaryUser() {
SessionControllerImpl* session_controller =
Shell::Get()->session_controller();
return session_controller->IsActiveUserSessionStarted() &&
!session_controller->IsUserPrimary();
}
} // namespace
// NetworkListView:
NetworkListView::NetworkListView(DetailedViewDelegate* delegate,
LoginStatus login)
: NetworkStateListDetailedView(delegate, LIST_TYPE_NETWORK, login) {
BindCrosNetworkConfig();
}
NetworkListView::~NetworkListView() {
network_icon::NetworkIconAnimation::GetInstance()->RemoveObserver(this);
}
void NetworkListView::UpdateNetworkList() {
CHECK(scroll_content());
DCHECK(cros_network_config_ptr_);
cros_network_config_ptr_->GetDeviceStateList(base::BindOnce(
&NetworkListView::OnGetDeviceStateList, base::Unretained(this)));
}
bool NetworkListView::IsNetworkEntry(views::View* view,
std::string* guid) const {
std::map<views::View*, std::string>::const_iterator found =
network_map_.find(view);
if (found == network_map_.end())
return false;
*guid = found->second;
return true;
}
void NetworkListView::BindCrosNetworkConfig() {
// Ensure binding is reset in case this is called after a failure.
cros_network_config_ptr_.reset();
service_manager::Connector* connector = Shell::Get()->connector();
if (!connector)
return;
connector->BindInterface(chromeos::network_config::mojom::kServiceName,
&cros_network_config_ptr_);
// If the connection is lost (e.g. due to a crash), attempt to rebind it.
cros_network_config_ptr_.set_connection_error_handler(base::BindOnce(
&NetworkListView::BindCrosNetworkConfig, base::Unretained(this)));
}
void NetworkListView::OnGetDeviceStateList(
std::vector<DeviceStatePropertiesPtr> devices) {
device_states_.clear();
for (auto& device : devices)
device_states_[device->type] = device->state;
cros_network_config_ptr_->GetNetworkStateList(
NetworkFilter::New(FilterType::kVisible, NetworkType::kAll,
chromeos::network_config::mojom::kNoLimit),
base::BindOnce(&NetworkListView::OnGetNetworkStateList,
base::Unretained(this)));
}
void NetworkListView::OnGetNetworkStateList(
std::vector<NetworkStatePropertiesPtr> networks) {
// |network_list_| contains all the info and is going to be cleared and
// recreated. Save them to |last_network_info_map_|.
last_network_info_map_.clear();
for (auto& info : network_list_)
last_network_info_map_[info->guid] = std::move(info);
bool animating = false;
network_list_.clear();
vpn_connected_ = false;
wifi_has_networks_ = false;
for (auto& network : networks) {
ConnectionStateType connection_state = network->connection_state;
if (network->type == NetworkType::kVPN) {
if (chromeos::network_config::StateIsConnected(connection_state))
vpn_connected_ = true;
continue;
}
// If cellular is not enabled, skip cellular networks with no service.
ActivationStateType activation_state =
network->cellular ? network->cellular->activation_state
: ActivationStateType::kUnknown;
if (network->type == NetworkType::kCellular &&
GetDeviceState(NetworkType::kCellular) != DeviceStateType::kEnabled &&
activation_state == ActivationStateType::kNoService) {
continue;
}
if (network->type == NetworkType::kWiFi)
wifi_has_networks_ = true;
auto info = std::make_unique<NetworkInfo>(network->guid);
network_icon::NetworkIconState network_icon_state(network.get());
info->label = network_icon::GetLabelForNetworkList(network_icon_state);
// |network_list_| only contains non virtual networks.
info->image = network_icon::GetImageForNonVirtualNetwork(
network_icon_state, network_icon::ICON_TYPE_LIST,
false /* badge_vpn */);
info->disable = activation_state == ActivationStateType::kActivating ||
network->prohibited_by_policy;
if (network->prohibited_by_policy) {
info->tooltip =
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_PROHIBITED);
}
info->connection_state = connection_state;
if (network->tether)
info->battery_percentage = network->tether->battery_percentage;
if (network->captive_portal_provider) {
info->captive_portal_provider_name =
network->captive_portal_provider->name;
}
info->type = network->type;
info->source = network->source;
if (!animating && connection_state == ConnectionStateType::kConnecting)
animating = true;
network_list_.push_back(std::move(info));
}
if (animating)
network_icon::NetworkIconAnimation::GetInstance()->AddObserver(this);
else
network_icon::NetworkIconAnimation::GetInstance()->RemoveObserver(this);
UpdateNetworkListInternal();
}
void NetworkListView::UpdateNetworkListInternal() {
// Get the updated list entries.
needs_relayout_ = false;
network_map_.clear();
std::unique_ptr<std::set<std::string>> new_guids = UpdateNetworkListEntries();
// Remove old children.
std::set<std::string> remove_guids;
for (const auto& iter : network_guid_map_) {
if (new_guids->find(iter.first) == new_guids->end()) {
remove_guids.insert(iter.first);
network_map_.erase(iter.second);
delete iter.second;
needs_relayout_ = true;
}
}
for (const auto& remove_iter : remove_guids)
network_guid_map_.erase(remove_iter);
if (!needs_relayout_)
return;
views::View* selected_view = nullptr;
for (const auto& iter : network_guid_map_) {
if (iter.second->IsMouseHovered()) {
selected_view = iter.second;
break;
}
}
scroll_content()->SizeToPreferredSize();
scroller()->Layout();
if (selected_view)
scroll_content()->ScrollRectToVisible(selected_view->bounds());
}
std::unique_ptr<std::set<std::string>>
NetworkListView::UpdateNetworkListEntries() {
// Keep an index where the next child should be inserted.
int index = 0;
bool using_proxy = false;
// TODO(https://crbug.com/718072): Create UIProxyConfigService under mash, or
// provide this via network_config.mojom.
if (!::features::IsMultiProcessMash()) {
using_proxy = chromeos::NetworkHandler::Get()
->ui_proxy_config_service()
->HasDefaultNetworkProxyConfigured();
}
// Show a warning that the connection might be monitored if connected to a VPN
// or if the default network has a proxy installed.
if (vpn_connected_ || using_proxy) {
if (!connection_warning_)
connection_warning_ = CreateConnectionWarning();
PlaceViewAtIndex(connection_warning_, index++);
}
// First add Ethernet networks.
std::unique_ptr<std::set<std::string>> new_guids =
UpdateNetworkChildren(NetworkType::kEthernet, index);
index += new_guids->size();
if (ShouldMobileDataSectionBeShown()) {
if (!mobile_header_view_)
mobile_header_view_ = new MobileSectionHeaderView();
index = UpdateNetworkSectionHeader(
NetworkType::kMobile, false /* enabled */, index, mobile_header_view_,
&mobile_separator_view_);
std::unique_ptr<std::set<std::string>> new_cellular_guids =
UpdateNetworkChildren(NetworkType::kMobile, index);
int mobile_status_message =
mobile_header_view_->UpdateToggleAndGetStatusMessage();
// |mobile_status_message| may be zero. Passing zero to UpdateInfoLabel
// clears the label.
UpdateInfoLabel(mobile_status_message, index, &mobile_status_message_);
if (mobile_status_message)
++index;
index += new_cellular_guids->size();
new_guids->insert(new_cellular_guids->begin(), new_cellular_guids->end());
}
if (!wifi_header_view_)
wifi_header_view_ = new WifiSectionHeaderView();
bool wifi_enabled =
GetDeviceState(NetworkType::kWiFi) == DeviceStateType::kEnabled;
index = UpdateNetworkSectionHeader(NetworkType::kWiFi, wifi_enabled, index,
wifi_header_view_, &wifi_separator_view_);
if (!wifi_enabled) {
UpdateInfoLabel(IDS_ASH_STATUS_TRAY_NETWORK_WIFI_DISABLED, index,
&wifi_status_message_);
return new_guids;
}
bool should_clear_info_label = true;
if (!wifi_has_networks_) {
UpdateInfoLabel(IDS_ASH_STATUS_TRAY_NETWORK_WIFI_ENABLED, index,
&wifi_status_message_);
++index;
should_clear_info_label = false;
}
// Add Wi-Fi networks.
std::unique_ptr<std::set<std::string>> new_wifi_guids =
UpdateNetworkChildren(NetworkType::kWiFi, index);
index += new_wifi_guids->size();
new_guids->insert(new_wifi_guids->begin(), new_wifi_guids->end());
// No networks or other messages (fallback).
if (index == 0) {
UpdateInfoLabel(IDS_ASH_STATUS_TRAY_NO_NETWORKS, index,
&wifi_status_message_);
} else if (should_clear_info_label) {
// Update the label to show nothing.
UpdateInfoLabel(0, index, &wifi_status_message_);
}
return new_guids;
}
bool NetworkListView::ShouldMobileDataSectionBeShown() {
// The section should always be shown if Cellular networks are available.
if (GetDeviceState(NetworkType::kCellular) != DeviceStateType::kUnavailable)
return true;
DeviceStateType tether_state = 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;
}
void NetworkListView::UpdateViewForNetwork(HoverHighlightView* view,
const NetworkInfo& info) {
view->Reset();
gfx::ImageSkia network_image;
if (NetworkTypeMatchesType(info.type, NetworkType::kMobile) &&
info.connection_state == ConnectionStateType::kNotConnected) {
// Mobile icons which are not connecting or connected should display a small
// "X" icon superimposed so that it is clear that they are disconnected.
network_image = gfx::ImageSkiaOperations::CreateSuperimposedImage(
info.image, gfx::CreateVectorIcon(kNetworkMobileNotConnectedXIcon,
info.image.height(),
kMobileNotConnectedXIconColor));
} else {
network_image = info.image;
}
view->AddIconAndLabel(network_image, info.label);
if (StateIsConnected(info.connection_state))
SetupConnectedScrollListItem(view);
else if (info.connection_state == ConnectionStateType::kConnecting)
SetupConnectingScrollListItem(view);
view->SetTooltipText(info.tooltip);
// Add an additional icon to the right of the label for networks
// that require it (e.g. Tether, controlled by extension).
views::View* icon = CreatePowerStatusView(info);
if (icon) {
view->AddRightView(icon, views::CreateEmptyBorder(gfx::Insets(
0 /* top */, 0 /* left */, 0 /* bottom */,
kPowerStatusPaddingRight)));
} else {
icon = CreatePolicyView(info);
if (!icon)
icon = CreateControlledByExtensionView(info);
if (icon)
view->AddRightView(icon);
}
needs_relayout_ = true;
}
views::View* NetworkListView::CreatePowerStatusView(const NetworkInfo& info) {
// Mobile can be Cellular or Tether.
if (!NetworkTypeMatchesType(info.type, NetworkType::kMobile))
return nullptr;
// Only return a battery icon for Tether network type.
if (info.type != NetworkType::kTether)
return nullptr;
views::ImageView* icon = TrayPopupUtils::CreateMoreImageView();
PowerStatus::BatteryImageInfo icon_info;
icon_info.charge_percent = info.battery_percentage;
icon->SetImage(
PowerStatus::GetBatteryImage(icon_info, kMobileNetworkBatteryIconSize,
kMenuIconColorDisabled, kMenuIconColor));
// Show the numeric battery percentage on hover.
icon->set_tooltip_text(base::FormatPercent(info.battery_percentage));
return icon;
}
views::View* NetworkListView::CreatePolicyView(const NetworkInfo& info) {
// Check if the network is managed by policy.
ONCSource source = info.source;
if (source != ONCSource::kDevicePolicy && source != ONCSource::kUserPolicy)
return nullptr;
views::ImageView* controlled_icon = TrayPopupUtils::CreateMainImageView();
controlled_icon->SetImage(
gfx::CreateVectorIcon(kSystemMenuBusinessIcon, kMenuIconColor));
return controlled_icon;
}
views::View* NetworkListView::CreateControlledByExtensionView(
const NetworkInfo& info) {
if (info.captive_portal_provider_name.empty())
return nullptr;
views::ImageView* controlled_icon = TrayPopupUtils::CreateMainImageView();
controlled_icon->SetImage(
gfx::CreateVectorIcon(kCaptivePortalIcon, kMenuIconColor));
controlled_icon->set_tooltip_text(l10n_util::GetStringFUTF16(
IDS_ASH_STATUS_TRAY_EXTENSION_CONTROLLED_WIFI,
base::UTF8ToUTF16(info.captive_portal_provider_name)));
controlled_icon->SetID(VIEW_ID_EXTENSION_CONTROLLED_WIFI);
return controlled_icon;
}
std::unique_ptr<std::set<std::string>> NetworkListView::UpdateNetworkChildren(
NetworkType type,
int index) {
std::unique_ptr<std::set<std::string>> new_guids(new std::set<std::string>);
for (const auto& info : network_list_) {
if (!NetworkTypeMatchesType(info->type, type))
continue;
UpdateNetworkChild(index++, info.get());
new_guids->insert(info->guid);
}
return new_guids;
}
void NetworkListView::UpdateNetworkChild(int index, const NetworkInfo* info) {
HoverHighlightView* network_view = nullptr;
NetworkGuidMap::const_iterator found = network_guid_map_.find(info->guid);
if (found == network_guid_map_.end()) {
network_view = new HoverHighlightView(this);
UpdateViewForNetwork(network_view, *info);
} else {
network_view = found->second;
if (NeedUpdateViewForNetwork(*info))
UpdateViewForNetwork(network_view, *info);
}
PlaceViewAtIndex(network_view, index);
if (info->disable)
network_view->SetEnabled(false);
network_map_[network_view] = info->guid;
network_guid_map_[info->guid] = network_view;
}
void NetworkListView::PlaceViewAtIndex(views::View* view, int index) {
if (view->parent() != scroll_content()) {
scroll_content()->AddChildViewAt(view, index);
} else if (index > 0 && size_t{index} < scroll_content()->children().size() &&
scroll_content()->children()[size_t{index}] == view) {
// ReorderChildView() would no-op in this case, but we still want to avoid
// setting |needs_relayout_|.
return;
} else {
scroll_content()->ReorderChildView(view, index);
}
needs_relayout_ = true;
}
void NetworkListView::UpdateInfoLabel(int message_id,
int insertion_index,
TrayInfoLabel** info_label_ptr) {
TrayInfoLabel* info_label = *info_label_ptr;
if (!message_id) {
if (info_label) {
needs_relayout_ = true;
delete info_label;
*info_label_ptr = nullptr;
}
return;
}
if (!info_label)
info_label = new TrayInfoLabel(nullptr /* delegate */, message_id);
else
info_label->Update(message_id);
PlaceViewAtIndex(info_label, insertion_index);
*info_label_ptr = info_label;
}
int NetworkListView::UpdateNetworkSectionHeader(
chromeos::network_config::mojom::NetworkType type,
bool enabled,
int child_index,
NetworkSectionHeaderView* view,
views::Separator** separator_view) {
// Show or hide a separator above the header. The separator should only be
// visible when the header row is not at the top of the list.
if (child_index > 0) {
if (!*separator_view)
*separator_view = CreateListSubHeaderSeparator();
PlaceViewAtIndex(*separator_view, child_index++);
} else {
if (*separator_view)
delete *separator_view;
*separator_view = nullptr;
}
bool default_toggle_enabled = !IsSecondaryUser();
// Mobile updates its toggle state independently.
if (!NetworkTypeMatchesType(type, NetworkType::kMobile))
view->SetToggleState(default_toggle_enabled, enabled /* is_on */);
PlaceViewAtIndex(view, child_index++);
return child_index;
}
void NetworkListView::NetworkIconChanged() {
Update();
}
bool NetworkListView::NeedUpdateViewForNetwork(const NetworkInfo& info) const {
NetworkInfoMap::const_iterator found = last_network_info_map_.find(info.guid);
if (found == last_network_info_map_.end()) {
// If we cannot find |info| in |last_network_info_map_|, just return true
// since this is a new network so we have nothing to compare.
return true;
} else {
return *found->second != info;
}
}
TriView* NetworkListView::CreateConnectionWarning() {
// Set up layout and apply sticky row property.
TriView* connection_warning = TrayPopupUtils::CreateDefaultRowView();
TrayPopupUtils::ConfigureAsStickyHeader(connection_warning);
// Set 'info' icon on left side.
views::ImageView* image_view = TrayPopupUtils::CreateMainImageView();
image_view->SetImage(
gfx::CreateVectorIcon(kSystemMenuInfoIcon, kMenuIconColor));
image_view->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
connection_warning->AddView(TriView::Container::START, image_view);
// Set message label in middle of row.
views::Label* label = TrayPopupUtils::CreateDefaultLabel();
label->SetText(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_MONITORED_WARNING));
label->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL);
style.SetupLabel(label);
connection_warning->AddView(TriView::Container::CENTER, label);
connection_warning->SetContainerBorder(
TriView::Container::CENTER, views::CreateEmptyBorder(gfx::Insets(
0, 0, 0, kTrayPopupLabelRightPadding)));
// Nothing to the right of the text.
connection_warning->SetContainerVisible(TriView::Container::END, false);
return connection_warning;
}
DeviceStateType NetworkListView::GetDeviceState(NetworkType type) const {
auto iter = device_states_.find(type);
if (iter == device_states_.end())
return DeviceStateType::kUnavailable;
return iter->second;
}
} // namespace tray
} // namespace ash