1// Copyright 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 5/** 6 * Javascript for local_discovery.html, served from chrome://devices/ 7 * This is used to show discoverable devices near the user as well as 8 * cloud devices registered to them. 9 * 10 * The object defined in this javascript file listens for callbacks from the 11 * C++ code saying that a new device is available as well as manages the UI for 12 * registering a device on the local network. 13 */ 14 15cr.define('local_discovery', function() { 16 'use strict'; 17 18 // Histogram buckets for UMA tracking. 19 /** @const */ var DEVICES_PAGE_EVENTS = { 20 OPENED: 0, 21 LOG_IN_STARTED_FROM_REGISTER_PROMO: 1, 22 LOG_IN_STARTED_FROM_DEVICE_LIST_PROMO: 2, 23 ADD_PRINTER_CLICKED: 3, 24 REGISTER_CLICKED: 4, 25 REGISTER_CONFIRMED: 5, 26 REGISTER_SUCCESS: 6, 27 REGISTER_CANCEL: 7, 28 REGISTER_FAILURE: 8, 29 MANAGE_CLICKED: 9, 30 REGISTER_CANCEL_ON_PRINTER: 10, 31 REGISTER_TIMEOUT: 11, 32 LOG_IN_STARTED_FROM_REGISTER_OVERLAY_PROMO: 12, 33 MAX_EVENT: 13, 34 }; 35 36 /** 37 * Map of service names to corresponding service objects. 38 * @type {Object.<string,Service>} 39 */ 40 var devices = {}; 41 42 /** 43 * Whether or not the user is currently logged in. 44 * @type bool 45 */ 46 var isUserLoggedIn = true; 47 48 /** 49 * Whether or not the user is supervised or off the record. 50 * @type bool 51 */ 52 var isUserSupervisedOrOffTheRecord = false; 53 54 /** 55 * Whether or not the path-based dialog has been shown. 56 * @type bool 57 */ 58 var dialogFromPathHasBeenShown = false; 59 60 /** 61 * Focus manager for page. 62 */ 63 var focusManager = null; 64 65 /** 66 * Object that represents a device in the device list. 67 * @param {Object} info Information about the device. 68 * @constructor 69 */ 70 function Device(info, registerEnabled) { 71 this.info = info; 72 this.domElement = null; 73 this.registerButton = null; 74 this.registerEnabled = registerEnabled; 75 } 76 77 Device.prototype = { 78 /** 79 * Update the device. 80 * @param {Object} info New information about the device. 81 */ 82 updateDevice: function(info) { 83 this.info = info; 84 this.renderDevice(); 85 }, 86 87 /** 88 * Delete the device. 89 */ 90 removeDevice: function() { 91 this.deviceContainer().removeChild(this.domElement); 92 }, 93 94 /** 95 * Render the device to the device list. 96 */ 97 renderDevice: function() { 98 if (this.domElement) { 99 clearElement(this.domElement); 100 } else { 101 this.domElement = document.createElement('div'); 102 this.deviceContainer().appendChild(this.domElement); 103 } 104 105 this.registerButton = fillDeviceDescription( 106 this.domElement, 107 this.info.display_name, 108 this.info.description, 109 this.info.type, 110 loadTimeData.getString('serviceRegister'), 111 this.showRegister.bind(this, this.info.type)); 112 113 this.setRegisterEnabled(this.registerEnabled); 114 }, 115 116 /** 117 * Return the correct container for the device. 118 * @param {boolean} is_mine Whether or not the device is in the 'Registered' 119 * section. 120 */ 121 deviceContainer: function() { 122 return $('register-device-list'); 123 }, 124 /** 125 * Register the device. 126 */ 127 register: function() { 128 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CONFIRMED); 129 chrome.send('registerDevice', [this.info.service_name]); 130 setRegisterPage(isPrinter(this.info.type) ? 131 'register-printer-page-adding1' : 'register-device-page-adding1'); 132 }, 133 /** 134 * Show registrtation UI for device. 135 */ 136 showRegister: function() { 137 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CLICKED); 138 $('register-message').textContent = loadTimeData.getStringF( 139 isPrinter(this.info.type) ? 'registerPrinterConfirmMessage' : 140 'registerDeviceConfirmMessage', 141 this.info.display_name); 142 $('register-continue-button').onclick = this.register.bind(this); 143 showRegisterOverlay(); 144 }, 145 /** 146 * Set registration button enabled/disabled 147 */ 148 setRegisterEnabled: function(isEnabled) { 149 this.registerEnabled = isEnabled; 150 if (this.registerButton) { 151 this.registerButton.disabled = !isEnabled; 152 } 153 } 154 }; 155 156 /** 157 * Manages focus for local devices page. 158 * @constructor 159 * @extends {cr.ui.FocusManager} 160 */ 161 function LocalDiscoveryFocusManager() { 162 cr.ui.FocusManager.call(this); 163 this.focusParent_ = document.body; 164 } 165 166 LocalDiscoveryFocusManager.prototype = { 167 __proto__: cr.ui.FocusManager.prototype, 168 /** @override */ 169 getFocusParent: function() { 170 return document.querySelector('#overlay .showing') || 171 $('main-page'); 172 } 173 }; 174 175 /** 176 * Returns a textual representation of the number of printers on the network. 177 * @return {string} Number of printers on the network as localized string. 178 */ 179 function generateNumberPrintersAvailableText(numberPrinters) { 180 if (numberPrinters == 0) { 181 return loadTimeData.getString('printersOnNetworkZero'); 182 } else if (numberPrinters == 1) { 183 return loadTimeData.getString('printersOnNetworkOne'); 184 } else { 185 return loadTimeData.getStringF('printersOnNetworkMultiple', 186 numberPrinters); 187 } 188 } 189 190 /** 191 * Fill device element with the description of a device. 192 * @param {HTMLElement} device_dom_element Element to be filled. 193 * @param {string} name Name of device. 194 * @param {string} description Description of device. 195 * @param {string} type Type of device. 196 * @param {string} button_text Text to appear on button. 197 * @param {function()?} button_action Action for button. 198 * @return {HTMLElement} The button (for enabling/disabling/rebinding) 199 */ 200 function fillDeviceDescription(device_dom_element, 201 name, 202 description, 203 type, 204 button_text, 205 button_action) { 206 device_dom_element.classList.add('device'); 207 if (isPrinter(type)) 208 device_dom_element.classList.add('printer'); 209 210 var deviceInfo = document.createElement('div'); 211 deviceInfo.className = 'device-info'; 212 device_dom_element.appendChild(deviceInfo); 213 214 var deviceName = document.createElement('h3'); 215 deviceName.className = 'device-name'; 216 deviceName.textContent = name; 217 deviceInfo.appendChild(deviceName); 218 219 var deviceDescription = document.createElement('div'); 220 deviceDescription.className = 'device-subline'; 221 deviceDescription.textContent = description; 222 deviceInfo.appendChild(deviceDescription); 223 224 if (button_action) { 225 var button = document.createElement('button'); 226 button.textContent = button_text; 227 button.addEventListener('click', button_action); 228 device_dom_element.appendChild(button); 229 } 230 231 return button; 232 } 233 234 /** 235 * Show the register overlay. 236 */ 237 function showRegisterOverlay() { 238 recordUmaEvent(DEVICES_PAGE_EVENTS.ADD_PRINTER_CLICKED); 239 240 var registerOverlay = $('register-overlay'); 241 registerOverlay.classList.add('showing'); 242 registerOverlay.focus(); 243 244 $('overlay').hidden = false; 245 setRegisterPage('register-page-confirm'); 246 } 247 248 /** 249 * Hide the register overlay. 250 */ 251 function hideRegisterOverlay() { 252 $('register-overlay').classList.remove('showing'); 253 $('overlay').hidden = true; 254 } 255 256 /** 257 * Clear a DOM element of all children. 258 * @param {HTMLElement} element DOM element to clear. 259 */ 260 function clearElement(element) { 261 while (element.firstChild) { 262 element.removeChild(element.firstChild); 263 } 264 } 265 266 /** 267 * Announce that a registration failed. 268 */ 269 function onRegistrationFailed() { 270 $('error-message').textContent = 271 loadTimeData.getString('addingErrorMessage'); 272 setRegisterPage('register-page-error'); 273 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_FAILURE); 274 } 275 276 /** 277 * Announce that a registration has been canceled on the printer. 278 */ 279 function onRegistrationCanceledPrinter() { 280 $('error-message').textContent = 281 loadTimeData.getString('addingCanceledMessage'); 282 setRegisterPage('register-page-error'); 283 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CANCEL_ON_PRINTER); 284 } 285 286 /** 287 * Announce that a registration has timed out. 288 */ 289 function onRegistrationTimeout() { 290 $('error-message').textContent = 291 loadTimeData.getString('addingTimeoutMessage'); 292 setRegisterPage('register-page-error'); 293 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_TIMEOUT); 294 } 295 296 /** 297 * Update UI to reflect that registration has been confirmed on the printer. 298 */ 299 function onRegistrationConfirmedOnPrinter() { 300 setRegisterPage('register-printer-page-adding2'); 301 } 302 303 /** 304 * Shows UI to confirm security code. 305 * @param {string} code The security code to confirm. 306 */ 307 function onRegistrationConfirmDeviceCode(code) { 308 setRegisterPage('register-device-page-adding2'); 309 $('register-device-page-code').textContent = code; 310 } 311 312 /** 313 * Update device unregistered device list, and update related strings to 314 * reflect the number of devices available to register. 315 * @param {string} name Name of the device. 316 * @param {string} info Additional info of the device or null if the device 317 * has been removed. 318 */ 319 function onUnregisteredDeviceUpdate(name, info) { 320 if (info) { 321 if (devices.hasOwnProperty(name)) { 322 devices[name].updateDevice(info); 323 } else { 324 devices[name] = new Device(info, isUserLoggedIn); 325 devices[name].renderDevice(); 326 } 327 328 if (name == getOverlayIDFromPath() && !dialogFromPathHasBeenShown) { 329 dialogFromPathHasBeenShown = true; 330 devices[name].showRegister(); 331 } 332 } else { 333 if (devices.hasOwnProperty(name)) { 334 devices[name].removeDevice(); 335 delete devices[name]; 336 } 337 } 338 339 updateUIToReflectState(); 340 } 341 342 /** 343 * Create the DOM for a cloud device described by the device section. 344 * @param {Array.<Object>} devices_list List of devices. 345 */ 346 function createCloudDeviceDOM(device) { 347 var devicesDomElement = document.createElement('div'); 348 349 var description; 350 if (device.description == '') { 351 if (isPrinter(device.type)) 352 description = loadTimeData.getString('noDescriptionPrinter'); 353 else 354 description = loadTimeData.getString('noDescriptionDevice'); 355 } else { 356 description = device.description; 357 } 358 359 fillDeviceDescription(devicesDomElement, device.display_name, 360 description, device.type, 361 loadTimeData.getString('manageDevice'), 362 isPrinter(device.type) ? 363 manageCloudDevice.bind(null, device.id) : null); 364 return devicesDomElement; 365 } 366 367 /** 368 * Handle a list of cloud devices available to the user globally. 369 * @param {Array.<Object>} devices_list List of devices. 370 */ 371 function onCloudDeviceListAvailable(devices_list) { 372 var devicesListLength = devices_list.length; 373 var devicesContainer = $('cloud-devices'); 374 375 clearElement(devicesContainer); 376 $('cloud-devices-loading').hidden = true; 377 378 for (var i = 0; i < devicesListLength; i++) { 379 devicesContainer.appendChild(createCloudDeviceDOM(devices_list[i])); 380 } 381 } 382 383 /** 384 * Handle the case where the list of cloud devices is not available. 385 */ 386 function onCloudDeviceListUnavailable() { 387 if (isUserLoggedIn) { 388 $('cloud-devices-loading').hidden = true; 389 $('cloud-devices-unavailable').hidden = false; 390 } 391 } 392 393 /** 394 * Handle the case where the cache for local devices has been flushed.. 395 */ 396 function onDeviceCacheFlushed() { 397 for (var deviceName in devices) { 398 devices[deviceName].removeDevice(); 399 delete devices[deviceName]; 400 } 401 402 updateUIToReflectState(); 403 } 404 405 /** 406 * Update UI strings to reflect the number of local devices. 407 */ 408 function updateUIToReflectState() { 409 var numberPrinters = $('register-device-list').children.length; 410 if (numberPrinters == 0) { 411 $('no-printers-message').hidden = false; 412 413 $('register-login-promo').hidden = true; 414 } else { 415 $('no-printers-message').hidden = true; 416 $('register-login-promo').hidden = isUserLoggedIn || 417 isUserSupervisedOrOffTheRecord; 418 } 419 } 420 421 /** 422 * Announce that a registration succeeeded. 423 */ 424 function onRegistrationSuccess(device_data) { 425 hideRegisterOverlay(); 426 427 if (device_data.service_name == getOverlayIDFromPath()) { 428 window.close(); 429 } 430 431 var deviceDOM = createCloudDeviceDOM(device_data); 432 $('cloud-devices').insertBefore(deviceDOM, $('cloud-devices').firstChild); 433 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_SUCCESS); 434 } 435 436 /** 437 * Update visibility status for page. 438 */ 439 function updateVisibility() { 440 chrome.send('isVisible', [!document.hidden]); 441 } 442 443 /** 444 * Set the page that the register wizard is on. 445 * @param {string} page_id ID string for page. 446 */ 447 function setRegisterPage(page_id) { 448 var pages = $('register-overlay').querySelectorAll('.register-page'); 449 var pagesLength = pages.length; 450 for (var i = 0; i < pagesLength; i++) { 451 pages[i].hidden = true; 452 } 453 454 $(page_id).hidden = false; 455 } 456 457 /** 458 * Request the device list. 459 */ 460 function requestDeviceList() { 461 if (isUserLoggedIn) { 462 clearElement($('cloud-devices')); 463 $('cloud-devices-loading').hidden = false; 464 $('cloud-devices-unavailable').hidden = true; 465 466 chrome.send('requestDeviceList'); 467 } 468 } 469 470 /** 471 * Go to management page for a cloud device. 472 * @param {string} device_id ID of device. 473 */ 474 function manageCloudDevice(device_id) { 475 recordUmaEvent(DEVICES_PAGE_EVENTS.MANAGE_CLICKED); 476 chrome.send('openCloudPrintURL', [device_id]); 477 } 478 479 /** 480 * Record an event in the UMA histogram. 481 * @param {number} eventId The id of the event to be recorded. 482 * @private 483 */ 484 function recordUmaEvent(eventId) { 485 chrome.send('metricsHandler:recordInHistogram', 486 ['LocalDiscovery.DevicesPage', eventId, DEVICES_PAGE_EVENTS.MAX_EVENT]); 487 } 488 489 /** 490 * Cancel the registration. 491 */ 492 function cancelRegistration() { 493 hideRegisterOverlay(); 494 chrome.send('cancelRegistration'); 495 recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CANCEL); 496 } 497 498 /** 499 * Confirms device code. 500 */ 501 function confirmCode() { 502 chrome.send('confirmCode'); 503 setRegisterPage('register-device-page-adding1'); 504 } 505 506 /** 507 * Retry loading the devices from Google Cloud Print. 508 */ 509 function retryLoadCloudDevices() { 510 requestDeviceList(); 511 } 512 513 /** 514 * User is not logged in. 515 */ 516 function setUserLoggedIn(userLoggedIn, userSupervisedOrOffTheRecord) { 517 isUserLoggedIn = userLoggedIn; 518 isUserSupervisedOrOffTheRecord = userSupervisedOrOffTheRecord; 519 520 $('cloud-devices-login-promo').hidden = isUserLoggedIn || 521 isUserSupervisedOrOffTheRecord; 522 $('register-overlay-login-promo').hidden = isUserLoggedIn || 523 isUserSupervisedOrOffTheRecord; 524 $('register-continue-button').disabled = !isUserLoggedIn || 525 isUserSupervisedOrOffTheRecord; 526 527 $('my-devices-container').hidden = userSupervisedOrOffTheRecord; 528 529 if (isUserSupervisedOrOffTheRecord) { 530 $('cloud-print-connector-section').hidden = true; 531 } 532 533 if (isUserLoggedIn && !isUserSupervisedOrOffTheRecord) { 534 requestDeviceList(); 535 $('register-login-promo').hidden = true; 536 } else { 537 $('cloud-devices-loading').hidden = true; 538 $('cloud-devices-unavailable').hidden = true; 539 clearElement($('cloud-devices')); 540 hideRegisterOverlay(); 541 } 542 543 updateUIToReflectState(); 544 545 for (var device in devices) { 546 devices[device].setRegisterEnabled(isUserLoggedIn); 547 } 548 } 549 550 function openSignInPage() { 551 chrome.send('showSyncUI'); 552 } 553 554 function registerLoginButtonClicked() { 555 recordUmaEvent(DEVICES_PAGE_EVENTS.LOG_IN_STARTED_FROM_REGISTER_PROMO); 556 openSignInPage(); 557 } 558 559 function registerOverlayLoginButtonClicked() { 560 recordUmaEvent( 561 DEVICES_PAGE_EVENTS.LOG_IN_STARTED_FROM_REGISTER_OVERLAY_PROMO); 562 openSignInPage(); 563 } 564 565 function cloudDevicesLoginButtonClicked() { 566 recordUmaEvent(DEVICES_PAGE_EVENTS.LOG_IN_STARTED_FROM_DEVICE_LIST_PROMO); 567 openSignInPage(); 568 } 569 570 /** 571 * Set the Cloud Print proxy UI to enabled, disabled, or processing. 572 * @private 573 */ 574 function setupCloudPrintConnectorSection(disabled, label, allowed) { 575 if (!cr.isChromeOS) { 576 $('cloudPrintConnectorLabel').textContent = label; 577 if (disabled || !allowed) { 578 $('cloudPrintConnectorSetupButton').textContent = 579 loadTimeData.getString('cloudPrintConnectorDisabledButton'); 580 } else { 581 $('cloudPrintConnectorSetupButton').textContent = 582 loadTimeData.getString('cloudPrintConnectorEnabledButton'); 583 } 584 $('cloudPrintConnectorSetupButton').disabled = !allowed; 585 586 if (disabled) { 587 $('cloudPrintConnectorSetupButton').onclick = function(event) { 588 // Disable the button, set its text to the intermediate state. 589 $('cloudPrintConnectorSetupButton').textContent = 590 loadTimeData.getString('cloudPrintConnectorEnablingButton'); 591 $('cloudPrintConnectorSetupButton').disabled = true; 592 chrome.send('showCloudPrintSetupDialog'); 593 }; 594 } else { 595 $('cloudPrintConnectorSetupButton').onclick = function(event) { 596 chrome.send('disableCloudPrintConnector'); 597 requestDeviceList(); 598 }; 599 } 600 } 601 } 602 603 function removeCloudPrintConnectorSection() { 604 if (!cr.isChromeOS) { 605 var connectorSectionElm = $('cloud-print-connector-section'); 606 if (connectorSectionElm) 607 connectorSectionElm.parentNode.removeChild(connectorSectionElm); 608 } 609 } 610 611 function getOverlayIDFromPath() { 612 if (document.location.pathname == '/register') { 613 var params = parseQueryParams(document.location); 614 return params['id'] || null; 615 } 616 } 617 618 /** 619 * Returns true of device is printer. 620 * @param {string} type Type of printer. 621 */ 622 function isPrinter(type) { 623 return type == 'printer'; 624 } 625 626 document.addEventListener('DOMContentLoaded', function() { 627 cr.ui.overlay.setupOverlay($('overlay')); 628 cr.ui.overlay.globalInitialization(); 629 $('overlay').addEventListener('cancelOverlay', cancelRegistration); 630 631 [].forEach.call( 632 document.querySelectorAll('.register-cancel'), function(button) { 633 button.addEventListener('click', cancelRegistration); 634 }); 635 636 [].forEach.call( 637 document.querySelectorAll('.confirm-code'), function(button) { 638 button.addEventListener('click', confirmCode); 639 }); 640 641 $('register-error-exit').addEventListener('click', cancelRegistration); 642 643 644 $('cloud-devices-retry-button').addEventListener('click', 645 retryLoadCloudDevices); 646 647 $('cloud-devices-login-button').addEventListener( 648 'click', 649 cloudDevicesLoginButtonClicked); 650 651 $('register-login-button').addEventListener( 652 'click', 653 registerLoginButtonClicked); 654 655 $('register-overlay-login-button').addEventListener( 656 'click', 657 registerOverlayLoginButtonClicked); 658 659 if (loadTimeData.valueExists('backButtonURL')) { 660 $('back-button').hidden = false; 661 $('back-button').addEventListener('click', function() { 662 window.location.href = loadTimeData.getString('backButtonURL'); 663 }); 664 } 665 666 updateVisibility(); 667 document.addEventListener('visibilitychange', updateVisibility, false); 668 669 focusManager = new LocalDiscoveryFocusManager(); 670 focusManager.initialize(); 671 672 chrome.send('start'); 673 recordUmaEvent(DEVICES_PAGE_EVENTS.OPENED); 674 }); 675 676 return { 677 onRegistrationSuccess: onRegistrationSuccess, 678 onRegistrationFailed: onRegistrationFailed, 679 onUnregisteredDeviceUpdate: onUnregisteredDeviceUpdate, 680 onRegistrationConfirmedOnPrinter: onRegistrationConfirmedOnPrinter, 681 onRegistrationConfirmDeviceCode: onRegistrationConfirmDeviceCode, 682 onCloudDeviceListAvailable: onCloudDeviceListAvailable, 683 onCloudDeviceListUnavailable: onCloudDeviceListUnavailable, 684 onDeviceCacheFlushed: onDeviceCacheFlushed, 685 onRegistrationCanceledPrinter: onRegistrationCanceledPrinter, 686 onRegistrationTimeout: onRegistrationTimeout, 687 setUserLoggedIn: setUserLoggedIn, 688 setupCloudPrintConnectorSection: setupCloudPrintConnectorSection, 689 removeCloudPrintConnectorSection: removeCloudPrintConnectorSection 690 }; 691}); 692