Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / options / chromeos / network_list.js
blobd3978c939abc09c0ff294770c3a9c3b8b3f36d63
1 // Copyright (c) 2012 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 /**
6  * Partial definition of the result of networkingPrivate.getProperties()).
7  * TODO(stevenjb): Replace with chrome.networkingPrivate.NetworkStateProperties
8  * once that is fully speced.
9  * @typedef {{
10  *   ConnectionState: string,
11  *   Cellular: {
12  *     Family: ?string,
13  *     SIMPresent: ?boolean,
14  *     SIMLockStatus: { LockType: ?string },
15  *     SupportNetworkScan: ?boolean
16  *   },
17  *   GUID: string,
18  *   Name: string,
19  *   Source: string,
20  *   Type: string
21  * }}
22  * @see extensions/common/api/networking_private.idl
23  */
24 var NetworkProperties;
26 cr.define('options.network', function() {
27   var ArrayDataModel = cr.ui.ArrayDataModel;
28   var List = cr.ui.List;
29   var ListItem = cr.ui.ListItem;
30   var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
31   var Menu = cr.ui.Menu;
32   var MenuItem = cr.ui.MenuItem;
33   var ControlledSettingIndicator = options.ControlledSettingIndicator;
35   /**
36    * Network settings constants. These enums usually match their C++
37    * counterparts.
38    */
39   function Constants() {}
41   /**
42    * Valid network type names.
43    */
44   Constants.NETWORK_TYPES = ['Ethernet', 'WiFi', 'WiMAX', 'Cellular', 'VPN'];
46   /**
47    * Helper function to check whether |type| is a valid network type.
48    * @param {string} type A string that may contain a valid network type.
49    * @return {boolean} Whether the string represents a valid network type.
50    */
51   function isNetworkType(type) {
52     return (Constants.NETWORK_TYPES.indexOf(type) != -1);
53   }
55   /**
56    * Order in which controls are to appear in the network list sorted by key.
57    */
58   Constants.NETWORK_ORDER = ['Ethernet',
59                              'WiFi',
60                              'WiMAX',
61                              'Cellular',
62                              'VPN',
63                              'addConnection'];
65   /**
66    * ID of the menu that is currently visible.
67    * @type {?string}
68    * @private
69    */
70   var activeMenu_ = null;
72   /**
73    * The state of the cellular device or undefined if not available.
74    * @type {?chrome.networkingPrivate.DeviceStateProperties}
75    * @private
76    */
77   var cellularDevice_ = null;
79   /**
80    * The active cellular network or null if none.
81    * @type {?NetworkProperties}
82    * @private
83    */
84   var cellularNetwork_ = null;
86   /**
87    * The active ethernet network or null if none.
88    * @type {?NetworkProperties}
89    * @private
90    */
91   var ethernetNetwork_ = null;
93   /**
94    * The state of the WiFi device or undefined if not available.
95    * @type {string|undefined}
96    * @private
97    */
98   var wifiDeviceState_ = undefined;
100   /**
101    * The state of the WiMAX device or undefined if not available.
102    * @type {string|undefined}
103    * @private
104    */
105   var wimaxDeviceState_ = undefined;
107   /**
108    * Indicates if mobile data roaming is enabled.
109    * @type {boolean}
110    * @private
111    */
112   var enableDataRoaming_ = false;
114   /**
115    * Returns the display name for 'network'.
116    * @param {NetworkProperties} data The network data dictionary.
117    */
118   function getNetworkName(data) {
119     if (data.Type == 'Ethernet')
120       return loadTimeData.getString('ethernetName');
121     if (data.Type == 'VPN')
122       return options.VPNProviders.formatNetworkName(new cr.onc.OncData(data));
123     return data.Name;
124   }
126   /**
127    * Create an element in the network list for controlling network
128    * connectivity.
129    * @param {Object} data Description of the network list or command.
130    * @constructor
131    * @extends {cr.ui.ListItem}
132    */
133   function NetworkListItem(data) {
134     var el = cr.doc.createElement('li');
135     el.data_ = {};
136     for (var key in data)
137       el.data_[key] = data[key];
138     NetworkListItem.decorate(el);
139     return el;
140   }
142   /**
143    * @param {string} action An action to send to coreOptionsUserMetricsAction.
144    */
145   function sendChromeMetricsAction(action) {
146     chrome.send('coreOptionsUserMetricsAction', [action]);
147   }
149   /**
150    * @param {string} guid The network GUID.
151    */
152   function showDetails(guid) {
153     chrome.networkingPrivate.getManagedProperties(
154       guid, DetailsInternetPage.initializeDetailsPage);
155   }
157   /**
158    * Decorate an element as a NetworkListItem.
159    * @param {!Element} el The element to decorate.
160    */
161   NetworkListItem.decorate = function(el) {
162     el.__proto__ = NetworkListItem.prototype;
163     el.decorate();
164   };
166   NetworkListItem.prototype = {
167     __proto__: ListItem.prototype,
169     /**
170      * Description of the network group or control.
171      * @type {Object<Object>}
172      * @private
173      */
174     data_: null,
176     /**
177      * Element for the control's subtitle.
178      * @type {?Element}
179      * @private
180      */
181     subtitle_: null,
183     /**
184      * Div containing the list item icon.
185      * @type {?Element}
186      * @private
187      */
188     iconDiv_: null,
190     /**
191      * Description of the network control.
192      * @type {Object}
193      */
194     get data() {
195       return this.data_;
196     },
198     /**
199      * Text label for the subtitle.
200      * @type {string}
201      */
202     set subtitle(text) {
203       if (text)
204         this.subtitle_.textContent = text;
205       this.subtitle_.hidden = !text;
206     },
208     /**
209      * Sets the icon based on a network state object.
210      * @param {!NetworkProperties} data Network state properties.
211      */
212     set iconData(data) {
213       if (!isNetworkType(data.Type))
214         return;
215       var networkIcon = this.getNetworkIcon();
216       networkIcon.networkState =
217           /** @type {chrome.networkingPrivate.NetworkStateProperties} */ (data);
218     },
220     /**
221      * Sets the icon based on a network type or a special type indecator, e.g.
222      * 'add-connection'
223      * @type {string}
224      */
225     set iconType(type) {
226       if (isNetworkType(type)) {
227         var networkIcon = this.getNetworkIcon();
228         networkIcon.networkType = type;
229       } else {
230         // Special cases. e.g. 'add-connection'. Background images are
231         // defined in browser_options.css.
232         var oldIcon = /** @type {CrNetworkIconElement} */ (
233             this.iconDiv_.querySelector('cr-network-icon'));
234         if (oldIcon)
235           this.iconDiv_.removeChild(oldIcon);
236         this.iconDiv_.classList.add('network-' + type.toLowerCase());
237       }
238     },
240     /**
241      * Returns any existing network icon for the list item or creates a new one.
242      * @return {!CrNetworkIconElement} The network icon for the list item.
243      */
244     getNetworkIcon: function() {
245       var networkIcon = /** @type {CrNetworkIconElement} */ (
246           this.iconDiv_.querySelector('cr-network-icon'));
247       if (!networkIcon) {
248         networkIcon = /** @type {!CrNetworkIconElement} */ (
249             document.createElement('cr-network-icon'));
250         networkIcon.isListItem = false;
251         this.iconDiv_.appendChild(networkIcon);
252       }
253       return networkIcon;
254     },
256     /**
257      * Set the direction of the text.
258      * @param {string} direction The direction of the text, e.g. 'ltr'.
259      */
260     setSubtitleDirection: function(direction) {
261       this.subtitle_.dir = direction;
262     },
264     /**
265      * Indicate that the selector arrow should be shown.
266      */
267     showSelector: function() {
268       this.subtitle_.classList.add('network-selector');
269     },
271     /**
272      * Adds an indicator to show that the network is policy managed.
273      */
274     showManagedNetworkIndicator: function() {
275       this.appendChild(new ManagedNetworkIndicator());
276     },
278     /** @override */
279     decorate: function() {
280       ListItem.prototype.decorate.call(this);
281       this.className = 'network-group';
282       this.iconDiv_ = this.ownerDocument.createElement('div');
283       this.iconDiv_.className = 'network-icon';
284       this.appendChild(this.iconDiv_);
285       var textContent = this.ownerDocument.createElement('div');
286       textContent.className = 'network-group-labels';
287       this.appendChild(textContent);
288       var categoryLabel = this.ownerDocument.createElement('div');
289       var title;
290       if (this.data_.key == 'addConnection')
291         title = 'addConnectionTitle';
292       else
293         title = this.data_.key.toLowerCase() + 'Title';
294       categoryLabel.className = 'network-title';
295       categoryLabel.textContent = loadTimeData.getString(title);
296       textContent.appendChild(categoryLabel);
297       this.subtitle_ = this.ownerDocument.createElement('div');
298       this.subtitle_.className = 'network-subtitle';
299       textContent.appendChild(this.subtitle_);
300     },
301   };
303   /**
304    * Creates a control that displays a popup menu when clicked.
305    * @param {Object} data  Description of the control.
306    * @constructor
307    * @extends {NetworkListItem}
308    */
309   function NetworkMenuItem(data) {
310     var el = new NetworkListItem(data);
311     el.__proto__ = NetworkMenuItem.prototype;
312     el.decorate();
313     return el;
314   }
316   NetworkMenuItem.prototype = {
317     __proto__: NetworkListItem.prototype,
319     /**
320      * Popup menu element.
321      * @type {?Element}
322      * @private
323      */
324     menu_: null,
326     /** @override */
327     decorate: function() {
328       this.subtitle = null;
329       if (this.data.iconType)
330         this.iconType = this.data.iconType;
331       this.addEventListener('click', (function() {
332         this.showMenu();
333       }).bind(this));
334     },
336     /**
337      * Retrieves the ID for the menu.
338      */
339     getMenuName: function() {
340       return this.data_.key.toLowerCase() + '-network-menu';
341     },
343     /**
344      * Creates a popup menu for the control.
345      * @return {Element} The newly created menu.
346      */
347     createMenu: function() {
348       if (this.data.menu) {
349         var menu = this.ownerDocument.createElement('div');
350         menu.id = this.getMenuName();
351         menu.className = 'network-menu';
352         menu.hidden = true;
353         Menu.decorate(menu);
354         menu.menuItemSelector = '.network-menu-item';
355         for (var i = 0; i < this.data.menu.length; i++) {
356           var entry = this.data.menu[i];
357           createCallback_(menu, null, entry.label, entry.command);
358         }
359         return menu;
360       }
361       return null;
362     },
364     /**
365      * Determines if a menu can be updated on the fly. Menus that cannot be
366      * updated are fully regenerated using createMenu. The advantage of
367      * updating a menu is that it can preserve ordering of networks avoiding
368      * entries from jumping around after an update.
369      * @return {boolean} Whether the menu can be updated on the fly.
370      */
371     canUpdateMenu: function() {
372       return false;
373     },
375     /**
376      * Removes the current menu contents, causing it to be regenerated when the
377      * menu is shown the next time. If the menu is showing right now, its
378      * contents are regenerated immediately and the menu remains visible.
379      */
380     refreshMenu: function() {
381       this.menu_ = null;
382       if (activeMenu_ == this.getMenuName())
383         this.showMenu();
384     },
386     /**
387      * Displays a popup menu.
388      */
389     showMenu: function() {
390       var rebuild = false;
391       // Force a rescan if opening the menu for WiFi networks to ensure the
392       // list is up to date. Networks are periodically rescanned, but depending
393       // on timing, there could be an excessive delay before the first rescan
394       // unless forced.
395       var rescan = !activeMenu_ && this.data_.key == 'WiFi';
396       if (!this.menu_) {
397         rebuild = true;
398         var existing = $(this.getMenuName());
399         if (existing) {
400           if (this.canUpdateMenu() && this.updateMenu())
401             return;
402           closeMenu_();
403         }
404         this.menu_ = this.createMenu();
405         this.menu_.addEventListener('mousedown', function(e) {
406           // Prevent blurring of list, which would close the menu.
407           e.preventDefault();
408         });
409         var parent = $('network-menus');
410         if (existing)
411           parent.replaceChild(this.menu_, existing);
412         else
413           parent.appendChild(this.menu_);
414       }
415       var top = this.offsetTop + this.clientHeight;
416       var menuId = this.getMenuName();
417       if (menuId != activeMenu_ || rebuild) {
418         closeMenu_();
419         activeMenu_ = menuId;
420         this.menu_.style.setProperty('top', top + 'px');
421         this.menu_.hidden = false;
422       }
423       if (rescan) {
424         chrome.networkingPrivate.requestNetworkScan();
425       }
426     }
427   };
429   /**
430    * Creates a control for selecting or configuring a network connection based
431    * on the type of connection (e.g. wifi versus vpn).
432    * @param {{key: string, networkList: Array<!NetworkProperties>}} data
433    *     An object containing the network type (key) and an array of networks.
434    * @constructor
435    * @extends {NetworkMenuItem}
436    */
437   function NetworkSelectorItem(data) {
438     var el = new NetworkMenuItem(data);
439     el.__proto__ = NetworkSelectorItem.prototype;
440     el.decorate();
441     return el;
442   }
444   /**
445    * Returns true if |source| is a policy managed source.
446    * @param {string} source The ONC source of a network.
447    * @return {boolean} Whether |source| is a managed source.
448    */
449   function isManaged(source) {
450     return (source == 'DevicePolicy' || source == 'UserPolicy');
451   }
453   /**
454    * Returns true if |network| is visible.
455    * @param {!chrome.networkingPrivate.NetworkStateProperties} network The
456    *     network state properties.
457    * @return {boolean} Whether |network| is visible.
458    */
459   function networkIsVisible(network) {
460     if (network.Type == 'WiFi')
461       return !!(network.WiFi && (network.WiFi.SignalStrength > 0));
462     if (network.Type == 'WiMAX')
463       return !!(network.WiMAX && (network.WiMAX.SignalStrength > 0));
464     // Other network types are always considered 'visible'.
465     return true;
466   }
468   /**
469    * Returns true if |cellular| is a GSM network with no sim present.
470    * @param {?chrome.networkingPrivate.DeviceStateProperties} cellularDevice
471    * @return {boolean} Whether |network| is missing a SIM card.
472    */
473   function isCellularSimAbsent(cellularDevice) {
474     return !!cellularDevice && cellularDevice.SimPresent === false;
475   }
477   /**
478    * Returns true if |cellular| has a locked SIM card.
479    * @param {?chrome.networkingPrivate.DeviceStateProperties} cellularDevice
480    * @return {boolean} Whether |network| has a locked SIM card.
481    */
482   function isCellularSimLocked(cellularDevice) {
483     return !!cellularDevice && !!cellularDevice.SimLockType;
484   }
486   NetworkSelectorItem.prototype = {
487     __proto__: NetworkMenuItem.prototype,
489     /** @override */
490     decorate: function() {
491       // TODO(kevers): Generalize method of setting default label.
492       this.subtitle = loadTimeData.getString('OncConnectionStateNotConnected');
493       var list = this.data_.networkList;
494       var candidateData = null;
495       for (var i = 0; i < list.length; i++) {
496         var networkDetails = list[i];
497         if (networkDetails.ConnectionState == 'Connecting' ||
498             networkDetails.ConnectionState == 'Connected') {
499           this.subtitle = getNetworkName(networkDetails);
500           this.setSubtitleDirection('ltr');
501           candidateData = networkDetails;
502           // Only break when we see a connecting network as it is possible to
503           // have a connected network and a connecting network at the same
504           // time.
505           if (networkDetails.ConnectionState == 'Connecting')
506             break;
507         }
508       }
509       if (candidateData)
510         this.iconData = candidateData;
511       else
512         this.iconType = this.data.key;
514       this.showSelector();
516       if (candidateData && isManaged(candidateData.Source))
517         this.showManagedNetworkIndicator();
519       if (activeMenu_ == this.getMenuName()) {
520         // Menu is already showing and needs to be updated. Explicitly calling
521         // show menu will force the existing menu to be replaced.  The call
522         // is deferred in order to ensure that position of this element has
523         // beem properly updated.
524         var self = this;
525         setTimeout(function() {self.showMenu();}, 0);
526       }
527     },
529     /**
530      * Creates a menu for selecting, configuring or disconnecting from a
531      * network.
532      * @return {!Element} The newly created menu.
533      */
534     createMenu: function() {
535       var menu = this.ownerDocument.createElement('div');
536       menu.id = this.getMenuName();
537       menu.className = 'network-menu';
538       menu.hidden = true;
539       Menu.decorate(menu);
540       menu.menuItemSelector = '.network-menu-item';
541       var addendum = [];
542       if (this.data_.key == 'WiFi') {
543         addendum.push({
544           label: loadTimeData.getString('joinOtherNetwork'),
545           command: createAddNonVPNConnectionCallback_('WiFi'),
546           data: {}
547         });
548       } else if (this.data_.key == 'Cellular') {
549         if (cellularDevice_.State == 'Enabled' &&
550             cellularNetwork_ && cellularNetwork_.Cellular &&
551             cellularNetwork_.Cellular.SupportNetworkScan) {
552           addendum.push({
553             label: loadTimeData.getString('otherCellularNetworks'),
554             command: createAddNonVPNConnectionCallback_('Cellular'),
555             addClass: ['other-cellulars'],
556             data: {}
557           });
558         }
560         var label = enableDataRoaming_ ? 'disableDataRoaming' :
561             'enableDataRoaming';
562         var disabled = !loadTimeData.getValue('loggedInAsOwner');
563         var entry = {label: loadTimeData.getString(label),
564                      data: {}};
565         if (disabled) {
566           entry.command = null;
567           entry.tooltip =
568               loadTimeData.getString('dataRoamingDisableToggleTooltip');
569         } else {
570           var self = this;
571           entry.command = function() {
572             options.Preferences.setBooleanPref(
573                 'cros.signed.data_roaming_enabled',
574                 !enableDataRoaming_, true);
575             // Force revalidation of the menu the next time it is displayed.
576             self.menu_ = null;
577           };
578         }
579         addendum.push(entry);
580       } else if (this.data_.key == 'VPN') {
581         addendum = addendum.concat(createAddVPNConnectionEntries_());
582       }
584       var list = this.data.rememberedNetworks;
585       if (list && list.length > 0) {
586         var callback = function(list) {
587           $('remembered-network-list').clear();
588           var dialog = options.PreferredNetworks.getInstance();
589           PageManager.showPageByName('preferredNetworksPage', false);
590           dialog.update(list);
591           sendChromeMetricsAction('Options_NetworkShowPreferred');
592         };
593         addendum.push({label: loadTimeData.getString('preferredNetworks'),
594                        command: callback,
595                        data: list});
596       }
598       var networkGroup = this.ownerDocument.createElement('div');
599       networkGroup.className = 'network-menu-group';
600       list = this.data.networkList;
601       var empty = !list || list.length == 0;
602       if (list) {
603         var connectedVpnGuid = '';
604         for (var i = 0; i < list.length; i++) {
605           var data = list[i];
606           this.createNetworkOptionsCallback_(networkGroup, data);
607           // For VPN only, append a 'Disconnect' item to the dropdown menu.
608           if (!connectedVpnGuid && data.Type == 'VPN' &&
609               (data.ConnectionState == 'Connected' ||
610                data.ConnectionState == 'Connecting')) {
611             connectedVpnGuid = data.GUID;
612           }
613         }
614         if (connectedVpnGuid) {
615           var disconnectCallback = function() {
616             sendChromeMetricsAction('Options_NetworkDisconnectVPN');
617             chrome.networkingPrivate.startDisconnect(connectedVpnGuid);
618           };
619           // Add separator
620           addendum.push({});
621           addendum.push({label: loadTimeData.getString('disconnectNetwork'),
622                          command: disconnectCallback,
623                          data: data});
624         }
625       }
626       if (this.data_.key == 'WiFi' || this.data_.key == 'WiMAX' ||
627           this.data_.key == 'Cellular') {
628         addendum.push({});
629         if (this.data_.key == 'WiFi') {
630           addendum.push({
631             label: loadTimeData.getString('turnOffWifi'),
632             command: function() {
633               sendChromeMetricsAction('Options_NetworkWifiToggle');
634               chrome.networkingPrivate.disableNetworkType(
635                   chrome.networkingPrivate.NetworkType.WI_FI);
636             },
637             data: {}});
638         } else if (this.data_.key == 'WiMAX') {
639           addendum.push({
640             label: loadTimeData.getString('turnOffWimax'),
641             command: function() {
642               chrome.networkingPrivate.disableNetworkType(
643                   chrome.networkingPrivate.NetworkType.WI_MAX);
644             },
645             data: {}});
646         } else if (this.data_.key == 'Cellular') {
647           addendum.push({
648             label: loadTimeData.getString('turnOffCellular'),
649             command: function() {
650               chrome.networkingPrivate.disableNetworkType(
651                   chrome.networkingPrivate.NetworkType.CELLULAR);
652             },
653             data: {}});
654         }
655       }
656       if (!empty)
657         menu.appendChild(networkGroup);
658       if (addendum.length > 0) {
659         var separator = false;
660         if (!empty) {
661           menu.appendChild(MenuItem.createSeparator());
662           separator = true;
663         }
664         for (var i = 0; i < addendum.length; i++) {
665           var value = addendum[i];
666           if (value.data) {
667             var item = createCallback_(menu, value.data, value.label,
668                                        value.command);
669             if (value.tooltip)
670               item.title = value.tooltip;
671             if (value.addClass)
672               item.classList.add(value.addClass);
673             separator = false;
674           } else if (!separator) {
675             menu.appendChild(MenuItem.createSeparator());
676             separator = true;
677           }
678         }
679       }
680       return menu;
681     },
683     /** @override */
684     canUpdateMenu: function() {
685       return this.data_.key == 'WiFi' && activeMenu_ == this.getMenuName();
686     },
688     /**
689      * Updates an existing menu.  Updated menus preserve ordering of prior
690      * entries.  During the update process, the ordering may differ from the
691      * preferred ordering as determined by the network library.  If the
692      * ordering becomes potentially out of sync, then the updated menu is
693      * marked for disposal on close.  Reopening the menu will force a
694      * regeneration, which will in turn fix the ordering. This method must only
695      * be called if canUpdateMenu() returned |true|.
696      * @return {boolean} True if successfully updated.
697      */
698     updateMenu: function() {
699       var oldMenu = $(this.getMenuName());
700       var group = oldMenu.getElementsByClassName('network-menu-group')[0];
701       if (!group)
702         return false;
703       var newMenu = this.createMenu();
704       var discardOnClose = false;
705       var oldNetworkButtons = this.extractNetworkConnectButtons_(oldMenu);
706       var newNetworkButtons = this.extractNetworkConnectButtons_(newMenu);
707       for (var key in oldNetworkButtons) {
708         if (newNetworkButtons[key]) {
709           group.replaceChild(newNetworkButtons[key].button,
710                              oldNetworkButtons[key].button);
711           if (newNetworkButtons[key].index != oldNetworkButtons[key].index)
712             discardOnClose = true;
713           newNetworkButtons[key] = null;
714         } else {
715           // Leave item in list to prevent network items from jumping due to
716           // deletions.
717           oldNetworkButtons[key].disabled = true;
718           discardOnClose = true;
719         }
720       }
721       for (var key in newNetworkButtons) {
722         var entry = newNetworkButtons[key];
723         if (entry) {
724           group.appendChild(entry.button);
725           discardOnClose = true;
726         }
727       }
728       oldMenu.data = {discardOnClose: discardOnClose};
729       return true;
730     },
732     /**
733      * Extracts a mapping of network names to menu element and position.
734      * @param {!Element} menu The menu to process.
735      * @return {Object<?{index: number, button: Element}>}
736      *     Network mapping.
737      * @private
738      */
739     extractNetworkConnectButtons_: function(menu) {
740       var group = menu.getElementsByClassName('network-menu-group')[0];
741       var networkButtons = {};
742       if (!group)
743         return networkButtons;
744       var buttons = group.getElementsByClassName('network-menu-item');
745       for (var i = 0; i < buttons.length; i++) {
746         var label = buttons[i].data.label;
747         networkButtons[label] = {index: i, button: buttons[i]};
748       }
749       return networkButtons;
750     },
752     /**
753      * Adds a menu item for showing network details.
754      * @param {!Element} parent The parent element.
755      * @param {NetworkProperties} data Description of the network.
756      * @private
757      */
758     createNetworkOptionsCallback_: function(parent, data) {
759       var menuItem = createCallback_(parent,
760                                      data,
761                                      getNetworkName(data),
762                                      showDetails.bind(null, data.GUID));
763       if (isManaged(data.Source))
764         menuItem.appendChild(new ManagedNetworkIndicator());
765       if (data.ConnectionState == 'Connected' ||
766           data.ConnectionState == 'Connecting') {
767         var label = menuItem.getElementsByClassName(
768             'network-menu-item-label')[0];
769         label.classList.add('active-network');
770       }
771     }
772   };
774   /**
775    * Creates a button-like control for configurating internet connectivity.
776    * @param {{key: string, subtitle: string, command: Function}} data
777    *     Description of the network control.
778    * @constructor
779    * @extends {NetworkListItem}
780    */
781   function NetworkButtonItem(data) {
782     var el = new NetworkListItem(data);
783     el.__proto__ = NetworkButtonItem.prototype;
784     el.decorate();
785     return el;
786   }
788   NetworkButtonItem.prototype = {
789     __proto__: NetworkListItem.prototype,
791     /** @override */
792     decorate: function() {
793       if (this.data.subtitle)
794         this.subtitle = this.data.subtitle;
795       else
796        this.subtitle = null;
797       if (this.data.command)
798         this.addEventListener('click', this.data.command);
799       if (this.data.iconData)
800         this.iconData = this.data.iconData;
801       else if (this.data.iconType)
802         this.iconType = this.data.iconType;
803       if (isManaged(this.data.Source))
804         this.showManagedNetworkIndicator();
805     },
806   };
808   /**
809    * Adds a command to a menu for modifying network settings.
810    * @param {!Element} menu Parent menu.
811    * @param {?NetworkProperties} data Description of the network.
812    * @param {!string} label Display name for the menu item.
813    * @param {!Function} command Callback function.
814    * @return {!Element} The created menu item.
815    * @private
816    */
817   function createCallback_(menu, data, label, command) {
818     var button = menu.ownerDocument.createElement('div');
819     button.className = 'network-menu-item';
821     var buttonIconDiv = menu.ownerDocument.createElement('div');
822     buttonIconDiv.className = 'network-icon';
823     button.appendChild(buttonIconDiv);
824     if (data && isNetworkType(data.Type)) {
825       var networkIcon = /** @type {!CrNetworkIconElement} */ (
826           document.createElement('cr-network-icon'));
827       buttonIconDiv.appendChild(networkIcon);
828       networkIcon.isListItem = true;
829       networkIcon.networkState =
830           /** @type {chrome.networkingPrivate.NetworkStateProperties} */ (data);
831     }
833     var buttonLabel = menu.ownerDocument.createElement('span');
834     buttonLabel.className = 'network-menu-item-label';
835     buttonLabel.textContent = label;
836     button.appendChild(buttonLabel);
837     var callback = null;
838     if (command != null) {
839       if (data) {
840         callback = function() {
841           (/** @type {Function} */(command))(data);
842           closeMenu_();
843         };
844       } else {
845         callback = function() {
846           (/** @type {Function} */(command))();
847           closeMenu_();
848         };
849       }
850     }
851     if (callback != null)
852       button.addEventListener('activate', callback);
853     else
854       buttonLabel.classList.add('network-disabled-control');
856     button.data = {label: label};
857     MenuItem.decorate(button);
858     menu.appendChild(button);
859     return button;
860   }
862   /**
863    * A list of controls for manipulating network connectivity.
864    * @constructor
865    * @extends {cr.ui.List}
866    */
867   var NetworkList = cr.ui.define('list');
869   NetworkList.prototype = {
870     __proto__: List.prototype,
872     /** @override */
873     decorate: function() {
874       List.prototype.decorate.call(this);
875       this.startBatchUpdates();
876       this.autoExpands = true;
877       this.dataModel = new ArrayDataModel([]);
878       this.selectionModel = new ListSingleSelectionModel();
879       this.addEventListener('blur', this.onBlur_.bind(this));
880       this.selectionModel.addEventListener('change',
881                                            this.onSelectionChange_.bind(this));
883       // Wi-Fi control is always visible.
884       this.update({key: 'WiFi', networkList: []});
886       this.updateAddConnectionMenuEntries_();
888       var prefs = options.Preferences.getInstance();
889       prefs.addEventListener('cros.signed.data_roaming_enabled',
890           function(event) {
891             enableDataRoaming_ = event.value.value;
892           });
893       this.endBatchUpdates();
895       this.onNetworkListChanged_();  // Trigger an initial network update
897       chrome.networkingPrivate.onNetworkListChanged.addListener(
898           this.onNetworkListChanged_.bind(this));
899       chrome.networkingPrivate.onDeviceStateListChanged.addListener(
900           this.onNetworkListChanged_.bind(this));
902       chrome.networkingPrivate.requestNetworkScan();
904       options.VPNProviders.addObserver(this.onVPNProvidersChanged_.bind(this));
905     },
907     /**
908      * networkingPrivate event called when the network list has changed.
909      */
910     onNetworkListChanged_: function() {
911       var networkList = this;
912       chrome.networkingPrivate.getDeviceStates(function(deviceStates) {
913         var filter = {
914           networkType: chrome.networkingPrivate.NetworkType.ALL
915         };
916         chrome.networkingPrivate.getNetworks(filter, function(networkStates) {
917           networkList.updateNetworkStates(deviceStates, networkStates);
918         });
919       });
920     },
922     /**
923      * Called when the list of VPN providers changes. Refreshes the contents of
924      * menus that list VPN providers.
925      * @private
926      */
927     onVPNProvidersChanged_: function() {
928       // Refresh the contents of the VPN menu.
929       var index = this.indexOf('VPN');
930       if (index != undefined)
931         this.getListItemByIndex(index).refreshMenu();
933       // Refresh the contents of the "add connection" menu.
934       this.updateAddConnectionMenuEntries_();
935       index = this.indexOf('addConnection');
936       if (index != undefined)
937         this.getListItemByIndex(index).refreshMenu();
938     },
940     /**
941      * Updates the entries in the "add connection" menu, based on the VPN
942      * providers currently enabled in the primary user's profile.
943      * @private
944      */
945     updateAddConnectionMenuEntries_: function() {
946       var entries = [{
947         label: loadTimeData.getString('addConnectionWifi'),
948         command: createAddNonVPNConnectionCallback_('WiFi')
949       }];
950       entries = entries.concat(createAddVPNConnectionEntries_());
951       this.update({key: 'addConnection',
952                    iconType: 'add-connection',
953                    menu: entries
954                   });
955     },
957     /**
958      * When the list loses focus, unselect all items in the list and close the
959      * active menu.
960      * @private
961      */
962     onBlur_: function() {
963       this.selectionModel.unselectAll();
964       closeMenu_();
965     },
967     /** @override */
968     handleKeyDown: function(e) {
969       if (activeMenu_) {
970         // keyIdentifier does not report 'Esc' correctly
971         if (e.keyCode == 27 /* Esc */) {
972           closeMenu_();
973           return;
974         }
976         if ($(activeMenu_).handleKeyDown(e)) {
977           e.preventDefault();
978           e.stopPropagation();
979         }
980         return;
981       }
983       if (e.keyIdentifier == 'Enter' ||
984           e.keyIdentifier == 'U+0020' /* Space */) {
985         var selectedListItem = this.getListItemByIndex(
986             this.selectionModel.selectedIndex);
987         if (selectedListItem) {
988           selectedListItem.click();
989           return;
990         }
991       }
993       List.prototype.handleKeyDown.call(this, e);
994     },
996     /**
997      * Close bubble and menu when a different list item is selected.
998      * @param {Event} event Event detailing the selection change.
999      * @private
1000      */
1001     onSelectionChange_: function(event) {
1002       PageManager.hideBubble();
1003       // A list item may temporarily become unselected while it is constructing
1004       // its menu. The menu should therefore only be closed if a different item
1005       // is selected, not when the menu's owner item is deselected.
1006       if (activeMenu_) {
1007         for (var i = 0; i < event.changes.length; ++i) {
1008           if (event.changes[i].selected) {
1009             var item = this.dataModel.item(event.changes[i].index);
1010             if (!item.getMenuName || item.getMenuName() != activeMenu_) {
1011               closeMenu_();
1012               return;
1013             }
1014           }
1015         }
1016       }
1017     },
1019     /**
1020      * Finds the index of a network item within the data model based on
1021      * category.
1022      * @param {string} key Unique key for the item in the list.
1023      * @return {(number|undefined)} The index of the network item, or
1024      *     |undefined| if it is not found.
1025      */
1026     indexOf: function(key) {
1027       var size = this.dataModel.length;
1028       for (var i = 0; i < size; i++) {
1029         var entry = this.dataModel.item(i);
1030         if (entry.key == key)
1031           return i;
1032       }
1033       return undefined;
1034     },
1036     /**
1037      * Updates a network control.
1038      * @param {Object} data Description of the entry.
1039      */
1040     update: function(data) {
1041       this.startBatchUpdates();
1042       var index = this.indexOf(data.key);
1043       if (index == undefined) {
1044         // Find reference position for adding the element.  We cannot hide
1045         // individual list elements, thus we need to conditionally add or
1046         // remove elements and cannot rely on any element having a fixed index.
1047         for (var i = 0; i < Constants.NETWORK_ORDER.length; i++) {
1048           if (data.key == Constants.NETWORK_ORDER[i]) {
1049             data.sortIndex = i;
1050             break;
1051           }
1052         }
1053         var referenceIndex = -1;
1054         for (var i = 0; i < this.dataModel.length; i++) {
1055           var entry = this.dataModel.item(i);
1056           if (entry.sortIndex < data.sortIndex)
1057             referenceIndex = i;
1058           else
1059             break;
1060         }
1061         if (referenceIndex == -1) {
1062           // Prepend to the start of the list.
1063           this.dataModel.splice(0, 0, data);
1064         } else if (referenceIndex == this.dataModel.length) {
1065           // Append to the end of the list.
1066           this.dataModel.push(data);
1067         } else {
1068           // Insert after the reference element.
1069           this.dataModel.splice(referenceIndex + 1, 0, data);
1070         }
1071       } else {
1072         var entry = this.dataModel.item(index);
1073         data.sortIndex = entry.sortIndex;
1074         this.dataModel.splice(index, 1, data);
1075       }
1076       this.endBatchUpdates();
1077     },
1079     /**
1080      * @override
1081      * @param {Object} entry
1082      */
1083     createItem: function(entry) {
1084       if (entry.networkList)
1085         return new NetworkSelectorItem(
1086             /** @type {{key: string, networkList: Array<!NetworkProperties>}} */
1087             (entry));
1088       if (entry.command)
1089         return new NetworkButtonItem(
1090             /** @type {{key: string, subtitle: string, command: Function}} */(
1091                 entry));
1092       if (entry.menu)
1093         return new NetworkMenuItem(entry);
1094       assertNotReached();
1095     },
1097     /**
1098      * Deletes an element from the list.
1099      * @param {string} key  Unique identifier for the element.
1100      */
1101     deleteItem: function(key) {
1102       var index = this.indexOf(key);
1103       if (index != undefined)
1104         this.dataModel.splice(index, 1);
1105     },
1107     /**
1108      * Updates the state of network devices and services.
1109      * @param {!Array<!chrome.networkingPrivate.DeviceStateProperties>}
1110      *     deviceStates The result from networkingPrivate.getDeviceStates.
1111      * @param {!Array<!chrome.networkingPrivate.NetworkStateProperties>}
1112      *     networkStates The result from networkingPrivate.getNetworks.
1113      */
1114     updateNetworkStates: function(deviceStates, networkStates) {
1115       // Update device states.
1116       cellularDevice_ = null;
1117       wifiDeviceState_ = undefined;
1118       wimaxDeviceState_ = undefined;
1119       for (var i = 0; i < deviceStates.length; ++i) {
1120         var device = deviceStates[i];
1121         var type = device.Type;
1122         var state = device.State;
1123         if (type == 'Cellular')
1124           cellularDevice_ = cellularDevice_ || device;
1125         else if (type == 'WiFi')
1126           wifiDeviceState_ = wifiDeviceState_ || state;
1127         else if (type == 'WiMAX')
1128           wimaxDeviceState_ = wimaxDeviceState_ || state;
1129       }
1131       // Update active network states.
1132       cellularNetwork_ = null;
1133       ethernetNetwork_ = null;
1134       for (var i = 0; i < networkStates.length; i++) {
1135         // Note: This cast is valid since
1136         // networkingPrivate.NetworkStateProperties is a subset of
1137         // NetworkProperties and all missing properties are optional.
1138         var entry = /** @type {NetworkProperties} */ (networkStates[i]);
1139         switch (entry.Type) {
1140           case 'Cellular':
1141             cellularNetwork_ = cellularNetwork_ || entry;
1142             break;
1143           case 'Ethernet':
1144             // Ignore any EAP Parameters networks (which lack ConnectionState).
1145             if (entry.ConnectionState)
1146               ethernetNetwork_ = ethernetNetwork_ || entry;
1147             break;
1148         }
1149         if (cellularNetwork_ && ethernetNetwork_)
1150           break;
1151       }
1153       if (cellularNetwork_ && cellularNetwork_.GUID) {
1154         // Get the complete set of cellular properties which includes SIM and
1155         // Scan properties.
1156         var networkList = this;
1157         chrome.networkingPrivate.getProperties(
1158             cellularNetwork_.GUID, function(cellular) {
1159               cellularNetwork_ = /** @type {NetworkProperties} */ (cellular);
1160               networkList.updateControls(networkStates);
1161             });
1162       } else {
1163         this.updateControls(networkStates);
1164       }
1165     },
1167     /**
1168      * Updates network controls.
1169      * @param {!Array<!chrome.networkingPrivate.NetworkStateProperties>}
1170      *     networkStates The result from networkingPrivate.getNetworks.
1171      */
1172     updateControls: function(networkStates) {
1173       this.startBatchUpdates();
1175       // Only show Ethernet control if available.
1176       if (ethernetNetwork_) {
1177         var ethernetOptions = showDetails.bind(null, ethernetNetwork_.GUID);
1178         var state = ethernetNetwork_.ConnectionState;
1179         var subtitle;
1180         if (state == 'Connected')
1181           subtitle = loadTimeData.getString('OncConnectionStateConnected');
1182         else if (state == 'Connecting')
1183           subtitle = loadTimeData.getString('OncConnectionStateConnecting');
1184         else
1185           subtitle = loadTimeData.getString('OncConnectionStateNotConnected');
1186         this.update(
1187           { key: 'Ethernet',
1188             subtitle: subtitle,
1189             iconData: ethernetNetwork_,
1190             command: ethernetOptions,
1191             Source: ethernetNetwork_.Source }
1192         );
1193       } else {
1194         this.deleteItem('Ethernet');
1195       }
1197       if (wifiDeviceState_ == 'Enabled')
1198         loadData_('WiFi', networkStates);
1199       else
1200         addEnableNetworkButton_(chrome.networkingPrivate.NetworkType.WI_FI);
1202       // Only show cellular control if available.
1203       if (cellularDevice_) {
1204         if (cellularDevice_.State == 'Enabled' &&
1205             !isCellularSimAbsent(cellularDevice_) &&
1206             !isCellularSimLocked(cellularDevice_)) {
1207           loadData_('Cellular', networkStates);
1208         } else {
1209           addEnableNetworkButton_(
1210               chrome.networkingPrivate.NetworkType.CELLULAR);
1211         }
1212       } else {
1213         this.deleteItem('Cellular');
1214       }
1216       // Only show wimax control if available. Uses cellular icons.
1217       if (wimaxDeviceState_) {
1218         if (wimaxDeviceState_ == 'Enabled')
1219           loadData_('WiMAX', networkStates);
1220         else
1221           addEnableNetworkButton_(chrome.networkingPrivate.NetworkType.WI_MAX);
1222       } else {
1223         this.deleteItem('WiMAX');
1224       }
1226       // Only show VPN control if there is at least one VPN configured.
1227       if (loadData_('VPN', networkStates) == 0)
1228         this.deleteItem('VPN');
1230       this.endBatchUpdates();
1231     }
1232   };
1234   /**
1235    * Replaces a network menu with a button for enabling the network type.
1236    * @param {chrome.networkingPrivate.NetworkType} type
1237    * @private
1238    */
1239   function addEnableNetworkButton_(type) {
1240     var subtitle = loadTimeData.getString('networkDisabled');
1241     var enableNetwork = function() {
1242       if (type == chrome.networkingPrivate.NetworkType.WI_FI)
1243         sendChromeMetricsAction('Options_NetworkWifiToggle');
1244       if (type == chrome.networkingPrivate.NetworkType.CELLULAR) {
1245         if (isCellularSimLocked(cellularDevice_)) {
1246           chrome.send('simOperation', ['unlock']);
1247           return;
1248         } else if (isCellularSimAbsent(cellularDevice_)) {
1249           chrome.send('simOperation', ['configure']);
1250           return;
1251         }
1252       }
1253       chrome.networkingPrivate.enableNetworkType(type);
1254     };
1255     $('network-list').update({key: type,
1256                               subtitle: subtitle,
1257                               iconType: type,
1258                               command: enableNetwork});
1259   }
1261   /**
1262    * Element for indicating a policy managed network.
1263    * @constructor
1264    * @extends {options.ControlledSettingIndicator}
1265    */
1266   function ManagedNetworkIndicator() {
1267     var el = cr.doc.createElement('span');
1268     el.__proto__ = ManagedNetworkIndicator.prototype;
1269     el.decorate();
1270     return el;
1271   }
1273   ManagedNetworkIndicator.prototype = {
1274     __proto__: ControlledSettingIndicator.prototype,
1276     /** @override */
1277     decorate: function() {
1278       ControlledSettingIndicator.prototype.decorate.call(this);
1279       this.controlledBy = 'policy';
1280       var policyLabel = loadTimeData.getString('managedNetwork');
1281       this.setAttribute('textPolicy', policyLabel);
1282       this.removeAttribute('tabindex');
1283     },
1285     /** @override */
1286     handleEvent: function(event) {
1287       // Prevent focus blurring as that would close any currently open menu.
1288       if (event.type == 'mousedown')
1289         return;
1290       ControlledSettingIndicator.prototype.handleEvent.call(this, event);
1291     },
1293     /**
1294      * Handle mouse events received by the bubble, preventing focus blurring as
1295      * that would close any currently open menu and preventing propagation to
1296      * any elements located behind the bubble.
1297      * @param {Event} event Mouse event.
1298      */
1299     stopEvent: function(event) {
1300       event.preventDefault();
1301       event.stopPropagation();
1302     },
1304     /** @override */
1305     toggleBubble: function() {
1306       if (activeMenu_ && !$(activeMenu_).contains(this))
1307         closeMenu_();
1308       ControlledSettingIndicator.prototype.toggleBubble.call(this);
1309       if (this.showingBubble) {
1310         var bubble = PageManager.getVisibleBubble();
1311         bubble.addEventListener('mousedown', this.stopEvent);
1312         bubble.addEventListener('click', this.stopEvent);
1313       }
1314     }
1315   };
1317   /**
1318    * Updates the list of available networks and their status, filtered by
1319    * network type.
1320    * @param {string} type The type of network.
1321    * @param {Array<!chrome.networkingPrivate.NetworkStateProperties>} networks
1322    *     The list of network objects.
1323    * @return {number} The number of visible networks matching |type|.
1324    */
1325   function loadData_(type, networks) {
1326     var res = 0;
1327     var availableNetworks = [];
1328     var rememberedNetworks = [];
1329     for (var i = 0; i < networks.length; i++) {
1330       var network = networks[i];
1331       if (network.Type != type)
1332         continue;
1333       if (networkIsVisible(network)) {
1334         availableNetworks.push(network);
1335         ++res;
1336       }
1337       if ((type == 'WiFi' || type == 'VPN') && network.Source &&
1338           network.Source != 'None') {
1339         rememberedNetworks.push(network);
1340       }
1341     }
1342     var data = {
1343       key: type,
1344       networkList: availableNetworks,
1345       rememberedNetworks: rememberedNetworks
1346     };
1347     $('network-list').update(data);
1348     return res;
1349   }
1351   /**
1352    * Hides the currently visible menu.
1353    * @private
1354    */
1355   function closeMenu_() {
1356     if (activeMenu_) {
1357       var menu = $(activeMenu_);
1358       menu.hidden = true;
1359       if (menu.data && menu.data.discardOnClose)
1360         menu.parentNode.removeChild(menu);
1361       activeMenu_ = null;
1362     }
1363   }
1365   /**
1366    * Creates a callback function that adds a new connection of the given type.
1367    * This method may be used for all network types except VPN.
1368    * @param {string} type An ONC network type
1369    * @return {function()} The created callback.
1370    * @private
1371    */
1372   function createAddNonVPNConnectionCallback_(type) {
1373     return function() {
1374       if (type == 'WiFi')
1375         sendChromeMetricsAction('Options_NetworkJoinOtherWifi');
1376       chrome.send('addNonVPNConnection', [type]);
1377     };
1378   }
1380   /**
1381    * Creates a callback function that shows the "add network" dialog for a VPN
1382    * provider. If |opt_extensionID| is omitted, the dialog for the built-in
1383    * OpenVPN/L2TP provider is shown. Otherwise, |opt_extensionID| identifies the
1384    * third-party provider for which the dialog should be shown.
1385    * @param {string=} opt_extensionID Extension ID identifying the third-party
1386    *     VPN provider for which the dialog should be shown.
1387    * @return {function()} The created callback.
1388    * @private
1389    */
1390   function createVPNConnectionCallback_(opt_extensionID) {
1391     return function() {
1392       sendChromeMetricsAction(opt_extensionID ?
1393           'Options_NetworkAddVPNThirdParty' :
1394           'Options_NetworkAddVPNBuiltIn');
1395       chrome.send('addVPNConnection',
1396                   opt_extensionID ? [opt_extensionID] : undefined);
1397     };
1398   }
1400   /**
1401    * Generates an "add network" entry for each VPN provider currently enabled in
1402    * the primary user's profile.
1403    * @return {!Array<{label: string, command: function(), data: !Object}>} The
1404    *     list of entries.
1405    * @private
1406    */
1407   function createAddVPNConnectionEntries_() {
1408     var entries = [];
1409     var providers = options.VPNProviders.getProviders();
1410     for (var i = 0; i < providers.length; ++i) {
1411       entries.push({
1412         label: loadTimeData.getStringF('addConnectionVPNTemplate',
1413                                        providers[i].name),
1414         command: createVPNConnectionCallback_(
1415             providers[i].extensionID || undefined),
1416         data: {}
1417       });
1418     }
1419     return entries;
1420   }
1422   /**
1423    * Whether the Network list is disabled. Only used for display purpose.
1424    */
1425   cr.defineProperty(NetworkList, 'disabled', cr.PropertyKind.BOOL_ATTR);
1427   // Export
1428   return {
1429     NetworkList: NetworkList
1430   };