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