Upstream version 11.40.277.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 /**
6  * This object encapsulates everything related to tasks execution.
7  *
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.
11  * @constructor
12  */
13 function FileTasks(fileManager, opt_params) {
14   this.fileManager_ = fileManager;
15   this.params_ = opt_params;
16   this.tasks_ = null;
17   this.defaultTask_ = null;
18   this.entries_ = null;
19
20   /**
21    * List of invocations to be called once tasks are available.
22    *
23    * @private
24    * @type {Array.<Object>}
25    */
26   this.pendingInvocations_ = [];
27 }
28
29 /**
30  * Location of the Chrome Web Store.
31  *
32  * @const
33  * @type {string}
34  */
35 FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore';
36
37 /**
38  * Base URL of apps list in the Chrome Web Store. This constant is used in
39  * FileTasks.createWebStoreLink().
40  *
41  * @const
42  * @type {string}
43  */
44 FileTasks.WEB_STORE_HANDLER_BASE_URL =
45     'https://chrome.google.com/webstore/category/collection/file_handlers';
46
47
48 /**
49  * The app ID of the video player app.
50  * @const
51  * @type {string}
52  */
53 FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko';
54
55 /**
56  * The task id of the zip unpacker app.
57  * @const
58  * @type {string}
59  */
60 FileTasks.ZIP_UNPACKER_TASK_ID = 'oedeeodfidgoollimchfdnbmhcpnklnd|app|zip';
61
62 /**
63  * Returns URL of the Chrome Web Store which show apps supporting the given
64  * file-extension and mime-type.
65  *
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
69  */
70 FileTasks.createWebStoreLink = function(extension, mimeType) {
71   if (!extension || FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1)
72     return FileTasks.CHROME_WEB_STORE_URL;
73
74   if (extension[0] === '.')
75     extension = extension.substr(1);
76   else
77     console.warn('Please pass an extension with a dot to createWebStoreLink.');
78
79   var url = FileTasks.WEB_STORE_HANDLER_BASE_URL;
80   url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, '');
81
82   // If a mime is given, add it into the URL.
83   if (mimeType)
84     url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, '');
85   return url;
86 };
87
88 /**
89  * Complete the initialization.
90  *
91  * @param {Array.<Entry>} entries List of file entries.
92  * @param {Array.<string>=} opt_mimeTypes Mime-type specified for each entries.
93  */
94 FileTasks.prototype.init = function(entries, opt_mimeTypes) {
95   this.entries_ = entries;
96   this.mimeTypes_ = opt_mimeTypes || [];
97
98   // TODO(mtomasz): Move conversion from entry to url to custom bindings.
99   // crbug.com/345527.
100   var urls = util.entriesToURLs(entries);
101   if (urls.length > 0)
102     chrome.fileManagerPrivate.getFileTasks(urls, this.onTasks_.bind(this));
103 };
104
105 /**
106  * Returns amount of tasks.
107  *
108  * @return {number} amount of tasks.
109  */
110 FileTasks.prototype.size = function() {
111   return (this.tasks_ && this.tasks_.length) || 0;
112 };
113
114 /**
115  * Callback when tasks found.
116  *
117  * @param {Array.<Object>} tasks The tasks.
118  * @private
119  */
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);
126   }
127   this.pendingInvocations_ = [];
128 };
129
130 /**
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
133  * inserted.
134  *
135  * @const
136  * @type {Array.<string>}
137  * @private
138  */
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',
147 ]);
148
149 /**
150  * The list of executable file extensions.
151  *
152  * @const
153  * @type {Array.<string>}
154  */
155 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
156   '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
157 ]);
158
159 /**
160  * The list of extensions to skip the suggest app dialog.
161  * @const
162  * @type {Array.<string>}
163  * @private
164  */
165 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
166   '.crdownload', '.dsc', '.inf', '.crx',
167 ]);
168
169 /**
170  * Records trial of opening file grouped by extensions.
171  *
172  * @param {Array.<Entry>} entries The entries to be opened.
173  * @private
174  */
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) {
180       extension = 'other';
181     }
182     metrics.recordEnum(
183         'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_);
184   }
185 };
186
187 /**
188  * Returns true if the taskId is for an internal task.
189  *
190  * @param {string} taskId Task identifier.
191  * @return {boolean} True if the task ID is for an internal task.
192  * @private
193  */
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'));
204 };
205
206 /**
207  * Processes internal tasks.
208  *
209  * @param {Array.<Object>} tasks The tasks.
210  * @private
211  */
212 FileTasks.prototype.processTasks_ = function(tasks) {
213   this.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) {
220       isOnDrive = true;
221       break;
222     }
223   }
224
225   for (var i = 0; i < tasks.length; i++) {
226     var task = tasks[i];
227     var taskParts = task.taskId.split('|');
228
229     // Skip internal Files.app's handlers.
230     if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
231         taskParts[2] === 'select' || taskParts[2] === 'open')) {
232       continue;
233     }
234
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'))
263           continue;
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'))
269           continue;
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');
275       }
276     }
277
278     if (!task.iconType && taskParts[1] === 'web-intent') {
279       task.iconType = 'generic';
280     }
281
282     this.tasks_.push(task);
283     if (this.defaultTask_ === null && task.isDefault) {
284       this.defaultTask_ = task;
285     }
286   }
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;
294         break;
295       }
296     }
297   }
298 };
299
300 /**
301  * Executes default task.
302  *
303  * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the
304  *     default task is executed, or the error is occurred.
305  * @private
306  */
307 FileTasks.prototype.executeDefault_ = function(opt_callback) {
308   FileTasks.recordViewingFileTypeUMA_(this.entries_);
309   this.executeDefaultInternal_(this.entries_, opt_callback);
310 };
311
312 /**
313  * Executes default task.
314  *
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.
318  * @private
319  */
320 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
321   var callback = opt_callback || function(arg1, arg2) {};
322
323   if (this.defaultTask_ !== null) {
324     this.executeInternal_(this.defaultTask_.taskId, entries);
325     callback(true, entries);
326     return;
327   }
328
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])
332     return;
333
334   var filename = entries[0].name;
335   var extension = util.splitExtension(filename)[1];
336   var mimeType = this.mimeTypes_[0];
337
338   var showAlert = function() {
339     var textMessageId;
340     var titleMessageId;
341     switch (extension) {
342       case '.exe':
343       case '.msi':
344         textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
345         break;
346       case '.dmg':
347         textMessageId = 'NO_ACTION_FOR_DMG';
348         break;
349       case '.crx':
350         textMessageId = 'NO_ACTION_FOR_CRX';
351         titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
352         break;
353       default:
354         textMessageId = 'NO_ACTION_FOR_FILE';
355     }
356
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);
362   }.bind(this);
363
364   var onViewFilesFailure = function() {
365     var fm = this.fileManager_;
366     if (!fm.isOnDrive() ||
367         !entries[0] ||
368         FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
369       showAlert();
370       return;
371     }
372
373     fm.openSuggestAppsDialog(
374         entries[0],
375         function() {
376           var newTasks = new FileTasks(fm);
377           newTasks.init(entries, this.mimeTypes_);
378           newTasks.executeDefault();
379           callback(true, entries);
380         }.bind(this),
381         // Cancelled callback.
382         function() {
383           callback(false, entries);
384         },
385         showAlert);
386   }.bind(this);
387
388   var onViewFiles = function(result) {
389     switch (result) {
390       case 'opened':
391         callback(true, entries);
392         break;
393       case 'message_sent':
394         util.isTeleported(window).then(function(teleported) {
395           if (teleported) {
396             util.showOpenInOtherDesktopAlert(
397                 this.fileManager_.ui.alertDialog, entries);
398           }
399         }.bind(this));
400         callback(true, entries);
401         break;
402       case 'empty':
403         callback(true, entries);
404         break;
405       case 'failed':
406         onViewFilesFailure();
407         break;
408     }
409   }.bind(this);
410
411   this.checkAvailability_(function() {
412     // TODO(mtomasz): Move conversion from entry to url to custom bindings.
413     // crbug.com/345527.
414     var urls = util.entriesToURLs(entries);
415     var taskId = chrome.runtime.id + '|file|view-in-browser';
416     chrome.fileManagerPrivate.executeTask(taskId, urls, onViewFiles);
417   }.bind(this));
418 };
419
420 /**
421  * Executes a single task.
422  *
423  * @param {string} taskId Task identifier.
424  * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
425  *     this.entries_|.
426  * @private
427  */
428 FileTasks.prototype.execute_ = function(taskId, opt_entries) {
429   var entries = opt_entries || this.entries_;
430   FileTasks.recordViewingFileTypeUMA_(entries);
431   this.executeInternal_(taskId, entries);
432 };
433
434 /**
435  * The core implementation to execute a single task.
436  *
437  * @param {string} taskId Task identifier.
438  * @param {Array.<Entry>} entries Entries to execute.
439  * @private
440  */
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);
446     } else {
447       // TODO(mtomasz): Move conversion from entry to url to custom bindings.
448       // crbug.com/345527.
449       var urls = util.entriesToURLs(entries);
450       chrome.fileManagerPrivate.executeTask(taskId, urls, function(result) {
451         if (result !== 'message_sent')
452           return;
453         util.isTeleported(window).then(function(teleported) {
454           if (teleported) {
455             util.showOpenInOtherDesktopAlert(
456                 this.fileManager_.ui.alertDialog, entries);
457           }
458         }.bind(this));
459       }.bind(this));
460     }
461   }.bind(this));
462 };
463
464 /**
465  * Checks whether the remote files are available right now.
466  *
467  * @param {function()} callback The callback.
468  * @private
469  */
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.
474       return e && e[name];
475     };
476     return props.filter(isOne).length === props.length;
477   };
478
479   var fm = this.fileManager_;
480   var entries = this.entries_;
481
482   var isDriveOffline = fm.volumeManager.getDriveConnectionState().type ===
483       VolumeManagerCommon.DriveConnectionType.OFFLINE;
484
485   if (fm.isOnDrive() && isDriveOffline) {
486     fm.metadataCache_.get(entries, 'external', function(props) {
487       if (areAll(props, 'availableOffline')) {
488         callback();
489         return;
490       }
491
492       fm.alert.showHtml(
493           loadTimeData.getString('OFFLINE_HEADER'),
494           props[0].hosted ?
495               loadTimeData.getStringF(
496                   entries.length === 1 ?
497                       'HOSTED_OFFLINE_MESSAGE' :
498                       'HOSTED_OFFLINE_MESSAGE_PLURAL') :
499               loadTimeData.getStringF(
500                   entries.length === 1 ?
501                       'OFFLINE_MESSAGE' :
502                       'OFFLINE_MESSAGE_PLURAL',
503                   loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
504     });
505     return;
506   }
507
508   var isOnMetered = fm.volumeManager.getDriveConnectionState().type ===
509       VolumeManagerCommon.DriveConnectionType.METERED;
510
511   if (fm.isOnDrive() && isOnMetered) {
512     fm.metadataCache_.get(entries, 'external', function(driveProps) {
513       if (areAll(driveProps, 'availableWhenMetered')) {
514         callback();
515         return;
516       }
517
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;
523         }
524         fm.confirm.show(
525             loadTimeData.getStringF(
526                 entries.length === 1 ?
527                     'CONFIRM_MOBILE_DATA_USE' :
528                     'CONFIRM_MOBILE_DATA_USE_PLURAL',
529                 util.bytesToString(sizeToDownload)),
530             callback);
531       });
532     });
533     return;
534   }
535
536   callback();
537 };
538
539 /**
540  * Executes an internal task.
541  *
542  * @param {string} id The short task id.
543  * @param {Array.<Entry>} entries The entries to execute on.
544  * @private
545  */
546 FileTasks.prototype.executeInternalTask_ = function(id, entries) {
547   var fm = this.fileManager_;
548
549   if (id === 'mount-archive') {
550     this.mountArchivesInternal_(entries);
551     return;
552   }
553
554   console.error('Unexpected action ID: ' + id);
555 };
556
557 /**
558  * Mounts archives.
559  *
560  * @param {Array.<Entry>} entries Mount file entries list.
561  */
562 FileTasks.prototype.mountArchives = function(entries) {
563   FileTasks.recordViewingFileTypeUMA_(entries);
564   this.mountArchivesInternal_(entries);
565 };
566
567 /**
568  * The core implementation of mounts archives.
569  *
570  * @param {Array.<Entry>} entries Mount file entries list.
571  * @private
572  */
573 FileTasks.prototype.mountArchivesInternal_ = function(entries) {
574   var fm = this.fileManager_;
575
576   var tracker = fm.directoryModel.createDirectoryChangeTracker();
577   tracker.start();
578
579   // TODO(mtomasz): Move conversion from entry to url to custom bindings.
580   // crbug.com/345527.
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(
585         urls[index],
586         function(volumeInfo) {
587           if (tracker.hasChanged) {
588             tracker.stop();
589             return;
590           }
591           volumeInfo.resolveDisplayRoot(function(displayRoot) {
592             if (tracker.hasChanged) {
593               tracker.stop();
594               return;
595             }
596             fm.directoryModel.changeDirectoryEntry(displayRoot);
597           }, function() {
598             console.warn('Failed to resolve the display root after mounting.');
599             tracker.stop();
600           });
601         }, function(url, error) {
602           tracker.stop();
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]));
608   }
609 };
610
611 /**
612  * Displays the list of tasks in a task picker combobutton.
613  *
614  * @param {cr.ui.ComboButton} combobutton The task picker element.
615  * @private
616  */
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;
621     return;
622   }
623
624   combobutton.clear();
625   combobutton.hidden = false;
626
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_,
631           str('ACTION_OPEN'));
632     } else {
633       combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
634     }
635   } else {
636     combobutton.defaultItem = {
637       label: loadTimeData.getString('MORE_ACTIONS')
638     };
639   }
640
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_();
645
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]);
649     }
650
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')
657       });
658       changeDefaultMenuItem.classList.add('change-default');
659     }
660   }
661 };
662
663 /**
664  * Creates sorted array of available task descriptions such as title and icon.
665  *
666  * @return {Array} created array can be used to feed combobox, menus and so on.
667  * @private
668  */
669 FileTasks.prototype.createItems_ = function() {
670   var items = [];
671
672   // Create items.
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));
679     } else {
680       items.push(this.createCombobuttonItem_(task));
681     }
682   }
683
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);
688     if (isDefault !== 0)
689       return isDefault;
690
691     // Sort by isGenericFileHandler.
692     var isGenericFileHandler =
693         (a.isGenericFileHandler ? 1 : 0) - (b.isGenericFileHandler ? 1 : 0);
694     if (isGenericFileHandler !== 0)
695       return isGenericFileHandler;
696
697     // Sort by label.
698     return a.label.localeCompare(b.label);
699   });
700
701   return items;
702 };
703
704 /**
705  * Updates context menu with default item.
706  * @private
707  */
708
709 FileTasks.prototype.updateMenuItem_ = function() {
710   this.fileManager_.updateContextMenuActionItems(this.tasks_);
711 };
712
713 /**
714  * Creates combobutton item based on task.
715  *
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.
721  * @private
722  */
723 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
724                                                       opt_bold,
725                                                       opt_isDefault) {
726   return {
727     label: opt_title || task.title,
728     iconUrl: task.iconUrl,
729     iconType: task.iconType,
730     task: task,
731     bold: opt_bold || false,
732     isDefault: opt_isDefault || false,
733     isGenericFileHandler: task.isGenericFileHandler
734   };
735 };
736
737 /**
738  * Shows modal action picker dialog with currently available list of tasks.
739  *
740  * @param {cr.filebrowser.DefaultActionDialog} actionDialog Action dialog to
741  *     show and update.
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
746  *     handler or not.
747  */
748 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
749                                               onSuccess,
750                                               opt_hideGenericFileHandler) {
751   var hideGenericFileHandler = opt_hideGenericFileHandler || false;
752   var items = this.createItems_();
753
754   if (hideGenericFileHandler)
755     items = items.filter(function(item) { return !item.isGenericFileHandler; });
756
757   var defaultIdx = 0;
758   for (var j = 0; j < items.length; j++) {
759     if (items[j].task.taskId === this.defaultTask_.taskId)
760       defaultIdx = j;
761   }
762
763   actionDialog.show(
764       title,
765       message,
766       items, defaultIdx,
767       function(item) {
768         onSuccess(item.task);
769       });
770 };
771
772 /**
773  * Decorates a FileTasks method, so it will be actually executed after the tasks
774  * are available.
775  * This decorator expects an implementation called |method + '_'|.
776  *
777  * @param {string} method The method name.
778  */
779 FileTasks.decorate = function(method) {
780   var privateMethod = method + '_';
781   FileTasks.prototype[method] = function() {
782     if (this.tasks_) {
783       this[privateMethod].apply(this, arguments);
784     } else {
785       this.pendingInvocations_.push([privateMethod, arguments]);
786     }
787     return this;
788   };
789 };
790
791 FileTasks.decorate('display');
792 FileTasks.decorate('updateMenuItem');
793 FileTasks.decorate('execute');
794 FileTasks.decorate('executeDefault');