Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / suggest_apps_dialog.js
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 methdo to shows 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 });