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 * FileManager constructor.
9 *
10 * FileManager objects encapsulate the functionality of the file selector
11 * dialogs, as well as the full screen file manager application (though the
12 * latter is not yet implemented).
13 *
14 * @constructor
15 */
16function FileManager() {
17  // --------------------------------------------------------------------------
18  // Services FileManager depends on.
19
20  /**
21   * Volume manager.
22   * @type {VolumeManager}
23   * @private
24   */
25  this.volumeManager_ = null;
26
27  /**
28   * Metadata cache.
29   * @type {MetadataCache}
30   * @private
31   */
32  this.metadataCache_ = null;
33
34  /**
35   * File operation manager.
36   * @type {FileOperationManager}
37   * @private
38   */
39  this.fileOperationManager_ = null;
40
41  /**
42   * File transfer controller.
43   * @type {FileTransferController}
44   * @private
45   */
46  this.fileTransferController_ = null;
47
48  /**
49   * File filter.
50   * @type {FileFilter}
51   * @private
52   */
53  this.fileFilter_ = null;
54
55  /**
56   * File watcher.
57   * @type {FileWatcher}
58   * @private
59   */
60  this.fileWatcher_ = null;
61
62  /**
63   * Model of current directory.
64   * @type {DirectoryModel}
65   * @private
66   */
67  this.directoryModel_ = null;
68
69  /**
70   * Model of folder shortcuts.
71   * @type {FolderShortcutsDataModel}
72   * @private
73   */
74  this.folderShortcutsModel_ = null;
75
76  /**
77   * VolumeInfo of the current volume.
78   * @type {VolumeInfo}
79   * @private
80   */
81  this.currentVolumeInfo_ = null;
82
83  /**
84   * Handler for command events.
85   * @type {CommandHandler}
86   */
87  this.commandHandler = null;
88
89  /**
90   * Handler for the change of file selection.
91   * @type {SelectionHandler}
92   * @private
93   */
94  this.selectionHandler_ = null;
95
96  // --------------------------------------------------------------------------
97  // Parameters determining the type of file manager.
98
99  /**
100   * Dialog type of this window.
101   * @type {DialogType}
102   */
103  this.dialogType = DialogType.FULL_PAGE;
104
105  /**
106   * Current list type.
107   * @type {ListType}
108   * @private
109   */
110  this.listType_ = null;
111
112  /**
113   * List of acceptable file types for open dialog.
114   * @type {Array.<Object>}
115   * @private
116   */
117  this.fileTypes_ = [];
118
119  /**
120   * Startup parameters for this application.
121   * @type {Object}
122   * @private
123   */
124  this.params_ = null;
125
126  /**
127   * Startup preference about the view.
128   * @type {Object}
129   * @private
130   */
131  this.viewOptions_ = {};
132
133  /**
134   * The user preference.
135   * @type {Object}
136   * @private
137   */
138  this.preferences_ = null;
139
140  // --------------------------------------------------------------------------
141  // UI components.
142
143  /**
144   * UI management class of file manager.
145   * @type {FileManagerUI}
146   * @private
147   */
148  this.ui_ = null;
149
150  /**
151   * Preview panel.
152   * @type {PreviewPanel}
153   * @private
154   */
155  this.previewPanel_ = null;
156
157  /**
158   * Progress center panel.
159   * @type {ProgressCenterPanel}
160   * @private
161   */
162  this.progressCenterPanel_ = null;
163
164  /**
165   * Directory tree.
166   * @type {DirectoryTree}
167   * @private
168   */
169  this.directoryTree_ = null;
170
171  /**
172   * Auto-complete list.
173   * @type {AutocompleteList}
174   * @private
175   */
176  this.autocompleteList_ = null;
177
178  /**
179   * Banners in the file list.
180   * @type {FileListBannerController}
181   * @private
182   */
183  this.bannersController_ = null;
184
185  // --------------------------------------------------------------------------
186  // Dialogs.
187
188  /**
189   * Error dialog.
190   * @type {ErrorDialog}
191   */
192  this.error = null;
193
194  /**
195   * Alert dialog.
196   * @type {cr.ui.dialogs.AlertDialog}
197   */
198  this.alert = null;
199
200  /**
201   * Confirm dialog.
202   * @type {cr.ui.dialogs.ConfirmDialog}
203   */
204  this.confirm = null;
205
206  /**
207   * Prompt dialog.
208   * @type {cr.ui.dialogs.PromptDialog}
209   */
210  this.prompt = null;
211
212  /**
213   * Share dialog.
214   * @type {ShareDialog}
215   * @private
216   */
217  this.shareDialog_ = null;
218
219  /**
220   * Default task picker.
221   * @type {DefaultActionDialog}
222   */
223  this.defaultTaskPicker = null;
224
225  /**
226   * Suggest apps dialog.
227   * @type {SuggestAppsDialog}
228   */
229  this.suggestAppsDialog = null;
230
231  // --------------------------------------------------------------------------
232  // Menus.
233
234  /**
235   * Context menu for files.
236   * @type {HTMLMenuElement}
237   * @private
238   */
239  this.fileContextMenu_ = null;
240
241  /**
242   * Context menu for volumes or shortcuts displayed on left pane.
243   * @type {HTMLMenuElement}
244   * @private
245   */
246  this.rootsContextMenu_ = null;
247
248  /**
249   * Context menu for directory tree items.
250   * @type {HTMLMenuElement}
251   * @private
252   */
253  this.directoryTreeContextMenu_ = null;
254
255  /**
256   * Context menu for texts.
257   * @type {HTMLMenuElement}
258   * @private
259   */
260  this.textContextMenu_ = null;
261
262  // --------------------------------------------------------------------------
263  // DOM elements.
264
265  /**
266   * Background page.
267   * @type {Window}
268   * @private
269   */
270  this.backgroundPage_ = null;
271
272  /**
273   * The root DOM element of this app.
274   * @type {HTMLBodyElement}
275   * @private
276   */
277  this.dialogDom_ = null;
278
279  /**
280   * The document object of this app.
281   * @type {HTMLDocument}
282   * @private
283   */
284  this.document_ = null;
285
286  /**
287   * The menu item to toggle "Do not use mobile data for sync".
288   * @type {HTMLMenuItemElement}
289   */
290  this.syncButton = null;
291
292  /**
293   * The menu item to toggle "Show Google Docs files".
294   * @type {HTMLMenuItemElement}
295   */
296  this.hostedButton = null;
297
298  /**
299   * The menu item for doing default action.
300   * @type {HTMLMenuItemElement}
301   * @private
302   */
303  this.defaultActionMenuItem_ = null;
304
305  /**
306   * The button to open gear menu.
307   * @type {HTMLButtonElement}
308   * @private
309   */
310  this.gearButton_ = null;
311
312  /**
313   * The OK button.
314   * @type {HTMLButtonElement}
315   * @private
316   */
317  this.okButton_ = null;
318
319  /**
320   * The cancel button.
321   * @type {HTMLButtonElement}
322   * @private
323   */
324  this.cancelButton_ = null;
325
326  /**
327   * The combo button to specify the task.
328   * @type {HTMLButtonElement}
329   * @private
330   */
331  this.taskItems_ = null;
332
333  /**
334   * The input element to rename entry.
335   * @type {HTMLInputElement}
336   * @private
337   */
338  this.renameInput_ = null;
339
340  /**
341   * The input element to specify file name.
342   * @type {HTMLInputElement}
343   * @private
344   */
345  this.filenameInput_ = null;
346
347  /**
348   * The file table.
349   * @type {FileTable}
350   * @private
351   */
352  this.table_ = null;
353
354  /**
355   * The file grid.
356   * @type {FileGrid}
357   * @private
358   */
359  this.grid_ = null;
360
361  /**
362   * Current file list.
363   * @type {cr.ui.List}
364   * @private
365   */
366  this.currentList_ = null;
367
368  /**
369   * Spinner on file list which is shown while loading.
370   * @type {HTMLDivElement}
371   * @private
372   */
373  this.spinner_ = null;
374
375  /**
376   * The container element of the dialog.
377   * @type {HTMLDivElement}
378   * @private
379   */
380  this.dialogContainer_ = null;
381
382  /**
383   * The container element of the file list.
384   * @type {HTMLDivElement}
385   * @private
386   */
387  this.listContainer_ = null;
388
389  /**
390   * The input element in the search box.
391   * @type {HTMLInputElement}
392   * @private
393   */
394  this.searchBox_ = null;
395
396  /**
397   * The file type selector.
398   * @type {HTMLSelectElement}
399   * @private
400   */
401  this.fileTypeSelector_ = null;
402
403  /**
404   * Open-with command in the context menu.
405   * @type {cr.ui.Command}
406   * @private
407   */
408  this.openWithCommand_ = null;
409
410  // --------------------------------------------------------------------------
411  // Bound functions.
412
413  /**
414   * Bound function for onCopyProgress_.
415   * @type {this:FileManager, function(Event)}
416   * @private
417   */
418  this.onCopyProgressBound_ = null;
419
420  /**
421   * Bound function for onEntriesChanged_.
422   * @type {this:FileManager, function(Event)}
423   * @private
424   */
425  this.onEntriesChangedBound_ = null;
426
427  /**
428   * Bound function for onCancel_.
429   * @type {this:FileManager, function(Event)}
430   * @private
431   */
432  this.onCancelBound_ = null;
433
434  // --------------------------------------------------------------------------
435  // Scan state.
436
437  /**
438   * Whether a scan is in progress.
439   * @type {boolean}
440   * @private
441   */
442  this.scanInProgress_ = false;
443
444  /**
445   * Whether a scan is updated at least once. If true, spinner should disappear.
446   * @type {boolean}
447   * @private
448   */
449  this.scanUpdatedAtLeastOnceOrCompleted_ = false;
450
451  /**
452   * Timer ID to delay UI refresh after a scan is completed.
453   * @type {number}
454   * @private
455   */
456  this.scanCompletedTimer_ = 0;
457
458  /**
459   * Timer ID to delay UI refresh after a scan is updated.
460   * @type {number}
461   * @private
462   */
463  this.scanUpdatedTimer_ = 0;
464
465  /**
466   * Timer ID to delay showing spinner after a scan starts.
467   * @type {number}
468   * @private
469   */
470  this.showSpinnerTimeout_ = 0;
471
472  // --------------------------------------------------------------------------
473  // Search states.
474
475  /**
476   * The last search query.
477   * @type {string}
478   * @private
479   */
480  this.lastSearchQuery_ = '';
481
482  /**
483   * The last auto-complete query.
484   * @type {string}
485   * @private
486   */
487  this.lastAutocompleteQuery_ = '';
488
489  /**
490   * Whether auto-complete suggestion is busy to respond previous request.
491   * @type {boolean}
492   * @private
493   */
494  this.autocompleteSuggestionsBusy_ = false;
495
496  /**
497   * State of text-search, which is triggerd by keyboard input on file list.
498   * @type {Object}
499   * @private
500   */
501  this.textSearchState_ = {text: '', date: new Date()};
502
503  // --------------------------------------------------------------------------
504  // Miscellaneous FileManager's states.
505
506  /**
507   * Queue for ordering FileManager's initialization process.
508   * @type {AsyncUtil.Group}
509   * @private
510   */
511  this.initializeQueue_ = new AsyncUtil.Group();
512
513  /**
514   * True while a user is pressing <Tab>.
515   * This is used for identifying the trigger causing the filelist to
516   * be focused.
517   * @type {boolean}
518   * @private
519   */
520  this.pressingTab_ = false;
521
522  /**
523   * True while a user is pressing <Ctrl>.
524   *
525   * TODO(fukino): This key is used only for controlling gear menu, so it
526   * should be moved to GearMenu class. crbug.com/366032.
527   *
528   * @type {boolean}
529   * @private
530   */
531  this.pressingCtrl_ = false;
532
533  /**
534   * True if shown gear menu is in secret mode.
535   *
536   * TODO(fukino): The state of gear menu should be moved to GearMenu class.
537   * crbug.com/366032.
538   *
539   * @type {boolean}
540   * @private
541   */
542  this.isSecretGearMenuShown_ = false;
543
544  /**
545   * The last clicked item in the file list.
546   * @type {HTMLLIElement}
547   * @private
548   */
549  this.lastClickedItem_ = null;
550
551  /**
552   * Count of the SourceNotFound error.
553   * @type {number}
554   * @private
555   */
556  this.sourceNotFoundErrorCount_ = 0;
557
558  /**
559   * Whether the app should be closed on unmount.
560   * @type {boolean}
561   * @private
562   */
563  this.closeOnUnmount_ = false;
564
565  /**
566   * The key for storing startup preference.
567   * @type {string}
568   * @private
569   */
570  this.startupPrefName_ = '';
571
572  /**
573   * URL of directory which should be initial current directory.
574   * @type {string}
575   * @private
576   */
577  this.initCurrentDirectoryURL_ = '';
578
579  /**
580   * URL of entry which should be initially selected.
581   * @type {string}
582   * @private
583   */
584  this.initSelectionURL_ = '';
585
586  /**
587   * The name of target entry (not URL).
588   * @type {string}
589   * @private
590   */
591  this.initTargetName_ = '';
592
593  /**
594   * Data model which is used as a placefolder in inactive file list.
595   * @type {cr.ui.ArrayDataModel}
596   * @private
597   */
598  this.emptyDataModel_ = null;
599
600  /**
601   * Selection model which is used as a placefolder in inactive file list.
602   * @type {cr.ui.ListSelectionModel}
603   * @private
604   */
605  this.emptySelectionModel_ = null;
606
607  // Object.seal() has big performance/memory overhead for now, so we use
608  // Object.preventExtensions() here. crbug.com/412239.
609  Object.preventExtensions(this);
610}
611
612FileManager.prototype = {
613  __proto__: cr.EventTarget.prototype,
614  get directoryModel() {
615    return this.directoryModel_;
616  },
617  get directoryTree() {
618    return this.directoryTree_;
619  },
620  get document() {
621    return this.document_;
622  },
623  get fileTransferController() {
624    return this.fileTransferController_;
625  },
626  get fileOperationManager() {
627    return this.fileOperationManager_;
628  },
629  get backgroundPage() {
630    return this.backgroundPage_;
631  },
632  get volumeManager() {
633    return this.volumeManager_;
634  },
635  get ui() {
636    return this.ui_;
637  }
638};
639
640/**
641 * List of dialog types.
642 *
643 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
644 * FULL_PAGE which is specific to this code.
645 *
646 * @enum {string}
647 * @const
648 */
649var DialogType = {
650  SELECT_FOLDER: 'folder',
651  SELECT_UPLOAD_FOLDER: 'upload-folder',
652  SELECT_SAVEAS_FILE: 'saveas-file',
653  SELECT_OPEN_FILE: 'open-file',
654  SELECT_OPEN_MULTI_FILE: 'open-multi-file',
655  FULL_PAGE: 'full-page'
656};
657
658/**
659 * @param {string} type Dialog type.
660 * @return {boolean} Whether the type is modal.
661 */
662DialogType.isModal = function(type) {
663  return type == DialogType.SELECT_FOLDER ||
664      type == DialogType.SELECT_UPLOAD_FOLDER ||
665      type == DialogType.SELECT_SAVEAS_FILE ||
666      type == DialogType.SELECT_OPEN_FILE ||
667      type == DialogType.SELECT_OPEN_MULTI_FILE;
668};
669
670/**
671 * @param {string} type Dialog type.
672 * @return {boolean} Whether the type is open dialog.
673 */
674DialogType.isOpenDialog = function(type) {
675  return type == DialogType.SELECT_OPEN_FILE ||
676         type == DialogType.SELECT_OPEN_MULTI_FILE ||
677         type == DialogType.SELECT_FOLDER ||
678         type == DialogType.SELECT_UPLOAD_FOLDER;
679};
680
681/**
682 * @param {string} type Dialog type.
683 * @return {boolean} Whether the type is open dialog for file(s).
684 */
685DialogType.isOpenFileDialog = function(type) {
686  return type == DialogType.SELECT_OPEN_FILE ||
687         type == DialogType.SELECT_OPEN_MULTI_FILE;
688};
689
690/**
691 * @param {string} type Dialog type.
692 * @return {boolean} Whether the type is folder selection dialog.
693 */
694DialogType.isFolderDialog = function(type) {
695  return type == DialogType.SELECT_FOLDER ||
696         type == DialogType.SELECT_UPLOAD_FOLDER;
697};
698
699Object.freeze(DialogType);
700
701/**
702 * Bottom margin of the list and tree for transparent preview panel.
703 * @const
704 */
705var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
706
707// Anonymous "namespace".
708(function() {
709
710  // Private variables and helper functions.
711
712  /**
713   * Number of milliseconds in a day.
714   */
715  var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
716
717  /**
718   * Some UI elements react on a single click and standard double click handling
719   * leads to confusing results. We ignore a second click if it comes soon
720   * after the first.
721   */
722  var DOUBLE_CLICK_TIMEOUT = 200;
723
724  /**
725   * Updates the element to display the information about remaining space for
726   * the storage.
727   *
728   * @param {!Object<string, number>} sizeStatsResult Map containing remaining
729   *     space information.
730   * @param {!Element} spaceInnerBar Block element for a percentage bar
731   *     representing the remaining space.
732   * @param {!Element} spaceInfoLabel Inline element to contain the message.
733   * @param {!Element} spaceOuterBar Block element around the percentage bar.
734   */
735  var updateSpaceInfo = function(
736      sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
737    spaceInnerBar.removeAttribute('pending');
738    if (sizeStatsResult) {
739      var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
740      spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
741
742      var usedSpace =
743          sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
744      spaceInnerBar.style.width =
745          (100 * usedSpace / sizeStatsResult.totalSize) + '%';
746
747      spaceOuterBar.hidden = false;
748    } else {
749      spaceOuterBar.hidden = true;
750      spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
751    }
752  };
753
754  // Public statics.
755
756  FileManager.ListType = {
757    DETAIL: 'detail',
758    THUMBNAIL: 'thumb'
759  };
760
761  FileManager.prototype.initPreferences_ = function(callback) {
762    var group = new AsyncUtil.Group();
763
764    // DRIVE preferences should be initialized before creating DirectoryModel
765    // to rebuild the roots list.
766    group.add(this.getPreferences_.bind(this));
767
768    // Get startup preferences.
769    group.add(function(done) {
770      chrome.storage.local.get(this.startupPrefName_, function(values) {
771        var value = values[this.startupPrefName_];
772        if (!value) {
773          done();
774          return;
775        }
776        // Load the global default options.
777        try {
778          this.viewOptions_ = JSON.parse(value);
779        } catch (ignore) {}
780        // Override with window-specific options.
781        if (window.appState && window.appState.viewOptions) {
782          for (var key in window.appState.viewOptions) {
783            if (window.appState.viewOptions.hasOwnProperty(key))
784              this.viewOptions_[key] = window.appState.viewOptions[key];
785          }
786        }
787        done();
788      }.bind(this));
789    }.bind(this));
790
791    group.run(callback);
792  };
793
794  /**
795   * One time initialization for the file system and related things.
796   *
797   * @param {function()} callback Completion callback.
798   * @private
799   */
800  FileManager.prototype.initFileSystemUI_ = function(callback) {
801    this.table_.startBatchUpdates();
802    this.grid_.startBatchUpdates();
803
804    this.initFileList_();
805    this.setupCurrentDirectory_();
806
807    // PyAuto tests monitor this state by polling this variable
808    this.__defineGetter__('workerInitialized_', function() {
809      return this.metadataCache_.isInitialized();
810    }.bind(this));
811
812    this.initDateTimeFormatters_();
813
814    var self = this;
815
816    // Get the 'allowRedeemOffers' preference before launching
817    // FileListBannerController.
818    this.getPreferences_(function(pref) {
819      /** @type {boolean} */
820      var showOffers = pref['allowRedeemOffers'];
821      self.bannersController_ = new FileListBannerController(
822          self.directoryModel_, self.volumeManager_, self.document_,
823          showOffers);
824      self.bannersController_.addEventListener('relayout',
825                                               self.onResize_.bind(self));
826    });
827
828    var dm = this.directoryModel_;
829    dm.addEventListener('directory-changed',
830                        this.onDirectoryChanged_.bind(this));
831
832    var listBeingUpdated = null;
833    dm.addEventListener('begin-update-files', function() {
834      self.currentList_.startBatchUpdates();
835      // Remember the list which was used when updating files started, so
836      // endBatchUpdates() is called on the same list.
837      listBeingUpdated = self.currentList_;
838    });
839    dm.addEventListener('end-update-files', function() {
840      self.restoreItemBeingRenamed_();
841      listBeingUpdated.endBatchUpdates();
842      listBeingUpdated = null;
843    });
844
845    dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
846    dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
847    dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
848    dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
849    dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
850    dm.addEventListener('rescan-completed',
851                        this.onRescanCompleted_.bind(this));
852
853    this.directoryTree_.addEventListener('change', function() {
854      this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
855    }.bind(this));
856
857    var stateChangeHandler =
858        this.onPreferencesChanged_.bind(this);
859    chrome.fileManagerPrivate.onPreferencesChanged.addListener(
860        stateChangeHandler);
861    stateChangeHandler();
862
863    var driveConnectionChangedHandler =
864        this.onDriveConnectionChanged_.bind(this);
865    this.volumeManager_.addEventListener('drive-connection-changed',
866        driveConnectionChangedHandler);
867    driveConnectionChangedHandler();
868
869    // Set the initial focus.
870    this.refocus();
871    // Set it as a fallback when there is no focus.
872    this.document_.addEventListener('focusout', function(e) {
873      setTimeout(function() {
874        // When there is no focus, the active element is the <body>.
875        if (this.document_.activeElement == this.document_.body)
876          this.refocus();
877      }.bind(this), 0);
878    }.bind(this));
879
880    this.initDataTransferOperations_();
881
882    this.initContextMenus_();
883    this.initCommands_();
884
885    this.updateFileTypeFilter_();
886
887    this.selectionHandler_.onFileSelectionChanged();
888
889    this.table_.endBatchUpdates();
890    this.grid_.endBatchUpdates();
891
892    callback();
893  };
894
895  /**
896   * If |item| in the directory tree is behind the preview panel, scrolls up the
897   * parent view and make the item visible. This should be called when:
898   *  - the selected item is changed in the directory tree.
899   *  - the visibility of the the preview panel is changed.
900   *
901   * @private
902   */
903  FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
904      function() {
905    var selectedSubTree = this.directoryTree_.selectedItem;
906    if (!selectedSubTree)
907      return;
908    var item = selectedSubTree.rowElement;
909    var parentView = this.directoryTree_;
910
911    var itemRect = item.getBoundingClientRect();
912    if (!itemRect)
913      return;
914
915    var listRect = parentView.getBoundingClientRect();
916    if (!listRect)
917      return;
918
919    var previewPanel = this.dialogDom_.querySelector('.preview-panel');
920    var previewPanelRect = previewPanel.getBoundingClientRect();
921    var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
922
923    var itemBottom = itemRect.bottom;
924    var listBottom = listRect.bottom - panelHeight;
925
926    if (itemBottom > listBottom) {
927      var scrollOffset = itemBottom - listBottom;
928      parentView.scrollTop += scrollOffset;
929    }
930  };
931
932  /**
933   * @private
934   */
935  FileManager.prototype.initDateTimeFormatters_ = function() {
936    var use12hourClock = !this.preferences_['use24hourClock'];
937    this.table_.setDateTimeFormat(use12hourClock);
938  };
939
940  /**
941   * @private
942   */
943  FileManager.prototype.initDataTransferOperations_ = function() {
944    this.fileOperationManager_ =
945        this.backgroundPage_.background.fileOperationManager;
946
947    // CopyManager are required for 'Delete' operation in
948    // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
949    if (this.dialogType != DialogType.FULL_PAGE) return;
950
951    // TODO(hidehiko): Extract FileOperationManager related code from
952    // FileManager to simplify it.
953    this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
954    this.fileOperationManager_.addEventListener(
955        'copy-progress', this.onCopyProgressBound_);
956
957    this.onEntriesChangedBound_ = this.onEntriesChanged_.bind(this);
958    this.fileOperationManager_.addEventListener(
959        'entries-changed', this.onEntriesChangedBound_);
960
961    var controller = this.fileTransferController_ =
962        new FileTransferController(
963                this.document_,
964                this.fileOperationManager_,
965                this.metadataCache_,
966                this.directoryModel_,
967                this.volumeManager_,
968                this.ui_.multiProfileShareDialog,
969                this.backgroundPage_.background.progressCenter);
970    controller.attachDragSource(this.table_.list);
971    controller.attachFileListDropTarget(this.table_.list);
972    controller.attachDragSource(this.grid_);
973    controller.attachFileListDropTarget(this.grid_);
974    controller.attachTreeDropTarget(this.directoryTree_);
975    controller.attachCopyPasteHandlers();
976    controller.addEventListener('selection-copied',
977        this.blinkSelection.bind(this));
978    controller.addEventListener('selection-cut',
979        this.blinkSelection.bind(this));
980    controller.addEventListener('source-not-found',
981        this.onSourceNotFound_.bind(this));
982  };
983
984  /**
985   * Handles an error that the source entry of file operation is not found.
986   * @private
987   */
988  FileManager.prototype.onSourceNotFound_ = function(event) {
989    var item = new ProgressCenterItem();
990    item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
991    if (event.progressType === ProgressItemType.COPY)
992      item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
993    else if (event.progressType === ProgressItemType.MOVE)
994      item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
995    item.state = ProgressItemState.ERROR;
996    this.backgroundPage_.background.progressCenter.updateItem(item);
997    this.sourceNotFoundErrorCount_++;
998  };
999
1000  /**
1001   * One-time initialization of context menus.
1002   * @private
1003   */
1004  FileManager.prototype.initContextMenus_ = function() {
1005    this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
1006    cr.ui.Menu.decorate(this.fileContextMenu_);
1007
1008    cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
1009    cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
1010        this.fileContextMenu_);
1011    cr.ui.contextMenuHandler.setContextMenu(
1012        this.document_.querySelector('.drive-welcome.page'),
1013        this.fileContextMenu_);
1014
1015    this.rootsContextMenu_ =
1016        this.dialogDom_.querySelector('#roots-context-menu');
1017    cr.ui.Menu.decorate(this.rootsContextMenu_);
1018    this.directoryTree_.contextMenuForRootItems = this.rootsContextMenu_;
1019
1020    this.directoryTreeContextMenu_ =
1021        this.dialogDom_.querySelector('#directory-tree-context-menu');
1022    cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
1023    this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
1024
1025    this.textContextMenu_ =
1026        this.dialogDom_.querySelector('#text-context-menu');
1027    cr.ui.Menu.decorate(this.textContextMenu_);
1028
1029    this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
1030    this.gearButton_.addEventListener('menushow',
1031        this.onShowGearMenu_.bind(this));
1032
1033    this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
1034        'menuitem, hr';
1035    cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
1036
1037    this.syncButton.checkable = true;
1038    this.hostedButton.checkable = true;
1039
1040    if (util.runningInBrowser()) {
1041      // Suppresses the default context menu.
1042      this.dialogDom_.addEventListener('contextmenu', function(e) {
1043        e.preventDefault();
1044        e.stopPropagation();
1045      });
1046    }
1047  };
1048
1049  FileManager.prototype.onShowGearMenu_ = function() {
1050    this.refreshRemainingSpace_(false);  /* Without loading caption. */
1051
1052    // If the menu is opened while CTRL key pressed, secret menu itemscan be
1053    // shown.
1054    this.isSecretGearMenuShown_ = this.pressingCtrl_;
1055
1056    // Update view of drive-related settings.
1057    this.commandHandler.updateAvailability();
1058    this.document_.getElementById('drive-separator').hidden =
1059        !this.shouldShowDriveSettings();
1060
1061    // Force to update the gear menu position.
1062    // TODO(hirono): Remove the workaround for the crbug.com/374093 after fixing
1063    // it.
1064    var gearMenu = this.document_.querySelector('#gear-menu');
1065    gearMenu.style.left = '';
1066    gearMenu.style.right = '';
1067    gearMenu.style.top = '';
1068    gearMenu.style.bottom = '';
1069  };
1070
1071  /**
1072   * One-time initialization of commands.
1073   * @private
1074   */
1075  FileManager.prototype.initCommands_ = function() {
1076    this.commandHandler = new CommandHandler(this);
1077
1078    // TODO(hirono): Move the following block to the UI part.
1079    var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
1080    for (var j = 0; j < commandButtons.length; j++)
1081      CommandButton.decorate(commandButtons[j]);
1082
1083    var inputs = this.dialogDom_.querySelectorAll(
1084        'input[type=text], input[type=search], textarea');
1085    for (var i = 0; i < inputs.length; i++) {
1086      cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
1087      this.registerInputCommands_(inputs[i]);
1088    }
1089
1090    cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
1091                                            this.textContextMenu_);
1092    this.registerInputCommands_(this.renameInput_);
1093    this.document_.addEventListener('command',
1094                                    this.setNoHover_.bind(this, true));
1095  };
1096
1097  /**
1098   * Registers cut, copy, paste and delete commands on input element.
1099   *
1100   * @param {Node} node Text input element to register on.
1101   * @private
1102   */
1103  FileManager.prototype.registerInputCommands_ = function(node) {
1104    CommandUtil.forceDefaultHandler(node, 'cut');
1105    CommandUtil.forceDefaultHandler(node, 'copy');
1106    CommandUtil.forceDefaultHandler(node, 'paste');
1107    CommandUtil.forceDefaultHandler(node, 'delete');
1108    node.addEventListener('keydown', function(e) {
1109      var key = util.getKeyModifiers(e) + e.keyCode;
1110      if (key === '190' /* '/' */ || key === '191' /* '.' */) {
1111        // If this key event is propagated, this is handled search command,
1112        // which calls 'preventDefault' method.
1113        e.stopPropagation();
1114      }
1115    });
1116  };
1117
1118  /**
1119   * Entry point of the initialization.
1120   * This method is called from main.js.
1121   */
1122  FileManager.prototype.initializeCore = function() {
1123    this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
1124    this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
1125                              [], 'initBackgroundPage');
1126    this.initializeQueue_.add(this.initPreferences_.bind(this),
1127                              ['initGeneral'], 'initPreferences');
1128    this.initializeQueue_.add(this.initVolumeManager_.bind(this),
1129                              ['initGeneral', 'initBackgroundPage'],
1130                              'initVolumeManager');
1131
1132    this.initializeQueue_.run();
1133    window.addEventListener('pagehide', this.onUnload_.bind(this));
1134  };
1135
1136  FileManager.prototype.initializeUI = function(dialogDom, callback) {
1137    this.dialogDom_ = dialogDom;
1138    this.document_ = this.dialogDom_.ownerDocument;
1139
1140    this.initializeQueue_.add(
1141        this.initEssentialUI_.bind(this),
1142        ['initGeneral', 'initBackgroundPage'],
1143        'initEssentialUI');
1144    this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
1145        ['initEssentialUI'], 'initAdditionalUI');
1146    this.initializeQueue_.add(
1147        this.initFileSystemUI_.bind(this),
1148        ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
1149
1150    // Run again just in case if all pending closures have completed and the
1151    // queue has stopped and monitor the completion.
1152    this.initializeQueue_.run(callback);
1153  };
1154
1155  /**
1156   * Initializes general purpose basic things, which are used by other
1157   * initializing methods.
1158   *
1159   * @param {function()} callback Completion callback.
1160   * @private
1161   */
1162  FileManager.prototype.initGeneral_ = function(callback) {
1163    // Initialize the application state.
1164    // TODO(mtomasz): Unify window.appState with location.search format.
1165    if (window.appState) {
1166      this.params_ = window.appState.params || {};
1167      this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
1168      this.initSelectionURL_ = window.appState.selectionURL;
1169      this.initTargetName_ = window.appState.targetName;
1170    } else {
1171      // Used by the select dialog only.
1172      this.params_ = location.search ?
1173                     JSON.parse(decodeURIComponent(location.search.substr(1))) :
1174                     {};
1175      this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
1176      this.initSelectionURL_ = this.params_.selectionURL;
1177      this.initTargetName_ = this.params_.targetName;
1178    }
1179
1180    // Initialize the member variables that depend this.params_.
1181    this.dialogType = this.params_.type || DialogType.FULL_PAGE;
1182    this.startupPrefName_ = 'file-manager-' + this.dialogType;
1183    this.fileTypes_ = this.params_.typeList || [];
1184
1185    callback();
1186  };
1187
1188  /**
1189   * Initialize the background page.
1190   * @param {function()} callback Completion callback.
1191   * @private
1192   */
1193  FileManager.prototype.initBackgroundPage_ = function(callback) {
1194    chrome.runtime.getBackgroundPage(function(backgroundPage) {
1195      this.backgroundPage_ = backgroundPage;
1196      this.backgroundPage_.background.ready(function() {
1197        loadTimeData.data = this.backgroundPage_.background.stringData;
1198        if (util.runningInBrowser())
1199          this.backgroundPage_.registerDialog(window);
1200        callback();
1201      }.bind(this));
1202    }.bind(this));
1203  };
1204
1205  /**
1206   * Initializes the VolumeManager instance.
1207   * @param {function()} callback Completion callback.
1208   * @private
1209   */
1210  FileManager.prototype.initVolumeManager_ = function(callback) {
1211    // Auto resolving to local path does not work for folders (e.g., dialog for
1212    // loading unpacked extensions).
1213    var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
1214
1215    // If this condition is false, VolumeManagerWrapper hides all drive
1216    // related event and data, even if Drive is enabled on preference.
1217    // In other words, even if Drive is disabled on preference but Files.app
1218    // should show Drive when it is re-enabled, then the value should be set to
1219    // true.
1220    // Note that the Drive enabling preference change is listened by
1221    // DriveIntegrationService, so here we don't need to take care about it.
1222    var driveEnabled =
1223        !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
1224    this.volumeManager_ = new VolumeManagerWrapper(
1225        driveEnabled, this.backgroundPage_);
1226    callback();
1227  };
1228
1229  /**
1230   * One time initialization of the Files.app's essential UI elements. These
1231   * elements will be shown to the user. Only visible elements should be
1232   * initialized here. Any heavy operation should be avoided. Files.app's
1233   * window is shown at the end of this routine.
1234   *
1235   * @param {function()} callback Completion callback.
1236   * @private
1237   */
1238  FileManager.prototype.initEssentialUI_ = function(callback) {
1239    // Record stats of dialog types. New values must NOT be inserted into the
1240    // array enumerating the types. It must be in sync with
1241    // FileDialogType enum in tools/metrics/histograms/histogram.xml.
1242    metrics.recordEnum('Create', this.dialogType,
1243        [DialogType.SELECT_FOLDER,
1244         DialogType.SELECT_UPLOAD_FOLDER,
1245         DialogType.SELECT_SAVEAS_FILE,
1246         DialogType.SELECT_OPEN_FILE,
1247         DialogType.SELECT_OPEN_MULTI_FILE,
1248         DialogType.FULL_PAGE]);
1249
1250    // Create the metadata cache.
1251    this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
1252
1253    // Create the root view of FileManager.
1254    this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
1255    this.fileTypeSelector_ = this.ui_.fileTypeSelector;
1256    this.okButton_ = this.ui_.okButton;
1257    this.cancelButton_ = this.ui_.cancelButton;
1258
1259    // Show the window as soon as the UI pre-initialization is done.
1260    if (this.dialogType == DialogType.FULL_PAGE && !util.runningInBrowser()) {
1261      chrome.app.window.current().show();
1262      setTimeout(callback, 100);  // Wait until the animation is finished.
1263    } else {
1264      callback();
1265    }
1266  };
1267
1268  /**
1269   * One-time initialization of dialogs.
1270   * @private
1271   */
1272  FileManager.prototype.initDialogs_ = function() {
1273    // Initialize the dialog.
1274    this.ui_.initDialogs();
1275    FileManagerDialogBase.setFileManager(this);
1276
1277    // Obtains the dialog instances from FileManagerUI.
1278    // TODO(hirono): Remove the properties from the FileManager class.
1279    this.error = this.ui_.errorDialog;
1280    this.alert = this.ui_.alertDialog;
1281    this.confirm = this.ui_.confirmDialog;
1282    this.prompt = this.ui_.promptDialog;
1283    this.shareDialog_ = this.ui_.shareDialog;
1284    this.defaultTaskPicker = this.ui_.defaultTaskPicker;
1285    this.suggestAppsDialog = this.ui_.suggestAppsDialog;
1286  };
1287
1288  /**
1289   * One-time initialization of various DOM nodes. Loads the additional DOM
1290   * elements visible to the user. Initialize here elements, which are expensive
1291   * or hidden in the beginning.
1292   *
1293   * @param {function()} callback Completion callback.
1294   * @private
1295   */
1296  FileManager.prototype.initAdditionalUI_ = function(callback) {
1297    this.initDialogs_();
1298    this.ui_.initAdditionalUI();
1299
1300    this.dialogDom_.addEventListener('drop', function(e) {
1301      // Prevent opening an URL by dropping it onto the page.
1302      e.preventDefault();
1303    });
1304
1305    this.dialogDom_.addEventListener('click',
1306                                     this.onExternalLinkClick_.bind(this));
1307    // Cache nodes we'll be manipulating.
1308    var dom = this.dialogDom_;
1309
1310    this.filenameInput_ = dom.querySelector('#filename-input-box input');
1311    this.taskItems_ = dom.querySelector('#tasks');
1312
1313    this.table_ = dom.querySelector('.detail-table');
1314    this.grid_ = dom.querySelector('.thumbnail-grid');
1315    this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
1316    this.showSpinner_(true);
1317
1318    var fullPage = this.dialogType == DialogType.FULL_PAGE;
1319    FileTable.decorate(
1320        this.table_, this.metadataCache_, this.volumeManager_, fullPage);
1321    FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_);
1322
1323    this.ui_.locationBreadcrumbs = new BreadcrumbsController(
1324        dom.querySelector('#location-breadcrumbs'),
1325        this.metadataCache_,
1326        this.volumeManager_);
1327    this.ui_.locationBreadcrumbs.addEventListener(
1328        'pathclick', this.onBreadcrumbClick_.bind(this));
1329
1330    this.previewPanel_ = new PreviewPanel(
1331        dom.querySelector('.preview-panel'),
1332        DialogType.isOpenDialog(this.dialogType) ?
1333            PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
1334            PreviewPanel.VisibilityType.AUTO,
1335        this.metadataCache_,
1336        this.volumeManager_);
1337    this.previewPanel_.addEventListener(
1338        PreviewPanel.Event.VISIBILITY_CHANGE,
1339        this.onPreviewPanelVisibilityChange_.bind(this));
1340    this.previewPanel_.initialize();
1341
1342    // Initialize progress center panel.
1343    this.progressCenterPanel_ = new ProgressCenterPanel(
1344        dom.querySelector('#progress-center'));
1345    this.backgroundPage_.background.progressCenter.addPanel(
1346        this.progressCenterPanel_);
1347
1348    this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
1349    this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
1350
1351    this.renameInput_ = this.document_.createElement('input');
1352    this.renameInput_.className = 'rename entry-name';
1353
1354    this.renameInput_.addEventListener(
1355        'keydown', this.onRenameInputKeyDown_.bind(this));
1356    this.renameInput_.addEventListener(
1357        'blur', this.onRenameInputBlur_.bind(this));
1358
1359    // TODO(hirono): Rename the handler after creating the DialogFooter class.
1360    this.filenameInput_.addEventListener(
1361        'input', this.onFilenameInputInput_.bind(this));
1362    this.filenameInput_.addEventListener(
1363        'keydown', this.onFilenameInputKeyDown_.bind(this));
1364    this.filenameInput_.addEventListener(
1365        'focus', this.onFilenameInputFocus_.bind(this));
1366
1367    this.listContainer_ = this.dialogDom_.querySelector('#list-container');
1368    this.listContainer_.addEventListener(
1369        'keydown', this.onListKeyDown_.bind(this));
1370    this.listContainer_.addEventListener(
1371        'keypress', this.onListKeyPress_.bind(this));
1372    this.listContainer_.addEventListener(
1373        'mousemove', this.onListMouseMove_.bind(this));
1374
1375    this.okButton_.addEventListener('click', this.onOk_.bind(this));
1376    this.onCancelBound_ = this.onCancel_.bind(this);
1377    this.cancelButton_.addEventListener('click', this.onCancelBound_);
1378
1379    this.decorateSplitter(
1380        this.dialogDom_.querySelector('#navigation-list-splitter'));
1381
1382    this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
1383
1384    this.syncButton = this.dialogDom_.querySelector(
1385        '#gear-menu-drive-sync-settings');
1386    this.hostedButton = this.dialogDom_.querySelector(
1387        '#gear-menu-drive-hosted-settings');
1388
1389    this.ui_.toggleViewButton.addEventListener('click',
1390        this.onToggleViewButtonClick_.bind(this));
1391
1392    cr.ui.ComboButton.decorate(this.taskItems_);
1393    this.taskItems_.showMenu = function(shouldSetFocus) {
1394      // Prevent the empty menu from opening.
1395      if (!this.menu.length)
1396        return;
1397      cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
1398    };
1399    this.taskItems_.addEventListener('select',
1400        this.onTaskItemClicked_.bind(this));
1401
1402    this.dialogDom_.ownerDocument.defaultView.addEventListener(
1403        'resize', this.onResize_.bind(this));
1404
1405    this.searchBox_ = this.ui_.searchBox.inputElement;
1406    this.searchBox_.addEventListener(
1407        'input', this.onSearchBoxUpdate_.bind(this));
1408    this.ui_.searchBox.clearButton.addEventListener(
1409        'click', this.onSearchClearButtonClick_.bind(this));
1410
1411    this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
1412    this.autocompleteList_.requestSuggestions =
1413        this.requestAutocompleteSuggestions_.bind(this);
1414
1415    // Instead, open the suggested item when Enter key is pressed or
1416    // mouse-clicked.
1417    this.autocompleteList_.handleEnterKeydown = function(event) {
1418      this.openAutocompleteSuggestion_();
1419      this.lastAutocompleteQuery_ = '';
1420      this.autocompleteList_.suggestions = [];
1421    }.bind(this);
1422    this.autocompleteList_.addEventListener('mousedown', function(event) {
1423      this.openAutocompleteSuggestion_();
1424      this.lastAutocompleteQuery_ = '';
1425      this.autocompleteList_.suggestions = [];
1426    }.bind(this));
1427
1428    this.defaultActionMenuItem_ =
1429        this.dialogDom_.querySelector('#default-action');
1430
1431    this.openWithCommand_ =
1432        this.dialogDom_.querySelector('#open-with');
1433
1434    this.defaultActionMenuItem_.addEventListener('activate',
1435        this.dispatchSelectionAction_.bind(this));
1436
1437    this.initFileTypeFilter_();
1438
1439    util.addIsFocusedMethod();
1440
1441    // Populate the static localized strings.
1442    i18nTemplate.process(this.document_, loadTimeData);
1443
1444    // Arrange the file list.
1445    this.table_.normalizeColumns();
1446    this.table_.redraw();
1447
1448    callback();
1449  };
1450
1451  /**
1452   * @param {Event} event Click event.
1453   * @private
1454   */
1455  FileManager.prototype.onBreadcrumbClick_ = function(event) {
1456    this.directoryModel_.changeDirectoryEntry(event.entry);
1457  };
1458
1459  /**
1460   * Constructs table and grid (heavy operation).
1461   * @private
1462   **/
1463  FileManager.prototype.initFileList_ = function() {
1464    // Always sharing the data model between the detail/thumb views confuses
1465    // them.  Instead we maintain this bogus data model, and hook it up to the
1466    // view that is not in use.
1467    this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
1468    this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
1469
1470    var singleSelection =
1471        this.dialogType == DialogType.SELECT_OPEN_FILE ||
1472        this.dialogType == DialogType.SELECT_FOLDER ||
1473        this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
1474        this.dialogType == DialogType.SELECT_SAVEAS_FILE;
1475
1476    this.fileFilter_ = new FileFilter(
1477        this.metadataCache_,
1478        false  /* Don't show dot files and *.crdownload by default. */);
1479
1480    this.fileWatcher_ = new FileWatcher(this.metadataCache_);
1481    this.fileWatcher_.addEventListener(
1482        'watcher-metadata-changed',
1483        this.onWatcherMetadataChanged_.bind(this));
1484
1485    this.directoryModel_ = new DirectoryModel(
1486        singleSelection,
1487        this.fileFilter_,
1488        this.fileWatcher_,
1489        this.metadataCache_,
1490        this.volumeManager_);
1491
1492    this.folderShortcutsModel_ = new FolderShortcutsDataModel(
1493        this.volumeManager_);
1494
1495    this.selectionHandler_ = new FileSelectionHandler(this);
1496
1497    var dataModel = this.directoryModel_.getFileList();
1498    dataModel.addEventListener('permuted',
1499                               this.updateStartupPrefs_.bind(this));
1500
1501    this.directoryModel_.getFileListSelection().addEventListener('change',
1502        this.selectionHandler_.onFileSelectionChanged.bind(
1503            this.selectionHandler_));
1504
1505    this.initList_(this.grid_);
1506    this.initList_(this.table_.list);
1507
1508    var fileListFocusBound = this.onFileListFocus_.bind(this);
1509    this.table_.list.addEventListener('focus', fileListFocusBound);
1510    this.grid_.addEventListener('focus', fileListFocusBound);
1511
1512    var draggingBound = this.onDragging_.bind(this);
1513    var dragEndBound = this.onDragEnd_.bind(this);
1514
1515    // Listen to drag events to hide preview panel while user is dragging files.
1516    // Files.app prevents default actions in 'dragstart' in some situations,
1517    // so we listen to 'drag' to know the list is actually being dragged.
1518    this.table_.list.addEventListener('drag', draggingBound);
1519    this.grid_.addEventListener('drag', draggingBound);
1520    this.table_.list.addEventListener('dragend', dragEndBound);
1521    this.grid_.addEventListener('dragend', dragEndBound);
1522
1523    // Listen to dragselection events to hide preview panel while the user is
1524    // selecting files by drag operation.
1525    this.table_.list.addEventListener('dragselectionstart', draggingBound);
1526    this.grid_.addEventListener('dragselectionstart', draggingBound);
1527    this.table_.list.addEventListener('dragselectionend', dragEndBound);
1528    this.grid_.addEventListener('dragselectionend', dragEndBound);
1529
1530    // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
1531    // attach the directory model.
1532    this.initDirectoryTree_();
1533
1534    this.table_.addEventListener('column-resize-end',
1535                                 this.updateStartupPrefs_.bind(this));
1536
1537    // Restore preferences.
1538    this.directoryModel_.getFileList().sort(
1539        this.viewOptions_.sortField || 'modificationTime',
1540        this.viewOptions_.sortDirection || 'desc');
1541    if (this.viewOptions_.columns) {
1542      var cm = this.table_.columnModel;
1543      for (var i = 0; i < cm.totalSize; i++) {
1544        if (this.viewOptions_.columns[i] > 0)
1545          cm.setWidth(i, this.viewOptions_.columns[i]);
1546      }
1547    }
1548    this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1549
1550    this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1551
1552    if (this.closeOnUnmount_) {
1553      this.volumeManager_.addEventListener('externally-unmounted',
1554          this.onExternallyUnmounted_.bind(this));
1555    }
1556
1557    // Update metadata to change 'Today' and 'Yesterday' dates.
1558    var today = new Date();
1559    today.setHours(0);
1560    today.setMinutes(0);
1561    today.setSeconds(0);
1562    today.setMilliseconds(0);
1563    setTimeout(this.dailyUpdateModificationTime_.bind(this),
1564               today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1565  };
1566
1567  /**
1568   * @private
1569   */
1570  FileManager.prototype.initDirectoryTree_ = function() {
1571    var fakeEntriesVisible =
1572        this.dialogType !== DialogType.SELECT_SAVEAS_FILE;
1573    this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1574    DirectoryTree.decorate(this.directoryTree_,
1575                           this.directoryModel_,
1576                           this.volumeManager_,
1577                           this.metadataCache_,
1578                           fakeEntriesVisible);
1579    this.directoryTree_.dataModel = new NavigationListModel(
1580        this.volumeManager_, this.folderShortcutsModel_);
1581
1582    // Visible height of the directory tree depends on the size of progress
1583    // center panel. When the size of progress center panel changes, directory
1584    // tree has to be notified to adjust its components (e.g. progress bar).
1585    var observer = new MutationObserver(
1586        this.directoryTree_.relayout.bind(this.directoryTree_));
1587    observer.observe(this.progressCenterPanel_.element,
1588                     {subtree: true, attributes: true, childList: true});
1589  };
1590
1591  /**
1592   * @private
1593   */
1594  FileManager.prototype.updateStartupPrefs_ = function() {
1595    var sortStatus = this.directoryModel_.getFileList().sortStatus;
1596    var prefs = {
1597      sortField: sortStatus.field,
1598      sortDirection: sortStatus.direction,
1599      columns: [],
1600      listType: this.listType_
1601    };
1602    var cm = this.table_.columnModel;
1603    for (var i = 0; i < cm.totalSize; i++) {
1604      prefs.columns.push(cm.getWidth(i));
1605    }
1606    // Save the global default.
1607    var items = {};
1608    items[this.startupPrefName_] = JSON.stringify(prefs);
1609    chrome.storage.local.set(items);
1610
1611    // Save the window-specific preference.
1612    if (window.appState) {
1613      window.appState.viewOptions = prefs;
1614      util.saveAppState();
1615    }
1616  };
1617
1618  FileManager.prototype.refocus = function() {
1619    var targetElement;
1620    if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1621      targetElement = this.filenameInput_;
1622    else
1623      targetElement = this.currentList_;
1624
1625    // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1626    // shown. Focus to a button on the dialog instead.
1627    if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1628      targetElement = document.querySelector('button:not([tabIndex="-1"])');
1629
1630    if (targetElement)
1631      targetElement.focus();
1632  };
1633
1634  /**
1635   * File list focus handler. Used to select the top most element on the list
1636   * if nothing was selected.
1637   *
1638   * @private
1639   */
1640  FileManager.prototype.onFileListFocus_ = function() {
1641    // If the file list is focused by <Tab>, select the first item if no item
1642    // is selected.
1643    if (this.pressingTab_) {
1644      if (this.getSelection() && this.getSelection().totalCount == 0)
1645        this.directoryModel_.selectIndex(0);
1646    }
1647  };
1648
1649  /**
1650   * Index of selected item in the typeList of the dialog params.
1651   *
1652   * @return {number} 1-based index of selected type or 0 if no type selected.
1653   * @private
1654   */
1655  FileManager.prototype.getSelectedFilterIndex_ = function() {
1656    var index = Number(this.fileTypeSelector_.selectedIndex);
1657    if (index < 0)  // Nothing selected.
1658      return 0;
1659    if (this.params_.includeAllFiles)  // Already 1-based.
1660      return index;
1661    return index + 1;  // Convert to 1-based;
1662  };
1663
1664  FileManager.prototype.setListType = function(type) {
1665    if (type && type == this.listType_)
1666      return;
1667
1668    this.table_.list.startBatchUpdates();
1669    this.grid_.startBatchUpdates();
1670
1671    // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1672    // cause any UI bugs. Currently, the only right way is first to set display
1673    // style and only then set dataModel.
1674
1675    if (type == FileManager.ListType.DETAIL) {
1676      this.table_.dataModel = this.directoryModel_.getFileList();
1677      this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1678      this.table_.hidden = false;
1679      this.grid_.hidden = true;
1680      this.grid_.selectionModel = this.emptySelectionModel_;
1681      this.grid_.dataModel = this.emptyDataModel_;
1682      this.table_.hidden = false;
1683      /** @type {cr.ui.List} */
1684      this.currentList_ = this.table_.list;
1685      this.ui_.toggleViewButton.classList.remove('table');
1686      this.ui_.toggleViewButton.classList.add('grid');
1687    } else if (type == FileManager.ListType.THUMBNAIL) {
1688      this.grid_.dataModel = this.directoryModel_.getFileList();
1689      this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1690      this.grid_.hidden = false;
1691      this.table_.hidden = true;
1692      this.table_.selectionModel = this.emptySelectionModel_;
1693      this.table_.dataModel = this.emptyDataModel_;
1694      this.grid_.hidden = false;
1695      /** @type {cr.ui.List} */
1696      this.currentList_ = this.grid_;
1697      this.ui_.toggleViewButton.classList.remove('grid');
1698      this.ui_.toggleViewButton.classList.add('table');
1699    } else {
1700      throw new Error('Unknown list type: ' + type);
1701    }
1702
1703    this.listType_ = type;
1704    this.updateStartupPrefs_();
1705    this.onResize_();
1706
1707    this.table_.list.endBatchUpdates();
1708    this.grid_.endBatchUpdates();
1709  };
1710
1711  /**
1712   * Initialize the file list table or grid.
1713   *
1714   * @param {cr.ui.List} list The list.
1715   * @private
1716   */
1717  FileManager.prototype.initList_ = function(list) {
1718    // Overriding the default role 'list' to 'listbox' for better accessibility
1719    // on ChromeOS.
1720    list.setAttribute('role', 'listbox');
1721    list.addEventListener('click', this.onDetailClick_.bind(this));
1722    list.id = 'file-list';
1723  };
1724
1725  /**
1726   * @private
1727   */
1728  FileManager.prototype.onCopyProgress_ = function(event) {
1729    if (event.reason == 'ERROR' &&
1730        event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1731        event.error.data.toDrive &&
1732        event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1733      this.alert.showHtml(
1734          strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1735          strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1736              decodeURIComponent(
1737                  event.error.data.sourceFileUrl.split('/').pop()),
1738              str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1739    }
1740  };
1741
1742  /**
1743   * Handler of file manager operations. Called when an entry has been
1744   * changed.
1745   * This updates directory model to reflect operation result immediately (not
1746   * waiting for directory update event). Also, preloads thumbnails for the
1747   * images of new entries.
1748   * See also FileOperationManager.EventRouter.
1749   *
1750   * @param {Event} event An event for the entry change.
1751   * @private
1752   */
1753  FileManager.prototype.onEntriesChanged_ = function(event) {
1754    var kind = event.kind;
1755    var entries = event.entries;
1756    this.directoryModel_.onEntriesChanged(kind, entries);
1757    this.selectionHandler_.onFileSelectionChanged();
1758
1759    if (kind !== util.EntryChangedKind.CREATED)
1760      return;
1761
1762    var preloadThumbnail = function(entry) {
1763      var locationInfo = this.volumeManager_.getLocationInfo(entry);
1764      if (!locationInfo)
1765        return;
1766      this.metadataCache_.getOne(entry, 'thumbnail|external',
1767          function(metadata) {
1768            var thumbnailLoader_ = new ThumbnailLoader(
1769                entry,
1770                ThumbnailLoader.LoaderType.CANVAS,
1771                metadata,
1772                undefined,  // Media type.
1773                locationInfo.isDriveBased ?
1774                    ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1775                    ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1776                10);  // Very low priority.
1777            thumbnailLoader_.loadDetachedImage(function(success) {});
1778          });
1779    }.bind(this);
1780
1781    for (var i = 0; i < entries.length; i++) {
1782      // Preload a thumbnail if the new copied entry an image.
1783      if (FileType.isImage(entries[i]))
1784        preloadThumbnail(entries[i]);
1785    }
1786  };
1787
1788  /**
1789   * Fills the file type list or hides it.
1790   * @private
1791   */
1792  FileManager.prototype.initFileTypeFilter_ = function() {
1793    if (this.params_.includeAllFiles) {
1794      var option = this.document_.createElement('option');
1795      option.innerText = str('ALL_FILES_FILTER');
1796      this.fileTypeSelector_.appendChild(option);
1797      option.value = 0;
1798    }
1799
1800    for (var i = 0; i !== this.fileTypes_.length; i++) {
1801      var fileType = this.fileTypes_[i];
1802      var option = this.document_.createElement('option');
1803      var description = fileType.description;
1804      if (!description) {
1805        // See if all the extensions in the group have the same description.
1806        for (var j = 0; j !== fileType.extensions.length; j++) {
1807          var currentDescription = FileType.typeToString(
1808              FileType.getTypeForName('.' + fileType.extensions[j]));
1809          if (!description)  // Set the first time.
1810            description = currentDescription;
1811          else if (description != currentDescription) {
1812            // No single description, fall through to the extension list.
1813            description = null;
1814            break;
1815          }
1816        }
1817
1818        if (!description)
1819          // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1820          description = fileType.extensions.map(function(s) {
1821            return '*.' + s;
1822          }).join(', ');
1823      }
1824      option.innerText = description;
1825
1826      option.value = i + 1;
1827
1828      if (fileType.selected)
1829        option.selected = true;
1830
1831      this.fileTypeSelector_.appendChild(option);
1832    }
1833
1834    var options = this.fileTypeSelector_.querySelectorAll('option');
1835    if (options.length >= 2) {
1836      // There is in fact no choice, show the selector.
1837      this.fileTypeSelector_.hidden = false;
1838
1839      this.fileTypeSelector_.addEventListener('change',
1840          this.updateFileTypeFilter_.bind(this));
1841    }
1842  };
1843
1844  /**
1845   * Filters file according to the selected file type.
1846   * @private
1847   */
1848  FileManager.prototype.updateFileTypeFilter_ = function() {
1849    this.fileFilter_.removeFilter('fileType');
1850    var selectedIndex = this.getSelectedFilterIndex_();
1851    if (selectedIndex > 0) { // Specific filter selected.
1852      var regexp = new RegExp('\\.(' +
1853          this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1854      var filter = function(entry) {
1855        return entry.isDirectory || regexp.test(entry.name);
1856      };
1857      this.fileFilter_.addFilter('fileType', filter);
1858
1859      // In save dialog, update the destination name extension.
1860      if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1861        var current = this.filenameInput_.value;
1862        var newExt = this.fileTypes_[selectedIndex - 1].extensions[0];
1863        if (newExt && !regexp.test(current)) {
1864          var i = current.lastIndexOf('.');
1865          if (i >= 0) {
1866            this.filenameInput_.value = current.substr(0, i) + '.' + newExt;
1867            this.selectTargetNameInFilenameInput_();
1868          }
1869        }
1870      }
1871    }
1872  };
1873
1874  /**
1875   * Resize details and thumb views to fit the new window size.
1876   * @private
1877   */
1878  FileManager.prototype.onResize_ = function() {
1879    if (this.listType_ == FileManager.ListType.THUMBNAIL)
1880      this.grid_.relayout();
1881    else
1882      this.table_.relayout();
1883
1884    // May not be available during initialization.
1885    if (this.directoryTree_)
1886      this.directoryTree_.relayout();
1887
1888    this.ui_.locationBreadcrumbs.truncate();
1889  };
1890
1891  /**
1892   * Handles local metadata changes in the currect directory.
1893   * @param {Event} event Change event.
1894   * @private
1895   */
1896  FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1897    this.updateMetadataInUI_(
1898        event.metadataType, event.entries, event.properties);
1899  };
1900
1901  /**
1902   * Resize details and thumb views to fit the new window size.
1903   * @private
1904   */
1905  FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1906    // This method may be called on initialization. Some object may not be
1907    // initialized.
1908
1909    var panelHeight = this.previewPanel_.visible ?
1910        this.previewPanel_.height : 0;
1911    if (this.grid_)
1912      this.grid_.setBottomMarginForPanel(panelHeight);
1913    if (this.table_)
1914      this.table_.setBottomMarginForPanel(panelHeight);
1915  };
1916
1917  /**
1918   * Invoked while the drag is being performed on the list or the grid.
1919   * Note: this method may be called multiple times before onDragEnd_().
1920   * @private
1921   */
1922  FileManager.prototype.onDragging_ = function() {
1923    // On open file dialog, the preview panel is always shown.
1924    if (DialogType.isOpenDialog(this.dialogType))
1925      return;
1926    this.previewPanel_.visibilityType =
1927        PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1928  };
1929
1930  /**
1931   * Invoked when the drag is ended on the list or the grid.
1932   * @private
1933   */
1934  FileManager.prototype.onDragEnd_ = function() {
1935    // On open file dialog, the preview panel is always shown.
1936    if (DialogType.isOpenDialog(this.dialogType))
1937      return;
1938    this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1939  };
1940
1941  /**
1942   * Sets up the current directory during initialization.
1943   * @private
1944   */
1945  FileManager.prototype.setupCurrentDirectory_ = function() {
1946    var tracker = this.directoryModel_.createDirectoryChangeTracker();
1947    var queue = new AsyncUtil.Queue();
1948
1949    // Wait until the volume manager is initialized.
1950    queue.run(function(callback) {
1951      tracker.start();
1952      this.volumeManager_.ensureInitialized(callback);
1953    }.bind(this));
1954
1955    var nextCurrentDirEntry;
1956    var selectionEntry;
1957
1958    // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1959    // in case of being a display root or a default directory to open files.
1960    queue.run(function(callback) {
1961      if (!this.initSelectionURL_) {
1962        callback();
1963        return;
1964      }
1965      webkitResolveLocalFileSystemURL(
1966          this.initSelectionURL_,
1967          function(inEntry) {
1968            var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1969            // If location information is not available, then the volume is
1970            // no longer (or never) available.
1971            if (!locationInfo) {
1972              callback();
1973              return;
1974            }
1975            // If the selection is root, then use it as a current directory
1976            // instead. This is because, selecting a root entry is done as
1977            // opening it.
1978            if (locationInfo.isRootEntry)
1979              nextCurrentDirEntry = inEntry;
1980
1981            // If this dialog attempts to open file(s) and the selection is a
1982            // directory, the selection should be the current directory.
1983            if (DialogType.isOpenFileDialog(this.dialogType) &&
1984                inEntry.isDirectory) {
1985              nextCurrentDirEntry = inEntry;
1986            }
1987
1988            // By default, the selection should be selected entry and the
1989            // parent directory of it should be the current directory.
1990            if (!nextCurrentDirEntry)
1991              selectionEntry = inEntry;
1992
1993            callback();
1994          }.bind(this), callback);
1995    }.bind(this));
1996    // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1997    // by the previous step).
1998    queue.run(function(callback) {
1999      if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
2000        callback();
2001        return;
2002      }
2003      webkitResolveLocalFileSystemURL(
2004          this.initCurrentDirectoryURL_,
2005          function(inEntry) {
2006            var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
2007            if (!locationInfo) {
2008              callback();
2009              return;
2010            }
2011            nextCurrentDirEntry = inEntry;
2012            callback();
2013          }.bind(this), callback);
2014      // TODO(mtomasz): Implement reopening on special search, when fake
2015      // entries are converted to directory providers.
2016    }.bind(this));
2017
2018    // If the directory to be changed to is not available, then first fallback
2019    // to the parent of the selection entry.
2020    queue.run(function(callback) {
2021      if (nextCurrentDirEntry || !selectionEntry) {
2022        callback();
2023        return;
2024      }
2025      selectionEntry.getParent(function(inEntry) {
2026        nextCurrentDirEntry = inEntry;
2027        callback();
2028      }.bind(this));
2029    }.bind(this));
2030
2031    // Check if the next current directory is not a virtual directory which is
2032    // not available in UI. This may happen to shared on Drive.
2033    queue.run(function(callback) {
2034      if (!nextCurrentDirEntry) {
2035        callback();
2036        return;
2037      }
2038      var locationInfo = this.volumeManager_.getLocationInfo(
2039          nextCurrentDirEntry);
2040      // If we can't check, assume that the directory is illegal.
2041      if (!locationInfo) {
2042        nextCurrentDirEntry = null;
2043        callback();
2044        return;
2045      }
2046      // Having root directory of DRIVE_OTHER here should be only for shared
2047      // with me files. Fallback to Drive root in such case.
2048      if (locationInfo.isRootEntry && locationInfo.rootType ===
2049              VolumeManagerCommon.RootType.DRIVE_OTHER) {
2050        var volumeInfo = this.volumeManager_.getVolumeInfo(nextCurrentDirEntry);
2051        if (!volumeInfo) {
2052          nextCurrentDirEntry = null;
2053          callback();
2054          return;
2055        }
2056        volumeInfo.resolveDisplayRoot().then(
2057            function(entry) {
2058              nextCurrentDirEntry = entry;
2059              callback();
2060            }).catch(function(error) {
2061              console.error(error.stack || error);
2062              nextCurrentDirEntry = null;
2063              callback();
2064            });
2065      } else {
2066        callback();
2067      }
2068    }.bind(this));
2069
2070    // If the directory to be changed to is still not resolved, then fallback
2071    // to the default display root.
2072    queue.run(function(callback) {
2073      if (nextCurrentDirEntry) {
2074        callback();
2075        return;
2076      }
2077      this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
2078        nextCurrentDirEntry = displayRoot;
2079        callback();
2080      }.bind(this));
2081    }.bind(this));
2082
2083    // If selection failed to be resolved (eg. didn't exist, in case of saving
2084    // a file, or in case of a fallback of the current directory, then try to
2085    // resolve again using the target name.
2086    queue.run(function(callback) {
2087      if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
2088        callback();
2089        return;
2090      }
2091      // Try to resolve as a file first. If it fails, then as a directory.
2092      nextCurrentDirEntry.getFile(
2093          this.initTargetName_,
2094          {},
2095          function(targetEntry) {
2096            selectionEntry = targetEntry;
2097            callback();
2098          }, function() {
2099            // Failed to resolve as a file
2100            nextCurrentDirEntry.getDirectory(
2101                this.initTargetName_,
2102                {},
2103                function(targetEntry) {
2104                  selectionEntry = targetEntry;
2105                  callback();
2106                }, function() {
2107                  // Failed to resolve as either file or directory.
2108                  callback();
2109                });
2110          }.bind(this));
2111    }.bind(this));
2112
2113    // Finalize.
2114    queue.run(function(callback) {
2115      // Check directory change.
2116      tracker.stop();
2117      if (tracker.hasChanged) {
2118        callback();
2119        return;
2120      }
2121      // Finish setup current directory.
2122      this.finishSetupCurrentDirectory_(
2123          nextCurrentDirEntry,
2124          selectionEntry,
2125          this.initTargetName_);
2126      callback();
2127    }.bind(this));
2128  };
2129
2130  /**
2131   * @param {DirectoryEntry} directoryEntry Directory to be opened.
2132   * @param {Entry=} opt_selectionEntry Entry to be selected.
2133   * @param {string=} opt_suggestedName Suggested name for a non-existing\
2134   *     selection.
2135   * @private
2136   */
2137  FileManager.prototype.finishSetupCurrentDirectory_ = function(
2138      directoryEntry, opt_selectionEntry, opt_suggestedName) {
2139    // Open the directory, and select the selection (if passed).
2140    if (util.isFakeEntry(directoryEntry)) {
2141      this.directoryModel_.specialSearch(directoryEntry, '');
2142    } else {
2143      this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
2144        if (opt_selectionEntry)
2145          this.directoryModel_.selectEntry(opt_selectionEntry);
2146      }.bind(this));
2147    }
2148
2149    if (this.dialogType === DialogType.FULL_PAGE) {
2150      // In the FULL_PAGE mode if the restored URL points to a file we might
2151      // have to invoke a task after selecting it.
2152      if (this.params_.action === 'select')
2153        return;
2154
2155      var task = null;
2156
2157      // TODO(mtomasz): Implement remounting archives after crash.
2158      //                See: crbug.com/333139
2159
2160      // If there is a task to be run, run it after the scan is completed.
2161      if (task) {
2162        var listener = function() {
2163          if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
2164                                directoryEntry)) {
2165            // Opened on a different URL. Probably fallbacked. Therefore,
2166            // do not invoke a task.
2167            return;
2168          }
2169          this.directoryModel_.removeEventListener(
2170              'scan-completed', listener);
2171          task();
2172        }.bind(this);
2173        this.directoryModel_.addEventListener('scan-completed', listener);
2174      }
2175    } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
2176      this.filenameInput_.value = opt_suggestedName || '';
2177      this.selectTargetNameInFilenameInput_();
2178    }
2179  };
2180
2181  /**
2182   * @private
2183   */
2184  FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
2185    var entries = this.directoryModel_.getFileList().slice();
2186    var directoryEntry = this.directoryModel_.getCurrentDirEntry();
2187    if (!directoryEntry)
2188      return;
2189    // We don't pass callback here. When new metadata arrives, we have an
2190    // observer registered to update the UI.
2191
2192    // TODO(dgozman): refresh content metadata only when modificationTime
2193    // changed.
2194    var isFakeEntry = util.isFakeEntry(directoryEntry);
2195    var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
2196    if (!isFakeEntry)
2197      this.metadataCache_.clearRecursively(directoryEntry, '*');
2198    this.metadataCache_.get(getEntries, 'filesystem|external', null);
2199
2200    var visibleItems = this.currentList_.items;
2201    var visibleEntries = [];
2202    for (var i = 0; i < visibleItems.length; i++) {
2203      var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
2204      var entry = this.directoryModel_.getFileList().item(index);
2205      // The following check is a workaround for the bug in list: sometimes item
2206      // does not have listIndex, and therefore is not found in the list.
2207      if (entry) visibleEntries.push(entry);
2208    }
2209    // Refreshes the metadata.
2210    this.metadataCache_.getLatest(visibleEntries, 'thumbnail', null);
2211  };
2212
2213  /**
2214   * @private
2215   */
2216  FileManager.prototype.dailyUpdateModificationTime_ = function() {
2217    var entries = this.directoryModel_.getFileList().slice();
2218    this.metadataCache_.get(
2219        entries,
2220        'filesystem',
2221        function() {
2222          this.updateMetadataInUI_('filesystem', entries);
2223        }.bind(this));
2224
2225    setTimeout(this.dailyUpdateModificationTime_.bind(this),
2226               MILLISECONDS_IN_DAY);
2227  };
2228
2229  /**
2230   * @param {string} type Type of metadata changed.
2231   * @param {Array.<Entry>} entries Array of entries.
2232   * @private
2233   */
2234  FileManager.prototype.updateMetadataInUI_ = function(type, entries) {
2235    if (this.listType_ == FileManager.ListType.DETAIL)
2236      this.table_.updateListItemsMetadata(type, entries);
2237    else
2238      this.grid_.updateListItemsMetadata(type, entries);
2239    // TODO: update bottom panel thumbnails.
2240  };
2241
2242  /**
2243   * Restore the item which is being renamed while refreshing the file list. Do
2244   * nothing if no item is being renamed or such an item disappeared.
2245   *
2246   * While refreshing file list it gets repopulated with new file entries.
2247   * There is not a big difference whether DOM items stay the same or not.
2248   * Except for the item that the user is renaming.
2249   *
2250   * @private
2251   */
2252  FileManager.prototype.restoreItemBeingRenamed_ = function() {
2253    if (!this.isRenamingInProgress())
2254      return;
2255
2256    var dm = this.directoryModel_;
2257    var leadIndex = dm.getFileListSelection().leadIndex;
2258    if (leadIndex < 0)
2259      return;
2260
2261    var leadEntry = dm.getFileList().item(leadIndex);
2262    if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
2263      return;
2264
2265    var leadListItem = this.findListItemForNode_(this.renameInput_);
2266    if (this.currentList_ == this.table_.list) {
2267      this.table_.updateFileMetadata(leadListItem, leadEntry);
2268    }
2269    this.currentList_.restoreLeadItem(leadListItem);
2270  };
2271
2272  /**
2273   * TODO(mtomasz): Move this to a utility function working on the root type.
2274   * @return {boolean} True if the current directory content is from Google
2275   *     Drive.
2276   */
2277  FileManager.prototype.isOnDrive = function() {
2278    var rootType = this.directoryModel_.getCurrentRootType();
2279    return rootType === VolumeManagerCommon.RootType.DRIVE ||
2280           rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
2281           rootType === VolumeManagerCommon.RootType.DRIVE_RECENT ||
2282           rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE;
2283  };
2284
2285  /**
2286   * Check if the drive-related setting items should be shown on currently
2287   * displayed gear menu.
2288   * @return {boolean} True if those setting items should be shown.
2289   */
2290  FileManager.prototype.shouldShowDriveSettings = function() {
2291    return this.isOnDrive() && this.isSecretGearMenuShown_;
2292  };
2293
2294  /**
2295   * Overrides default handling for clicks on hyperlinks.
2296   * In a packaged apps links with targer='_blank' open in a new tab by
2297   * default, other links do not open at all.
2298   *
2299   * @param {Event} event Click event.
2300   * @private
2301   */
2302  FileManager.prototype.onExternalLinkClick_ = function(event) {
2303    if (event.target.tagName != 'A' || !event.target.href)
2304      return;
2305
2306    if (this.dialogType != DialogType.FULL_PAGE)
2307      this.onCancel_();
2308  };
2309
2310  /**
2311   * Task combobox handler.
2312   *
2313   * @param {Object} event Event containing task which was clicked.
2314   * @private
2315   */
2316  FileManager.prototype.onTaskItemClicked_ = function(event) {
2317    var selection = this.getSelection();
2318    if (!selection.tasks) return;
2319
2320    if (event.item.task) {
2321      // Task field doesn't exist on change-default dropdown item.
2322      selection.tasks.execute(event.item.task.taskId);
2323    } else {
2324      var extensions = [];
2325
2326      for (var i = 0; i < selection.entries.length; i++) {
2327        var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
2328        if (match) {
2329          var ext = match[1].toUpperCase();
2330          if (extensions.indexOf(ext) == -1) {
2331            extensions.push(ext);
2332          }
2333        }
2334      }
2335
2336      var format = '';
2337
2338      if (extensions.length == 1) {
2339        format = extensions[0];
2340      }
2341
2342      // Change default was clicked. We should open "change default" dialog.
2343      selection.tasks.showTaskPicker(this.defaultTaskPicker,
2344          loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
2345          strf('CHANGE_DEFAULT_CAPTION', format),
2346          this.onDefaultTaskDone_.bind(this));
2347    }
2348  };
2349
2350  /**
2351   * Sets the given task as default, when this task is applicable.
2352   *
2353   * @param {Object} task Task to set as default.
2354   * @private
2355   */
2356  FileManager.prototype.onDefaultTaskDone_ = function(task) {
2357    // TODO(dgozman): move this method closer to tasks.
2358    var selection = this.getSelection();
2359    // TODO(mtomasz): Move conversion from entry to url to custom bindings.
2360    // crbug.com/345527.
2361    chrome.fileManagerPrivate.setDefaultTask(
2362        task.taskId,
2363        util.entriesToURLs(selection.entries),
2364        selection.mimeTypes);
2365    selection.tasks = new FileTasks(this);
2366    selection.tasks.init(selection.entries, selection.mimeTypes);
2367    selection.tasks.display(this.taskItems_);
2368    this.refreshCurrentDirectoryMetadata_();
2369    this.selectionHandler_.onFileSelectionChanged();
2370  };
2371
2372  /**
2373   * @private
2374   */
2375  FileManager.prototype.onPreferencesChanged_ = function() {
2376    var self = this;
2377    this.getPreferences_(function(prefs) {
2378      self.initDateTimeFormatters_();
2379      self.refreshCurrentDirectoryMetadata_();
2380
2381      if (prefs.cellularDisabled)
2382        self.syncButton.setAttribute('checked', '');
2383      else
2384        self.syncButton.removeAttribute('checked');
2385
2386      if (self.hostedButton.hasAttribute('checked') ===
2387          prefs.hostedFilesDisabled && self.isOnDrive()) {
2388        self.directoryModel_.rescan(false);
2389      }
2390
2391      if (!prefs.hostedFilesDisabled)
2392        self.hostedButton.setAttribute('checked', '');
2393      else
2394        self.hostedButton.removeAttribute('checked');
2395    },
2396    true /* refresh */);
2397  };
2398
2399  FileManager.prototype.onDriveConnectionChanged_ = function() {
2400    var connection = this.volumeManager_.getDriveConnectionState();
2401    if (this.commandHandler)
2402      this.commandHandler.updateAvailability();
2403    if (this.dialogContainer_)
2404      this.dialogContainer_.setAttribute('connection', connection.type);
2405    this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
2406    this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
2407  };
2408
2409  /**
2410   * Tells whether the current directory is read only.
2411   * TODO(mtomasz): Remove and use EntryLocation directly.
2412   * @return {boolean} True if read only, false otherwise.
2413   */
2414  FileManager.prototype.isOnReadonlyDirectory = function() {
2415    return this.directoryModel_.isReadOnly();
2416  };
2417
2418  /**
2419   * @param {Event} event Unmount event.
2420   * @private
2421   */
2422  FileManager.prototype.onExternallyUnmounted_ = function(event) {
2423    if (event.volumeInfo === this.currentVolumeInfo_) {
2424      if (this.closeOnUnmount_) {
2425        // If the file manager opened automatically when a usb drive inserted,
2426        // user have never changed current volume (that implies the current
2427        // directory is still on the device) then close this window.
2428        window.close();
2429      }
2430    }
2431  };
2432
2433  /**
2434   * @return {Array.<Entry>} List of all entries in the current directory.
2435   */
2436  FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
2437    return this.directoryModel_.getFileList().slice();
2438  };
2439
2440  FileManager.prototype.isRenamingInProgress = function() {
2441    return !!this.renameInput_.currentEntry;
2442  };
2443
2444  /**
2445   * @private
2446   */
2447  FileManager.prototype.focusCurrentList_ = function() {
2448    if (this.listType_ == FileManager.ListType.DETAIL)
2449      this.table_.focus();
2450    else  // this.listType_ == FileManager.ListType.THUMBNAIL)
2451      this.grid_.focus();
2452  };
2453
2454  /**
2455   * Return DirectoryEntry of the current directory or null.
2456   * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
2457   *     null if the directory model is not ready or the current directory is
2458   *     not set.
2459   */
2460  FileManager.prototype.getCurrentDirectoryEntry = function() {
2461    return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
2462  };
2463
2464  /**
2465   * Shows the share dialog for the selected file or directory.
2466   */
2467  FileManager.prototype.shareSelection = function() {
2468    var entries = this.getSelection().entries;
2469    if (entries.length != 1) {
2470      console.warn('Unable to share multiple items at once.');
2471      return;
2472    }
2473    // Add the overlapped class to prevent the applicaiton window from
2474    // captureing mouse events.
2475    this.shareDialog_.show(entries[0], function(result) {
2476      if (result == ShareDialog.Result.NETWORK_ERROR)
2477        this.error.show(str('SHARE_ERROR'));
2478    }.bind(this));
2479  };
2480
2481  /**
2482   * Creates a folder shortcut.
2483   * @param {Entry} entry A shortcut which refers to |entry| to be created.
2484   */
2485  FileManager.prototype.createFolderShortcut = function(entry) {
2486    // Duplicate entry.
2487    if (this.folderShortcutExists(entry))
2488      return;
2489
2490    this.folderShortcutsModel_.add(entry);
2491  };
2492
2493  /**
2494   * Checkes if the shortcut which refers to the given folder exists or not.
2495   * @param {Entry} entry Entry of the folder to be checked.
2496   */
2497  FileManager.prototype.folderShortcutExists = function(entry) {
2498    return this.folderShortcutsModel_.exists(entry);
2499  };
2500
2501  /**
2502   * Removes the folder shortcut.
2503   * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2504   */
2505  FileManager.prototype.removeFolderShortcut = function(entry) {
2506    this.folderShortcutsModel_.remove(entry);
2507  };
2508
2509  /**
2510   * Blinks the selection. Used to give feedback when copying or cutting the
2511   * selection.
2512   */
2513  FileManager.prototype.blinkSelection = function() {
2514    var selection = this.getSelection();
2515    if (!selection || selection.totalCount == 0)
2516      return;
2517
2518    for (var i = 0; i < selection.entries.length; i++) {
2519      var selectedIndex = selection.indexes[i];
2520      var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2521      if (listItem)
2522        this.blinkListItem_(listItem);
2523    }
2524  };
2525
2526  /**
2527   * @param {Element} listItem List item element.
2528   * @private
2529   */
2530  FileManager.prototype.blinkListItem_ = function(listItem) {
2531    listItem.classList.add('blink');
2532    setTimeout(function() {
2533      listItem.classList.remove('blink');
2534    }, 100);
2535  };
2536
2537  /**
2538   * @private
2539   */
2540  FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2541    var input = this.filenameInput_;
2542    input.focus();
2543    var selectionEnd = input.value.lastIndexOf('.');
2544    if (selectionEnd == -1) {
2545      input.select();
2546    } else {
2547      input.selectionStart = 0;
2548      input.selectionEnd = selectionEnd;
2549    }
2550  };
2551
2552  /**
2553   * Handles mouse click or tap.
2554   *
2555   * @param {Event} event The click event.
2556   * @private
2557   */
2558  FileManager.prototype.onDetailClick_ = function(event) {
2559    if (this.isRenamingInProgress()) {
2560      // Don't pay attention to clicks during a rename.
2561      return;
2562    }
2563
2564    var listItem = this.findListItemForEvent_(event);
2565    var selection = this.getSelection();
2566    if (!listItem || !listItem.selected || selection.totalCount != 1) {
2567      return;
2568    }
2569
2570    // React on double click, but only if both clicks hit the same item.
2571    // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2572    var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2573    this.lastClickedItem_ = listItem;
2574
2575    if (event.detail != clickNumber)
2576      return;
2577
2578    var entry = selection.entries[0];
2579    if (entry.isDirectory) {
2580      this.onDirectoryAction_(entry);
2581    } else {
2582      this.dispatchSelectionAction_();
2583    }
2584  };
2585
2586  /**
2587   * @private
2588   */
2589  FileManager.prototype.dispatchSelectionAction_ = function() {
2590    if (this.dialogType == DialogType.FULL_PAGE) {
2591      var selection = this.getSelection();
2592      var tasks = selection.tasks;
2593      var urls = selection.urls;
2594      var mimeTypes = selection.mimeTypes;
2595      if (tasks)
2596        tasks.executeDefault();
2597      return true;
2598    }
2599    if (!this.okButton_.disabled) {
2600      this.onOk_();
2601      return true;
2602    }
2603    return false;
2604  };
2605
2606  /**
2607   * Opens the suggest file dialog.
2608   *
2609   * @param {Entry} entry Entry of the file.
2610   * @param {function()} onSuccess Success callback.
2611   * @param {function()} onCancelled User-cancelled callback.
2612   * @param {function()} onFailure Failure callback.
2613   * @private
2614   */
2615  FileManager.prototype.openSuggestAppsDialog =
2616      function(entry, onSuccess, onCancelled, onFailure) {
2617    if (!url) {
2618      onFailure();
2619      return;
2620    }
2621
2622    this.metadataCache_.getOne(entry, 'external', function(prop) {
2623      if (!prop || !prop.contentMimeType) {
2624        onFailure();
2625        return;
2626      }
2627
2628      var basename = entry.name;
2629      var splitted = util.splitExtension(basename);
2630      var filename = splitted[0];
2631      var extension = splitted[1];
2632      var mime = prop.contentMimeType;
2633
2634      // Returns with failure if the file has neither extension nor mime.
2635      if (!extension || !mime) {
2636        onFailure();
2637        return;
2638      }
2639
2640      var onDialogClosed = function(result) {
2641        switch (result) {
2642          case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2643            onSuccess();
2644            break;
2645          case SuggestAppsDialog.Result.FAILED:
2646            onFailure();
2647            break;
2648          default:
2649            onCancelled();
2650        }
2651      };
2652
2653      if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2654        this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2655      } else {
2656        this.suggestAppsDialog.showByExtensionAndMime(
2657            extension, mime, onDialogClosed);
2658      }
2659    }.bind(this));
2660  };
2661
2662  /**
2663   * Called when a dialog is shown or hidden.
2664   * @param {boolean} show True if a dialog is shown, false if hidden.
2665   */
2666  FileManager.prototype.onDialogShownOrHidden = function(show) {
2667    if (show) {
2668      // If a dialog is shown, activate the window.
2669      var appWindow = chrome.app.window.current();
2670      if (appWindow)
2671        appWindow.focus();
2672    }
2673
2674    // Set/unset a flag to disable dragging on the title area.
2675    this.dialogContainer_.classList.toggle('disable-header-drag', show);
2676  };
2677
2678  /**
2679   * Executes directory action (i.e. changes directory).
2680   *
2681   * @param {DirectoryEntry} entry Directory entry to which directory should be
2682   *                               changed.
2683   * @private
2684   */
2685  FileManager.prototype.onDirectoryAction_ = function(entry) {
2686    return this.directoryModel_.changeDirectoryEntry(entry);
2687  };
2688
2689  /**
2690   * Update the window title.
2691   * @private
2692   */
2693  FileManager.prototype.updateTitle_ = function() {
2694    if (this.dialogType != DialogType.FULL_PAGE)
2695      return;
2696
2697    if (!this.currentVolumeInfo_)
2698      return;
2699
2700    this.document_.title = this.currentVolumeInfo_.label;
2701  };
2702
2703  /**
2704   * Updates the location information displayed on the toolbar.
2705   * @param {DirectoryEntry=} opt_entry Directory entry to be displayed as
2706   *     current location. Default entry is the current directory.
2707   * @private
2708   */
2709  FileManager.prototype.updateLocationLine_ = function(opt_entry) {
2710    var entry = opt_entry || this.getCurrentDirectoryEntry();
2711    // Updates volume icon.
2712    var location = this.volumeManager_.getLocationInfo(entry);
2713    if (location && location.rootType && location.isRootEntry) {
2714      this.ui_.locationVolumeIcon.setAttribute(
2715          'volume-type-icon', location.rootType);
2716      this.ui_.locationVolumeIcon.removeAttribute('volume-subtype');
2717    } else {
2718      this.ui_.locationVolumeIcon.setAttribute(
2719          'volume-type-icon', location.volumeInfo.volumeType);
2720      this.ui_.locationVolumeIcon.setAttribute(
2721          'volume-subtype', location.volumeInfo.deviceType);
2722    }
2723    // Updates breadcrumbs.
2724    this.ui_.locationBreadcrumbs.show(entry);
2725  };
2726
2727  /**
2728   * Update the gear menu.
2729   * @private
2730   */
2731  FileManager.prototype.updateGearMenu_ = function() {
2732    this.refreshRemainingSpace_(true);  // Show loading caption.
2733  };
2734
2735  /**
2736   * Refreshes space info of the current volume.
2737   * @param {boolean} showLoadingCaption Whether show loading caption or not.
2738   * @private
2739   */
2740  FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2741    if (!this.currentVolumeInfo_)
2742      return;
2743
2744    var volumeSpaceInfo =
2745        this.dialogDom_.querySelector('#volume-space-info');
2746    var volumeSpaceInfoSeparator =
2747        this.dialogDom_.querySelector('#volume-space-info-separator');
2748    var volumeSpaceInfoLabel =
2749        this.dialogDom_.querySelector('#volume-space-info-label');
2750    var volumeSpaceInnerBar =
2751        this.dialogDom_.querySelector('#volume-space-info-bar');
2752    var volumeSpaceOuterBar =
2753        this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2754
2755    var currentVolumeInfo = this.currentVolumeInfo_;
2756
2757    // TODO(mtomasz): Add support for remaining space indication for provided
2758    // file systems.
2759    if (currentVolumeInfo.volumeType ==
2760        VolumeManagerCommon.VolumeType.PROVIDED) {
2761      volumeSpaceInfo.hidden = true;
2762      volumeSpaceInfoSeparator.hidden = true;
2763      return;
2764    }
2765
2766    volumeSpaceInfo.hidden = false;
2767    volumeSpaceInfoSeparator.hidden = false;
2768    volumeSpaceInnerBar.setAttribute('pending', '');
2769
2770    if (showLoadingCaption) {
2771      volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2772      volumeSpaceInnerBar.style.width = '100%';
2773    }
2774
2775    chrome.fileManagerPrivate.getSizeStats(
2776        currentVolumeInfo.volumeId, function(result) {
2777          var volumeInfo = this.volumeManager_.getVolumeInfo(
2778              this.directoryModel_.getCurrentDirEntry());
2779          if (currentVolumeInfo !== this.currentVolumeInfo_)
2780            return;
2781          updateSpaceInfo(result,
2782                          volumeSpaceInnerBar,
2783                          volumeSpaceInfoLabel,
2784                          volumeSpaceOuterBar);
2785        }.bind(this));
2786  };
2787
2788  /**
2789   * Update the UI when the current directory changes.
2790   *
2791   * @param {Event} event The directory-changed event.
2792   * @private
2793   */
2794  FileManager.prototype.onDirectoryChanged_ = function(event) {
2795    var oldCurrentVolumeInfo = this.currentVolumeInfo_;
2796
2797    // Remember the current volume info.
2798    this.currentVolumeInfo_ = this.volumeManager_.getVolumeInfo(
2799        event.newDirEntry);
2800
2801    // If volume has changed, then update the gear menu.
2802    if (oldCurrentVolumeInfo !== this.currentVolumeInfo_) {
2803      this.updateGearMenu_();
2804      // If the volume has changed, and it was previously set, then do not
2805      // close on unmount anymore.
2806      if (oldCurrentVolumeInfo)
2807        this.closeOnUnmount_ = false;
2808    }
2809
2810    this.selectionHandler_.onFileSelectionChanged();
2811    this.ui_.searchBox.clear();
2812    // TODO(mtomasz): Consider remembering the selection.
2813    util.updateAppState(
2814        this.getCurrentDirectoryEntry() ?
2815        this.getCurrentDirectoryEntry().toURL() : '',
2816        '' /* selectionURL */,
2817        '' /* opt_param */);
2818
2819    if (this.commandHandler)
2820      this.commandHandler.updateAvailability();
2821
2822    this.updateUnformattedVolumeStatus_();
2823    this.updateTitle_();
2824    this.updateLocationLine_();
2825
2826    var currentEntry = this.getCurrentDirectoryEntry();
2827    this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2828        null : currentEntry;
2829  };
2830
2831  FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2832    var volumeInfo = this.volumeManager_.getVolumeInfo(
2833        this.directoryModel_.getCurrentDirEntry());
2834
2835    if (volumeInfo && volumeInfo.error) {
2836      this.dialogDom_.setAttribute('unformatted', '');
2837
2838      var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2839      if (volumeInfo.error ===
2840          VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
2841        errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2842      } else {
2843        errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2844      }
2845
2846      // Update 'canExecute' for format command so the format button's disabled
2847      // property is properly set.
2848      if (this.commandHandler)
2849        this.commandHandler.updateAvailability();
2850    } else {
2851      this.dialogDom_.removeAttribute('unformatted');
2852    }
2853  };
2854
2855  FileManager.prototype.findListItemForEvent_ = function(event) {
2856    return this.findListItemForNode_(event.touchedElement || event.srcElement);
2857  };
2858
2859  FileManager.prototype.findListItemForNode_ = function(node) {
2860    var item = this.currentList_.getListItemAncestor(node);
2861    // TODO(serya): list should check that.
2862    return item && this.currentList_.isItem(item) ? item : null;
2863  };
2864
2865  /**
2866   * Unload handler for the page.
2867   * @private
2868   */
2869  FileManager.prototype.onUnload_ = function() {
2870    if (this.directoryModel_)
2871      this.directoryModel_.dispose();
2872    if (this.volumeManager_)
2873      this.volumeManager_.dispose();
2874    for (var i = 0;
2875         i < this.fileTransferController_.pendingTaskIds.length;
2876         i++) {
2877      var taskId = this.fileTransferController_.pendingTaskIds[i];
2878      var item =
2879          this.backgroundPage_.background.progressCenter.getItemById(taskId);
2880      item.message = '';
2881      item.state = ProgressItemState.CANCELED;
2882      this.backgroundPage_.background.progressCenter.updateItem(item);
2883    }
2884    if (this.progressCenterPanel_) {
2885      this.backgroundPage_.background.progressCenter.removePanel(
2886          this.progressCenterPanel_);
2887    }
2888    if (this.fileOperationManager_) {
2889      if (this.onCopyProgressBound_) {
2890        this.fileOperationManager_.removeEventListener(
2891            'copy-progress', this.onCopyProgressBound_);
2892      }
2893      if (this.onEntriesChangedBound_) {
2894        this.fileOperationManager_.removeEventListener(
2895            'entries-changed', this.onEntriesChangedBound_);
2896      }
2897    }
2898    window.closing = true;
2899    if (this.backgroundPage_)
2900      this.backgroundPage_.background.tryClose();
2901  };
2902
2903  FileManager.prototype.initiateRename = function() {
2904    var item = this.currentList_.ensureLeadItemExists();
2905    if (!item)
2906      return;
2907    var label = item.querySelector('.filename-label');
2908    var input = this.renameInput_;
2909    var currentEntry = this.currentList_.dataModel.item(item.listIndex);
2910
2911    input.value = label.textContent;
2912    item.setAttribute('renaming', '');
2913    label.parentNode.appendChild(input);
2914    input.focus();
2915
2916    var selectionEnd = input.value.lastIndexOf('.');
2917    if (currentEntry.isFile && selectionEnd !== -1) {
2918      input.selectionStart = 0;
2919      input.selectionEnd = selectionEnd;
2920    } else {
2921      input.select();
2922    }
2923
2924    // This has to be set late in the process so we don't handle spurious
2925    // blur events.
2926    input.currentEntry = currentEntry;
2927    this.table_.startBatchUpdates();
2928    this.grid_.startBatchUpdates();
2929  };
2930
2931  /**
2932   * @type {Event} Key event.
2933   * @private
2934   */
2935  FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2936    if (!this.isRenamingInProgress())
2937      return;
2938
2939    // Do not move selection or lead item in list during rename.
2940    if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2941      event.stopPropagation();
2942    }
2943
2944    switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2945      case 'U+001B':  // Escape
2946        this.cancelRename_();
2947        event.preventDefault();
2948        break;
2949
2950      case 'Enter':
2951        this.commitRename_();
2952        event.preventDefault();
2953        break;
2954    }
2955  };
2956
2957  /**
2958   * @type {Event} Blur event.
2959   * @private
2960   */
2961  FileManager.prototype.onRenameInputBlur_ = function(event) {
2962    if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2963      this.commitRename_();
2964  };
2965
2966  /**
2967   * @private
2968   */
2969  FileManager.prototype.commitRename_ = function() {
2970    var input = this.renameInput_;
2971    var entry = input.currentEntry;
2972    var newName = input.value;
2973
2974    if (newName == entry.name) {
2975      this.cancelRename_();
2976      return;
2977    }
2978
2979    var renamedItemElement = this.findListItemForNode_(this.renameInput_);
2980    var nameNode = renamedItemElement.querySelector('.filename-label');
2981
2982    input.validation_ = true;
2983    var validationDone = function(valid) {
2984      input.validation_ = false;
2985
2986      if (!valid) {
2987        // Cancel rename if it fails to restore focus from alert dialog.
2988        // Otherwise, just cancel the commitment and continue to rename.
2989        if (this.document_.activeElement != input)
2990          this.cancelRename_();
2991        return;
2992      }
2993
2994      // Validation succeeded. Do renaming.
2995      this.renameInput_.currentEntry = null;
2996      if (this.renameInput_.parentNode)
2997        this.renameInput_.parentNode.removeChild(this.renameInput_);
2998      renamedItemElement.setAttribute('renaming', 'provisional');
2999
3000      // Optimistically apply new name immediately to avoid flickering in
3001      // case of success.
3002      nameNode.textContent = newName;
3003
3004      util.rename(
3005          entry, newName,
3006          function(newEntry) {
3007            this.directoryModel_.onRenameEntry(entry, newEntry);
3008            renamedItemElement.removeAttribute('renaming');
3009            this.table_.endBatchUpdates();
3010            this.grid_.endBatchUpdates();
3011            // Focus may go out of the list. Back it to the list.
3012            this.currentList_.focus();
3013          }.bind(this),
3014          function(error) {
3015            // Write back to the old name.
3016            nameNode.textContent = entry.name;
3017            renamedItemElement.removeAttribute('renaming');
3018            this.table_.endBatchUpdates();
3019            this.grid_.endBatchUpdates();
3020
3021            // Show error dialog.
3022            var message;
3023            if (error.name == util.FileError.PATH_EXISTS_ERR ||
3024                error.name == util.FileError.TYPE_MISMATCH_ERR) {
3025              // Check the existing entry is file or not.
3026              // 1) If the entry is a file:
3027              //   a) If we get PATH_EXISTS_ERR, a file exists.
3028              //   b) If we get TYPE_MISMATCH_ERR, a directory exists.
3029              // 2) If the entry is a directory:
3030              //   a) If we get PATH_EXISTS_ERR, a directory exists.
3031              //   b) If we get TYPE_MISMATCH_ERR, a file exists.
3032              message = strf(
3033                  (entry.isFile && error.name ==
3034                      util.FileError.PATH_EXISTS_ERR) ||
3035                  (!entry.isFile && error.name ==
3036                      util.FileError.TYPE_MISMATCH_ERR) ?
3037                      'FILE_ALREADY_EXISTS' :
3038                      'DIRECTORY_ALREADY_EXISTS',
3039                  newName);
3040            } else {
3041              message = strf('ERROR_RENAMING', entry.name,
3042                             util.getFileErrorString(error.name));
3043            }
3044
3045            this.alert.show(message);
3046          }.bind(this));
3047    };
3048
3049    // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
3050    // parent if the directory content is a search result. Fix it to do proper
3051    // validation.
3052    this.validateFileName_(this.getCurrentDirectoryEntry(),
3053                           newName,
3054                           validationDone.bind(this));
3055  };
3056
3057  /**
3058   * @private
3059   */
3060  FileManager.prototype.cancelRename_ = function() {
3061    this.renameInput_.currentEntry = null;
3062
3063    var item = this.findListItemForNode_(this.renameInput_);
3064    if (item)
3065      item.removeAttribute('renaming');
3066
3067    var parent = this.renameInput_.parentNode;
3068    if (parent)
3069      parent.removeChild(this.renameInput_);
3070
3071    this.table_.endBatchUpdates();
3072    this.grid_.endBatchUpdates();
3073
3074    // Focus may go out of the list. Back it to the list.
3075    this.currentList_.focus();
3076  };
3077
3078  /**
3079   * @private
3080   */
3081  FileManager.prototype.onFilenameInputInput_ = function() {
3082    this.selectionHandler_.updateOkButton();
3083  };
3084
3085  /**
3086   * @param {Event} event Key event.
3087   * @private
3088   */
3089  FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
3090    if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
3091      this.okButton_.click();
3092  };
3093
3094  /**
3095   * @param {Event} event Focus event.
3096   * @private
3097   */
3098  FileManager.prototype.onFilenameInputFocus_ = function(event) {
3099    var input = this.filenameInput_;
3100
3101    // On focus we want to select everything but the extension, but
3102    // Chrome will select-all after the focus event completes.  We
3103    // schedule a timeout to alter the focus after that happens.
3104    setTimeout(function() {
3105      var selectionEnd = input.value.lastIndexOf('.');
3106      if (selectionEnd == -1) {
3107        input.select();
3108      } else {
3109        input.selectionStart = 0;
3110        input.selectionEnd = selectionEnd;
3111      }
3112    }, 0);
3113  };
3114
3115  /**
3116   * @private
3117   */
3118  FileManager.prototype.onScanStarted_ = function() {
3119    if (this.scanInProgress_) {
3120      this.table_.list.endBatchUpdates();
3121      this.grid_.endBatchUpdates();
3122    }
3123
3124    if (this.commandHandler)
3125      this.commandHandler.updateAvailability();
3126    this.table_.list.startBatchUpdates();
3127    this.grid_.startBatchUpdates();
3128    this.scanInProgress_ = true;
3129
3130    this.scanUpdatedAtLeastOnceOrCompleted_ = false;
3131    if (this.scanCompletedTimer_) {
3132      clearTimeout(this.scanCompletedTimer_);
3133      this.scanCompletedTimer_ = 0;
3134    }
3135
3136    if (this.scanUpdatedTimer_) {
3137      clearTimeout(this.scanUpdatedTimer_);
3138      this.scanUpdatedTimer_ = 0;
3139    }
3140
3141    if (this.spinner_.hidden) {
3142      this.cancelSpinnerTimeout_();
3143      this.showSpinnerTimeout_ =
3144          setTimeout(this.showSpinner_.bind(this, true), 500);
3145    }
3146  };
3147
3148  /**
3149   * @private
3150   */
3151  FileManager.prototype.onScanCompleted_ = function() {
3152    if (!this.scanInProgress_) {
3153      console.error('Scan-completed event recieved. But scan is not started.');
3154      return;
3155    }
3156
3157    if (this.commandHandler)
3158      this.commandHandler.updateAvailability();
3159    this.hideSpinnerLater_();
3160
3161    if (this.scanUpdatedTimer_) {
3162      clearTimeout(this.scanUpdatedTimer_);
3163      this.scanUpdatedTimer_ = 0;
3164    }
3165
3166    // To avoid flickering postpone updating the ui by a small amount of time.
3167    // There is a high chance, that metadata will be received within 50 ms.
3168    this.scanCompletedTimer_ = setTimeout(function() {
3169      // Check if batch updates are already finished by onScanUpdated_().
3170      if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3171        this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3172      }
3173
3174      this.scanInProgress_ = false;
3175      this.table_.list.endBatchUpdates();
3176      this.grid_.endBatchUpdates();
3177      this.scanCompletedTimer_ = 0;
3178    }.bind(this), 50);
3179  };
3180
3181  /**
3182   * @private
3183   */
3184  FileManager.prototype.onScanUpdated_ = function() {
3185    if (!this.scanInProgress_) {
3186      console.error('Scan-updated event recieved. But scan is not started.');
3187      return;
3188    }
3189
3190    if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
3191      return;
3192
3193    // Show contents incrementally by finishing batch updated, but only after
3194    // 200ms elapsed, to avoid flickering when it is not necessary.
3195    this.scanUpdatedTimer_ = setTimeout(function() {
3196      // We need to hide the spinner only once.
3197      if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3198        this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3199        this.hideSpinnerLater_();
3200      }
3201
3202      // Update the UI.
3203      if (this.scanInProgress_) {
3204        this.table_.list.endBatchUpdates();
3205        this.grid_.endBatchUpdates();
3206        this.table_.list.startBatchUpdates();
3207        this.grid_.startBatchUpdates();
3208      }
3209      this.scanUpdatedTimer_ = 0;
3210    }.bind(this), 200);
3211  };
3212
3213  /**
3214   * @private
3215   */
3216  FileManager.prototype.onScanCancelled_ = function() {
3217    if (!this.scanInProgress_) {
3218      console.error('Scan-cancelled event recieved. But scan is not started.');
3219      return;
3220    }
3221
3222    if (this.commandHandler)
3223      this.commandHandler.updateAvailability();
3224    this.hideSpinnerLater_();
3225    if (this.scanCompletedTimer_) {
3226      clearTimeout(this.scanCompletedTimer_);
3227      this.scanCompletedTimer_ = 0;
3228    }
3229    if (this.scanUpdatedTimer_) {
3230      clearTimeout(this.scanUpdatedTimer_);
3231      this.scanUpdatedTimer_ = 0;
3232    }
3233    // Finish unfinished batch updates.
3234    if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3235      this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3236    }
3237
3238    this.scanInProgress_ = false;
3239    this.table_.list.endBatchUpdates();
3240    this.grid_.endBatchUpdates();
3241  };
3242
3243  /**
3244   * Handle the 'rescan-completed' from the DirectoryModel.
3245   * @private
3246   */
3247  FileManager.prototype.onRescanCompleted_ = function() {
3248    this.selectionHandler_.onFileSelectionChanged();
3249  };
3250
3251  /**
3252   * @private
3253   */
3254  FileManager.prototype.cancelSpinnerTimeout_ = function() {
3255    if (this.showSpinnerTimeout_) {
3256      clearTimeout(this.showSpinnerTimeout_);
3257      this.showSpinnerTimeout_ = 0;
3258    }
3259  };
3260
3261  /**
3262   * @private
3263   */
3264  FileManager.prototype.hideSpinnerLater_ = function() {
3265    this.cancelSpinnerTimeout_();
3266    this.showSpinner_(false);
3267  };
3268
3269  /**
3270   * @param {boolean} on True to show, false to hide.
3271   * @private
3272   */
3273  FileManager.prototype.showSpinner_ = function(on) {
3274    if (on && this.directoryModel_ && this.directoryModel_.isScanning())
3275      this.spinner_.hidden = false;
3276
3277    if (!on && (!this.directoryModel_ ||
3278                !this.directoryModel_.isScanning() ||
3279                this.directoryModel_.getFileList().length != 0)) {
3280      this.spinner_.hidden = true;
3281    }
3282  };
3283
3284  FileManager.prototype.createNewFolder = function() {
3285    var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
3286
3287    // Find a name that doesn't exist in the data model.
3288    var files = this.directoryModel_.getFileList();
3289    var hash = {};
3290    for (var i = 0; i < files.length; i++) {
3291      var name = files.item(i).name;
3292      // Filtering names prevents from conflicts with prototype's names
3293      // and '__proto__'.
3294      if (name.substring(0, defaultName.length) == defaultName)
3295        hash[name] = 1;
3296    }
3297
3298    var baseName = defaultName;
3299    var separator = '';
3300    var suffix = '';
3301    var index = '';
3302
3303    var advance = function() {
3304      separator = ' (';
3305      suffix = ')';
3306      index++;
3307    };
3308
3309    var current = function() {
3310      return baseName + separator + index + suffix;
3311    };
3312
3313    // Accessing hasOwnProperty is safe since hash properties filtered.
3314    while (hash.hasOwnProperty(current())) {
3315      advance();
3316    }
3317
3318    var self = this;
3319    var list = self.currentList_;
3320    var tryCreate = function() {
3321    };
3322
3323    var onSuccess = function(entry) {
3324      metrics.recordUserAction('CreateNewFolder');
3325      list.selectedItem = entry;
3326
3327      self.table_.list.endBatchUpdates();
3328      self.grid_.endBatchUpdates();
3329
3330      self.initiateRename();
3331    };
3332
3333    var onError = function(error) {
3334      self.table_.list.endBatchUpdates();
3335      self.grid_.endBatchUpdates();
3336
3337      self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
3338                           util.getFileErrorString(error.name)));
3339    };
3340
3341    var onAbort = function() {
3342      self.table_.list.endBatchUpdates();
3343      self.grid_.endBatchUpdates();
3344    };
3345
3346    this.table_.list.startBatchUpdates();
3347    this.grid_.startBatchUpdates();
3348    this.directoryModel_.createDirectory(current(),
3349                                         onSuccess,
3350                                         onError,
3351                                         onAbort);
3352  };
3353
3354  /**
3355   * Handles click event on the toggle-view button.
3356   * @param {Event} event Click event.
3357   * @private
3358   */
3359  FileManager.prototype.onToggleViewButtonClick_ = function(event) {
3360    if (this.listType_ === FileManager.ListType.DETAIL)
3361      this.setListType(FileManager.ListType.THUMBNAIL);
3362    else
3363      this.setListType(FileManager.ListType.DETAIL);
3364
3365    event.target.blur();
3366  };
3367
3368  /**
3369   * KeyDown event handler for the document.
3370   * @param {Event} event Key event.
3371   * @private
3372   */
3373  FileManager.prototype.onKeyDown_ = function(event) {
3374    if (event.keyCode === 9)  // Tab
3375      this.pressingTab_ = true;
3376    if (event.keyCode === 17)  // Ctrl
3377      this.pressingCtrl_ = true;
3378
3379    if (event.srcElement === this.renameInput_) {
3380      // Ignore keydown handler in the rename input box.
3381      return;
3382    }
3383
3384    switch (util.getKeyModifiers(event) + event.keyIdentifier) {
3385      case 'Ctrl-U+00BE':  // Ctrl-. => Toggle filter files.
3386        this.fileFilter_.setFilterHidden(
3387            !this.fileFilter_.isFilterHiddenOn());
3388        event.preventDefault();
3389        return;
3390
3391      case 'U+001B':  // Escape => Cancel dialog.
3392        if (this.dialogType != DialogType.FULL_PAGE) {
3393          // If there is nothing else for ESC to do, then cancel the dialog.
3394          event.preventDefault();
3395          this.cancelButton_.click();
3396        }
3397        break;
3398    }
3399  };
3400
3401  /**
3402   * KeyUp event handler for the document.
3403   * @param {Event} event Key event.
3404   * @private
3405   */
3406  FileManager.prototype.onKeyUp_ = function(event) {
3407    if (event.keyCode === 9)  // Tab
3408      this.pressingTab_ = false;
3409    if (event.keyCode == 17)  // Ctrl
3410      this.pressingCtrl_ = false;
3411  };
3412
3413  /**
3414   * KeyDown event handler for the div#list-container element.
3415   * @param {Event} event Key event.
3416   * @private
3417   */
3418  FileManager.prototype.onListKeyDown_ = function(event) {
3419    if (event.srcElement.tagName == 'INPUT') {
3420      // Ignore keydown handler in the rename input box.
3421      return;
3422    }
3423
3424    switch (util.getKeyModifiers(event) + event.keyCode) {
3425      case '8':  // Backspace => Up one directory.
3426        event.preventDefault();
3427        // TODO(mtomasz): Use Entry.getParent() instead.
3428        if (!this.getCurrentDirectoryEntry())
3429          break;
3430        var currentEntry = this.getCurrentDirectoryEntry();
3431        var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
3432        // TODO(mtomasz): There may be a tiny race in here.
3433        if (locationInfo && !locationInfo.isRootEntry &&
3434            !locationInfo.isSpecialSearchRoot) {
3435          currentEntry.getParent(function(parentEntry) {
3436            this.directoryModel_.changeDirectoryEntry(parentEntry);
3437          }.bind(this), function() { /* Ignore errors. */});
3438        }
3439        break;
3440
3441      case '13':  // Enter => Change directory or perform default action.
3442        // TODO(dgozman): move directory action to dispatchSelectionAction.
3443        var selection = this.getSelection();
3444        if (selection.totalCount == 1 &&
3445            selection.entries[0].isDirectory &&
3446            !DialogType.isFolderDialog(this.dialogType)) {
3447          event.preventDefault();
3448          this.onDirectoryAction_(selection.entries[0]);
3449        } else if (this.dispatchSelectionAction_()) {
3450          event.preventDefault();
3451        }
3452        break;
3453    }
3454
3455    switch (event.keyIdentifier) {
3456      case 'Home':
3457      case 'End':
3458      case 'Up':
3459      case 'Down':
3460      case 'Left':
3461      case 'Right':
3462        // When navigating with keyboard we hide the distracting mouse hover
3463        // highlighting until the user moves the mouse again.
3464        this.setNoHover_(true);
3465        break;
3466    }
3467  };
3468
3469  /**
3470   * Suppress/restore hover highlighting in the list container.
3471   * @param {boolean} on True to temporarity hide hover state.
3472   * @private
3473   */
3474  FileManager.prototype.setNoHover_ = function(on) {
3475    if (on) {
3476      this.listContainer_.classList.add('nohover');
3477    } else {
3478      this.listContainer_.classList.remove('nohover');
3479    }
3480  };
3481
3482  /**
3483   * KeyPress event handler for the div#list-container element.
3484   * @param {Event} event Key event.
3485   * @private
3486   */
3487  FileManager.prototype.onListKeyPress_ = function(event) {
3488    if (event.srcElement.tagName == 'INPUT') {
3489      // Ignore keypress handler in the rename input box.
3490      return;
3491    }
3492
3493    if (event.ctrlKey || event.metaKey || event.altKey)
3494      return;
3495
3496    var now = new Date();
3497    var char = String.fromCharCode(event.charCode).toLowerCase();
3498    var text = now - this.textSearchState_.date > 1000 ? '' :
3499        this.textSearchState_.text;
3500    this.textSearchState_ = {text: text + char, date: now};
3501
3502    this.doTextSearch_();
3503  };
3504
3505  /**
3506   * Mousemove event handler for the div#list-container element.
3507   * @param {Event} event Mouse event.
3508   * @private
3509   */
3510  FileManager.prototype.onListMouseMove_ = function(event) {
3511    // The user grabbed the mouse, restore the hover highlighting.
3512    this.setNoHover_(false);
3513  };
3514
3515  /**
3516   * Performs a 'text search' - selects a first list entry with name
3517   * starting with entered text (case-insensitive).
3518   * @private
3519   */
3520  FileManager.prototype.doTextSearch_ = function() {
3521    var text = this.textSearchState_.text;
3522    if (!text)
3523      return;
3524
3525    var dm = this.directoryModel_.getFileList();
3526    for (var index = 0; index < dm.length; ++index) {
3527      var name = dm.item(index).name;
3528      if (name.substring(0, text.length).toLowerCase() == text) {
3529        this.currentList_.selectionModel.selectedIndexes = [index];
3530        return;
3531      }
3532    }
3533
3534    this.textSearchState_.text = '';
3535  };
3536
3537  /**
3538   * Handle a click of the cancel button.  Closes the window.
3539   * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3540   *
3541   * @param {Event} event The click event.
3542   * @private
3543   */
3544  FileManager.prototype.onCancel_ = function(event) {
3545    chrome.fileManagerPrivate.cancelDialog();
3546    window.close();
3547  };
3548
3549  /**
3550   * Tries to close this modal dialog with some files selected.
3551   * Performs preprocessing if needed (e.g. for Drive).
3552   * @param {Object} selection Contains urls, filterIndex and multiple fields.
3553   * @private
3554   */
3555  FileManager.prototype.selectFilesAndClose_ = function(selection) {
3556    var callSelectFilesApiAndClose = function(callback) {
3557      var onFileSelected = function() {
3558        callback();
3559        if (!chrome.runtime.lastError) {
3560          // Call next method on a timeout, as it's unsafe to
3561          // close a window from a callback.
3562          setTimeout(window.close.bind(window), 0);
3563        }
3564      };
3565      if (selection.multiple) {
3566        chrome.fileManagerPrivate.selectFiles(
3567            selection.urls,
3568            this.params_.shouldReturnLocalPath,
3569            onFileSelected);
3570      } else {
3571        chrome.fileManagerPrivate.selectFile(
3572            selection.urls[0],
3573            selection.filterIndex,
3574            this.dialogType != DialogType.SELECT_SAVEAS_FILE /* for opening */,
3575            this.params_.shouldReturnLocalPath,
3576            onFileSelected);
3577      }
3578    }.bind(this);
3579
3580    if (!this.isOnDrive() || this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3581      callSelectFilesApiAndClose(function() {});
3582      return;
3583    }
3584
3585    var shade = this.document_.createElement('div');
3586    shade.className = 'shade';
3587    var footer = this.dialogDom_.querySelector('.button-panel');
3588    var progress = footer.querySelector('.progress-track');
3589    progress.style.width = '0%';
3590    var cancelled = false;
3591
3592    var progressMap = {};
3593    var filesStarted = 0;
3594    var filesTotal = selection.urls.length;
3595    for (var index = 0; index < selection.urls.length; index++) {
3596      progressMap[selection.urls[index]] = -1;
3597    }
3598    var lastPercent = 0;
3599    var bytesTotal = 0;
3600    var bytesDone = 0;
3601
3602    var onFileTransfersUpdated = function(status) {
3603      if (!(status.fileUrl in progressMap))
3604        return;
3605      if (status.total == -1)
3606        return;
3607
3608      var old = progressMap[status.fileUrl];
3609      if (old == -1) {
3610        // -1 means we don't know file size yet.
3611        bytesTotal += status.total;
3612        filesStarted++;
3613        old = 0;
3614      }
3615      bytesDone += status.processed - old;
3616      progressMap[status.fileUrl] = status.processed;
3617
3618      var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3619      // For files we don't have information about, assume the progress is zero.
3620      percent = percent * filesStarted / filesTotal * 100;
3621      // Do not decrease the progress. This may happen, if first downloaded
3622      // file is small, and the second one is large.
3623      lastPercent = Math.max(lastPercent, percent);
3624      progress.style.width = lastPercent + '%';
3625    }.bind(this);
3626
3627    var setup = function() {
3628      this.document_.querySelector('.dialog-container').appendChild(shade);
3629      setTimeout(function() { shade.setAttribute('fadein', 'fadein'); }, 100);
3630      footer.setAttribute('progress', 'progress');
3631      this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3632      this.cancelButton_.addEventListener('click', onCancel);
3633      chrome.fileManagerPrivate.onFileTransfersUpdated.addListener(
3634          onFileTransfersUpdated);
3635    }.bind(this);
3636
3637    var cleanup = function() {
3638      shade.parentNode.removeChild(shade);
3639      footer.removeAttribute('progress');
3640      this.cancelButton_.removeEventListener('click', onCancel);
3641      this.cancelButton_.addEventListener('click', this.onCancelBound_);
3642      chrome.fileManagerPrivate.onFileTransfersUpdated.removeListener(
3643          onFileTransfersUpdated);
3644    }.bind(this);
3645
3646    var onCancel = function() {
3647      // According to API cancel may fail, but there is no proper UI to reflect
3648      // this. So, we just silently assume that everything is cancelled.
3649      chrome.fileManagerPrivate.cancelFileTransfers(
3650          selection.urls, function(response) {});
3651    }.bind(this);
3652
3653    var onProperties = function(properties) {
3654      for (var i = 0; i < properties.length; i++) {
3655        if (!properties[i] || properties[i].present) {
3656          // For files already in GCache, we don't get any transfer updates.
3657          filesTotal--;
3658        }
3659      }
3660      callSelectFilesApiAndClose(cleanup);
3661    }.bind(this);
3662
3663    setup();
3664
3665    // TODO(mtomasz): Use Entry instead of URLs, if possible.
3666    util.URLsToEntries(selection.urls, function(entries) {
3667      this.metadataCache_.get(entries, 'external', onProperties);
3668    }.bind(this));
3669  };
3670
3671  /**
3672   * Handle a click of the ok button.
3673   *
3674   * The ok button has different UI labels depending on the type of dialog, but
3675   * in code it's always referred to as 'ok'.
3676   *
3677   * @param {Event} event The click event.
3678   * @private
3679   */
3680  FileManager.prototype.onOk_ = function(event) {
3681    if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3682      // Save-as doesn't require a valid selection from the list, since
3683      // we're going to take the filename from the text input.
3684      var filename = this.filenameInput_.value;
3685      if (!filename)
3686        throw new Error('Missing filename!');
3687
3688      var directory = this.getCurrentDirectoryEntry();
3689      this.validateFileName_(directory, filename, function(isValid) {
3690        if (!isValid)
3691          return;
3692
3693        if (util.isFakeEntry(directory)) {
3694          // Can't save a file into a fake directory.
3695          return;
3696        }
3697
3698        var selectFileAndClose = function() {
3699          // TODO(mtomasz): Clean this up by avoiding constructing a URL
3700          //                via string concatenation.
3701          var currentDirUrl = directory.toURL();
3702          if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3703            currentDirUrl += '/';
3704          this.selectFilesAndClose_({
3705            urls: [currentDirUrl + encodeURIComponent(filename)],
3706            multiple: false,
3707            filterIndex: this.getSelectedFilterIndex_(filename)
3708          });
3709        }.bind(this);
3710
3711        directory.getFile(
3712            filename, {create: false},
3713            function(entry) {
3714              // An existing file is found. Show confirmation dialog to
3715              // overwrite it. If the user select "OK" on the dialog, save it.
3716              this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3717                                selectFileAndClose);
3718            }.bind(this),
3719            function(error) {
3720              if (error.name == util.FileError.NOT_FOUND_ERR) {
3721                // The file does not exist, so it should be ok to create a
3722                // new file.
3723                selectFileAndClose();
3724                return;
3725              }
3726              if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3727                // An directory is found.
3728                // Do not allow to overwrite directory.
3729                this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3730                return;
3731              }
3732
3733              // Unexpected error.
3734              console.error('File save failed: ' + error.code);
3735            }.bind(this));
3736      }.bind(this));
3737      return;
3738    }
3739
3740    var files = [];
3741    var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3742
3743    if (DialogType.isFolderDialog(this.dialogType) &&
3744        selectedIndexes.length == 0) {
3745      var url = this.getCurrentDirectoryEntry().toURL();
3746      var singleSelection = {
3747        urls: [url],
3748        multiple: false,
3749        filterIndex: this.getSelectedFilterIndex_()
3750      };
3751      this.selectFilesAndClose_(singleSelection);
3752      return;
3753    }
3754
3755    // All other dialog types require at least one selected list item.
3756    // The logic to control whether or not the ok button is enabled should
3757    // prevent us from ever getting here, but we sanity check to be sure.
3758    if (!selectedIndexes.length)
3759      throw new Error('Nothing selected!');
3760
3761    var dm = this.directoryModel_.getFileList();
3762    for (var i = 0; i < selectedIndexes.length; i++) {
3763      var entry = dm.item(selectedIndexes[i]);
3764      if (!entry) {
3765        console.error('Error locating selected file at index: ' + i);
3766        continue;
3767      }
3768
3769      files.push(entry.toURL());
3770    }
3771
3772    // Multi-file selection has no other restrictions.
3773    if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3774      var multipleSelection = {
3775        urls: files,
3776        multiple: true
3777      };
3778      this.selectFilesAndClose_(multipleSelection);
3779      return;
3780    }
3781
3782    // Everything else must have exactly one.
3783    if (files.length > 1)
3784      throw new Error('Too many files selected!');
3785
3786    var selectedEntry = dm.item(selectedIndexes[0]);
3787
3788    if (DialogType.isFolderDialog(this.dialogType)) {
3789      if (!selectedEntry.isDirectory)
3790        throw new Error('Selected entry is not a folder!');
3791    } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3792      if (!selectedEntry.isFile)
3793        throw new Error('Selected entry is not a file!');
3794    }
3795
3796    var singleSelection = {
3797      urls: [files[0]],
3798      multiple: false,
3799      filterIndex: this.getSelectedFilterIndex_()
3800    };
3801    this.selectFilesAndClose_(singleSelection);
3802  };
3803
3804  /**
3805   * Verifies the user entered name for file or folder to be created or
3806   * renamed to. See also util.validateFileName.
3807   *
3808   * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
3809   * @param {string} name New file or folder name.
3810   * @param {function} onDone Function to invoke when user closes the
3811   *    warning box or immediatelly if file name is correct. If the name was
3812   *    valid it is passed true, and false otherwise.
3813   * @private
3814   */
3815  FileManager.prototype.validateFileName_ = function(
3816      parentEntry, name, onDone) {
3817    var fileNameErrorPromise = util.validateFileName(
3818        parentEntry,
3819        name,
3820        this.fileFilter_.isFilterHiddenOn());
3821    fileNameErrorPromise.then(onDone.bind(null, true), function(message) {
3822      this.alert.show(message, onDone.bind(null, false));
3823    }.bind(this)).catch(function(error) {
3824      console.error(error.stack || error);
3825    });
3826  };
3827
3828  /**
3829   * Toggle whether mobile data is used for sync.
3830   */
3831  FileManager.prototype.toggleDriveSyncSettings = function() {
3832    // If checked, the sync is disabled.
3833    var nowCellularDisabled = this.syncButton.hasAttribute('checked');
3834    var changeInfo = {cellularDisabled: !nowCellularDisabled};
3835    chrome.fileManagerPrivate.setPreferences(changeInfo);
3836  };
3837
3838  /**
3839   * Toggle whether Google Docs files are shown.
3840   */
3841  FileManager.prototype.toggleDriveHostedSettings = function() {
3842    // If checked, showing drive hosted files is enabled.
3843    var nowHostedFilesEnabled = this.hostedButton.hasAttribute('checked');
3844    var nowHostedFilesDisabled = !nowHostedFilesEnabled;
3845    /*
3846    var changeInfo = {hostedFilesDisabled: !nowHostedFilesDisabled};
3847    */
3848    var changeInfo = {};
3849    changeInfo['hostedFilesDisabled'] = !nowHostedFilesDisabled;
3850    chrome.fileManagerPrivate.setPreferences(changeInfo);
3851  };
3852
3853  /**
3854   * Invoked when the search box is changed.
3855   *
3856   * @param {Event} event The changed event.
3857   * @private
3858   */
3859  FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3860    var searchString = this.searchBox_.value;
3861
3862    if (this.isOnDrive()) {
3863      // When the search text is changed, finishes the search and showes back
3864      // the last directory by passing an empty string to
3865      // {@code DirectoryModel.search()}.
3866      if (this.directoryModel_.isSearching() &&
3867          this.lastSearchQuery_ != searchString) {
3868        this.doSearch('');
3869      }
3870
3871      // On drive, incremental search is not invoked since we have an auto-
3872      // complete suggestion instead.
3873      return;
3874    }
3875
3876    this.search_(searchString);
3877  };
3878
3879  /**
3880   * Handle the search clear button click.
3881   * @private
3882   */
3883  FileManager.prototype.onSearchClearButtonClick_ = function() {
3884    this.ui_.searchBox.clear();
3885    this.onSearchBoxUpdate_();
3886  };
3887
3888  /**
3889   * Search files and update the list with the search result.
3890   *
3891   * @param {string} searchString String to be searched with.
3892   * @private
3893   */
3894  FileManager.prototype.search_ = function(searchString) {
3895    var noResultsDiv = this.document_.getElementById('no-search-results');
3896
3897    var reportEmptySearchResults = function() {
3898      if (this.directoryModel_.getFileList().length === 0) {
3899        // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3900        // hence we escapes |searchString| here.
3901        var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3902                        util.htmlEscape(searchString));
3903        noResultsDiv.innerHTML = html;
3904        noResultsDiv.setAttribute('show', 'true');
3905      } else {
3906        noResultsDiv.removeAttribute('show');
3907      }
3908
3909      // If the current location is somewhere in Drive, all files in Drive can
3910      // be listed as search results regardless of current location.
3911      // In this case, showing current location is confusing, so use the Drive
3912      // root "My Drive" as the current location.
3913      var entry = this.getCurrentDirectoryEntry();
3914      var locationInfo = this.volumeManager_.getLocationInfo(entry);
3915      if (locationInfo && locationInfo.isDriveBased) {
3916        var rootEntry = locationInfo.volumeInfo.displayRoot;
3917        if (rootEntry)
3918          this.updateLocationLine_(rootEntry);
3919      }
3920    };
3921
3922    var hideNoResultsDiv = function() {
3923      noResultsDiv.removeAttribute('show');
3924      this.updateLocationLine_();
3925    };
3926
3927    this.doSearch(searchString,
3928                  reportEmptySearchResults.bind(this),
3929                  hideNoResultsDiv.bind(this));
3930  };
3931
3932  /**
3933   * Performs search and displays results.
3934   *
3935   * @param {string} searchString Query that will be searched for.
3936   * @param {function()=} opt_onSearchRescan Function that will be called when
3937   *     the search directory is rescanned (i.e. search results are displayed).
3938   * @param {function()=} opt_onClearSearch Function to be called when search
3939   *     state gets cleared.
3940   */
3941  FileManager.prototype.doSearch = function(
3942      searchString, opt_onSearchRescan, opt_onClearSearch) {
3943    var onSearchRescan = opt_onSearchRescan || function() {};
3944    var onClearSearch = opt_onClearSearch || function() {};
3945
3946    this.lastSearchQuery_ = searchString;
3947    this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3948  };
3949
3950  /**
3951   * Requests autocomplete suggestions for files on Drive.
3952   * Once the suggestions are returned, the autocomplete popup will show up.
3953   *
3954   * @param {string} query The text to autocomplete from.
3955   * @private
3956   */
3957  FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3958    query = query.trimLeft();
3959
3960    // Only Drive supports auto-compelete
3961    if (!this.isOnDrive())
3962      return;
3963
3964    // Remember the most recent query. If there is an other request in progress,
3965    // then it's result will be discarded and it will call a new request for
3966    // this query.
3967    this.lastAutocompleteQuery_ = query;
3968    if (this.autocompleteSuggestionsBusy_)
3969      return;
3970
3971    if (!query) {
3972      this.autocompleteList_.suggestions = [];
3973      return;
3974    }
3975
3976    var headerItem = {isHeaderItem: true, searchQuery: query};
3977    if (!this.autocompleteList_.dataModel ||
3978        this.autocompleteList_.dataModel.length == 0)
3979      this.autocompleteList_.suggestions = [headerItem];
3980    else
3981      // Updates only the head item to prevent a flickering on typing.
3982      this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3983
3984    // The autocomplete list should be resized and repositioned here as the
3985    // search box is resized when it's focused.
3986    this.autocompleteList_.syncWidthAndPositionToInput();
3987
3988    this.autocompleteSuggestionsBusy_ = true;
3989
3990    var searchParams = {
3991      'query': query,
3992      'types': 'ALL',
3993      'maxResults': 4
3994    };
3995    chrome.fileManagerPrivate.searchDriveMetadata(
3996        searchParams,
3997        function(suggestions) {
3998          this.autocompleteSuggestionsBusy_ = false;
3999
4000          // Discard results for previous requests and fire a new search
4001          // for the most recent query.
4002          if (query != this.lastAutocompleteQuery_) {
4003            this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
4004            return;
4005          }
4006
4007          // Keeps the items in the suggestion list.
4008          this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
4009        }.bind(this));
4010  };
4011
4012  /**
4013   * Opens the currently selected suggestion item.
4014   * @private
4015   */
4016  FileManager.prototype.openAutocompleteSuggestion_ = function() {
4017    var selectedItem = this.autocompleteList_.selectedItem;
4018
4019    // If the entry is the search item or no entry is selected, just change to
4020    // the search result.
4021    if (!selectedItem || selectedItem.isHeaderItem) {
4022      var query = selectedItem ?
4023          selectedItem.searchQuery : this.searchBox_.value;
4024      this.search_(query);
4025      return;
4026    }
4027
4028    var entry = selectedItem.entry;
4029    // If the entry is a directory, just change the directory.
4030    if (entry.isDirectory) {
4031      this.onDirectoryAction_(entry);
4032      return;
4033    }
4034
4035    var entries = [entry];
4036    var self = this;
4037
4038    // To open a file, first get the mime type.
4039    this.metadataCache_.get(entries, 'external', function(props) {
4040      var mimeType = props[0].contentMimeType || '';
4041      var mimeTypes = [mimeType];
4042      var openIt = function() {
4043        if (self.dialogType == DialogType.FULL_PAGE) {
4044          var tasks = new FileTasks(self);
4045          tasks.init(entries, mimeTypes);
4046          tasks.executeDefault();
4047        } else {
4048          self.onOk_();
4049        }
4050      };
4051
4052      // Change the current directory to the directory that contains the
4053      // selected file. Note that this is necessary for an image or a video,
4054      // which should be opened in the gallery mode, as the gallery mode
4055      // requires the entry to be in the current directory model. For
4056      // consistency, the current directory is always changed regardless of
4057      // the file type.
4058      entry.getParent(function(parentEntry) {
4059        var onDirectoryChanged = function(event) {
4060          self.directoryModel_.removeEventListener('scan-completed',
4061                                                   onDirectoryChanged);
4062          self.directoryModel_.selectEntry(entry);
4063          openIt();
4064        };
4065        // changeDirectoryEntry() returns immediately. We should wait until the
4066        // directory scan is complete.
4067        self.directoryModel_.addEventListener('scan-completed',
4068                                              onDirectoryChanged);
4069        self.directoryModel_.changeDirectoryEntry(
4070            parentEntry,
4071            function() {
4072              // Remove the listner if the change directory failed.
4073              self.directoryModel_.removeEventListener('scan-completed',
4074                                                       onDirectoryChanged);
4075            });
4076      });
4077    });
4078  };
4079
4080  FileManager.prototype.decorateSplitter = function(splitterElement) {
4081    var self = this;
4082
4083    var Splitter = cr.ui.Splitter;
4084
4085    var customSplitter = cr.ui.define('div');
4086
4087    customSplitter.prototype = {
4088      __proto__: Splitter.prototype,
4089
4090      handleSplitterDragStart: function(e) {
4091        Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
4092        this.ownerDocument.documentElement.classList.add('col-resize');
4093      },
4094
4095      handleSplitterDragMove: function(deltaX) {
4096        Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
4097        self.onResize_();
4098      },
4099
4100      handleSplitterDragEnd: function(e) {
4101        Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
4102        this.ownerDocument.documentElement.classList.remove('col-resize');
4103      }
4104    };
4105
4106    customSplitter.decorate(splitterElement);
4107  };
4108
4109  /**
4110   * Updates default action menu item to match passed taskItem (icon,
4111   * label and action).
4112   *
4113   * @param {Object} defaultItem - taskItem to match.
4114   * @param {boolean} isMultiple - if multiple tasks available.
4115   */
4116  FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
4117                                                                isMultiple) {
4118    if (defaultItem) {
4119      if (defaultItem.iconType) {
4120        this.defaultActionMenuItem_.style.backgroundImage = '';
4121        this.defaultActionMenuItem_.setAttribute('file-type-icon',
4122                                                 defaultItem.iconType);
4123      } else if (defaultItem.iconUrl) {
4124        this.defaultActionMenuItem_.style.backgroundImage =
4125            'url(' + defaultItem.iconUrl + ')';
4126      } else {
4127        this.defaultActionMenuItem_.style.backgroundImage = '';
4128      }
4129
4130      this.defaultActionMenuItem_.label = defaultItem.title;
4131      this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
4132      this.defaultActionMenuItem_.taskId = defaultItem.taskId;
4133    }
4134
4135    var defaultActionSeparator =
4136        this.dialogDom_.querySelector('#default-action-separator');
4137
4138    this.openWithCommand_.canExecuteChange();
4139    this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
4140    this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
4141
4142    this.defaultActionMenuItem_.hidden = !defaultItem;
4143    defaultActionSeparator.hidden = !defaultItem;
4144  };
4145
4146  /**
4147   * @return {FileSelection} Selection object.
4148   */
4149  FileManager.prototype.getSelection = function() {
4150    return this.selectionHandler_.selection;
4151  };
4152
4153  /**
4154   * @return {ArrayDataModel} File list.
4155   */
4156  FileManager.prototype.getFileList = function() {
4157    return this.directoryModel_.getFileList();
4158  };
4159
4160  /**
4161   * @return {cr.ui.List} Current list object.
4162   */
4163  FileManager.prototype.getCurrentList = function() {
4164    return this.currentList_;
4165  };
4166
4167  /**
4168   * Retrieve the preferences of the files.app. This method caches the result
4169   * and returns it unless opt_update is true.
4170   * @param {function(Object.<string, *>)} callback Callback to get the
4171   *     preference.
4172   * @param {boolean=} opt_update If is's true, don't use the cache and
4173   *     retrieve latest preference. Default is false.
4174   * @private
4175   */
4176  FileManager.prototype.getPreferences_ = function(callback, opt_update) {
4177    if (!opt_update && this.preferences_ !== null) {
4178      callback(this.preferences_);
4179      return;
4180    }
4181
4182    chrome.fileManagerPrivate.getPreferences(function(prefs) {
4183      this.preferences_ = prefs;
4184      callback(prefs);
4185    }.bind(this));
4186  };
4187})();
4188