OLD | NEW |
| (Empty) |
1 // Copyright (c) 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 'use strict'; | |
6 | |
7 /** | |
8 * SuggestAppsDialog contains a list box to select an app to be opened the file | |
9 * with. This dialog should be used as action picker for file operations. | |
10 */ | |
11 | |
12 /** | |
13 * The width of the widget (in pixel). | |
14 * @type {number} | |
15 * @const | |
16 */ | |
17 var WEBVIEW_WIDTH = 735; | |
18 /** | |
19 * The height of the widget (in pixel). | |
20 * @type {number} | |
21 * @const | |
22 */ | |
23 var WEBVIEW_HEIGHT = 480; | |
24 | |
25 /** | |
26 * The URL of the widget. | |
27 * @type {string} | |
28 * @const | |
29 */ | |
30 var CWS_WIDGET_URL = | |
31 'https://clients5.google.com/webstore/wall/cros-widget-container'; | |
32 /** | |
33 * The origin of the widget. | |
34 * @type {string} | |
35 * @const | |
36 */ | |
37 var CWS_WIDGET_ORIGIN = 'https://clients5.google.com'; | |
38 | |
39 /** | |
40 * Creates dialog in DOM tree. | |
41 * | |
42 * @param {HTMLElement} parentNode Node to be parent for this dialog. | |
43 * @param {Object} state Static state of suggest app dialog. | |
44 * @constructor | |
45 * @extends {FileManagerDialogBase} | |
46 */ | |
47 function SuggestAppsDialog(parentNode, state) { | |
48 FileManagerDialogBase.call(this, parentNode); | |
49 | |
50 this.frame_.id = 'suggest-app-dialog'; | |
51 | |
52 this.webviewContainer_ = this.document_.createElement('div'); | |
53 this.webviewContainer_.id = 'webview-container'; | |
54 this.webviewContainer_.style.width = WEBVIEW_WIDTH + 'px'; | |
55 this.webviewContainer_.style.height = WEBVIEW_HEIGHT + 'px'; | |
56 this.frame_.insertBefore(this.webviewContainer_, this.text_.nextSibling); | |
57 | |
58 var spinnerLayer = this.document_.createElement('div'); | |
59 spinnerLayer.className = 'spinner-layer'; | |
60 this.webviewContainer_.appendChild(spinnerLayer); | |
61 | |
62 this.buttons_ = this.document_.createElement('div'); | |
63 this.buttons_.id = 'buttons'; | |
64 this.frame_.appendChild(this.buttons_); | |
65 | |
66 this.webstoreButton_ = this.document_.createElement('div'); | |
67 this.webstoreButton_.id = 'webstore-button'; | |
68 this.webstoreButton_.innerHTML = str('SUGGEST_DIALOG_LINK_TO_WEBSTORE'); | |
69 this.webstoreButton_.addEventListener( | |
70 'click', this.onWebstoreLinkClicked_.bind(this)); | |
71 this.buttons_.appendChild(this.webstoreButton_); | |
72 | |
73 this.initialFocusElement_ = this.webviewContainer_; | |
74 | |
75 this.webview_ = null; | |
76 this.accessToken_ = null; | |
77 this.widgetUrl_ = | |
78 state.overrideCwsContainerUrlForTest || CWS_WIDGET_URL; | |
79 this.widgetOrigin_ = | |
80 state.overrideCwsContainerOriginForTest || CWS_WIDGET_ORIGIN; | |
81 | |
82 this.extension_ = null; | |
83 this.mime_ = null; | |
84 this.installingItemId_ = null; | |
85 this.state_ = SuggestAppsDialog.State.UNINITIALIZED; | |
86 | |
87 this.initializationTask_ = new AsyncUtil.Group(); | |
88 this.initializationTask_.add(this.retrieveAuthorizeToken_.bind(this)); | |
89 this.initializationTask_.run(); | |
90 } | |
91 | |
92 SuggestAppsDialog.prototype = { | |
93 __proto__: FileManagerDialogBase.prototype | |
94 }; | |
95 | |
96 /** | |
97 * @enum {string} | |
98 * @const | |
99 */ | |
100 SuggestAppsDialog.State = { | |
101 UNINITIALIZED: 'SuggestAppsDialog.State.UNINITIALIZED', | |
102 INITIALIZING: 'SuggestAppsDialog.State.INITIALIZING', | |
103 INITIALIZE_FAILED_CLOSING: | |
104 'SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING', | |
105 INITIALIZED: 'SuggestAppsDialog.State.INITIALIZED', | |
106 INSTALLING: 'SuggestAppsDialog.State.INSTALLING', | |
107 INSTALLED_CLOSING: 'SuggestAppsDialog.State.INSTALLED_CLOSING', | |
108 OPENING_WEBSTORE_CLOSING: 'SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING', | |
109 CANCELED_CLOSING: 'SuggestAppsDialog.State.CANCELED_CLOSING' | |
110 }; | |
111 Object.freeze(SuggestAppsDialog.State); | |
112 | |
113 /** | |
114 * @enum {string} | |
115 * @const | |
116 */ | |
117 SuggestAppsDialog.Result = { | |
118 // Install is done. The install app should be opened. | |
119 INSTALL_SUCCESSFUL: 'SuggestAppsDialog.Result.INSTALL_SUCCESSFUL', | |
120 // User cancelled the suggest app dialog. No message should be shown. | |
121 USER_CANCELL: 'SuggestAppsDialog.Result.USER_CANCELL', | |
122 // User clicked the link to web store so the dialog is closed. | |
123 WEBSTORE_LINK_OPENED: 'SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED', | |
124 // Failed to load the widget. Error message should be shown. | |
125 FAILED: 'SuggestAppsDialog.Result.FAILED' | |
126 }; | |
127 Object.freeze(SuggestAppsDialog.Result); | |
128 | |
129 /** | |
130 * @override | |
131 */ | |
132 SuggestAppsDialog.prototype.onInputFocus = function() { | |
133 this.webviewContainer_.select(); | |
134 }; | |
135 | |
136 /** | |
137 * Injects headers into the passed request. | |
138 * | |
139 * @param {Event} e Request event. | |
140 * @return {{requestHeaders: HttpHeaders}} Modified headers. | |
141 * @private | |
142 */ | |
143 SuggestAppsDialog.prototype.authorizeRequest_ = function(e) { | |
144 e.requestHeaders.push({ | |
145 name: 'Authorization', | |
146 value: 'Bearer ' + this.accessToken_ | |
147 }); | |
148 return {requestHeaders: e.requestHeaders}; | |
149 }; | |
150 | |
151 /** | |
152 * Retrieves the authorize token. This method should be called in | |
153 * initialization of the dialog. | |
154 * | |
155 * @param {function()} callback Called when the token is retrieved. | |
156 * @private | |
157 */ | |
158 SuggestAppsDialog.prototype.retrieveAuthorizeToken_ = function(callback) { | |
159 if (window.IN_TEST) { | |
160 // In test, use a dummy string as token. This must be a non-empty string. | |
161 this.accessToken_ = 'DUMMY_ACCESS_TOKEN_FOR_TEST'; | |
162 } | |
163 | |
164 if (this.accessToken_) { | |
165 callback(); | |
166 return; | |
167 } | |
168 | |
169 // Fetch or update the access token. | |
170 chrome.fileBrowserPrivate.requestWebStoreAccessToken( | |
171 function(accessToken) { | |
172 // In case of error, this.accessToken_ will be set to null. | |
173 this.accessToken_ = accessToken; | |
174 callback(); | |
175 }.bind(this)); | |
176 }; | |
177 | |
178 /** | |
179 * Dummy function for SuggestAppsDialog.show() not to be called unintentionally. | |
180 */ | |
181 SuggestAppsDialog.prototype.show = function() { | |
182 console.error('SuggestAppsDialog.show() shouldn\'t be called directly.'); | |
183 }; | |
184 | |
185 /** | |
186 * Shows suggest-apps dialog by file extension and mime. | |
187 * | |
188 * @param {string} extension Extension of the file. | |
189 * @param {string} mime Mime of the file. | |
190 * @param {function(boolean)} onDialogClosed Called when the dialog is closed. | |
191 * The argument is the result of installation: true if an app is installed, | |
192 * false otherwise. | |
193 */ | |
194 SuggestAppsDialog.prototype.showByExtensionAndMime = | |
195 function(extension, mime, onDialogClosed) { | |
196 this.text_.hidden = true; | |
197 this.dialogText_ = ''; | |
198 this.showInternal_(null, extension, mime, onDialogClosed); | |
199 }; | |
200 | |
201 /** | |
202 * Shows suggest-apps dialog by the filename. | |
203 * | |
204 * @param {string} filename Filename (without extension) of the file. | |
205 * @param {function(boolean)} onDialogClosed Called when the dialog is closed. | |
206 * The argument is the result of installation: true if an app is installed, | |
207 * false otherwise. | |
208 */ | |
209 SuggestAppsDialog.prototype.showByFilename = | |
210 function(filename, onDialogClosed) { | |
211 this.text_.hidden = false; | |
212 this.dialogText_ = str('SUGGEST_DIALOG_MESSAGE_FOR_EXECUTABLE'); | |
213 this.showInternal_(filename, null, null, onDialogClosed); | |
214 }; | |
215 | |
216 /** | |
217 * Internal method to show a dialog. This should be called only from 'Suggest. | |
218 * appDialog.showXxxx()' functions. | |
219 * | |
220 * @param {string} filename Filename (without extension) of the file. | |
221 * @param {string} extension Extension of the file. | |
222 * @param {string} mime Mime of the file. | |
223 * @param {function(boolean)} onDialogClosed Called when the dialog is closed. | |
224 * The argument is the result of installation: true if an app is installed, | |
225 * false otherwise. | |
226 * @private | |
227 */ | |
228 SuggestAppsDialog.prototype.showInternal_ = | |
229 function(filename, extension, mime, onDialogClosed) { | |
230 if (this.state_ != SuggestAppsDialog.State.UNINITIALIZED) { | |
231 console.error('Invalid state.'); | |
232 return; | |
233 } | |
234 | |
235 this.extension_ = extension; | |
236 this.mimeType_ = mime; | |
237 this.onDialogClosed_ = onDialogClosed; | |
238 this.state_ = SuggestAppsDialog.State.INITIALIZING; | |
239 | |
240 SuggestAppsDialog.Metrics.recordShowDialog(); | |
241 SuggestAppsDialog.Metrics.startLoad(); | |
242 | |
243 // Makes it sure that the initialization is completed. | |
244 this.initializationTask_.run(function() { | |
245 if (!this.accessToken_) { | |
246 this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING; | |
247 this.onHide_(); | |
248 return; | |
249 } | |
250 | |
251 var title = str('SUGGEST_DIALOG_TITLE'); | |
252 var show = this.dialogText_ ? | |
253 FileManagerDialogBase.prototype.showTitleAndTextDialog.call( | |
254 this, title, this.dialogText_) : | |
255 FileManagerDialogBase.prototype.showTitleOnlyDialog.call( | |
256 this, title); | |
257 if (!show) { | |
258 console.error('SuggestAppsDialog can\'t be shown'); | |
259 this.state_ = SuggestAppsDialog.State.UNINITIALIZED; | |
260 this.onHide(); | |
261 return; | |
262 } | |
263 | |
264 this.webview_ = this.document_.createElement('webview'); | |
265 this.webview_.id = 'cws-widget'; | |
266 this.webview_.partition = 'persist:cwswidgets'; | |
267 this.webview_.style.width = WEBVIEW_WIDTH + 'px'; | |
268 this.webview_.style.height = WEBVIEW_HEIGHT + 'px'; | |
269 this.webview_.request.onBeforeSendHeaders.addListener( | |
270 this.authorizeRequest_.bind(this), | |
271 {urls: [this.widgetOrigin_ + '/*']}, | |
272 ['blocking', 'requestHeaders']); | |
273 this.webview_.addEventListener('newwindow', function(event) { | |
274 // Discard the window object and reopen in an external window. | |
275 event.window.discard(); | |
276 util.visitURL(event.targetUrl); | |
277 event.preventDefault(); | |
278 }); | |
279 this.webviewContainer_.appendChild(this.webview_); | |
280 | |
281 this.frame_.classList.add('show-spinner'); | |
282 | |
283 this.webviewClient_ = new CWSContainerClient( | |
284 this.webview_, | |
285 extension, mime, filename, | |
286 WEBVIEW_WIDTH, WEBVIEW_HEIGHT, | |
287 this.widgetUrl_, this.widgetOrigin_); | |
288 this.webviewClient_.addEventListener(CWSContainerClient.Events.LOADED, | |
289 this.onWidgetLoaded_.bind(this)); | |
290 this.webviewClient_.addEventListener(CWSContainerClient.Events.LOAD_FAILED, | |
291 this.onWidgetLoadFailed_.bind(this)); | |
292 this.webviewClient_.addEventListener( | |
293 CWSContainerClient.Events.REQUEST_INSTALL, | |
294 this.onInstallRequest_.bind(this)); | |
295 this.webviewClient_.load(); | |
296 }.bind(this)); | |
297 }; | |
298 | |
299 /** | |
300 * Called when the 'See more...' link is clicked to be navigated to Webstore. | |
301 * @param {Event} e Event. | |
302 * @private | |
303 */ | |
304 SuggestAppsDialog.prototype.onWebstoreLinkClicked_ = function(e) { | |
305 var webStoreUrl = | |
306 FileTasks.createWebStoreLink(this.extension_, this.mimeType_); | |
307 util.visitURL(webStoreUrl); | |
308 this.state_ = SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING; | |
309 this.hide(); | |
310 }; | |
311 | |
312 /** | |
313 * Called when the widget is loaded successfully. | |
314 * @param {Event} event Event. | |
315 * @private | |
316 */ | |
317 SuggestAppsDialog.prototype.onWidgetLoaded_ = function(event) { | |
318 SuggestAppsDialog.Metrics.finishLoad(); | |
319 SuggestAppsDialog.Metrics.recordLoad( | |
320 SuggestAppsDialog.Metrics.LOAD.SUCCEEDED); | |
321 | |
322 this.frame_.classList.remove('show-spinner'); | |
323 this.state_ = SuggestAppsDialog.State.INITIALIZED; | |
324 | |
325 this.webview_.focus(); | |
326 }; | |
327 | |
328 /** | |
329 * Called when the widget is failed to load. | |
330 * @param {Event} event Event. | |
331 * @private | |
332 */ | |
333 SuggestAppsDialog.prototype.onWidgetLoadFailed_ = function(event) { | |
334 SuggestAppsDialog.Metrics.recordLoad(SuggestAppsDialog.Metrics.LOAD.FAILURE); | |
335 | |
336 this.frame_.classList.remove('show-spinner'); | |
337 this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING; | |
338 | |
339 this.hide(); | |
340 }; | |
341 | |
342 /** | |
343 * Called when the connection status is changed. | |
344 * @param {util.DriveConnectionType} connectionType Current connection type. | |
345 */ | |
346 SuggestAppsDialog.prototype.onDriveConnectionChanged = | |
347 function(connectionType) { | |
348 if (this.state_ !== SuggestAppsDialog.State.UNINITIALIZED && | |
349 connectionType === util.DriveConnectionType.OFFLINE) { | |
350 this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING; | |
351 this.hide(); | |
352 } | |
353 }; | |
354 | |
355 /** | |
356 * Called when receiving the install request from the webview client. | |
357 * @param {Event} e Event. | |
358 * @private | |
359 */ | |
360 SuggestAppsDialog.prototype.onInstallRequest_ = function(e) { | |
361 var itemId = e.itemId; | |
362 this.installingItemId_ = itemId; | |
363 | |
364 this.appInstaller_ = new AppInstaller(itemId); | |
365 this.appInstaller_.install(this.onInstallCompleted_.bind(this)); | |
366 | |
367 this.frame_.classList.add('show-spinner'); | |
368 this.state_ = SuggestAppsDialog.State.INSTALLING; | |
369 }; | |
370 | |
371 /** | |
372 * Called when the installation is completed from the app installer. | |
373 * @param {AppInstaller.Result} result Result of the installation. | |
374 * @param {string} error Detail of the error. | |
375 * @private | |
376 */ | |
377 SuggestAppsDialog.prototype.onInstallCompleted_ = function(result, error) { | |
378 var success = (result === AppInstaller.Result.SUCCESS); | |
379 | |
380 this.frame_.classList.remove('show-spinner'); | |
381 this.state_ = success ? | |
382 SuggestAppsDialog.State.INSTALLED_CLOSING : | |
383 SuggestAppsDialog.State.INITIALIZED; // Back to normal state. | |
384 this.webviewClient_.onInstallCompleted(success, this.installingItemId_); | |
385 this.installingItemId_ = null; | |
386 | |
387 switch (result) { | |
388 case AppInstaller.Result.SUCCESS: | |
389 SuggestAppsDialog.Metrics.recordInstall( | |
390 SuggestAppsDialog.Metrics.INSTALL.SUCCESS); | |
391 this.hide(); | |
392 break; | |
393 case AppInstaller.Result.CANCELLED: | |
394 SuggestAppsDialog.Metrics.recordInstall( | |
395 SuggestAppsDialog.Metrics.INSTALL.CANCELLED); | |
396 // User cancelled the installation. Do nothing. | |
397 break; | |
398 case AppInstaller.Result.ERROR: | |
399 SuggestAppsDialog.Metrics.recordInstall( | |
400 SuggestAppsDialog.Metrics.INSTALL.FAILED); | |
401 fileManager.error.show(str('SUGGEST_DIALOG_INSTALLATION_FAILED')); | |
402 break; | |
403 } | |
404 }; | |
405 | |
406 /** | |
407 * @override | |
408 */ | |
409 SuggestAppsDialog.prototype.hide = function(opt_originalOnHide) { | |
410 switch (this.state_) { | |
411 case SuggestAppsDialog.State.INSTALLING: | |
412 // Install is being aborted. Send the failure result. | |
413 // Cancels the install. | |
414 if (this.webviewClient_) | |
415 this.webviewClient_.onInstallCompleted(false, this.installingItemId_); | |
416 this.installingItemId_ = null; | |
417 | |
418 // Assumes closing the dialog as canceling the install. | |
419 this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; | |
420 break; | |
421 case SuggestAppsDialog.State.INITIALIZING: | |
422 SuggestAppsDialog.Metrics.recordLoad( | |
423 SuggestAppsDialog.Metrics.LOAD.CANCELLED); | |
424 this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; | |
425 break; | |
426 case SuggestAppsDialog.State.INSTALLED_CLOSING: | |
427 case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING: | |
428 case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING: | |
429 // Do nothing. | |
430 break; | |
431 case SuggestAppsDialog.State.INITIALIZED: | |
432 this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; | |
433 break; | |
434 default: | |
435 this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; | |
436 console.error('Invalid state.'); | |
437 } | |
438 | |
439 if (this.webviewClient_) { | |
440 this.webviewClient_.dispose(); | |
441 this.webviewClient_ = null; | |
442 } | |
443 | |
444 this.webviewContainer_.removeChild(this.webview_); | |
445 this.webview_ = null; | |
446 this.extension_ = null; | |
447 this.mime_ = null; | |
448 | |
449 FileManagerDialogBase.prototype.hide.call( | |
450 this, | |
451 this.onHide_.bind(this, opt_originalOnHide)); | |
452 }; | |
453 | |
454 /** | |
455 * @param {function()=} opt_originalOnHide Original onHide function passed to | |
456 * SuggestAppsDialog.hide(). | |
457 * @private | |
458 */ | |
459 SuggestAppsDialog.prototype.onHide_ = function(opt_originalOnHide) { | |
460 // Calls the callback after the dialog hides. | |
461 if (opt_originalOnHide) | |
462 opt_originalOnHide(); | |
463 | |
464 var result; | |
465 switch (this.state_) { | |
466 case SuggestAppsDialog.State.INSTALLED_CLOSING: | |
467 result = SuggestAppsDialog.Result.INSTALL_SUCCESSFUL; | |
468 SuggestAppsDialog.Metrics.recordCloseDialog( | |
469 SuggestAppsDialog.Metrics.CLOSE_DIALOG.ITEM_INSTALLED); | |
470 break; | |
471 case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING: | |
472 result = SuggestAppsDialog.Result.FAILED; | |
473 break; | |
474 case SuggestAppsDialog.State.CANCELED_CLOSING: | |
475 result = SuggestAppsDialog.Result.USER_CANCELL; | |
476 SuggestAppsDialog.Metrics.recordCloseDialog( | |
477 SuggestAppsDialog.Metrics.CLOSE_DIALOG.USER_CANCELL); | |
478 break; | |
479 case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING: | |
480 result = SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED; | |
481 SuggestAppsDialog.Metrics.recordCloseDialog( | |
482 SuggestAppsDialog.Metrics.CLOSE_DIALOG.WEB_STORE_LINK); | |
483 break; | |
484 default: | |
485 result = SuggestAppsDialog.Result.USER_CANCELL; | |
486 SuggestAppsDialog.Metrics.recordCloseDialog( | |
487 SuggestAppsDialog.Metrics.CLOSE_DIALOG.UNKNOWN_ERROR); | |
488 console.error('Invalid state.'); | |
489 } | |
490 this.state_ = SuggestAppsDialog.State.UNINITIALIZED; | |
491 | |
492 this.onDialogClosed_(result); | |
493 }; | |
494 | |
495 /** | |
496 * Utility methods and constants to record histograms. | |
497 */ | |
498 SuggestAppsDialog.Metrics = Object.freeze({ | |
499 LOAD: Object.freeze({ | |
500 SUCCEEDED: 0, | |
501 CANCELLED: 1, | |
502 FAILED: 2, | |
503 }), | |
504 | |
505 /** | |
506 * @param {SuggestAppsDialog.Metrics.LOAD} result Result of load. | |
507 */ | |
508 recordLoad: function(result) { | |
509 if (0 <= result && result < 3) | |
510 metrics.recordEnum('SuggestApps.Load', result, 3); | |
511 }, | |
512 | |
513 CLOSE_DIALOG: Object.freeze({ | |
514 UNKOWN_ERROR: 0, | |
515 ITEM_INSTALLED: 1, | |
516 USER_CANCELLED: 2, | |
517 WEBSTORE_LINK_OPENED: 3, | |
518 }), | |
519 | |
520 /** | |
521 * @param {SuggestAppsDialog.Metrics.CLOSE_DIALOG} reason Reason of closing | |
522 * dialog. | |
523 */ | |
524 recordCloseDialog: function(reason) { | |
525 if (0 <= reason && reason < 4) | |
526 metrics.recordEnum('SuggestApps.CloseDialog', reason, 4); | |
527 }, | |
528 | |
529 INSTALL: Object.freeze({ | |
530 SUCCEEDED: 0, | |
531 CANCELLED: 1, | |
532 FAILED: 2, | |
533 }), | |
534 | |
535 /** | |
536 * @param {SuggestAppsDialog.Metrics.INSTALL} result Result of installation. | |
537 */ | |
538 recordInstall: function(result) { | |
539 if (0 <= result && result < 3) | |
540 metrics.recordEnum('SuggestApps.Install', result, 3); | |
541 }, | |
542 | |
543 recordShowDialog: function() { | |
544 metrics.recordUserAction('SuggestApps.ShowDialog'); | |
545 }, | |
546 | |
547 startLoad: function() { | |
548 metrics.startInterval('SuggestApps.LoadTime'); | |
549 }, | |
550 | |
551 finishLoad: function() { | |
552 metrics.recordInterval('SuggestApps.LoadTime'); | |
553 }, | |
554 }); | |
OLD | NEW |