Use chrome.app.window full-screen API for apps v2.
[chromium-blink-merge.git] / remoting / webapp / remoting.js
blob07bd8280a0a2461038005fddedd375858d89b3a9
1 // Copyright (c) 2012 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.
5 'use strict';
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
10 /** @type {remoting.HostSession} */ remoting.hostSession = null;
12 /**
13  * @type {boolean} True if this is a v2 app; false if it is a legacy app.
14  */
15 remoting.isAppsV2 = false;
17 /**
18  * Show the authorization consent UI and register a one-shot event handler to
19  * continue the authorization process.
20  *
21  * @param {function():void} authContinue Callback to invoke when the user
22  *     clicks "Continue".
23  */
24 function consentRequired_(authContinue) {
25   /** @type {HTMLElement} */
26   var dialog = document.getElementById('auth-dialog');
27   /** @type {HTMLElement} */
28   var button = document.getElementById('auth-button');
29   var consentGranted = function(event) {
30     dialog.hidden = true;
31     button.removeEventListener('click', consentGranted, false);
32     authContinue();
33   };
34   dialog.hidden = false;
35   button.addEventListener('click', consentGranted, false);
38 /**
39  * Entry point for app initialization.
40  */
41 remoting.init = function() {
42   // Determine whether or not this is a V2 web-app. In order to keep the apps
43   // v2 patch as small as possible, all JS changes needed for apps v2 are done
44   // at run-time. Only the manifest is patched.
45   var manifest = chrome.runtime.getManifest();
46   if (manifest && manifest.app && manifest.app.background) {
47     remoting.isAppsV2 = true;
48     var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
49     htmlNode.classList.add('apps-v2');
50   }
52   if (!remoting.isAppsV2) {
53     migrateLocalToChromeStorage_();
54   }
56   console.log(remoting.getExtensionInfo());
57   l10n.localize();
59   // Create global objects.
60   remoting.settings = new remoting.Settings();
61   if (remoting.isAppsV2) {
62     remoting.identity = new remoting.Identity(consentRequired_);
63     remoting.fullscreen = new remoting.FullscreenAppsV2();
64   } else {
65     remoting.oauth2 = new remoting.OAuth2();
66     if (!remoting.oauth2.isAuthenticated()) {
67       document.getElementById('auth-dialog').hidden = false;
68     }
69     remoting.identity = remoting.oauth2;
70     remoting.fullscreen = new remoting.FullscreenAppsV1();
71   }
72   remoting.stats = new remoting.ConnectionStats(
73       document.getElementById('statistics'));
74   remoting.formatIq = new remoting.FormatIq();
75   remoting.hostList = new remoting.HostList(
76       document.getElementById('host-list'),
77       document.getElementById('host-list-empty'),
78       document.getElementById('host-list-error-message'),
79       document.getElementById('host-list-refresh-failed-button'),
80       document.getElementById('host-list-loading-indicator'));
81   remoting.toolbar = new remoting.Toolbar(
82       document.getElementById('session-toolbar'));
83   remoting.clipboard = new remoting.Clipboard();
84   var sandbox = /** @type {HTMLIFrameElement} */
85       document.getElementById('wcs-sandbox');
86   remoting.wcsSandbox = new remoting.WcsSandboxContainer(sandbox.contentWindow);
87   var menuFeedback = new remoting.Feedback(
88       document.getElementById('help-feedback-main'),
89       document.getElementById('help-main'),
90       document.getElementById('send-feedback-main'));
91   var toolbarFeedback = new remoting.Feedback(
92       document.getElementById('help-feedback-toolbar'),
93       document.getElementById('help-toolbar'),
94       document.getElementById('send-feedback-toolbar'));
96   /** @param {remoting.Error} error */
97   var onGetEmailError = function(error) {
98     // No need to show the error message for NOT_AUTHENTICATED
99     // because we will show "auth-dialog".
100     if (error != remoting.Error.NOT_AUTHENTICATED) {
101       remoting.showErrorMessage(error);
102     }
103   }
104   remoting.identity.getEmail(remoting.onEmail, onGetEmailError);
106   remoting.showOrHideIT2MeUi();
107   remoting.showOrHideMe2MeUi();
109   // The plugin's onFocus handler sends a paste command to |window|, because
110   // it can't send one to the plugin element itself.
111   window.addEventListener('paste', pluginGotPaste_, false);
112   window.addEventListener('copy', pluginGotCopy_, false);
114   remoting.initModalDialogs();
116   if (isHostModeSupported_()) {
117     var noShare = document.getElementById('chrome-os-no-share');
118     noShare.parentNode.removeChild(noShare);
119   } else {
120     var button = document.getElementById('share-button');
121     button.disabled = true;
122   }
124   var onLoad = function() {
125     // Parse URL parameters.
126     var urlParams = getUrlParameters_();
127     if ('mode' in urlParams) {
128       if (urlParams['mode'] == 'me2me') {
129         var hostId = urlParams['hostId'];
130         remoting.connectMe2Me(hostId);
131         return;
132       }
133     }
134     // No valid URL parameters, start up normally.
135     remoting.initHomeScreenUi();
136   }
137   remoting.hostList.load(onLoad);
139   // For Apps v1, check the tab type to warn the user if they are not getting
140   // the best keyboard experience.
141   if (!remoting.isAppsV2 && navigator.platform.indexOf('Mac') == -1) {
142     /** @param {boolean} isWindowed */
143     var onIsWindowed = function(isWindowed) {
144       if (!isWindowed) {
145         document.getElementById('startup-mode-box-me2me').hidden = false;
146         document.getElementById('startup-mode-box-it2me').hidden = false;
147       }
148     };
149     isWindowed_(onIsWindowed);
150   }
154  * Returns whether or not IT2Me is supported via the host NPAPI plugin.
156  * @return {boolean}
157  */
158 function isIT2MeSupported_() {
159   // Currently, IT2Me on Chromebooks is not supported.
160   return !remoting.runningOnChromeOS();
164  * Create an instance of the NPAPI plugin.
165  * @param {Element} container The element to add the plugin to.
166  * @return {remoting.HostPlugin} The new plugin instance or null if it failed to
167  *     load.
168  */
169 remoting.createNpapiPlugin = function(container) {
170   var plugin = document.createElement('embed');
171   plugin.type = remoting.settings.PLUGIN_MIMETYPE;
172   // Hiding the plugin means it doesn't load, so make it size zero instead.
173   plugin.width = 0;
174   plugin.height = 0;
175   container.appendChild(plugin);
177   // Verify if the plugin was loaded successfully.
178   if (!plugin.hasOwnProperty('REQUESTED_ACCESS_CODE')) {
179     container.removeChild(plugin);
180     return null;
181   }
183   return /** @type {remoting.HostPlugin} */ (plugin);
187  * Returns true if the current platform is fully supported. It's only used when
188  * we detect that host native messaging components are not installed. In that
189  * case the result of this function determines if the webapp should show the
190  * controls that allow to install and enable Me2Me host.
192  * @return {boolean}
193  */
194 remoting.isMe2MeInstallable = function() {
195   /** @type {string} */
196   var platform = navigator.platform;
197   // The chromoting host is currently not installable on ChromeOS.
198   // For Linux, we have a install package for Ubuntu but not other distros.
199   // Since we cannot tell from javascript alone the Linux distro the client is
200   // on, we don't show the daemon-control UI for Linux unless the host is
201   // installed.
202   return platform == 'Win32' || platform == 'MacIntel';
206  * Display the user's email address and allow access to the rest of the app,
207  * including parsing URL parameters.
209  * @param {string} email The user's email address.
210  * @return {void} Nothing.
211  */
212 remoting.onEmail = function(email) {
213   document.getElementById('current-email').innerText = email;
214   document.getElementById('get-started-it2me').disabled = false;
215   document.getElementById('get-started-me2me').disabled = false;
219  * initHomeScreenUi is called if the app is not starting up in session mode,
220  * and also if the user cancels pin entry or the connection in session mode.
221  */
222 remoting.initHomeScreenUi = function() {
223   remoting.hostController = new remoting.HostController();
224   document.getElementById('share-button').disabled = !isIT2MeSupported_();
225   remoting.setMode(remoting.AppMode.HOME);
226   remoting.hostSetupDialog =
227       new remoting.HostSetupDialog(remoting.hostController);
228   var dialog = document.getElementById('paired-clients-list');
229   var message = document.getElementById('paired-client-manager-message');
230   var deleteAll = document.getElementById('delete-all-paired-clients');
231   var close = document.getElementById('close-paired-client-manager-dialog');
232   var working = document.getElementById('paired-client-manager-dialog-working');
233   var error = document.getElementById('paired-client-manager-dialog-error');
234   var noPairedClients = document.getElementById('no-paired-clients');
235   remoting.pairedClientManager =
236       new remoting.PairedClientManager(remoting.hostController, dialog, message,
237                                        deleteAll, close, noPairedClients,
238                                        working, error);
239   // Display the cached host list, then asynchronously update and re-display it.
240   remoting.updateLocalHostState();
241   remoting.hostList.refresh(remoting.updateLocalHostState);
242   remoting.butterBar = new remoting.ButterBar();
246  * Fetches local host state and updates the DOM accordingly.
247  */
248 remoting.updateLocalHostState = function() {
249   /**
250    * @param {remoting.HostController.State} state Host state.
251    */
252   var onHostState = function(state) {
253     if (state == remoting.HostController.State.STARTED) {
254       remoting.hostController.getLocalHostId(onHostId.bind(null, state));
255     } else {
256       onHostId(state, null);
257     }
258   };
260   /**
261    * @param {remoting.HostController.State} state Host state.
262    * @param {string?} hostId Host id.
263    */
264   var onHostId = function(state, hostId) {
265     remoting.hostList.setLocalHostStateAndId(state, hostId);
266     remoting.hostList.display();
267   };
269   /**
270    * @param {boolean} response True if the feature is present.
271    */
272   var onHasFeatureResponse = function(response) {
273     /**
274      * @param {remoting.Error} error
275      */
276     var onError = function(error) {
277       console.error('Failed to get pairing status: ' + error);
278       remoting.pairedClientManager.setPairedClients([]);
279     };
281     if (response) {
282       remoting.hostController.getPairedClients(
283           remoting.pairedClientManager.setPairedClients.bind(
284               remoting.pairedClientManager),
285           onError);
286     } else {
287       console.log('Pairing registry not supported by host.');
288       remoting.pairedClientManager.setPairedClients([]);
289     }
290   };
292   remoting.hostController.hasFeature(
293       remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse);
294   remoting.hostController.getLocalHostState(onHostState);
298  * @return {string} Information about the current extension.
299  */
300 remoting.getExtensionInfo = function() {
301   var v2OrLegacy = remoting.isAppsV2 ? " (v2)" : " (legacy)";
302   var manifest = chrome.runtime.getManifest();
303   if (manifest && manifest.version) {
304     var name = chrome.i18n.getMessage('PRODUCT_NAME');
305     return name + ' version: ' + manifest.version + v2OrLegacy;
306   } else {
307     return 'Failed to get product version. Corrupt manifest?';
308   }
312  * If an IT2Me client or host is active then prompt the user before closing.
313  * If a Me2Me client is active then don't bother, since closing the window is
314  * the more intuitive way to end a Me2Me session, and re-connecting is easy.
315  */
316 remoting.promptClose = function() {
317   if (!remoting.clientSession ||
318       remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) {
319     return null;
320   }
321   switch (remoting.currentMode) {
322     case remoting.AppMode.CLIENT_CONNECTING:
323     case remoting.AppMode.HOST_WAITING_FOR_CODE:
324     case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
325     case remoting.AppMode.HOST_SHARED:
326     case remoting.AppMode.IN_SESSION:
327       return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
328     default:
329       return null;
330   }
334  * Sign the user out of Chromoting by clearing (and revoking, if possible) the
335  * OAuth refresh token.
337  * Also clear all local storage, to avoid leaking information.
338  */
339 remoting.signOut = function() {
340   remoting.oauth2.clear();
341   chrome.storage.local.clear();
342   remoting.setMode(remoting.AppMode.HOME);
343   document.getElementById('auth-dialog').hidden = false;
347  * Returns whether the app is running on ChromeOS.
349  * @return {boolean} True if the app is running on ChromeOS.
350  */
351 remoting.runningOnChromeOS = function() {
352   return !!navigator.userAgent.match(/\bCrOS\b/);
356  * Callback function called when the browser window gets a paste operation.
358  * @param {Event} eventUncast
359  * @return {void} Nothing.
360  */
361 function pluginGotPaste_(eventUncast) {
362   var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
363   if (event && event.clipboardData) {
364     remoting.clipboard.toHost(event.clipboardData);
365   }
369  * Callback function called when the browser window gets a copy operation.
371  * @param {Event} eventUncast
372  * @return {void} Nothing.
373  */
374 function pluginGotCopy_(eventUncast) {
375   var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
376   if (event && event.clipboardData) {
377     if (remoting.clipboard.toOs(event.clipboardData)) {
378       // The default action may overwrite items that we added to clipboardData.
379       event.preventDefault();
380     }
381   }
385  * Returns whether Host mode is supported on this platform.
387  * @return {boolean} True if Host mode is supported.
388  */
389 function isHostModeSupported_() {
390   // Currently, sharing on Chromebooks is not supported.
391   return !remoting.runningOnChromeOS();
395  * @return {Object.<string, string>} The URL parameters.
396  */
397 function getUrlParameters_() {
398   var result = {};
399   var parts = window.location.search.substring(1).split('&');
400   for (var i = 0; i < parts.length; i++) {
401     var pair = parts[i].split('=');
402     result[pair[0]] = decodeURIComponent(pair[1]);
403   }
404   return result;
408  * @param {string} jsonString A JSON-encoded string.
409  * @return {*} The decoded object, or undefined if the string cannot be parsed.
410  */
411 function jsonParseSafe(jsonString) {
412   try {
413     return JSON.parse(jsonString);
414   } catch (err) {
415     return undefined;
416   }
420  * Return the current time as a formatted string suitable for logging.
422  * @return {string} The current time, formatted as [mmdd/hhmmss.xyz]
423  */
424 remoting.timestamp = function() {
425   /**
426    * @param {number} num A number.
427    * @param {number} len The required length of the answer.
428    * @return {string} The number, formatted as a string of the specified length
429    *     by prepending zeroes as necessary.
430    */
431   var pad = function(num, len) {
432     var result = num.toString();
433     if (result.length < len) {
434       result = new Array(len - result.length + 1).join('0') + result;
435     }
436     return result;
437   };
438   var now = new Date();
439   var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' +
440       pad(now.getHours(), 2) + pad(now.getMinutes(), 2) +
441       pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
442   return '[' + timestamp + ']';
446  * Show an error message, optionally including a short-cut for signing in to
447  * Chromoting again.
449  * @param {remoting.Error} error
450  * @return {void} Nothing.
451  */
452 remoting.showErrorMessage = function(error) {
453   l10n.localizeElementFromTag(
454       document.getElementById('token-refresh-error-message'),
455       error);
456   var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
457   document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
458   document.getElementById('token-refresh-other-error').hidden = auth_failed;
459   remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
463  * Determine whether or not the app is running in a window.
464  * @param {function(boolean):void} callback Callback to receive whether or not
465  *     the current tab is running in windowed mode.
466  */
467 function isWindowed_(callback) {
468   /** @param {chrome.Window} win The current window. */
469   var windowCallback = function(win) {
470     callback(win.type == 'popup');
471   };
472   /** @param {chrome.Tab} tab The current tab. */
473   var tabCallback = function(tab) {
474     if (tab.pinned) {
475       callback(false);
476     } else {
477       chrome.windows.get(tab.windowId, null, windowCallback);
478     }
479   };
480   if (chrome.tabs) {
481     chrome.tabs.getCurrent(tabCallback);
482   } else {
483     console.error('chome.tabs is not available.');
484   }
488  * Migrate settings in window.localStorage to chrome.storage.local so that
489  * users of older web-apps that used the former do not lose their settings.
490  */
491 function migrateLocalToChromeStorage_() {
492   // The OAuth2 class still uses window.localStorage, so don't migrate any of
493   // those settings.
494   var oauthSettings = [
495       'oauth2-refresh-token',
496       'oauth2-refresh-token-revokable',
497       'oauth2-access-token',
498       'oauth2-xsrf-token',
499       'remoting-email'
500   ];
501   for (var setting in window.localStorage) {
502     if (oauthSettings.indexOf(setting) == -1) {
503       var copy = {}
504       copy[setting] = window.localStorage.getItem(setting);
505       chrome.storage.local.set(copy);
506       window.localStorage.removeItem(setting);
507     }
508   }
512  * Generate a nonce, to be used as an xsrf protection token.
514  * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
515 remoting.generateXsrfToken = function() {
516   var random = new Uint8Array(16);
517   window.crypto.getRandomValues(random);
518   var base64Token = window.btoa(String.fromCharCode.apply(null, random));
519   return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');