OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview | 6 * @fileoverview |
7 * This class implements the functionality that is specific to application | 7 * This class implements the functionality that is specific to application |
8 * remoting ("AppRemoting" or AR). | 8 * remoting ("AppRemoting" or AR). |
9 */ | 9 */ |
10 | 10 |
11 'use strict'; | 11 'use strict'; |
12 | 12 |
13 /** @suppress {duplicate} */ | 13 /** @suppress {duplicate} */ |
14 var remoting = remoting || {}; | 14 var remoting = remoting || {}; |
15 | 15 |
16 /** | 16 /** |
17 * Interval to test the connection speed. | |
18 * @const {number} | |
19 */ | |
20 var CONNECTION_SPEED_PING_INTERVAL_MS = 10 * 1000; | |
21 | |
22 /** | |
23 * Interval to refresh the google drive access token. | |
24 * @const {number} | |
25 */ | |
26 var DRIVE_ACCESS_TOKEN_REFRESH_INTERVAL_MS = 15 * 60 * 1000; | |
27 | |
28 /** | |
29 * @param {Array<string>} appCapabilities Array of application capabilities. | |
30 * @constructor | 17 * @constructor |
31 * @implements {remoting.ApplicationInterface} | 18 * @implements {remoting.ApplicationInterface} |
32 * @implements {remoting.ProtocolExtension} | |
33 * @implements {remoting.ClientSession.EventHandler} | |
34 * @extends {remoting.Application} | 19 * @extends {remoting.Application} |
35 */ | 20 */ |
36 remoting.AppRemoting = function(appCapabilities) { | 21 remoting.AppRemoting = function() { |
37 base.inherits(this, remoting.Application); | 22 base.inherits(this, remoting.Application); |
38 | 23 |
39 /** @private {remoting.ApplicationContextMenu} */ | 24 /** @private {remoting.Activity} */ |
40 this.contextMenu_ = null; | 25 this.activity_ = null; |
41 | |
42 /** @private {remoting.KeyboardLayoutsMenu} */ | |
43 this.keyboardLayoutsMenu_ = null; | |
44 | |
45 /** @private {remoting.WindowActivationMenu} */ | |
46 this.windowActivationMenu_ = null; | |
47 | |
48 /** @private {remoting.AppConnectedView} */ | |
49 this.connectedView_ = null; | |
50 | |
51 /** @private {base.Disposables} */ | |
52 this.extensionDisposables_ = null; | |
53 | |
54 /** @private */ | |
55 this.supportsGoogleDrive_ = false; | |
56 | |
57 /** @private */ | |
58 this.sessionConnector_ = remoting.SessionConnector.factory.createConnector( | |
59 document.getElementById('client-container'), | |
60 appCapabilities, this); | |
61 }; | 26 }; |
62 | 27 |
63 /** | 28 /** |
64 * Type definition for the RunApplicationResponse returned by the API. | |
65 * | |
66 * @constructor | |
67 * @private | |
68 */ | |
69 remoting.AppRemoting.AppHostResponse = function() { | |
70 /** @type {string} */ | |
71 this.status = ''; | |
72 /** @type {string} */ | |
73 this.hostJid = ''; | |
74 /** @type {string} */ | |
75 this.authorizationCode = ''; | |
76 /** @type {string} */ | |
77 this.sharedSecret = ''; | |
78 | |
79 this.host = { | |
80 /** @type {string} */ | |
81 applicationId: '', | |
82 | |
83 /** @type {string} */ | |
84 hostId: ''}; | |
85 }; | |
86 | |
87 /** | |
88 * @return {string} Application product name to be used in UI. | 29 * @return {string} Application product name to be used in UI. |
89 * @override {remoting.ApplicationInterface} | 30 * @override {remoting.ApplicationInterface} |
90 */ | 31 */ |
91 remoting.AppRemoting.prototype.getApplicationName = function() { | 32 remoting.AppRemoting.prototype.getApplicationName = function() { |
92 var manifest = chrome.runtime.getManifest(); | 33 var manifest = chrome.runtime.getManifest(); |
93 return manifest.name; | 34 return manifest.name; |
94 }; | 35 }; |
95 | 36 |
96 /** | 37 /** |
97 * @param {!remoting.Error} error The failure reason. | 38 * @param {!remoting.Error} error The failure reason. |
98 * @override {remoting.ApplicationInterface} | 39 * @override {remoting.ApplicationInterface} |
99 */ | 40 */ |
100 remoting.AppRemoting.prototype.signInFailed_ = function(error) { | 41 remoting.AppRemoting.prototype.signInFailed_ = function(error) { |
101 this.onError(error); | 42 remoting.MessageWindow.showErrorMessage( |
43 chrome.i18n.getMessage(/*i18n-content*/'CONNECTION_FAILED'), | |
44 chrome.i18n.getMessage(error.getTag())); | |
102 }; | 45 }; |
103 | 46 |
104 /** | 47 /** |
105 * @override {remoting.ApplicationInterface} | 48 * @override {remoting.ApplicationInterface} |
106 */ | 49 */ |
107 remoting.AppRemoting.prototype.initApplication_ = function() { | 50 remoting.AppRemoting.prototype.initApplication_ = function() { |
108 // TODO(jamiewalch): Remove ClientSession's dependency on remoting.fullscreen | |
kelvinp
2015/04/14 00:46:33
No longer needed, as we did such a good job cleanu
garykac
2015/04/15 00:52:37
Wahoo!
| |
109 // so that this is no longer required. | |
110 remoting.fullscreen = new remoting.FullscreenAppsV2(); | |
111 | |
112 remoting.windowShape.updateClientWindowShape(); | 51 remoting.windowShape.updateClientWindowShape(); |
113 | 52 |
114 // Initialize the context menus. | 53 this.activity_ = new remoting.AppRemotingActivity(); |
115 if (remoting.platformIsChromeOS()) { | |
116 var adapter = new remoting.ContextMenuChrome(); | |
117 } else { | |
118 var adapter = new remoting.ContextMenuDom( | |
119 document.getElementById('context-menu')); | |
120 } | |
121 this.contextMenu_ = new remoting.ApplicationContextMenu(adapter); | |
122 this.keyboardLayoutsMenu_ = new remoting.KeyboardLayoutsMenu(adapter); | |
123 this.windowActivationMenu_ = new remoting.WindowActivationMenu(adapter); | |
124 }; | 54 }; |
125 | 55 |
126 /** | 56 /** |
127 * @param {string} token An OAuth access token. | 57 * @param {string} token An OAuth access token. |
128 * @override {remoting.ApplicationInterface} | 58 * @override {remoting.ApplicationInterface} |
129 */ | 59 */ |
130 remoting.AppRemoting.prototype.startApplication_ = function(token) { | 60 remoting.AppRemoting.prototype.startApplication_ = function(token) { |
kelvinp
2015/04/14 00:46:33
Moved to AppRemotingActivity
| |
131 remoting.LoadingWindow.show(); | 61 this.activity_.start(); |
132 | |
133 /** @type {remoting.AppRemoting} */ | |
134 var that = this; | |
135 | |
136 /** @param {!remoting.Xhr.Response} xhrResponse */ | |
137 var parseAppHostResponse = function(xhrResponse) { | |
138 if (xhrResponse.status == 200) { | |
139 var response = /** @type {remoting.AppRemoting.AppHostResponse} */ | |
140 (base.jsonParseSafe(xhrResponse.getText())); | |
141 if (response && | |
142 response.status && | |
143 response.status == 'done' && | |
144 response.hostJid && | |
145 response.authorizationCode && | |
146 response.sharedSecret && | |
147 response.host && | |
148 response.host.hostId) { | |
149 var hostJid = response.hostJid; | |
150 that.contextMenu_.setHostId(response.host.hostId); | |
151 var host = new remoting.Host(response.host.hostId); | |
152 host.jabberId = hostJid; | |
153 host.authorizationCode = response.authorizationCode; | |
154 host.sharedSecret = response.sharedSecret; | |
155 | |
156 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
157 | |
158 var idleDetector = new remoting.IdleDetector( | |
159 document.getElementById('idle-dialog'), | |
160 remoting.app.disconnect.bind(remoting.app)); | |
161 | |
162 /** | |
163 * @param {string} tokenUrl Token-issue URL received from the host. | |
164 * @param {string} hostPublicKey Host public key (DER and Base64 | |
165 * encoded). | |
166 * @param {string} scope OAuth scope to request the token for. | |
167 * @param {function(string, string):void} onThirdPartyTokenFetched | |
168 * Callback. | |
169 */ | |
170 var fetchThirdPartyToken = function( | |
171 tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) { | |
172 // Use the authentication tokens returned by the app-remoting server. | |
173 onThirdPartyTokenFetched(host['authorizationCode'], | |
174 host['sharedSecret']); | |
175 }; | |
176 | |
177 that.sessionConnector_.connect( | |
178 remoting.Application.Mode.APP_REMOTING, host, | |
179 new remoting.CredentialsProvider( | |
180 {fetchThirdPartyToken: fetchThirdPartyToken})); | |
181 } else if (response && response.status == 'pending') { | |
182 that.onError(new remoting.Error( | |
183 remoting.Error.Tag.SERVICE_UNAVAILABLE)); | |
184 } | |
185 } else { | |
186 console.error('Invalid "runApplication" response from server.'); | |
187 that.onError(remoting.Error.fromHttpStatus(xhrResponse.status)); | |
188 } | |
189 }; | |
190 | |
191 new remoting.Xhr({ | |
192 method: 'POST', | |
193 url: that.runApplicationUrl_(), | |
194 oauthToken: token | |
195 }).start().then(parseAppHostResponse); | |
196 }; | 62 }; |
197 | 63 |
198 /** | 64 /** |
199 * @override {remoting.ApplicationInterface} | 65 * @override {remoting.ApplicationInterface} |
200 */ | 66 */ |
201 remoting.AppRemoting.prototype.exitApplication_ = function() { | 67 remoting.AppRemoting.prototype.exitApplication_ = function() { |
202 remoting.LoadingWindow.close(); | 68 this.activity_.dispose(); |
203 this.closeMainWindow_(); | 69 this.closeMainWindow_(); |
204 }; | 70 }; |
205 | |
206 /** | |
207 * @param {remoting.ConnectionInfo} connectionInfo | |
kelvinp
2015/04/14 00:46:33
Moved to AppRemotingActivity
| |
208 */ | |
209 remoting.AppRemoting.prototype.onConnected = function(connectionInfo) { | |
210 this.supportsGoogleDrive_ = connectionInfo.session().hasCapability( | |
211 remoting.ClientSession.Capability.GOOGLE_DRIVE); | |
212 | |
213 connectionInfo.plugin().extensions().register(this); | |
214 | |
215 this.connectedView_ = new remoting.AppConnectedView( | |
216 document.getElementById('client-container'), connectionInfo); | |
217 | |
218 // Map Cmd to Ctrl on Mac since hosts typically use Ctrl for keyboard | |
219 // shortcuts, but we want them to act as natively as possible. | |
220 if (remoting.platformIsMac()) { | |
221 connectionInfo.plugin().setRemapKeys('0x0700e3>0x0700e0,0x0700e7>0x0700e4'); | |
222 } | |
223 }; | |
224 | |
225 remoting.AppRemoting.prototype.onDisconnected = function() { | |
226 base.dispose(this.connectedView_); | |
227 this.connectedView_ = null; | |
228 this.stopExtension_(); | |
229 chrome.app.window.current().close(); | |
230 }; | |
231 | |
232 /** | |
233 * @param {!remoting.Error} error | |
234 */ | |
235 remoting.AppRemoting.prototype.onConnectionFailed = function(error) { | |
236 this.onError(error); | |
237 }; | |
238 | |
239 /** | |
240 * @param {!remoting.Error} error The error to be localized and displayed. | |
241 */ | |
242 remoting.AppRemoting.prototype.onError = function(error) { | |
243 console.error('Connection failed: ' + error.toString()); | |
244 remoting.LoadingWindow.close(); | |
245 remoting.MessageWindow.showErrorMessage( | |
246 chrome.i18n.getMessage(/*i18n-content*/'CONNECTION_FAILED'), | |
247 chrome.i18n.getMessage(error.getTag())); | |
248 this.stopExtension_(); | |
249 }; | |
250 | |
251 | |
252 /** | |
kelvinp
2015/04/14 00:46:33
Moved to AppConnectedView
| |
253 * @return {Array<string>} | |
254 * @override {remoting.ProtocolExtension} | |
255 */ | |
256 remoting.AppRemoting.prototype.getExtensionTypes = function() { | |
257 return ['openURL', 'onWindowRemoved', 'onWindowAdded', | |
258 'onAllWindowsMinimized', 'setKeyboardLayouts', 'pingResponse']; | |
259 }; | |
260 | |
261 /** | |
262 * @param {function(string,string)} sendMessageToHost Callback to send a message | |
263 * to the host. | |
264 * @override {remoting.ProtocolExtension} | |
265 */ | |
266 remoting.AppRemoting.prototype.startExtension = function(sendMessageToHost) { | |
267 this.windowActivationMenu_.setExtensionMessageSender(sendMessageToHost); | |
268 this.keyboardLayoutsMenu_.setExtensionMessageSender(sendMessageToHost); | |
269 | |
270 remoting.identity.getUserInfo().then(function(userInfo) { | |
271 sendMessageToHost('setUserDisplayInfo', | |
272 JSON.stringify({fullName: userInfo.name})); | |
273 }); | |
274 | |
275 base.dispose(this.extensionDisposables_); | |
276 | |
277 var onRestoreHook = new base.ChromeEventHook( | |
278 chrome.app.window.current().onRestored, function() { | |
279 sendMessageToHost('restoreAllWindows', ''); | |
280 }); | |
281 | |
282 var pingTimer = new base.RepeatingTimer(function() { | |
283 var message = {timestamp: new Date().getTime()}; | |
284 sendMessageToHost('pingRequest', JSON.stringify(message)); | |
285 }, CONNECTION_SPEED_PING_INTERVAL_MS); | |
286 | |
287 this.extensionDisposables_ = new base.Disposables(onRestoreHook, pingTimer); | |
288 | |
289 if (this.supportsGoogleDrive_) { | |
290 this.extensionDisposables_.add(new base.RepeatingTimer( | |
291 this.sendGoogleDriveAccessToken_.bind(this, sendMessageToHost), | |
292 DRIVE_ACCESS_TOKEN_REFRESH_INTERVAL_MS, true)); | |
293 } | |
294 }; | |
295 | |
296 /** @private */ | |
297 remoting.AppRemoting.prototype.stopExtension_ = function() { | |
298 this.windowActivationMenu_.setExtensionMessageSender(base.doNothing); | |
299 this.keyboardLayoutsMenu_.setExtensionMessageSender(base.doNothing); | |
300 | |
301 base.dispose(this.extensionDisposables_); | |
302 this.extensionDisposables_ = null; | |
303 }; | |
304 | |
305 /** | |
306 * @param {string} type The message type. | |
307 * @param {Object} message The parsed extension message data. | |
308 * @override {remoting.ProtocolExtension} | |
309 */ | |
310 remoting.AppRemoting.prototype.onExtensionMessage = function(type, message) { | |
311 switch (type) { | |
312 | |
313 case 'openURL': | |
314 // URL requests from the hosted app are untrusted, so disallow anything | |
315 // other than HTTP or HTTPS. | |
316 var url = base.getStringAttr(message, 'url'); | |
317 if (url.indexOf('http:') != 0 && url.indexOf('https:') != 0) { | |
318 console.error('Bad URL: ' + url); | |
319 } else { | |
320 window.open(url); | |
321 } | |
322 return true; | |
323 | |
324 case 'onWindowRemoved': | |
325 var id = base.getNumberAttr(message, 'id'); | |
326 this.windowActivationMenu_.remove(id); | |
327 return true; | |
328 | |
329 case 'onWindowAdded': | |
330 var id = base.getNumberAttr(message, 'id'); | |
331 var title = base.getStringAttr(message, 'title'); | |
332 this.windowActivationMenu_.add(id, title); | |
333 return true; | |
334 | |
335 case 'onAllWindowsMinimized': | |
336 chrome.app.window.current().minimize(); | |
337 return true; | |
338 | |
339 case 'setKeyboardLayouts': | |
340 var supportedLayouts = base.getArrayAttr(message, 'supportedLayouts'); | |
341 var currentLayout = base.getStringAttr(message, 'currentLayout'); | |
342 console.log('Current host keyboard layout: ' + currentLayout); | |
343 console.log('Supported host keyboard layouts: ' + supportedLayouts); | |
344 this.keyboardLayoutsMenu_.setLayouts(supportedLayouts, currentLayout); | |
345 return true; | |
346 | |
347 case 'pingResponse': | |
348 var then = base.getNumberAttr(message, 'timestamp'); | |
349 var now = new Date().getTime(); | |
350 this.contextMenu_.updateConnectionRTT(now - then); | |
351 return true; | |
352 } | |
353 | |
354 return false; | |
355 }; | |
356 | |
357 /** | |
358 * Timer callback to send the access token to the host. | |
359 * @param {function(string, string)} sendExtensionMessage | |
360 * @private | |
361 */ | |
362 remoting.AppRemoting.prototype.sendGoogleDriveAccessToken_ | |
363 = function(sendExtensionMessage) { | |
364 var googleDriveScopes = [ | |
365 'https://docs.google.com/feeds/', | |
366 'https://www.googleapis.com/auth/drive' | |
367 ]; | |
368 remoting.identity.getNewToken(googleDriveScopes).then( | |
369 function(/** string */ token){ | |
370 sendExtensionMessage('accessToken', token); | |
371 }).catch(remoting.Error.handler(function(/** remoting.Error */ error) { | |
372 console.log('Failed to refresh access token: ' + error.toString()); | |
373 })); | |
374 }; | |
375 | |
376 /** | |
377 * @return {string} | |
378 * @private | |
379 */ | |
380 remoting.AppRemoting.prototype.runApplicationUrl_ = function() { | |
381 return remoting.settings.APP_REMOTING_API_BASE_URL + '/applications/' + | |
382 remoting.settings.getAppRemotingApplicationId() + '/run'; | |
383 }; | |
OLD | NEW |