1 // Copyright 2014 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.
7 * This class implements the functionality that is specific to application
8 * remoting ("AppRemoting" or AR).
13 /** @suppress {duplicate} */
14 var remoting = remoting || {};
17 * @param {remoting.Application} app The main app that owns this delegate.
19 * @implements {remoting.Application.Delegate}
21 remoting.AppRemoting = function(app) {
22 app.setDelegate(this);
25 * @type {remoting.ApplicationContextMenu}
28 this.contextMenu_ = null;
31 * @type {remoting.KeyboardLayoutsMenu}
34 this.keyboardLayoutsMenu_ = null;
37 * @type {remoting.WindowActivationMenu}
40 this.windowActivationMenu_ = null;
46 this.pingTimerId_ = 0;
50 * Type definition for the RunApplicationResponse returned by the API.
55 remoting.AppRemoting.AppHostResponse = function() {
61 this.authorizationCode = '';
63 this.sharedSecret = '';
74 * Callback for when the userinfo (email and user name) is available from
77 * @param {string} email The user's email address.
78 * @param {string} fullName The user's full name.
79 * @return {void} Nothing.
81 remoting.onUserInfoAvailable = function(email, fullName) {
85 * Initialize the application and register all event handlers. After this
86 * is called, the app is running and waiting for user events.
88 * @param {remoting.SessionConnector} connector
89 * @return {void} Nothing.
91 remoting.AppRemoting.prototype.init = function(connector) {
92 remoting.initGlobalObjects();
93 remoting.initIdentity(remoting.onUserInfoAvailable);
95 // TODO(jamiewalch): Remove ClientSession's dependency on remoting.fullscreen
96 // so that this is no longer required.
97 remoting.fullscreen = new remoting.FullscreenAppsV2();
99 var restoreHostWindows = function() {
100 if (remoting.clientSession) {
101 remoting.clientSession.sendClientMessage('restoreAllWindows', '');
104 chrome.app.window.current().onRestored.addListener(restoreHostWindows);
106 remoting.windowShape.updateClientWindowShape();
108 // Initialize the context menus.
109 if (remoting.platformIsChromeOS()) {
110 var adapter = new remoting.ContextMenuChrome();
112 var adapter = new remoting.ContextMenuDom(
113 document.getElementById('context-menu'));
115 this.contextMenu_ = new remoting.ApplicationContextMenu(adapter);
116 this.keyboardLayoutsMenu_ = new remoting.KeyboardLayoutsMenu(adapter);
117 this.windowActivationMenu_ = new remoting.WindowActivationMenu(adapter);
119 /** @type {remoting.AppRemoting} */
122 /** @param {XMLHttpRequest} xhr */
123 var parseAppHostResponse = function(xhr) {
124 if (xhr.status == 200) {
125 var response = /** @type {remoting.AppRemoting.AppHostResponse} */
126 (base.jsonParseSafe(xhr.responseText));
129 response.status == 'done' &&
131 response.authorizationCode &&
132 response.sharedSecret &&
134 response.host.hostId) {
135 var hostJid = response.hostJid;
136 that.contextMenu_.setHostId(response.host.hostId);
137 var host = new remoting.Host;
138 host.hostId = response.host.hostId;
139 host.jabberId = hostJid;
140 host.authorizationCode = response.authorizationCode;
141 host.sharedSecret = response.sharedSecret;
143 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
145 var idleDetector = new remoting.IdleDetector(
146 document.getElementById('idle-dialog'),
147 remoting.disconnect);
150 * @param {string} tokenUrl Token-issue URL received from the host.
151 * @param {string} hostPublicKey Host public key (DER and Base64
153 * @param {string} scope OAuth scope to request the token for.
154 * @param {function(string, string):void} onThirdPartyTokenFetched
157 var fetchThirdPartyToken = function(
158 tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) {
159 // Use the authentication tokens returned by the app-remoting server.
160 onThirdPartyTokenFetched(host['authorizationCode'],
161 host['sharedSecret']);
164 connector.connectMe2App(host, fetchThirdPartyToken);
165 } else if (response && response.status == 'pending') {
166 that.handleError(remoting.Error.SERVICE_UNAVAILABLE);
169 console.error('Invalid "runApplication" response from server.');
170 // TODO(garykac) Start using remoting.Error.fromHttpStatus once it has
171 // been updated to properly report 'unknown' errors (rather than
172 // reporting them as AUTHENTICATION_FAILED).
173 if (xhr.status == 0) {
174 that.handleError(remoting.Error.NETWORK_FAILURE);
175 } else if (xhr.status == 401) {
176 that.handleError(remoting.Error.AUTHENTICATION_FAILED);
177 } else if (xhr.status == 403) {
178 that.handleError(remoting.Error.APP_NOT_AUTHORIZED);
179 } else if (xhr.status == 502 || xhr.status == 503) {
180 that.handleError(remoting.Error.SERVICE_UNAVAILABLE);
182 that.handleError(remoting.Error.UNEXPECTED);
187 /** @param {string} token */
188 var getAppHost = function(token) {
191 url: that.runApplicationUrl(),
192 onDone: parseAppHostResponse,
197 /** @param {remoting.Error} error */
198 var onError = function(error) {
199 that.handleError(error);
202 remoting.LoadingWindow.show();
204 remoting.identity.getToken().then(getAppHost).
205 catch(remoting.Error.handler(onError));
209 * @return {string} Application product name to be used in UI.
211 remoting.AppRemoting.prototype.getApplicationName = function() {
212 var manifest = chrome.runtime.getManifest();
213 return manifest.name;
216 /** @return {string} */
217 remoting.AppRemoting.prototype.runApplicationUrl = function() {
218 return remoting.settings.APP_REMOTING_API_BASE_URL + '/applications/' +
219 remoting.settings.getAppRemotingApplicationId() + '/run';
223 * @return {string} The default remap keys for the current platform.
225 remoting.AppRemoting.prototype.getDefaultRemapKeys = function() {
226 // Map Cmd to Ctrl on Mac since hosts typically use Ctrl for keyboard
227 // shortcuts, but we want them to act as natively as possible.
228 if (remoting.platformIsMac()) {
229 return '0x0700e3>0x0700e0,0x0700e7>0x0700e4';
235 * Called when a new session has been connected.
237 * @param {remoting.ClientSession} clientSession
238 * @return {void} Nothing.
240 remoting.AppRemoting.prototype.handleConnected = function(clientSession) {
241 remoting.identity.getUserInfo().then(
243 remoting.clientSession.sendClientMessage(
244 'setUserDisplayInfo',
245 JSON.stringify({fullName: userInfo.name}));
248 // Set up a ping at 10-second intervals to test the connection speed.
250 var message = { timestamp: new Date().getTime() };
251 clientSession.sendClientMessage('pingRequest', JSON.stringify(message));
254 this.pingTimerId_ = window.setInterval(ping, 10 * 1000);
258 * Called when the current session has been disconnected.
260 * @return {void} Nothing.
262 remoting.AppRemoting.prototype.handleDisconnected = function() {
263 // Cancel the ping when the connection closes.
264 window.clearInterval(this.pingTimerId_);
266 chrome.app.window.current().close();
270 * Called when the current session's connection has failed.
272 * @param {remoting.SessionConnector} connector
273 * @param {remoting.Error} error
274 * @return {void} Nothing.
276 remoting.AppRemoting.prototype.handleConnectionFailed = function(
278 this.handleError(error);
282 * Called when the current session has reached the point where the host has
283 * started streaming video frames to the client.
285 * @return {void} Nothing.
287 remoting.AppRemoting.prototype.handleVideoStreamingStarted = function() {
288 remoting.LoadingWindow.close();
292 * Called when an extension message needs to be handled.
294 * @param {string} type The type of the extension message.
295 * @param {Object} message The parsed extension message data.
296 * @return {boolean} True if the extension message was recognized.
298 remoting.AppRemoting.prototype.handleExtensionMessage = function(
303 // URL requests from the hosted app are untrusted, so disallow anything
304 // other than HTTP or HTTPS.
305 var url = getStringAttr(message, 'url');
306 if (url.indexOf('http:') != 0 && url.indexOf('https:') != 0) {
307 console.error('Bad URL: ' + url);
313 case 'onWindowRemoved':
314 var id = getNumberAttr(message, 'id');
315 this.windowActivationMenu_.remove(id);
318 case 'onWindowAdded':
319 var id = getNumberAttr(message, 'id');
320 var title = getStringAttr(message, 'title');
321 this.windowActivationMenu_.add(id, title);
324 case 'onAllWindowsMinimized':
325 chrome.app.window.current().minimize();
328 case 'setKeyboardLayouts':
329 var supportedLayouts = getArrayAttr(message, 'supportedLayouts');
330 var currentLayout = getStringAttr(message, 'currentLayout');
331 console.log('Current host keyboard layout: ' + currentLayout);
332 console.log('Supported host keyboard layouts: ' + supportedLayouts);
333 this.keyboardLayoutsMenu_.setLayouts(supportedLayouts, currentLayout);
337 var then = getNumberAttr(message, 'timestamp');
338 var now = new Date().getTime();
339 this.contextMenu_.updateConnectionRTT(now - then);
347 * Called when an error needs to be displayed to the user.
349 * @param {remoting.Error} errorTag The error to be localized and displayed.
350 * @return {void} Nothing.
352 remoting.AppRemoting.prototype.handleError = function(errorTag) {
353 console.error('Connection failed: ' + errorTag);
354 remoting.LoadingWindow.close();
355 remoting.MessageWindow.showErrorMessage(
356 chrome.i18n.getMessage(/*i18n-content*/'CONNECTION_FAILED'),
357 chrome.i18n.getMessage(/** @type {string} */ (errorTag)));