Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / file_tasks.js
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.
4
5 'use strict';
6
7 /**
8  * This object encapsulates everything related to tasks execution.
9  *
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.
13  * @constructor
14  */
15 function FileTasks(fileManager, opt_params) {
16   this.fileManager_ = fileManager;
17   this.params_ = opt_params;
18   this.tasks_ = null;
19   this.defaultTask_ = null;
20   this.entries_ = null;
21
22   /**
23    * List of invocations to be called once tasks are available.
24    *
25    * @private
26    * @type {Array.<Object>}
27    */
28   this.pendingInvocations_ = [];
29 }
30
31 /**
32  * Location of the Chrome Web Store.
33  *
34  * @const
35  * @type {string}
36  */
37 FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore';
38
39 /**
40  * Base URL of apps list in the Chrome Web Store. This constant is used in
41  * FileTasks.createWebStoreLink().
42  *
43  * @const
44  * @type {string}
45  */
46 FileTasks.WEB_STORE_HANDLER_BASE_URL =
47     'https://chrome.google.com/webstore/category/collection/file_handlers';
48
49
50 /**
51  * The app ID of the video player app.
52  * @const
53  * @type {string}
54  */
55 FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko';
56
57 /**
58  * Returns URL of the Chrome Web Store which show apps supporting the given
59  * file-extension and mime-type.
60  *
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
64  */
65 FileTasks.createWebStoreLink = function(extension, mimeType) {
66   if (!extension || FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1)
67     return FileTasks.CHROME_WEB_STORE_URL;
68
69   if (extension[0] === '.')
70     extension = extension.substr(1);
71   else
72     console.warn('Please pass an extension with a dot to createWebStoreLink.');
73
74   var url = FileTasks.WEB_STORE_HANDLER_BASE_URL;
75   url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, '');
76
77   // If a mime is given, add it into the URL.
78   if (mimeType)
79     url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, '');
80   return url;
81 };
82
83 /**
84  * Complete the initialization.
85  *
86  * @param {Array.<Entry>} entries List of file entries.
87  * @param {Array.<string>=} opt_mimeTypes Mime-type specified for each entries.
88  */
89 FileTasks.prototype.init = function(entries, opt_mimeTypes) {
90   this.entries_ = entries;
91   this.mimeTypes_ = opt_mimeTypes || [];
92
93   // TODO(mtomasz): Move conversion from entry to url to custom bindings.
94   // crbug.com/345527.
95   var urls = util.entriesToURLs(entries);
96   if (urls.length > 0)
97     chrome.fileManagerPrivate.getFileTasks(urls, this.onTasks_.bind(this));
98 };
99
100 /**
101  * Returns amount of tasks.
102  *
103  * @return {number} amount of tasks.
104  */
105 FileTasks.prototype.size = function() {
106   return (this.tasks_ && this.tasks_.length) || 0;
107 };
108
109 /**
110  * Callback when tasks found.
111  *
112  * @param {Array.<Object>} tasks The tasks.
113  * @private
114  */
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);
121   }
122   this.pendingInvocations_ = [];
123 };
124
125 /**
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
128  * inserted.
129  *
130  * @const
131  * @type {Array.<string>}
132  * @private
133  */
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',
142 ]);
143
144 /**
145  * The list of executable file extensions.
146  *
147  * @const
148  * @type {Array.<string>}
149  */
150 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
151   '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
152 ]);
153
154 /**
155  * The list of extensions to skip the suggest app dialog.
156  * @const
157  * @type {Array.<string>}
158  * @private
159  */
160 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
161   '.crdownload', '.dsc', '.inf', '.crx',
162 ]);
163
164 /**
165  * Records trial of opening file grouped by extensions.
166  *
167  * @param {Array.<Entry>} entries The entries to be opened.
168  * @private
169  */
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) {
175       extension = 'other';
176     }
177     metrics.recordEnum(
178         'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_);
179   }
180 };
181
182 /**
183  * Returns true if the taskId is for an internal task.
184  *
185  * @param {string} taskId Task identifier.
186  * @return {boolean} True if the task ID is for an internal task.
187  * @private
188  */
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'));
199 };
200
201 /**
202  * Processes internal tasks.
203  *
204  * @param {Array.<Object>} tasks The tasks.
205  * @private
206  */
207 FileTasks.prototype.processTasks_ = function(tasks) {
208   this.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) {
215       isOnDrive = true;
216       break;
217     }
218   }
219
220   for (var i = 0; i < tasks.length; i++) {
221     var task = tasks[i];
222     var taskParts = task.taskId.split('|');
223
224     // Skip internal Files.app's handlers.
225     if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
226         taskParts[2] === 'select' || taskParts[2] === 'open')) {
227       continue;
228     }
229
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'))
258           continue;
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'))
264           continue;
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');
270       }
271     }
272
273     if (!task.iconType && taskParts[1] === 'web-intent') {
274       task.iconType = 'generic';
275     }
276
277     this.tasks_.push(task);
278     if (this.defaultTask_ === null && task.isDefault) {
279       this.defaultTask_ = task;
280     }
281   }
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];
287   }
288 };
289
290 /**
291  * Executes default task.
292  *
293  * @param {function(boolean, Array.<string>)=} opt_callback Called when the
294  *     default task is executed, or the error is occurred.
295  * @private
296  */
297 FileTasks.prototype.executeDefault_ = function(opt_callback) {
298   FileTasks.recordViewingFileTypeUMA_(this.entries_);
299   this.executeDefaultInternal_(this.entries_, opt_callback);
300 };
301
302 /**
303  * Executes default task.
304  *
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.
308  * @private
309  */
310 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
311   var callback = opt_callback || function(arg1, arg2) {};
312
313   if (this.defaultTask_ !== null) {
314     this.executeInternal_(this.defaultTask_.taskId, entries);
315     callback(true, entries);
316     return;
317   }
318
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])
322     return;
323
324   var filename = entries[0].name;
325   var extension = util.splitExtension(filename)[1];
326   var mimeType = this.mimeTypes_[0];
327
328   var showAlert = function() {
329     var textMessageId;
330     var titleMessageId;
331     switch (extension) {
332       case '.exe':
333       case '.msi':
334         textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
335         break;
336       case '.dmg':
337         textMessageId = 'NO_ACTION_FOR_DMG';
338         break;
339       case '.crx':
340         textMessageId = 'NO_ACTION_FOR_CRX';
341         titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
342         break;
343       default:
344         textMessageId = 'NO_ACTION_FOR_FILE';
345     }
346
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);
352   }.bind(this);
353
354   var onViewFilesFailure = function() {
355     var fm = this.fileManager_;
356     if (!fm.isOnDrive() ||
357         !entries[0] ||
358         FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
359       showAlert();
360       return;
361     }
362
363     fm.openSuggestAppsDialog(
364         entries[0],
365         function() {
366           var newTasks = new FileTasks(fm);
367           newTasks.init(entries, this.mimeTypes_);
368           newTasks.executeDefault();
369           callback(true, entries);
370         }.bind(this),
371         // Cancelled callback.
372         function() {
373           callback(false, entries);
374         },
375         showAlert);
376   }.bind(this);
377
378   var onViewFiles = function(result) {
379     switch (result) {
380       case 'opened':
381         callback(success, entries);
382         break;
383       case 'message_sent':
384         util.isTeleported(window).then(function(teleported) {
385           if (teleported) {
386             util.showOpenInOtherDesktopAlert(
387                 this.fileManager_.ui.alertDialog, entries);
388           }
389         }.bind(this));
390         callback(success, entries);
391         break;
392       case 'empty':
393         callback(success, entries);
394         break;
395       case 'failed':
396         onViewFilesFailure();
397         break;
398     }
399   }.bind(this);
400
401   this.checkAvailability_(function() {
402     // TODO(mtomasz): Move conversion from entry to url to custom bindings.
403     // crbug.com/345527.
404     var urls = util.entriesToURLs(entries);
405     var taskId = chrome.runtime.id + '|file|view-in-browser';
406     chrome.fileManagerPrivate.executeTask(taskId, urls, onViewFiles);
407   }.bind(this));
408 };
409
410 /**
411  * Executes a single task.
412  *
413  * @param {string} taskId Task identifier.
414  * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
415  *     this.entries_|.
416  * @private
417  */
418 FileTasks.prototype.execute_ = function(taskId, opt_entries) {
419   var entries = opt_entries || this.entries_;
420   FileTasks.recordViewingFileTypeUMA_(entries);
421   this.executeInternal_(taskId, entries);
422 };
423
424 /**
425  * The core implementation to execute a single task.
426  *
427  * @param {string} taskId Task identifier.
428  * @param {Array.<Entry>} entries Entries to execute.
429  * @private
430  */
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);
436     } else {
437       // TODO(mtomasz): Move conversion from entry to url to custom bindings.
438       // crbug.com/345527.
439       var urls = util.entriesToURLs(entries);
440       chrome.fileManagerPrivate.executeTask(taskId, urls, function(result) {
441         if (result !== 'message_sent')
442           return;
443         util.isTeleported(window).then(function(teleported) {
444           if (teleported) {
445             util.showOpenInOtherDesktopAlert(
446                 this.fileManager_.ui.alertDialog, entries);
447           }
448         }.bind(this));
449       }.bind(this));
450     }
451   }.bind(this));
452 };
453
454 /**
455  * Checks whether the remote files are available right now.
456  *
457  * @param {function} callback The callback.
458  * @private
459  */
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.
464       return e && e[name];
465     };
466     return props.filter(isOne).length === props.length;
467   };
468
469   var fm = this.fileManager_;
470   var entries = this.entries_;
471
472   var isDriveOffline = fm.volumeManager.getDriveConnectionState().type ===
473       VolumeManagerCommon.DriveConnectionType.OFFLINE;
474
475   if (fm.isOnDrive() && isDriveOffline) {
476     fm.metadataCache_.get(entries, 'external', function(props) {
477       if (areAll(props, 'availableOffline')) {
478         callback();
479         return;
480       }
481
482       fm.alert.showHtml(
483           loadTimeData.getString('OFFLINE_HEADER'),
484           props[0].hosted ?
485               loadTimeData.getStringF(
486                   entries.length === 1 ?
487                       'HOSTED_OFFLINE_MESSAGE' :
488                       'HOSTED_OFFLINE_MESSAGE_PLURAL') :
489               loadTimeData.getStringF(
490                   entries.length === 1 ?
491                       'OFFLINE_MESSAGE' :
492                       'OFFLINE_MESSAGE_PLURAL',
493                   loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
494     });
495     return;
496   }
497
498   var isOnMetered = fm.volumeManager.getDriveConnectionState().type ===
499       VolumeManagerCommon.DriveConnectionType.METERED;
500
501   if (fm.isOnDrive() && isOnMetered) {
502     fm.metadataCache_.get(entries, 'external', function(driveProps) {
503       if (areAll(driveProps, 'availableWhenMetered')) {
504         callback();
505         return;
506       }
507
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;
513         }
514         fm.confirm.show(
515             loadTimeData.getStringF(
516                 entries.length === 1 ?
517                     'CONFIRM_MOBILE_DATA_USE' :
518                     'CONFIRM_MOBILE_DATA_USE_PLURAL',
519                 util.bytesToString(sizeToDownload)),
520             callback);
521       });
522     });
523     return;
524   }
525
526   callback();
527 };
528
529 /**
530  * Executes an internal task.
531  *
532  * @param {string} id The short task id.
533  * @param {Array.<Entry>} entries The entries to execute on.
534  * @private
535  */
536 FileTasks.prototype.executeInternalTask_ = function(id, entries) {
537   var fm = this.fileManager_;
538
539   if (id === 'play') {
540     var selectedEntry = entries[0];
541     if (entries.length === 1) {
542       // If just a single audio file is selected pass along every audio file
543       // in the directory.
544       entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio);
545     }
546     // TODO(mtomasz): Move conversion from entry to url to custom bindings.
547     // crbug.com/345527.
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);
554         });
555     return;
556   }
557
558   if (id === 'mount-archive') {
559     this.mountArchivesInternal_(entries);
560     return;
561   }
562
563   console.error('Unexpected action ID: ' + id);
564 };
565
566 /**
567  * Mounts archives.
568  *
569  * @param {Array.<Entry>} entries Mount file entries list.
570  */
571 FileTasks.prototype.mountArchives = function(entries) {
572   FileTasks.recordViewingFileTypeUMA_(entries);
573   this.mountArchivesInternal_(entries);
574 };
575
576 /**
577  * The core implementation of mounts archives.
578  *
579  * @param {Array.<Entry>} entries Mount file entries list.
580  * @private
581  */
582 FileTasks.prototype.mountArchivesInternal_ = function(entries) {
583   var fm = this.fileManager_;
584
585   var tracker = fm.directoryModel.createDirectoryChangeTracker();
586   tracker.start();
587
588   // TODO(mtomasz): Move conversion from entry to url to custom bindings.
589   // crbug.com/345527.
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(
594         urls[index],
595         function(volumeInfo) {
596           if (tracker.hasChanged) {
597             tracker.stop();
598             return;
599           }
600           volumeInfo.resolveDisplayRoot(function(displayRoot) {
601             if (tracker.hasChanged) {
602               tracker.stop();
603               return;
604             }
605             fm.directoryModel.changeDirectoryEntry(displayRoot);
606           }, function() {
607             console.warn('Failed to resolve the display root after mounting.');
608             tracker.stop();
609           });
610         }, function(url, error) {
611           tracker.stop();
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]));
617   }
618 };
619
620 /**
621  * Displays the list of tasks in a task picker combobutton.
622  *
623  * @param {cr.ui.ComboButton} combobutton The task picker element.
624  * @private
625  */
626 FileTasks.prototype.display_ = function(combobutton) {
627   if (this.tasks_.length === 0) {
628     combobutton.hidden = true;
629     return;
630   }
631
632   combobutton.clear();
633   combobutton.hidden = false;
634   combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
635
636   var items = this.createItems_();
637
638   if (items.length > 1) {
639     var defaultIdx = 0;
640
641     for (var j = 0; j < items.length; j++) {
642       combobutton.addDropDownItem(items[j]);
643       if (items[j].task.taskId === this.defaultTask_.taskId)
644         defaultIdx = j;
645     }
646
647     combobutton.addSeparator();
648     var changeDefaultMenuItem = combobutton.addDropDownItem({
649       label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
650     });
651     changeDefaultMenuItem.classList.add('change-default');
652   }
653 };
654
655 /**
656  * Creates sorted array of available task descriptions such as title and icon.
657  *
658  * @return {Array} created array can be used to feed combobox, menus and so on.
659  * @private
660  */
661 FileTasks.prototype.createItems_ = function() {
662   var items = [];
663   var title = this.defaultTask_.title + ' ' +
664               loadTimeData.getString('DEFAULT_ACTION_LABEL');
665   items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
666
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));
671   }
672
673   items.sort(function(a, b) {
674     return a.label.localeCompare(b.label);
675   });
676
677   return items;
678 };
679
680 /**
681  * Updates context menu with default item.
682  * @private
683  */
684
685 FileTasks.prototype.updateMenuItem_ = function() {
686   this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
687       this.tasks_.length > 1);
688 };
689
690 /**
691  * Creates combobutton item based on task.
692  *
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.
697  * @private
698  */
699 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
700                                                       opt_bold) {
701   return {
702     label: opt_title || task.title,
703     iconUrl: task.iconUrl,
704     iconType: task.iconType,
705     task: task,
706     bold: opt_bold || false
707   };
708 };
709
710 /**
711  * Shows modal action picker dialog with currently available list of tasks.
712  *
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.
717  */
718 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
719                                               onSuccess) {
720   var items = this.createItems_();
721
722   var defaultIdx = 0;
723   for (var j = 0; j < items.length; j++) {
724     if (items[j].task.taskId === this.defaultTask_.taskId)
725       defaultIdx = j;
726   }
727
728   actionDialog.show(
729       title,
730       message,
731       items, defaultIdx,
732       function(item) {
733         onSuccess(item.task);
734       });
735 };
736
737 /**
738  * Decorates a FileTasks method, so it will be actually executed after the tasks
739  * are available.
740  * This decorator expects an implementation called |method + '_'|.
741  *
742  * @param {string} method The method name.
743  */
744 FileTasks.decorate = function(method) {
745   var privateMethod = method + '_';
746   FileTasks.prototype[method] = function() {
747     if (this.tasks_) {
748       this[privateMethod].apply(this, arguments);
749     } else {
750       this.pendingInvocations_.push([privateMethod, arguments]);
751     }
752     return this;
753   };
754 };
755
756 FileTasks.decorate('display');
757 FileTasks.decorate('updateMenuItem');
758 FileTasks.decorate('execute');
759 FileTasks.decorate('executeDefault');