1 // Copyright (c) 2012 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.
6 * This object encapsulates everything related to tasks execution.
8 * TODO(hirono): Pass each component instead of the entire FileManager.
9 * @param {FileManager} fileManager FileManager instance.
10 * @param {Object=} opt_params File manager load parameters.
13 function FileTasks(fileManager, opt_params) {
14 this.fileManager_ = fileManager;
15 this.params_ = opt_params;
17 this.defaultTask_ = null;
21 * List of invocations to be called once tasks are available.
24 * @type {Array.<Object>}
26 this.pendingInvocations_ = [];
30 * Location of the Chrome Web Store.
35 FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore';
38 * Base URL of apps list in the Chrome Web Store. This constant is used in
39 * FileTasks.createWebStoreLink().
44 FileTasks.WEB_STORE_HANDLER_BASE_URL =
45 'https://chrome.google.com/webstore/category/collection/file_handlers';
49 * The app ID of the video player app.
53 FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko';
56 * The task id of the zip unpacker app.
60 FileTasks.ZIP_UNPACKER_TASK_ID = 'oedeeodfidgoollimchfdnbmhcpnklnd|app|zip';
63 * Returns URL of the Chrome Web Store which show apps supporting the given
64 * file-extension and mime-type.
66 * @param {?string} extension Extension of the file (with the first dot).
67 * @param {?string} mimeType Mime type of the file.
68 * @return {string} URL
70 FileTasks.createWebStoreLink = function(extension, mimeType) {
71 if (!extension || FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1)
72 return FileTasks.CHROME_WEB_STORE_URL;
74 if (extension[0] === '.')
75 extension = extension.substr(1);
77 console.warn('Please pass an extension with a dot to createWebStoreLink.');
79 var url = FileTasks.WEB_STORE_HANDLER_BASE_URL;
80 url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, '');
82 // If a mime is given, add it into the URL.
84 url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, '');
89 * Complete the initialization.
91 * @param {Array.<Entry>} entries List of file entries.
92 * @param {Array.<string>=} opt_mimeTypes Mime-type specified for each entries.
94 FileTasks.prototype.init = function(entries, opt_mimeTypes) {
95 this.entries_ = entries;
96 this.mimeTypes_ = opt_mimeTypes || [];
98 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
100 var urls = util.entriesToURLs(entries);
102 chrome.fileManagerPrivate.getFileTasks(urls, this.onTasks_.bind(this));
106 * Returns amount of tasks.
108 * @return {number} amount of tasks.
110 FileTasks.prototype.size = function() {
111 return (this.tasks_ && this.tasks_.length) || 0;
115 * Callback when tasks found.
117 * @param {Array.<Object>} tasks The tasks.
120 FileTasks.prototype.onTasks_ = function(tasks) {
121 this.processTasks_(tasks);
122 for (var index = 0; index < this.pendingInvocations_.length; index++) {
123 var name = this.pendingInvocations_[index][0];
124 var args = this.pendingInvocations_[index][1];
125 this[name].apply(this, args);
127 this.pendingInvocations_ = [];
131 * The list of known extensions to record UMA.
132 * Note: Because the data is recorded by the index, so new item shouldn't be
136 * @type {Array.<string>}
139 FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_ = Object.freeze([
140 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv',
141 '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8',
142 '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf',
143 '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt',
144 '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp',
145 '.wma', '.wmv', '.xls', '.xlsx', '.crdownload', '.crx', '.dmg', '.exe',
146 '.html', 'htm', '.jar', '.ps', '.torrent', '.txt', '.zip',
150 * The list of executable file extensions.
153 * @type {Array.<string>}
155 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
156 '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
160 * The list of extensions to skip the suggest app dialog.
162 * @type {Array.<string>}
165 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
166 '.crdownload', '.dsc', '.inf', '.crx',
170 * Records trial of opening file grouped by extensions.
172 * @param {Array.<Entry>} entries The entries to be opened.
175 FileTasks.recordViewingFileTypeUMA_ = function(entries) {
176 for (var i = 0; i < entries.length; i++) {
177 var entry = entries[i];
178 var extension = FileType.getExtension(entry).toLowerCase();
179 if (FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_.indexOf(extension) < 0) {
183 'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_);
188 * Returns true if the taskId is for an internal task.
190 * @param {string} taskId Task identifier.
191 * @return {boolean} True if the task ID is for an internal task.
194 FileTasks.isInternalTask_ = function(taskId) {
195 var taskParts = taskId.split('|');
196 var appId = taskParts[0];
197 var taskType = taskParts[1];
198 var actionId = taskParts[2];
199 // The action IDs here should match ones used in executeInternalTask_().
200 return (appId === chrome.runtime.id &&
201 taskType === 'file' &&
202 (actionId === 'play' ||
203 actionId === 'mount-archive'));
207 * Processes internal tasks.
209 * @param {Array.<Object>} tasks The tasks.
212 FileTasks.prototype.processTasks_ = function(tasks) {
214 var id = chrome.runtime.id;
215 var isOnDrive = false;
216 var fm = this.fileManager_;
217 for (var index = 0; index < this.entries_.length; ++index) {
218 var locationInfo = fm.volumeManager.getLocationInfo(this.entries_[index]);
219 if (locationInfo && locationInfo.isDriveBased) {
225 for (var i = 0; i < tasks.length; i++) {
227 var taskParts = task.taskId.split('|');
229 // Skip internal Files.app's handlers.
230 if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
231 taskParts[2] === 'select' || taskParts[2] === 'open')) {
235 // Tweak images, titles of internal tasks.
236 if (taskParts[0] === id && taskParts[1] === 'file') {
237 if (taskParts[2] === 'play') {
238 // TODO(serya): This hack needed until task.iconUrl is working
239 // (see GetFileTasksFileBrowserFunction::RunImpl).
240 task.iconType = 'audio';
241 task.title = loadTimeData.getString('ACTION_LISTEN');
242 } else if (taskParts[2] === 'mount-archive') {
243 task.iconType = 'archive';
244 task.title = loadTimeData.getString('MOUNT_ARCHIVE');
245 } else if (taskParts[2] === 'open-hosted-generic') {
246 if (this.entries_.length > 1)
247 task.iconType = 'generic';
248 else // Use specific icon.
249 task.iconType = FileType.getIcon(this.entries_[0]);
250 task.title = loadTimeData.getString('ACTION_OPEN');
251 } else if (taskParts[2] === 'open-hosted-gdoc') {
252 task.iconType = 'gdoc';
253 task.title = loadTimeData.getString('ACTION_OPEN_GDOC');
254 } else if (taskParts[2] === 'open-hosted-gsheet') {
255 task.iconType = 'gsheet';
256 task.title = loadTimeData.getString('ACTION_OPEN_GSHEET');
257 } else if (taskParts[2] === 'open-hosted-gslides') {
258 task.iconType = 'gslides';
259 task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES');
260 } else if (taskParts[2] === 'view-swf') {
261 // Do not render this task if disabled.
262 if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED'))
264 task.iconType = 'generic';
265 task.title = loadTimeData.getString('ACTION_VIEW');
266 } else if (taskParts[2] === 'view-pdf') {
267 // Do not render this task if disabled.
268 if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED'))
270 task.iconType = 'pdf';
271 task.title = loadTimeData.getString('ACTION_VIEW');
272 } else if (taskParts[2] === 'view-in-browser') {
273 task.iconType = 'generic';
274 task.title = loadTimeData.getString('ACTION_VIEW');
278 if (!task.iconType && taskParts[1] === 'web-intent') {
279 task.iconType = 'generic';
282 this.tasks_.push(task);
283 if (this.defaultTask_ === null && task.isDefault) {
284 this.defaultTask_ = task;
287 if (!this.defaultTask_ && this.tasks_.length > 0) {
288 // If we haven't picked a default task yet, then just pick the first one
289 // which is not generic file handler.
290 for (var i = 0; i < this.tasks_.length; i++) {
291 var task = this.tasks_[i];
292 if (!task.isGenericFileHandler) {
293 this.defaultTask_ = task;
301 * Executes default task.
303 * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the
304 * default task is executed, or the error is occurred.
307 FileTasks.prototype.executeDefault_ = function(opt_callback) {
308 FileTasks.recordViewingFileTypeUMA_(this.entries_);
309 this.executeDefaultInternal_(this.entries_, opt_callback);
313 * Executes default task.
315 * @param {Array.<Entry>} entries Entries to execute.
316 * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the
317 * default task is executed, or the error is occurred.
320 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
321 var callback = opt_callback || function(arg1, arg2) {};
323 if (this.defaultTask_ !== null) {
324 this.executeInternal_(this.defaultTask_.taskId, entries);
325 callback(true, entries);
329 // We don't have tasks, so try to show a file in a browser tab.
330 // We only do that for single selection to avoid confusion.
331 if (entries.length !== 1 || !entries[0])
334 var filename = entries[0].name;
335 var extension = util.splitExtension(filename)[1];
336 var mimeType = this.mimeTypes_[0];
338 var showAlert = function() {
344 textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
347 textMessageId = 'NO_ACTION_FOR_DMG';
350 textMessageId = 'NO_ACTION_FOR_CRX';
351 titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
354 textMessageId = 'NO_ACTION_FOR_FILE';
357 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType);
358 var text = strf(textMessageId, webStoreUrl, str('NO_ACTION_FOR_FILE_URL'));
359 var title = titleMessageId ? str(titleMessageId) : filename;
360 this.fileManager_.alert.showHtml(title, text, function() {});
361 callback(false, entries);
364 var onViewFilesFailure = function() {
365 var fm = this.fileManager_;
366 if (!fm.isOnDrive() ||
368 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
373 fm.openSuggestAppsDialog(
376 var newTasks = new FileTasks(fm);
377 newTasks.init(entries, this.mimeTypes_);
378 newTasks.executeDefault();
379 callback(true, entries);
381 // Cancelled callback.
383 callback(false, entries);
388 var onViewFiles = function(result) {
391 callback(true, entries);
394 util.isTeleported(window).then(function(teleported) {
396 util.showOpenInOtherDesktopAlert(
397 this.fileManager_.ui.alertDialog, entries);
400 callback(true, entries);
403 callback(true, entries);
406 onViewFilesFailure();
411 this.checkAvailability_(function() {
412 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
414 var urls = util.entriesToURLs(entries);
415 var taskId = chrome.runtime.id + '|file|view-in-browser';
416 chrome.fileManagerPrivate.executeTask(taskId, urls, onViewFiles);
421 * Executes a single task.
423 * @param {string} taskId Task identifier.
424 * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
428 FileTasks.prototype.execute_ = function(taskId, opt_entries) {
429 var entries = opt_entries || this.entries_;
430 FileTasks.recordViewingFileTypeUMA_(entries);
431 this.executeInternal_(taskId, entries);
435 * The core implementation to execute a single task.
437 * @param {string} taskId Task identifier.
438 * @param {Array.<Entry>} entries Entries to execute.
441 FileTasks.prototype.executeInternal_ = function(taskId, entries) {
442 this.checkAvailability_(function() {
443 if (FileTasks.isInternalTask_(taskId)) {
444 var taskParts = taskId.split('|');
445 this.executeInternalTask_(taskParts[2], entries);
447 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
449 var urls = util.entriesToURLs(entries);
450 chrome.fileManagerPrivate.executeTask(taskId, urls, function(result) {
451 if (result !== 'message_sent')
453 util.isTeleported(window).then(function(teleported) {
455 util.showOpenInOtherDesktopAlert(
456 this.fileManager_.ui.alertDialog, entries);
465 * Checks whether the remote files are available right now.
467 * @param {function()} callback The callback.
470 FileTasks.prototype.checkAvailability_ = function(callback) {
471 var areAll = function(props, name) {
472 var isOne = function(e) {
473 // If got no properties, we safely assume that item is unavailable.
476 return props.filter(isOne).length === props.length;
479 var fm = this.fileManager_;
480 var entries = this.entries_;
482 var isDriveOffline = fm.volumeManager.getDriveConnectionState().type ===
483 VolumeManagerCommon.DriveConnectionType.OFFLINE;
485 if (fm.isOnDrive() && isDriveOffline) {
486 fm.metadataCache_.get(entries, 'external', function(props) {
487 if (areAll(props, 'availableOffline')) {
493 loadTimeData.getString('OFFLINE_HEADER'),
495 loadTimeData.getStringF(
496 entries.length === 1 ?
497 'HOSTED_OFFLINE_MESSAGE' :
498 'HOSTED_OFFLINE_MESSAGE_PLURAL') :
499 loadTimeData.getStringF(
500 entries.length === 1 ?
502 'OFFLINE_MESSAGE_PLURAL',
503 loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
508 var isOnMetered = fm.volumeManager.getDriveConnectionState().type ===
509 VolumeManagerCommon.DriveConnectionType.METERED;
511 if (fm.isOnDrive() && isOnMetered) {
512 fm.metadataCache_.get(entries, 'external', function(driveProps) {
513 if (areAll(driveProps, 'availableWhenMetered')) {
518 fm.metadataCache_.get(entries, 'filesystem', function(fileProps) {
519 var sizeToDownload = 0;
520 for (var i = 0; i !== entries.length; i++) {
521 if (!driveProps[i].availableWhenMetered)
522 sizeToDownload += fileProps[i].size;
525 loadTimeData.getStringF(
526 entries.length === 1 ?
527 'CONFIRM_MOBILE_DATA_USE' :
528 'CONFIRM_MOBILE_DATA_USE_PLURAL',
529 util.bytesToString(sizeToDownload)),
540 * Executes an internal task.
542 * @param {string} id The short task id.
543 * @param {Array.<Entry>} entries The entries to execute on.
546 FileTasks.prototype.executeInternalTask_ = function(id, entries) {
547 var fm = this.fileManager_;
549 if (id === 'mount-archive') {
550 this.mountArchivesInternal_(entries);
554 console.error('Unexpected action ID: ' + id);
560 * @param {Array.<Entry>} entries Mount file entries list.
562 FileTasks.prototype.mountArchives = function(entries) {
563 FileTasks.recordViewingFileTypeUMA_(entries);
564 this.mountArchivesInternal_(entries);
568 * The core implementation of mounts archives.
570 * @param {Array.<Entry>} entries Mount file entries list.
573 FileTasks.prototype.mountArchivesInternal_ = function(entries) {
574 var fm = this.fileManager_;
576 var tracker = fm.directoryModel.createDirectoryChangeTracker();
579 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
581 var urls = util.entriesToURLs(entries);
582 for (var index = 0; index < urls.length; ++index) {
583 // TODO(mtomasz): Pass Entry instead of URL.
584 fm.volumeManager.mountArchive(
586 function(volumeInfo) {
587 if (tracker.hasChanged) {
591 volumeInfo.resolveDisplayRoot(function(displayRoot) {
592 if (tracker.hasChanged) {
596 fm.directoryModel.changeDirectoryEntry(displayRoot);
598 console.warn('Failed to resolve the display root after mounting.');
601 }, function(url, error) {
603 var path = util.extractFilePath(url);
604 var namePos = path.lastIndexOf('/');
605 fm.alert.show(strf('ARCHIVE_MOUNT_FAILED',
606 path.substr(namePos + 1), error));
607 }.bind(null, urls[index]));
612 * Displays the list of tasks in a task picker combobutton.
614 * @param {cr.ui.ComboButton} combobutton The task picker element.
617 FileTasks.prototype.display_ = function(combobutton) {
618 // If there does not exist available task, hide combobutton.
619 if (this.tasks_.length === 0) {
620 combobutton.hidden = true;
625 combobutton.hidden = false;
627 // If there exist defaultTask show it on the combobutton.
628 if (this.defaultTask_) {
629 if (this.defaultTask_.taskId === FileTasks.ZIP_UNPACKER_TASK_ID) {
630 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_,
633 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
636 combobutton.defaultItem = {
637 label: loadTimeData.getString('MORE_ACTIONS')
641 // If there exist 2 or more available tasks, show them in context menu
642 // (including defaultTask). If only one generic task is available, we also
643 // show it in the context menu.
644 var items = this.createItems_();
646 if (items.length > 1 || (items.length === 1 && this.defaultTask_ === null)) {
647 for (var j = 0; j < items.length; j++) {
648 combobutton.addDropDownItem(items[j]);
651 // If there exist non generic task (i.e. defaultTask is set), we show an
652 // item to change default action.
653 if (this.defaultTask_) {
654 combobutton.addSeparator();
655 var changeDefaultMenuItem = combobutton.addDropDownItem({
656 label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
658 changeDefaultMenuItem.classList.add('change-default');
664 * Creates sorted array of available task descriptions such as title and icon.
666 * @return {Array} created array can be used to feed combobox, menus and so on.
669 FileTasks.prototype.createItems_ = function() {
673 for (var index = 0; index < this.tasks_.length; index++) {
674 var task = this.tasks_[index];
675 if (task === this.defaultTask_) {
676 var title = task.title + ' ' +
677 loadTimeData.getString('DEFAULT_ACTION_LABEL');
678 items.push(this.createCombobuttonItem_(task, title, true, true));
680 items.push(this.createCombobuttonItem_(task));
684 // Sort items (Sort order: isDefault, isGenericFileHandler, label).
685 items.sort(function(a, b) {
686 // Sort by isDefaultTask.
687 var isDefault = (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0);
691 // Sort by isGenericFileHandler.
692 var isGenericFileHandler =
693 (a.isGenericFileHandler ? 1 : 0) - (b.isGenericFileHandler ? 1 : 0);
694 if (isGenericFileHandler !== 0)
695 return isGenericFileHandler;
698 return a.label.localeCompare(b.label);
705 * Updates context menu with default item.
709 FileTasks.prototype.updateMenuItem_ = function() {
710 this.fileManager_.updateContextMenuActionItems(this.tasks_);
714 * Creates combobutton item based on task.
716 * @param {Object} task Task to convert.
717 * @param {string=} opt_title Title.
718 * @param {boolean=} opt_bold Make a menu item bold.
719 * @param {boolean=} opt_isDefault Mark the item as default item.
720 * @return {Object} Item appendable to combobutton drop-down list.
723 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
727 label: opt_title || task.title,
728 iconUrl: task.iconUrl,
729 iconType: task.iconType,
731 bold: opt_bold || false,
732 isDefault: opt_isDefault || false,
733 isGenericFileHandler: task.isGenericFileHandler
738 * Shows modal action picker dialog with currently available list of tasks.
740 * @param {cr.filebrowser.DefaultActionDialog} actionDialog Action dialog to
742 * @param {string} title Title to use.
743 * @param {string} message Message to use.
744 * @param {function(Object)} onSuccess Callback to pass selected task.
745 * @param {boolean=} opt_hideGenericFileHandler Whether to hide generic file
748 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
750 opt_hideGenericFileHandler) {
751 var hideGenericFileHandler = opt_hideGenericFileHandler || false;
752 var items = this.createItems_();
754 if (hideGenericFileHandler)
755 items = items.filter(function(item) { return !item.isGenericFileHandler; });
758 for (var j = 0; j < items.length; j++) {
759 if (items[j].task.taskId === this.defaultTask_.taskId)
768 onSuccess(item.task);
773 * Decorates a FileTasks method, so it will be actually executed after the tasks
775 * This decorator expects an implementation called |method + '_'|.
777 * @param {string} method The method name.
779 FileTasks.decorate = function(method) {
780 var privateMethod = method + '_';
781 FileTasks.prototype[method] = function() {
783 this[privateMethod].apply(this, arguments);
785 this.pendingInvocations_.push([privateMethod, arguments]);
791 FileTasks.decorate('display');
792 FileTasks.decorate('updateMenuItem');
793 FileTasks.decorate('execute');
794 FileTasks.decorate('executeDefault');