Home | History | Annotate | Download | only in 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  * @param {FileManager} fileManager FileManager instance.
     11  * @param {Object=} opt_params File manager load parameters.
     12  * @constructor
     13  */
     14 function FileTasks(fileManager, opt_params) {
     15   this.fileManager_ = fileManager;
     16   this.params_ = opt_params;
     17   this.tasks_ = null;
     18   this.defaultTask_ = 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 FAQ about the file actions.
     31  *
     32  * @const
     33  * @type {string}
     34  */
     35 FileTasks.NO_ACTION_FOR_FILE_URL = 'http://support.google.com/chromeos/bin/' +
     36     'answer.py?answer=1700055&topic=29026&ctx=topic';
     37 
     38 /**
     39  * Base URL of apps list in the Chrome Web Store. This constant is used in
     40  * FileTasks.createWebStoreLink().
     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  * Returns URL of the Chrome Web Store which show apps supporting the given
     49  * file-extension and mime-type.
     50  *
     51  * @param {string} extension Extension of the file.
     52  * @param {string} mimeType Mime type of the file.
     53  * @return {string} URL
     54  */
     55 FileTasks.createWebStoreLink = function(extension, mimeType) {
     56   var url = FileTasks.WEB_STORE_HANDLER_BASE_URL;
     57   url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, '');
     58   if (mimeType)
     59     url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, '');
     60   return url;
     61 };
     62 
     63 /**
     64  * Complete the initialization.
     65  *
     66  * @param {Array.<string>} urls List of file urls.
     67  * @param {Array.<string>=} opt_mimeTypes List of MIME types for each
     68  *     of the files.
     69  */
     70 FileTasks.prototype.init = function(urls, opt_mimeTypes) {
     71   this.urls_ = urls;
     72   if (urls.length > 0)
     73     chrome.fileBrowserPrivate.getFileTasks(urls, opt_mimeTypes || [],
     74       this.onTasks_.bind(this));
     75 };
     76 
     77 /**
     78  * Returns amount of tasks.
     79  *
     80  * @return {number} amount of tasks.
     81  */
     82 FileTasks.prototype.size = function() {
     83   return (this.tasks_ && this.tasks_.length) || 0;
     84 };
     85 
     86 /**
     87  * Callback when tasks found.
     88  *
     89  * @param {Array.<Object>} tasks The tasks.
     90  * @private
     91  */
     92 FileTasks.prototype.onTasks_ = function(tasks) {
     93   this.processTasks_(tasks);
     94   for (var index = 0; index < this.pendingInvocations_.length; index++) {
     95     var name = this.pendingInvocations_[index][0];
     96     var args = this.pendingInvocations_[index][1];
     97     this[name].apply(this, args);
     98   }
     99   this.pendingInvocations_ = [];
    100 };
    101 
    102 /**
    103  * The list of known extensions to record UMA.
    104  * Note: Because the data is recorded by the index, so new item shouldn't be
    105  * inserted.
    106  *
    107  * @const
    108  * @type {Array.<string>}
    109  * @private
    110  */
    111 FileTasks.knownExtensions_ = [
    112   'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv',
    113   '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8',
    114   '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf',
    115   '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt',
    116   '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp',
    117   '.wma', '.wmv', '.xls', '.xlsx',
    118 ];
    119 
    120 /**
    121  * Records trial of opening file grouped by extensions.
    122  *
    123  * @param {Array.<string>} urls The path to be opened.
    124  * @private
    125  */
    126 FileTasks.recordViewingFileTypeUMA_ = function(urls) {
    127   for (var i = 0; i < urls.length; i++) {
    128     var url = urls[i];
    129     var extension = FileType.getExtension(url).toLowerCase();
    130     if (FileTasks.knownExtensions_.indexOf(extension) < 0) {
    131       extension = 'other';
    132     }
    133     metrics.recordEnum(
    134         'ViewingFileType', extension, FileTasks.knownExtensions_);
    135   }
    136 };
    137 
    138 /**
    139  * Processes internal tasks.
    140  *
    141  * @param {Array.<Object>} tasks The tasks.
    142  * @private
    143  */
    144 FileTasks.prototype.processTasks_ = function(tasks) {
    145   this.tasks_ = [];
    146   var id = chrome.runtime.id;
    147   var isOnDrive = false;
    148   for (var index = 0; index < this.urls_.length; ++index) {
    149     if (FileType.isOnDrive(this.urls_[index])) {
    150       isOnDrive = true;
    151       break;
    152     }
    153   }
    154 
    155   for (var i = 0; i < tasks.length; i++) {
    156     var task = tasks[i];
    157     var taskParts = task.taskId.split('|');
    158 
    159     // Skip Drive App if the file is not on Drive.
    160     if (!isOnDrive && task.driveApp)
    161       continue;
    162 
    163     // Skip internal Files.app's handlers.
    164     if (taskParts[0] == id && (taskParts[2] == 'auto-open' ||
    165         taskParts[2] == 'select' || taskParts[2] == 'open')) {
    166       continue;
    167     }
    168 
    169     // Tweak images, titles of internal tasks.
    170     if (taskParts[0] == id && taskParts[1] == 'file') {
    171       if (taskParts[2] == 'play') {
    172         // TODO(serya): This hack needed until task.iconUrl is working
    173         //             (see GetFileTasksFileBrowserFunction::RunImpl).
    174         task.iconType = 'audio';
    175         task.title = loadTimeData.getString('ACTION_LISTEN');
    176       } else if (taskParts[2] == 'mount-archive') {
    177         task.iconType = 'archive';
    178         task.title = loadTimeData.getString('MOUNT_ARCHIVE');
    179       } else if (taskParts[2] == 'gallery') {
    180         task.iconType = 'image';
    181         task.title = loadTimeData.getString('ACTION_OPEN');
    182       } else if (taskParts[2] == 'watch') {
    183         task.iconType = 'video';
    184         task.title = loadTimeData.getString('ACTION_WATCH');
    185       } else if (taskParts[2] == 'open-hosted-generic') {
    186         if (this.urls_.length > 1)
    187           task.iconType = 'generic';
    188         else // Use specific icon.
    189           task.iconType = FileType.getIcon(this.urls_[0]);
    190         task.title = loadTimeData.getString('ACTION_OPEN');
    191       } else if (taskParts[2] == 'open-hosted-gdoc') {
    192         task.iconType = 'gdoc';
    193         task.title = loadTimeData.getString('ACTION_OPEN_GDOC');
    194       } else if (taskParts[2] == 'open-hosted-gsheet') {
    195         task.iconType = 'gsheet';
    196         task.title = loadTimeData.getString('ACTION_OPEN_GSHEET');
    197       } else if (taskParts[2] == 'open-hosted-gslides') {
    198         task.iconType = 'gslides';
    199         task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES');
    200       } else if (taskParts[2] == 'view-swf') {
    201         // Do not render this task if disabled.
    202         if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED'))
    203           continue;
    204         task.iconType = 'generic';
    205         task.title = loadTimeData.getString('ACTION_VIEW');
    206       } else if (taskParts[2] == 'view-pdf') {
    207         // Do not render this task if disabled.
    208         if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED'))
    209           continue;
    210         task.iconType = 'pdf';
    211         task.title = loadTimeData.getString('ACTION_VIEW');
    212       } else if (taskParts[2] == 'view-in-browser') {
    213         task.iconType = 'generic';
    214         task.title = loadTimeData.getString('ACTION_VIEW');
    215       } else if (taskParts[2] == 'install-crx') {
    216         task.iconType = 'generic';
    217         task.title = loadTimeData.getString('INSTALL_CRX');
    218       }
    219     }
    220 
    221     if (!task.iconType && taskParts[1] == 'web-intent') {
    222       task.iconType = 'generic';
    223     }
    224 
    225     this.tasks_.push(task);
    226     if (this.defaultTask_ == null && task.isDefault) {
    227       this.defaultTask_ = task;
    228     }
    229   }
    230   if (!this.defaultTask_ && this.tasks_.length > 0) {
    231     // If we haven't picked a default task yet, then just pick the first one.
    232     // This is not the preferred way we want to pick this, but better this than
    233     // no default at all if the C++ code didn't set one.
    234     this.defaultTask_ = this.tasks_[0];
    235   }
    236 };
    237 
    238 /**
    239  * Executes default task.
    240  *
    241  * @private
    242  */
    243 FileTasks.prototype.executeDefault_ = function() {
    244   var urls = this.urls_;
    245   FileTasks.recordViewingFileTypeUMA_(urls);
    246   this.executeDefaultInternal_(urls);
    247 };
    248 
    249 /**
    250  * Executes default task.
    251  *
    252  * @param {Array.<string>} urls Urls to execute.
    253  * @private
    254  */
    255 FileTasks.prototype.executeDefaultInternal_ = function(urls) {
    256   if (this.defaultTask_ != null) {
    257     this.executeInternal_(this.defaultTask_.taskId, urls);
    258     return;
    259   }
    260 
    261   // We don't have tasks, so try to show a file in a browser tab.
    262   // We only do that for single selection to avoid confusion.
    263   if (urls.length == 1) {
    264     var callback = function(success) {
    265       if (!success) {
    266         var filename = decodeURIComponent(urls[0]);
    267         if (filename.indexOf('/') != -1)
    268           filename = filename.substr(filename.lastIndexOf('/') + 1);
    269         var extension = filename.lastIndexOf('.') != -1 ?
    270             filename.substr(filename.lastIndexOf('.') + 1) : '';
    271 
    272         this.fileManager_.metadataCache_.get(urls, 'drive', function(props) {
    273           var mimeType;
    274           if (props && props[0] && props[0].contentMimeType)
    275             mimeType = props[0].contentMimeType;
    276 
    277           var messageString = extension == 'exe' ? 'NO_ACTION_FOR_EXECUTABLE' :
    278                                                    'NO_ACTION_FOR_FILE';
    279           var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType);
    280           var text = loadTimeData.getStringF(messageString,
    281                                              webStoreUrl,
    282                                              FileTasks.NO_ACTION_FOR_FILE_URL);
    283           this.fileManager_.alert.showHtml(filename, text, function() {});
    284         }.bind(this));
    285       }
    286     }.bind(this);
    287 
    288     this.checkAvailability_(function() {
    289       chrome.fileBrowserPrivate.viewFiles(urls, callback);
    290     }.bind(this));
    291   }
    292 
    293   // Do nothing for multiple urls.
    294 };
    295 
    296 /**
    297  * Executes a single task.
    298  *
    299  * @param {string} taskId Task identifier.
    300  * @param {Array.<string>=} opt_urls Urls to execute on instead of |this.urls_|.
    301  * @private
    302  */
    303 FileTasks.prototype.execute_ = function(taskId, opt_urls) {
    304   var urls = opt_urls || this.urls_;
    305   FileTasks.recordViewingFileTypeUMA_(urls);
    306   this.executeInternal_(taskId, urls);
    307 };
    308 
    309 /**
    310  * The core implementation to execute a single task.
    311  *
    312  * @param {string} taskId Task identifier.
    313  * @param {Array.<string>} urls Urls to execute.
    314  * @private
    315  */
    316 FileTasks.prototype.executeInternal_ = function(taskId, urls) {
    317   this.checkAvailability_(function() {
    318     var taskParts = taskId.split('|');
    319     if (taskParts[0] == chrome.runtime.id && taskParts[1] == 'file') {
    320       // For internal tasks we do not listen to the event to avoid
    321       // handling the same task instance from multiple tabs.
    322       // So, we manually execute the task.
    323       this.executeInternalTask_(taskParts[2], urls);
    324     } else {
    325       chrome.fileBrowserPrivate.executeTask(taskId, urls);
    326     }
    327   }.bind(this));
    328 };
    329 
    330 /**
    331  * Checks whether the remote files are available right now.
    332  *
    333  * @param {function} callback The callback.
    334  * @private
    335  */
    336 FileTasks.prototype.checkAvailability_ = function(callback) {
    337   var areAll = function(props, name) {
    338     var isOne = function(e) {
    339       // If got no properties, we safely assume that item is unavailable.
    340       return e && e[name];
    341     };
    342     return props.filter(isOne).length == props.length;
    343   };
    344 
    345   var fm = this.fileManager_;
    346   var urls = this.urls_;
    347 
    348   if (fm.isOnDrive() && fm.isDriveOffline()) {
    349     fm.metadataCache_.get(urls, 'drive', function(props) {
    350       if (areAll(props, 'availableOffline')) {
    351         callback();
    352         return;
    353       }
    354 
    355       fm.alert.showHtml(
    356           loadTimeData.getString('OFFLINE_HEADER'),
    357           props[0].hosted ?
    358             loadTimeData.getStringF(
    359                 urls.length == 1 ?
    360                     'HOSTED_OFFLINE_MESSAGE' :
    361                     'HOSTED_OFFLINE_MESSAGE_PLURAL') :
    362             loadTimeData.getStringF(
    363                 urls.length == 1 ?
    364                     'OFFLINE_MESSAGE' :
    365                     'OFFLINE_MESSAGE_PLURAL',
    366                 loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
    367     });
    368     return;
    369   }
    370 
    371   if (fm.isOnDrive() && fm.isDriveOnMeteredConnection()) {
    372     fm.metadataCache_.get(urls, 'drive', function(driveProps) {
    373       if (areAll(driveProps, 'availableWhenMetered')) {
    374         callback();
    375         return;
    376       }
    377 
    378       fm.metadataCache_.get(urls, 'filesystem', function(fileProps) {
    379         var sizeToDownload = 0;
    380         for (var i = 0; i != urls.length; i++) {
    381           if (!driveProps[i].availableWhenMetered)
    382             sizeToDownload += fileProps[i].size;
    383         }
    384         fm.confirm.show(
    385             loadTimeData.getStringF(
    386                 urls.length == 1 ?
    387                     'CONFIRM_MOBILE_DATA_USE' :
    388                     'CONFIRM_MOBILE_DATA_USE_PLURAL',
    389                 util.bytesToString(sizeToDownload)),
    390             callback);
    391       });
    392     });
    393     return;
    394   }
    395 
    396   callback();
    397 };
    398 
    399 /**
    400  * Executes an internal task.
    401  *
    402  * @param {string} id The short task id.
    403  * @param {Array.<string>} urls The urls to execute on.
    404  * @private
    405  */
    406 FileTasks.prototype.executeInternalTask_ = function(id, urls) {
    407   var fm = this.fileManager_;
    408 
    409   if (id == 'play') {
    410     var position = 0;
    411     if (urls.length == 1) {
    412       // If just a single audio file is selected pass along every audio file
    413       // in the directory.
    414       var selectedUrl = urls[0];
    415       urls = fm.getAllUrlsInCurrentDirectory().filter(FileType.isAudio);
    416       position = urls.indexOf(selectedUrl);
    417     }
    418     chrome.runtime.getBackgroundPage(function(background) {
    419       background.launchAudioPlayer({ items: urls, position: position });
    420     });
    421     return;
    422   }
    423 
    424   if (id == 'watch') {
    425     console.assert(urls.length == 1, 'Cannot open multiple videos');
    426     chrome.runtime.getBackgroundPage(function(background) {
    427       background.launchVideoPlayer(urls[0]);
    428     });
    429     return;
    430   }
    431 
    432   if (id == 'mount-archive') {
    433     this.mountArchivesInternal_(urls);
    434     return;
    435   }
    436 
    437   if (id == 'format-device') {
    438     fm.confirm.show(loadTimeData.getString('FORMATTING_WARNING'), function() {
    439       chrome.fileBrowserPrivate.formatDevice(urls[0]);
    440     });
    441     return;
    442   }
    443 
    444   if (id == 'gallery') {
    445     this.openGalleryInternal_(urls);
    446     return;
    447   }
    448 
    449   if (id == 'view-pdf' || id == 'view-swf' || id == 'view-in-browser' ||
    450       id == 'install-crx' || id.match(/^open-hosted-/) || id == 'watch') {
    451     chrome.fileBrowserPrivate.viewFiles(urls, function(success) {
    452       if (!success)
    453         console.error('chrome.fileBrowserPrivate.viewFiles failed', urls);
    454     });
    455   }
    456 };
    457 
    458 /**
    459  * Mounts archives.
    460  *
    461  * @param {Array.<string>} urls Mount file urls list.
    462  */
    463 FileTasks.prototype.mountArchives = function(urls) {
    464   FileTasks.recordViewingFileTypeUMA_(urls);
    465   this.mountArchivesInternal_(urls);
    466 };
    467 
    468 /**
    469  * The core implementation of mounts archives.
    470  *
    471  * @param {Array.<string>} urls Mount file urls list.
    472  * @private
    473  */
    474 FileTasks.prototype.mountArchivesInternal_ = function(urls) {
    475   var fm = this.fileManager_;
    476 
    477   var tracker = fm.directoryModel_.createDirectoryChangeTracker();
    478   tracker.start();
    479 
    480   fm.resolveSelectResults_(urls, function(urls) {
    481     for (var index = 0; index < urls.length; ++index) {
    482       fm.volumeManager_.mountArchive(urls[index], function(mountPath) {
    483         tracker.stop();
    484         if (!tracker.hasChanged)
    485           fm.directoryModel_.changeDirectory(mountPath);
    486       }, function(url, error) {
    487         var path = util.extractFilePath(url);
    488         tracker.stop();
    489         var namePos = path.lastIndexOf('/');
    490         fm.alert.show(strf('ARCHIVE_MOUNT_FAILED',
    491                            path.substr(namePos + 1), error));
    492       }.bind(null, urls[index]));
    493     }
    494   });
    495 };
    496 
    497 /**
    498  * Open the Gallery.
    499  *
    500  * @param {Array.<string>} urls List of selected urls.
    501  */
    502 FileTasks.prototype.openGallery = function(urls) {
    503   FileTasks.recordViewingFileTypeUMA_(urls);
    504   this.openGalleryInternal_(urls);
    505 };
    506 
    507 /**
    508  * The core implementation to open the Gallery.
    509  *
    510  * @param {Array.<string>} urls List of selected urls.
    511  * @private
    512  */
    513 FileTasks.prototype.openGalleryInternal_ = function(urls) {
    514   var fm = this.fileManager_;
    515 
    516   var allUrls =
    517       fm.getAllUrlsInCurrentDirectory().filter(FileType.isImageOrVideo);
    518 
    519   var galleryFrame = fm.document_.createElement('iframe');
    520   galleryFrame.className = 'overlay-pane';
    521   galleryFrame.scrolling = 'no';
    522   galleryFrame.setAttribute('webkitallowfullscreen', true);
    523 
    524   if (this.params_ && this.params_.gallery) {
    525     // Remove the Gallery state from the location, we do not need it any more.
    526     util.updateAppState(null /* keep path */, '' /* remove search. */);
    527   }
    528 
    529   var savedAppState = window.appState;
    530   var savedTitle = document.title;
    531 
    532   // Push a temporary state which will be replaced every time the selection
    533   // changes in the Gallery and popped when the Gallery is closed.
    534   util.updateAppState();
    535 
    536   var onBack = function(selectedUrls) {
    537     fm.directoryModel_.selectUrls(selectedUrls);
    538     fm.closeFilePopup_();  // Will call Gallery.unload.
    539     window.appState = savedAppState;
    540     util.saveAppState();
    541     document.title = savedTitle;
    542   };
    543 
    544   var onClose = function() {
    545     fm.onClose();
    546   };
    547 
    548   var onMaximize = function() {
    549     fm.onMaximize();
    550   };
    551 
    552   galleryFrame.onload = function() {
    553     galleryFrame.contentWindow.ImageUtil.metrics = metrics;
    554     window.galleryTestAPI = galleryFrame.contentWindow.galleryTestAPI;
    555 
    556     // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the
    557     // root. We should check more granular permission to know whether the file
    558     // is writable or not.
    559     var readonly = fm.isOnReadonlyDirectory();
    560     var currentDir = fm.directoryModel_.getCurrentDirEntry();
    561     var downloadsDir = fm.directoryModel_.getRootsList().item(0);
    562     var readonlyDirName = null;
    563     if (readonly) {
    564       readonlyDirName = fm.isOnDrive() ?
    565           PathUtil.getRootLabel(PathUtil.getRootPath(currentDir.fullPath)) :
    566           fm.directoryModel_.getCurrentRootName();
    567     }
    568 
    569     var context = {
    570       // We show the root label in readonly warning (e.g. archive name).
    571       readonlyDirName: readonlyDirName,
    572       curDirEntry: currentDir,
    573       saveDirEntry: readonly ? downloadsDir : null,
    574       searchResults: fm.directoryModel_.isSearching(),
    575       metadataCache: fm.metadataCache_,
    576       pageState: this.params_,
    577       appWindow: chrome.app.window.current(),
    578       onBack: onBack,
    579       onClose: onClose,
    580       onMaximize: onMaximize,
    581       displayStringFunction: strf
    582     };
    583     galleryFrame.contentWindow.Gallery.open(context, allUrls, urls);
    584   }.bind(this);
    585 
    586   galleryFrame.src = 'gallery.html';
    587   fm.openFilePopup_(galleryFrame, fm.updateTitle_.bind(fm));
    588 };
    589 
    590 /**
    591  * Displays the list of tasks in a task picker combobutton.
    592  *
    593  * @param {cr.ui.ComboButton} combobutton The task picker element.
    594  * @private
    595  */
    596 FileTasks.prototype.display_ = function(combobutton) {
    597   if (this.tasks_.length == 0) {
    598     combobutton.hidden = true;
    599     return;
    600   }
    601 
    602   combobutton.clear();
    603   combobutton.hidden = false;
    604   combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
    605 
    606   var items = this.createItems_();
    607 
    608   if (items.length > 1) {
    609     var defaultIdx = 0;
    610 
    611     for (var j = 0; j < items.length; j++) {
    612       combobutton.addDropDownItem(items[j]);
    613       if (items[j].task.taskId == this.defaultTask_.taskId)
    614         defaultIdx = j;
    615     }
    616 
    617     combobutton.addSeparator();
    618     var changeDefaultMenuItem = combobutton.addDropDownItem({
    619         label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
    620     });
    621     changeDefaultMenuItem.classList.add('change-default');
    622   }
    623 };
    624 
    625 /**
    626  * Creates sorted array of available task descriptions such as title and icon.
    627  *
    628  * @return {Array} created array can be used to feed combobox, menus and so on.
    629  * @private
    630  */
    631 FileTasks.prototype.createItems_ = function() {
    632   var items = [];
    633   var title = this.defaultTask_.title + ' ' +
    634               loadTimeData.getString('DEFAULT_ACTION_LABEL');
    635   items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
    636 
    637   for (var index = 0; index < this.tasks_.length; index++) {
    638     var task = this.tasks_[index];
    639     if (task != this.defaultTask_)
    640       items.push(this.createCombobuttonItem_(task));
    641   }
    642 
    643   items.sort(function(a, b) {
    644     return a.label.localeCompare(b.label);
    645   });
    646 
    647   return items;
    648 };
    649 
    650 /**
    651  * Updates context menu with default item.
    652  * @private
    653  */
    654 
    655 FileTasks.prototype.updateMenuItem_ = function() {
    656   this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
    657       this.tasks_.length > 1);
    658 };
    659 
    660 /**
    661  * Creates combobutton item based on task.
    662  *
    663  * @param {Object} task Task to convert.
    664  * @param {string=} opt_title Title.
    665  * @param {boolean=} opt_bold Make a menu item bold.
    666  * @return {Object} Item appendable to combobutton drop-down list.
    667  * @private
    668  */
    669 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
    670                                                       opt_bold) {
    671   return {
    672     label: opt_title || task.title,
    673     iconUrl: task.iconUrl,
    674     iconType: task.iconType,
    675     task: task,
    676     bold: opt_bold || false
    677   };
    678 };
    679 
    680 
    681 /**
    682  * Decorates a FileTasks method, so it will be actually executed after the tasks
    683  * are available.
    684  * This decorator expects an implementation called |method + '_'|.
    685  *
    686  * @param {string} method The method name.
    687  */
    688 FileTasks.decorate = function(method) {
    689   var privateMethod = method + '_';
    690   FileTasks.prototype[method] = function() {
    691     if (this.tasks_) {
    692       this[privateMethod].apply(this, arguments);
    693     } else {
    694       this.pendingInvocations_.push([privateMethod, arguments]);
    695     }
    696     return this;
    697   };
    698 };
    699 
    700 /**
    701  * Shows modal action picker dialog with currently available list of tasks.
    702  *
    703  * @param {DefaultActionDialog} actionDialog Action dialog to show and update.
    704  * @param {string} title Title to use.
    705  * @param {string} message Message to use.
    706  * @param {function(Object)} onSuccess Callback to pass selected task.
    707  */
    708 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
    709                                               onSuccess) {
    710   var items = this.createItems_();
    711 
    712   var defaultIdx = 0;
    713   for (var j = 0; j < items.length; j++) {
    714     if (items[j].task.taskId == this.defaultTask_.taskId)
    715       defaultIdx = j;
    716   }
    717 
    718   actionDialog.show(
    719       title,
    720       message,
    721       items, defaultIdx,
    722       function(item) {
    723         onSuccess(item.task);
    724       });
    725 };
    726 
    727 FileTasks.decorate('display');
    728 FileTasks.decorate('updateMenuItem');
    729 FileTasks.decorate('execute');
    730 FileTasks.decorate('executeDefault');
    731