Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / 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 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 {VolumeManagerCommon.DriveConnectionType} connectionType Current
345  *     connection type.
346  */
347 SuggestAppsDialog.prototype.onDriveConnectionChanged =
348     function(connectionType) {
349   if (this.state_ !== SuggestAppsDialog.State.UNINITIALIZED &&
350       connectionType === VolumeManagerCommon.DriveConnectionType.OFFLINE) {
351     this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING;
352     this.hide();
353   }
354 };
355
356 /**
357  * Called when receiving the install request from the webview client.
358  * @param {Event} e Event.
359  * @private
360  */
361 SuggestAppsDialog.prototype.onInstallRequest_ = function(e) {
362   var itemId = e.itemId;
363   this.installingItemId_ = itemId;
364
365   this.appInstaller_ = new AppInstaller(itemId);
366   this.appInstaller_.install(this.onInstallCompleted_.bind(this));
367
368   this.frame_.classList.add('show-spinner');
369   this.state_ = SuggestAppsDialog.State.INSTALLING;
370 };
371
372 /**
373  * Called when the installation is completed from the app installer.
374  * @param {AppInstaller.Result} result Result of the installation.
375  * @param {string} error Detail of the error.
376  * @private
377  */
378 SuggestAppsDialog.prototype.onInstallCompleted_ = function(result, error) {
379   var success = (result === AppInstaller.Result.SUCCESS);
380
381   this.frame_.classList.remove('show-spinner');
382   this.state_ = success ?
383                 SuggestAppsDialog.State.INSTALLED_CLOSING :
384                 SuggestAppsDialog.State.INITIALIZED;  // Back to normal state.
385   this.webviewClient_.onInstallCompleted(success, this.installingItemId_);
386   this.installingItemId_ = null;
387
388   switch (result) {
389   case AppInstaller.Result.SUCCESS:
390     SuggestAppsDialog.Metrics.recordInstall(
391         SuggestAppsDialog.Metrics.INSTALL.SUCCESS);
392     this.hide();
393     break;
394   case AppInstaller.Result.CANCELLED:
395     SuggestAppsDialog.Metrics.recordInstall(
396         SuggestAppsDialog.Metrics.INSTALL.CANCELLED);
397     // User cancelled the installation. Do nothing.
398     break;
399   case AppInstaller.Result.ERROR:
400     SuggestAppsDialog.Metrics.recordInstall(
401         SuggestAppsDialog.Metrics.INSTALL.FAILED);
402     fileManager.error.show(str('SUGGEST_DIALOG_INSTALLATION_FAILED'));
403     break;
404   }
405 };
406
407 /**
408  * @override
409  */
410 SuggestAppsDialog.prototype.hide = function(opt_originalOnHide) {
411   switch (this.state_) {
412     case SuggestAppsDialog.State.INSTALLING:
413       // Install is being aborted. Send the failure result.
414       // Cancels the install.
415       if (this.webviewClient_)
416         this.webviewClient_.onInstallCompleted(false, this.installingItemId_);
417       this.installingItemId_ = null;
418
419       // Assumes closing the dialog as canceling the install.
420       this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
421       break;
422     case SuggestAppsDialog.State.INITIALIZING:
423       SuggestAppsDialog.Metrics.recordLoad(
424           SuggestAppsDialog.Metrics.LOAD.CANCELLED);
425       this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
426       break;
427     case SuggestAppsDialog.State.INSTALLED_CLOSING:
428     case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING:
429     case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING:
430       // Do nothing.
431       break;
432     case SuggestAppsDialog.State.INITIALIZED:
433       this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
434       break;
435     default:
436       this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
437       console.error('Invalid state.');
438   }
439
440   if (this.webviewClient_) {
441     this.webviewClient_.dispose();
442     this.webviewClient_ = null;
443   }
444
445   this.webviewContainer_.removeChild(this.webview_);
446   this.webview_ = null;
447   this.extension_ = null;
448   this.mime_ = null;
449
450   FileManagerDialogBase.prototype.hide.call(
451       this,
452       this.onHide_.bind(this, opt_originalOnHide));
453 };
454
455 /**
456  * @param {function()=} opt_originalOnHide Original onHide function passed to
457  *     SuggestAppsDialog.hide().
458  * @private
459  */
460 SuggestAppsDialog.prototype.onHide_ = function(opt_originalOnHide) {
461   // Calls the callback after the dialog hides.
462   if (opt_originalOnHide)
463     opt_originalOnHide();
464
465   var result;
466   switch (this.state_) {
467     case SuggestAppsDialog.State.INSTALLED_CLOSING:
468       result = SuggestAppsDialog.Result.INSTALL_SUCCESSFUL;
469       SuggestAppsDialog.Metrics.recordCloseDialog(
470           SuggestAppsDialog.Metrics.CLOSE_DIALOG.ITEM_INSTALLED);
471       break;
472     case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING:
473       result = SuggestAppsDialog.Result.FAILED;
474       break;
475     case SuggestAppsDialog.State.CANCELED_CLOSING:
476       result = SuggestAppsDialog.Result.USER_CANCELL;
477       SuggestAppsDialog.Metrics.recordCloseDialog(
478           SuggestAppsDialog.Metrics.CLOSE_DIALOG.USER_CANCELL);
479       break;
480     case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING:
481       result = SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED;
482       SuggestAppsDialog.Metrics.recordCloseDialog(
483           SuggestAppsDialog.Metrics.CLOSE_DIALOG.WEB_STORE_LINK);
484       break;
485     default:
486       result = SuggestAppsDialog.Result.USER_CANCELL;
487       SuggestAppsDialog.Metrics.recordCloseDialog(
488           SuggestAppsDialog.Metrics.CLOSE_DIALOG.UNKNOWN_ERROR);
489       console.error('Invalid state.');
490   }
491   this.state_ = SuggestAppsDialog.State.UNINITIALIZED;
492
493   this.onDialogClosed_(result);
494 };
495
496 /**
497  * Utility methods and constants to record histograms.
498  */
499 SuggestAppsDialog.Metrics = Object.freeze({
500   LOAD: Object.freeze({
501     SUCCEEDED: 0,
502     CANCELLED: 1,
503     FAILED: 2,
504   }),
505
506   /**
507    * @param {SuggestAppsDialog.Metrics.LOAD} result Result of load.
508    */
509   recordLoad: function(result) {
510     if (0 <= result && result < 3)
511       metrics.recordEnum('SuggestApps.Load', result, 3);
512   },
513
514   CLOSE_DIALOG: Object.freeze({
515     UNKOWN_ERROR: 0,
516     ITEM_INSTALLED: 1,
517     USER_CANCELLED: 2,
518     WEBSTORE_LINK_OPENED: 3,
519   }),
520
521   /**
522    * @param {SuggestAppsDialog.Metrics.CLOSE_DIALOG} reason Reason of closing
523    * dialog.
524    */
525   recordCloseDialog: function(reason) {
526     if (0 <= reason && reason < 4)
527       metrics.recordEnum('SuggestApps.CloseDialog', reason, 4);
528   },
529
530   INSTALL: Object.freeze({
531     SUCCEEDED: 0,
532     CANCELLED: 1,
533     FAILED: 2,
534   }),
535
536   /**
537    * @param {SuggestAppsDialog.Metrics.INSTALL} result Result of installation.
538    */
539   recordInstall: function(result) {
540     if (0 <= result && result < 3)
541       metrics.recordEnum('SuggestApps.Install', result, 3);
542   },
543
544   recordShowDialog: function() {
545     metrics.recordUserAction('SuggestApps.ShowDialog');
546   },
547
548   startLoad: function() {
549     metrics.startInterval('SuggestApps.LoadTime');
550   },
551
552   finishLoad: function() {
553     metrics.recordInterval('SuggestApps.LoadTime');
554   },
555 });