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