• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2013 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
5cr.define('network.status', function() {
6  var ArrayDataModel = cr.ui.ArrayDataModel;
7  var List = cr.ui.List;
8  var ListItem = cr.ui.ListItem;
9  var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
10
11  /**
12   * Returns the entries of |dataModel| as an array.
13   * @param {ArrayDataModel} dataModel .
14   * @return {Array} .
15   */
16  function dataModelToArray(dataModel) {
17    var array = [];
18    for (var i = 0; i < dataModel.length; i++) {
19      array.push(dataModel.item(i));
20    }
21    return array;
22  }
23
24  /**
25   * Calculates both set difference of |a| and |b| and returns them in an array:
26   * [ a - b, b - a ].
27   * @param {Array.<T>} a .
28   * @param {Array.<T>} b .
29   * @param {function(T): K} toKey .
30   * @return {Array.<Array.<T>>} .
31   */
32  function differenceBy(a, b, toKey) {
33    var inA = {};
34    a.forEach(function(elA) {
35      inA[toKey(elA)] = elA;
36    });
37    var bMinusA = [];
38    b.forEach(function(elB) {
39      var keyB = toKey(elB);
40      if (inA[keyB])
41        delete inA[keyB];
42      else
43        bMinusA.push(elB);
44    });
45    var aMinusB = [];
46    for (var keyA in inA) {
47      aMinusB.push(inA[keyA]);
48    }
49    return [aMinusB, bMinusA];
50  }
51
52  /**
53   * Updates the data model of |uiList| to |newList|. Ensures that unchanged
54   * entries are not touched. Doesn't preserve the order of |newList|.
55   * @param {List} uiList .
56   * @param {Array} newList .
57   */
58  function updateDataModel(uiList, newList) {
59    uiList.startBatchUpdates();
60    var dataModel = uiList.dataModel;
61    var diff = differenceBy(dataModelToArray(dataModel), newList,
62                            function(e) { return e[0]; });
63    var toRemove = diff[0];
64    var toAdd = diff[1];
65    toRemove.forEach(function(element) {
66      dataModel.splice(dataModel.indexOf(element), 1);
67    });
68    dataModel.splice.apply(dataModel, [dataModel.length, 0].concat(toAdd));
69    uiList.endBatchUpdates();
70  }
71
72  /**
73   * Creates a map of the entries of |array|. Each entry is associated to the
74   * key getKey(entry).
75   * @param {Array.<T>} array .
76   * @param {function(T): K} getKey .
77   * @return {Object.<K, T>} .
78   */
79  function createMapFromList(array, getKey) {
80    var result = {};
81    array.forEach(function(entry) {
82      result[getKey(entry)] = entry;
83    });
84    return result;
85  }
86
87  /**
88   * Wraps each entry in |array| into an array. The result contains rows with
89   * one entry each.
90   * @param {Array.<T>} array .
91   * @return {Array.<Array.<T>>} .
92   */
93  function arrayToTable(array) {
94    return array.map(function(e) {
95      return [e];
96    });
97  }
98
99  /**
100   * The NetworkStatusList contains various button types according to the
101   * following hierarchy. Note: this graph doesn't depict inheritance but
102   * ownership of instances of these types.
103   *
104   * NetworkStatusList
105   *   +-- TechnologyButton
106   *       +-- NestedStatusButton
107   *
108   * Inheritance hierarchy:
109   * ListItem
110   *   +-- Button
111   *       +-- UnfoldingButton
112   *       |   +-- TechnologyButton
113   *       +-- NestedStatusButton
114   */
115
116  /**
117   * Base class for all buttons in the NetworkStatusList. |key| is used to
118   * identify this button.
119   * After construction, update() has to be called!
120   * @param {NetworkStatusList} networkStatus .
121   * @param {string} key .
122   * @constructor
123   */
124  function Button(networkStatus, key) {
125    var el = cr.doc.createElement('li');
126    el.__proto__ = Button.prototype;
127    el.decorate(networkStatus, key);
128    return el;
129  }
130
131  Button.prototype = {
132    __proto__: ListItem.prototype,
133
134    /**
135     * @override
136     */
137    decorate: function(networkStatus, key) {
138      ListItem.prototype.decorate.call(this);
139      this.networkStatus_ = networkStatus;
140      this.key_ = key;
141      this.className = 'network-status-button';
142
143      var textContent = this.ownerDocument.createElement('div');
144      textContent.className = 'network-status-button-labels';
145
146      var title = this.ownerDocument.createElement('div');
147      title.className = 'nested-status-button-title';
148      textContent.appendChild(title);
149      this.title_ = title;
150
151      var subTitle = this.ownerDocument.createElement('div');
152      subTitle.className = 'nested-status-button-subtitle';
153      textContent.appendChild(subTitle);
154      this.subTitle_ = subTitle;
155
156      this.appendChild(textContent);
157    },
158
159    /**
160     * @type {string}
161     */
162    get key() {
163      return this.key_;
164    },
165
166    /**
167     * Should be called if the data presented by this button
168     * changed. E.g. updates the button's title, icon and nested buttons.
169     * To be overriden by subclasses.
170     */
171    update: function() {
172      this.title_.textContent = this.key;
173    }
174  };
175
176  /**
177   * A button that shows the status of one particular network.
178   * @param {NetworkStatusList} networkStatus .
179   * @param {string} networkID .
180   * @extends {Button}
181   * @constructor
182   */
183  function NestedStatusButton(networkStatus, networkID) {
184    var el = new Button(networkStatus, networkID);
185    el.__proto__ = NestedStatusButton.prototype;
186    return el;
187  }
188
189  NestedStatusButton.prototype = {
190    __proto__: Button.prototype,
191
192    /**
193     * @override
194     */
195    update: function() {
196      var network = this.networkStatus_.getNetworkByID(this.networkID);
197      this.title_.textContent = network.Name;
198      this.subTitle_.textContent = network.ConnectionState;
199    },
200
201    get networkID() {
202      return this.key;
203    }
204  };
205
206  /**
207   * A button (toplevel in the NetworkStatusList) that unfolds a list of nested
208   * buttons when clicked. Only one button will be unfolded at a time.
209   * @param {NetworkStatusList} networkStatus .
210   * @param {string} key .
211   * @extends {Button}
212   * @constructor
213   */
214  function UnfoldingButton(networkStatus, key) {
215    var el = new Button(networkStatus, key);
216    el.__proto__ = UnfoldingButton.prototype;
217    el.decorate();
218    return el;
219  }
220
221  UnfoldingButton.prototype = {
222    __proto__: Button.prototype,
223
224    /**
225     * @override
226     */
227    decorate: function() {
228      this.dropdown_ = null;
229      this.addEventListener('click', this.toggleDropdown_.bind(this));
230    },
231
232    /**
233     * Returns the list of identifiers for which nested buttons will be created.
234     * To be overridden by subclasses.
235     * @return {Array.<string>} .
236     */
237    getEntries: function() {
238      return [];
239    },
240
241    /**
242     * Creates a nested button for |entry| of the current |getEntries|.
243     * To be overridden by subclasses.
244     * @param {string} entry .
245     * @return {ListItem} .
246     */
247    createNestedButton: function(entry) {
248      return new ListItem(entry);
249    },
250
251    /**
252     * Creates the dropdown list containing the nested buttons.
253     * To be overridden by subclasses.
254     * @return {List} .
255     */
256    createDropdown: function() {
257      var list = new List();
258      var self = this;
259      list.createItem = function(row) {
260        return self.createNestedButton(row[0]);
261      };
262      list.autoExpands = true;
263      list.dataModel = new ArrayDataModel(arrayToTable(this.getEntries()));
264      list.selectionModel = new ListSingleSelectionModel();
265      return list;
266    },
267
268    /**
269     * @override
270     */
271    update: function() {
272      Button.prototype.update.call(this);
273      if (!this.dropdown_)
274        return;
275      updateDataModel(this.dropdown_, arrayToTable(this.getEntries()));
276    },
277
278    openDropdown_: function() {
279      var dropdown = this.createDropdown();
280      dropdown.className = 'network-dropdown';
281      this.appendChild(dropdown);
282      this.dropdown_ = dropdown;
283      this.networkStatus_.openDropdown = this;
284    },
285
286    closeDropdown_: function() {
287      this.removeChild(this.dropdown_);
288      this.dropdown_ = null;
289    },
290
291    toggleDropdown_: function() {
292      // TODO(pneubeck): request rescan
293      if (this.networkStatus_.openDropdown === this) {
294        this.closeDropdown_();
295        this.networkStatus_.openDropdown = null;
296      } else if (this.networkStatus_.openDropdown) {
297        this.networkStatus_.openDropdown.closeDropdown_();
298        this.openDropdown_();
299      } else {
300        this.openDropdown_();
301      }
302    }
303  };
304
305  /**
306   * A button (toplevel in the NetworkStatusList) that represents one network
307   * technology (like WiFi or Ethernet) and unfolds a list of nested buttons
308   * when clicked.
309   * @param {NetworkStatusList} networkStatus .
310   * @param {string} technology .
311   * @extends {UnfoldingButton}
312   * @constructor
313   */
314  function TechnologyButton(networkStatus, technology) {
315    var el = new UnfoldingButton(networkStatus, technology);
316    el.__proto__ = TechnologyButton.prototype;
317    el.decorate(technology);
318    return el;
319  }
320
321  TechnologyButton.prototype = {
322    __proto__: UnfoldingButton.prototype,
323
324    /**
325     * @param {string} technology .
326     * @override
327     */
328    decorate: function(technology) {
329      this.technology_ = technology;
330    },
331
332    /**
333     * @override
334     */
335    getEntries: function() {
336      return this.networkStatus_.getNetworkIDsOfType(this.technology_);
337    },
338
339    /**
340     * @override
341     */
342    createNestedButton: function(id) {
343      var self = this;
344      var button = new NestedStatusButton(this.networkStatus_, id);
345      button.onclick = function(e) {
346        e.stopPropagation();
347        self.networkStatus_.handleUserAction({
348          command: 'openConfiguration',
349          networkId: id
350        });
351      };
352      button.update();
353      return button;
354    },
355
356    /**
357     * @override
358     */
359    createDropdown: function() {
360      var list = UnfoldingButton.prototype.createDropdown.call(this);
361      var getIndex = this.networkStatus_.getIndexOfNetworkID.bind(
362          this.networkStatus_);
363      list.dataModel.setCompareFunction(0, function(a, b) {
364        return ArrayDataModel.prototype.defaultValuesCompareFunction(
365            getIndex(a),
366            getIndex(b));
367      });
368      list.dataModel.sort(0, 'asc');
369      return list;
370    },
371
372    /**
373     * @type {string}
374     */
375    get technology() {
376      return this.technology_;
377    },
378
379    /**
380     * @override
381     */
382    update: function() {
383      UnfoldingButton.prototype.update.call(this);
384      if (!this.dropdown_)
385        return;
386      this.dropdown_.items.forEach(function(button) {
387        button.update();
388      });
389    }
390  };
391
392  /**
393   * The order of the toplevel buttons.
394   */
395  var BUTTON_ORDER = [
396    'Ethernet',
397    'WiFi',
398    'Cellular',
399    'VPN',
400    'addConnection'
401  ];
402
403  /**
404   * A map from button key to index according to |BUTTON_ORDER|.
405   */
406  var BUTTON_POSITION = {};
407  BUTTON_ORDER.forEach(function(entry, index) {
408    BUTTON_POSITION[entry] = index;
409  });
410
411  /**
412   * Groups networks by type.
413   * @param {Object.<string, Object>} networkByID A map from network ID to
414   * network properties.
415   * @return {Object.<string, Array.<string>>} A map from network type to the
416   * list of IDs of networks of that type.
417   */
418  function createNetworkIDsByType(networkByID) {
419    var byType = {};
420    for (var id in networkByID) {
421      var network = networkByID[id];
422      var group = byType[network.Type];
423      if (group === undefined) {
424        group = [];
425        byType[network.Type] = group;
426      }
427      group.push(network.GUID);
428    }
429    return byType;
430  }
431
432  /**
433   * A list-like control showing the available networks and controls to
434   * dis-/connect to networks and to open dialogs to create, modify and remove
435   * network configurations.
436   * @constructor
437   */
438  var NetworkStatusList = cr.ui.define('list');
439
440  NetworkStatusList.prototype = {
441    __proto__: List.prototype,
442
443    /**
444     * @override
445     */
446    decorate: function() {
447      List.prototype.decorate.call(this);
448
449      /**
450       * The currently open unfolding button.
451       * @type {UnfoldingButton}
452       */
453      this.openDropdown = null;
454
455      /**
456       * The set of technologies shown to the user.
457       * @type {Object.<string, boolean>}
458       */
459      this.technologies_ = {};
460
461      /**
462       * A map from network type to the array of IDs of network of that type.
463       * @type {Object.<string, Array.<string>>}
464       */
465      this.networkIDsByType_ = {};
466
467      /**
468       * A map from network ID to the network's properties.
469       * @type {Object.<string, Object>}
470       */
471      this.networkByID_ = {};
472
473      /**
474       * A map from network ID to the network's position in the last received
475       * network list.
476       * @type {Object.<string, number>}
477       */
478      this.networkIndexByID_ = {};
479
480      /**
481       * A function that handles the various user actions.
482       * See |setUserActionHandler|.
483       * @type {function({command: string, networkID: string})}
484       */
485      this.userActionHandler_ = function() {};
486
487      this.autoExpands = true;
488      this.dataModel = new ArrayDataModel([]);
489      this.dataModel.setCompareFunction(0, function(a, b) {
490        return ArrayDataModel.prototype.defaultValuesCompareFunction(
491            BUTTON_POSITION[a], BUTTON_POSITION[b]);
492      });
493      this.dataModel.sort(0, 'asc');
494      this.selectionModel = new ListSingleSelectionModel();
495
496      this.updateDataStructuresAndButtons_();
497      this.registerStatusListener_();
498    },
499
500    /**
501     * @override
502     */
503    createItem: function(row) {
504      var key = row[0];
505      if (key in this.technologies_) {
506        var button = new TechnologyButton(
507            this,
508            key);
509        button.update();
510        return button;
511      } else {
512        return new Button(this, key);
513      }
514    },
515
516    /**
517     * See |setUserActionHandler| for the possible commands.
518     * @param {{command: string, networkID: string}} action .
519     */
520    handleUserAction: function(action) {
521      this.userActionHandler_(action);
522    },
523
524    /**
525     * A function that handles the various user actions.
526     * |command| will be one of
527     *   - openConfiguration
528     * @param {function({command: string, networkID: string})} handler .
529     */
530    setUserActionHandler: function(handler) {
531      this.userActionHandler_ = handler;
532    },
533
534    /**
535     * @param {string} technology .
536     * @return {Array.<string>} Array of network IDs.
537     */
538    getNetworkIDsOfType: function(technology) {
539      var networkIDs = this.networkIDsByType_[technology];
540      if (!networkIDs)
541        return [];
542      return networkIDs;
543    },
544
545    /**
546     * @param {string} networkID .
547     * @return {number} The index of network with |networkID| in the last
548     * received network list.
549     */
550    getIndexOfNetworkID: function(networkID) {
551      return this.networkIndexByID_[networkID];
552    },
553
554    /**
555     * @param {string} networkID .
556     * @return {Object} The last received properties of network with
557     * |networkID|.
558     */
559    getNetworkByID: function(networkID) {
560      return this.networkByID_[networkID];
561    },
562
563    /**
564     * @param {string} networkType .
565     * @return {?TechnologyButton} .
566     */
567    getTechnologyButtonForType_: function(networkType) {
568      var buttons = this.items;
569      for (var i = 0; i < buttons.length; i++) {
570        var button = buttons[i];
571        if (button instanceof TechnologyButton &&
572            button.technology === networkType) {
573          return button;
574        }
575      }
576      console.log('TechnologyButton for type ' + networkType +
577          ' requested but not found.');
578      return null;
579    },
580
581    updateTechnologiesFromNetworks_: function() {
582      var newTechnologies = {};
583      Object.keys(this.networkIDsByType_).forEach(function(technology) {
584        newTechnologies[technology] = true;
585      });
586      this.technologies_ = newTechnologies;
587    },
588
589    updateDataStructuresAndButtons_: function() {
590      this.networkIDsByType_ = createNetworkIDsByType(this.networkByID_);
591      this.updateTechnologiesFromNetworks_();
592      var keys = Object.keys(this.technologies_);
593      // Add keys of always visible toplevel buttons.
594      keys.push('addConnection');
595      updateDataModel(this, arrayToTable(keys));
596      this.items.forEach(function(button) {
597        button.update();
598      });
599    },
600
601    /**
602     * @param {Array.<string>} networkIDs .
603     */
604    updateIndexes_: function(networkIDs) {
605      var newNetworkIndexByID = {};
606      networkIDs.forEach(function(id, index) {
607        newNetworkIndexByID[id] = index;
608      });
609      this.networkIndexByID_ = newNetworkIndexByID;
610    },
611
612    /**
613     * @param {Array.<string>} networkIDs .
614     */
615    onNetworkListChanged_: function(networkIDs) {
616      var diff = differenceBy(Object.keys(this.networkByID_),
617                              networkIDs,
618                              function(e) { return e; });
619      var toRemove = diff[0];
620      var toAdd = diff[1];
621
622      var addCallback = this.addNetworkCallback_.bind(this);
623      toAdd.forEach(function(id) {
624        console.log('NetworkStatus: Network ' + id + ' added.');
625        chrome.networkingPrivate.getProperties(id, addCallback);
626      });
627
628      toRemove.forEach(function(id) {
629        console.log('NetworkStatus: Network ' + id + ' removed.');
630        delete this.networkByID_[id];
631      }, this);
632
633      this.updateIndexes_(networkIDs);
634      this.updateDataStructuresAndButtons_();
635    },
636
637    /**
638     * @param {Array.<string>} networkIDs .
639     */
640    onNetworksChanged_: function(networkIDs) {
641      var updateCallback = this.updateNetworkCallback_.bind(this);
642      networkIDs.forEach(function(id) {
643        console.log('NetworkStatus: Network ' + id + ' changed.');
644        chrome.networkingPrivate.getProperties(id, updateCallback);
645      });
646    },
647
648    /**
649     * @param {Object} network .
650     */
651    updateNetworkCallback_: function(network) {
652      this.networkByID_[network.GUID] = network;
653      this.getTechnologyButtonForType_(network.Type).update();
654    },
655
656    /**
657     * @param {Object} network .
658     */
659    addNetworkCallback_: function(network) {
660      this.networkByID_[network.GUID] = network;
661      this.updateDataStructuresAndButtons_();
662    },
663
664    /**
665     * @param {Array.<Object>} networks .
666     */
667    setVisibleNetworks: function(networks) {
668      this.networkByID_ = createMapFromList(
669          networks,
670          function(network) {
671            return network.GUID;
672          });
673      this.updateIndexes_(networks.map(function(network) {
674        return network.GUID;
675      }));
676      this.updateDataStructuresAndButtons_();
677    },
678
679    /**
680     * Registers |this| at the networkingPrivate extension API and requests an
681     * initial list of all networks.
682     */
683    registerStatusListener_: function() {
684      chrome.networkingPrivate.onNetworkListChanged.addListener(
685          this.onNetworkListChanged_.bind(this));
686      chrome.networkingPrivate.onNetworksChanged.addListener(
687          this.onNetworksChanged_.bind(this));
688      chrome.networkingPrivate.getNetworks(
689          { 'networkType': 'All', 'visible': true },
690          this.setVisibleNetworks.bind(this));
691    }
692  };
693
694  return {
695    NetworkStatusList: NetworkStatusList
696  };
697});
698