| // Copyright 2015 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/vpn_list_view.h" |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "ash/metrics/user_metrics_recorder.h" |
| #include "ash/public/cpp/ash_pref_names.h" |
| #include "ash/public/cpp/system_tray_client.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/style/ash_color_provider.h" |
| #include "ash/system/model/system_tray_model.h" |
| #include "ash/system/network/network_icon.h" |
| #include "ash/system/network/network_icon_animation.h" |
| #include "ash/system/network/network_icon_animation_observer.h" |
| #include "ash/system/network/tray_network_state_model.h" |
| #include "ash/system/network/vpn_list.h" |
| #include "ash/system/tray/hover_highlight_view.h" |
| #include "ash/system/tray/system_menu_button.h" |
| #include "ash/system/tray/tray_popup_utils.h" |
| #include "ash/system/tray/tri_view.h" |
| #include "ash/system/tray/view_click_listener.h" |
| #include "ash/system/unified/unified_system_tray_view.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chromeos/network/network_connect.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 "components/onc/onc_constants.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "third_party/cros_system_api/dbus/service_constants.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/view.h" |
| |
| using chromeos::network_config::mojom::ConnectionStateType; |
| using chromeos::network_config::mojom::FilterType; |
| 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::VpnProvider; |
| using chromeos::network_config::mojom::VpnProviderPtr; |
| using chromeos::network_config::mojom::VPNStatePropertiesPtr; |
| using chromeos::network_config::mojom::VpnType; |
| |
| namespace ash { |
| namespace tray { |
| namespace { |
| |
| struct CompareArcVpnProviderByLastLaunchTime { |
| bool operator()(const VpnProviderPtr& provider1, |
| const VpnProviderPtr& provider2) { |
| return provider1->last_launch_time > provider2->last_launch_time; |
| } |
| }; |
| |
| // Indicates whether |network| belongs to this VPN provider. |
| bool VpnProviderMatchesNetwork(const VpnProvider* provider, |
| const NetworkStateProperties* network) { |
| DCHECK(network); |
| // Never display non-VPN networks or VPNs with no provider info. |
| if (network->type != NetworkType::kVPN) |
| return false; |
| |
| const VPNStatePropertiesPtr& vpn = network->type_state->get_vpn(); |
| if (vpn->type == VpnType::kArc || vpn->type == VpnType::kExtension) { |
| return vpn->type == provider->type && |
| vpn->provider_id == provider->provider_id; |
| } |
| |
| // Internal provider types all match the default internal provider. |
| return provider->type == VpnType::kOpenVPN; |
| } |
| |
| // Returns the PrefService that should be used for kVpnConfigAllowed, which is |
| // controlled by policy. If multiple users are logged in, the more restrictive |
| // policy is most likely in the primary user. |
| PrefService* GetPrefService() { |
| SessionControllerImpl* controller = Shell::Get()->session_controller(); |
| PrefService* prefs = controller->GetPrimaryUserPrefService(); |
| return prefs ? prefs : controller->GetActivePrefService(); |
| } |
| |
| bool IsVpnConfigAllowed() { |
| PrefService* prefs = GetPrefService(); |
| DCHECK(prefs); |
| return prefs->GetBoolean(prefs::kVpnConfigAllowed); |
| } |
| |
| // A list entry that represents a VPN provider. |
| class VPNListProviderEntry : public views::View { |
| public: |
| // Currently the |enabled| flag will be always true for VPN providers other |
| // than the built-in VPNs. |
| VPNListProviderEntry(const VpnProviderPtr& vpn_provider, |
| bool top_item, |
| const std::string& name, |
| bool enabled, |
| int button_accessible_name_id) |
| : vpn_provider_(vpn_provider->Clone()) { |
| TrayPopupUtils::ConfigureAsStickyHeader(this); |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| TriView* tri_view = TrayPopupUtils::CreateSubHeaderRowView(true); |
| tri_view->AddView(TriView::Container::START, |
| TrayPopupUtils::CreateMainImageView()); |
| AddChildView(tri_view); |
| |
| // Add the VPN label. |
| views::Label* label = TrayPopupUtils::CreateDefaultLabel(); |
| auto* color_provider = AshColorProvider::Get(); |
| label->SetEnabledColor(color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kTextColorPrimary)); |
| TrayPopupUtils::SetLabelFontList(label, |
| TrayPopupUtils::FontStyle::kSubHeader); |
| label->SetText(base::ASCIIToUTF16(name)); |
| tri_view->AddView(TriView::Container::CENTER, label); |
| |
| // Add the VPN policy indicator if using this |vpn_provider| is disabled. |
| if (!enabled) { |
| views::ImageView* policy_indicator_icon = GetPolicyIndicatorIcon(); |
| tri_view->AddView(TriView::Container::END, policy_indicator_icon); |
| } |
| |
| // Add the VPN add button. |
| const SkColor image_color = color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorProminent); |
| |
| const gfx::ImageSkia enabled_icon = |
| gfx::CreateVectorIcon(kSystemMenuAddConnectionIcon, image_color); |
| const gfx::ImageSkia disabled_icon = |
| gfx::CreateVectorIcon(kSystemMenuAddConnectionIcon, |
| AshColorProvider::GetDisabledColor(image_color)); |
| |
| SystemMenuButton* add_vpn_button = new SystemMenuButton( |
| base::BindRepeating(&VPNListProviderEntry::AddVpnButtonPressed, |
| base::Unretained(this)), |
| enabled_icon, disabled_icon, button_accessible_name_id); |
| |
| // 'Add VPN' is disabled in the login screen since user configured |
| // device-wide VPNs are unsupported. |
| LoginStatus login_status = |
| Shell::Get()->session_controller()->login_status(); |
| add_vpn_button->SetEnabled(enabled && |
| login_status != LoginStatus::NOT_LOGGED_IN); |
| tri_view->AddView(TriView::Container::END, add_vpn_button); |
| } |
| |
| // views::View: |
| const char* GetClassName() const override { return "VPNListProviderEntry"; } |
| |
| private: |
| views::ImageView* GetPolicyIndicatorIcon() { |
| views::ImageView* policy_indicator_icon = |
| TrayPopupUtils::CreateMainImageView(); |
| policy_indicator_icon->SetImage(gfx::CreateVectorIcon( |
| kSystemMenuBusinessIcon, |
| AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorPrimary))); |
| policy_indicator_icon->SetAccessibleName(l10n_util::GetStringFUTF16( |
| IDS_ASH_ACCESSIBILITY_FEATURE_MANAGED, |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_VPN_BUILT_IN_PROVIDER))); |
| return policy_indicator_icon; |
| } |
| |
| void AddVpnButtonPressed() { |
| // If the user clicks on a provider entry, request that the "add network" |
| // dialog for this provider be shown. |
| if (vpn_provider_->type == VpnType::kExtension) { |
| Shell::Get()->metrics()->RecordUserMetricsAction( |
| UMA_STATUS_AREA_VPN_ADD_THIRD_PARTY_CLICKED); |
| Shell::Get()->system_tray_model()->client()->ShowThirdPartyVpnCreate( |
| vpn_provider_->app_id); |
| } else if (vpn_provider_->type == VpnType::kArc) { |
| // TODO(lgcheng@) Add UMA status if needed. |
| Shell::Get()->system_tray_model()->client()->ShowArcVpnCreate( |
| vpn_provider_->app_id); |
| } else { |
| Shell::Get()->metrics()->RecordUserMetricsAction( |
| UMA_STATUS_AREA_VPN_ADD_BUILT_IN_CLICKED); |
| Shell::Get()->system_tray_model()->client()->ShowNetworkCreate( |
| ::onc::network_type::kVPN); |
| } |
| } |
| |
| VpnProviderPtr vpn_provider_; |
| |
| DISALLOW_COPY_AND_ASSIGN(VPNListProviderEntry); |
| }; |
| |
| // A list entry that represents a network. If the network is currently |
| // connecting, the icon shown by this list entry will be animated. If the |
| // network is currently connected, a disconnect button will be shown next to its |
| // name. |
| class VPNListNetworkEntry : public HoverHighlightView, |
| public network_icon::AnimationObserver { |
| public: |
| VPNListNetworkEntry(VPNListView* vpn_list_view, |
| TrayNetworkStateModel* model, |
| const NetworkStateProperties* network); |
| ~VPNListNetworkEntry() override; |
| |
| // network_icon::AnimationObserver: |
| void NetworkIconChanged() override; |
| |
| // views::View: |
| const char* GetClassName() const override { return "VPNListNetworkEntry"; } |
| |
| private: |
| void OnGetNetworkState(NetworkStatePropertiesPtr result); |
| void UpdateFromNetworkState(const NetworkStateProperties* network); |
| |
| VPNListView* const owner_; |
| TrayNetworkStateModel* model_; |
| const std::string guid_; |
| |
| views::LabelButton* disconnect_button_ = nullptr; |
| |
| base::WeakPtrFactory<VPNListNetworkEntry> weak_ptr_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(VPNListNetworkEntry); |
| }; |
| |
| VPNListNetworkEntry::VPNListNetworkEntry(VPNListView* owner, |
| TrayNetworkStateModel* model, |
| const NetworkStateProperties* network) |
| : HoverHighlightView(owner), |
| owner_(owner), |
| model_(model), |
| guid_(network->guid) { |
| UpdateFromNetworkState(network); |
| } |
| |
| VPNListNetworkEntry::~VPNListNetworkEntry() { |
| network_icon::NetworkIconAnimation::GetInstance()->RemoveObserver(this); |
| } |
| |
| void VPNListNetworkEntry::NetworkIconChanged() { |
| model_->cros_network_config()->GetNetworkState( |
| guid_, base::BindOnce(&VPNListNetworkEntry::OnGetNetworkState, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void VPNListNetworkEntry::OnGetNetworkState(NetworkStatePropertiesPtr result) { |
| UpdateFromNetworkState(result.get()); |
| } |
| |
| void VPNListNetworkEntry::UpdateFromNetworkState( |
| const NetworkStateProperties* vpn) { |
| if (vpn && vpn->connection_state == ConnectionStateType::kConnecting) |
| network_icon::NetworkIconAnimation::GetInstance()->AddObserver(this); |
| else |
| network_icon::NetworkIconAnimation::GetInstance()->RemoveObserver(this); |
| |
| if (!vpn) { |
| // This is a transient state where the vpn has been removed already but |
| // the network list in the UI has not been updated yet. |
| return; |
| } |
| Reset(); |
| disconnect_button_ = nullptr; |
| |
| gfx::ImageSkia image = |
| network_icon::GetImageForVPN(vpn, network_icon::ICON_TYPE_LIST); |
| base::string16 label = network_icon::GetLabelForNetworkList(vpn); |
| AddIconAndLabel(image, label); |
| if (chromeos::network_config::StateIsConnected(vpn->connection_state)) { |
| owner_->SetupConnectedScrollListItem(this); |
| if (IsVpnConfigAllowed()) { |
| disconnect_button_ = TrayPopupUtils::CreateTrayPopupButton( |
| // TODO(stevenjb): Replace with mojo API. https://crbug.com/862420. |
| base::BindRepeating( |
| &chromeos::NetworkConnect::DisconnectFromNetworkId, |
| base::Unretained(chromeos::NetworkConnect::Get()), guid_), |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_VPN_DISCONNECT)); |
| disconnect_button_->SetAccessibleName(l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_DISCONNECT_BUTTON_A11Y_LABEL, label)); |
| AddRightView(disconnect_button_); |
| } |
| tri_view()->SetContainerBorder( |
| TriView::Container::END, |
| views::CreateEmptyBorder( |
| 0, kTrayPopupButtonEndMargin - kTrayPopupLabelHorizontalPadding, 0, |
| kTrayPopupButtonEndMargin)); |
| SetAccessibleName(l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_A11Y_LABEL_OPEN_WITH_CONNECTION_STATUS, |
| label, |
| l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTED))); |
| } else if (vpn->connection_state == ConnectionStateType::kConnecting) { |
| SetAccessibleName(l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_A11Y_LABEL_OPEN_WITH_CONNECTION_STATUS, |
| label, |
| l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTING))); |
| owner_->SetupConnectingScrollListItem(this); |
| } else { |
| SetAccessibleName(l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_A11Y_LABEL_CONNECT, label)); |
| } |
| |
| Layout(); |
| } |
| |
| } // namespace |
| |
| VPNListView::VPNListView(DetailedViewDelegate* delegate, LoginStatus login) |
| : NetworkStateListDetailedView(delegate, LIST_TYPE_VPN, login) { |
| model()->vpn_list()->AddObserver(this); |
| } |
| |
| VPNListView::~VPNListView() { |
| model()->vpn_list()->RemoveObserver(this); |
| } |
| |
| void VPNListView::UpdateNetworkList() { |
| model()->cros_network_config()->GetNetworkStateList( |
| NetworkFilter::New(FilterType::kVisible, NetworkType::kVPN, |
| chromeos::network_config::mojom::kNoLimit), |
| base::BindOnce(&VPNListView::OnGetNetworkStateList, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void VPNListView::OnGetNetworkStateList(NetworkStateList networks) { |
| // Before updating the list, determine whether the user was hovering over one |
| // of the VPN provider or network entries. |
| VpnProviderPtr hovered_provider; |
| std::string hovered_network_guid; |
| for (const std::pair<const views::View* const, VpnProviderPtr>& entry : |
| provider_view_map_) { |
| if (entry.first->IsMouseHovered()) { |
| hovered_provider = entry.second->Clone(); |
| break; |
| } |
| } |
| if (!hovered_provider) { |
| for (const std::pair<const views::View* const, std::string>& entry : |
| network_view_guid_map_) { |
| if (entry.first->IsMouseHovered()) { |
| hovered_network_guid = entry.second; |
| break; |
| } |
| } |
| } |
| |
| // Clear the list. |
| scroll_content()->RemoveAllChildViews(true); |
| provider_view_map_.clear(); |
| network_view_guid_map_.clear(); |
| list_empty_ = true; |
| |
| // Show all VPN providers and all networks that are currently disconnected. |
| AddProvidersAndNetworks(networks); |
| |
| // Determine whether one of the new list entries corresponds to the entry that |
| // the user was previously hovering over. If such an entry is found, the list |
| // will be scrolled to ensure the entry is visible. |
| const views::View* scroll_to_show_view = nullptr; |
| if (hovered_provider) { |
| for (const std::pair<const views::View* const, VpnProviderPtr>& entry : |
| provider_view_map_) { |
| if (entry.second->Equals(*hovered_provider)) { |
| scroll_to_show_view = entry.first; |
| break; |
| } |
| } |
| } else if (!hovered_network_guid.empty()) { |
| for (const std::pair<const views::View* const, std::string>& entry : |
| network_view_guid_map_) { |
| if (entry.second == hovered_network_guid) { |
| scroll_to_show_view = entry.first; |
| break; |
| } |
| } |
| } |
| |
| // Layout the updated list. |
| scroll_content()->SizeToPreferredSize(); |
| scroller()->Layout(); |
| |
| if (scroll_to_show_view) { |
| // Scroll the list so that |scroll_to_show_view| is in view. |
| scroll_content()->ScrollRectToVisible(scroll_to_show_view->bounds()); |
| } |
| } |
| |
| bool VPNListView::IsNetworkEntry(views::View* view, std::string* guid) const { |
| const auto& entry = network_view_guid_map_.find(view); |
| if (entry == network_view_guid_map_.end()) |
| return false; |
| *guid = entry->second; |
| return true; |
| } |
| |
| void VPNListView::OnVpnProvidersChanged() { |
| UpdateNetworkList(); |
| } |
| |
| void VPNListView::RegisterProfilePrefs(PrefRegistrySimple* registry) { |
| registry->RegisterBooleanPref(prefs::kVpnConfigAllowed, true); |
| } |
| |
| const char* VPNListView::GetClassName() const { |
| return "VPNListView"; |
| } |
| |
| void VPNListView::AddNetwork(const NetworkStateProperties* network) { |
| views::View* entry(new VPNListNetworkEntry(this, model(), network)); |
| scroll_content()->AddChildView(entry); |
| network_view_guid_map_[entry] = network->guid; |
| list_empty_ = false; |
| } |
| |
| void VPNListView::AddProviderAndNetworks(VpnProviderPtr vpn_provider, |
| const NetworkStateList& networks) { |
| // Add a visual separator, unless this is the topmost entry in the list. |
| if (!list_empty_) { |
| scroll_content()->AddChildView(CreateListSubHeaderSeparator()); |
| } |
| std::string vpn_name = |
| vpn_provider->type == VpnType::kOpenVPN |
| ? l10n_util::GetStringUTF8(IDS_ASH_STATUS_TRAY_VPN_BUILT_IN_PROVIDER) |
| : vpn_provider->provider_name; |
| |
| // Add a list entry for the VPN provider. |
| views::View* provider_view = nullptr; |
| |
| // Note: Currently only built-in VPNs can be disabled by policy. |
| bool vpn_enabled = |
| vpn_provider->type != VpnType::kOpenVPN || model()->IsBuiltinVpnEnabled(); |
| |
| provider_view = |
| new VPNListProviderEntry(vpn_provider, list_empty_, vpn_name, vpn_enabled, |
| IDS_ASH_STATUS_TRAY_ADD_CONNECTION); |
| scroll_content()->AddChildView(provider_view); |
| const VpnProvider* vpn_providerp = vpn_provider.get(); |
| provider_view_map_[provider_view] = std::move(vpn_provider); |
| list_empty_ = false; |
| |
| if (vpn_enabled) { |
| // Add the networks belonging to this provider, in the priority order |
| // returned by shill. |
| for (const auto& network : networks) { |
| if (VpnProviderMatchesNetwork(vpn_providerp, network.get())) |
| AddNetwork(network.get()); |
| } |
| } |
| } |
| |
| bool VPNListView::ProcessProviderForNetwork( |
| const NetworkStateProperties* network, |
| const NetworkStateList& networks, |
| std::vector<VpnProviderPtr>* providers) { |
| for (auto provider_iter = providers->begin(); |
| provider_iter != providers->end(); ++provider_iter) { |
| if (!VpnProviderMatchesNetwork(provider_iter->get(), network)) |
| continue; |
| AddProviderAndNetworks(std::move(*provider_iter), networks); |
| providers->erase(provider_iter); |
| return true; |
| } |
| return false; |
| } |
| |
| void VPNListView::AddProvidersAndNetworks(const NetworkStateList& networks) { |
| // Copy the list of Extension VPN providers enabled in the primary user's |
| // profile. |
| std::vector<VpnProviderPtr> extension_providers; |
| for (const VpnProviderPtr& provider : |
| model()->vpn_list()->extension_vpn_providers()) { |
| extension_providers.push_back(provider->Clone()); |
| } |
| // Copy the list of Arc VPN providers installed in the primary user's profile. |
| std::vector<VpnProviderPtr> arc_providers; |
| for (const VpnProviderPtr& provider : |
| model()->vpn_list()->arc_vpn_providers()) { |
| arc_providers.push_back(provider->Clone()); |
| } |
| |
| std::sort(arc_providers.begin(), arc_providers.end(), |
| CompareArcVpnProviderByLastLaunchTime()); |
| |
| // Add connected ARCVPN network. If we can find the correct provider, nest |
| // the network under the provider. Otherwise list it unnested. |
| for (const auto& network : networks) { |
| if (network->connection_state == ConnectionStateType::kNotConnected) |
| break; |
| if (network->type_state->get_vpn()->type != VpnType::kArc) |
| continue; |
| |
| // If no matched provider found for this network. Show it unnested. |
| // TODO(lgcheng@) add UMA status to track this. |
| if (!ProcessProviderForNetwork(network.get(), networks, &arc_providers)) |
| AddNetwork(network.get()); |
| } |
| |
| // Add providers with at least one configured network along with their |
| // networks. Providers are added in the order of their highest priority |
| // network. |
| for (const auto& network : networks) |
| ProcessProviderForNetwork(network.get(), networks, &extension_providers); |
| |
| // Add providers without any configured networks, in the order that the |
| // providers were returned by the extensions system. |
| for (VpnProviderPtr& extension_provider : extension_providers) |
| AddProviderAndNetworks(std::move(extension_provider), {}); |
| |
| // Add Arc VPN providers without any connected or connecting networks. These |
| // providers are sorted by last launch time. |
| for (VpnProviderPtr& arc_provider : arc_providers) { |
| AddProviderAndNetworks(std::move(arc_provider), {}); |
| } |
| } |
| |
| } // namespace tray |
| } // namespace ash |