• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
15function 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 */
37FileTasks.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 */
46FileTasks.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 */
55FileTasks.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 */
65FileTasks.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 */
90FileTasks.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 */
107FileTasks.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 */
117FileTasks.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 */
136FileTasks.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 executable file extensions.
147 *
148 * @const
149 * @type {Array.<string>}
150 */
151FileTasks.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 */
161FileTasks.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 */
171FileTasks.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 */
190FileTasks.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 */
210FileTasks.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 */
304FileTasks.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 */
317FileTasks.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 = util.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, entries);
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 */
420FileTasks.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 */
433FileTasks.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 */
461FileTasks.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      VolumeManagerCommon.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      VolumeManagerCommon.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 */
537FileTasks.prototype.executeInternalTask_ = function(id, entries) {
538  var fm = this.fileManager_;
539
540  if (id === 'play') {
541    var selectedEntry = entries[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      entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio);
546    }
547    // TODO(mtomasz): Pass entries instead.
548    var urls = util.entriesToURLs(entries);
549    var position = urls.indexOf(selectedEntry.toURL());
550    chrome.fileBrowserPrivate.getProfiles(function(profiles,
551                                                   currentId,
552                                                   displayedId) {
553      fm.backgroundPage.launchAudioPlayer({items: urls, position: position},
554                                          displayedId);
555    });
556    return;
557  }
558
559  if (id === 'mount-archive') {
560    this.mountArchivesInternal_(entries);
561    return;
562  }
563
564  if (id === 'gallery' || id === 'gallery-video') {
565    this.openGalleryInternal_(entries);
566    return;
567  }
568
569  console.error('Unexpected action ID: ' + id);
570};
571
572/**
573 * Mounts archives.
574 *
575 * @param {Array.<Entry>} entries Mount file entries list.
576 */
577FileTasks.prototype.mountArchives = function(entries) {
578  FileTasks.recordViewingFileTypeUMA_(entries);
579  this.mountArchivesInternal_(entries);
580};
581
582/**
583 * The core implementation of mounts archives.
584 *
585 * @param {Array.<Entry>} entries Mount file entries list.
586 * @private
587 */
588FileTasks.prototype.mountArchivesInternal_ = function(entries) {
589  var fm = this.fileManager_;
590
591  var tracker = fm.directoryModel.createDirectoryChangeTracker();
592  tracker.start();
593
594  // TODO(mtomasz): Pass Entries instead of URLs.
595  var urls = util.entriesToURLs(entries);
596  fm.resolveSelectResults_(urls, function(resolvedURLs) {
597    for (var index = 0; index < resolvedURLs.length; ++index) {
598      // TODO(mtomasz): Pass Entry instead of URL.
599      fm.volumeManager.mountArchive(resolvedURLs[index],
600        function(volumeInfo) {
601          if (tracker.hasChanged) {
602            tracker.stop();
603            return;
604          }
605          volumeInfo.resolveDisplayRoot(function(displayRoot) {
606            if (tracker.hasChanged) {
607              tracker.stop();
608              return;
609            }
610            fm.directoryModel.changeDirectoryEntry(displayRoot);
611          }, function() {
612            console.warn('Failed to resolve the display root after mounting.');
613            tracker.stop();
614          });
615        }, function(url, error) {
616          tracker.stop();
617          var path = util.extractFilePath(url);
618          var namePos = path.lastIndexOf('/');
619          fm.alert.show(strf('ARCHIVE_MOUNT_FAILED',
620                             path.substr(namePos + 1), error));
621        }.bind(null, resolvedURLs[index]));
622      }
623  });
624};
625
626/**
627 * Open the Gallery.
628 *
629 * @param {Array.<Entry>} entries List of selected entries.
630 */
631FileTasks.prototype.openGallery = function(entries) {
632  FileTasks.recordViewingFileTypeUMA_(entries);
633  this.openGalleryInternal_(entries);
634};
635
636/**
637 * The core implementation to open the Gallery.
638 *
639 * @param {Array.<Entry>} entries List of selected entries.
640 * @private
641 */
642FileTasks.prototype.openGalleryInternal_ = function(entries) {
643  var fm = this.fileManager_;
644
645  var allEntries =
646      fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo);
647
648  var galleryFrame = fm.document_.createElement('iframe');
649  galleryFrame.className = 'overlay-pane';
650  galleryFrame.scrolling = 'no';
651  galleryFrame.setAttribute('webkitallowfullscreen', true);
652
653  if (this.params_ && this.params_.gallery) {
654    // Remove the Gallery state from the location, we do not need it any more.
655    // TODO(mtomasz): Consider keeping the selection path.
656    util.updateAppState(
657        null, /* keep current directory */
658        '', /* remove current selection */
659        '' /* remove search. */);
660  }
661
662  var savedAppState = JSON.parse(JSON.stringify(window.appState));
663  var savedTitle = document.title;
664
665  // Push a temporary state which will be replaced every time the selection
666  // changes in the Gallery and popped when the Gallery is closed.
667  util.updateAppState();
668
669  var onBack = function(selectedEntries) {
670    fm.directoryModel.selectEntries(selectedEntries);
671    fm.closeFilePopup();  // Will call Gallery.unload.
672    window.appState = savedAppState;
673    util.saveAppState();
674    document.title = savedTitle;
675  };
676
677  var onAppRegionChanged = function(visible) {
678    fm.onFilePopupAppRegionChanged(visible);
679  };
680
681  galleryFrame.onload = function() {
682    galleryFrame.contentWindow.ImageUtil.metrics = metrics;
683
684    // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the
685    // root. We should check more granular permission to know whether the file
686    // is writable or not.
687    var readonly = fm.isOnReadonlyDirectory();
688    var currentDir = fm.getCurrentDirectoryEntry();
689    var downloadsVolume = fm.volumeManager.getCurrentProfileVolumeInfo(
690            VolumeManagerCommon.RootType.DOWNLOADS);
691    var downloadsDir = downloadsVolume && downloadsVolume.fileSystem.root;
692
693    // TODO(mtomasz): Pass Entry instead of localized name. Conversion to a
694    //     display string should be done in gallery.js.
695    var readonlyDirName = null;
696    if (readonly && currentDir)
697      readonlyDirName = util.getEntryLabel(fm.volumeManager, currentDir);
698
699    var context = {
700      // We show the root label in readonly warning (e.g. archive name).
701      readonlyDirName: readonlyDirName,
702      curDirEntry: currentDir,
703      saveDirEntry: readonly ? downloadsDir : null,
704      searchResults: fm.directoryModel.isSearching(),
705      metadataCache: fm.metadataCache_,
706      pageState: this.params_,
707      appWindow: chrome.app.window.current(),
708      onBack: onBack,
709      onClose: fm.onClose.bind(fm),
710      onMaximize: fm.onMaximize.bind(fm),
711      onMinimize: fm.onMinimize.bind(fm),
712      onAppRegionChanged: onAppRegionChanged,
713      loadTimeData: fm.backgroundPage.background.stringData
714    };
715    galleryFrame.contentWindow.Gallery.open(
716        context, fm.volumeManager, allEntries, entries);
717  }.bind(this);
718
719  galleryFrame.src = 'gallery.html';
720  fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm));
721};
722
723/**
724 * Displays the list of tasks in a task picker combobutton.
725 *
726 * @param {cr.ui.ComboButton} combobutton The task picker element.
727 * @private
728 */
729FileTasks.prototype.display_ = function(combobutton) {
730  if (this.tasks_.length === 0) {
731    combobutton.hidden = true;
732    return;
733  }
734
735  combobutton.clear();
736  combobutton.hidden = false;
737  combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
738
739  var items = this.createItems_();
740
741  if (items.length > 1) {
742    var defaultIdx = 0;
743
744    for (var j = 0; j < items.length; j++) {
745      combobutton.addDropDownItem(items[j]);
746      if (items[j].task.taskId === this.defaultTask_.taskId)
747        defaultIdx = j;
748    }
749
750    combobutton.addSeparator();
751    var changeDefaultMenuItem = combobutton.addDropDownItem({
752        label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
753    });
754    changeDefaultMenuItem.classList.add('change-default');
755  }
756};
757
758/**
759 * Creates sorted array of available task descriptions such as title and icon.
760 *
761 * @return {Array} created array can be used to feed combobox, menus and so on.
762 * @private
763 */
764FileTasks.prototype.createItems_ = function() {
765  var items = [];
766  var title = this.defaultTask_.title + ' ' +
767              loadTimeData.getString('DEFAULT_ACTION_LABEL');
768  items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
769
770  for (var index = 0; index < this.tasks_.length; index++) {
771    var task = this.tasks_[index];
772    if (task !== this.defaultTask_)
773      items.push(this.createCombobuttonItem_(task));
774  }
775
776  items.sort(function(a, b) {
777    return a.label.localeCompare(b.label);
778  });
779
780  return items;
781};
782
783/**
784 * Updates context menu with default item.
785 * @private
786 */
787
788FileTasks.prototype.updateMenuItem_ = function() {
789  this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
790      this.tasks_.length > 1);
791};
792
793/**
794 * Creates combobutton item based on task.
795 *
796 * @param {Object} task Task to convert.
797 * @param {string=} opt_title Title.
798 * @param {boolean=} opt_bold Make a menu item bold.
799 * @return {Object} Item appendable to combobutton drop-down list.
800 * @private
801 */
802FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
803                                                      opt_bold) {
804  return {
805    label: opt_title || task.title,
806    iconUrl: task.iconUrl,
807    iconType: task.iconType,
808    task: task,
809    bold: opt_bold || false
810  };
811};
812
813/**
814 * Shows modal action picker dialog with currently available list of tasks.
815 *
816 * @param {DefaultActionDialog} actionDialog Action dialog to show and update.
817 * @param {string} title Title to use.
818 * @param {string} message Message to use.
819 * @param {function(Object)} onSuccess Callback to pass selected task.
820 */
821FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
822                                              onSuccess) {
823  var items = this.createItems_();
824
825  var defaultIdx = 0;
826  for (var j = 0; j < items.length; j++) {
827    if (items[j].task.taskId === this.defaultTask_.taskId)
828      defaultIdx = j;
829  }
830
831  actionDialog.show(
832      title,
833      message,
834      items, defaultIdx,
835      function(item) {
836        onSuccess(item.task);
837      });
838};
839
840/**
841 * Decorates a FileTasks method, so it will be actually executed after the tasks
842 * are available.
843 * This decorator expects an implementation called |method + '_'|.
844 *
845 * @param {string} method The method name.
846 */
847FileTasks.decorate = function(method) {
848  var privateMethod = method + '_';
849  FileTasks.prototype[method] = function() {
850    if (this.tasks_) {
851      this[privateMethod].apply(this, arguments);
852    } else {
853      this.pendingInvocations_.push([privateMethod, arguments]);
854    }
855    return this;
856  };
857};
858
859FileTasks.decorate('display');
860FileTasks.decorate('updateMenuItem');
861FileTasks.decorate('execute');
862FileTasks.decorate('executeDefault');
863