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) {
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 List of MIME types for each
90 FileTasks.prototype.init = function(entries, opt_mimeTypes) {
91 this.entries_ = entries;
92 this.mimeTypes_ = opt_mimeTypes || [];
94 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
95 var urls = util.entriesToURLs(entries);
96 if (urls.length > 0) {
97 chrome.fileBrowserPrivate.getFileTasks(urls, this.mimeTypes_,
98 this.onTasks_.bind(this));
103 * Returns amount of tasks.
105 * @return {number} amount of tasks.
107 FileTasks.prototype.size = function() {
108 return (this.tasks_ && this.tasks_.length) || 0;
112 * Callback when tasks found.
114 * @param {Array.<Object>} tasks The tasks.
117 FileTasks.prototype.onTasks_ = function(tasks) {
118 this.processTasks_(tasks);
119 for (var index = 0; index < this.pendingInvocations_.length; index++) {
120 var name = this.pendingInvocations_[index][0];
121 var args = this.pendingInvocations_[index][1];
122 this[name].apply(this, args);
124 this.pendingInvocations_ = [];
128 * The list of known extensions to record UMA.
129 * Note: Because the data is recorded by the index, so new item shouldn't be
133 * @type {Array.<string>}
136 FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_ = Object.freeze([
137 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv',
138 '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8',
139 '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf',
140 '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt',
141 '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp',
142 '.wma', '.wmv', '.xls', '.xlsx',
146 * The list of excutable file extensions.
149 * @type {Array.<string>}
151 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
152 '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
156 * The list of extensions to skip the suggest app dialog.
158 * @type {Array.<string>}
161 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
162 '.crdownload', '.dsc', '.inf', '.crx',
166 * Records trial of opening file grouped by extensions.
168 * @param {Array.<Entry>} entries The entries to be opened.
171 FileTasks.recordViewingFileTypeUMA_ = function(entries) {
172 for (var i = 0; i < entries.length; i++) {
173 var entry = entries[i];
174 var extension = FileType.getExtension(entry).toLowerCase();
175 if (FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_.indexOf(extension) < 0) {
179 'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_);
184 * Returns true if the taskId is for an internal task.
186 * @param {string} taskId Task identifier.
187 * @return {boolean} True if the task ID is for an internal task.
190 FileTasks.isInternalTask_ = function(taskId) {
191 var taskParts = taskId.split('|');
192 var appId = taskParts[0];
193 var taskType = taskParts[1];
194 var actionId = taskParts[2];
195 // The action IDs here should match ones used in executeInternalTask_().
196 return (appId === chrome.runtime.id &&
197 taskType === 'file' &&
198 (actionId === 'play' ||
199 actionId === 'mount-archive' ||
200 actionId === 'gallery' ||
201 actionId === 'gallery-video'));
205 * Processes internal tasks.
207 * @param {Array.<Object>} tasks The tasks.
210 FileTasks.prototype.processTasks_ = function(tasks) {
212 var id = chrome.runtime.id;
213 var isOnDrive = false;
214 var fm = this.fileManager_;
215 for (var index = 0; index < this.entries_.length; ++index) {
216 var locationInfo = fm.volumeManager.getLocationInfo(this.entries_[index]);
217 if (locationInfo && locationInfo.isDriveBased) {
223 for (var i = 0; i < tasks.length; i++) {
225 var taskParts = task.taskId.split('|');
227 // Skip internal Files.app's handlers.
228 if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
229 taskParts[2] === 'select' || taskParts[2] === 'open')) {
233 // Tweak images, titles of internal tasks.
234 if (taskParts[0] === id && taskParts[1] === 'file') {
235 if (taskParts[2] === 'play') {
236 // TODO(serya): This hack needed until task.iconUrl is working
237 // (see GetFileTasksFileBrowserFunction::RunImpl).
238 task.iconType = 'audio';
239 task.title = loadTimeData.getString('ACTION_LISTEN');
240 } else if (taskParts[2] === 'mount-archive') {
241 task.iconType = 'archive';
242 task.title = loadTimeData.getString('MOUNT_ARCHIVE');
243 } else if (taskParts[2] === 'gallery' ||
244 taskParts[2] === 'gallery-video') {
245 task.iconType = 'image';
246 task.title = loadTimeData.getString('ACTION_OPEN');
247 } else if (taskParts[2] === 'open-hosted-generic') {
248 if (this.entries_.length > 1)
249 task.iconType = 'generic';
250 else // Use specific icon.
251 task.iconType = FileType.getIcon(this.entries_[0]);
252 task.title = loadTimeData.getString('ACTION_OPEN');
253 } else if (taskParts[2] === 'open-hosted-gdoc') {
254 task.iconType = 'gdoc';
255 task.title = loadTimeData.getString('ACTION_OPEN_GDOC');
256 } else if (taskParts[2] === 'open-hosted-gsheet') {
257 task.iconType = 'gsheet';
258 task.title = loadTimeData.getString('ACTION_OPEN_GSHEET');
259 } else if (taskParts[2] === 'open-hosted-gslides') {
260 task.iconType = 'gslides';
261 task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES');
262 } else if (taskParts[2] === 'view-swf') {
263 // Do not render this task if disabled.
264 if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED'))
266 task.iconType = 'generic';
267 task.title = loadTimeData.getString('ACTION_VIEW');
268 } else if (taskParts[2] === 'view-pdf') {
269 // Do not render this task if disabled.
270 if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED'))
272 task.iconType = 'pdf';
273 task.title = loadTimeData.getString('ACTION_VIEW');
274 } else if (taskParts[2] === 'view-in-browser') {
275 task.iconType = 'generic';
276 task.title = loadTimeData.getString('ACTION_VIEW');
280 if (!task.iconType && taskParts[1] === 'web-intent') {
281 task.iconType = 'generic';
284 this.tasks_.push(task);
285 if (this.defaultTask_ === null && task.isDefault) {
286 this.defaultTask_ = task;
289 if (!this.defaultTask_ && this.tasks_.length > 0) {
290 // If we haven't picked a default task yet, then just pick the first one.
291 // This is not the preferred way we want to pick this, but better this than
292 // no default at all if the C++ code didn't set one.
293 this.defaultTask_ = this.tasks_[0];
298 * Executes default task.
300 * @param {function(boolean, Array.<string>)=} opt_callback Called when the
301 * default task is executed, or the error is occurred.
304 FileTasks.prototype.executeDefault_ = function(opt_callback) {
305 FileTasks.recordViewingFileTypeUMA_(this.entries_);
306 this.executeDefaultInternal_(this.entries_, opt_callback);
310 * Executes default task.
312 * @param {Array.<Entry>} entries Entries to execute.
313 * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the
314 * default task is executed, or the error is occurred.
317 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
318 var callback = opt_callback || function(arg1, arg2) {};
320 if (this.defaultTask_ !== null) {
321 this.executeInternal_(this.defaultTask_.taskId, entries);
322 callback(true, entries);
326 // We don't have tasks, so try to show a file in a browser tab.
327 // We only do that for single selection to avoid confusion.
328 if (entries.length !== 1 || !entries[0])
331 var filename = entries[0].name;
332 var extension = PathUtil.splitExtension(filename)[1];
333 var mimeType = this.mimeTypes_[0];
335 var showAlert = function() {
340 textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
343 textMessageId = 'NO_ACTION_FOR_CRX';
344 titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
347 textMessageId = 'NO_ACTION_FOR_FILE';
350 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType);
351 var text = strf(textMessageId, webStoreUrl, str('NO_ACTION_FOR_FILE_URL'));
352 var title = titleMessageId ? str(titleMessageId) : filename;
353 this.fileManager_.alert.showHtml(title, text, function() {});
354 callback(false, urls);
357 var onViewFilesFailure = function() {
358 var fm = this.fileManager_;
359 if (!fm.isOnDrive() ||
361 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
366 fm.openSuggestAppsDialog(
369 var newTasks = new FileTasks(fm);
370 newTasks.init(entries, this.mimeTypes_);
371 newTasks.executeDefault();
372 callback(true, entries);
374 // Cancelled callback.
376 callback(false, entries);
381 var onViewFiles = function(result) {
384 callback(success, entries);
387 util.isTeleported(window).then(function(teleported) {
389 util.showOpenInOtherDesktopAlert(
390 this.fileManager_.ui.alertDialog, entries);
393 callback(success, entries);
396 callback(success, entries);
399 onViewFilesFailure();
404 this.checkAvailability_(function() {
405 // TODO(mtomasz): Pass entries instead.
406 var urls = util.entriesToURLs(entries);
407 var taskId = chrome.runtime.id + '|file|view-in-browser';
408 chrome.fileBrowserPrivate.executeTask(taskId, urls, onViewFiles);
413 * Executes a single task.
415 * @param {string} taskId Task identifier.
416 * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
420 FileTasks.prototype.execute_ = function(taskId, opt_entries) {
421 var entries = opt_entries || this.entries_;
422 FileTasks.recordViewingFileTypeUMA_(entries);
423 this.executeInternal_(taskId, entries);
427 * The core implementation to execute a single task.
429 * @param {string} taskId Task identifier.
430 * @param {Array.<Entry>} entries Entries to execute.
433 FileTasks.prototype.executeInternal_ = function(taskId, entries) {
434 this.checkAvailability_(function() {
435 if (FileTasks.isInternalTask_(taskId)) {
436 var taskParts = taskId.split('|');
437 this.executeInternalTask_(taskParts[2], entries);
439 // TODO(mtomasz): Pass entries instead.
440 var urls = util.entriesToURLs(entries);
441 chrome.fileBrowserPrivate.executeTask(taskId, urls, function(result) {
442 if (result !== 'message_sent')
444 util.isTeleported(window).then(function(teleported) {
446 util.showOpenInOtherDesktopAlert(
447 this.fileManager_.ui.alertDialog, entries);
456 * Checks whether the remote files are available right now.
458 * @param {function} callback The callback.
461 FileTasks.prototype.checkAvailability_ = function(callback) {
462 var areAll = function(props, name) {
463 var isOne = function(e) {
464 // If got no properties, we safely assume that item is unavailable.
467 return props.filter(isOne).length === props.length;
470 var fm = this.fileManager_;
471 var entries = this.entries_;
473 var isDriveOffline = fm.volumeManager.getDriveConnectionState().type ===
474 util.DriveConnectionType.OFFLINE;
476 if (fm.isOnDrive() && isDriveOffline) {
477 fm.metadataCache_.get(entries, 'drive', function(props) {
478 if (areAll(props, 'availableOffline')) {
484 loadTimeData.getString('OFFLINE_HEADER'),
486 loadTimeData.getStringF(
487 entries.length === 1 ?
488 'HOSTED_OFFLINE_MESSAGE' :
489 'HOSTED_OFFLINE_MESSAGE_PLURAL') :
490 loadTimeData.getStringF(
491 entries.length === 1 ?
493 'OFFLINE_MESSAGE_PLURAL',
494 loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
499 var isOnMetered = fm.volumeManager.getDriveConnectionState().type ===
500 util.DriveConnectionType.METERED;
502 if (fm.isOnDrive() && isOnMetered) {
503 fm.metadataCache_.get(entries, 'drive', function(driveProps) {
504 if (areAll(driveProps, 'availableWhenMetered')) {
509 fm.metadataCache_.get(entries, 'filesystem', function(fileProps) {
510 var sizeToDownload = 0;
511 for (var i = 0; i !== entries.length; i++) {
512 if (!driveProps[i].availableWhenMetered)
513 sizeToDownload += fileProps[i].size;
516 loadTimeData.getStringF(
517 entries.length === 1 ?
518 'CONFIRM_MOBILE_DATA_USE' :
519 'CONFIRM_MOBILE_DATA_USE_PLURAL',
520 util.bytesToString(sizeToDownload)),
531 * Executes an internal task.
533 * @param {string} id The short task id.
534 * @param {Array.<Entry>} entries The entries to execute on.
537 FileTasks.prototype.executeInternalTask_ = function(id, entries) {
538 var fm = this.fileManager_;
542 if (entries.length === 1) {
543 // If just a single audio file is selected pass along every audio file
545 var selectedEntries = entries[0];
546 entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio);
547 position = entries.indexOf(selectedEntries);
549 // TODO(mtomasz): Pass entries instead.
550 var urls = util.entriesToURLs(entries);
551 chrome.fileBrowserPrivate.getProfiles(function(profiles,
554 fm.backgroundPage.launchAudioPlayer({items: urls, position: position},
560 if (id === 'mount-archive') {
561 this.mountArchivesInternal_(entries);
565 if (id === 'gallery' || id === 'gallery-video') {
566 this.openGalleryInternal_(entries);
570 console.error('Unexpected action ID: ' + id);
576 * @param {Array.<Entry>} entries Mount file entries list.
578 FileTasks.prototype.mountArchives = function(entries) {
579 FileTasks.recordViewingFileTypeUMA_(entries);
580 this.mountArchivesInternal_(entries);
584 * The core implementation of mounts archives.
586 * @param {Array.<Entry>} entries Mount file entries list.
589 FileTasks.prototype.mountArchivesInternal_ = function(entries) {
590 var fm = this.fileManager_;
592 var tracker = fm.directoryModel.createDirectoryChangeTracker();
595 // TODO(mtomasz): Pass Entries instead of URLs.
596 var urls = util.entriesToURLs(entries);
597 fm.resolveSelectResults_(urls, function(resolvedURLs) {
598 for (var index = 0; index < resolvedURLs.length; ++index) {
599 // TODO(mtomasz): Pass Entry instead of URL.
600 fm.volumeManager.mountArchive(resolvedURLs[index],
601 function(volumeInfo) {
602 if (tracker.hasChanged) {
606 volumeInfo.resolveDisplayRoot(function(displayRoot) {
607 if (tracker.hasChanged) {
611 fm.directoryModel.changeDirectoryEntry(displayRoot);
613 console.warn('Failed to resolve the display root after mounting.');
616 }, function(url, error) {
618 var path = util.extractFilePath(url);
619 var namePos = path.lastIndexOf('/');
620 fm.alert.show(strf('ARCHIVE_MOUNT_FAILED',
621 path.substr(namePos + 1), error));
622 }.bind(null, resolvedURLs[index]));
630 * @param {Array.<Entry>} entries List of selected entries.
632 FileTasks.prototype.openGallery = function(entries) {
633 FileTasks.recordViewingFileTypeUMA_(entries);
634 this.openGalleryInternal_(entries);
638 * The core implementation to open the Gallery.
640 * @param {Array.<Entry>} entries List of selected entries.
643 FileTasks.prototype.openGalleryInternal_ = function(entries) {
644 var fm = this.fileManager_;
647 fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo);
649 var galleryFrame = fm.document_.createElement('iframe');
650 galleryFrame.className = 'overlay-pane';
651 galleryFrame.scrolling = 'no';
652 galleryFrame.setAttribute('webkitallowfullscreen', true);
654 if (this.params_ && this.params_.gallery) {
655 // Remove the Gallery state from the location, we do not need it any more.
656 // TODO(mtomasz): Consider keeping the selection path.
658 null, /* keep current directory */
659 '', /* remove current selection */
660 '' /* remove search. */);
663 var savedAppState = JSON.parse(JSON.stringify(window.appState));
664 var savedTitle = document.title;
666 // Push a temporary state which will be replaced every time the selection
667 // changes in the Gallery and popped when the Gallery is closed.
668 util.updateAppState();
670 var onBack = function(selectedEntries) {
671 fm.directoryModel.selectEntries(selectedEntries);
672 fm.closeFilePopup(); // Will call Gallery.unload.
673 window.appState = savedAppState;
675 document.title = savedTitle;
678 var onAppRegionChanged = function(visible) {
679 fm.onFilePopupAppRegionChanged(visible);
682 galleryFrame.onload = function() {
683 galleryFrame.contentWindow.ImageUtil.metrics = metrics;
685 // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the
686 // root. We should check more granular permission to know whether the file
687 // is writable or not.
688 var readonly = fm.isOnReadonlyDirectory();
689 var currentDir = fm.getCurrentDirectoryEntry();
690 var downloadsVolume =
691 fm.volumeManager.getCurrentProfileVolumeInfo(RootType.DOWNLOADS);
692 var downloadsDir = downloadsVolume && downloadsVolume.fileSystem.root;
693 var readonlyDirName = null;
694 if (readonly && currentDir) {
695 // TODO(mtomasz): Pass Entry instead of localized name. Conversion to a
696 // display string should be done in gallery.js.
697 var locationInfo = fm.volumeManager.getLocationInfo(currentDir);
698 if (locationInfo && locationInfo.isRootEntry) {
699 readonlyDirName = PathUtil.getRootTypeLabel(currentDir.rootType) ||
702 readonlyDirName = currentDir.name;
707 // We show the root label in readonly warning (e.g. archive name).
708 readonlyDirName: readonlyDirName,
709 curDirEntry: currentDir,
710 saveDirEntry: readonly ? downloadsDir : null,
711 searchResults: fm.directoryModel.isSearching(),
712 metadataCache: fm.metadataCache_,
713 pageState: this.params_,
714 appWindow: chrome.app.window.current(),
716 onClose: fm.onClose.bind(fm),
717 onMaximize: fm.onMaximize.bind(fm),
718 onMinimize: fm.onMinimize.bind(fm),
719 onAppRegionChanged: onAppRegionChanged,
720 loadTimeData: fm.backgroundPage.background.stringData
722 galleryFrame.contentWindow.Gallery.open(
723 context, fm.volumeManager, allEntries, entries);
726 galleryFrame.src = 'gallery.html';
727 fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm));
731 * Displays the list of tasks in a task picker combobutton.
733 * @param {cr.ui.ComboButton} combobutton The task picker element.
736 FileTasks.prototype.display_ = function(combobutton) {
737 if (this.tasks_.length === 0) {
738 combobutton.hidden = true;
743 combobutton.hidden = false;
744 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
746 var items = this.createItems_();
748 if (items.length > 1) {
751 for (var j = 0; j < items.length; j++) {
752 combobutton.addDropDownItem(items[j]);
753 if (items[j].task.taskId === this.defaultTask_.taskId)
757 combobutton.addSeparator();
758 var changeDefaultMenuItem = combobutton.addDropDownItem({
759 label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
761 changeDefaultMenuItem.classList.add('change-default');
766 * Creates sorted array of available task descriptions such as title and icon.
768 * @return {Array} created array can be used to feed combobox, menus and so on.
771 FileTasks.prototype.createItems_ = function() {
773 var title = this.defaultTask_.title + ' ' +
774 loadTimeData.getString('DEFAULT_ACTION_LABEL');
775 items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
777 for (var index = 0; index < this.tasks_.length; index++) {
778 var task = this.tasks_[index];
779 if (task !== this.defaultTask_)
780 items.push(this.createCombobuttonItem_(task));
783 items.sort(function(a, b) {
784 return a.label.localeCompare(b.label);
791 * Updates context menu with default item.
795 FileTasks.prototype.updateMenuItem_ = function() {
796 this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
797 this.tasks_.length > 1);
801 * Creates combobutton item based on task.
803 * @param {Object} task Task to convert.
804 * @param {string=} opt_title Title.
805 * @param {boolean=} opt_bold Make a menu item bold.
806 * @return {Object} Item appendable to combobutton drop-down list.
809 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
812 label: opt_title || task.title,
813 iconUrl: task.iconUrl,
814 iconType: task.iconType,
816 bold: opt_bold || false
821 * Shows modal action picker dialog with currently available list of tasks.
823 * @param {DefaultActionDialog} actionDialog Action dialog to show and update.
824 * @param {string} title Title to use.
825 * @param {string} message Message to use.
826 * @param {function(Object)} onSuccess Callback to pass selected task.
828 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
830 var items = this.createItems_();
833 for (var j = 0; j < items.length; j++) {
834 if (items[j].task.taskId === this.defaultTask_.taskId)
843 onSuccess(item.task);
848 * Decorates a FileTasks method, so it will be actually executed after the tasks
850 * This decorator expects an implementation called |method + '_'|.
852 * @param {string} method The method name.
854 FileTasks.decorate = function(method) {
855 var privateMethod = method + '_';
856 FileTasks.prototype[method] = function() {
858 this[privateMethod].apply(this, arguments);
860 this.pendingInvocations_.push([privateMethod, arguments]);
866 FileTasks.decorate('display');
867 FileTasks.decorate('updateMenuItem');
868 FileTasks.decorate('execute');
869 FileTasks.decorate('executeDefault');