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