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.
8 * This object encapsulates everything related to tasks execution.
10 * TODO(hirono): Pass each component instead of the entire FileManager.
11 * @param {FileManager} fileManager FileManager instance.
12 * @param {Object=} opt_params File manager load parameters.
15 function FileTasks(fileManager, opt_params) {
16 this.fileManager_ = fileManager;
17 this.params_ = opt_params;
19 this.defaultTask_ = null;
23 * List of invocations to be called once tasks are available.
26 * @type {Array.<Object>}
28 this.pendingInvocations_ = [];
32 * Location of the Chrome Web Store.
37 FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore';
40 * Base URL of apps list in the Chrome Web Store. This constant is used in
41 * FileTasks.createWebStoreLink().
46 FileTasks.WEB_STORE_HANDLER_BASE_URL =
47 'https://chrome.google.com/webstore/category/collection/file_handlers';
51 * The app ID of the video player app.
55 FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko';
58 * Returns URL of the Chrome Web Store which show apps supporting the given
59 * file-extension and mime-type.
61 * @param {string} extension Extension of the file (with the first dot).
62 * @param {string} mimeType Mime type of the file.
63 * @return {string} URL
65 FileTasks.createWebStoreLink = function(extension, mimeType) {
66 if (!extension || FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1)
67 return FileTasks.CHROME_WEB_STORE_URL;
69 if (extension[0] === '.')
70 extension = extension.substr(1);
72 console.warn('Please pass an extension with a dot to createWebStoreLink.');
74 var url = FileTasks.WEB_STORE_HANDLER_BASE_URL;
75 url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, '');
77 // If a mime is given, add it into the URL.
79 url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, '');
84 * Complete the initialization.
86 * @param {Array.<Entry>} entries List of file entries.
87 * @param {Array.<string>=} opt_mimeTypes Mime-type specified for each entries.
89 FileTasks.prototype.init = function(entries, opt_mimeTypes) {
90 this.entries_ = entries;
91 this.mimeTypes_ = opt_mimeTypes || [];
93 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
95 var urls = util.entriesToURLs(entries);
97 chrome.fileManagerPrivate.getFileTasks(urls, this.onTasks_.bind(this));
101 * Returns amount of tasks.
103 * @return {number} amount of tasks.
105 FileTasks.prototype.size = function() {
106 return (this.tasks_ && this.tasks_.length) || 0;
110 * Callback when tasks found.
112 * @param {Array.<Object>} tasks The tasks.
115 FileTasks.prototype.onTasks_ = function(tasks) {
116 this.processTasks_(tasks);
117 for (var index = 0; index < this.pendingInvocations_.length; index++) {
118 var name = this.pendingInvocations_[index][0];
119 var args = this.pendingInvocations_[index][1];
120 this[name].apply(this, args);
122 this.pendingInvocations_ = [];
126 * The list of known extensions to record UMA.
127 * Note: Because the data is recorded by the index, so new item shouldn't be
131 * @type {Array.<string>}
134 FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_ = Object.freeze([
135 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv',
136 '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8',
137 '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf',
138 '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt',
139 '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp',
140 '.wma', '.wmv', '.xls', '.xlsx', '.crdownload', '.crx', '.dmg', '.exe',
141 '.html', 'htm', '.jar', '.ps', '.torrent', '.txt', '.zip',
145 * The list of executable file extensions.
148 * @type {Array.<string>}
150 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
151 '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
155 * The list of extensions to skip the suggest app dialog.
157 * @type {Array.<string>}
160 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
161 '.crdownload', '.dsc', '.inf', '.crx',
165 * Records trial of opening file grouped by extensions.
167 * @param {Array.<Entry>} entries The entries to be opened.
170 FileTasks.recordViewingFileTypeUMA_ = function(entries) {
171 for (var i = 0; i < entries.length; i++) {
172 var entry = entries[i];
173 var extension = FileType.getExtension(entry).toLowerCase();
174 if (FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_.indexOf(extension) < 0) {
178 'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_);
183 * Returns true if the taskId is for an internal task.
185 * @param {string} taskId Task identifier.
186 * @return {boolean} True if the task ID is for an internal task.
189 FileTasks.isInternalTask_ = function(taskId) {
190 var taskParts = taskId.split('|');
191 var appId = taskParts[0];
192 var taskType = taskParts[1];
193 var actionId = taskParts[2];
194 // The action IDs here should match ones used in executeInternalTask_().
195 return (appId === chrome.runtime.id &&
196 taskType === 'file' &&
197 (actionId === 'play' ||
198 actionId === 'mount-archive'));
202 * Processes internal tasks.
204 * @param {Array.<Object>} tasks The tasks.
207 FileTasks.prototype.processTasks_ = function(tasks) {
209 var id = chrome.runtime.id;
210 var isOnDrive = false;
211 var fm = this.fileManager_;
212 for (var index = 0; index < this.entries_.length; ++index) {
213 var locationInfo = fm.volumeManager.getLocationInfo(this.entries_[index]);
214 if (locationInfo && locationInfo.isDriveBased) {
220 for (var i = 0; i < tasks.length; i++) {
222 var taskParts = task.taskId.split('|');
224 // Skip internal Files.app's handlers.
225 if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
226 taskParts[2] === 'select' || taskParts[2] === 'open')) {
230 // Tweak images, titles of internal tasks.
231 if (taskParts[0] === id && taskParts[1] === 'file') {
232 if (taskParts[2] === 'play') {
233 // TODO(serya): This hack needed until task.iconUrl is working
234 // (see GetFileTasksFileBrowserFunction::RunImpl).
235 task.iconType = 'audio';
236 task.title = loadTimeData.getString('ACTION_LISTEN');
237 } else if (taskParts[2] === 'mount-archive') {
238 task.iconType = 'archive';
239 task.title = loadTimeData.getString('MOUNT_ARCHIVE');
240 } else if (taskParts[2] === 'open-hosted-generic') {
241 if (this.entries_.length > 1)
242 task.iconType = 'generic';
243 else // Use specific icon.
244 task.iconType = FileType.getIcon(this.entries_[0]);
245 task.title = loadTimeData.getString('ACTION_OPEN');
246 } else if (taskParts[2] === 'open-hosted-gdoc') {
247 task.iconType = 'gdoc';
248 task.title = loadTimeData.getString('ACTION_OPEN_GDOC');
249 } else if (taskParts[2] === 'open-hosted-gsheet') {
250 task.iconType = 'gsheet';
251 task.title = loadTimeData.getString('ACTION_OPEN_GSHEET');
252 } else if (taskParts[2] === 'open-hosted-gslides') {
253 task.iconType = 'gslides';
254 task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES');
255 } else if (taskParts[2] === 'view-swf') {
256 // Do not render this task if disabled.
257 if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED'))
259 task.iconType = 'generic';
260 task.title = loadTimeData.getString('ACTION_VIEW');
261 } else if (taskParts[2] === 'view-pdf') {
262 // Do not render this task if disabled.
263 if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED'))
265 task.iconType = 'pdf';
266 task.title = loadTimeData.getString('ACTION_VIEW');
267 } else if (taskParts[2] === 'view-in-browser') {
268 task.iconType = 'generic';
269 task.title = loadTimeData.getString('ACTION_VIEW');
273 if (!task.iconType && taskParts[1] === 'web-intent') {
274 task.iconType = 'generic';
277 this.tasks_.push(task);
278 if (this.defaultTask_ === null && task.isDefault) {
279 this.defaultTask_ = task;
282 if (!this.defaultTask_ && this.tasks_.length > 0) {
283 // If we haven't picked a default task yet, then just pick the first one.
284 // This is not the preferred way we want to pick this, but better this than
285 // no default at all if the C++ code didn't set one.
286 this.defaultTask_ = this.tasks_[0];
291 * Executes default task.
293 * @param {function(boolean, Array.<string>)=} opt_callback Called when the
294 * default task is executed, or the error is occurred.
297 FileTasks.prototype.executeDefault_ = function(opt_callback) {
298 FileTasks.recordViewingFileTypeUMA_(this.entries_);
299 this.executeDefaultInternal_(this.entries_, opt_callback);
303 * Executes default task.
305 * @param {Array.<Entry>} entries Entries to execute.
306 * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the
307 * default task is executed, or the error is occurred.
310 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
311 var callback = opt_callback || function(arg1, arg2) {};
313 if (this.defaultTask_ !== null) {
314 this.executeInternal_(this.defaultTask_.taskId, entries);
315 callback(true, entries);
319 // We don't have tasks, so try to show a file in a browser tab.
320 // We only do that for single selection to avoid confusion.
321 if (entries.length !== 1 || !entries[0])
324 var filename = entries[0].name;
325 var extension = util.splitExtension(filename)[1];
326 var mimeType = this.mimeTypes_[0];
328 var showAlert = function() {
334 textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
337 textMessageId = 'NO_ACTION_FOR_DMG';
340 textMessageId = 'NO_ACTION_FOR_CRX';
341 titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
344 textMessageId = 'NO_ACTION_FOR_FILE';
347 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType);
348 var text = strf(textMessageId, webStoreUrl, str('NO_ACTION_FOR_FILE_URL'));
349 var title = titleMessageId ? str(titleMessageId) : filename;
350 this.fileManager_.alert.showHtml(title, text, function() {});
351 callback(false, entries);
354 var onViewFilesFailure = function() {
355 var fm = this.fileManager_;
356 if (!fm.isOnDrive() ||
358 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
363 fm.openSuggestAppsDialog(
366 var newTasks = new FileTasks(fm);
367 newTasks.init(entries, this.mimeTypes_);
368 newTasks.executeDefault();
369 callback(true, entries);
371 // Cancelled callback.
373 callback(false, entries);
378 var onViewFiles = function(result) {
381 callback(success, entries);
384 util.isTeleported(window).then(function(teleported) {
386 util.showOpenInOtherDesktopAlert(
387 this.fileManager_.ui.alertDialog, entries);
390 callback(success, entries);
393 callback(success, entries);
396 onViewFilesFailure();
401 this.checkAvailability_(function() {
402 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
404 var urls = util.entriesToURLs(entries);
405 var taskId = chrome.runtime.id + '|file|view-in-browser';
406 chrome.fileManagerPrivate.executeTask(taskId, urls, onViewFiles);
411 * Executes a single task.
413 * @param {string} taskId Task identifier.
414 * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
418 FileTasks.prototype.execute_ = function(taskId, opt_entries) {
419 var entries = opt_entries || this.entries_;
420 FileTasks.recordViewingFileTypeUMA_(entries);
421 this.executeInternal_(taskId, entries);
425 * The core implementation to execute a single task.
427 * @param {string} taskId Task identifier.
428 * @param {Array.<Entry>} entries Entries to execute.
431 FileTasks.prototype.executeInternal_ = function(taskId, entries) {
432 this.checkAvailability_(function() {
433 if (FileTasks.isInternalTask_(taskId)) {
434 var taskParts = taskId.split('|');
435 this.executeInternalTask_(taskParts[2], entries);
437 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
439 var urls = util.entriesToURLs(entries);
440 chrome.fileManagerPrivate.executeTask(taskId, urls, function(result) {
441 if (result !== 'message_sent')
443 util.isTeleported(window).then(function(teleported) {
445 util.showOpenInOtherDesktopAlert(
446 this.fileManager_.ui.alertDialog, entries);
455 * Checks whether the remote files are available right now.
457 * @param {function} callback The callback.
460 FileTasks.prototype.checkAvailability_ = function(callback) {
461 var areAll = function(props, name) {
462 var isOne = function(e) {
463 // If got no properties, we safely assume that item is unavailable.
466 return props.filter(isOne).length === props.length;
469 var fm = this.fileManager_;
470 var entries = this.entries_;
472 var isDriveOffline = fm.volumeManager.getDriveConnectionState().type ===
473 VolumeManagerCommon.DriveConnectionType.OFFLINE;
475 if (fm.isOnDrive() && isDriveOffline) {
476 fm.metadataCache_.get(entries, 'external', function(props) {
477 if (areAll(props, 'availableOffline')) {
483 loadTimeData.getString('OFFLINE_HEADER'),
485 loadTimeData.getStringF(
486 entries.length === 1 ?
487 'HOSTED_OFFLINE_MESSAGE' :
488 'HOSTED_OFFLINE_MESSAGE_PLURAL') :
489 loadTimeData.getStringF(
490 entries.length === 1 ?
492 'OFFLINE_MESSAGE_PLURAL',
493 loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
498 var isOnMetered = fm.volumeManager.getDriveConnectionState().type ===
499 VolumeManagerCommon.DriveConnectionType.METERED;
501 if (fm.isOnDrive() && isOnMetered) {
502 fm.metadataCache_.get(entries, 'external', function(driveProps) {
503 if (areAll(driveProps, 'availableWhenMetered')) {
508 fm.metadataCache_.get(entries, 'filesystem', function(fileProps) {
509 var sizeToDownload = 0;
510 for (var i = 0; i !== entries.length; i++) {
511 if (!driveProps[i].availableWhenMetered)
512 sizeToDownload += fileProps[i].size;
515 loadTimeData.getStringF(
516 entries.length === 1 ?
517 'CONFIRM_MOBILE_DATA_USE' :
518 'CONFIRM_MOBILE_DATA_USE_PLURAL',
519 util.bytesToString(sizeToDownload)),
530 * Executes an internal task.
532 * @param {string} id The short task id.
533 * @param {Array.<Entry>} entries The entries to execute on.
536 FileTasks.prototype.executeInternalTask_ = function(id, entries) {
537 var fm = this.fileManager_;
540 var selectedEntry = entries[0];
541 if (entries.length === 1) {
542 // If just a single audio file is selected pass along every audio file
544 entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio);
546 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
548 var urls = util.entriesToURLs(entries);
549 var position = urls.indexOf(selectedEntry.toURL());
550 chrome.fileManagerPrivate.getProfiles(
551 function(profiles, currentId, displayedId) {
552 fm.backgroundPage.launchAudioPlayer(
553 {items: urls, position: position}, displayedId);
558 if (id === 'mount-archive') {
559 this.mountArchivesInternal_(entries);
563 console.error('Unexpected action ID: ' + id);
569 * @param {Array.<Entry>} entries Mount file entries list.
571 FileTasks.prototype.mountArchives = function(entries) {
572 FileTasks.recordViewingFileTypeUMA_(entries);
573 this.mountArchivesInternal_(entries);
577 * The core implementation of mounts archives.
579 * @param {Array.<Entry>} entries Mount file entries list.
582 FileTasks.prototype.mountArchivesInternal_ = function(entries) {
583 var fm = this.fileManager_;
585 var tracker = fm.directoryModel.createDirectoryChangeTracker();
588 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
590 var urls = util.entriesToURLs(entries);
591 for (var index = 0; index < urls.length; ++index) {
592 // TODO(mtomasz): Pass Entry instead of URL.
593 fm.volumeManager.mountArchive(
595 function(volumeInfo) {
596 if (tracker.hasChanged) {
600 volumeInfo.resolveDisplayRoot(function(displayRoot) {
601 if (tracker.hasChanged) {
605 fm.directoryModel.changeDirectoryEntry(displayRoot);
607 console.warn('Failed to resolve the display root after mounting.');
610 }, function(url, error) {
612 var path = util.extractFilePath(url);
613 var namePos = path.lastIndexOf('/');
614 fm.alert.show(strf('ARCHIVE_MOUNT_FAILED',
615 path.substr(namePos + 1), error));
616 }.bind(null, urls[index]));
621 * Displays the list of tasks in a task picker combobutton.
623 * @param {cr.ui.ComboButton} combobutton The task picker element.
626 FileTasks.prototype.display_ = function(combobutton) {
627 if (this.tasks_.length === 0) {
628 combobutton.hidden = true;
633 combobutton.hidden = false;
634 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
636 var items = this.createItems_();
638 if (items.length > 1) {
641 for (var j = 0; j < items.length; j++) {
642 combobutton.addDropDownItem(items[j]);
643 if (items[j].task.taskId === this.defaultTask_.taskId)
647 combobutton.addSeparator();
648 var changeDefaultMenuItem = combobutton.addDropDownItem({
649 label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
651 changeDefaultMenuItem.classList.add('change-default');
656 * Creates sorted array of available task descriptions such as title and icon.
658 * @return {Array} created array can be used to feed combobox, menus and so on.
661 FileTasks.prototype.createItems_ = function() {
663 var title = this.defaultTask_.title + ' ' +
664 loadTimeData.getString('DEFAULT_ACTION_LABEL');
665 items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
667 for (var index = 0; index < this.tasks_.length; index++) {
668 var task = this.tasks_[index];
669 if (task !== this.defaultTask_)
670 items.push(this.createCombobuttonItem_(task));
673 items.sort(function(a, b) {
674 return a.label.localeCompare(b.label);
681 * Updates context menu with default item.
685 FileTasks.prototype.updateMenuItem_ = function() {
686 this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
687 this.tasks_.length > 1);
691 * Creates combobutton item based on task.
693 * @param {Object} task Task to convert.
694 * @param {string=} opt_title Title.
695 * @param {boolean=} opt_bold Make a menu item bold.
696 * @return {Object} Item appendable to combobutton drop-down list.
699 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
702 label: opt_title || task.title,
703 iconUrl: task.iconUrl,
704 iconType: task.iconType,
706 bold: opt_bold || false
711 * Shows modal action picker dialog with currently available list of tasks.
713 * @param {DefaultActionDialog} actionDialog Action dialog to show and update.
714 * @param {string} title Title to use.
715 * @param {string} message Message to use.
716 * @param {function(Object)} onSuccess Callback to pass selected task.
718 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
720 var items = this.createItems_();
723 for (var j = 0; j < items.length; j++) {
724 if (items[j].task.taskId === this.defaultTask_.taskId)
733 onSuccess(item.task);
738 * Decorates a FileTasks method, so it will be actually executed after the tasks
740 * This decorator expects an implementation called |method + '_'|.
742 * @param {string} method The method name.
744 FileTasks.decorate = function(method) {
745 var privateMethod = method + '_';
746 FileTasks.prototype[method] = function() {
748 this[privateMethod].apply(this, arguments);
750 this.pendingInvocations_.push([privateMethod, arguments]);
756 FileTasks.decorate('display');
757 FileTasks.decorate('updateMenuItem');
758 FileTasks.decorate('execute');
759 FileTasks.decorate('executeDefault');