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.
4
5/**
6 * @typedef {{
7 *   ConnectionState: string,
8 *   iconURL: string,
9 *   policyManaged: boolean,
10 *   servicePath: string
11 * }}
12 * @see chrome/browser/ui/webui/options/chromeos/internet_options_handler.cc
13 */
14var NetworkInfo;
15
16cr.define('options.network', function() {
17  var ArrayDataModel = cr.ui.ArrayDataModel;
18  var List = cr.ui.List;
19  var ListItem = cr.ui.ListItem;
20  var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
21  var Menu = cr.ui.Menu;
22  var MenuItem = cr.ui.MenuItem;
23  var ControlledSettingIndicator = options.ControlledSettingIndicator;
24
25  /**
26   * Network settings constants. These enums usually match their C++
27   * counterparts.
28   */
29  function Constants() {}
30
31  // Cellular activation states:
32  Constants.ACTIVATION_STATE_UNKNOWN = 0;
33  Constants.ACTIVATION_STATE_ACTIVATED = 1;
34  Constants.ACTIVATION_STATE_ACTIVATING = 2;
35  Constants.ACTIVATION_STATE_NOT_ACTIVATED = 3;
36  Constants.ACTIVATION_STATE_PARTIALLY_ACTIVATED = 4;
37
38  /**
39   * Order in which controls are to appear in the network list sorted by key.
40   */
41  Constants.NETWORK_ORDER = ['Ethernet',
42                             'WiFi',
43                             'WiMAX',
44                             'Cellular',
45                             'VPN',
46                             'addConnection'];
47
48  /**
49   * ID of the menu that is currently visible.
50   * @type {?string}
51   * @private
52   */
53  var activeMenu_ = null;
54
55  /**
56   * Indicates if cellular networks are available.
57   * @type {boolean}
58   * @private
59   */
60  var cellularAvailable_ = false;
61
62  /**
63   * Indicates if cellular networks are enabled.
64   * @type {boolean}
65   * @private
66   */
67  var cellularEnabled_ = false;
68
69  /**
70   * Indicates if cellular device supports network scanning.
71   * @type {boolean}
72   * @private
73   */
74  var cellularSupportsScan_ = false;
75
76  /**
77   * Indicates the current SIM lock type of the cellular device.
78   * @type {boolean}
79   * @private
80   */
81  var cellularSimLockType_ = '';
82
83  /**
84   * Indicates whether the SIM card is absent on the cellular device.
85   * @type {boolean}
86   * @private
87   */
88  var cellularSimAbsent_ = false;
89
90  /**
91   * Indicates if WiMAX networks are available.
92   * @type {boolean}
93   * @private
94   */
95  var wimaxAvailable_ = false;
96
97  /**
98   * Indicates if WiMAX networks are enabled.
99   * @type {boolean}
100   * @private
101   */
102  var wimaxEnabled_ = false;
103
104  /**
105   * Indicates if mobile data roaming is enabled.
106   * @type {boolean}
107   * @private
108   */
109  var enableDataRoaming_ = false;
110
111  /**
112   * Icon to use when not connected to a particular type of network.
113   * @type {!Object.<string, string>} Mapping of network type to icon data url.
114   * @private
115   */
116  var defaultIcons_ = {};
117
118  /**
119   * Returns the display name for 'network'.
120   * @param {Object} data The network data dictionary.
121   */
122  function getNetworkName(data) {
123    if (data.Type == 'Ethernet')
124      return loadTimeData.getString('ethernetName');
125    return data.Name;
126  }
127
128  /**
129   * Create an element in the network list for controlling network
130   * connectivity.
131   * @param {Object} data Description of the network list or command.
132   * @constructor
133   * @extends {cr.ui.ListItem}
134   */
135  function NetworkListItem(data) {
136    var el = cr.doc.createElement('li');
137    el.data_ = {};
138    for (var key in data)
139      el.data_[key] = data[key];
140    NetworkListItem.decorate(el);
141    return el;
142  }
143
144  /**
145   * @param {string} action An action to send to coreOptionsUserMetricsAction.
146   */
147  function sendChromeMetricsAction(action) {
148    chrome.send('coreOptionsUserMetricsAction', [action]);
149  }
150
151  /**
152   * Decorate an element as a NetworkListItem.
153   * @param {!Element} el The element to decorate.
154   */
155  NetworkListItem.decorate = function(el) {
156    el.__proto__ = NetworkListItem.prototype;
157    el.decorate();
158  };
159
160  NetworkListItem.prototype = {
161    __proto__: ListItem.prototype,
162
163    /**
164     * Description of the network group or control.
165     * @type {Object.<string,Object>}
166     * @private
167     */
168    data_: null,
169
170    /**
171     * Element for the control's subtitle.
172     * @type {?Element}
173     * @private
174     */
175    subtitle_: null,
176
177    /**
178     * Icon for the network control.
179     * @type {?Element}
180     * @private
181     */
182    icon_: null,
183
184    /**
185     * Indicates if in the process of connecting to a network.
186     * @type {boolean}
187     * @private
188     */
189    connecting_: false,
190
191    /**
192     * Description of the network control.
193     * @type {Object}
194     */
195    get data() {
196      return this.data_;
197    },
198
199    /**
200     * Text label for the subtitle.
201     * @type {string}
202     */
203    set subtitle(text) {
204      if (text)
205        this.subtitle_.textContent = text;
206      this.subtitle_.hidden = !text;
207    },
208
209    /**
210     * URL for the network icon.
211     * @type {string}
212     */
213    set iconURL(iconURL) {
214      this.icon_.style.backgroundImage = url(iconURL);
215    },
216
217    /**
218     * Type of network icon.  Each type corresponds to a CSS rule.
219     * @type {string}
220     */
221    set iconType(type) {
222      if (defaultIcons_[type])
223        this.iconURL = defaultIcons_[type];
224      else
225        this.icon_.classList.add('network-' + type.toLowerCase());
226    },
227
228    /**
229     * Indicates if the network is in the process of being connected.
230     * @type {boolean}
231     */
232    set connecting(state) {
233      this.connecting_ = state;
234      if (state)
235        this.icon_.classList.add('network-connecting');
236      else
237        this.icon_.classList.remove('network-connecting');
238    },
239
240    /**
241     * Indicates if the network is in the process of being connected.
242     * @type {boolean}
243     */
244    get connecting() {
245      return this.connecting_;
246    },
247
248    /**
249     * Set the direction of the text.
250     * @param {string} direction The direction of the text, e.g. 'ltr'.
251     */
252    setSubtitleDirection: function(direction) {
253      this.subtitle_.dir = direction;
254    },
255
256    /**
257     * Indicate that the selector arrow should be shown.
258     */
259    showSelector: function() {
260      this.subtitle_.classList.add('network-selector');
261    },
262
263    /**
264     * Adds an indicator to show that the network is policy managed.
265     */
266    showManagedNetworkIndicator: function() {
267      this.appendChild(new ManagedNetworkIndicator());
268    },
269
270    /** @override */
271    decorate: function() {
272      ListItem.prototype.decorate.call(this);
273      this.className = 'network-group';
274      this.icon_ = this.ownerDocument.createElement('div');
275      this.icon_.className = 'network-icon';
276      this.appendChild(this.icon_);
277      var textContent = this.ownerDocument.createElement('div');
278      textContent.className = 'network-group-labels';
279      this.appendChild(textContent);
280      var categoryLabel = this.ownerDocument.createElement('div');
281      var title;
282      if (this.data_.key == 'addConnection')
283        title = 'addConnectionTitle';
284      else
285        title = this.data_.key.toLowerCase() + 'Title';
286      categoryLabel.className = 'network-title';
287      categoryLabel.textContent = loadTimeData.getString(title);
288      textContent.appendChild(categoryLabel);
289      this.subtitle_ = this.ownerDocument.createElement('div');
290      this.subtitle_.className = 'network-subtitle';
291      textContent.appendChild(this.subtitle_);
292    },
293  };
294
295  /**
296   * Creates a control that displays a popup menu when clicked.
297   * @param {Object} data  Description of the control.
298   * @constructor
299   * @extends {NetworkListItem}
300   */
301  function NetworkMenuItem(data) {
302    var el = new NetworkListItem(data);
303    el.__proto__ = NetworkMenuItem.prototype;
304    el.decorate();
305    return el;
306  }
307
308  NetworkMenuItem.prototype = {
309    __proto__: NetworkListItem.prototype,
310
311    /**
312     * Popup menu element.
313     * @type {?Element}
314     * @private
315     */
316    menu_: null,
317
318    /** @override */
319    decorate: function() {
320      this.subtitle = null;
321      if (this.data.iconType)
322        this.iconType = this.data.iconType;
323      this.addEventListener('click', function() {
324        this.showMenu();
325      });
326    },
327
328    /**
329     * Retrieves the ID for the menu.
330     */
331    getMenuName: function() {
332      return this.data_.key.toLowerCase() + '-network-menu';
333    },
334
335    /**
336     * Creates a popup menu for the control.
337     * @return {Element} The newly created menu.
338     */
339    createMenu: function() {
340      if (this.data.menu) {
341        var menu = this.ownerDocument.createElement('div');
342        menu.id = this.getMenuName();
343        menu.className = 'network-menu';
344        menu.hidden = true;
345        Menu.decorate(menu);
346        for (var i = 0; i < this.data.menu.length; i++) {
347          var entry = this.data.menu[i];
348          createCallback_(menu, null, entry.label, entry.command);
349        }
350        return menu;
351      }
352      return null;
353    },
354
355    canUpdateMenu: function() {
356      return false;
357    },
358
359    /**
360     * Displays a popup menu.
361     */
362    showMenu: function() {
363      var rebuild = false;
364      // Force a rescan if opening the menu for WiFi networks to ensure the
365      // list is up to date. Networks are periodically rescanned, but depending
366      // on timing, there could be an excessive delay before the first rescan
367      // unless forced.
368      var rescan = !activeMenu_ && this.data_.key == 'WiFi';
369      if (!this.menu_) {
370        rebuild = true;
371        var existing = $(this.getMenuName());
372        if (existing) {
373          if (this.updateMenu())
374            return;
375          closeMenu_();
376        }
377        this.menu_ = this.createMenu();
378        this.menu_.addEventListener('mousedown', function(e) {
379          // Prevent blurring of list, which would close the menu.
380          e.preventDefault();
381        });
382        var parent = $('network-menus');
383        if (existing)
384          parent.replaceChild(this.menu_, existing);
385        else
386          parent.appendChild(this.menu_);
387      }
388      var top = this.offsetTop + this.clientHeight;
389      var menuId = this.getMenuName();
390      if (menuId != activeMenu_ || rebuild) {
391        closeMenu_();
392        activeMenu_ = menuId;
393        this.menu_.style.setProperty('top', top + 'px');
394        this.menu_.hidden = false;
395      }
396      if (rescan) {
397        // TODO(stevenjb): chrome.networkingPrivate.requestNetworkScan
398        chrome.send('requestNetworkScan');
399      }
400    }
401  };
402
403  /**
404   * Creates a control for selecting or configuring a network connection based
405   * on the type of connection (e.g. wifi versus vpn).
406   * @param {{key: string, networkList: Array.<NetworkInfo>}} data Description
407   *     of the network.
408   * @constructor
409   * @extends {NetworkMenuItem}
410   */
411  function NetworkSelectorItem(data) {
412    var el = new NetworkMenuItem(data);
413    el.__proto__ = NetworkSelectorItem.prototype;
414    el.decorate();
415    return el;
416  }
417
418  NetworkSelectorItem.prototype = {
419    __proto__: NetworkMenuItem.prototype,
420
421    /** @override */
422    decorate: function() {
423      // TODO(kevers): Generalize method of setting default label.
424      var policyManaged = false;
425      this.subtitle = loadTimeData.getString('OncConnectionStateNotConnected');
426      var list = this.data_.networkList;
427      var candidateURL = null;
428      for (var i = 0; i < list.length; i++) {
429        var networkDetails = list[i];
430        if (networkDetails.ConnectionState == 'Connecting' ||
431            networkDetails.ConnectionState == 'Connected') {
432          this.subtitle = getNetworkName(networkDetails);
433          this.setSubtitleDirection('ltr');
434          policyManaged = networkDetails.policyManaged;
435          candidateURL = networkDetails.iconURL;
436          // Only break when we see a connecting network as it is possible to
437          // have a connected network and a connecting network at the same
438          // time.
439          if (networkDetails.ConnectionState == 'Connecting') {
440            this.connecting = true;
441            candidateURL = null;
442            break;
443          }
444        }
445      }
446      if (candidateURL)
447        this.iconURL = candidateURL;
448      else
449        this.iconType = this.data.key;
450
451      this.showSelector();
452
453      if (policyManaged)
454        this.showManagedNetworkIndicator();
455
456      if (activeMenu_ == this.getMenuName()) {
457        // Menu is already showing and needs to be updated. Explicitly calling
458        // show menu will force the existing menu to be replaced.  The call
459        // is deferred in order to ensure that position of this element has
460        // beem properly updated.
461        var self = this;
462        setTimeout(function() {self.showMenu();}, 0);
463      }
464    },
465
466    /**
467     * Creates a menu for selecting, configuring or disconnecting from a
468     * network.
469     * @return {!Element} The newly created menu.
470     */
471    createMenu: function() {
472      var menu = this.ownerDocument.createElement('div');
473      menu.id = this.getMenuName();
474      menu.className = 'network-menu';
475      menu.hidden = true;
476      Menu.decorate(menu);
477      var addendum = [];
478      if (this.data_.key == 'WiFi') {
479        addendum.push({
480          label: loadTimeData.getString('joinOtherNetwork'),
481          command: createAddConnectionCallback_('WiFi'),
482          data: {}
483        });
484      } else if (this.data_.key == 'Cellular') {
485        if (cellularEnabled_ && cellularSupportsScan_) {
486          addendum.push({
487            label: loadTimeData.getString('otherCellularNetworks'),
488            command: createAddConnectionCallback_('Cellular'),
489            addClass: ['other-cellulars'],
490            data: {}
491          });
492        }
493
494        var label = enableDataRoaming_ ? 'disableDataRoaming' :
495            'enableDataRoaming';
496        var disabled = !loadTimeData.getValue('loggedInAsOwner');
497        var entry = {label: loadTimeData.getString(label),
498                     data: {}};
499        if (disabled) {
500          entry.command = null;
501          entry.tooltip =
502              loadTimeData.getString('dataRoamingDisableToggleTooltip');
503        } else {
504          var self = this;
505          entry.command = function() {
506            options.Preferences.setBooleanPref(
507                'cros.signed.data_roaming_enabled',
508                !enableDataRoaming_, true);
509            // Force revalidation of the menu the next time it is displayed.
510            self.menu_ = null;
511          };
512        }
513        addendum.push(entry);
514      } else if (this.data_.key == 'VPN') {
515        addendum.push({
516          label: loadTimeData.getString('joinOtherNetwork'),
517          command: createAddConnectionCallback_('VPN'),
518          data: {}
519        });
520      }
521
522      var list = this.data.rememberedNetworks;
523      if (list && list.length > 0) {
524        var callback = function(list) {
525          $('remembered-network-list').clear();
526          var dialog = options.PreferredNetworks.getInstance();
527          PageManager.showPageByName('preferredNetworksPage', false);
528          dialog.update(list);
529          sendChromeMetricsAction('Options_NetworkShowPreferred');
530        };
531        addendum.push({label: loadTimeData.getString('preferredNetworks'),
532                       command: callback,
533                       data: list});
534      }
535
536      var networkGroup = this.ownerDocument.createElement('div');
537      networkGroup.className = 'network-menu-group';
538      list = this.data.networkList;
539      var empty = !list || list.length == 0;
540      if (list) {
541        var connectedVpnServicePath = '';
542        for (var i = 0; i < list.length; i++) {
543          var data = list[i];
544          this.createNetworkOptionsCallback_(networkGroup, data);
545          // For VPN only, append a 'Disconnect' item to the dropdown menu.
546          if (!connectedVpnServicePath && data.Type == 'VPN' &&
547              (data.ConnectionState == 'Connected' ||
548               data.ConnectionState == 'Connecting')) {
549            connectedVpnServicePath = data.servicePath;
550          }
551        }
552        if (connectedVpnServicePath) {
553          var disconnectCallback = function() {
554            sendChromeMetricsAction('Options_NetworkDisconnectVPN');
555            // TODO(stevenjb): chrome.networkingPrivate.startDisconnect
556            chrome.send('startDisconnect', [connectedVpnServicePath]);
557          };
558          // Add separator
559          addendum.push({});
560          addendum.push({label: loadTimeData.getString('disconnectNetwork'),
561                         command: disconnectCallback,
562                         data: data});
563        }
564      }
565      if (this.data_.key == 'WiFi' || this.data_.key == 'WiMAX' ||
566          this.data_.key == 'Cellular') {
567        addendum.push({});
568        if (this.data_.key == 'WiFi') {
569          addendum.push({
570            label: loadTimeData.getString('turnOffWifi'),
571            command: function() {
572              sendChromeMetricsAction('Options_NetworkWifiToggle');
573              // TODO(stevenjb): chrome.networkingPrivate.disableNetworkType
574              chrome.send('disableNetworkType', ['WiFi']);
575            },
576            data: {}});
577        } else if (this.data_.key == 'WiMAX') {
578          addendum.push({
579            label: loadTimeData.getString('turnOffWimax'),
580            command: function() {
581              // TODO(stevenjb): chrome.networkingPrivate.disableNetworkType
582              chrome.send('disableNetworkType', ['WiMAX']);
583            },
584            data: {}});
585        } else if (this.data_.key == 'Cellular') {
586          addendum.push({
587            label: loadTimeData.getString('turnOffCellular'),
588            command: function() {
589              // TODO(stevenjb): chrome.networkingPrivate.disableNetworkType
590              chrome.send('disableNetworkType', ['Cellular']);
591            },
592            data: {}});
593        }
594      }
595      if (!empty)
596        menu.appendChild(networkGroup);
597      if (addendum.length > 0) {
598        var separator = false;
599        if (!empty) {
600          menu.appendChild(MenuItem.createSeparator());
601          separator = true;
602        }
603        for (var i = 0; i < addendum.length; i++) {
604          var value = addendum[i];
605          if (value.data) {
606            var item = createCallback_(menu, value.data, value.label,
607                                       value.command);
608            if (value.tooltip)
609              item.title = value.tooltip;
610            if (value.addClass)
611              item.classList.add(value.addClass);
612            separator = false;
613          } else if (!separator) {
614            menu.appendChild(MenuItem.createSeparator());
615            separator = true;
616          }
617        }
618      }
619      return menu;
620    },
621
622    /**
623     * Determines if a menu can be updated on the fly. Menus that cannot be
624     * updated are fully regenerated using createMenu. The advantage of
625     * updating a menu is that it can preserve ordering of networks avoiding
626     * entries from jumping around after an update.
627     */
628    canUpdateMenu: function() {
629      return this.data_.key == 'WiFi' && activeMenu_ == this.getMenuName();
630    },
631
632    /**
633     * Updates an existing menu.  Updated menus preserve ordering of prior
634     * entries.  During the update process, the ordering may differ from the
635     * preferred ordering as determined by the network library.  If the
636     * ordering becomes potentially out of sync, then the updated menu is
637     * marked for disposal on close.  Reopening the menu will force a
638     * regeneration, which will in turn fix the ordering.
639     * @return {boolean} True if successfully updated.
640     */
641    updateMenu: function() {
642      if (!this.canUpdateMenu())
643        return false;
644      var oldMenu = $(this.getMenuName());
645      var group = oldMenu.getElementsByClassName('network-menu-group')[0];
646      if (!group)
647        return false;
648      var newMenu = this.createMenu();
649      var discardOnClose = false;
650      var oldNetworkButtons = this.extractNetworkConnectButtons_(oldMenu);
651      var newNetworkButtons = this.extractNetworkConnectButtons_(newMenu);
652      for (var key in oldNetworkButtons) {
653        if (newNetworkButtons[key]) {
654          group.replaceChild(newNetworkButtons[key].button,
655                             oldNetworkButtons[key].button);
656          if (newNetworkButtons[key].index != oldNetworkButtons[key].index)
657            discardOnClose = true;
658          newNetworkButtons[key] = null;
659        } else {
660          // Leave item in list to prevent network items from jumping due to
661          // deletions.
662          oldNetworkButtons[key].disabled = true;
663          discardOnClose = true;
664        }
665      }
666      for (var key in newNetworkButtons) {
667        var entry = newNetworkButtons[key];
668        if (entry) {
669          group.appendChild(entry.button);
670          discardOnClose = true;
671        }
672      }
673      oldMenu.data = {discardOnClose: discardOnClose};
674      return true;
675    },
676
677    /**
678     * Extracts a mapping of network names to menu element and position.
679     * @param {!Element} menu The menu to process.
680     * @return {Object.<string, ?{index: number, button: Element}>}
681     *     Network mapping.
682     * @private
683     */
684    extractNetworkConnectButtons_: function(menu) {
685      var group = menu.getElementsByClassName('network-menu-group')[0];
686      var networkButtons = {};
687      if (!group)
688        return networkButtons;
689      var buttons = group.getElementsByClassName('network-menu-item');
690      for (var i = 0; i < buttons.length; i++) {
691        var label = buttons[i].data.label;
692        networkButtons[label] = {index: i, button: buttons[i]};
693      }
694      return networkButtons;
695    },
696
697    /**
698     * Adds a menu item for showing network details.
699     * @param {!Element} parent The parent element.
700     * @param {Object} data Description of the network.
701     * @private
702     */
703    createNetworkOptionsCallback_: function(parent, data) {
704      var menuItem = createCallback_(parent,
705                                     data,
706                                     getNetworkName(data),
707                                     'showDetails',
708                                     data.iconURL);
709      if (data.policyManaged)
710        menuItem.appendChild(new ManagedNetworkIndicator());
711      if (data.ConnectionState == 'Connected' ||
712          data.ConnectionState == 'Connecting') {
713        var label = menuItem.getElementsByClassName(
714            'network-menu-item-label')[0];
715        label.classList.add('active-network');
716      }
717    }
718  };
719
720  /**
721   * Creates a button-like control for configurating internet connectivity.
722   * @param {{key: string, subtitle: string, command: Function}} data
723   *     Description of the network control.
724   * @constructor
725   * @extends {NetworkListItem}
726   */
727  function NetworkButtonItem(data) {
728    var el = new NetworkListItem(data);
729    el.__proto__ = NetworkButtonItem.prototype;
730    el.decorate();
731    return el;
732  }
733
734  NetworkButtonItem.prototype = {
735    __proto__: NetworkListItem.prototype,
736
737    /** @override */
738    decorate: function() {
739      if (this.data.subtitle)
740        this.subtitle = this.data.subtitle;
741      else
742       this.subtitle = null;
743      if (this.data.command)
744        this.addEventListener('click', this.data.command);
745      if (this.data.iconURL)
746        this.iconURL = this.data.iconURL;
747      else if (this.data.iconType)
748        this.iconType = this.data.iconType;
749      if (this.data.policyManaged)
750        this.showManagedNetworkIndicator();
751    },
752  };
753
754  /**
755   * Adds a command to a menu for modifying network settings.
756   * @param {!Element} menu Parent menu.
757   * @param {Object} data Description of the network.
758   * @param {!string} label Display name for the menu item.
759   * @param {?(string|!Function)} command Callback function or name
760   *     of the command for |networkCommand|.
761   * @param {string=} opt_iconURL Optional URL to an icon for the menu item.
762   * @return {!Element} The created menu item.
763   * @private
764   */
765  function createCallback_(menu, data, label, command, opt_iconURL) {
766    var button = menu.ownerDocument.createElement('div');
767    button.className = 'network-menu-item';
768
769    var buttonIcon = menu.ownerDocument.createElement('div');
770    buttonIcon.className = 'network-menu-item-icon';
771    button.appendChild(buttonIcon);
772    if (opt_iconURL)
773      buttonIcon.style.backgroundImage = url(opt_iconURL);
774
775    var buttonLabel = menu.ownerDocument.createElement('span');
776    buttonLabel.className = 'network-menu-item-label';
777    buttonLabel.textContent = label;
778    button.appendChild(buttonLabel);
779    var callback = null;
780    if (typeof command == 'string') {
781      var type = data.Type;
782      var path = data.servicePath;
783      callback = function() {
784        chrome.send('networkCommand', [type, path, command]);
785        closeMenu_();
786      };
787    } else if (command != null) {
788      if (data) {
789        callback = function() {
790          (/** @type {Function} */(command))(data);
791          closeMenu_();
792        };
793      } else {
794        callback = function() {
795          (/** @type {Function} */(command))();
796          closeMenu_();
797        };
798      }
799    }
800    if (callback != null)
801      button.addEventListener('click', callback);
802    else
803      buttonLabel.classList.add('network-disabled-control');
804
805    button.data = {label: label};
806    MenuItem.decorate(button);
807    menu.appendChild(button);
808    return button;
809  }
810
811  /**
812   * A list of controls for manipulating network connectivity.
813   * @constructor
814   * @extends {cr.ui.List}
815   */
816  var NetworkList = cr.ui.define('list');
817
818  NetworkList.prototype = {
819    __proto__: List.prototype,
820
821    /** @override */
822    decorate: function() {
823      List.prototype.decorate.call(this);
824      this.startBatchUpdates();
825      this.autoExpands = true;
826      this.dataModel = new ArrayDataModel([]);
827      this.selectionModel = new ListSingleSelectionModel();
828      this.addEventListener('blur', this.onBlur_.bind(this));
829      this.selectionModel.addEventListener('change',
830                                           this.onSelectionChange_.bind(this));
831
832      // Wi-Fi control is always visible.
833      this.update({key: 'WiFi', networkList: []});
834
835      var entryAddWifi = {
836        label: loadTimeData.getString('addConnectionWifi'),
837        command: createAddConnectionCallback_('WiFi')
838      };
839      var entryAddVPN = {
840        label: loadTimeData.getString('addConnectionVPN'),
841        command: createAddConnectionCallback_('VPN')
842      };
843      this.update({key: 'addConnection',
844                   iconType: 'add-connection',
845                   menu: [entryAddWifi, entryAddVPN]
846                  });
847
848      var prefs = options.Preferences.getInstance();
849      prefs.addEventListener('cros.signed.data_roaming_enabled',
850          function(event) {
851            enableDataRoaming_ = event.value.value;
852          });
853      this.endBatchUpdates();
854    },
855
856    /**
857     * When the list loses focus, unselect all items in the list and close the
858     * active menu.
859     * @private
860     */
861    onBlur_: function() {
862      this.selectionModel.unselectAll();
863      closeMenu_();
864    },
865
866    /**
867     * Close bubble and menu when a different list item is selected.
868     * @param {Event} event Event detailing the selection change.
869     * @private
870     */
871    onSelectionChange_: function(event) {
872      PageManager.hideBubble();
873      // A list item may temporarily become unselected while it is constructing
874      // its menu. The menu should therefore only be closed if a different item
875      // is selected, not when the menu's owner item is deselected.
876      if (activeMenu_) {
877        for (var i = 0; i < event.changes.length; ++i) {
878          if (event.changes[i].selected) {
879            var item = this.dataModel.item(event.changes[i].index);
880            if (!item.getMenuName || item.getMenuName() != activeMenu_) {
881              closeMenu_();
882              return;
883            }
884          }
885        }
886      }
887    },
888
889    /**
890     * Finds the index of a network item within the data model based on
891     * category.
892     * @param {string} key Unique key for the item in the list.
893     * @return {(number|undefined)} The index of the network item, or
894     *     |undefined| if it is not found.
895     */
896    indexOf: function(key) {
897      var size = this.dataModel.length;
898      for (var i = 0; i < size; i++) {
899        var entry = this.dataModel.item(i);
900        if (entry.key == key)
901          return i;
902      }
903      return undefined;
904    },
905
906    /**
907     * Updates a network control.
908     * @param {Object.<string,string>} data Description of the entry.
909     */
910    update: function(data) {
911      this.startBatchUpdates();
912      var index = this.indexOf(data.key);
913      if (index == undefined) {
914        // Find reference position for adding the element.  We cannot hide
915        // individual list elements, thus we need to conditionally add or
916        // remove elements and cannot rely on any element having a fixed index.
917        for (var i = 0; i < Constants.NETWORK_ORDER.length; i++) {
918          if (data.key == Constants.NETWORK_ORDER[i]) {
919            data.sortIndex = i;
920            break;
921          }
922        }
923        var referenceIndex = -1;
924        for (var i = 0; i < this.dataModel.length; i++) {
925          var entry = this.dataModel.item(i);
926          if (entry.sortIndex < data.sortIndex)
927            referenceIndex = i;
928          else
929            break;
930        }
931        if (referenceIndex == -1) {
932          // Prepend to the start of the list.
933          this.dataModel.splice(0, 0, data);
934        } else if (referenceIndex == this.dataModel.length) {
935          // Append to the end of the list.
936          this.dataModel.push(data);
937        } else {
938          // Insert after the reference element.
939          this.dataModel.splice(referenceIndex + 1, 0, data);
940        }
941      } else {
942        var entry = this.dataModel.item(index);
943        data.sortIndex = entry.sortIndex;
944        this.dataModel.splice(index, 1, data);
945      }
946      this.endBatchUpdates();
947    },
948
949    /**
950     * @override
951     * @param {Object} entry
952     */
953    createItem: function(entry) {
954      if (entry.networkList)
955        return new NetworkSelectorItem(
956            /** @type {{key: string, networkList: Array.<NetworkInfo>}} */(
957                entry));
958      if (entry.command)
959        return new NetworkButtonItem(
960            /** @type {{key: string, subtitle: string, command: Function}} */(
961                entry));
962      if (entry.menu)
963        return new NetworkMenuItem(entry);
964      return undefined;
965    },
966
967    /**
968     * Deletes an element from the list.
969     * @param {string} key  Unique identifier for the element.
970     */
971    deleteItem: function(key) {
972      var index = this.indexOf(key);
973      if (index != undefined)
974        this.dataModel.splice(index, 1);
975    },
976
977    /**
978     * Updates the state of a toggle button.
979     * @param {string} key Unique identifier for the element.
980     * @param {boolean} active Whether the control is active.
981     */
982    updateToggleControl: function(key, active) {
983      var index = this.indexOf(key);
984      if (index != undefined) {
985        var entry = this.dataModel.item(index);
986        entry.iconType = active ? 'control-active' :
987            'control-inactive';
988        this.update(entry);
989      }
990    }
991  };
992
993  /**
994   * Sets the default icon to use for each network type if disconnected.
995   * @param {!Object.<string, string>} data Mapping of network type to icon
996   *     data url.
997   */
998  NetworkList.setDefaultNetworkIcons = function(data) {
999    defaultIcons_ = Object.create(data);
1000  };
1001
1002  /**
1003   * Chrome callback for updating network controls.
1004   * @param {{wiredList: Array.<NetworkInfo>, wirelessList: Array.<NetworkInfo>,
1005   *     vpnList: Array.<NetworkInfo>, rememberedList: Array.<NetworkInfo>,
1006   *     wifiAvailable: boolean, wifiEnabled: boolean, wimaxAvailable: boolean,
1007   *     wimaxEnabled: boolean, cellularAvailable: boolean,
1008   *     cellularEnabled: boolean, cellularSupportsScan: boolean}} data
1009   *     Description of available network devices and their corresponding state.
1010   */
1011  NetworkList.refreshNetworkData = function(data) {
1012    var networkList = $('network-list');
1013    networkList.startBatchUpdates();
1014    cellularAvailable_ = data.cellularAvailable;
1015    cellularEnabled_ = data.cellularEnabled;
1016    cellularSupportsScan_ = data.cellularSupportsScan;
1017    cellularSimAbsent_ = data.cellularSimAbsent;
1018    cellularSimLockType_ = data.cellularSimLockType;
1019    wimaxAvailable_ = data.wimaxAvailable;
1020    wimaxEnabled_ = data.wimaxEnabled;
1021
1022    // Only show Ethernet control if connected.
1023    var ethernetConnection = getConnection_(data.wiredList);
1024    if (ethernetConnection) {
1025      var type = String('Ethernet');
1026      var path = ethernetConnection.servicePath;
1027      var ethernetOptions = function() {
1028        chrome.send('networkCommand', [type, path, 'showDetails']);
1029      };
1030      networkList.update(
1031          { key: 'Ethernet',
1032            subtitle: loadTimeData.getString('OncConnectionStateConnected'),
1033            iconURL: ethernetConnection.iconURL,
1034            command: ethernetOptions,
1035            policyManaged: ethernetConnection.policyManaged }
1036          );
1037    } else {
1038      networkList.deleteItem('Ethernet');
1039    }
1040
1041    if (data.wifiEnabled)
1042      loadData_('WiFi', data.wirelessList, data.rememberedList);
1043    else
1044      addEnableNetworkButton_('WiFi');
1045
1046    // Only show cellular control if available.
1047    if (data.cellularAvailable) {
1048      if (data.cellularEnabled)
1049        loadData_('Cellular', data.wirelessList, data.rememberedList);
1050      else
1051        addEnableNetworkButton_('Cellular');
1052    } else {
1053      networkList.deleteItem('Cellular');
1054    }
1055
1056    // Only show wimax control if available. Uses cellular icons.
1057    if (data.wimaxAvailable) {
1058      if (data.wimaxEnabled)
1059        loadData_('WiMAX', data.wirelessList, data.rememberedList);
1060      else
1061        addEnableNetworkButton_('WiMAX');
1062    } else {
1063      networkList.deleteItem('WiMAX');
1064    }
1065
1066    // Only show VPN control if there is at least one VPN configured.
1067    if (data.vpnList.length > 0)
1068      loadData_('VPN', data.vpnList, data.rememberedList);
1069    else
1070      networkList.deleteItem('VPN');
1071    networkList.endBatchUpdates();
1072  };
1073
1074  /**
1075   * Replaces a network menu with a button for enabling the network type.
1076   * @param {string} type The type of network (WiFi, Cellular or Wimax).
1077   * @private
1078   */
1079  function addEnableNetworkButton_(type) {
1080    var subtitle = loadTimeData.getString('networkDisabled');
1081    var icon = (type == 'WiMAX') ? 'Cellular' : type;
1082    var enableNetwork = function() {
1083      if (type == 'WiFi')
1084        sendChromeMetricsAction('Options_NetworkWifiToggle');
1085      if (type == 'Cellular') {
1086        if (cellularSimLockType_) {
1087          chrome.send('simOperation', ['unlock']);
1088          return;
1089        } else if (cellularEnabled_ && cellularSimAbsent_) {
1090          chrome.send('simOperation', ['configure']);
1091          return;
1092        }
1093      }
1094      // TODO(stevenjb): chrome.networkingPrivate.enableNetworkType
1095      chrome.send('enableNetworkType', [type]);
1096    };
1097    $('network-list').update({key: type,
1098                              subtitle: subtitle,
1099                              iconType: icon,
1100                              command: enableNetwork});
1101  }
1102
1103  /**
1104   * Element for indicating a policy managed network.
1105   * @constructor
1106   * @extends {options.ControlledSettingIndicator}
1107   */
1108  function ManagedNetworkIndicator() {
1109    var el = cr.doc.createElement('span');
1110    el.__proto__ = ManagedNetworkIndicator.prototype;
1111    el.decorate();
1112    return el;
1113  }
1114
1115  ManagedNetworkIndicator.prototype = {
1116    __proto__: ControlledSettingIndicator.prototype,
1117
1118    /** @override */
1119    decorate: function() {
1120      ControlledSettingIndicator.prototype.decorate.call(this);
1121      this.controlledBy = 'policy';
1122      var policyLabel = loadTimeData.getString('managedNetwork');
1123      this.setAttribute('textPolicy', policyLabel);
1124      this.removeAttribute('tabindex');
1125    },
1126
1127    /** @override */
1128    handleEvent: function(event) {
1129      // Prevent focus blurring as that would close any currently open menu.
1130      if (event.type == 'mousedown')
1131        return;
1132      ControlledSettingIndicator.prototype.handleEvent.call(this, event);
1133    },
1134
1135    /**
1136     * Handle mouse events received by the bubble, preventing focus blurring as
1137     * that would close any currently open menu and preventing propagation to
1138     * any elements located behind the bubble.
1139     * @param {Event} event Mouse event.
1140     */
1141    stopEvent: function(event) {
1142      event.preventDefault();
1143      event.stopPropagation();
1144    },
1145
1146    /** @override */
1147    toggleBubble: function() {
1148      if (activeMenu_ && !$(activeMenu_).contains(this))
1149        closeMenu_();
1150      ControlledSettingIndicator.prototype.toggleBubble.call(this);
1151      if (this.showingBubble) {
1152        var bubble = PageManager.getVisibleBubble();
1153        bubble.addEventListener('mousedown', this.stopEvent);
1154        bubble.addEventListener('click', this.stopEvent);
1155      }
1156    }
1157  };
1158
1159  /**
1160   * Updates the list of available networks and their status, filtered by
1161   * network type.
1162   * @param {string} type The type of network.
1163   * @param {Array} available The list of available networks and their status.
1164   * @param {Array} remembered The list of remmebered networks.
1165   */
1166  function loadData_(type, available, remembered) {
1167    var data = {key: type};
1168    var availableNetworks = [];
1169    for (var i = 0; i < available.length; i++) {
1170      if (available[i].Type == type)
1171        availableNetworks.push(available[i]);
1172    }
1173    data.networkList = availableNetworks;
1174    if (remembered) {
1175      var rememberedNetworks = [];
1176      for (var i = 0; i < remembered.length; i++) {
1177        if (remembered[i].Type == type)
1178          rememberedNetworks.push(remembered[i]);
1179      }
1180      data.rememberedNetworks = rememberedNetworks;
1181    }
1182    $('network-list').update(data);
1183  }
1184
1185  /**
1186   * Hides the currently visible menu.
1187   * @private
1188   */
1189  function closeMenu_() {
1190    if (activeMenu_) {
1191      var menu = $(activeMenu_);
1192      menu.hidden = true;
1193      if (menu.data && menu.data.discardOnClose)
1194        menu.parentNode.removeChild(menu);
1195      activeMenu_ = null;
1196    }
1197  }
1198
1199  /**
1200   * Fetches the active connection.
1201   * @param {Array.<Object>} networkList List of networks.
1202   * @return {Object}
1203   * @private
1204   */
1205  function getConnection_(networkList) {
1206    if (!networkList)
1207      return null;
1208    for (var i = 0; i < networkList.length; i++) {
1209      var entry = networkList[i];
1210      if (entry.ConnectionState == 'Connected' ||
1211          entry.ConnectionState == 'Connecting')
1212        return entry;
1213    }
1214    return null;
1215  }
1216
1217  /**
1218   * Create a callback function that adds a new connection of the given type.
1219   * @param {string} type An ONC network type
1220   * @return {function()} The created callback.
1221   * @private
1222   */
1223  function createAddConnectionCallback_(type) {
1224    return function() {
1225      if (type == 'WiFi')
1226        sendChromeMetricsAction('Options_NetworkJoinOtherWifi');
1227      else if (type == 'VPN')
1228        sendChromeMetricsAction('Options_NetworkJoinOtherVPN');
1229      chrome.send('networkCommand', [type, '', 'add']);
1230    };
1231  }
1232
1233  /**
1234   * Whether the Network list is disabled. Only used for display purpose.
1235   */
1236  cr.defineProperty(NetworkList, 'disabled', cr.PropertyKind.BOOL_ATTR);
1237
1238  // Export
1239  return {
1240    NetworkList: NetworkList
1241  };
1242});
1243