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