blob: 83b415bcce9a2964fb91b02a6247dfd0773a2ff2 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* SuggestAppsDialog contains a list box to select an app to be opened the file
* with. This dialog should be used as action picker for file operations.
*/
/**
* Creates dialog in DOM tree.
*
* @param {!ProvidersModel} providersModel Model for providers.
* @param {!HTMLElement} parentNode Node to be parent for this dialog.
* @param {!SuggestAppDialogState} state Static state of suggest app dialog.
* @constructor
* @extends {FileManagerDialogBase}
*/
function SuggestAppsDialog(providersModel, parentNode, state) {
FileManagerDialogBase.call(this, parentNode);
/**
* @private {!ProvidersModel}
* @const
*/
this.providersModel_ = providersModel;
this.frame_.id = 'suggest-app-dialog';
/**
* The root element for the Chrome Web Store widget container.
* @const {!HTMLElement}
*/
const widgetRoot = this.document_.createElement('div');
this.frame_.insertBefore(widgetRoot, this.text_.nextSibling);
/**
* The wrapper around Chrome Web Store widget.
* @const {!CWSWidgetContainer}
* @private
*/
this.widget_ = new CWSWidgetContainer(
this.document_, widgetRoot, this.createWidgetPlatformDelegate_(), state);
this.initialFocusElement_ = this.widget_.getInitiallyFocusedElement();
/**
* The reported widget result.
* @type {SuggestAppsDialog.Result}
* @private
*/
this.result_ = SuggestAppsDialog.Result.FAILED;
// Hide default dialog buttons.
this.buttons.hidden = true;
// Override default dialog styles.
this.title_.classList.add('suggest-apps-dialog-title');
this.text_.classList.add('suggest-apps-dialog-text');
}
SuggestAppsDialog.prototype = {
__proto__: FileManagerDialogBase.prototype
};
/**
* @enum {string}
*/
SuggestAppsDialog.Result = {
// Install is done. The install app should be opened.
SUCCESS: 'SuggestAppsDialog.Result.SUCCESS',
// User cancelled the suggest app dialog. No message should be shown.
CANCELLED: 'SuggestAppsDialog.Result.CANCELLED',
// Failed to load the widget. Error message should be shown.
FAILED: 'SuggestAppsDialog.Result.FAILED'
};
Object.freeze(SuggestAppsDialog.Result);
/**
* Dummy function for SuggestAppsDialog.show() not to be called unintentionally.
*/
SuggestAppsDialog.prototype.show = () => {
console.error('SuggestAppsDialog.show() shouldn\'t be called directly.');
};
/**
* Shows suggest-apps dialog by file extension and mime.
*
* @param {string} extension Extension of the file with a trailing dot.
* @param {?string} mime Mime of the file.
* @param {function(SuggestAppsDialog.Result, ?string)} onDialogClosed Called
* when the dialog is closed, with a result code and an optionally an
* extension id, if an extension was installed.
*/
SuggestAppsDialog.prototype.showByExtensionAndMime = function(
extension, mime, onDialogClosed) {
assert(extension && extension[0] === '.');
const options = {file_extension: extension.substr(1)};
if (mime) {
options.mime_type = mime;
}
this.showInternal_(
options, str('SUGGEST_DIALOG_TITLE'),
webStoreUtils.createWebStoreLink(extension, mime), onDialogClosed);
};
/**
* Shows suggest-apps dialog for FSP API
* @param {function(SuggestAppsDialog.Result, ?string)} onDialogClosed Called
* when the dialog is closed, with a result code and an optionally an
* extension id, if an extension was installed.
*/
SuggestAppsDialog.prototype.showProviders = function(onDialogClosed) {
this.showInternal_(
{file_system_provider: true}, str('SUGGEST_DIALOG_FOR_PROVIDERS_TITLE'),
null /* webStoreUrl */, onDialogClosed);
};
/**
* Creates platform delegate for CWSWidgetContainer.
* @return {!CWSWidgetContainerPlatformDelegate}
* @private
*/
SuggestAppsDialog.prototype.createWidgetPlatformDelegate_ = function() {
return {
strings: {
UI_LOCALE: util.getCurrentLocaleOrDefault(),
LINK_TO_WEBSTORE: str('SUGGEST_DIALOG_LINK_TO_WEBSTORE'),
INSTALLATION_FAILED_MESSAGE: str('SUGGEST_DIALOG_INSTALLATION_FAILED'),
LOADING_SPINNER_ALT: str('SUGGEST_DIALOG_LOADING_SPINNER_ALT'),
INSTALLING_SPINNER_ALT: str('SUGGEST_DIALOG_INSTALLING_SPINNER_ALT')
},
metricsImpl: {
/**
* @param {string} enumName
* @param {number} value
* @param {number} enumSize
*/
recordEnum: function(enumName, value, enumSize) {
metrics.recordEnum('SuggestApps.' + enumName, value, enumSize);
},
/** @param {string} actionName */
recordUserAction: function(actionName) {
metrics.recordUserAction('SuggestApps.' + actionName);
},
/** @param {string} intervalName */
startInterval: function(intervalName) {
metrics.startInterval('SuggestApps.' + intervalName);
},
/** @param {string} intervalName */
recordInterval: function(intervalName) {
metrics.recordInterval('SuggestApps.' + intervalName);
}
},
/**
* @param {string} itemId,
* @param {function(?string)} callback Callback argument is set to error
* message (null on success)
*/
installWebstoreItem: function(itemId, callback) {
chrome.webstoreWidgetPrivate.installWebstoreItem(
itemId, false /* show installation prompt */, () => {
callback(
chrome.runtime.lastError ?
chrome.runtime.lastError.message || 'UNKNOWN ERROR' :
null);
});
},
/**
* @param {function(?Array<!string>)} callback Callback
* argument is a list of installed item ids (null on error).
*/
getInstalledItems: callback => {
// Return only installed provided extensions. Returning other
// extensions/apps is redundant, as the suggest app for non-providers is
// executed only when there is no extension/app matching a file task.
// Hence, none of the suggested extensions/apps can be already installed.
this.providersModel_.getInstalledProviders()
.then(providers => {
callback(providers.map(provider => {
// Assume that the provider is an extension backed provider. In
// such case the providerId is the same as extensionId.
return provider.providerId;
}));
})
.catch(error => {
console.error(error.stack || error);
callback(null);
});
},
/**
* @param {function(?string)} callback Callback argument is the requested
* token (null on error).
*/
requestWebstoreAccessToken: function(callback) {
chrome.fileManagerPrivate.requestWebStoreAccessToken(token => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
callback(null);
return;
}
callback(assert(token));
});
}
};
};
/**
* Internal method to show a dialog. This should be called only from 'Suggest.
* appDialog.showXxxx()' functions.
*
* @param {!Object<*>} options Map of options for the dialog.
* @param {string} title Title of the dialog.
* @param {?string} webStoreUrl Url for more results. Null if not supported.
* @param {function(SuggestAppsDialog.Result, ?string)} onDialogClosed Called
* when the dialog is closed, with a result code and an optionally an
* extension id, if an extension was installed.
* @private
*/
SuggestAppsDialog.prototype.showInternal_ = function(
options, title, webStoreUrl, onDialogClosed) {
this.text_.hidden = true;
this.dialogText_ = '';
if (!this.widget_.isInInitialState()) {
onDialogClosed(SuggestAppsDialog.Result.CANCELLED, null);
return;
}
let dialogShown = false;
let tokenObtained = false;
this.widget_.ready()
.then(/** @return {!Promise} */
() => {
tokenObtained = true;
return this.showDialog_(title);
})
.then(/** @return {!Promise<CWSWidgetContainer.ResolveReason>} */
() => {
dialogShown = true;
// This is not set before so it doesn't pollute state if the
// previous dialog hasn't finished hiding.
this.onDialogClosed_ = onDialogClosed;
return this.widget_.start(options, webStoreUrl);
})
.then(/** @param {CWSWidgetContainer.ResolveReason} reason */
reason => {
if (reason !== CWSWidgetContainer.ResolveReason.RESET) {
this.hide();
}
})
.catch(error => {
console.error('Failed to start CWS widget: ' + error);
if (!dialogShown) {
// Reset any widget state set in |this.widget_.ready()|. The
// returned value is ignored because it doesn't influence the
// value reported by dialog.
this.widget_.finalizeAndGetResult();
const result = tokenObtained ?
// Got access token but the widget dialog was not shown.
// Consider the widget was cancelled.
SuggestAppsDialog.Result.CANCELLED :
// Access token was unavailable.
// This can happen in the Guest mode. crbug.com/694419
// Callback shows an alert notifying the file was not opened
// because of the unsupported type.
SuggestAppsDialog.Result.FAILED;
onDialogClosed(result, null);
return;
}
this.result_ = SuggestAppsDialog.Result.FAILED;
this.hide();
});
};
/**
* Internal method for showing the dialog in the file manager window.
* @param {string} title The dialog title.
* @return {!Promise}
*/
SuggestAppsDialog.prototype.showDialog_ = function(title) {
return new Promise((resolve, reject) => {
const success = this.dialogText_ ?
FileManagerDialogBase.prototype.showTitleAndTextDialog.call(
this, title, this.dialogText_) :
FileManagerDialogBase.prototype.showTitleOnlyDialog.call(this, title);
if (!success) {
reject('SuggestAppsDialog cannot be shown.');
return;
}
resolve();
});
};
/**
* Called when the connection status is changed.
* @param {VolumeManagerCommon.DriveConnectionType} connectionType Current
* connection type.
*/
SuggestAppsDialog.prototype.onDriveConnectionChanged = function(
connectionType) {
if (connectionType === VolumeManagerCommon.DriveConnectionType.OFFLINE) {
this.widget_.onConnectionLost();
}
};
/**
* @param {Function=} opt_originalOnHide Called when the original dialog is
* hidden.
* @override
*/
SuggestAppsDialog.prototype.hide = function(opt_originalOnHide) {
const widgetResult = this.widget_.finalizeAndGetResult();
switch (widgetResult.result) {
case CWSWidgetContainer.Result.INSTALL_SUCCESSFUL:
this.result_ = SuggestAppsDialog.Result.SUCCESS;
break;
case CWSWidgetContainer.Result.WEBSTORE_LINK_OPENED:
case CWSWidgetContainer.Result.USER_CANCEL:
this.result_ = SuggestAppsDialog.Result.CANCELLED;
break;
default:
this.result_ = SuggestAppsDialog.Result.FAILED;
}
this.installedItemId_ = widgetResult.installedItemId;
FileManagerDialogBase.prototype.hide.call(
this, this.onHide_.bind(this, opt_originalOnHide));
};
/**
* @param {Function=} opt_originalOnHide Original onHide function passed to
* SuggestAppsDialog.hide().
* @private
*/
SuggestAppsDialog.prototype.onHide_ = function(opt_originalOnHide) {
// Calls the callback after the dialog hides.
if (opt_originalOnHide) {
opt_originalOnHide();
}
this.onDialogClosed_(this.result_, this.installedItemId_);
};