Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / components / chrome_apps / webstore_widget / cws_widget / cws_widget_container.js
blob14554b7cc422436b2afbad45ba06cf63858fbc3e
1 // Copyright 2015 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.
5 /**
6  * CWSWidgetContainer contains a Chrome Web Store widget that displays list of
7  * apps that satisfy certain constraints (e.g. fileHandler apps that can handle
8  * files with specific file extension or MIME type) and enables the user to
9  * install apps directly from it.
10  * CWSWidgetContainer implements client side of the widget, which handles
11  * widget loading and app installation.
12  */
14 /**
15  * The width of the widget (in pixels)
16  * @type {number}
17  * @const
18  */
19 var WEBVIEW_WIDTH = 735;
21 /**
22  * The height of the widget (in pixels).
23  * @type {number}
24  * @const
25  */
26 var WEBVIEW_HEIGHT = 480;
28 /**
29  * The URL of the widget showing suggested apps.
30  * @type {string}
31  * @const
32  */
33 var CWS_WIDGET_URL =
34     'https://clients5.google.com/webstore/wall/cros-widget-container';
36 /**
37  * The origin of the widget.
38  * @type {string}
39  * @const
40  */
41 var CWS_WIDGET_ORIGIN = 'https://clients5.google.com';
43 /**
44  * Creates the widget container element in DOM tree.
45  *
46  * @param {!HTMLDocument} document The document to contain this container.
47  * @param {!HTMLElement} parentNode Node to be parent for this container.
48  * @param {!CWSWidgetContainer.PlatformDelegate} delegate Delegate for accessing
49  *     Chrome platform APIs.
50  * @param {!{
51  *   overrideCwsContainerUrlForTest: (string|undefined),
52  *   overrideCwsContainerOriginForTest: (string|undefined)
53  * }} params Overrides for container params.
54  * @constructor
55  */
56 function CWSWidgetContainer(document, parentNode, delegate, params) {
57   /** @private {!CWSWidgetContainer.PlatformDelegate} */
58   this.delegate_ = delegate;
60   /** @private {!CWSWidgetContainer.MetricsRecorder} */
61   this.metricsRecorder_ =
62       new CWSWidgetContainer.MetricsRecorder(delegate.metricsImpl);
64   /**
65    * The document that will contain the container.
66    * @const {!HTMLDocument}
67    * @private
68    */
69   this.document_ = document;
71   /**
72    * The element containing the widget webview.
73    * @type {!Element}
74    * @private
75    */
76   this.webviewContainer_ = document.createElement('div');
77   this.webviewContainer_.classList.add('cws-widget-webview-container');
78   this.webviewContainer_.style.width = WEBVIEW_WIDTH + 'px';
79   this.webviewContainer_.style.height = WEBVIEW_HEIGHT + 'px';
80   parentNode.appendChild(this.webviewContainer_);
82   parentNode.classList.add('cws-widget-container-root');
84   /**
85    * Element showing spinner layout in place of Web Store widget.
86    * @type {!Element}
87    */
88   var spinnerLayer = document.createElement('div');
89   spinnerLayer.className = 'cws-widget-spinner-layer';
90   parentNode.appendChild(spinnerLayer);
92   /** @private {!CWSWidgetContainer.SpinnerLayerController} */
93   this.spinnerLayerController_ =
94       new CWSWidgetContainer.SpinnerLayerController(spinnerLayer);
96   /**
97    * The widget container's button strip.
98    * @type {!Element}
99    */
100   var buttons = document.createElement('div');
101   buttons.classList.add('cws-widget-buttons');
102   parentNode.appendChild(buttons);
104   /**
105    * Button that opens the Webstore URL.
106    * @const {!Element}
107    * @private
108    */
109   this.webstoreButton_ = document.createElement('div');
110   this.webstoreButton_.hidden = true;
111   this.webstoreButton_.setAttribute('role', 'button');
112   this.webstoreButton_.tabIndex = 0;
114   /**
115    * Icon for the Webstore button.
116    * @type {!Element}
117    */
118   var webstoreButtonIcon = this.document_.createElement('span');
119   webstoreButtonIcon.classList.add('cws-widget-webstore-button-icon');
120   this.webstoreButton_.appendChild(webstoreButtonIcon);
122   /**
123    * The label for the Webstore button.
124    * @type {!Element}
125    */
126   var webstoreButtonLabel = this.document_.createElement('span');
127   webstoreButtonLabel.classList.add('cws-widget-webstore-button-label');
128   webstoreButtonLabel.textContent = this.delegate_.strings.LINK_TO_WEBSTORE;
129   this.webstoreButton_.appendChild(webstoreButtonLabel);
131   this.webstoreButton_.addEventListener(
132       'click', this.onWebstoreLinkActivated_.bind(this));
133   this.webstoreButton_.addEventListener(
134       'keydown', this.onWebstoreLinkKeyDown_.bind(this));
136   buttons.appendChild(this.webstoreButton_);
138   /**
139    * The webview element containing the Chrome Web Store widget.
140    * @type {?WebView}
141    * @private
142    */
143   this.webview_ = null;
145   /**
146    * The Chrome Web Store widget URL.
147    * @const {string}
148    * @private
149    */
150   this.widgetUrl_ = params.overrideCwsContainerUrlForTest || CWS_WIDGET_URL;
152   /**
153    * The Chrome Web Store widget origin.
154    * @const {string}
155    * @private
156    */
157   this.widgetOrigin_ = params.overrideCwsContainerOriginForTest ||
158       CWS_WIDGET_ORIGIN;
160   /**
161    * Map of options for the widget.
162    * @type {?Object<*>}
163    * @private
164    */
165   this.options_ = null;
167   /**
168    * The ID of the item being installed. Null if no items are being installed.
169    * @type {?string}
170    * @private
171    */
172   this.installingItemId_ = null;
174   /**
175    * The ID of the the installed item. Null if no item was installed.
176    * @type {?string}
177    * @private
178    */
179   this.installedItemId_ = null;
181   /**
182    * The current widget state.
183    * @type {CWSWidgetContainer.State}
184    * @private
185    */
186   this.state_ = CWSWidgetContainer.State.UNINITIALIZED;
188   /**
189    * The Chrome Web Store access token to be used when communicating with the
190    * Chrome Web Store widget.
191    * @type {?string}
192    * @private
193    */
194   this.accessToken_ = null;
196   /**
197    * Called when the Chrome Web Store widget is done. It resolves the promise
198    * returned by {@code this.start()}.
199    * @type {?function(CWSWidgetContainer.ResolveReason)}
200    * @private
201    */
202   this.resolveStart_ = null;
204   /**
205    * Promise for retriving {@code this.accessToken_}.
206    * @type {Promise.<string>}
207    * @private
208    */
209   this.tokenGetter_ = this.createTokenGetter_();
211   /**
212    * Dialog to be shown when an installation attempt fails.
213    * @type {CWSWidgetContainerErrorDialog}
214    * @private
215    */
216   this.errorDialog_ = new CWSWidgetContainerErrorDialog(parentNode);
220  * Strings required by the widget container.
221  * @typedef {{
222  *   UI_LOCALE: string,
223  *   LINK_TO_WEBSTORE: string,
224  *   INSTALLATION_FAILED_MESSAGE: string,
225  *   LOADING_SPINNER_ALT: string,
226  *   INSTALLING_SPINNER_ALT: string
227  * }}
228  */
229 CWSWidgetContainer.Strings;
232  * Functions for reporting metrics for the widget.
233  * @typedef {{
234  *   recordEnum: function(string, number, number),
235  *   recordUserAction: function(string),
236  *   startInterval: function(string),
237  *   recordInterval: function(string)
238  * }}
239  */
240 CWSWidgetContainer.MetricsImpl;
243  * Type for delegate used by CWSWidgetContainer component to access Chrome
244  * platform APIs.
245  * @typedef {{
246  *   strings: !CWSWidgetContainer.Strings,
247  *   metricsImpl: !CWSWidgetContainer.MetricsImpl,
248  *   installWebstoreItem: function(string, function(?string)),
249  *   getInstalledItems: function(function(?Array<!string>)),
250  *   requestWebstoreAccessToken: function(function(?string))
251  * }}
252  */
253 CWSWidgetContainer.PlatformDelegate;
256  * @enum {string}
257  * @private
258  */
259 CWSWidgetContainer.State = {
260   UNINITIALIZED: 'CWSWidgetContainer.State.UNINITIALIZED',
261   GETTING_ACCESS_TOKEN: 'CWSWidgetContainer.State.GETTING_ACCESS_TOKEN',
262   ACCESS_TOKEN_READY: 'CWSWidgetContainer.State.ACCESS_TOKEN_READY',
263   INITIALIZING: 'CWSWidgetContainer.State.INITIALIZING',
264   INITIALIZE_FAILED_CLOSING:
265       'CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING',
266   INITIALIZED: 'CWSWidgetContainer.State.INITIALIZED',
267   INSTALLING: 'CWSWidgetContainer.State.INSTALLING',
268   WAITING_FOR_CONFIRMATION: 'CWSWidgetContainer.State.WAITING_FOR_CONFIRMATION',
269   INSTALLED_CLOSING: 'CWSWidgetContainer.State.INSTALLED_CLOSING',
270   OPENING_WEBSTORE_CLOSING: 'CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING',
271   CANCELED_CLOSING: 'CWSWidgetContainer.State.CANCELED_CLOSING'
273 Object.freeze(CWSWidgetContainer.State);
276  * @enum {string}
277  * @const
278  */
279 CWSWidgetContainer.Result = {
280   /** Install is done. The install app should be opened. */
281   INSTALL_SUCCESSFUL: 'CWSWidgetContainer.Result.INSTALL_SUCCESSFUL',
282   /** User cancelled the suggest app dialog. No message should be shown. */
283   USER_CANCEL: 'CWSWidgetContainer.Result.USER_CANCEL',
284   /** User clicked the link to web store so the dialog is closed. */
285   WEBSTORE_LINK_OPENED: 'CWSWidgetContainer.Result.WEBSTORE_LINK_OPENED',
286   /** Failed to load the widget. Error message should be shown. */
287   FAILED: 'CWSWidgetContainer.Result.FAILED'
289 Object.freeze(CWSWidgetContainer.Result);
292  * The reason due to which the container is resolving {@code this.start}
293  * promise.
294  * @enum {string}
295  */
296 CWSWidgetContainer.ResolveReason = {
297   /** The widget container ended up in its final state. */
298   DONE: 'CWSWidgetContainer.ResolveReason.DONE',
299   /** The widget container is being reset. */
300   RESET: 'CWSWidgetContainer.CloserReason.RESET'
302 Object.freeze(CWSWidgetContainer.ResolveReason);
305  * @return {!Element} The element that should be focused initially.
306  */
307 CWSWidgetContainer.prototype.getInitiallyFocusedElement = function() {
308   return this.webviewContainer_;
312  * Injects headers into the passed request.
314  * @param {!Object} e Request event.
315  * @return {!BlockingResponse} Modified headers.
316  * @private
317  */
318 CWSWidgetContainer.prototype.authorizeRequest_ = function(e) {
319   e.requestHeaders.push({
320     name: 'Authorization',
321     value: 'Bearer ' + this.accessToken_
322   });
323   return /** @type {!BlockingResponse}*/ ({requestHeaders: e.requestHeaders});
327  * Retrieves the authorize token.
328  * @return {Promise.<string>} The promise with the retrived access token.
329  * @private
330  */
331 CWSWidgetContainer.prototype.createTokenGetter_ = function() {
332   return new Promise(function(resolve, reject) {
333     if (window.IN_TEST) {
334       // In test, use a dummy string as token. This must be a non-empty string.
335       resolve('DUMMY_ACCESS_TOKEN_FOR_TEST');
336       return;
337     }
339     // Fetch or update the access token.
340     this.delegate_.requestWebstoreAccessToken(
341         /** @param {?string} accessToken The requested token. Null on error. */
342         function(accessToken) {
343           if (!accessToken) {
344             reject('Error retriveing Web Store access token.');
345             return;
346           }
347           resolve(accessToken)
348         });
349   }.bind(this));
353  * @return {boolean} Whether the container is in initial state, i.e. inactive.
354  */
355 CWSWidgetContainer.prototype.isInInitialState = function() {
356   return this.state_ === CWSWidgetContainer.State.UNINITIALIZED;
360  * Ensures that the widget container is in the state where it can properly
361  * handle showing the Chrome Web Store webview.
362  * @return {Promise} Resolved when the container is ready to be used.
363  */
364 CWSWidgetContainer.prototype.ready = function() {
365   return new Promise(function(resolve, reject) {
366     if (this.state_ !== CWSWidgetContainer.State.UNINITIALIZED) {
367       reject('Invalid state.');
368       return;
369     }
371     this.spinnerLayerController_.setAltText(
372         this.delegate_.strings.LOADING_SPINNER_ALT);
373     this.spinnerLayerController_.setVisible(true);
375     this.metricsRecorder_.recordShowDialog();
376     this.metricsRecorder_.startLoad();
378     this.state_ = CWSWidgetContainer.State.GETTING_ACCESS_TOKEN;
380     this.tokenGetter_.then(function(accessToken) {
381       this.state_ = CWSWidgetContainer.State.ACCESS_TOKEN_READY;
382       this.accessToken_ = accessToken;
383       resolve();
384     }.bind(this), function(error) {
385       this.spinnerLayerController_.setVisible(false);
386       this.state_ = CWSWidgetContainer.State.UNINITIALIZED;
387       reject('Failed to get Web Store access token: ' + error);
388     }.bind(this));
389   }.bind(this));
393  * Initializes and starts loading the Chrome Web Store widget webview.
394  * Must not be called before {@code this.ready()} is resolved.
396  * @param {!Object<*>} options Map of options for the dialog.
397  * @param {?string} webStoreUrl Url for more results. Null if not supported.
398  * @return {!Promise.<CWSWidgetContainer.ResolveReason>} Resolved when app
399  *     installation is done, or the installation is cancelled.
400  */
401 CWSWidgetContainer.prototype.start = function(options, webStoreUrl) {
402   return new Promise(function(resolve, reject) {
403     if (this.state_ !== CWSWidgetContainer.State.ACCESS_TOKEN_READY) {
404       this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
405       reject('Invalid state in |start|.');
406       return;
407     }
409     if (!this.accessToken_) {
410       this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
411       reject('No access token.');
412       return;
413     }
415     this.resolveStart_ = resolve;
417     this.state_ = CWSWidgetContainer.State.INITIALIZING;
419     this.webStoreUrl_ = webStoreUrl;
420     this.options_ = options;
422     this.webstoreButton_.hidden = !webStoreUrl;
423     this.webstoreButton_.classList.toggle('cws-widget-webstore-button',
424                                           !!webStoreUrl);
426     this.webview_ =
427         /** @type {!WebView} */(this.document_.createElement('webview'));
428     this.webview_.id = 'cws-widget';
429     this.webview_.partition = 'persist:cwswidgets';
430     this.webview_.style.width = WEBVIEW_WIDTH + 'px';
431     this.webview_.style.height = WEBVIEW_HEIGHT + 'px';
432     this.webview_.request.onBeforeSendHeaders.addListener(
433         this.authorizeRequest_.bind(this),
434         /** @type {!RequestFilter}*/ ({urls: [this.widgetOrigin_ + '/*']}),
435         ['blocking', 'requestHeaders']);
436     this.webview_.addEventListener('newwindow', function(event) {
437       event = /** @type {NewWindowEvent} */ (event);
438       // Discard the window object and reopen in an external window.
439       event.window.discard();
440       window.open(event.targetUrl);
441       event.preventDefault();
442     });
443     this.webviewContainer_.appendChild(this.webview_);
445     this.spinnerLayerController_.setElementToFocusOnHide(this.webview_);
446     this.spinnerLayerController_.setAltText(
447         this.delegate_.strings.LOADING_SPINNER_ALT);
448     this.spinnerLayerController_.setVisible(true);
450     this.webviewClient_ = new CWSContainerClient(
451         this.webview_,
452         WEBVIEW_WIDTH,
453         WEBVIEW_HEIGHT,
454         this.widgetUrl_,
455         this.widgetOrigin_,
456         this.options_,
457         this.delegate_);
458     this.webviewClient_.addEventListener(CWSContainerClient.Events.LOADED,
459                                          this.onWidgetLoaded_.bind(this));
460     this.webviewClient_.addEventListener(CWSContainerClient.Events.LOAD_FAILED,
461                                          this.onWidgetLoadFailed_.bind(this));
462     this.webviewClient_.addEventListener(
463         CWSContainerClient.Events.REQUEST_INSTALL,
464         this.onInstallRequest_.bind(this));
465     this.webviewClient_.addEventListener(
466         CWSContainerClient.Events.INSTALL_DONE,
467         this.onInstallDone_.bind(this));
468     this.webviewClient_.load();
469   }.bind(this));
473  * Called when the 'See more...' button is activated. It opens
474  * {@code this.webstoreUrl_}.
475  * @param {Event} e The event that activated the link. Either mouse click or
476  *     key down event.
477  * @private
478  */
479 CWSWidgetContainer.prototype.onWebstoreLinkActivated_ = function(e) {
480   if (!this.webStoreUrl_)
481     return;
482   window.open(this.webStoreUrl_);
483   this.state_ = CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING;
484   this.reportDone_();
488  * Key down event handler for webstore button element. If the key is enter, it
489  * activates the button.
490  * @param {Event} e The event
491  * @private
492  */
493 CWSWidgetContainer.prototype.onWebstoreLinkKeyDown_ = function(e) {
494   if (e.keyCode !== 13 /* Enter */)
495     return;
496   this.onWebstoreLinkActivated_(e);
500  * Called when the widget is loaded successfully.
501  * @param {Event} event Event.
502  * @private
503  */
504 CWSWidgetContainer.prototype.onWidgetLoaded_ = function(event) {
505   this.metricsRecorder_.finishLoad();
506   this.metricsRecorder_.recordLoad(
507       CWSWidgetContainer.MetricsRecorder.LOAD.SUCCEEDED);
509   this.state_ = CWSWidgetContainer.State.INITIALIZED;
511   this.spinnerLayerController_.setVisible(false);
512   this.webview_.focus();
516  * Called when the widget is failed to load.
517  * @param {Event} event Event.
518  * @private
519  */
520 CWSWidgetContainer.prototype.onWidgetLoadFailed_ = function(event) {
521   this.metricsRecorder_.recordLoad(
522       CWSWidgetContainer.MetricsRecorder.LOAD.FAILED);
524   this.spinnerLayerController_.setVisible(false);
525   this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
526   this.reportDone_();
530  * Called when the connection status is changed to offline.
531  */
532 CWSWidgetContainer.prototype.onConnectionLost = function() {
533   if (this.state_ !== CWSWidgetContainer.State.UNINITIALIZED) {
534     this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
535     this.reportDone_();
536   }
540  * Called when receiving the install request from the webview client.
541  * @param {Event} e Event.
542  * @private
543  */
544 CWSWidgetContainer.prototype.onInstallRequest_ = function(e) {
545   var itemId = e.itemId;
546   this.installingItemId_ = itemId;
548   this.appInstaller_ = new AppInstaller(itemId, this.delegate_);
549   this.appInstaller_.install(this.onItemInstalled_.bind(this));
551   this.spinnerLayerController_.setAltText(
552       this.delegate_.strings.INSTALLING_SPINNER_ALT);
553   this.spinnerLayerController_.setVisible(true);
554   this.state_ = CWSWidgetContainer.State.INSTALLING;
558  * Called when the webview client receives install confirmation from the
559  * Web Store widget.
560  * @param {Event} e Event
561  * @private
562  */
563 CWSWidgetContainer.prototype.onInstallDone_ = function(e) {
564   this.spinnerLayerController_.setVisible(false);
565   this.state_ = CWSWidgetContainer.State.INSTALLED_CLOSING;
566   this.reportDone_();
570  * Called when the installation is completed from the app installer.
571  * @param {AppInstaller.Result} result Result of the installation.
572  * @param {string} error Detail of the error.
573  * @private
574  */
575 CWSWidgetContainer.prototype.onItemInstalled_ = function(result, error) {
576   var success = (result === AppInstaller.Result.SUCCESS);
578   // If install succeeded, the spinner will be removed once
579   // |this.webviewClient_| dispatched INSTALL_DONE event.
580   if (!success)
581     this.spinnerLayerController_.setVisible(false);
583   this.state_ = success ?
584                 CWSWidgetContainer.State.WAITING_FOR_CONFIRMATION :
585                 CWSWidgetContainer.State.INITIALIZED;  // Back to normal state.
586   this.webviewClient_.onInstallCompleted(success, this.installingItemId_);
587   this.installedItemId_ = this.installingItemId_;
588   this.installingItemId_ = null;
590   switch (result) {
591     case AppInstaller.Result.SUCCESS:
592       this.metricsRecorder_.recordInstall(
593           CWSWidgetContainer.MetricsRecorder.INSTALL.SUCCEEDED);
594       // Wait for the widget webview container to dispatch INSTALL_DONE.
595       break;
596     case AppInstaller.Result.CANCELLED:
597       this.metricsRecorder_.recordInstall(
598           CWSWidgetContainer.MetricsRecorder.INSTALL.CANCELLED);
599       // User cancelled the installation. Do nothing.
600       break;
601     case AppInstaller.Result.ERROR:
602       this.metricsRecorder_.recordInstall(
603           CWSWidgetContainer.MetricsRecorder.INSTALL.FAILED);
604       this.errorDialog_.show(
605           this.delegate_.strings.INSTALLATION_FAILED_MESSAGE,
606           null,
607           null,
608           null);
609       break;
610   }
614  * Resolves the promise returned by {@code this.start} when widget is done with
615  * installing apps.
616  * @private
617  */
618 CWSWidgetContainer.prototype.reportDone_ = function() {
619   if (this.resolveStart_)
620     this.resolveStart_(CWSWidgetContainer.ResolveReason.DONE);
621   this.resolveStart_ = null;
625  * Finalizes the widget container state and returns the final app instalation
626  * result. The widget should not be used after calling this. If called before
627  * promise returned by {@code this.start} is resolved, the reported result will
628  * be as if the widget was cancelled.
629  * @return {{result: CWSWidgetContainer.Result, installedItemId: ?string}}
630  */
631 CWSWidgetContainer.prototype.finalizeAndGetResult = function() {
632   switch (this.state_) {
633     case CWSWidgetContainer.State.INSTALLING:
634       // Install is being aborted. Send the failure result.
635       // Cancels the install.
636       if (this.webviewClient_)
637         this.webviewClient_.onInstallCompleted(false, this.installingItemId_);
638       this.installingItemId_ = null;
640       // Assumes closing the dialog as canceling the install.
641       this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
642       break;
643     case CWSWidgetContainer.State.GETTING_ACCESS_TOKEN:
644     case CWSWidgetContainer.State.ACCESS_TOKEN_READY:
645     case CWSWidgetContainer.State.INITIALIZING:
646       this.metricsRecorder_.recordLoad(
647           CWSWidgetContainer.MetricsRecorder.LOAD.CANCELLED);
648       this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
649       break;
650     case CWSWidgetContainer.State.WAITING_FOR_CONFIRMATION:
651       // This can happen if the dialog is closed by the user before Web Store
652       // widget replies with 'after_install'.
653       // Consider this success, as the app has actually been installed.
654       // TODO(tbarzic): Should the app be uninstalled in this case?
655       this.state_ = CWSWidgetContainer.State.INSTALLED_CLOSING;
656       break;
657     case CWSWidgetContainer.State.INSTALLED_CLOSING:
658     case CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING:
659     case CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING:
660       // Do nothing.
661       break;
662     case CWSWidgetContainer.State.INITIALIZED:
663       this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
664       break;
665     default:
666       this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
667       console.error('Invalid state.');
668   }
670   var result;
671   switch (this.state_) {
672     case CWSWidgetContainer.State.INSTALLED_CLOSING:
673       result = CWSWidgetContainer.Result.INSTALL_SUCCESSFUL;
674       this.metricsRecorder_.recordCloseDialog(
675           CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.ITEM_INSTALLED);
676       break;
677     case CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING:
678       result = CWSWidgetContainer.Result.FAILED;
679       break;
680     case CWSWidgetContainer.State.CANCELED_CLOSING:
681       result = CWSWidgetContainer.Result.USER_CANCEL;
682       this.metricsRecorder_.recordCloseDialog(
683           CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.USER_CANCELLED);
684       break;
685     case CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING:
686       result = CWSWidgetContainer.Result.WEBSTORE_LINK_OPENED;
687       this.metricsRecorder_.recordCloseDialog(
688           CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.WEBSTORE_LINK_OPENED);
689       break;
690     default:
691       result = CWSWidgetContainer.Result.USER_CANCEL;
692       this.metricsRecorder_.recordCloseDialog(
693           CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.UNKNOWN_ERROR);
694   }
696   this.state_ = CWSWidgetContainer.State.UNINITIALIZED;
698   this.reset_();
700   return {result: result, installedItemId: this.installedItemId_};
704  * Resets the widget.
705  * @private
706  */
707 CWSWidgetContainer.prototype.reset_ = function () {
708   if (this.state_ !== CWSWidgetContainer.State.UNINITIALIZED)
709     console.error('Widget reset before its state was finalized.');
711   if (this.resolveStart_) {
712     this.resolveStart_(CWSWidgetContainer.ResolveReason.RESET);
713     this.resolveStart_ = null;
714   }
716   this.spinnerLayerController_.reset();
718   if (this.webviewClient_) {
719     this.webviewClient_.dispose();
720     this.webviewClient_ = null;
721   }
723   if (this.webview_) {
724     this.webviewContainer_.removeChild(this.webview_);
725     this.webview_ = null;
726   }
728   if (this.appInstaller_) {
729     this.appInstaller_.cancel();
730     this.appInstaller_ = null;
731   }
733   this.options_ = null;
735   if (this.errorDialog_.shown())
736     this.errorDialog_.hide();
740  * Controls showing and hiding spinner layer.
741  * @param {!Element} spinnerLayer The spinner layer element.
742  * @constructor
743  */
744 CWSWidgetContainer.SpinnerLayerController = function(spinnerLayer) {
745   /** @private {!Element} */
746   this.spinnerLayer_ = spinnerLayer;
748   /** @private {boolean} */
749   this.visible_ = false;
751   /**
752    * Set only if spinner is transitioning between visible and hidden states.
753    * Calling the function clears event handlers set for handling the transition,
754    * and updates spinner layer class list to its final state.
755    * @type {?function()}
756    * @private
757    */
758   this.clearTransition_ = null;
760   /**
761    * Reference to the timeout set to ensure {@code this.clearTransision_} gets
762    * called even if 'transitionend' event does not fire.
763    * @type {?number}
764    * @private
765    */
766   this.clearTransitionTimeout_ = null;
768   /**
769    * Element to be focused when the layer is hidden.
770    * @type {Element}
771    * @private
772    */
773   this.focusOnHide_ = null;
775   spinnerLayer.tabIndex = -1;
777   // Prevent default Tab key handling in order to prevent the widget from
778   // taking the focus while the spinner layer is active.
779   // NOTE: This assumes that there are no elements allowed to become active
780   // while the spinner is shown. Something smarter would be needed if this
781   // assumption becomes invalid.
782   spinnerLayer.addEventListener('keydown', this.handleKeyDown_.bind(this));
786  * Sets element to be focused when the layer is hidden.
787  * @param {!Element} el
788  */
789 CWSWidgetContainer.SpinnerLayerController.prototype.setElementToFocusOnHide =
790     function(el) {
791   this.focusOnHide_ = el;
795  * Prevents default Tab key handling in order to prevent spinner layer from
796  * losing focus.
797  * @param {Event} e The key down event.
798  * @private
799  */
800 CWSWidgetContainer.SpinnerLayerController.prototype.handleKeyDown_ =
801     function(e) {
802   if (!this.visible_)
803     return;
804   if (e.keyCode === 9 /* Tab */)
805     e.preventDefault();
809  * Resets the spinner layer controllers state, and makes sure the spinner
810  * layre gets hidden.
811  */
812 CWSWidgetContainer.SpinnerLayerController.prototype.reset = function() {
813   this.visible_ = false;
814   this.focusOnHide_ = null;
815   if (this.clearTransision_)
816     this.clearTransition_();
820  * Sets alt text for the spinner layer.
821  * @param {string} text
822  */
823 CWSWidgetContainer.SpinnerLayerController.prototype.setAltText = function(
824     text) {
825   this.spinnerLayer_.setAttribute('aria-label', text);
829  * Shows or hides the spinner layer and handles the layer's opacity transition.
830  * @param {boolean} visible Whether the layer should become visible.
831  */
832 CWSWidgetContainer.SpinnerLayerController.prototype.setVisible =
833     function(visible) {
834   if (this.visible_ === visible)
835     return;
837   if (this.clearTransition_)
838     this.clearTransition_();
840   this.visible_ = visible;
842   // Spinner should be shown during transition.
843   this.spinnerLayer_.classList.toggle('cws-widget-show-spinner', true);
845   if (this.visible_) {
846     this.spinnerLayer_.focus();
847    } else if (this.focusOnHide_) {
848         this.focusOnHide_.focus();
849    }
851   if (!this.visible_)
852     this.spinnerLayer_.classList.add('cws-widget-hiding-spinner');
854   this.clearTransition_ = function() {
855     if (this.clearTransitionTimeout_)
856       clearTimeout(this.clearTransitionTimeout_);
857     this.clearTransitionTimeout_ = null;
859     this.spinnerLayer_.removeEventListener(
860         'transitionend', this.clearTransition_);
861     this.clearTransition_ = null;
863     if (!this.visible_) {
864       this.spinnerLayer_.classList.remove('cws-widget-hiding-spinner');
865       this.spinnerLayer_.classList.remove('cws-widget-show-spinner');
866     }
867   }.bind(this);
869   this.spinnerLayer_.addEventListener('transitionend', this.clearTransition_);
871   // Ensure the transition state gets cleared, even if transitionend is not
872   // fired.
873   this.clearTransitionTimeout_ = setTimeout(function() {
874     this.clearTransitionTimeout_ = null;
875     this.clearTransition_();
876   }.bind(this), 550 /* ms */);
880  * Utility methods and constants to record histograms.
881  * @param {!CWSWidgetContainer.MetricsImpl} metricsImpl
882  * @constructor
883  */
884 CWSWidgetContainer.MetricsRecorder = function(metricsImpl) {
885   /** @private {!CWSWidgetContainer.MetricsImpl} */
886   this.metricsImpl_ = metricsImpl;
890  * @enum {number}
891  * @const
892  */
893 CWSWidgetContainer.MetricsRecorder.LOAD = {
894   SUCCEEDED: 0,
895   CANCELLED: 1,
896   FAILED: 2,
900  * @enum {number}
901  * @const
902  */
903 CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG = {
904   UNKNOWN_ERROR: 0,
905   ITEM_INSTALLED: 1,
906   USER_CANCELLED: 2,
907   WEBSTORE_LINK_OPENED: 3,
911  * @enum {number}
912  * @const
913  */
914 CWSWidgetContainer.MetricsRecorder.INSTALL = {
915   SUCCEEDED: 0,
916   CANCELLED: 1,
917   FAILED: 2,
921  * @param {number} result Result of load, which must be defined in
922  *     CWSWidgetContainer.MetricsRecorder.LOAD.
923  */
924 CWSWidgetContainer.MetricsRecorder.prototype.recordLoad = function(result) {
925   if (0 <= result && result < 3)
926     this.metricsImpl_.recordEnum('Load', result, 3);
930  * @param {number} reason Reason of closing dialog, which must be defined in
931  *     CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.
932  */
933 CWSWidgetContainer.MetricsRecorder.prototype.recordCloseDialog = function(
934     reason) {
935   if (0 <= reason && reason < 4)
936     this.metricsImpl_.recordEnum('CloseDialog', reason, 4);
940  * @param {number} result Result of installation, which must be defined in
941  *     CWSWidgetContainer.MetricsRecorder.INSTALL.
942  */
943 CWSWidgetContainer.MetricsRecorder.prototype.recordInstall = function(result) {
944   if (0 <= result && result < 3)
945     this.metricsImpl_.recordEnum('Install', result, 3);
948 CWSWidgetContainer.MetricsRecorder.prototype.recordShowDialog = function() {
949   this.metricsImpl_.recordUserAction('ShowDialog');
952 CWSWidgetContainer.MetricsRecorder.prototype.startLoad = function() {
953   this.metricsImpl_.startInterval('LoadTime');
956 CWSWidgetContainer.MetricsRecorder.prototype.finishLoad = function() {
957   this.metricsImpl_.recordInterval('LoadTime');