Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / 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)
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 List of MIME types for each
88  *     of the files.
89  */
90 FileTasks.prototype.init = function(entries, opt_mimeTypes) {
91   this.entries_ = entries;
92   this.mimeTypes_ = opt_mimeTypes || [];
93
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));
99   }
100 };
101
102 /**
103  * Returns amount of tasks.
104  *
105  * @return {number} amount of tasks.
106  */
107 FileTasks.prototype.size = function() {
108   return (this.tasks_ && this.tasks_.length) || 0;
109 };
110
111 /**
112  * Callback when tasks found.
113  *
114  * @param {Array.<Object>} tasks The tasks.
115  * @private
116  */
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);
123   }
124   this.pendingInvocations_ = [];
125 };
126
127 /**
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
130  * inserted.
131  *
132  * @const
133  * @type {Array.<string>}
134  * @private
135  */
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',
143 ]);
144
145 /**
146  * The list of excutable file extensions.
147  *
148  * @const
149  * @type {Array.<string>}
150  */
151 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
152   '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
153 ]);
154
155 /**
156  * The list of extensions to skip the suggest app dialog.
157  * @const
158  * @type {Array.<string>}
159  * @private
160  */
161 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
162   '.crdownload', '.dsc', '.inf', '.crx',
163 ]);
164
165 /**
166  * Records trial of opening file grouped by extensions.
167  *
168  * @param {Array.<Entry>} entries The entries to be opened.
169  * @private
170  */
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) {
176       extension = 'other';
177     }
178     metrics.recordEnum(
179         'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_);
180   }
181 };
182
183 /**
184  * Returns true if the taskId is for an internal task.
185  *
186  * @param {string} taskId Task identifier.
187  * @return {boolean} True if the task ID is for an internal task.
188  * @private
189  */
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'));
202 };
203
204 /**
205  * Processes internal tasks.
206  *
207  * @param {Array.<Object>} tasks The tasks.
208  * @private
209  */
210 FileTasks.prototype.processTasks_ = function(tasks) {
211   this.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) {
218       isOnDrive = true;
219       break;
220     }
221   }
222
223   for (var i = 0; i < tasks.length; i++) {
224     var task = tasks[i];
225     var taskParts = task.taskId.split('|');
226
227     // Skip internal Files.app's handlers.
228     if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
229         taskParts[2] === 'select' || taskParts[2] === 'open')) {
230       continue;
231     }
232
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'))
265           continue;
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'))
271           continue;
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');
277       }
278     }
279
280     if (!task.iconType && taskParts[1] === 'web-intent') {
281       task.iconType = 'generic';
282     }
283
284     this.tasks_.push(task);
285     if (this.defaultTask_ === null && task.isDefault) {
286       this.defaultTask_ = task;
287     }
288   }
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];
294   }
295 };
296
297 /**
298  * Executes default task.
299  *
300  * @param {function(boolean, Array.<string>)=} opt_callback Called when the
301  *     default task is executed, or the error is occurred.
302  * @private
303  */
304 FileTasks.prototype.executeDefault_ = function(opt_callback) {
305   FileTasks.recordViewingFileTypeUMA_(this.entries_);
306   this.executeDefaultInternal_(this.entries_, opt_callback);
307 };
308
309 /**
310  * Executes default task.
311  *
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.
315  * @private
316  */
317 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
318   var callback = opt_callback || function(arg1, arg2) {};
319
320   if (this.defaultTask_ !== null) {
321     this.executeInternal_(this.defaultTask_.taskId, entries);
322     callback(true, entries);
323     return;
324   }
325
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])
329     return;
330
331   var filename = entries[0].name;
332   var extension = PathUtil.splitExtension(filename)[1];
333   var mimeType = this.mimeTypes_[0];
334
335   var showAlert = function() {
336     var textMessageId;
337     var titleMessageId;
338     switch (extension) {
339       case '.exe':
340         textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
341         break;
342       case '.crx':
343         textMessageId = 'NO_ACTION_FOR_CRX';
344         titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
345         break;
346       default:
347         textMessageId = 'NO_ACTION_FOR_FILE';
348     }
349
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);
355   }.bind(this);
356
357   var onViewFilesFailure = function() {
358     var fm = this.fileManager_;
359     if (!fm.isOnDrive() ||
360         !entries[0] ||
361         FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
362       showAlert();
363       return;
364     }
365
366     fm.openSuggestAppsDialog(
367         entries[0],
368         function() {
369           var newTasks = new FileTasks(fm);
370           newTasks.init(entries, this.mimeTypes_);
371           newTasks.executeDefault();
372           callback(true, entries);
373         }.bind(this),
374         // Cancelled callback.
375         function() {
376           callback(false, entries);
377         },
378         showAlert);
379   }.bind(this);
380
381   var onViewFiles = function(result) {
382     switch (result) {
383       case 'opened':
384         callback(success, entries);
385         break;
386       case 'message_sent':
387         util.isTeleported(window).then(function(teleported) {
388           if (teleported) {
389             util.showOpenInOtherDesktopAlert(
390                 this.fileManager_.ui.alertDialog, entries);
391           }
392         }.bind(this));
393         callback(success, entries);
394         break;
395       case 'empty':
396         callback(success, entries);
397         break;
398       case 'failed':
399         onViewFilesFailure();
400         break;
401     }
402   }.bind(this);
403
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);
409   }.bind(this));
410 };
411
412 /**
413  * Executes a single task.
414  *
415  * @param {string} taskId Task identifier.
416  * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
417  *     this.entries_|.
418  * @private
419  */
420 FileTasks.prototype.execute_ = function(taskId, opt_entries) {
421   var entries = opt_entries || this.entries_;
422   FileTasks.recordViewingFileTypeUMA_(entries);
423   this.executeInternal_(taskId, entries);
424 };
425
426 /**
427  * The core implementation to execute a single task.
428  *
429  * @param {string} taskId Task identifier.
430  * @param {Array.<Entry>} entries Entries to execute.
431  * @private
432  */
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);
438     } else {
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')
443           return;
444         util.isTeleported(window).then(function(teleported) {
445           if (teleported) {
446             util.showOpenInOtherDesktopAlert(
447                 this.fileManager_.ui.alertDialog, entries);
448           }
449         }.bind(this));
450       }.bind(this));
451     }
452   }.bind(this));
453 };
454
455 /**
456  * Checks whether the remote files are available right now.
457  *
458  * @param {function} callback The callback.
459  * @private
460  */
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.
465       return e && e[name];
466     };
467     return props.filter(isOne).length === props.length;
468   };
469
470   var fm = this.fileManager_;
471   var entries = this.entries_;
472
473   var isDriveOffline = fm.volumeManager.getDriveConnectionState().type ===
474       util.DriveConnectionType.OFFLINE;
475
476   if (fm.isOnDrive() && isDriveOffline) {
477     fm.metadataCache_.get(entries, 'drive', function(props) {
478       if (areAll(props, 'availableOffline')) {
479         callback();
480         return;
481       }
482
483       fm.alert.showHtml(
484           loadTimeData.getString('OFFLINE_HEADER'),
485           props[0].hosted ?
486             loadTimeData.getStringF(
487                 entries.length === 1 ?
488                     'HOSTED_OFFLINE_MESSAGE' :
489                     'HOSTED_OFFLINE_MESSAGE_PLURAL') :
490             loadTimeData.getStringF(
491                 entries.length === 1 ?
492                     'OFFLINE_MESSAGE' :
493                     'OFFLINE_MESSAGE_PLURAL',
494                 loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
495     });
496     return;
497   }
498
499   var isOnMetered = fm.volumeManager.getDriveConnectionState().type ===
500       util.DriveConnectionType.METERED;
501
502   if (fm.isOnDrive() && isOnMetered) {
503     fm.metadataCache_.get(entries, 'drive', function(driveProps) {
504       if (areAll(driveProps, 'availableWhenMetered')) {
505         callback();
506         return;
507       }
508
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;
514         }
515         fm.confirm.show(
516             loadTimeData.getStringF(
517                 entries.length === 1 ?
518                     'CONFIRM_MOBILE_DATA_USE' :
519                     'CONFIRM_MOBILE_DATA_USE_PLURAL',
520                 util.bytesToString(sizeToDownload)),
521             callback);
522       });
523     });
524     return;
525   }
526
527   callback();
528 };
529
530 /**
531  * Executes an internal task.
532  *
533  * @param {string} id The short task id.
534  * @param {Array.<Entry>} entries The entries to execute on.
535  * @private
536  */
537 FileTasks.prototype.executeInternalTask_ = function(id, entries) {
538   var fm = this.fileManager_;
539
540   if (id === 'play') {
541     var position = 0;
542     if (entries.length === 1) {
543       // If just a single audio file is selected pass along every audio file
544       // in the directory.
545       var selectedEntries = entries[0];
546       entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio);
547       position = entries.indexOf(selectedEntries);
548     }
549     // TODO(mtomasz): Pass entries instead.
550     var urls = util.entriesToURLs(entries);
551     chrome.fileBrowserPrivate.getProfiles(function(profiles,
552                                                    currentId,
553                                                    displayedId) {
554       fm.backgroundPage.launchAudioPlayer({items: urls, position: position},
555                                           displayedId);
556     });
557     return;
558   }
559
560   if (id === 'mount-archive') {
561     this.mountArchivesInternal_(entries);
562     return;
563   }
564
565   if (id === 'gallery' || id === 'gallery-video') {
566     this.openGalleryInternal_(entries);
567     return;
568   }
569
570   console.error('Unexpected action ID: ' + id);
571 };
572
573 /**
574  * Mounts archives.
575  *
576  * @param {Array.<Entry>} entries Mount file entries list.
577  */
578 FileTasks.prototype.mountArchives = function(entries) {
579   FileTasks.recordViewingFileTypeUMA_(entries);
580   this.mountArchivesInternal_(entries);
581 };
582
583 /**
584  * The core implementation of mounts archives.
585  *
586  * @param {Array.<Entry>} entries Mount file entries list.
587  * @private
588  */
589 FileTasks.prototype.mountArchivesInternal_ = function(entries) {
590   var fm = this.fileManager_;
591
592   var tracker = fm.directoryModel.createDirectoryChangeTracker();
593   tracker.start();
594
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) {
603             tracker.stop();
604             return;
605           }
606           volumeInfo.resolveDisplayRoot(function(displayRoot) {
607             if (tracker.hasChanged) {
608               tracker.stop();
609               return;
610             }
611             fm.directoryModel.changeDirectoryEntry(displayRoot);
612           }, function() {
613             console.warn('Failed to resolve the display root after mounting.');
614             tracker.stop();
615           });
616         }, function(url, error) {
617           tracker.stop();
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]));
623       }
624   });
625 };
626
627 /**
628  * Open the Gallery.
629  *
630  * @param {Array.<Entry>} entries List of selected entries.
631  */
632 FileTasks.prototype.openGallery = function(entries) {
633   FileTasks.recordViewingFileTypeUMA_(entries);
634   this.openGalleryInternal_(entries);
635 };
636
637 /**
638  * The core implementation to open the Gallery.
639  *
640  * @param {Array.<Entry>} entries List of selected entries.
641  * @private
642  */
643 FileTasks.prototype.openGalleryInternal_ = function(entries) {
644   var fm = this.fileManager_;
645
646   var allEntries =
647       fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo);
648
649   var galleryFrame = fm.document_.createElement('iframe');
650   galleryFrame.className = 'overlay-pane';
651   galleryFrame.scrolling = 'no';
652   galleryFrame.setAttribute('webkitallowfullscreen', true);
653
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.
657     util.updateAppState(
658         null, /* keep current directory */
659         '', /* remove current selection */
660         '' /* remove search. */);
661   }
662
663   var savedAppState = JSON.parse(JSON.stringify(window.appState));
664   var savedTitle = document.title;
665
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();
669
670   var onBack = function(selectedEntries) {
671     fm.directoryModel.selectEntries(selectedEntries);
672     fm.closeFilePopup();  // Will call Gallery.unload.
673     window.appState = savedAppState;
674     util.saveAppState();
675     document.title = savedTitle;
676   };
677
678   var onAppRegionChanged = function(visible) {
679     fm.onFilePopupAppRegionChanged(visible);
680   };
681
682   galleryFrame.onload = function() {
683     galleryFrame.contentWindow.ImageUtil.metrics = metrics;
684
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) ||
700             currentDir.name;
701       } else {
702         readonlyDirName = currentDir.name;
703       }
704     }
705
706     var context = {
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(),
715       onBack: onBack,
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
721     };
722     galleryFrame.contentWindow.Gallery.open(
723         context, fm.volumeManager, allEntries, entries);
724   }.bind(this);
725
726   galleryFrame.src = 'gallery.html';
727   fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm));
728 };
729
730 /**
731  * Displays the list of tasks in a task picker combobutton.
732  *
733  * @param {cr.ui.ComboButton} combobutton The task picker element.
734  * @private
735  */
736 FileTasks.prototype.display_ = function(combobutton) {
737   if (this.tasks_.length === 0) {
738     combobutton.hidden = true;
739     return;
740   }
741
742   combobutton.clear();
743   combobutton.hidden = false;
744   combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
745
746   var items = this.createItems_();
747
748   if (items.length > 1) {
749     var defaultIdx = 0;
750
751     for (var j = 0; j < items.length; j++) {
752       combobutton.addDropDownItem(items[j]);
753       if (items[j].task.taskId === this.defaultTask_.taskId)
754         defaultIdx = j;
755     }
756
757     combobutton.addSeparator();
758     var changeDefaultMenuItem = combobutton.addDropDownItem({
759         label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
760     });
761     changeDefaultMenuItem.classList.add('change-default');
762   }
763 };
764
765 /**
766  * Creates sorted array of available task descriptions such as title and icon.
767  *
768  * @return {Array} created array can be used to feed combobox, menus and so on.
769  * @private
770  */
771 FileTasks.prototype.createItems_ = function() {
772   var items = [];
773   var title = this.defaultTask_.title + ' ' +
774               loadTimeData.getString('DEFAULT_ACTION_LABEL');
775   items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
776
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));
781   }
782
783   items.sort(function(a, b) {
784     return a.label.localeCompare(b.label);
785   });
786
787   return items;
788 };
789
790 /**
791  * Updates context menu with default item.
792  * @private
793  */
794
795 FileTasks.prototype.updateMenuItem_ = function() {
796   this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
797       this.tasks_.length > 1);
798 };
799
800 /**
801  * Creates combobutton item based on task.
802  *
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.
807  * @private
808  */
809 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
810                                                       opt_bold) {
811   return {
812     label: opt_title || task.title,
813     iconUrl: task.iconUrl,
814     iconType: task.iconType,
815     task: task,
816     bold: opt_bold || false
817   };
818 };
819
820 /**
821  * Shows modal action picker dialog with currently available list of tasks.
822  *
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.
827  */
828 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
829                                               onSuccess) {
830   var items = this.createItems_();
831
832   var defaultIdx = 0;
833   for (var j = 0; j < items.length; j++) {
834     if (items[j].task.taskId === this.defaultTask_.taskId)
835       defaultIdx = j;
836   }
837
838   actionDialog.show(
839       title,
840       message,
841       items, defaultIdx,
842       function(item) {
843         onSuccess(item.task);
844       });
845 };
846
847 /**
848  * Decorates a FileTasks method, so it will be actually executed after the tasks
849  * are available.
850  * This decorator expects an implementation called |method + '_'|.
851  *
852  * @param {string} method The method name.
853  */
854 FileTasks.decorate = function(method) {
855   var privateMethod = method + '_';
856   FileTasks.prototype[method] = function() {
857     if (this.tasks_) {
858       this[privateMethod].apply(this, arguments);
859     } else {
860       this.pendingInvocations_.push([privateMethod, arguments]);
861     }
862     return this;
863   };
864 };
865
866 FileTasks.decorate('display');
867 FileTasks.decorate('updateMenuItem');
868 FileTasks.decorate('execute');
869 FileTasks.decorate('executeDefault');