| // Copyright (c) 2012 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. |
| |
| /** |
| * Represents a collection of available tasks to execute for a specific list |
| * of entries. |
| * |
| * @param {!VolumeManager} volumeManager |
| * @param {!MetadataModel} metadataModel |
| * @param {!DirectoryModel} directoryModel |
| * @param {!FileManagerUI} ui |
| * @param {!Array<!Entry>} entries |
| * @param {!Array<?string>} mimeTypes |
| * @param {!Array<!chrome.fileManagerPrivate.FileTask>} tasks |
| * @param {chrome.fileManagerPrivate.FileTask} defaultTask |
| * @param {!TaskHistory} taskHistory |
| * @constructor |
| * @struct |
| */ |
| function FileTasks( |
| volumeManager, metadataModel, directoryModel, ui, entries, mimeTypes, tasks, |
| defaultTask, taskHistory) { |
| /** |
| * @private {!VolumeManager} |
| * @const |
| */ |
| this.volumeManager_ = volumeManager; |
| |
| /** |
| * @private {!MetadataModel} |
| * @const |
| */ |
| this.metadataModel_ = metadataModel; |
| |
| /** |
| * @private {!DirectoryModel} |
| * @const |
| */ |
| this.directoryModel_ = directoryModel; |
| |
| /** |
| * @private {!FileManagerUI} |
| * @const |
| */ |
| this.ui_ = ui; |
| |
| /** |
| * @private {!Array<!Entry>} |
| * @const |
| */ |
| this.entries_ = entries; |
| |
| /** |
| * @private {!Array<?string>} |
| * @const |
| */ |
| this.mimeTypes_ = mimeTypes; |
| |
| /** |
| * @private {!Array<!chrome.fileManagerPrivate.FileTask>} |
| * @const |
| */ |
| this.tasks_ = tasks; |
| |
| /** |
| * @private {chrome.fileManagerPrivate.FileTask} |
| * @const |
| */ |
| this.defaultTask_ = defaultTask; |
| |
| /** |
| * @private {!TaskHistory} |
| * @const |
| */ |
| this.taskHistory_ = taskHistory; |
| } |
| |
| FileTasks.prototype = { |
| /** |
| * @return {!Array<!Entry>} |
| */ |
| get entries() { |
| return this.entries_; |
| } |
| }; |
| |
| /** |
| * The app ID of the video player app. |
| * @const |
| * @type {string} |
| */ |
| FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko'; |
| |
| /** |
| * The task id of the zip unpacker app. |
| * @const |
| * @type {string} |
| */ |
| FileTasks.ZIP_UNPACKER_TASK_ID = 'oedeeodfidgoollimchfdnbmhcpnklnd|app|zip'; |
| |
| /** |
| * The task id of unzip action of Zip Archiver app. |
| * @const |
| * @type {string} |
| */ |
| FileTasks.ZIP_ARCHIVER_UNZIP_TASK_ID = |
| 'dmboannefpncccogfdikhmhpmdnddgoe|app|open'; |
| |
| /** |
| * The task id of zip action of Zip Archiver app. |
| * @const |
| * @type {string} |
| */ |
| FileTasks.ZIP_ARCHIVER_ZIP_TASK_ID = |
| 'dmboannefpncccogfdikhmhpmdnddgoe|app|pack'; |
| |
| /** |
| * The task id of zip action of Zip Archiver app, using temporary dir as workdir |
| * @const |
| * @type {string} |
| */ |
| FileTasks.ZIP_ARCHIVER_ZIP_USING_TMP_TASK_ID = |
| 'dmboannefpncccogfdikhmhpmdnddgoe|app|pack_using_tmp'; |
| |
| |
| /** |
| * Available tasks in task menu button. |
| * @enum {string} |
| */ |
| FileTasks.TaskMenuButtonItemType = { |
| ShowMenu: 'ShowMenu', |
| RunTask: 'RunTask', |
| ChangeDefaultTask: 'ChangeDefaultTask' |
| }; |
| |
| /** |
| * Dialog types to show a task picker. |
| * @enum {string} |
| */ |
| FileTasks.TaskPickerType = { |
| ChangeDefault: 'ChangeDefault', |
| OpenWith: 'OpenWith', |
| MoreActions: 'MoreActions' |
| }; |
| |
| /** |
| * Creates an instance of FileTasks for the specified list of entries with mime |
| * types. |
| * |
| * @param {!VolumeManager} volumeManager |
| * @param {!MetadataModel} metadataModel |
| * @param {!DirectoryModel} directoryModel |
| * @param {!FileManagerUI} ui |
| * @param {!Array<!Entry>} entries |
| * @param {!Array<?string>} mimeTypes |
| * @param {!TaskHistory} taskHistory |
| * @return {!Promise<!FileTasks>} |
| */ |
| FileTasks.create = function( |
| volumeManager, metadataModel, directoryModel, ui, entries, mimeTypes, |
| taskHistory) { |
| var tasksPromise = new Promise(function(fulfill) { |
| // getFileTasks supports only native entries. |
| entries = entries.filter(util.isNativeEntry); |
| if (entries.length === 0) { |
| fulfill([]); |
| return; |
| } |
| chrome.fileManagerPrivate.getFileTasks(entries, function(taskItems) { |
| if (chrome.runtime.lastError) { |
| console.error('Failed to fetch file tasks due to: ' + |
| chrome.runtime.lastError.message); |
| Promise.reject(); |
| return; |
| } |
| |
| // Linux package installation is currently only supported for a single |
| // file already inside the Linux container. |
| // TODO(timloh): Instead of filtering these out, we probably should show |
| // a dialog with an error message, similar to when attempting to run |
| // Crostini tasks with non-Crostini entries. |
| if (entries.length !== 1 || |
| !Crostini.isCrostiniEntry(entries[0], volumeManager)) { |
| taskItems = taskItems.filter(function(item) { |
| var taskParts = item.taskId.split('|'); |
| var appId = taskParts[0]; |
| var taskType = taskParts[1]; |
| var actionId = taskParts[2]; |
| return !( |
| appId === chrome.runtime.id && taskType === 'file' && |
| actionId === 'install-linux-package'); |
| }); |
| } |
| |
| // Filters out Pack with Zip Archiver task because it will be accessible |
| // via 'Zip selection' context menu button |
| taskItems = taskItems.filter(function(item) { |
| return item.taskId !== FileTasks.ZIP_ARCHIVER_ZIP_TASK_ID && |
| item.taskId !== FileTasks.ZIP_ARCHIVER_ZIP_USING_TMP_TASK_ID; |
| }); |
| |
| fulfill(FileTasks.annotateTasks_(assert(taskItems), entries)); |
| }); |
| }); |
| |
| var defaultTaskPromise = tasksPromise.then(function(tasks) { |
| return FileTasks.getDefaultTask(tasks, taskHistory); |
| }); |
| |
| return Promise.all([tasksPromise, defaultTaskPromise]).then(function(args) { |
| return new FileTasks( |
| volumeManager, metadataModel, directoryModel, ui, entries, mimeTypes, |
| args[0], args[1], taskHistory); |
| }); |
| }; |
| |
| /** |
| * Obtains the task items. |
| * @return {!Array<!chrome.fileManagerPrivate.FileTask>} |
| */ |
| FileTasks.prototype.getTaskItems = function() { |
| return this.tasks_; |
| }; |
| |
| /** |
| * Obtain tasks which are categorized as OPEN tasks. |
| * @return {!Array<!chrome.fileManagerPrivate.FileTask>} |
| */ |
| FileTasks.prototype.getOpenTaskItems = function() { |
| return this.tasks_.filter(FileTasks.isOpenTask); |
| }; |
| |
| /** |
| * Obtain tasks which are not categorized as OPEN tasks. |
| * @return {!Array<!chrome.fileManagerPrivate.FileTask>} |
| */ |
| FileTasks.prototype.getNonOpenTaskItems = function() { |
| return this.tasks_.filter(task => !FileTasks.isOpenTask(task)); |
| }; |
| |
| /** |
| * Opens the suggest file dialog. |
| * |
| * @param {function()} onSuccess Success callback. |
| * @param {function()} onCancelled User-cancelled callback. |
| * @param {function()} onFailure Failure callback. |
| */ |
| FileTasks.prototype.openSuggestAppsDialog = function( |
| onSuccess, onCancelled, onFailure) { |
| if (this.entries_.length !== 1) { |
| onFailure(); |
| return; |
| } |
| |
| var entry = this.entries_[0]; |
| var mimeType = this.mimeTypes_[0]; |
| var basename = entry.name; |
| var splitted = util.splitExtension(basename); |
| var extension = splitted[1]; |
| |
| // Returns with failure if the file has neither extension nor MIME type. |
| if (!extension && !mimeType) { |
| onFailure(); |
| return; |
| } |
| |
| var onDialogClosed = function(result, itemId) { |
| switch (result) { |
| case SuggestAppsDialog.Result.SUCCESS: |
| onSuccess(); |
| break; |
| case SuggestAppsDialog.Result.FAILED: |
| onFailure(); |
| break; |
| default: |
| onCancelled(); |
| } |
| }; |
| |
| this.ui_.suggestAppsDialog.showByExtensionAndMime( |
| extension, mimeType, onDialogClosed); |
| }; |
| |
| /** |
| * The list of known extensions to record UMA. |
| * Note: Because the data is recorded by the index, so new item shouldn't be |
| * inserted. |
| * Must match the ViewFileType entry in enums.xml. |
| * |
| * @const |
| * @type {Array<string>} |
| */ |
| FileTasks.UMA_INDEX_KNOWN_EXTENSIONS = Object.freeze([ |
| 'other', '.3ga', '.3gp', |
| '.aac', '.alac', '.asf', |
| '.avi', '.bmp', '.csv', |
| '.doc', '.docx', '.flac', |
| '.gif', '.jpeg', '.jpg', |
| '.log', '.m3u', '.m3u8', |
| '.m4a', '.m4v', '.mid', |
| '.mkv', '.mov', '.mp3', |
| '.mp4', '.mpg', '.odf', |
| '.odp', '.ods', '.odt', |
| '.oga', '.ogg', '.ogv', |
| '.pdf', '.png', '.ppt', |
| '.pptx', '.ra', '.ram', |
| '.rar', '.rm', '.rtf', |
| '.wav', '.webm', '.webp', |
| '.wma', '.wmv', '.xls', |
| '.xlsx', '.crdownload', '.crx', |
| '.dmg', '.exe', '.html', |
| '.htm', '.jar', '.ps', |
| '.torrent', '.txt', '.zip', |
| 'directory', 'no extension', 'unknown extension', |
| '.mhtml', '.gdoc', '.gsheet', |
| '.gslides' |
| ]); |
| |
| /** |
| * The list of extensions to skip the suggest app dialog. |
| * @const |
| * @type {Array<string>} |
| * @private |
| */ |
| FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([ |
| '.crdownload', '.dsc', '.inf', '.crx', |
| ]); |
| |
| /** |
| * Task IDs of the zip file handlers to be recorded. |
| * The indexes of the IDs must match with the values of |
| * FileManagerZipHandlerType in enums.xml, and should not change. |
| */ |
| FileTasks.UMA_ZIP_HANDLER_TASK_IDS_ = Object.freeze([ |
| FileTasks.ZIP_UNPACKER_TASK_ID, FileTasks.ZIP_ARCHIVER_UNZIP_TASK_ID, |
| FileTasks.ZIP_ARCHIVER_ZIP_TASK_ID |
| ]); |
| |
| /** |
| * Records trial of opening file grouped by extensions. |
| * |
| * @param {Array<!Entry>} entries The entries to be opened. |
| * @private |
| */ |
| FileTasks.recordViewingFileTypeUMA_ = function(entries) { |
| for (var i = 0; i < entries.length; i++) { |
| var entry = entries[i]; |
| var extension = FileType.getExtension(entry).toLowerCase(); |
| if (FileTasks.UMA_INDEX_KNOWN_EXTENSIONS.indexOf(extension) < 0) { |
| extension = 'other'; |
| } |
| metrics.recordEnum( |
| 'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS); |
| } |
| }; |
| |
| /** |
| * Records trial of opening file grouped by root types. |
| * |
| * @param {?VolumeManagerCommon.RootType} rootType The type of the root where |
| * entries are being opened. |
| * @private |
| */ |
| FileTasks.recordViewingRootTypeUMA_ = function(rootType) { |
| if (rootType !== null) { |
| metrics.recordEnum( |
| 'ViewingRootType', rootType, VolumeManagerCommon.RootTypesForUMA); |
| } |
| }; |
| |
| FileTasks.recordZipHandlerUMA_ = function(taskId) { |
| if (FileTasks.UMA_ZIP_HANDLER_TASK_IDS_.indexOf(taskId) != -1) { |
| metrics.recordEnum( |
| 'ZipFileTask', taskId, FileTasks.UMA_ZIP_HANDLER_TASK_IDS_); |
| } |
| }; |
| |
| /** |
| * Crostini Share Dialog types. |
| * Keep in sync with enums.xml FileManagerCrostiniShareDialogType. |
| * @enum {string} |
| */ |
| FileTasks.CrostiniShareDialogType = { |
| None: 'None', |
| ShareBeforeOpen: 'ShareBeforeOpen', |
| UnableToOpen: 'UnableToOpen', |
| }; |
| |
| /** |
| * The indexes of these types must match with the values of |
| * FileManagerCrostiniShareDialogType in enums.xml, and should not change. |
| */ |
| FileTasks.UMA_CROSTINI_SHARE_DIALOG_TYPES_ = Object.freeze([ |
| FileTasks.CrostiniShareDialogType.None, |
| FileTasks.CrostiniShareDialogType.ShareBeforeOpen, |
| FileTasks.CrostiniShareDialogType.UnableToOpen, |
| ]); |
| |
| |
| /** |
| * Records the type of dialog shown when using a crostini app to open a file. |
| * @param {!FileTasks.CrostiniShareDialogType} dialogType |
| * @private |
| */ |
| FileTasks.recordCrostiniShareDialogTypeUMA_ = function(dialogType) { |
| metrics.recordEnum( |
| 'CrostiniShareDialog', dialogType, |
| FileTasks.UMA_CROSTINI_SHARE_DIALOG_TYPES_); |
| }; |
| |
| /** |
| * Returns true if the taskId is for an internal task. |
| * |
| * @param {string} taskId Task identifier. |
| * @return {boolean} True if the task ID is for an internal task. |
| * @private |
| */ |
| FileTasks.isInternalTask_ = function(taskId) { |
| var taskParts = taskId.split('|'); |
| var appId = taskParts[0]; |
| var taskType = taskParts[1]; |
| var actionId = taskParts[2]; |
| return ( |
| appId === chrome.runtime.id && taskType === 'file' && |
| (actionId === 'mount-archive' || actionId === 'install-linux-package')); |
| }; |
| |
| /** |
| * Returns true if the given task is categorized as an OPEN task. |
| * |
| * @param {!chrome.fileManagerPrivate.FileTask} task |
| * @return {boolean} True if the given task is an OPEN task. |
| */ |
| FileTasks.isOpenTask = function(task) { |
| // We consider following types of tasks as OPEN tasks. |
| // - Files app's internal tasks |
| // - file_handler tasks with OPEN_WITH verb |
| return !task.verb || task.verb == chrome.fileManagerPrivate.Verb.OPEN_WITH; |
| }; |
| |
| /** |
| * Annotates tasks returned from the API. |
| * |
| * @param {!Array<!chrome.fileManagerPrivate.FileTask>} tasks Input tasks from |
| * the API. |
| * @param {!Array<!Entry>} entries List of entries for the tasks. |
| * @return {!Array<!chrome.fileManagerPrivate.FileTask>} Annotated tasks. |
| * @private |
| */ |
| FileTasks.annotateTasks_ = function(tasks, entries) { |
| var result = []; |
| var id = chrome.runtime.id; |
| for (var i = 0; i < tasks.length; i++) { |
| var task = tasks[i]; |
| var taskParts = task.taskId.split('|'); |
| |
| // Skip internal Files app's handlers. |
| if (taskParts[0] === id && |
| (taskParts[2] === 'select' || taskParts[2] === 'open')) { |
| continue; |
| } |
| |
| // Tweak images, titles of internal tasks. |
| if (taskParts[0] === id && taskParts[1] === 'file') { |
| if (taskParts[2] === 'play') { |
| // TODO(serya): This hack needed until task.iconUrl is working |
| // (see GetFileTasksFileBrowserFunction::RunImpl). |
| task.iconType = 'audio'; |
| task.title = loadTimeData.getString('TASK_LISTEN'); |
| } else if (taskParts[2] === 'mount-archive') { |
| task.iconType = 'archive'; |
| task.title = loadTimeData.getString('MOUNT_ARCHIVE'); |
| } else if (taskParts[2] === 'open-hosted-generic') { |
| if (entries.length > 1) |
| task.iconType = 'generic'; |
| else // Use specific icon. |
| task.iconType = FileType.getIcon(entries[0]); |
| task.title = loadTimeData.getString('TASK_OPEN'); |
| } else if (taskParts[2] === 'open-hosted-gdoc') { |
| task.iconType = 'gdoc'; |
| task.title = loadTimeData.getString('TASK_OPEN_GDOC'); |
| } else if (taskParts[2] === 'open-hosted-gsheet') { |
| task.iconType = 'gsheet'; |
| task.title = loadTimeData.getString('TASK_OPEN_GSHEET'); |
| } else if (taskParts[2] === 'open-hosted-gslides') { |
| task.iconType = 'gslides'; |
| task.title = loadTimeData.getString('TASK_OPEN_GSLIDES'); |
| } else if (taskParts[2] === 'install-linux-package') { |
| task.iconType = 'crostini'; |
| task.title = loadTimeData.getString('TASK_INSTALL_LINUX_PACKAGE'); |
| } else if (taskParts[2] === 'view-swf') { |
| // Do not render this task if disabled. |
| if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED')) |
| continue; |
| task.iconType = 'generic'; |
| task.title = loadTimeData.getString('TASK_VIEW'); |
| } else if (taskParts[2] === 'view-pdf') { |
| // Do not render this task if disabled. |
| if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED')) |
| continue; |
| task.iconType = 'pdf'; |
| task.title = loadTimeData.getString('TASK_VIEW'); |
| } else if (taskParts[2] === 'view-in-browser') { |
| task.iconType = 'generic'; |
| task.title = loadTimeData.getString('TASK_VIEW'); |
| } |
| } |
| if (!task.iconType && taskParts[1] === 'web-intent') { |
| task.iconType = 'generic'; |
| } |
| |
| // Add verb to title. |
| if (task.verb) { |
| var verbButtonLabel = ''; |
| switch (task.verb) { |
| case chrome.fileManagerPrivate.Verb.ADD_TO: |
| verbButtonLabel = 'ADD_TO_VERB_BUTTON_LABEL'; |
| break; |
| case chrome.fileManagerPrivate.Verb.PACK_WITH: |
| verbButtonLabel = 'PACK_WITH_VERB_BUTTON_LABEL'; |
| break; |
| case chrome.fileManagerPrivate.Verb.SHARE_WITH: |
| // Even when the task has SHARE_WITH verb, we don't prefix the title |
| // with "Share with" when the task is from SEND/SEND_MULTIPLE intent |
| // handlers from Android apps, since the title can already have an |
| // appropriate verb. |
| if (!(taskParts[1] == 'arc' && |
| (taskParts[2] == 'send' || taskParts[2] == 'send_multiple'))) { |
| verbButtonLabel = 'SHARE_WITH_VERB_BUTTON_LABEL'; |
| } |
| break; |
| case chrome.fileManagerPrivate.Verb.OPEN_WITH: |
| verbButtonLabel = 'OPEN_WITH_VERB_BUTTON_LABEL'; |
| break; |
| default: |
| console.error('Invalid task verb: ' + task.verb + '.'); |
| } |
| if (verbButtonLabel) |
| task.label = loadTimeData.getStringF(verbButtonLabel, task.title); |
| } |
| |
| result.push(task); |
| } |
| |
| return result; |
| }; |
| |
| /** |
| * Checks if task is a crostini task and all entries are accessible to, or can |
| * be shared with crostini. Shares files as required if possible and invokes |
| * callback, or shows Unable to Open error dialog and does not invoke callback. |
| * @param {!chrome.fileManagerPrivate.FileTask} task Task to run. |
| * @param {function()} callback Callback is called when all files (if any) are |
| * accessible to crostini, else error dialog is shown. |
| * @private |
| */ |
| FileTasks.prototype.maybeShareWithCrostiniOrShowDialog_ = function( |
| task, callback) { |
| // Check if this is a crostini task. |
| if (task.taskId.split('|', 2)[1] !== 'crostini' || this.entries_.length < 1) |
| return callback(); |
| |
| let showUnableToOpen = false; |
| const entriesToShare = []; |
| |
| for (let i = 0; i < this.entries_.length; i++) { |
| const entry = this.entries_[i]; |
| if (Crostini.isCrostiniEntry(entry, this.volumeManager_) || |
| Crostini.isPathShared(entry, this.volumeManager_)) { |
| continue; |
| } |
| if (!Crostini.canSharePath( |
| entry, false /* persist */, this.volumeManager_)) { |
| showUnableToOpen = true; |
| break; |
| } |
| entriesToShare.push(entry); |
| } |
| |
| // Show unable to open alert dialog. |
| if (showUnableToOpen) { |
| this.ui_.alertDialog.showHtml( |
| strf('UNABLE_TO_OPEN_CROSTINI_TITLE', task.title), |
| strf('UNABLE_TO_OPEN_CROSTINI', task.title)); |
| FileTasks.recordCrostiniShareDialogTypeUMA_( |
| FileTasks.CrostiniShareDialogType.UnableToOpen); |
| return; |
| } |
| |
| // No sharing required. |
| if (entriesToShare.length === 0) { |
| FileTasks.recordCrostiniShareDialogTypeUMA_( |
| FileTasks.CrostiniShareDialogType.None); |
| return callback(); |
| } |
| |
| // Share then invoke callback. |
| FileTasks.recordCrostiniShareDialogTypeUMA_( |
| FileTasks.CrostiniShareDialogType.ShareBeforeOpen); |
| // Set persist to false when sharing paths to open with a crostini app. |
| chrome.fileManagerPrivate.sharePathsWithCrostini( |
| entriesToShare, false /* persist */, () => { |
| // It is unexpected to get an error sharing any files since we have |
| // already validated that all selected files can be shared. |
| // But if it happens, log error, and do not execute callback. |
| if (chrome.runtime.lastError) { |
| return console.error( |
| 'Error sharing with linux to execute: ' + |
| chrome.runtime.lastError.message); |
| } |
| // Register paths as shared, and now we are ready to execute. |
| entriesToShare.forEach((entry) => { |
| Crostini.registerSharedPath(entry, this.volumeManager_); |
| }); |
| callback(); |
| }); |
| }; |
| |
| /** |
| * Executes default task. |
| * |
| * @param {function(boolean, Array<!Entry>)=} opt_callback Called when the |
| * default task is executed, or the error is occurred. |
| */ |
| FileTasks.prototype.executeDefault = function(opt_callback) { |
| FileTasks.recordViewingFileTypeUMA_(this.entries_); |
| FileTasks.recordViewingRootTypeUMA_( |
| this.directoryModel_.getCurrentRootType()); |
| this.executeDefaultInternal_(opt_callback); |
| }; |
| |
| /** |
| * Executes default task. |
| * |
| * @param {function(boolean, Array<!Entry>)=} opt_callback Called when the |
| * default task is executed, or the error is occurred. |
| * @private |
| */ |
| FileTasks.prototype.executeDefaultInternal_ = function(opt_callback) { |
| var callback = opt_callback || function(arg1, arg2) {}; |
| |
| if (this.defaultTask_ !== null) { |
| this.executeInternal_(this.defaultTask_); |
| callback(true, this.entries_); |
| return; |
| } |
| |
| var nonGenericTasks = this.tasks_.filter(t => !t.isGenericFileHandler); |
| // If there is only one task that is not a generic file handler, it should be |
| // executed as a default task. If there are multiple tasks that are not |
| // generic file handlers, and none of them are considered as default, we show |
| // a task picker to ask the user to choose one. |
| if (nonGenericTasks.length >= 2) { |
| this.showTaskPicker( |
| this.ui_.defaultTaskPicker, str('OPEN_WITH_BUTTON_LABEL'), |
| '', function(task) { |
| this.execute(task); |
| }.bind(this), FileTasks.TaskPickerType.OpenWith); |
| return; |
| } |
| |
| // We don't have tasks, so try to show a file in a browser tab. |
| // We only do that for single selection to avoid confusion. |
| if (this.entries_.length !== 1) |
| return; |
| |
| var filename = this.entries_[0].name; |
| var extension = util.splitExtension(filename)[1] || null; |
| var mimeType = this.mimeTypes_[0] || null; |
| |
| var showAlert = function() { |
| var textMessageId; |
| var titleMessageId; |
| switch (extension) { |
| case '.exe': |
| case '.msi': |
| textMessageId = 'NO_TASK_FOR_EXECUTABLE'; |
| break; |
| case '.dmg': |
| textMessageId = 'NO_TASK_FOR_DMG'; |
| break; |
| case '.crx': |
| textMessageId = 'NO_TASK_FOR_CRX'; |
| titleMessageId = 'NO_TASK_FOR_CRX_TITLE'; |
| break; |
| default: |
| textMessageId = 'NO_TASK_FOR_FILE'; |
| } |
| |
| var webStoreUrl = webStoreUtils.createWebStoreLink(extension, mimeType); |
| var text = strf(textMessageId, webStoreUrl, str('NO_TASK_FOR_FILE_URL')); |
| var title = titleMessageId ? str(titleMessageId) : filename; |
| this.ui_.alertDialog.showHtml(title, text, null, null, null); |
| callback(false, this.entries_); |
| }.bind(this); |
| |
| var onViewFilesFailure = function() { |
| if (extension && |
| (FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1 || |
| constants.EXECUTABLE_EXTENSIONS.indexOf(assert(extension)) !== -1)) { |
| showAlert(); |
| return; |
| } |
| |
| this.openSuggestAppsDialog( |
| function() { |
| FileTasks |
| .create( |
| this.volumeManager_, this.metadataModel_, |
| this.directoryModel_, this.ui_, this.entries_, |
| this.mimeTypes_, this.taskHistory_) |
| .then( |
| function(tasks) { |
| tasks.executeDefault(); |
| callback(true, this.entries_); |
| }.bind(this), |
| function() { |
| callback(false, this.entries_); |
| }.bind(this)); |
| }.bind(this), |
| // Cancelled callback. |
| function() { |
| callback(false, this.entries_); |
| }.bind(this), |
| showAlert); |
| }.bind(this); |
| |
| var onViewFiles = function(result) { |
| switch (result) { |
| case 'opened': |
| callback(true, this.entries_); |
| break; |
| case 'message_sent': |
| util.isTeleported(window).then(function(teleported) { |
| if (teleported) |
| this.ui_.showOpenInOtherDesktopAlert(this.entries_); |
| }.bind(this)); |
| callback(true, this.entries_); |
| break; |
| case 'empty': |
| callback(true, this.entries_); |
| break; |
| case 'failed': |
| onViewFilesFailure(); |
| break; |
| } |
| }.bind(this); |
| |
| this.checkAvailability_(function() { |
| var taskId = chrome.runtime.id + '|file|view-in-browser'; |
| chrome.fileManagerPrivate.executeTask(taskId, this.entries_, onViewFiles); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Executes a single task. |
| * |
| * @param {chrome.fileManagerPrivate.FileTask} task FileTask. |
| */ |
| FileTasks.prototype.execute = function(task) { |
| FileTasks.recordViewingFileTypeUMA_(this.entries_); |
| FileTasks.recordViewingRootTypeUMA_( |
| this.directoryModel_.getCurrentRootType()); |
| this.executeInternal_(task); |
| }; |
| |
| /** |
| * The core implementation to execute a single task. |
| * |
| * @param {chrome.fileManagerPrivate.FileTask} task FileTask. |
| * @private |
| */ |
| FileTasks.prototype.executeInternal_ = function(task) { |
| this.checkAvailability_(() => { |
| this.maybeShareWithCrostiniOrShowDialog_(task, () => { |
| this.taskHistory_.recordTaskExecuted(task.taskId); |
| if (FileTasks.isInternalTask_(task.taskId)) { |
| this.executeInternalTask_(task.taskId); |
| } else { |
| FileTasks.recordZipHandlerUMA_(task.taskId); |
| chrome.fileManagerPrivate.executeTask( |
| task.taskId, this.entries_, (result) => { |
| if (result !== 'message_sent') |
| return; |
| util.isTeleported(window).then((teleported) => { |
| if (teleported) |
| this.ui_.showOpenInOtherDesktopAlert(this.entries_); |
| }); |
| }); |
| } |
| }); |
| }); |
| }; |
| |
| /** |
| * Ensures that the all files are available right now. |
| * |
| * Must not call before initialization. |
| * @param {function()} callback Called when checking is completed and all files |
| * are available. Otherwise not called. |
| * @private |
| */ |
| FileTasks.prototype.checkAvailability_ = function(callback) { |
| var areAll = function(entries, props, name) { |
| // TODO(cmihail): Make files in directories available offline. |
| // See http://crbug.com/569767. |
| var okEntriesNum = 0; |
| for (var i = 0; i < entries.length; i++) { |
| // If got no properties, we safely assume that item is available. |
| if (props[i] && (props[i][name] || entries[i].isDirectory)) |
| okEntriesNum++; |
| } |
| return okEntriesNum === props.length; |
| }; |
| |
| var containsDriveEntries = |
| this.entries_.some(function(entry) { |
| var volumeInfo = this.volumeManager_.getVolumeInfo(entry); |
| return volumeInfo && volumeInfo.volumeType === |
| VolumeManagerCommon.VolumeType.DRIVE; |
| }.bind(this)); |
| |
| // Availability is not checked for non-Drive files, as availableOffline, nor |
| // availableWhenMetered are not exposed for other types of volumes at this |
| // moment. |
| if (!containsDriveEntries) { |
| callback(); |
| return; |
| } |
| |
| var isDriveOffline = this.volumeManager_.getDriveConnectionState().type === |
| VolumeManagerCommon.DriveConnectionType.OFFLINE; |
| |
| if (isDriveOffline) { |
| this.metadataModel_.get(this.entries_, ['availableOffline', 'hosted']).then( |
| function(props) { |
| if (areAll(this.entries_, props, 'availableOffline')) { |
| callback(); |
| return; |
| } |
| |
| this.ui_.alertDialog.showHtml( |
| loadTimeData.getString('OFFLINE_HEADER'), |
| props[0].hosted ? |
| loadTimeData.getStringF( |
| this.entries_.length === 1 ? |
| 'HOSTED_OFFLINE_MESSAGE' : |
| 'HOSTED_OFFLINE_MESSAGE_PLURAL') : |
| loadTimeData.getStringF( |
| this.entries_.length === 1 ? |
| 'OFFLINE_MESSAGE' : |
| 'OFFLINE_MESSAGE_PLURAL', |
| loadTimeData.getString('OFFLINE_COLUMN_LABEL')), |
| null, null, null); |
| }.bind(this)); |
| return; |
| } |
| |
| var isOnMetered = this.volumeManager_.getDriveConnectionState().type === |
| VolumeManagerCommon.DriveConnectionType.METERED; |
| |
| if (isOnMetered) { |
| this.metadataModel_.get(this.entries_, ['availableWhenMetered', 'size']) |
| .then(function(props) { |
| if (areAll(this.entries_, props, 'availableWhenMetered')) { |
| callback(); |
| return; |
| } |
| |
| var sizeToDownload = 0; |
| for (var i = 0; i !== this.entries_.length; i++) { |
| if (!props[i].availableWhenMetered) |
| sizeToDownload += props[i].size; |
| } |
| this.ui_.confirmDialog.show( |
| loadTimeData.getStringF( |
| this.entries_.length === 1 ? |
| 'CONFIRM_MOBILE_DATA_USE' : |
| 'CONFIRM_MOBILE_DATA_USE_PLURAL', |
| util.bytesToString(sizeToDownload)), |
| callback, null, null); |
| }.bind(this)); |
| return; |
| } |
| |
| callback(); |
| }; |
| |
| /** |
| * Executes an internal task. |
| * |
| * @param {string} taskId The task id. |
| * @private |
| */ |
| FileTasks.prototype.executeInternalTask_ = function(taskId) { |
| var taskParts = taskId.split('|'); |
| if (taskParts[2] === 'mount-archive') { |
| this.mountArchivesInternal_(); |
| return; |
| } |
| if (taskParts[2] === 'install-linux-package') { |
| this.installLinuxPackageInternal_(); |
| return; |
| } |
| |
| console.error('The specified task is not a valid internal task: ' + taskId); |
| }; |
| |
| /** |
| * Install a Linux Package in the Linux container. |
| * @private |
| */ |
| FileTasks.prototype.installLinuxPackageInternal_ = function() { |
| assert(this.entries_.length === 1); |
| this.ui_.installLinuxPackageDialog.showInstallLinuxPackageDialog( |
| this.entries_[0]); |
| }; |
| |
| /** |
| * The core implementation of mounts archives. |
| * @private |
| */ |
| FileTasks.prototype.mountArchivesInternal_ = function() { |
| var tracker = this.directoryModel_.createDirectoryChangeTracker(); |
| tracker.start(); |
| |
| // TODO(mtomasz): Move conversion from entry to url to custom bindings. |
| // crbug.com/345527. |
| var urls = util.entriesToURLs(this.entries_); |
| for (var index = 0; index < urls.length; ++index) { |
| // TODO(mtomasz): Pass Entry instead of URL. |
| this.volumeManager_.mountArchive( |
| urls[index], |
| function(volumeInfo) { |
| if (tracker.hasChanged) { |
| tracker.stop(); |
| return; |
| } |
| volumeInfo.resolveDisplayRoot( |
| function(displayRoot) { |
| if (tracker.hasChanged) { |
| tracker.stop(); |
| return; |
| } |
| this.directoryModel_.changeDirectoryEntry(displayRoot); |
| }.bind(this), |
| function() { |
| console.warn( |
| 'Failed to resolve the display root after mounting.'); |
| tracker.stop(); |
| }); |
| }.bind(this), |
| function(url, error) { |
| tracker.stop(); |
| var path = util.extractFilePath(url); |
| var namePos = path.lastIndexOf('/'); |
| this.ui_.alertDialog.show( |
| strf('ARCHIVE_MOUNT_FAILED', path.substr(namePos + 1), error), |
| null, |
| null); |
| }.bind(this, urls[index])); |
| } |
| }; |
| |
| /** |
| * Displays the list of tasks in a open task picker combobutton and a share |
| * options menu. |
| * |
| * @param {!cr.ui.ComboButton} openCombobutton The open task picker combobutton. |
| * @param {!cr.ui.MenuButton} shareMenuButton The menu button for share options. |
| * @public |
| */ |
| FileTasks.prototype.display = function(openCombobutton, shareMenuButton) { |
| var openTasks = []; |
| var otherTasks = []; |
| for (var i = 0; i < this.tasks_.length; i++) { |
| var task = this.tasks_[i]; |
| if (FileTasks.isOpenTask(task)) |
| openTasks.push(task); |
| else |
| otherTasks.push(task); |
| } |
| this.updateOpenComboButton_(openCombobutton, openTasks); |
| this.updateShareMenuButton_(shareMenuButton, otherTasks); |
| }; |
| |
| /** |
| * Setup a task picker combobutton based on the given tasks. |
| * @param {!cr.ui.ComboButton} combobutton |
| * @param {!Array<!chrome.fileManagerPrivate.FileTask>} tasks |
| */ |
| FileTasks.prototype.updateOpenComboButton_ = function(combobutton, tasks) { |
| combobutton.hidden = tasks.length == 0; |
| if (tasks.length == 0) |
| return; |
| |
| combobutton.clear(); |
| |
| // If there exist defaultTask show it on the combobutton. |
| if (this.defaultTask_) { |
| combobutton.defaultItem = |
| this.createCombobuttonItem_(this.defaultTask_, str('TASK_OPEN')); |
| } else { |
| combobutton.defaultItem = { |
| type: FileTasks.TaskMenuButtonItemType.ShowMenu, |
| label: str('OPEN_WITH_BUTTON_LABEL') |
| }; |
| } |
| |
| // If there exist 2 or more available tasks, show them in context menu |
| // (including defaultTask). If only one generic task is available, we |
| // also show it in the context menu. |
| var items = this.createItems_(tasks); |
| if (items.length > 1 || (items.length === 1 && this.defaultTask_ === null)) { |
| for (var j = 0; j < items.length; j++) { |
| combobutton.addDropDownItem(items[j]); |
| } |
| |
| // If there exist non generic task (i.e. defaultTask is set), we show |
| // an item to change default task. |
| if (this.defaultTask_) { |
| combobutton.addSeparator(); |
| var changeDefaultMenuItem = combobutton.addDropDownItem({ |
| type: FileTasks.TaskMenuButtonItemType.ChangeDefaultTask, |
| label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM') |
| }); |
| changeDefaultMenuItem.classList.add('change-default'); |
| } |
| } |
| }; |
| |
| /** |
| * Setup a menu button for sharing options based on the given tasks. |
| * @param {!cr.ui.MenuButton} shareMenuButton |
| * @param {!Array<!chrome.fileManagerPrivate.FileTask>} tasks |
| */ |
| FileTasks.prototype.updateShareMenuButton_ = function(shareMenuButton, tasks) { |
| var driveShareCommand = |
| shareMenuButton.menu.querySelector('cr-menu-item[command="#share"]'); |
| var driveShareCommandSeparator = |
| shareMenuButton.menu.querySelector('#drive-share-separator'); |
| |
| shareMenuButton.hidden = driveShareCommand.disabled && tasks.length == 0; |
| |
| // Show the separator if Drive share command is enabled and there is at least |
| // one other share actions. |
| driveShareCommandSeparator.hidden = |
| driveShareCommand.disabled || tasks.length == 0; |
| |
| // Clear menu items except for drive share menu and a separator for it. |
| // As querySelectorAll() returns live NodeList, we need to copy elements to |
| // Array object to modify DOM in the for loop. |
| var itemsToRemove = [].slice.call(shareMenuButton.menu.querySelectorAll( |
| 'cr-menu-item:not([command="#share"])')); |
| for (var i = 0; i < itemsToRemove.length; i++) { |
| var item = itemsToRemove[i]; |
| item.parentNode.removeChild(item); |
| } |
| |
| // Add menu items for the new tasks. |
| var items = this.createItems_(tasks); |
| for (var i = 0; i < items.length; i++) { |
| var menuitem = shareMenuButton.menu.addMenuItem(items[i]); |
| cr.ui.decorate(menuitem, cr.ui.FilesMenuItem); |
| menuitem.data = items[i]; |
| if (items[i].iconType) { |
| menuitem.style.backgroundImage = ''; |
| menuitem.setAttribute('file-type-icon', items[i].iconType); |
| } |
| } |
| }; |
| |
| /** |
| * Creates sorted array of available task descriptions such as title and icon. |
| * |
| * @param {!Array<!chrome.fileManagerPrivate.FileTask>} tasks Tasks to create |
| * items. |
| * @return {!Array<!FileTasks.ComboButtonItem>} Created array can be used to |
| * feed combobox, menus and so on. |
| * @private |
| */ |
| FileTasks.prototype.createItems_ = function(tasks) { |
| var items = []; |
| |
| // Create items. |
| for (var index = 0; index < tasks.length; index++) { |
| var task = tasks[index]; |
| if (task === this.defaultTask_) { |
| var title = task.title + ' ' + |
| loadTimeData.getString('DEFAULT_TASK_LABEL'); |
| items.push(this.createCombobuttonItem_(task, title, true, true)); |
| } else { |
| items.push(this.createCombobuttonItem_(task)); |
| } |
| } |
| |
| // Sort items (Sort order: isDefault, lastExecutedTime, label). |
| items.sort(function(a, b) { |
| // Sort by isDefaultTask. |
| var isDefault = (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0); |
| if (isDefault !== 0) |
| return isDefault; |
| |
| // Sort by last-executed time. |
| var aTime = this.taskHistory_.getLastExecutedTime(a.task.taskId); |
| var bTime = this.taskHistory_.getLastExecutedTime(b.task.taskId); |
| if (aTime != bTime) |
| return bTime - aTime; |
| |
| // Sort by label. |
| return a.label.localeCompare(b.label); |
| }.bind(this)); |
| |
| return items; |
| }; |
| |
| /** |
| * @typedef {{ |
| * type: !FileTasks.TaskMenuButtonItemType, |
| * label: string, |
| * iconUrl: string, |
| * iconType: string, |
| * task: !chrome.fileManagerPrivate.FileTask, |
| * bold: boolean, |
| * isDefault: boolean, |
| * isGenericFileHandler: boolean, |
| * }} |
| */ |
| FileTasks.ComboButtonItem; |
| |
| /** |
| * Creates combobutton item based on task. |
| * |
| * @param {!chrome.fileManagerPrivate.FileTask} task Task to convert. |
| * @param {string=} opt_title Title. |
| * @param {boolean=} opt_bold Make a menu item bold. |
| * @param {boolean=} opt_isDefault Mark the item as default item. |
| * @return {!FileTasks.ComboButtonItem} Item appendable to combobutton drop-down |
| * list. |
| * @private |
| */ |
| FileTasks.prototype.createCombobuttonItem_ = function( |
| task, opt_title, opt_bold, opt_isDefault) { |
| return { |
| type: FileTasks.TaskMenuButtonItemType.RunTask, |
| label: opt_title || task.label || task.title, |
| iconUrl: task.iconUrl, |
| iconType: task.iconType || '', |
| task: task, |
| bold: opt_bold || false, |
| isDefault: opt_isDefault || false, |
| isGenericFileHandler: task.isGenericFileHandler |
| }; |
| }; |
| |
| /** |
| * Shows modal task picker dialog with currently available list of tasks. |
| * |
| * @param {cr.filebrowser.DefaultTaskDialog} taskDialog Task dialog to show and |
| * update. |
| * @param {string} title Title to use. |
| * @param {string} message Message to use. |
| * @param {function(!chrome.fileManagerPrivate.FileTask)} onSuccess Callback to |
| * pass selected task. |
| * @param {FileTasks.TaskPickerType} pickerType Task picker type. |
| */ |
| FileTasks.prototype.showTaskPicker = function( |
| taskDialog, title, message, onSuccess, pickerType) { |
| var tasks = pickerType == FileTasks.TaskPickerType.MoreActions ? |
| this.getNonOpenTaskItems() : |
| this.getOpenTaskItems(); |
| var items = this.createItems_(tasks); |
| if (pickerType == FileTasks.TaskPickerType.ChangeDefault) |
| items = items.filter(item => !item.isGenericFileHandler); |
| |
| var defaultIdx = 0; |
| for (var j = 0; j < items.length; j++) { |
| if (this.defaultTask_ && items[j].task.taskId === this.defaultTask_.taskId) |
| defaultIdx = j; |
| } |
| |
| taskDialog.showDefaultTaskDialog( |
| title, |
| message, |
| items, defaultIdx, |
| function(item) { |
| onSuccess(item.task); |
| }); |
| }; |
| |
| /** |
| * Gets the default task from tasks. In case there is no such task (i.e. all |
| * tasks are generic file handlers), then return null. |
| * |
| * @param {!Array<!chrome.fileManagerPrivate.FileTask>} tasks The list of tasks |
| * from where to choose the default task. |
| * @param {!TaskHistory} taskHistory |
| * @return {?chrome.fileManagerPrivate.FileTask} the default task, or null if |
| * no default task found. |
| */ |
| FileTasks.getDefaultTask = function(tasks, taskHistory) { |
| // 1. Default app set for MIME or file extension by user, or built-in app. |
| for (var i = 0; i < tasks.length; i++) { |
| if (tasks[i].isDefault) { |
| return tasks[i]; |
| } |
| } |
| var nonGenericTasks = tasks.filter(t => !t.isGenericFileHandler); |
| // 2. Most recently executed non-generic task. |
| var latest = nonGenericTasks[0]; |
| if (latest && taskHistory.getLastExecutedTime(latest.taskId)) { |
| return latest; |
| } |
| // 3. Sole non-generic handler. |
| if (nonGenericTasks.length == 1) { |
| return nonGenericTasks[0]; |
| } |
| return null; |
| }; |