1// Copyright (c) 2011 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// WK Bug 55728 is fixed on the chrome 12 branch but not on the trunk.
6// TODO(rginda): Enable this everywhere once we have a trunk-worthy fix.
7const ENABLE_EXIF_READER = navigator.userAgent.match(/chrome\/12\.0/i);
8
9// Thumbnail view is painful without the exif reader.
10const ENABLE_THUMBNAIL_VIEW = ENABLE_EXIF_READER;
11
12var g_slideshow_data = null;
13
14/**
15 * FileManager constructor.
16 *
17 * FileManager objects encapsulate the functionality of the file selector
18 * dialogs, as well as the full screen file manager application (though the
19 * latter is not yet implemented).
20 *
21 * @param {HTMLElement} dialogDom The DOM node containing the prototypical
22 *     dialog UI.
23 * @param {DOMFileSystem} filesystem The HTML5 filesystem object representing
24 *     the root filesystem for the new FileManager.
25 * @param {Object} params A map of parameter names to values controlling the
26 *     appearance of the FileManager.  Names are:
27 *     - type: A value from FileManager.DialogType defining what kind of
28 *       dialog to present.  Defaults to FULL_PAGE.
29 *     - title: The title for the dialog.  Defaults to a localized string based
30 *       on the dialog type.
31 *     - defaultPath: The default path for the dialog.  The default path should
32 *       end with a trailing slash if it represents a directory.
33 */
34function FileManager(dialogDom, rootEntries, params) {
35  console.log('Init FileManager: ' + dialogDom);
36
37  this.dialogDom_ = dialogDom;
38  this.rootEntries_ = rootEntries;
39  this.filesystem_ = rootEntries[0].filesystem;
40  this.params_ = params || {};
41
42  this.listType_ = null;
43
44  this.exifCache_ = {};
45
46  // True if we should filter out files that start with a dot.
47  this.filterFiles_ = true;
48
49  this.commands_ = {};
50
51  this.document_ = dialogDom.ownerDocument;
52  this.dialogType_ =
53    this.params_.type || FileManager.DialogType.FULL_PAGE;
54
55  this.defaultPath_ = this.params_.defaultPath || '/';
56
57  // This is set to just the directory portion of defaultPath in initDialogType.
58  this.defaultFolder_ = '/';
59
60  this.showCheckboxes_ =
61      (this.dialogType_ == FileManager.DialogType.FULL_PAGE ||
62       this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE);
63
64  // DirectoryEntry representing the current directory of the dialog.
65  this.currentDirEntry_ = null;
66
67  window.addEventListener('popstate', this.onPopState_.bind(this));
68  this.addEventListener('directory-changed',
69                        this.onDirectoryChanged_.bind(this));
70  this.addEventListener('selection-summarized',
71                        this.onSelectionSummarized_.bind(this));
72
73  this.initCommands_();
74  this.initDom_();
75  this.initDialogType_();
76
77  this.summarizeSelection_();
78  this.updatePreview_();
79  this.changeDirectory(this.defaultFolder_);
80
81  chrome.fileBrowserPrivate.onDiskChanged.addListener(
82      this.onDiskChanged_.bind(this));
83
84  this.table_.list_.focus();
85
86  if (ENABLE_EXIF_READER) {
87    this.exifReader = new Worker('js/exif_reader.js');
88    this.exifReader.onmessage = this.onExifReaderMessage_.bind(this);
89    this.exifReader.postMessage({verb: 'init'});
90  }
91}
92
93FileManager.prototype = {
94  __proto__: cr.EventTarget.prototype
95};
96
97// Anonymous "namespace".
98(function() {
99
100  // Private variables and helper functions.
101
102  /**
103   * Unicode codepoint for 'BLACK RIGHT-POINTING SMALL TRIANGLE'.
104   */
105  const RIGHT_TRIANGLE = '\u25b8';
106
107  /**
108   * The DirectoryEntry.fullPath value of the directory containing external
109   * storage volumes.
110   */
111  const MEDIA_DIRECTORY = '/media';
112
113  /**
114   * Translated strings.
115   */
116  var localStrings;
117
118  /**
119   * Map of icon types to regular expressions.
120   *
121   * The first regexp to match the file name determines the icon type
122   * assigned to dom elements for a file.  Order of evaluation is not
123   * defined, so don't depend on it.
124   */
125  const iconTypes = {
126    'audio': /\.(mp3|m4a|oga|ogg|wav)$/i,
127    'html': /\.(html?)$/i,
128    'image': /\.(bmp|gif|jpe?g|ico|png|webp)$/i,
129    'pdf' : /\.(pdf)$/i,
130    'text': /\.(pod|rst|txt|log)$/i,
131    'video': /\.(mov|mp4|m4v|mpe?g4?|ogm|ogv|ogx|webm)$/i
132  };
133
134  const previewArt = {
135    'audio': 'images/filetype_large_audio.png',
136    'folder': 'images/filetype_large_folder.png',
137    'unknown': 'images/filetype_large_generic.png',
138    'video': 'images/filetype_large_video.png'
139  };
140
141  /**
142   * Return a translated string.
143   *
144   * Wrapper function to make dealing with translated strings more concise.
145   * Equivilant to localStrings.getString(id).
146   *
147   * @param {string} id The id of the string to return.
148   * @return {string} The translated string.
149   */
150  function str(id) {
151    return localStrings.getString(id);
152  }
153
154  /**
155   * Return a translated string with arguments replaced.
156   *
157   * Wrapper function to make dealing with translated strings more concise.
158   * Equivilant to localStrings.getStringF(id, ...).
159   *
160   * @param {string} id The id of the string to return.
161   * @param {...string} The values to replace into the string.
162   * @return {string} The translated string with replaced values.
163   */
164  function strf(id, var_args) {
165    return localStrings.getStringF.apply(localStrings, arguments);
166  }
167
168  /**
169   * Checks if |parent_path| is parent file path of |child_path|.
170   *
171   * @param {string} parent_path The parent path.
172   * @param {string} child_path The child path.
173   */
174  function isParentPath(parent_path, child_path) {
175    if (!parent_path || parent_path.length == 0 ||
176        !child_path || child_path.length == 0)
177      return false;
178
179    if (parent_path[parent_path.length -1] != '/')
180      parent_path += '/';
181
182    if (child_path[child_path.length -1] != '/')
183      child_path += '/';
184
185    return child_path.indexOf(parent_path) == 0;
186  }
187
188  /**
189   * Returns parent folder path of file path.
190   *
191   * @param {string} path The file path.
192   */
193  function getParentPath(path) {
194    var parent = path.replace(/[\/]?[^\/]+[\/]?$/,'');
195    if (parent.length == 0)
196      parent = '/';
197    return parent;
198  }
199
200  /**
201   * Get the icon type for a given Entry.
202   *
203   * @param {Entry} entry An Entry subclass (FileEntry or DirectoryEntry).
204   * @return {string} One of the keys from FileManager.iconTypes, or
205   *     'unknown'.
206   */
207  function getIconType(entry) {
208    if (entry.cachedIconType_)
209      return entry.cachedIconType_;
210
211    var rv = 'unknown';
212
213    if (entry.isDirectory) {
214      rv = 'folder';
215    } else {
216      for (var name in iconTypes) {
217        var value = iconTypes[name];
218
219        if (value instanceof RegExp) {
220          if (value.test(entry.name))  {
221            rv = name;
222            break;
223          }
224        } else if (typeof value == 'function') {
225          try {
226            if (value(entry)) {
227              rv = name;
228              break;
229            }
230          } catch (ex) {
231            console.error('Caught exception while evaluating iconType: ' +
232                          name, ex);
233          }
234        } else {
235          console.log('Unexpected value in iconTypes[' + name + ']: ' + value);
236        }
237      }
238    }
239
240    entry.cachedIconType_ = rv;
241    return rv;
242  }
243
244  /**
245   * Call an asynchronous method on dirEntry, batching multiple callers.
246   *
247   * This batches multiple callers into a single invocation, calling all
248   * interested parties back when the async call completes.
249   *
250   * The Entry method to be invoked should take two callbacks as parameters
251   * (one for success and one for failure), and it should invoke those
252   * callbacks with a single parameter representing the result of the call.
253   * Example methods are Entry.getMetadata() and FileEntry.file().
254   *
255   * Warning: Because this method caches the first result, subsequent changes
256   * to the entry will not be visible to callers.
257   *
258   * Error results are never cached.
259   *
260   * @param {DirectoryEntry} dirEntry The DirectoryEntry to apply the method
261   *     to.
262   * @param {string} methodName The name of the method to dispatch.
263   * @param {function(*)} successCallback The function to invoke if the method
264   *     succeeds.  The result of the method will be the one parameter to this
265   *     callback.
266   * @param {function(*)} opt_errorCallback The function to invoke if the
267   *     method fails.  The result of the method will be the one parameter to
268   *     this callback.  If not provided, the default errorCallback will throw
269   *     an exception.
270   */
271  function batchAsyncCall(entry, methodName, successCallback,
272                          opt_errorCallback) {
273    var resultCache = methodName + '_resultCache_';
274
275    if (entry[resultCache]) {
276      // The result cache for this method already exists.  Just invoke the
277      // successCallback with the result of the previuos call.
278      // Callback via a setTimeout so the sync/async semantics don't change
279      // based on whether or not the value is cached.
280      setTimeout(function() { successCallback(entry[resultCache]) }, 0);
281      return;
282    }
283
284    if (!opt_errorCallback) {
285      opt_errorCallback = util.ferr('Error calling ' + methodName + ' for: ' +
286                                    entry.fullPath);
287    }
288
289    var observerList = methodName + '_observers_';
290
291    if (entry[observerList]) {
292      // The observer list already exists, indicating we have a pending call
293      // out to this method.  Add this caller to the list of observers and
294      // bail out.
295      entry[observerList].push([successCallback, opt_errorCallback]);
296      return;
297    }
298
299    entry[observerList] = [[successCallback, opt_errorCallback]];
300
301    function onComplete(success, result) {
302      if (success)
303        entry[resultCache] = result;
304
305      for (var i = 0; i < entry[observerList].length; i++) {
306        entry[observerList][i][success ? 0 : 1](result);
307      }
308
309      delete entry[observerList];
310    };
311
312    entry[methodName](function(rv) { onComplete(true, rv) },
313                      function(rv) { onComplete(false, rv) });
314  }
315
316  /**
317   * Get the size of a file, caching the result.
318   *
319   * When this method completes, the fileEntry object will get a
320   * 'cachedSize_' property (if it doesn't already have one) containing the
321   * size of the file in bytes.
322   *
323   * @param {Entry} entry An HTML5 Entry object.
324   * @param {function(Entry)} successCallback The function to invoke once the
325   *     file size is known.
326   */
327  function cacheEntrySize(entry, successCallback) {
328    if (entry.isDirectory) {
329      // No size for a directory, -1 ensures it's sorted before 0 length files.
330      entry.cachedSize_ = -1;
331    }
332
333    if ('cachedSize_' in entry) {
334      if (successCallback) {
335        // Callback via a setTimeout so the sync/async semantics don't change
336        // based on whether or not the value is cached.
337        setTimeout(function() { successCallback(entry) }, 0);
338      }
339      return;
340    }
341
342    batchAsyncCall(entry, 'file', function(file) {
343      entry.cachedSize_ = file.size;
344      if (successCallback)
345        successCallback(entry);
346    });
347  }
348
349  /**
350   * Get the mtime of a file, caching the result.
351   *
352   * When this method completes, the fileEntry object will get a
353   * 'cachedMtime_' property (if it doesn't already have one) containing the
354   * last modified time of the file as a Date object.
355   *
356   * @param {Entry} entry An HTML5 Entry object.
357   * @param {function(Entry)} successCallback The function to invoke once the
358   *     mtime is known.
359   */
360  function cacheEntryDate(entry, successCallback) {
361    if ('cachedMtime_' in entry) {
362      if (successCallback) {
363        // Callback via a setTimeout so the sync/async semantics don't change
364        // based on whether or not the value is cached.
365        setTimeout(function() { successCallback(entry) }, 0);
366      }
367      return;
368    }
369
370    if (entry.isFile) {
371      batchAsyncCall(entry, 'file', function(file) {
372        entry.cachedMtime_ = file.lastModifiedDate;
373        if (successCallback)
374          successCallback(entry);
375      });
376    } else {
377      batchAsyncCall(entry, 'getMetadata', function(metadata) {
378        entry.cachedMtime_ = metadata.modificationTime;
379        if (successCallback)
380          successCallback(entry);
381      });
382    }
383  }
384
385  /**
386   * Get the icon type of a file, caching the result.
387   *
388   * When this method completes, the fileEntry object will get a
389   * 'cachedIconType_' property (if it doesn't already have one) containing the
390   * icon type of the file as a string.
391   *
392   * The successCallback is always invoked synchronously, since this does not
393   * actually require an async call.  You should not depend on this, as it may
394   * change if we were to start reading magic numbers (for example).
395   *
396   * @param {Entry} entry An HTML5 Entry object.
397   * @param {function(Entry)} successCallback The function to invoke once the
398   *     icon type is known.
399   */
400  function cacheEntryIconType(entry, successCallback) {
401    getIconType(entry);
402    if (successCallback)
403      setTimeout(function() { successCallback(entry) }, 0);
404  }
405
406  // Public statics.
407
408  /**
409   * List of dialog types.
410   *
411   * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
412   * FULL_PAGE which is specific to this code.
413   *
414   * @enum {string}
415   */
416  FileManager.DialogType = {
417    SELECT_FOLDER: 'folder',
418    SELECT_SAVEAS_FILE: 'saveas-file',
419    SELECT_OPEN_FILE: 'open-file',
420    SELECT_OPEN_MULTI_FILE: 'open-multi-file',
421    FULL_PAGE: 'full-page'
422  };
423
424  FileManager.ListType = {
425    DETAIL: 'detail',
426    THUMBNAIL: 'thumb'
427  };
428
429  /**
430   * Load translated strings.
431   */
432  FileManager.initStrings = function(callback) {
433    chrome.fileBrowserPrivate.getStrings(function(strings) {
434      localStrings = new LocalStrings(strings);
435      cr.initLocale(strings);
436
437      if (callback)
438        callback();
439    });
440  };
441
442  // Instance methods.
443
444  /**
445   * One-time initialization of commands.
446   */
447  FileManager.prototype.initCommands_ = function() {
448    var commands = this.dialogDom_.querySelectorAll('command');
449    for (var i = 0; i < commands.length; i++) {
450      var command = commands[i];
451      cr.ui.Command.decorate(command);
452      this.commands_[command.id] = command;
453    }
454
455    this.fileContextMenu_ = this.dialogDom_.querySelector('.file-context-menu');
456    cr.ui.Menu.decorate(this.fileContextMenu_);
457
458    this.document_.addEventListener(
459        'canExecute', this.onRenameCanExecute_.bind(this));
460    this.document_.addEventListener(
461        'canExecute', this.onDeleteCanExecute_.bind(this));
462
463    this.document_.addEventListener('command', this.onCommand_.bind(this));
464  }
465
466  /**
467   * One-time initialization of various DOM nodes.
468   */
469  FileManager.prototype.initDom_ = function() {
470    // Cache nodes we'll be manipulating.
471    this.previewImage_ = this.dialogDom_.querySelector('.preview-img');
472    this.previewFilename_ = this.dialogDom_.querySelector('.preview-filename');
473    this.previewSummary_ = this.dialogDom_.querySelector('.preview-summary');
474    this.filenameInput_ = this.dialogDom_.querySelector('.filename-input');
475    this.taskButtons_ = this.dialogDom_.querySelector('.task-buttons');
476    this.okButton_ = this.dialogDom_.querySelector('.ok');
477    this.cancelButton_ = this.dialogDom_.querySelector('.cancel');
478    this.newFolderButton_ = this.dialogDom_.querySelector('.new-folder');
479
480    this.renameInput_ = this.document_.createElement('input');
481    this.renameInput_.className = 'rename';
482
483    this.renameInput_.addEventListener(
484        'keydown', this.onRenameInputKeyDown_.bind(this));
485    this.renameInput_.addEventListener(
486        'blur', this.onRenameInputBlur_.bind(this));
487
488    this.filenameInput_.addEventListener(
489        'keyup', this.onFilenameInputKeyUp_.bind(this));
490    this.filenameInput_.addEventListener(
491        'focus', this.onFilenameInputFocus_.bind(this));
492
493    this.dialogDom_.addEventListener('keydown', this.onKeyDown_.bind(this));
494    this.okButton_.addEventListener('click', this.onOk_.bind(this));
495    this.cancelButton_.addEventListener('click', this.onCancel_.bind(this));
496
497    this.dialogDom_.querySelector('button.new-folder').addEventListener(
498        'click', this.onNewFolderButtonClick_.bind(this));
499
500    if (ENABLE_THUMBNAIL_VIEW) {
501      this.dialogDom_.querySelector('button.detail-view').addEventListener(
502          'click', this.onDetailViewButtonClick_.bind(this));
503      this.dialogDom_.querySelector('button.thumbnail-view').addEventListener(
504          'click', this.onThumbnailViewButtonClick_.bind(this));
505    } else {
506      this.dialogDom_.querySelector(
507          'button.detail-view').style.display = 'none';
508      this.dialogDom_.querySelector(
509          'button.thumbnail-view').style.display = 'none';
510    }
511
512    this.dialogDom_.ownerDocument.defaultView.addEventListener(
513        'resize', this.onResize_.bind(this));
514
515    var ary = this.dialogDom_.querySelectorAll('[visibleif]');
516    for (var i = 0; i < ary.length; i++) {
517      var expr = ary[i].getAttribute('visibleif');
518      if (!eval(expr))
519        ary[i].style.display = 'none';
520    }
521
522    // Populate the static localized strings.
523    i18nTemplate.process(this.document_, localStrings.templateData);
524
525    // Always sharing the data model between the detail/thumb views confuses
526    // them.  Instead we maintain this bogus data model, and hook it up to the
527    // view that is not in use.
528    this.emptyDataModel_ = new cr.ui.table.TableDataModel([]);
529
530    this.dataModel_ = new cr.ui.table.TableDataModel([]);
531    this.dataModel_.sort('name');
532    this.dataModel_.addEventListener('sorted',
533                                this.onDataModelSorted_.bind(this));
534    this.dataModel_.prepareSort = this.prepareSort_.bind(this);
535
536    if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE ||
537        this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FOLDER ||
538        this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) {
539      this.selectionModelClass_ = cr.ui.table.TableSingleSelectionModel;
540    } else {
541      this.selectionModelClass_ = cr.ui.table.TableSelectionModel;
542    }
543
544    this.initTable_();
545    this.initGrid_();
546
547    this.setListType(FileManager.ListType.DETAIL);
548
549    this.onResize_();
550    this.dialogDom_.style.opacity = '1';
551  };
552
553  /**
554   * Force the canExecute events to be dispatched.
555   */
556  FileManager.prototype.updateCommands_ = function() {
557    this.commands_['rename'].canExecuteChange();
558    this.commands_['delete'].canExecuteChange();
559  };
560
561  /**
562   * Invoked to decide whether the "rename" command can be executed.
563   */
564  FileManager.prototype.onRenameCanExecute_ = function(event) {
565    event.canExecute =
566        (// Full page mode.
567         this.dialogType_ == FileManager.DialogType.FULL_PAGE &&
568         // Rename not in progress.
569         !this.renameInput_.currentEntry &&
570         // Not in root directory.
571         this.currentDirEntry_.fullPath != '/' &&
572         // Not in media directory.
573         this.currentDirEntry_.fullPath != MEDIA_DIRECTORY &&
574         // Only one file selected.
575         this.selection.totalCount == 1);
576  };
577
578  /**
579   * Invoked to decide whether the "delete" command can be executed.
580   */
581  FileManager.prototype.onDeleteCanExecute_ = function(event) {
582    event.canExecute =
583        (// Full page mode.
584         this.dialogType_ == FileManager.DialogType.FULL_PAGE &&
585         // Rename not in progress.
586         !this.renameInput_.currentEntry &&
587         // Not in root directory.
588         this.currentDirEntry_.fullPath != '/' &&
589         // Not in media directory.
590         this.currentDirEntry_.fullPath != MEDIA_DIRECTORY);
591  };
592
593  FileManager.prototype.setListType = function(type) {
594    if (type && type == this.listType_)
595      return;
596
597    if (type == FileManager.ListType.DETAIL) {
598      this.table_.dataModel = this.dataModel_;
599      this.table_.style.display = '';
600      this.grid_.style.display = 'none';
601      this.grid_.dataModel = this.emptyDataModel_;
602      this.currentList_ = this.table_;
603      this.dialogDom_.querySelector('button.detail-view').disabled = true;
604      this.dialogDom_.querySelector('button.thumbnail-view').disabled = false;
605    } else if (type == FileManager.ListType.THUMBNAIL) {
606      this.grid_.dataModel = this.dataModel_;
607      this.grid_.style.display = '';
608      this.table_.style.display = 'none';
609      this.table_.dataModel = this.emptyDataModel_;
610      this.currentList_ = this.grid_;
611      this.dialogDom_.querySelector('button.thumbnail-view').disabled = true;
612      this.dialogDom_.querySelector('button.detail-view').disabled = false;
613    } else {
614      throw new Error('Unknown list type: ' + type);
615    }
616
617    this.listType_ = type;
618    this.onResize_();
619  };
620
621  /**
622   * Initialize the file thumbnail grid.
623   */
624  FileManager.prototype.initGrid_ = function() {
625    this.grid_ = this.dialogDom_.querySelector('.thumbnail-grid');
626    cr.ui.Grid.decorate(this.grid_);
627
628    var self = this;
629    this.grid_.itemConstructor = function(entry) {
630      return self.renderThumbnail_(entry);
631    };
632
633    this.grid_.selectionModel = new this.selectionModelClass_();
634
635    this.grid_.addEventListener(
636        'dblclick', this.onDetailDoubleClick_.bind(this));
637    this.grid_.selectionModel.addEventListener(
638        'change', this.onSelectionChanged_.bind(this));
639
640    if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
641      cr.ui.contextMenuHandler.addContextMenuProperty(this.grid_);
642      this.grid_.contextMenu = this.fileContextMenu_;
643    }
644
645    this.grid_.addEventListener('mousedown',
646                                this.onGridMouseDown_.bind(this));
647  };
648
649  /**
650   * Initialize the file list table.
651   */
652  FileManager.prototype.initTable_ = function() {
653    var checkWidth = this.showCheckboxes_ ? 5 : 0;
654
655    var columns = [
656        new cr.ui.table.TableColumn('cachedIconType_', '',
657                                    5.4 + checkWidth),
658        new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'),
659                                    64 - checkWidth),
660        new cr.ui.table.TableColumn('cachedSize_',
661                                    str('SIZE_COLUMN_LABEL'), 15.5),
662        new cr.ui.table.TableColumn('cachedMtime_',
663                                    str('DATE_COLUMN_LABEL'), 21)
664    ];
665
666    columns[0].renderFunction = this.renderIconType_.bind(this);
667    columns[1].renderFunction = this.renderName_.bind(this);
668    columns[2].renderFunction = this.renderSize_.bind(this);
669    columns[3].renderFunction = this.renderDate_.bind(this);
670
671    this.table_ = this.dialogDom_.querySelector('.detail-table');
672    cr.ui.Table.decorate(this.table_);
673
674    this.table_.selectionModel = new this.selectionModelClass_();
675    this.table_.columnModel = new cr.ui.table.TableColumnModel(columns);
676
677    this.table_.addEventListener(
678        'dblclick', this.onDetailDoubleClick_.bind(this));
679    this.table_.selectionModel.addEventListener(
680        'change', this.onSelectionChanged_.bind(this));
681
682    if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
683      cr.ui.contextMenuHandler.addContextMenuProperty(this.table_);
684      this.table_.contextMenu = this.fileContextMenu_;
685    }
686
687    this.table_.addEventListener('mousedown',
688                                 this.onTableMouseDown_.bind(this));
689  };
690
691  /**
692   * Respond to a command being executed.
693   */
694  FileManager.prototype.onCommand_ = function(event) {
695    switch (event.command.id) {
696      case 'rename':
697        var leadIndex = this.currentList_.selectionModel.leadIndex;
698        var li = this.currentList_.getListItemByIndex(leadIndex);
699        var label = li.querySelector('.filename-label');
700        if (!label) {
701          console.warn('Unable to find label for rename of index: ' +
702                       leadIndex);
703          return;
704        }
705
706        this.initiateRename_(label);
707        break;
708
709      case 'delete':
710        this.deleteEntries(this.selection.entries);
711        break;
712    }
713  };
714
715  /**
716   * Respond to the back button.
717   */
718  FileManager.prototype.onPopState_ = function(event) {
719    this.changeDirectory(event.state, false);
720  };
721
722  /**
723   * Resize details and thumb views to fit the new window size.
724   */
725  FileManager.prototype.onResize_ = function() {
726    this.table_.style.height = this.grid_.style.height =
727      this.grid_.parentNode.clientHeight + 'px';
728    this.table_.style.width = this.grid_.style.width =
729      this.grid_.parentNode.clientWidth + 'px';
730
731    this.table_.list_.style.width = this.table_.parentNode.clientWidth + 'px';
732    this.table_.list_.style.height = (this.table_.clientHeight - 1 -
733                                      this.table_.header_.clientHeight) + 'px';
734
735    if (this.listType_ == FileManager.ListType.THUMBNAIL) {
736      var self = this;
737      setTimeout(function () {
738          self.grid_.columns = 0;
739          self.grid_.redraw();
740      }, 0);
741    } else {
742      this.currentList_.redraw();
743    }
744  };
745
746  /**
747   * Tweak the UI to become a particular kind of dialog, as determined by the
748   * dialog type parameter passed to the constructor.
749   */
750  FileManager.prototype.initDialogType_ = function() {
751    var defaultTitle;
752    var okLabel = str('OPEN_LABEL');
753
754    // Split the dirname from the basename.
755    var ary = this.defaultPath_.match(/^(.*?)(?:\/([^\/]+))?$/);
756    var defaultFolder;
757    var defaultTarget;
758
759    if (!ary) {
760      console.warn('Unable to split defaultPath: ' + defaultPath);
761      ary = [];
762    }
763
764    switch (this.dialogType_) {
765      case FileManager.DialogType.SELECT_FOLDER:
766        defaultTitle = str('SELECT_FOLDER_TITLE');
767        defaultFolder = ary[1] || '/';
768        defaultTarget = ary[2] || '';
769        break;
770
771      case FileManager.DialogType.SELECT_OPEN_FILE:
772        defaultTitle = str('SELECT_OPEN_FILE_TITLE');
773        defaultFolder = ary[1] || '/';
774        defaultTarget = '';
775
776        if (ary[2]) {
777          console.warn('Open should NOT have provided a default ' +
778                       'filename: ' + ary[2]);
779        }
780        break;
781
782      case FileManager.DialogType.SELECT_OPEN_MULTI_FILE:
783        defaultTitle = str('SELECT_OPEN_MULTI_FILE_TITLE');
784        defaultFolder = ary[1] || '/';
785        defaultTarget = '';
786
787        if (ary[2]) {
788          console.warn('Multi-open should NOT have provided a default ' +
789                       'filename: ' + ary[2]);
790        }
791        break;
792
793      case FileManager.DialogType.SELECT_SAVEAS_FILE:
794        defaultTitle = str('SELECT_SAVEAS_FILE_TITLE');
795        okLabel = str('SAVE_LABEL');
796
797        defaultFolder = ary[1] || '/';
798        defaultTarget = ary[2] || '';
799        if (!defaultTarget)
800          console.warn('Save-as should have provided a default filename.');
801        break;
802
803      case FileManager.DialogType.FULL_PAGE:
804        defaultFolder = ary[1] || '/';
805        defaultTarget = ary[2] || '';
806        break;
807
808      default:
809        throw new Error('Unknown dialog type: ' + this.dialogType_);
810    }
811
812    this.okButton_.textContent = okLabel;
813
814    dialogTitle = this.params_.title || defaultTitle;
815    this.dialogDom_.querySelector('.dialog-title').textContent = dialogTitle;
816
817    ary = defaultFolder.match(/^\/home\/[^\/]+\/user\/Downloads(\/.*)?$/);
818    if (ary) {
819        // Chrome will probably suggest the full path to Downloads, but
820        // we're working with 'virtual paths', so we have to translate.
821        // TODO(rginda): Maybe chrome should have suggested the correct place
822        // to begin with, but that would mean it would have to treat the
823        // file manager dialogs differently than the native ones.
824        defaultFolder = '/Downloads' + (ary[1] || '');
825      }
826
827    this.defaultFolder_ = defaultFolder;
828    this.filenameInput_.value = defaultTarget;
829  };
830
831  /**
832   * Cache necessary data before a sort happens.
833   *
834   * This is called by the table code before a sort happens, so that we can
835   * go fetch data for the sort field that we may not have yet.
836   */
837  FileManager.prototype.prepareSort_ = function(field, callback) {
838    var cacheFunction;
839
840    if (field == 'cachedMtime_') {
841      cacheFunction = cacheEntryDate;
842    } else if (field == 'cachedSize_') {
843      cacheFunction = cacheEntrySize;
844    } else if (field == 'cachedIconType_') {
845      cacheFunction = cacheEntryIconType;
846    } else {
847      callback();
848      return;
849    }
850
851    function checkCount() {
852      if (uncachedCount == 0) {
853        // Callback via a setTimeout so the sync/async semantics don't change
854        // based on whether or not the value is cached.
855        setTimeout(callback, 0);
856      }
857    }
858
859    var dataModel = this.dataModel_;
860    var uncachedCount = dataModel.length;
861
862    for (var i = uncachedCount - 1; i >= 0 ; i--) {
863      var entry = dataModel.item(i);
864      if (field in entry) {
865        uncachedCount--;
866      } else {
867        cacheFunction(entry, function() {
868          uncachedCount--;
869          checkCount();
870        });
871      }
872    }
873
874    checkCount();
875  }
876
877  /**
878   * Render (and wire up) a checkbox to be used in either a detail or a
879   * thumbnail list item.
880   */
881  FileManager.prototype.renderCheckbox_ = function(entry) {
882    var input = this.document_.createElement('input');
883    input.setAttribute('type', 'checkbox');
884    input.className = 'file-checkbox';
885    input.addEventListener('mousedown',
886                           this.onCheckboxMouseDownUp_.bind(this));
887    input.addEventListener('mouseup',
888                           this.onCheckboxMouseDownUp_.bind(this));
889    input.addEventListener('click',
890                           this.onCheckboxClick_.bind(this));
891
892    if (this.selection && this.selection.entries.indexOf(entry) != -1) {
893      // Our DOM nodes get discarded as soon as we're scrolled out of view,
894      // so we have to make sure the check state is correct when we're brought
895      // back to life.
896      input.checked = true;
897    }
898
899    return input;
900  }
901
902  FileManager.prototype.renderThumbnail_ = function(entry) {
903    var li = this.document_.createElement('li');
904    li.className = 'thumbnail-item';
905
906    if (this.showCheckboxes_)
907      li.appendChild(this.renderCheckbox_(entry));
908
909    var div = this.document_.createElement('div');
910    div.className = 'img-container';
911    li.appendChild(div);
912
913    var img = this.document_.createElement('img');
914    this.getThumbnailURL(entry, function(type, url) { img.src = url });
915    div.appendChild(img);
916
917    div = this.document_.createElement('div');
918    div.className = 'filename-label';
919    var labelText = entry.name;
920    if (this.currentDirEntry_.name == '')
921      labelText = this.getLabelForRootPath_(labelText);
922
923    div.textContent = labelText;
924    div.entry = entry;
925
926    li.appendChild(div);
927
928    cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
929    cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);
930    return li;
931  }
932
933  /**
934   * Render the type column of the detail table.
935   *
936   * Invoked by cr.ui.Table when a file needs to be rendered.
937   *
938   * @param {Entry} entry The Entry object to render.
939   * @param {string} columnId The id of the column to be rendered.
940   * @param {cr.ui.Table} table The table doing the rendering.
941   */
942  FileManager.prototype.renderIconType_ = function(entry, columnId, table) {
943    var div = this.document_.createElement('div');
944    div.className = 'detail-icon-container';
945
946    if (this.showCheckboxes_)
947      div.appendChild(this.renderCheckbox_(entry));
948
949    var icon = this.document_.createElement('div');
950    icon.className = 'detail-icon';
951    entry.cachedIconType_ = getIconType(entry);
952    icon.setAttribute('iconType', entry.cachedIconType_);
953    div.appendChild(icon);
954
955    return div;
956  };
957
958  FileManager.prototype.getLabelForRootPath_ = function(path) {
959    // This hack lets us localize the top level directories.
960    if (path == 'Downloads')
961      return str('DOWNLOADS_DIRECTORY_LABEL');
962
963    if (path == 'media')
964      return str('MEDIA_DIRECTORY_LABEL');
965
966    return path || str('ROOT_DIRECTORY_LABEL');
967  };
968
969  /**
970   * Render the Name column of the detail table.
971   *
972   * Invoked by cr.ui.Table when a file needs to be rendered.
973   *
974   * @param {Entry} entry The Entry object to render.
975   * @param {string} columnId The id of the column to be rendered.
976   * @param {cr.ui.Table} table The table doing the rendering.
977   */
978  FileManager.prototype.renderName_ = function(entry, columnId, table) {
979    var label = this.document_.createElement('div');
980    label.entry = entry;
981    label.className = 'filename-label';
982    if (this.currentDirEntry_.name == '') {
983      label.textContent = this.getLabelForRootPath_(entry.name);
984    } else {
985      label.textContent = entry.name;
986    }
987
988    return label;
989  };
990
991  /**
992   * Render the Size column of the detail table.
993   *
994   * @param {Entry} entry The Entry object to render.
995   * @param {string} columnId The id of the column to be rendered.
996   * @param {cr.ui.Table} table The table doing the rendering.
997   */
998  FileManager.prototype.renderSize_ = function(entry, columnId, table) {
999    var div = this.document_.createElement('div');
1000    div.className = 'detail-size';
1001
1002    div.textContent = '...';
1003    cacheEntrySize(entry, function(entry) {
1004      if (entry.cachedSize_ == -1) {
1005        div.textContent = '';
1006      } else {
1007        div.textContent = cr.locale.bytesToSi(entry.cachedSize_);
1008      }
1009    });
1010
1011    return div;
1012  };
1013
1014  /**
1015   * Render the Date column of the detail table.
1016   *
1017   * @param {Entry} entry The Entry object to render.
1018   * @param {string} columnId The id of the column to be rendered.
1019   * @param {cr.ui.Table} table The table doing the rendering.
1020   */
1021  FileManager.prototype.renderDate_ = function(entry, columnId, table) {
1022    var div = this.document_.createElement('div');
1023    div.className = 'detail-date';
1024
1025    div.textContent = '...';
1026
1027    var self = this;
1028    cacheEntryDate(entry, function(entry) {
1029      if (self.currentDirEntry_.fullPath == MEDIA_DIRECTORY &&
1030          entry.cachedMtime_.getTime() == 0) {
1031        // Mount points for FAT volumes have this time associated with them.
1032        // We'd rather display nothing than this bogus date.
1033        div.textContent = '---';
1034      } else {
1035        div.textContent = cr.locale.formatDate(entry.cachedMtime_,
1036                                               str('LOCALE_FMT_DATE_SHORT'));
1037      }
1038    });
1039
1040    return div;
1041  };
1042
1043  /**
1044   * Compute summary information about the current selection.
1045   *
1046   * This method dispatches the 'selection-summarized' event when it completes.
1047   * Depending on how many of the selected files already have known sizes, the
1048   * dispatch may happen immediately, or after a number of async calls complete.
1049   */
1050  FileManager.prototype.summarizeSelection_ = function() {
1051    var selection = this.selection = {
1052      entries: [],
1053      urls: [],
1054      leadEntry: null,
1055      totalCount: 0,
1056      fileCount: 0,
1057      directoryCount: 0,
1058      bytes: 0,
1059      iconType: null,
1060      indexes: this.currentList_.selectionModel.selectedIndexes
1061    };
1062
1063    this.previewSummary_.textContent = str('COMPUTING_SELECTION');
1064    this.taskButtons_.innerHTML = '';
1065
1066    if (!selection.indexes.length) {
1067      cr.dispatchSimpleEvent(this, 'selection-summarized');
1068      return;
1069    }
1070
1071    var fileCount = 0;
1072    var byteCount = 0;
1073    var pendingFiles = [];
1074
1075    for (var i = 0; i < selection.indexes.length; i++) {
1076      var entry = this.dataModel_.item(selection.indexes[i]);
1077
1078      selection.entries.push(entry);
1079      selection.urls.push(entry.toURL());
1080
1081      if (selection.iconType == null) {
1082        selection.iconType = getIconType(entry);
1083      } else if (selection.iconType != 'unknown') {
1084        var iconType = getIconType(entry);
1085        if (selection.iconType != iconType)
1086          selection.iconType = 'unknown';
1087      }
1088
1089      selection.totalCount++;
1090
1091      if (entry.isFile) {
1092        if (!('cachedSize_' in entry)) {
1093          // Any file that hasn't been rendered may be missing its cachedSize_
1094          // property.  For example, visit a large file list, and press ctrl-a
1095          // to select all.  In this case, we need to asynchronously get the
1096          // sizes for these files before telling the world the selection has
1097          // been summarized.  See the 'computeNextFile' logic below.
1098          pendingFiles.push(entry);
1099          continue;
1100        } else {
1101          selection.bytes += entry.cachedSize_;
1102        }
1103        selection.fileCount += 1;
1104      } else {
1105        selection.directoryCount += 1;
1106      }
1107    }
1108
1109    var leadIndex = this.currentList_.selectionModel.leadIndex;
1110    if (leadIndex > -1) {
1111      selection.leadEntry = this.dataModel_.item(leadIndex);
1112    } else {
1113      selection.leadEntry = selection.entries[0];
1114    }
1115
1116    var self = this;
1117
1118    function cacheNextFile(fileEntry) {
1119      if (fileEntry) {
1120        // We're careful to modify the 'selection', rather than 'self.selection'
1121        // here, just in case the selection has changed since this summarization
1122        // began.
1123        selection.bytes += fileEntry.cachedSize_;
1124      }
1125
1126      if (pendingFiles.length) {
1127        cacheEntrySize(pendingFiles.pop(), cacheNextFile);
1128      } else {
1129        self.dispatchEvent(new cr.Event('selection-summarized'));
1130      }
1131    };
1132
1133    if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
1134      chrome.fileBrowserPrivate.getFileTasks(selection.urls,
1135                                             this.onTasksFound_.bind(this));
1136    }
1137
1138    cacheNextFile();
1139  };
1140
1141  FileManager.prototype.onExifGiven_ = function(fileURL, metadata) {
1142    var observers = this.exifCache_[fileURL];
1143    if (!observers || !(observers instanceof Array)) {
1144      console.error('Missing or invalid exif observers: ' + fileURL + ': ' +
1145                    observers);
1146      return;
1147    }
1148
1149    for (var i = 0; i < observers.length; i++) {
1150      observers[i](metadata);
1151    }
1152
1153    this.exifCache_[fileURL] = metadata;
1154  };
1155
1156  FileManager.prototype.onExifError_ = function(fileURL, step, error) {
1157    console.warn('Exif error: ' + fileURL + ': ' + step + ': ' + error);
1158    this.onExifGiven_(fileURL, {});
1159  };
1160
1161  FileManager.prototype.onExifReaderMessage_ = function(event) {
1162    var data = event.data;
1163    var self = this;
1164
1165    function fwd(methodName, args) { self[methodName].apply(self, args) };
1166
1167    switch (data.verb) {
1168      case 'log':
1169        console.log.apply(console, ['exif:'].concat(data.arguments));
1170        break;
1171
1172      case 'give-exif':
1173        fwd('onExifGiven_', data.arguments);
1174        break;
1175
1176      case 'give-exif-error':
1177        fwd('onExifError_', data.arguments);
1178        break;
1179
1180      default:
1181        console.log('Unknown message from exif reader: ' + data.verb, data);
1182        break;
1183    }
1184  };
1185
1186  FileManager.prototype.onTasksFound_ = function(tasksList) {
1187    this.taskButtons_.innerHTML = '';
1188    for (var i = 0; i < tasksList.length; i++) {
1189      var task = tasksList[i];
1190
1191      // Tweak images, titles of internal tasks.
1192      var task_parts = task.taskId.split('|');
1193      if (task_parts[0] == this.getExtensionId_()) {
1194        if (task_parts[1] == 'preview') {
1195          // TODO(serya): This hack needed until task.iconUrl get working
1196          //              (see GetFileTasksFileBrowserFunction::RunImpl).
1197          task.iconUrl =
1198              chrome.extension.getURL('images/icon_preview_16x16.png');
1199          task.title = str('PREVIEW_IMAGE');
1200        } else if (task_parts[1] == 'play') {
1201          task.iconUrl =
1202              chrome.extension.getURL('images/icon_play_16x16.png');
1203          task.title = str('PLAY_MEDIA').replace("&", "");
1204        } else if (task_parts[1] == 'enqueue') {
1205          task.iconUrl =
1206              chrome.extension.getURL('images/icon_add_to_queue_16x16.png');
1207          task.title = str('ENQUEUE');
1208        }
1209      }
1210
1211      var button = this.document_.createElement('button');
1212      button.addEventListener('click', this.onTaskButtonClicked_.bind(this));
1213      button.className = 'task-button';
1214      button.task = task;
1215
1216      var img = this.document_.createElement('img');
1217      img.src = task.iconUrl;
1218
1219      button.appendChild(img);
1220      button.appendChild(this.document_.createTextNode(task.title));
1221
1222      this.taskButtons_.appendChild(button);
1223    }
1224  };
1225
1226  FileManager.prototype.getExtensionId_ = function() {
1227    return chrome.extension.getURL('').split('/')[2];
1228  };
1229
1230  FileManager.prototype.onTaskButtonClicked_ = function(event) {
1231    // Check internal tasks first.
1232    var task_parts = event.srcElement.task.taskId.split('|');
1233    if (task_parts[0] == this.getExtensionId_()) {
1234      if (task_parts[1] == 'preview') {
1235        g_slideshow_data = this.selection.urls;
1236        chrome.tabs.create({url: "slideshow.html"});
1237      } else if (task_parts[1] == 'play') {
1238        chrome.fileBrowserPrivate.viewFiles(this.selection.urls,
1239            event.srcElement.task.taskId);
1240      } else if (task_parts[1] == 'enqueue') {
1241        chrome.fileBrowserPrivate.viewFiles(this.selection.urls,
1242            event.srcElement.task.taskId);
1243      }
1244      return;
1245    }
1246
1247    chrome.fileBrowserPrivate.executeTask(event.srcElement.task.taskId,
1248                                          this.selection.urls);
1249  }
1250
1251  /**
1252   * Update the breadcrumb display to reflect the current directory.
1253   */
1254  FileManager.prototype.updateBreadcrumbs_ = function() {
1255    var bc = this.dialogDom_.querySelector('.breadcrumbs');
1256    bc.innerHTML = '';
1257
1258    var fullPath = this.currentDirEntry_.fullPath.replace(/\/$/, '');
1259    var pathNames = fullPath.split('/');
1260    var path = '';
1261
1262    for (var i = 0; i < pathNames.length; i++) {
1263      var pathName = pathNames[i];
1264      path += pathName + '/';
1265
1266      var div = this.document_.createElement('div');
1267      div.className = 'breadcrumb-path';
1268      if (i <= 1) {
1269        // i == 0: root directory itself, i == 1: the files it contains.
1270        div.textContent = this.getLabelForRootPath_(pathName);
1271      } else {
1272        div.textContent = pathName;
1273      }
1274
1275      div.path = path;
1276      div.addEventListener('click', this.onBreadcrumbClick_.bind(this));
1277
1278      bc.appendChild(div);
1279
1280      if (i == pathNames.length - 1) {
1281        div.classList.add('breadcrumb-last');
1282      } else {
1283        var spacer = this.document_.createElement('div');
1284        spacer.className = 'breadcrumb-spacer';
1285        spacer.textContent = RIGHT_TRIANGLE;
1286        bc.appendChild(spacer);
1287      }
1288    }
1289  };
1290
1291  /**
1292   * Update the preview panel to display a given entry.
1293   *
1294   * The selection summary line is handled by the onSelectionSummarized handler
1295   * rather than this function, because summarization may not complete quickly.
1296   */
1297  FileManager.prototype.updatePreview_ = function() {
1298    // Clear the preview image first, in case the thumbnail takes long to load.
1299    this.previewImage_.src = '';
1300    // The transparent-background class is used to display the checkerboard
1301    // background for image thumbnails.  We don't want to display it for
1302    // non-thumbnail preview images.
1303    this.previewImage_.classList.remove('transparent-background');
1304    // The multiple-selected class indicates that more than one entry is
1305    // selcted.
1306    this.previewImage_.classList.remove('multiple-selected');
1307
1308    if (!this.selection.totalCount) {
1309      this.previewFilename_.textContent = '';
1310      return;
1311    }
1312
1313    var previewName = this.selection.leadEntry.name;
1314    if (this.currentDirEntry_.name == '')
1315      previewName = this.getLabelForRootPath_(previewName);
1316
1317    this.previewFilename_.textContent = previewName;
1318
1319    var iconType = getIconType(this.selection.leadEntry);
1320    if (iconType == 'image') {
1321      if (fileManager.selection.totalCount > 1)
1322        this.previewImage_.classList.add('multiple-selected');
1323    }
1324
1325    var self = this;
1326    var leadEntry = this.selection.leadEntry;
1327
1328    this.getThumbnailURL(this.selection.leadEntry, function(iconType, url) {
1329      if (self.selection.leadEntry != leadEntry) {
1330        // Selection has changed since we asked, nevermind.
1331        return;
1332      }
1333
1334      if (url) {
1335        self.previewImage_.src = url;
1336        if (iconType == 'image')
1337          self.previewImage_.classList.add('transparent-background');
1338      } else {
1339        self.previewImage_.src = previewArt['unknown'];
1340      }
1341    });
1342  };
1343
1344  FileManager.prototype.cacheExifMetadata_ = function(entry, callback) {
1345    var url = entry.toURL();
1346    var cacheValue = this.exifCache_[url];
1347
1348    if (!cacheValue) {
1349      // This is the first time anyone's asked, go get it.
1350      this.exifCache_[url] = [callback];
1351      this.exifReader.postMessage({verb: 'get-exif',
1352                                   arguments: [entry.toURL()]});
1353      return;
1354    }
1355
1356    if (cacheValue instanceof Array) {
1357      // Something is already pending, add to the list of observers.
1358      cacheValue.push(callback);
1359      return;
1360    }
1361
1362    if (cacheValue instanceof Object) {
1363      // We already know the answer, let the caller know in a fresh call stack.
1364      setTimeout(function() { callback(cacheValue) });
1365      return;
1366    }
1367
1368    console.error('Unexpected exif cache value:' + cacheValue);
1369  };
1370
1371  FileManager.prototype.getThumbnailURL = function(entry, callback) {
1372    if (!entry)
1373      return;
1374
1375    var iconType = getIconType(entry);
1376    if (iconType != 'image') {
1377      // Not an image, display a canned clip-art graphic.
1378      if (!(iconType in previewArt))
1379        iconType = 'unknown';
1380
1381      setTimeout(function() { callback(iconType, previewArt[iconType]) });
1382      return;
1383    }
1384
1385    if (ENABLE_EXIF_READER) {
1386      if (entry.name.match(/\.jpe?g$/i)) {
1387        // File is a jpg image, fetch the exif thumbnail.
1388        this.cacheExifMetadata_(entry, function(metadata) {
1389          callback(iconType, metadata.thumbnailURL || entry.toURL());
1390        });
1391        return;
1392      }
1393    }
1394
1395    // File is some other kind of image, just return the url to the whole
1396    // thing.
1397    setTimeout(function() { callback(iconType, entry.toURL()) });
1398  };
1399
1400  /**
1401   * Change the current directory.
1402   *
1403   * Dispatches the 'directory-changed' event when the directory is successfully
1404   * changed.
1405   *
1406   * @param {string} path The absolute path to the new directory.
1407   * @param {bool} opt_saveHistory Save this in the history stack (defaults
1408   *     to true).
1409   */
1410  FileManager.prototype.changeDirectory = function(path, opt_saveHistory) {
1411    var self = this;
1412
1413    if (arguments.length == 1) {
1414      opt_saveHistory = true;
1415    } else {
1416      opt_saveHistory = !!opt_saveHistory;
1417    }
1418
1419    function onPathFound(dirEntry) {
1420      if (self.currentDirEntry_ &&
1421          self.currentDirEntry_.fullPath == dirEntry.fullPath) {
1422        // Directory didn't actually change.
1423        return;
1424      }
1425
1426      var e = new cr.Event('directory-changed');
1427      e.previousDirEntry = self.currentDirEntry_;
1428      e.newDirEntry = dirEntry;
1429      e.saveHistory = opt_saveHistory;
1430      self.currentDirEntry_ = dirEntry;
1431      self.dispatchEvent(e);
1432    };
1433
1434    if (path == '/')
1435      return onPathFound(this.filesystem_.root);
1436
1437    this.filesystem_.root.getDirectory(
1438        path, {create: false}, onPathFound,
1439        function(err) {
1440          console.error('Error changing directory to: ' + path + ', ' + err);
1441          if (!self.currentDirEntry_) {
1442            // If we've never successfully changed to a directory, force them
1443            // to the root.
1444            self.changeDirectory('/');
1445          }
1446        });
1447  };
1448
1449  FileManager.prototype.deleteEntries = function(entries) {
1450    if (!window.confirm(str('CONFIRM_DELETE')))
1451      return;
1452
1453    var count = entries.length;
1454
1455    var self = this;
1456    function onDelete() {
1457      if (--count == 0)
1458         self.rescanDirectory_();
1459    }
1460
1461    for (var i = 0; i < entries.length; i++) {
1462      var entry = entries[i];
1463      if (entry.isFile) {
1464        entry.remove(
1465            onDelete,
1466            util.flog('Error deleting file: ' + entry.fullPath, onDelete));
1467      } else {
1468        entry.removeRecursively(
1469            onDelete,
1470            util.flog('Error deleting folder: ' + entry.fullPath, onDelete));
1471      }
1472    }
1473  };
1474
1475  /**
1476   * Invoked by the table dataModel after a sort completes.
1477   *
1478   * We use this hook to make sure selected files stay visible after a sort.
1479   */
1480  FileManager.prototype.onDataModelSorted_ = function() {
1481    var i = this.currentList_.selectionModel.leadIndex;
1482    this.currentList_.scrollIntoView(i);
1483  }
1484
1485  /**
1486   * Update the selection summary UI when the selection summarization completes.
1487   */
1488  FileManager.prototype.onSelectionSummarized_ = function() {
1489    if (this.selection.totalCount == 0) {
1490      this.previewSummary_.textContent = str('NOTHING_SELECTED');
1491
1492    } else if (this.selection.totalCount == 1) {
1493      this.previewSummary_.textContent =
1494        strf('ONE_FILE_SELECTED', cr.locale.bytesToSi(this.selection.bytes));
1495
1496    } else {
1497      this.previewSummary_.textContent =
1498        strf('MANY_FILES_SELECTED', this.selection.totalCount,
1499             cr.locale.bytesToSi(this.selection.bytes));
1500    }
1501  };
1502
1503  /**
1504   * Handle a click event on a breadcrumb element.
1505   *
1506   * @param {Event} event The click event.
1507   */
1508  FileManager.prototype.onBreadcrumbClick_ = function(event) {
1509    this.changeDirectory(event.srcElement.path);
1510  };
1511
1512  FileManager.prototype.onCheckboxMouseDownUp_ = function(event) {
1513    // If exactly one file is selected and its checkbox is *not* clicked,
1514    // then this should be treated as a "normal" click (ie. the previous
1515    // selection should be cleared).
1516    if (this.selection.totalCount == 1 && this.selection.entries[0].isFile) {
1517      var selectedIndex = this.selection.indexes[0];
1518      var listItem = this.currentList_.getListItemByIndex(selectedIndex);
1519      var checkbox = listItem.querySelector('input[type="checkbox"]');
1520      if (!checkbox.checked)
1521        return;
1522    }
1523
1524    // Otherwise, treat clicking on a checkbox the same as a ctrl-click.
1525    // The default properties of event.ctrlKey make it read-only, but
1526    // don't prevent deletion, so we delete first, then set it true.
1527    delete event.ctrlKey;
1528    event.ctrlKey = true;
1529  };
1530
1531  FileManager.prototype.onCheckboxClick_ = function(event) {
1532    if (event.shiftKey) {
1533      // Something about the timing of shift-clicks causes the checkbox
1534      // to get selected and then very quickly unselected.  It appears that
1535      // we forcibly select the checkbox as part of onSelectionChanged, and
1536      // then the default action of this click event fires and toggles the
1537      // checkbox back off.
1538      //
1539      // Since we're going to force checkboxes into the correct state for any
1540      // multi-selection, we can prevent this shift click from toggling the
1541      // checkbox and avoid the trouble.
1542      event.preventDefault();
1543    }
1544  };
1545
1546  /**
1547   * Update the UI when the selection model changes.
1548   *
1549   * @param {cr.Event} event The change event.
1550   */
1551  FileManager.prototype.onSelectionChanged_ = function(event) {
1552    var selectable;
1553
1554    this.summarizeSelection_();
1555    this.updateOkButton_();
1556    this.updatePreview_();
1557
1558    var self = this;
1559    setTimeout(function() { self.onSelectionChangeComplete_(event) }, 0);
1560  };
1561
1562  FileManager.prototype.onSelectionChangeComplete_ = function(event) {
1563    if (!this.showCheckboxes_)
1564      return;
1565
1566    for (var i = 0; i < event.changes.length; i++) {
1567      // Turn off any checkboxes for items that are no longer selected.
1568      var selectedIndex = event.changes[i].index;
1569      var listItem = this.currentList_.getListItemByIndex(selectedIndex);
1570      if (!listItem) {
1571        // When changing directories, we get notified about list items
1572        // that are no longer there.
1573        continue;
1574      }
1575
1576      if (!event.changes[i].selected) {
1577        var checkbox = listItem.querySelector('input[type="checkbox"]');
1578        checkbox.checked = false;
1579      }
1580    }
1581
1582    if (this.selection.fileCount > 1) {
1583      // If more than one file is selected, make sure all checkboxes are lit
1584      // up.
1585      for (var i = 0; i < this.selection.entries.length; i++) {
1586        if (!this.selection.entries[i].isFile)
1587          continue;
1588
1589        var selectedIndex = this.selection.indexes[i];
1590        var listItem = this.currentList_.getListItemByIndex(selectedIndex);
1591        if (listItem)
1592          listItem.querySelector('input[type="checkbox"]').checked = true;
1593      }
1594    }
1595  };
1596
1597  FileManager.prototype.updateOkButton_ = function(event) {
1598    if (this.dialogType_ == FileManager.DialogType.SELECT_FOLDER) {
1599      selectable = this.selection.directoryCount == 1 &&
1600          this.selection.fileCount == 0;
1601    } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) {
1602      selectable = (this.selection.directoryCount == 0 &&
1603                    this.selection.fileCount == 1);
1604    } else if (this.dialogType_ ==
1605               FileManager.DialogType.SELECT_OPEN_MULTI_FILE) {
1606      selectable = (this.selection.directoryCount == 0 &&
1607                    this.selection.fileCount >= 1);
1608    } else if (this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) {
1609      if (this.selection.leadEntry && this.selection.leadEntry.isFile)
1610        this.filenameInput_.value = this.selection.leadEntry.name;
1611
1612      if (this.currentDirEntry_.fullPath == '/' ||
1613          this.currentDirEntry_.fullPath == MEDIA_DIRECTORY) {
1614        // Nothing can be saved in to the root or media/ directories.
1615        selectable = false;
1616      } else {
1617        selectable = !!this.filenameInput_.value;
1618      }
1619    } else if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
1620      // No "select" buttons on the full page UI.
1621      selectable = true;
1622    } else {
1623      throw new Error('Unknown dialog type');
1624    }
1625
1626    this.okButton_.disabled = !selectable;
1627  };
1628
1629  /**
1630   * Handle a double-click event on an entry in the detail list.
1631   *
1632   * @param {Event} event The click event.
1633   */
1634  FileManager.prototype.onDetailDoubleClick_ = function(event) {
1635    if (this.renameInput_.currentEntry) {
1636      // Don't pay attention to double clicks during a rename.
1637      return;
1638    }
1639
1640    var i = this.currentList_.selectionModel.leadIndex;
1641    var entry = this.dataModel_.item(i);
1642
1643    if (entry.isDirectory)
1644      return this.changeDirectory(entry.fullPath);
1645
1646    if (!this.okButton_.disabled)
1647      this.onOk_();
1648
1649  };
1650
1651  /**
1652   * Update the UI when the current directory changes.
1653   *
1654   * @param {cr.Event} event The directory-changed event.
1655   */
1656  FileManager.prototype.onDirectoryChanged_ = function(event) {
1657    if (event.saveHistory) {
1658      history.pushState(this.currentDirEntry_.fullPath,
1659                        this.currentDirEntry_.fullPath,
1660                        location.href);
1661    }
1662
1663    this.updateOkButton_();
1664    // New folder should never be enabled in the root or media/ directories.
1665    this.newFolderButton_.disabled =
1666        (this.currentDirEntry_.fullPath == '/' ||
1667         this.currentDirEntry_.fullPath == MEDIA_DIRECTORY);
1668
1669    this.document_.title = this.currentDirEntry_.fullPath;
1670    this.rescanDirectory_();
1671  };
1672
1673  /**
1674   * Update the UI when a disk is mounted or unmounted.
1675   *
1676   * @param {string} path The path that has been mounted or unmounted.
1677   */
1678  FileManager.prototype.onDiskChanged_ = function(event) {
1679    if (event.eventType == 'added') {
1680      this.changeDirectory(event.volumeInfo.mountPath);
1681    } else if (event.eventType == 'removed') {
1682      if (this.currentDirEntry_ &&
1683          isParentPath(event.volumeInfo.mountPath,
1684                       this.currentDirEntry_.fullPath)) {
1685        this.changeDirectory(getParentPath(event.volumeInfo.mountPath));
1686      }
1687    }
1688  };
1689
1690  /**
1691   * Rescan the current directory, refreshing the list.
1692   *
1693   * @param {function()} opt_callback Optional function to invoke when the
1694   *     rescan is complete.
1695   */
1696  FileManager.prototype.rescanDirectory_ = function(opt_callback) {
1697    var self = this;
1698    var reader;
1699
1700    function onReadSome(entries) {
1701      if (entries.length == 0) {
1702        if (self.dataModel_.sortStatus.field != 'name')
1703          self.dataModel_.updateIndex(0);
1704
1705        if (opt_callback)
1706          opt_callback();
1707        return;
1708      }
1709
1710      // Splice takes the to-be-spliced-in array as individual parameters,
1711      // rather than as an array, so we need to perform some acrobatics...
1712      var spliceArgs = [].slice.call(entries);
1713
1714      // Hide files that start with a dot ('.').
1715      // TODO(rginda): User should be able to override this.  Support for other
1716      // commonly hidden patterns might be nice too.
1717      if (self.filterFiles_) {
1718        spliceArgs = spliceArgs.filter(function(e) {
1719            return e.name.substr(0, 1) != '.';
1720          });
1721      }
1722
1723      spliceArgs.unshift(0, 0);  // index, deleteCount
1724      self.dataModel_.splice.apply(self.dataModel_, spliceArgs);
1725
1726      // Keep reading until entries.length is 0.
1727      reader.readEntries(onReadSome);
1728    };
1729
1730    this.lastLabelClick_ = null;
1731
1732    // Clear the table first.
1733    this.dataModel_.splice(0, this.dataModel_.length);
1734
1735    this.updateBreadcrumbs_();
1736
1737    if (this.currentDirEntry_.fullPath != '/') {
1738      // If not the root directory, just read the contents.
1739      reader = this.currentDirEntry_.createReader();
1740      reader.readEntries(onReadSome);
1741      return;
1742    }
1743
1744    // Otherwise, use the provided list of root subdirectories, since the
1745    // real local filesystem root directory (the one we use outside the
1746    // harness) can't be enumerated yet.
1747    var spliceArgs = [].slice.call(this.rootEntries_);
1748    spliceArgs.unshift(0, 0);  // index, deleteCount
1749    self.dataModel_.splice.apply(self.dataModel_, spliceArgs);
1750    self.dataModel_.updateIndex(0);
1751
1752    if (opt_callback)
1753      opt_callback();
1754  };
1755
1756  FileManager.prototype.findListItem_ = function(event) {
1757    var node = event.srcElement;
1758    while (node) {
1759      if (node.tagName == 'LI')
1760        break;
1761      node = node.parentNode;
1762    }
1763
1764    return node;
1765  };
1766
1767  FileManager.prototype.onGridMouseDown_ = function(event) {
1768    this.updateCommands_();
1769
1770    if (this.allowRenameClick_(event, event.srcElement.parentNode)) {
1771      event.preventDefault();
1772      this.initiateRename_(event.srcElement);
1773    }
1774
1775    if (event.button != 1)
1776      return;
1777
1778    var li = this.findListItem_(event);
1779    if (!li)
1780      return;
1781  };
1782
1783  FileManager.prototype.onTableMouseDown_ = function(event) {
1784    this.updateCommands_();
1785
1786    if (this.allowRenameClick_(event,
1787                               event.srcElement.parentNode.parentNode)) {
1788      event.preventDefault();
1789      this.initiateRename_(event.srcElement);
1790    }
1791
1792    if (event.button != 1)
1793      return;
1794
1795    var li = this.findListItem_(event);
1796    if (!li) {
1797      console.log('li not found', event);
1798      return;
1799    }
1800  };
1801
1802  /**
1803   * Determine whether or not a click should initiate a rename.
1804   *
1805   * Renames can happen on mouse click if the user clicks on a label twice,
1806   * at least a half second apart.
1807   */
1808  FileManager.prototype.allowRenameClick_ = function(event, row) {
1809    if (this.dialogType_ != FileManager.DialogType.FULL_PAGE ||
1810        this.currentDirEntry_.name == '') {
1811      // Renaming only enabled for full-page mode, outside of the root
1812      // directory.
1813      return false;
1814    }
1815
1816    // Rename already in progress.
1817    if (this.renameInput_.currentEntry)
1818      return false;
1819
1820    // Didn't click on the label.
1821    if (event.srcElement.className != 'filename-label')
1822      return false;
1823
1824    // Wrong button or using a keyboard modifier.
1825    if (event.button != 0 || event.shiftKey || event.metaKey || event.altKey) {
1826      this.lastLabelClick_ = null;
1827      return false;
1828    }
1829
1830    var now = new Date();
1831
1832    this.lastLabelClick_ = this.lastLabelClick_ || now;
1833    var delay = now - this.lastLabelClick_;
1834    if (!row.selected || delay < 500)
1835      return false;
1836
1837    this.lastLabelClick_ = now;
1838    return true;
1839  };
1840
1841  FileManager.prototype.initiateRename_= function(label) {
1842    var input = this.renameInput_;
1843
1844    window.label = label;
1845
1846    input.value = label.textContent;
1847    input.style.top = label.offsetTop + 'px';
1848    input.style.left = label.offsetLeft + 'px';
1849    input.style.width = label.clientWidth + 'px';
1850    label.parentNode.appendChild(input);
1851    input.focus();
1852    var selectionEnd = input.value.lastIndexOf('.');
1853    if (selectionEnd == -1) {
1854      input.select();
1855    } else {
1856      input.selectionStart = 0;
1857      input.selectionEnd = selectionEnd;
1858    }
1859
1860    // This has to be set late in the process so we don't handle spurious
1861    // blur events.
1862    input.currentEntry = label.entry;
1863  };
1864
1865  FileManager.prototype.onRenameInputKeyDown_ = function(event) {
1866    if (!this.renameInput_.currentEntry)
1867      return;
1868
1869    switch (event.keyCode) {
1870      case 27:  // Escape
1871        this.cancelRename_();
1872        event.preventDefault();
1873        break;
1874
1875      case 13:  // Enter
1876        this.commitRename_();
1877        event.preventDefault();
1878        break;
1879    }
1880  };
1881
1882  FileManager.prototype.onRenameInputBlur_ = function(event) {
1883    if (this.renameInput_.currentEntry)
1884      this.cancelRename_();
1885  };
1886
1887  FileManager.prototype.commitRename_ = function() {
1888    var entry = this.renameInput_.currentEntry;
1889    var newName = this.renameInput_.value;
1890
1891    this.renameInput_.currentEntry = null;
1892    this.lastLabelClick_ = null;
1893
1894    if (this.renameInput_.parentNode)
1895      this.renameInput_.parentNode.removeChild(this.renameInput_);
1896
1897    var self = this;
1898    function onSuccess() {
1899      self.rescanDirectory_(function () {
1900        for (var i = 0; i < self.dataModel_.length; i++) {
1901          if (self.dataModel_.item(i).name == newName) {
1902            self.currentList_.selectionModel.selectedIndex = i;
1903            self.currentList_.scrollIndexIntoView(i);
1904            self.currentList_.focus();
1905            return;
1906          }
1907        }
1908      });
1909    }
1910
1911    function onError(err) {
1912      window.alert(strf('ERROR_RENAMING', entry.name,
1913                        util.getFileErrorMnemonic(err.code)));
1914    }
1915
1916    entry.moveTo(this.currentDirEntry_, newName, onSuccess, onError);
1917  };
1918
1919  FileManager.prototype.cancelRename_ = function(event) {
1920    this.renameInput_.currentEntry = null;
1921    this.lastLabelClick_ = null;
1922
1923    if (this.renameInput_.parentNode)
1924      this.renameInput_.parentNode.removeChild(this.renameInput_);
1925  };
1926
1927  FileManager.prototype.onFilenameInputKeyUp_ = function(event) {
1928    this.okButton_.disabled = this.filenameInput_.value.length == 0;
1929
1930    if (event.keyCode == 13 /* Enter */ && !this.okButton_.disabled)
1931      this.onOk_();
1932  };
1933
1934  FileManager.prototype.onFilenameInputFocus_ = function(event) {
1935    var input = this.filenameInput_;
1936
1937    // On focus we want to select everything but the extension, but
1938    // Chrome will select-all after the focus event completes.  We
1939    // schedule a timeout to alter the focus after that happens.
1940    setTimeout(function() {
1941        var selectionEnd = input.value.lastIndexOf('.');
1942        if (selectionEnd == -1) {
1943          input.select();
1944        } else {
1945          input.selectionStart = 0;
1946          input.selectionEnd = selectionEnd;
1947        }
1948    }, 0);
1949  };
1950
1951  FileManager.prototype.onNewFolderButtonClick_ = function(event) {
1952    var name = '';
1953
1954    while (1) {
1955      name = window.prompt(str('NEW_FOLDER_PROMPT'), name);
1956      if (!name)
1957        return;
1958
1959      if (name.indexOf('/') == -1)
1960        break;
1961
1962      alert(strf('ERROR_INVALID_FOLDER_CHARACTER', '/'));
1963    }
1964
1965    var self = this;
1966
1967    function onSuccess(dirEntry) {
1968      self.rescanDirectory_(function () {
1969        for (var i = 0; i < self.dataModel_.length; i++) {
1970          if (self.dataModel_.item(i).name == dirEntry.name) {
1971            self.currentList_.selectionModel.selectedIndex = i;
1972            self.currentList_.scrollIndexIntoView(i);
1973            self.currentList_.focus();
1974            return;
1975          }
1976        }
1977      });
1978    }
1979
1980    function onError(err) {
1981      window.alert(strf('ERROR_CREATING_FOLDER', name,
1982                        util.getFileErrorMnemonic(err.code)));
1983    }
1984
1985    this.currentDirEntry_.getDirectory(name, {create: true, exclusive: true},
1986                                       onSuccess, onError);
1987  };
1988
1989  FileManager.prototype.onDetailViewButtonClick_ = function(event) {
1990    this.setListType(FileManager.ListType.DETAIL);
1991  };
1992
1993  FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
1994    this.setListType(FileManager.ListType.THUMBNAIL);
1995  };
1996
1997  FileManager.prototype.onKeyDown_ = function(event) {
1998    if (event.srcElement.tagName == 'INPUT')
1999      return;
2000
2001    switch (event.keyCode) {
2002      case 8:  // Backspace => Up one directory.
2003        event.preventDefault();
2004        var path = this.currentDirEntry_.fullPath;
2005        if (path && path != '/') {
2006          var path = path.replace(/\/[^\/]+$/, '');
2007          this.changeDirectory(path || '/');
2008        }
2009        break;
2010
2011      case 13:  // Enter => Change directory or complete dialog.
2012        if (this.selection.totalCount == 1 &&
2013            this.selection.leadEntry.isDirectory &&
2014            this.dialogType_ != FileManager.SELECT_FOLDER) {
2015          this.changeDirectory(this.selection.leadEntry.fullPath);
2016        } else if (!this.okButton_.disabled) {
2017          this.onOk_();
2018        }
2019        break;
2020
2021      case 32:  // Ctrl-Space => New Folder.
2022        if (this.newFolderButton_.style.display != 'none' && event.ctrlKey) {
2023          event.preventDefault();
2024          this.onNewFolderButtonClick_();
2025        }
2026        break;
2027
2028      case 190:  // Ctrl-. => Toggle filter files.
2029        if (event.ctrlKey) {
2030          this.filterFiles_ = !this.filterFiles_;
2031          this.rescanDirectory_();
2032        }
2033        break;
2034
2035      case 46:  // Delete.
2036        if (this.dialogType_ == FileManager.DialogType.FULL_PAGE &&
2037            this.selection.totalCount > 0) {
2038          event.preventDefault();
2039          this.deleteEntries(this.selection.entries);
2040        }
2041        break;
2042    }
2043  };
2044
2045  /**
2046   * Handle a click of the cancel button.  Closes the window.
2047   *
2048   * @param {Event} event The click event.
2049   */
2050  FileManager.prototype.onCancel_ = function(event) {
2051    chrome.fileBrowserPrivate.cancelDialog();
2052  };
2053
2054  /**
2055   * Handle a click of the ok button.
2056   *
2057   * The ok button has different UI labels depending on the type of dialog, but
2058   * in code it's always referred to as 'ok'.
2059   *
2060   * @param {Event} event The click event.
2061   */
2062  FileManager.prototype.onOk_ = function(event) {
2063    var currentDirUrl = this.currentDirEntry_.toURL();
2064
2065    if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
2066      currentDirUrl += '/';
2067
2068    if (this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) {
2069      // Save-as doesn't require a valid selection from the list, since
2070      // we're going to take the filename from the text input.
2071      var filename = this.filenameInput_.value;
2072      if (!filename)
2073        throw new Error('Missing filename!');
2074
2075      chrome.fileBrowserPrivate.selectFile(currentDirUrl + encodeURI(filename),
2076                                           0);
2077      // Window closed by above call.
2078      return;
2079    }
2080
2081    var ary = [];
2082    var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
2083
2084    // All other dialog types require at least one selected list item.
2085    // The logic to control whether or not the ok button is enabled should
2086    // prevent us from ever getting here, but we sanity check to be sure.
2087    if (!selectedIndexes.length)
2088      throw new Error('Nothing selected!');
2089
2090    for (var i = 0; i < selectedIndexes.length; i++) {
2091      var entry = this.dataModel_.item(selectedIndexes[i]);
2092      if (!entry) {
2093        console.log('Error locating selected file at index: ' + i);
2094        continue;
2095      }
2096
2097      ary.push(currentDirUrl + encodeURI(entry.name));
2098    }
2099
2100    // Multi-file selection has no other restrictions.
2101    if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE) {
2102      chrome.fileBrowserPrivate.selectFiles(ary);
2103      // Window closed by above call.
2104      return;
2105    }
2106
2107    // In full screen mode, open all files for vieweing.
2108    if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
2109      chrome.fileBrowserPrivate.viewFiles(ary, "default");
2110      // Window stays open.
2111      return;
2112    }
2113
2114    // Everything else must have exactly one.
2115    if (ary.length > 1)
2116      throw new Error('Too many files selected!');
2117
2118    if (this.dialogType_ == FileManager.DialogType.SELECT_FOLDER) {
2119      if (!this.selection.leadEntry.isDirectory)
2120        throw new Error('Selected entry is not a folder!');
2121    } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) {
2122      if (!this.selection.leadEntry.isFile)
2123        throw new Error('Selected entry is not a file!');
2124    }
2125
2126    chrome.fileBrowserPrivate.selectFile(ary[0], 0);
2127    // Window closed by above call.
2128  };
2129
2130})();
2131