Home | History | Annotate | Download | only in local_discovery
      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 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