1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ash/system/chromeos/network/vpn_list_view.h"
10 #include "ash/metrics/user_metrics_recorder.h"
11 #include "ash/shell.h"
12 #include "ash/system/chromeos/network/vpn_delegate.h"
13 #include "ash/system/tray/hover_highlight_view.h"
14 #include "ash/system/tray/system_tray_delegate.h"
15 #include "ash/system/tray/tray_constants.h"
16 #include "ash/system/tray/tray_popup_label_button.h"
17 #include "base/bind.h"
18 #include "base/bind_helpers.h"
19 #include "base/logging.h"
20 #include "base/memory/scoped_ptr.h"
21 #include "base/strings/utf_string_conversions.h"
22 #include "base/values.h"
23 #include "chromeos/network/network_connection_handler.h"
24 #include "chromeos/network/network_handler.h"
25 #include "chromeos/network/network_state.h"
26 #include "chromeos/network/network_type_pattern.h"
27 #include "grit/ash_strings.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/chromeos/network/network_icon.h"
30 #include "ui/chromeos/network/network_icon_animation.h"
31 #include "ui/chromeos/network/network_icon_animation_observer.h"
32 #include "ui/chromeos/network/network_list_delegate.h"
33 #include "ui/gfx/geometry/rect.h"
34 #include "ui/gfx/image/image_skia.h"
35 #include "ui/gfx/text_constants.h"
36 #include "ui/views/border.h"
37 #include "ui/views/controls/button/button.h"
38 #include "ui/views/controls/label.h"
39 #include "ui/views/controls/separator.h"
40 #include "ui/views/layout/box_layout.h"
41 #include "ui/views/view.h"
47 bool IsConnectedOrConnecting(const chromeos::NetworkState
* network
) {
48 return network
->IsConnectedState() || network
->IsConnectingState();
51 void IgnoreDisconnectError(const std::string
& error_name
,
52 scoped_ptr
<base::DictionaryValue
> error_data
) {
55 // The base class of all list entries, a |HoverHighlightView| with no border.
56 class VPNListEntryBase
: public HoverHighlightView
{
58 // When the user clicks the entry, the |parent|'s OnViewClicked() will be
60 explicit VPNListEntryBase(VPNListView
* parent
);
63 DISALLOW_COPY_AND_ASSIGN(VPNListEntryBase
);
66 // A list entry that represents a VPN provider.
67 class VPNListProviderEntry
: public VPNListEntryBase
{
69 VPNListProviderEntry(VPNListView
* parent
, const std::string
& name
);
72 DISALLOW_COPY_AND_ASSIGN(VPNListProviderEntry
);
75 // A list entry that represents a network. If the network is currently
76 // connecting, the icon shown by this list entry will be animated. If the
77 // network is currently connected, a disconnect button will be shown next to its
79 class VPNListNetworkEntry
: public VPNListEntryBase
,
80 public ui::network_icon::AnimationObserver
,
81 public views::ButtonListener
{
83 VPNListNetworkEntry(VPNListView
* parent
,
84 const chromeos::NetworkState
* network
);
85 ~VPNListNetworkEntry() override
;
87 // ui::network_icon::AnimationObserver:
88 void NetworkIconChanged() override
;
90 // views::ButtonListener:
91 void ButtonPressed(views::Button
* sender
, const ui::Event
& event
) override
;
94 // A disconnect button that will be shown if the network is currently
95 // connected. Updates the list entry's hover state as the mouse enters/exits
97 class DisconnectButton
: public TrayPopupLabelButton
{
99 explicit DisconnectButton(VPNListNetworkEntry
* parent
);
102 // TrayPopupLabelButton:
103 void OnMouseEntered(const ui::MouseEvent
& event
) override
;
104 void OnMouseExited(const ui::MouseEvent
& event
) override
;
105 void OnBoundsChanged(const gfx::Rect
& previous_bounds
) override
;
107 VPNListNetworkEntry
* parent_
;
109 DISALLOW_COPY_AND_ASSIGN(DisconnectButton
);
112 void UpdateFromNetworkState(const chromeos::NetworkState
* network
);
114 const std::string service_path_
;
116 DisconnectButton
* disconnect_button_
= nullptr;
118 DISALLOW_COPY_AND_ASSIGN(VPNListNetworkEntry
);
121 VPNListEntryBase::VPNListEntryBase(VPNListView
* parent
)
122 : HoverHighlightView(parent
) {
124 views::Border::CreateEmptyBorder(0, kTrayPopupPaddingHorizontal
, 0, 0));
127 VPNListProviderEntry::VPNListProviderEntry(VPNListView
* parent
,
128 const std::string
& name
)
129 : VPNListEntryBase(parent
) {
130 views::Label
* const label
=
131 AddLabel(base::UTF8ToUTF16(name
), gfx::ALIGN_LEFT
, false /* highlight */);
132 label
->SetBorder(views::Border::CreateEmptyBorder(5, 0, 5, 0));
135 VPNListNetworkEntry::VPNListNetworkEntry(VPNListView
* parent
,
136 const chromeos::NetworkState
* network
)
137 : VPNListEntryBase(parent
), service_path_(network
->path()) {
138 UpdateFromNetworkState(network
);
141 VPNListNetworkEntry::~VPNListNetworkEntry() {
142 ui::network_icon::NetworkIconAnimation::GetInstance()->RemoveObserver(this);
145 void VPNListNetworkEntry::NetworkIconChanged() {
146 UpdateFromNetworkState(
147 chromeos::NetworkHandler::Get()->network_state_handler()->GetNetworkState(
151 void VPNListNetworkEntry::ButtonPressed(views::Button
* sender
,
152 const ui::Event
& event
) {
153 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
154 UMA_STATUS_AREA_VPN_DISCONNECT_CLICKED
);
155 chromeos::NetworkHandler::Get()
156 ->network_connection_handler()
157 ->DisconnectNetwork(service_path_
, base::Bind(&base::DoNothing
),
158 base::Bind(&IgnoreDisconnectError
));
161 VPNListNetworkEntry::DisconnectButton::DisconnectButton(
162 VPNListNetworkEntry
* parent
)
163 : TrayPopupLabelButton(
165 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_VPN_DISCONNECT
)),
170 void VPNListNetworkEntry::DisconnectButton::OnMouseEntered(
171 const ui::MouseEvent
& event
) {
172 TrayPopupLabelButton::OnMouseEntered(event
);
173 parent_
->SetHoverHighlight(false);
176 void VPNListNetworkEntry::DisconnectButton::OnMouseExited(
177 const ui::MouseEvent
& event
) {
178 TrayPopupLabelButton::OnMouseExited(event
);
179 if (parent_
->IsMouseHovered())
180 parent_
->SetHoverHighlight(true);
183 void VPNListNetworkEntry::DisconnectButton::OnBoundsChanged(
184 const gfx::Rect
& previous_bounds
) {
185 TrayPopupLabelButton::OnBoundsChanged(previous_bounds
);
186 if (IsMouseHovered()) {
187 SetState(STATE_HOVERED
);
188 parent_
->SetHoverHighlight(false);
192 void VPNListNetworkEntry::UpdateFromNetworkState(
193 const chromeos::NetworkState
* network
) {
194 if (network
&& network
->IsConnectingState())
195 ui::network_icon::NetworkIconAnimation::GetInstance()->AddObserver(this);
197 ui::network_icon::NetworkIconAnimation::GetInstance()->RemoveObserver(this);
200 // This is a transient state where the network has been removed already but
201 // the network list in the UI has not been updated yet.
205 RemoveAllChildViews(true);
206 disconnect_button_
= nullptr;
208 AddIconAndLabel(ui::network_icon::GetImageForNetwork(
209 network
, ui::network_icon::ICON_TYPE_LIST
),
210 ui::network_icon::GetLabelForNetwork(
211 network
, ui::network_icon::ICON_TYPE_LIST
),
212 IsConnectedOrConnecting(network
));
213 if (network
->IsConnectedState()) {
214 disconnect_button_
= new DisconnectButton(this);
215 AddChildView(disconnect_button_
);
217 views::Border::CreateEmptyBorder(0, kTrayPopupPaddingHorizontal
, 0, 3));
220 views::Border::CreateEmptyBorder(0, kTrayPopupPaddingHorizontal
, 0, 0));
223 // The icon and the disconnect button are always set to their preferred size.
224 // All remaining space is used for the network name.
225 views::BoxLayout
* layout
= new views::BoxLayout(
226 views::BoxLayout::kHorizontal
, 0, 3, kTrayPopupPaddingBetweenItems
);
227 SetLayoutManager(layout
);
228 layout
->SetDefaultFlex(0);
229 layout
->SetFlexForView(text_label(), 1);
235 VPNListView::VPNListView(ui::NetworkListDelegate
* delegate
)
236 : delegate_(delegate
) {
237 Shell::GetInstance()->system_tray_delegate()->GetVPNDelegate()->AddObserver(
241 VPNListView::~VPNListView() {
243 ->system_tray_delegate()
245 ->RemoveObserver(this);
248 void VPNListView::Update() {
249 // Before updating the list, determine whether the user was hovering over one
250 // of the VPN provider or network entries.
251 scoped_ptr
<VPNProvider::Key
> hovered_provider_key
;
252 std::string hovered_network_service_path
;
253 for (const std::pair
<const views::View
* const, VPNProvider::Key
>& provider
:
254 provider_view_key_map_
) {
255 if (static_cast<const HoverHighlightView
*>(provider
.first
)->hover()) {
256 hovered_provider_key
.reset(new VPNProvider::Key(provider
.second
));
260 if (!hovered_provider_key
) {
261 for (const std::pair
<const views::View
*, std::string
>& entry
:
262 network_view_service_path_map_
) {
263 if (static_cast<const HoverHighlightView
*>(entry
.first
)->hover()) {
264 hovered_network_service_path
= entry
.second
;
271 container_
->RemoveAllChildViews(true);
272 provider_view_key_map_
.clear();
273 network_view_service_path_map_
.clear();
275 container_
->SetLayoutManager(
276 new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 0));
278 // Get the list of available VPN networks, in shill's priority order.
279 chromeos::NetworkStateHandler::NetworkStateList networks
;
280 chromeos::NetworkHandler::Get()
281 ->network_state_handler()
282 ->GetVisibleNetworkListByType(chromeos::NetworkTypePattern::VPN(),
285 if (!networks
.empty() && IsConnectedOrConnecting(networks
.front())) {
286 // If there is a connected or connecting network, show that network first.
287 AddNetwork(networks
.front());
288 networks
.erase(networks
.begin());
291 // Show all VPN providers and all networks that are currently disconnected.
292 AddProvidersAndNetworks(networks
);
294 // Determine whether one of the new list entries corresponds to the entry that
295 // the user was previously hovering over. If such an entry is found, the list
296 // will be scrolled to ensure the entry is visible.
297 const views::View
* scroll_to_show_view
= nullptr;
298 if (hovered_provider_key
) {
299 for (const std::pair
<const views::View
* const, VPNProvider::Key
>& provider
:
300 provider_view_key_map_
) {
301 if (provider
.second
== *hovered_provider_key
) {
302 scroll_to_show_view
= provider
.first
;
306 } else if (!hovered_network_service_path
.empty()) {
307 for (const std::pair
<const views::View
*, std::string
>& entry
:
308 network_view_service_path_map_
) {
309 if (entry
.second
== hovered_network_service_path
) {
310 scroll_to_show_view
= entry
.first
;
316 // Layout the updated list.
317 container_
->SizeToPreferredSize();
318 delegate_
->RelayoutScrollList();
320 if (scroll_to_show_view
) {
321 // Scroll the list so that |scroll_to_show_view| is in view.
322 container_
->ScrollRectToVisible(scroll_to_show_view
->bounds());
326 bool VPNListView::IsNetworkEntry(views::View
* view
,
327 std::string
* service_path
) const {
328 const auto& entry
= network_view_service_path_map_
.find(view
);
329 if (entry
== network_view_service_path_map_
.end())
331 *service_path
= entry
->second
;
335 void VPNListView::OnVPNProvidersChanged() {
339 void VPNListView::OnViewClicked(views::View
* sender
) {
340 const auto& provider
= provider_view_key_map_
.find(sender
);
341 if (provider
!= provider_view_key_map_
.end()) {
342 // If the user clicks on a provider entry, request that the "add network"
343 // dialog for this provider be shown.
344 const VPNProvider::Key
& key
= provider
->second
;
345 Shell
* shell
= Shell::GetInstance();
346 shell
->metrics()->RecordUserMetricsAction(
347 key
.third_party
? UMA_STATUS_AREA_VPN_ADD_THIRD_PARTY_CLICKED
348 : UMA_STATUS_AREA_VPN_ADD_BUILT_IN_CLICKED
);
349 shell
->system_tray_delegate()->GetVPNDelegate()->ShowAddPage(key
);
353 // If the user clicked on a network entry, let the |delegate_| trigger a
354 // connection attempt (if the network is currently disconnected) or show a
355 // configuration dialog (if the network is currently connected or connecting).
356 delegate_
->OnViewClicked(sender
);
359 void VPNListView::AddNetwork(const chromeos::NetworkState
* network
) {
360 views::View
* entry(new VPNListNetworkEntry(this, network
));
361 container_
->AddChildView(entry
);
362 network_view_service_path_map_
[entry
] = network
->path();
366 void VPNListView::AddProviderAndNetworks(
367 const VPNProvider::Key
& key
,
368 const std::string
& name
,
369 const chromeos::NetworkStateHandler::NetworkStateList
& networks
) {
370 // Add a visual separator, unless this is the topmost entry in the list.
372 views::Separator
* const separator
=
373 new views::Separator(views::Separator::HORIZONTAL
);
374 separator
->SetColor(kBorderLightColor
);
375 container_
->AddChildView(separator
);
379 // Add a list entry for the VPN provider.
380 views::View
* provider(new VPNListProviderEntry(this, name
));
381 container_
->AddChildView(provider
);
382 provider_view_key_map_
[provider
] = key
;
383 // Add the networks belonging to this provider, in the priority order returned
385 for (const chromeos::NetworkState
* const& network
: networks
) {
386 if (key
.MatchesNetwork(*network
))
391 void VPNListView::AddProvidersAndNetworks(
392 const chromeos::NetworkStateHandler::NetworkStateList
& networks
) {
393 // Get the list of VPN providers enabled in the primary user's profile.
394 std::vector
<VPNProvider
> providers
= Shell::GetInstance()
395 ->system_tray_delegate()
399 // Add providers with at least one configured network along with their
400 // networks. Providers are added in the order of their highest priority
402 for (const chromeos::NetworkState
* const& network
: networks
) {
403 for (auto provider
= providers
.begin(); provider
!= providers
.end();
405 if (!provider
->key
.MatchesNetwork(*network
))
407 AddProviderAndNetworks(provider
->key
, provider
->name
, networks
);
408 providers
.erase(provider
);
413 // Add providers without any configured networks, in the order that the
414 // providers were returned by the extensions system.
415 for (const VPNProvider
& provider
: providers
)
416 AddProviderAndNetworks(provider
.key
, provider
.name
, networks
);