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   /**
     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