• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7/**
8 * Represents each volume, such as "drive", "download directory", each "USB
9 * flush storage", or "mounted zip archive" etc.
10 *
11 * @param {VolumeManagerCommon.VolumeType} volumeType The type of the volume.
12 * @param {string} volumeId ID of the volume.
13 * @param {DOMFileSystem} fileSystem The file system object for this volume.
14 * @param {string} error The error if an error is found.
15 * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile'
16 *     |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc).
17 *     Can be null.
18 * @param {boolean} isReadOnly True if the volume is read only.
19 * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile
20 *     information.
21 * @param {string} label Label of the volume.
22 * @param {string} extensionId Id of the extension providing this volume. Empty
23 *     for native volumes.
24 * @constructor
25 */
26function VolumeInfo(
27    volumeType,
28    volumeId,
29    fileSystem,
30    error,
31    deviceType,
32    isReadOnly,
33    profile,
34    label,
35    extensionId) {
36  this.volumeType_ = volumeType;
37  this.volumeId_ = volumeId;
38  this.fileSystem_ = fileSystem;
39  this.label_ = label;
40  this.displayRoot_ = null;
41  this.fakeEntries_ = {};
42  this.displayRoot_ = null;
43  this.displayRootPromise_ = null;
44
45  if (volumeType === VolumeManagerCommon.VolumeType.DRIVE) {
46    // TODO(mtomasz): Convert fake entries to DirectoryProvider.
47    this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_OFFLINE] = {
48      isDirectory: true,
49      rootType: VolumeManagerCommon.RootType.DRIVE_OFFLINE,
50      toURL: function() { return 'fake-entry://drive_offline'; }
51    };
52    this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME] = {
53      isDirectory: true,
54      rootType: VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME,
55      toURL: function() { return 'fake-entry://drive_shared_with_me'; }
56    };
57    this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_RECENT] = {
58      isDirectory: true,
59      rootType: VolumeManagerCommon.RootType.DRIVE_RECENT,
60      toURL: function() { return 'fake-entry://drive_recent'; }
61    };
62  }
63
64  // Note: This represents if the mounting of the volume is successfully done
65  // or not. (If error is empty string, the mount is successfully done).
66  // TODO(hidehiko): Rename to make this more understandable.
67  this.error_ = error;
68  this.deviceType_ = deviceType;
69  this.isReadOnly_ = isReadOnly;
70  this.profile_ = Object.freeze(profile);
71  this.extensionId_ = extensionId;
72
73  Object.seal(this);
74}
75
76VolumeInfo.prototype = {
77  /**
78   * @return {VolumeManagerCommon.VolumeType} Volume type.
79   */
80  get volumeType() {
81    return this.volumeType_;
82  },
83  /**
84   * @return {string} Volume ID.
85   */
86  get volumeId() {
87    return this.volumeId_;
88  },
89  /**
90   * @return {DOMFileSystem} File system object.
91   */
92  get fileSystem() {
93    return this.fileSystem_;
94  },
95  /**
96   * @return {DirectoryEntry} Display root path. It is null before finishing to
97   * resolve the entry.
98   */
99  get displayRoot() {
100    return this.displayRoot_;
101  },
102  /**
103   * @return {Object.<string, Object>} Fake entries.
104   */
105  get fakeEntries() {
106    return this.fakeEntries_;
107  },
108  /**
109   * @return {string} Error identifier.
110   */
111  get error() {
112    return this.error_;
113  },
114  /**
115   * @return {string} Device type identifier.
116   */
117  get deviceType() {
118    return this.deviceType_;
119  },
120  /**
121   * @return {boolean} Whether read only or not.
122   */
123  get isReadOnly() {
124    return this.isReadOnly_;
125  },
126  /**
127   * @return {!{displayName:string, isCurrentProfile:boolean}} Profile data.
128   */
129  get profile() {
130    return this.profile_;
131  },
132  /**
133   * @return {string} Label for the volume.
134   */
135  get label() {
136    return this.label_;
137  },
138  /**
139   * @return {string} Id of an extennsion providing this volume.
140   */
141  get extensionId() {
142    return this.extensionId_;
143  }
144};
145
146/**
147 * Starts resolving the display root and obtains it.  It may take long time for
148 * Drive. Once resolved, it is cached.
149 *
150 * @param {function(DirectoryEntry)} onSuccess Success callback with the
151 *     display root directory as an argument.
152 * @param {function(FileError)} onFailure Failure callback.
153 */
154VolumeInfo.prototype.resolveDisplayRoot = function(onSuccess, onFailure) {
155  if (!this.displayRootPromise_) {
156    // TODO(mtomasz): Do not add VolumeInfo which failed to resolve root, and
157    // remove this if logic. Call onSuccess() always, instead.
158    if (this.volumeType !== VolumeManagerCommon.VolumeType.DRIVE) {
159      if (this.fileSystem_)
160        this.displayRootPromise_ = Promise.resolve(this.fileSystem_.root);
161      else
162        this.displayRootPromise_ = Promise.reject(this.error);
163    } else {
164      // For Drive, we need to resolve.
165      var displayRootURL = this.fileSystem_.root.toURL() + '/root';
166      this.displayRootPromise_ = new Promise(
167          webkitResolveLocalFileSystemURL.bind(null, displayRootURL));
168    }
169
170    // Store the obtained displayRoot.
171    this.displayRootPromise_.then(function(displayRoot) {
172      this.displayRoot_ = displayRoot;
173    }.bind(this));
174  }
175  this.displayRootPromise_.then(onSuccess, onFailure);
176};
177
178/**
179 * Utilities for volume manager implementation.
180 */
181var volumeManagerUtil = {};
182
183/**
184 * Throws an Error when the given error is not in
185 * VolumeManagerCommon.VolumeError.
186 *
187 * @param {VolumeManagerCommon.VolumeError} error Status string usually received
188 *     from APIs.
189 */
190volumeManagerUtil.validateError = function(error) {
191  for (var key in VolumeManagerCommon.VolumeError) {
192    if (error === VolumeManagerCommon.VolumeError[key])
193      return;
194  }
195
196  throw new Error('Invalid mount error: ' + error);
197};
198
199/**
200 * Builds the VolumeInfo data from VolumeMetadata.
201 * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume.
202 * @param {function(VolumeInfo)} callback Called on completion.
203 */
204volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) {
205  var localizedLabel;
206  switch (volumeMetadata.volumeType) {
207    case VolumeManagerCommon.VolumeType.DOWNLOADS:
208      localizedLabel = str('DOWNLOADS_DIRECTORY_LABEL');
209      break;
210    case VolumeManagerCommon.VolumeType.DRIVE:
211      localizedLabel = str('DRIVE_DIRECTORY_LABEL');
212      break;
213    default:
214      // TODO(mtomasz): Calculate volumeLabel for all types of volumes in the
215      // C++ layer.
216      localizedLabel = volumeMetadata.volumeLabel ||
217          volumeMetadata.volumeId.split(':', 2)[1];
218      break;
219  }
220
221  chrome.fileBrowserPrivate.requestFileSystem(
222      volumeMetadata.volumeId,
223      function(fileSystem) {
224        // TODO(mtomasz): chrome.runtime.lastError should have error reason.
225        if (!fileSystem) {
226          console.error('File system not found: ' + volumeMetadata.volumeId);
227          callback(new VolumeInfo(
228              volumeMetadata.volumeType,
229              volumeMetadata.volumeId,
230              null,  // File system is not found.
231              volumeMetadata.mountCondition,
232              volumeMetadata.deviceType,
233              volumeMetadata.isReadOnly,
234              volumeMetadata.profile,
235              localizedLabel,
236              volumeMetadata.extensionId));
237          return;
238        }
239        if (volumeMetadata.volumeType ==
240            VolumeManagerCommon.VolumeType.DRIVE) {
241          // After file system is mounted, we "read" drive grand root
242          // entry at first. This triggers full feed fetch on background.
243          // Note: we don't need to handle errors here, because even if
244          // it fails, accessing to some path later will just become
245          // a fast-fetch and it re-triggers full-feed fetch.
246          fileSystem.root.createReader().readEntries(
247              function() { /* do nothing */ },
248              function(error) {
249                console.error(
250                    'Triggering full feed fetch is failed: ' + error.name);
251              });
252        }
253        callback(new VolumeInfo(
254            volumeMetadata.volumeType,
255            volumeMetadata.volumeId,
256            fileSystem,
257            volumeMetadata.mountCondition,
258            volumeMetadata.deviceType,
259            volumeMetadata.isReadOnly,
260            volumeMetadata.profile,
261            localizedLabel,
262            volumeMetadata.extensionId));
263      });
264};
265
266/**
267 * The order of the volume list based on root type.
268 * @type {Array.<VolumeManagerCommon.VolumeType>}
269 * @const
270 * @private
271 */
272volumeManagerUtil.volumeListOrder_ = [
273  VolumeManagerCommon.VolumeType.DRIVE,
274  VolumeManagerCommon.VolumeType.DOWNLOADS,
275  VolumeManagerCommon.VolumeType.ARCHIVE,
276  VolumeManagerCommon.VolumeType.REMOVABLE,
277  VolumeManagerCommon.VolumeType.MTP,
278  VolumeManagerCommon.VolumeType.PROVIDED,
279  VolumeManagerCommon.VolumeType.CLOUD_DEVICE
280];
281
282/**
283 * Orders two volumes by volumeType and volumeId.
284 *
285 * The volumes at first are compared by volume type in the order of
286 * volumeListOrder_.  Then they are compared by volume ID.
287 *
288 * @param {VolumeInfo} volumeInfo1 Volume info to be compared.
289 * @param {VolumeInfo} volumeInfo2 Volume info to be compared.
290 * @return {number} Returns -1 if volume1 < volume2, returns 1 if volume2 >
291 *     volume1, returns 0 if volume1 === volume2.
292 * @private
293 */
294volumeManagerUtil.compareVolumeInfo_ = function(volumeInfo1, volumeInfo2) {
295  var typeIndex1 =
296      volumeManagerUtil.volumeListOrder_.indexOf(volumeInfo1.volumeType);
297  var typeIndex2 =
298      volumeManagerUtil.volumeListOrder_.indexOf(volumeInfo2.volumeType);
299  if (typeIndex1 !== typeIndex2)
300    return typeIndex1 < typeIndex2 ? -1 : 1;
301  if (volumeInfo1.volumeId !== volumeInfo2.volumeId)
302    return volumeInfo1.volumeId < volumeInfo2.volumeId ? -1 : 1;
303  return 0;
304};
305
306/**
307 * The container of the VolumeInfo for each mounted volume.
308 * @constructor
309 */
310function VolumeInfoList() {
311  var field = 'volumeType,volumeId';
312
313  /**
314   * Holds VolumeInfo instances.
315   * @type {cr.ui.ArrayDataModel}
316   * @private
317   */
318  this.model_ = new cr.ui.ArrayDataModel([]);
319  this.model_.setCompareFunction(field, volumeManagerUtil.compareVolumeInfo_);
320  this.model_.sort(field, 'asc');
321
322  Object.freeze(this);
323}
324
325VolumeInfoList.prototype = {
326  get length() { return this.model_.length; }
327};
328
329/**
330 * Adds the event listener to listen the change of volume info.
331 * @param {string} type The name of the event.
332 * @param {function(Event)} handler The handler for the event.
333 */
334VolumeInfoList.prototype.addEventListener = function(type, handler) {
335  this.model_.addEventListener(type, handler);
336};
337
338/**
339 * Removes the event listener.
340 * @param {string} type The name of the event.
341 * @param {function(Event)} handler The handler to be removed.
342 */
343VolumeInfoList.prototype.removeEventListener = function(type, handler) {
344  this.model_.removeEventListener(type, handler);
345};
346
347/**
348 * Adds the volumeInfo to the appropriate position. If there already exists,
349 * just replaces it.
350 * @param {VolumeInfo} volumeInfo The information of the new volume.
351 */
352VolumeInfoList.prototype.add = function(volumeInfo) {
353  var index = this.findIndex(volumeInfo.volumeId);
354  if (index !== -1)
355    this.model_.splice(index, 1, volumeInfo);
356  else
357    this.model_.push(volumeInfo);
358};
359
360/**
361 * Removes the VolumeInfo having the given ID.
362 * @param {string} volumeId ID of the volume.
363 */
364VolumeInfoList.prototype.remove = function(volumeId) {
365  var index = this.findIndex(volumeId);
366  if (index !== -1)
367    this.model_.splice(index, 1);
368};
369
370/**
371 * Obtains an index from the volume ID.
372 * @param {string} volumeId Volume ID.
373 * @return {number} Index of the volume.
374 */
375VolumeInfoList.prototype.findIndex = function(volumeId) {
376  for (var i = 0; i < this.model_.length; i++) {
377    if (this.model_.item(i).volumeId === volumeId)
378      return i;
379  }
380  return -1;
381};
382
383/**
384 * Searches the information of the volume that contains the passed entry.
385 * @param {Entry|Object} entry Entry on the volume to be found.
386 * @return {VolumeInfo} The volume's information, or null if not found.
387 */
388VolumeInfoList.prototype.findByEntry = function(entry) {
389  for (var i = 0; i < this.length; i++) {
390    var volumeInfo = this.item(i);
391    if (volumeInfo.fileSystem &&
392        util.isSameFileSystem(volumeInfo.fileSystem, entry.filesystem)) {
393      return volumeInfo;
394    }
395    // Additionally, check fake entries.
396    for (var key in volumeInfo.fakeEntries_) {
397      var fakeEntry = volumeInfo.fakeEntries_[key];
398      if (util.isSameEntry(fakeEntry, entry))
399        return volumeInfo;
400    }
401  }
402  return null;
403};
404
405/**
406 * @param {number} index The index of the volume in the list.
407 * @return {VolumeInfo} The VolumeInfo instance.
408 */
409VolumeInfoList.prototype.item = function(index) {
410  return this.model_.item(index);
411};
412
413/**
414 * VolumeManager is responsible for tracking list of mounted volumes.
415 *
416 * @constructor
417 * @extends {cr.EventTarget}
418 */
419function VolumeManager() {
420  /**
421   * The list of archives requested to mount. We will show contents once
422   * archive is mounted, but only for mounts from within this filebrowser tab.
423   * @type {Object.<string, Object>}
424   * @private
425   */
426  this.requests_ = {};
427
428  /**
429   * The list of VolumeInfo instances for each mounted volume.
430   * @type {VolumeInfoList}
431   */
432  this.volumeInfoList = new VolumeInfoList();
433
434  /**
435   * Queue for mounting.
436   * @type {AsyncUtil.Queue}
437   * @private
438   */
439  this.mountQueue_ = new AsyncUtil.Queue();
440
441  // The status should be merged into VolumeManager.
442  // TODO(hidehiko): Remove them after the migration.
443  this.driveConnectionState_ = {
444    type: VolumeManagerCommon.DriveConnectionType.OFFLINE,
445    reason: VolumeManagerCommon.DriveConnectionReason.NO_SERVICE
446  };
447
448  chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
449      this.onDriveConnectionStatusChanged_.bind(this));
450  this.onDriveConnectionStatusChanged_();
451}
452
453/**
454 * Invoked when the drive connection status is changed.
455 * @private_
456 */
457VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
458  chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
459    this.driveConnectionState_ = state;
460    cr.dispatchSimpleEvent(this, 'drive-connection-changed');
461  }.bind(this));
462};
463
464/**
465 * Returns the drive connection state.
466 * @return {VolumeManagerCommon.DriveConnectionType} Connection type.
467 */
468VolumeManager.prototype.getDriveConnectionState = function() {
469  return this.driveConnectionState_;
470};
471
472/**
473 * VolumeManager extends cr.EventTarget.
474 */
475VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
476
477/**
478 * Time in milliseconds that we wait a response for. If no response on
479 * mount/unmount received the request supposed failed.
480 */
481VolumeManager.TIMEOUT = 15 * 60 * 1000;
482
483/**
484 * Queue to run getInstance sequentially.
485 * @type {AsyncUtil.Queue}
486 * @private
487 */
488VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
489
490/**
491 * The singleton instance of VolumeManager. Initialized by the first invocation
492 * of getInstance().
493 * @type {VolumeManager}
494 * @private
495 */
496VolumeManager.instance_ = null;
497
498/**
499 * Returns the VolumeManager instance asynchronously. If it is not created or
500 * under initialization, it will waits for the finish of the initialization.
501 * @param {function(VolumeManager)} callback Called with the VolumeManager
502 *     instance.
503 */
504VolumeManager.getInstance = function(callback) {
505  VolumeManager.getInstanceQueue_.run(function(continueCallback) {
506    if (VolumeManager.instance_) {
507      callback(VolumeManager.instance_);
508      continueCallback();
509      return;
510    }
511
512    VolumeManager.instance_ = new VolumeManager();
513    VolumeManager.instance_.initialize_(function() {
514      callback(VolumeManager.instance_);
515      continueCallback();
516    });
517  });
518};
519
520/**
521 * Initializes mount points.
522 * @param {function()} callback Called upon the completion of the
523 *     initialization.
524 * @private
525 */
526VolumeManager.prototype.initialize_ = function(callback) {
527  chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) {
528    // We must subscribe to the mount completed event in the callback of
529    // getVolumeMetadataList. crbug.com/330061.
530    // But volumes reported by onMountCompleted events must be added after the
531    // volumes in the volumeMetadataList are mounted. crbug.com/135477.
532    this.mountQueue_.run(function(inCallback) {
533      // Create VolumeInfo for each volume.
534      var group = new AsyncUtil.Group();
535      for (var i = 0; i < volumeMetadataList.length; i++) {
536        group.add(function(volumeMetadata, continueCallback) {
537          volumeManagerUtil.createVolumeInfo(
538              volumeMetadata,
539              function(volumeInfo) {
540                this.volumeInfoList.add(volumeInfo);
541                if (volumeMetadata.volumeType ===
542                    VolumeManagerCommon.VolumeType.DRIVE)
543                  this.onDriveConnectionStatusChanged_();
544                continueCallback();
545              }.bind(this));
546        }.bind(this, volumeMetadataList[i]));
547      }
548      group.run(function() {
549        // Call the callback of the initialize function.
550        callback();
551        // Call the callback of AsyncQueue. Maybe it invokes callbacks
552        // registered by mountCompleted events.
553        inCallback();
554      });
555    }.bind(this));
556
557    chrome.fileBrowserPrivate.onMountCompleted.addListener(
558        this.onMountCompleted_.bind(this));
559  }.bind(this));
560};
561
562/**
563 * Event handler called when some volume was mounted or unmounted.
564 * @param {MountCompletedEvent} event Received event.
565 * @private
566 */
567VolumeManager.prototype.onMountCompleted_ = function(event) {
568  this.mountQueue_.run(function(callback) {
569    switch (event.eventType) {
570      case 'mount':
571        var requestKey = this.makeRequestKey_(
572            'mount',
573            event.volumeMetadata.sourcePath);
574
575        var error = event.status === 'success' ? '' : event.status;
576        if (!error || event.status === 'error_unknown_filesystem') {
577          volumeManagerUtil.createVolumeInfo(
578              event.volumeMetadata,
579              function(volumeInfo) {
580                this.volumeInfoList.add(volumeInfo);
581                this.finishRequest_(requestKey, event.status, volumeInfo);
582
583                if (volumeInfo.volumeType ===
584                    VolumeManagerCommon.VolumeType.DRIVE) {
585                  // Update the network connection status, because until the
586                  // drive is initialized, the status is set to not ready.
587                  // TODO(mtomasz): The connection status should be migrated
588                  // into VolumeMetadata.
589                  this.onDriveConnectionStatusChanged_();
590                }
591                callback();
592              }.bind(this));
593        } else {
594          console.warn('Failed to mount a volume: ' + event.status);
595          this.finishRequest_(requestKey, event.status);
596          callback();
597        }
598        break;
599
600      case 'unmount':
601        var volumeId = event.volumeMetadata.volumeId;
602        var status = event.status;
603        if (status === VolumeManagerCommon.VolumeError.PATH_UNMOUNTED) {
604          console.warn('Volume already unmounted: ', volumeId);
605          status = 'success';
606        }
607        var requestKey = this.makeRequestKey_('unmount', volumeId);
608        var requested = requestKey in this.requests_;
609        var volumeInfoIndex =
610            this.volumeInfoList.findIndex(volumeId);
611        var volumeInfo = volumeInfoIndex !== -1 ?
612            this.volumeInfoList.item(volumeInfoIndex) : null;
613        if (event.status === 'success' && !requested && volumeInfo) {
614          console.warn('Mounted volume without a request: ' + volumeId);
615          var e = new Event('externally-unmounted');
616          e.volumeInfo = volumeInfo;
617          this.dispatchEvent(e);
618        }
619
620        this.finishRequest_(requestKey, status);
621        if (event.status === 'success')
622          this.volumeInfoList.remove(event.volumeMetadata.volumeId);
623        callback();
624        break;
625    }
626  }.bind(this));
627};
628
629/**
630 * Creates string to match mount events with requests.
631 * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
632 *     enum.
633 * @param {string} argument Argument describing the request, eg. source file
634 *     path of the archive to be mounted, or a volumeId for unmounting.
635 * @return {string} Key for |this.requests_|.
636 * @private
637 */
638VolumeManager.prototype.makeRequestKey_ = function(requestType, argument) {
639  return requestType + ':' + argument;
640};
641
642/**
643 * @param {string} fileUrl File url to the archive file.
644 * @param {function(VolumeInfo)} successCallback Success callback.
645 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Error
646 *     callback.
647 */
648VolumeManager.prototype.mountArchive = function(
649    fileUrl, successCallback, errorCallback) {
650  chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
651    console.info(
652        'Mount request: url=' + fileUrl + '; sourcePath=' + sourcePath);
653    var requestKey = this.makeRequestKey_('mount', sourcePath);
654    this.startRequest_(requestKey, successCallback, errorCallback);
655  }.bind(this));
656};
657
658/**
659 * Unmounts volume.
660 * @param {!VolumeInfo} volumeInfo Volume to be unmounted.
661 * @param {function()} successCallback Success callback.
662 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Error
663 *     callback.
664 */
665VolumeManager.prototype.unmount = function(volumeInfo,
666                                           successCallback,
667                                           errorCallback) {
668  chrome.fileBrowserPrivate.removeMount(volumeInfo.volumeId);
669  var requestKey = this.makeRequestKey_('unmount', volumeInfo.volumeId);
670  this.startRequest_(requestKey, successCallback, errorCallback);
671};
672
673/**
674 * Obtains a volume info containing the passed entry.
675 * @param {Entry|Object} entry Entry on the volume to be returned. Can be fake.
676 * @return {VolumeInfo} The VolumeInfo instance or null if not found.
677 */
678VolumeManager.prototype.getVolumeInfo = function(entry) {
679  return this.volumeInfoList.findByEntry(entry);
680};
681
682/**
683 * Obtains volume information of the current profile.
684 *
685 * @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
686 * @return {VolumeInfo} Volume info.
687 */
688VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) {
689  for (var i = 0; i < this.volumeInfoList.length; i++) {
690    var volumeInfo = this.volumeInfoList.item(i);
691    if (volumeInfo.profile.isCurrentProfile &&
692        volumeInfo.volumeType === volumeType)
693      return volumeInfo;
694  }
695  return null;
696};
697
698/**
699 * Obtains location information from an entry.
700 *
701 * @param {Entry|Object} entry File or directory entry. It can be a fake entry.
702 * @return {EntryLocation} Location information.
703 */
704VolumeManager.prototype.getLocationInfo = function(entry) {
705  var volumeInfo = this.volumeInfoList.findByEntry(entry);
706  if (!volumeInfo)
707    return null;
708
709  if (util.isFakeEntry(entry)) {
710    return new EntryLocation(
711        volumeInfo,
712        entry.rootType,
713        true /* the entry points a root directory. */,
714        true /* fake entries are read only. */);
715  }
716
717  var rootType;
718  var isReadOnly;
719  var isRootEntry;
720  if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE) {
721    // For Drive, the roots are /root and /other, instead of /. Root URLs
722    // contain trailing slashes.
723    if (entry.fullPath == '/root' || entry.fullPath.indexOf('/root/') === 0) {
724      rootType = VolumeManagerCommon.RootType.DRIVE;
725      isReadOnly = volumeInfo.isReadOnly;
726      isRootEntry = entry.fullPath === '/root';
727    } else if (entry.fullPath == '/other' ||
728               entry.fullPath.indexOf('/other/') === 0) {
729      rootType = VolumeManagerCommon.RootType.DRIVE_OTHER;
730      isReadOnly = true;
731      isRootEntry = entry.fullPath === '/other';
732    } else {
733      // Accessing Drive files outside of /drive/root and /drive/other is not
734      // allowed, but can happen. Therefore returning null.
735      return null;
736    }
737  } else {
738    switch (volumeInfo.volumeType) {
739      case VolumeManagerCommon.VolumeType.DOWNLOADS:
740        rootType = VolumeManagerCommon.RootType.DOWNLOADS;
741        break;
742      case VolumeManagerCommon.VolumeType.REMOVABLE:
743        rootType = VolumeManagerCommon.RootType.REMOVABLE;
744        break;
745      case VolumeManagerCommon.VolumeType.ARCHIVE:
746        rootType = VolumeManagerCommon.RootType.ARCHIVE;
747        break;
748      case VolumeManagerCommon.VolumeType.CLOUD_DEVICE:
749        rootType = VolumeManagerCommon.RootType.CLOUD_DEVICE;
750        break;
751      case VolumeManagerCommon.VolumeType.MTP:
752        rootType = VolumeManagerCommon.RootType.MTP;
753        break;
754      case VolumeManagerCommon.VolumeType.PROVIDED:
755        rootType = VolumeManagerCommon.RootType.PROVIDED;
756        break;
757      default:
758        // Programming error, throw an exception.
759        throw new Error('Invalid volume type: ' + volumeInfo.volumeType);
760    }
761    isReadOnly = volumeInfo.isReadOnly;
762    isRootEntry = util.isSameEntry(entry, volumeInfo.fileSystem.root);
763  }
764
765  return new EntryLocation(volumeInfo, rootType, isRootEntry, isReadOnly);
766};
767
768/**
769 * @param {string} key Key produced by |makeRequestKey_|.
770 * @param {function(VolumeInfo)} successCallback To be called when the request
771 *     finishes successfully.
772 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback To be called
773 *     when the request fails.
774 * @private
775 */
776VolumeManager.prototype.startRequest_ = function(key,
777    successCallback, errorCallback) {
778  if (key in this.requests_) {
779    var request = this.requests_[key];
780    request.successCallbacks.push(successCallback);
781    request.errorCallbacks.push(errorCallback);
782  } else {
783    this.requests_[key] = {
784      successCallbacks: [successCallback],
785      errorCallbacks: [errorCallback],
786
787      timeout: setTimeout(this.onTimeout_.bind(this, key),
788                          VolumeManager.TIMEOUT)
789    };
790  }
791};
792
793/**
794 * Called if no response received in |TIMEOUT|.
795 * @param {string} key Key produced by |makeRequestKey_|.
796 * @private
797 */
798VolumeManager.prototype.onTimeout_ = function(key) {
799  this.invokeRequestCallbacks_(this.requests_[key],
800                               VolumeManagerCommon.VolumeError.TIMEOUT);
801  delete this.requests_[key];
802};
803
804/**
805 * @param {string} key Key produced by |makeRequestKey_|.
806 * @param {VolumeManagerCommon.VolumeError|'success'} status Status received
807 *     from the API.
808 * @param {VolumeInfo=} opt_volumeInfo Volume info of the mounted volume.
809 * @private
810 */
811VolumeManager.prototype.finishRequest_ = function(key, status, opt_volumeInfo) {
812  var request = this.requests_[key];
813  if (!request)
814    return;
815
816  clearTimeout(request.timeout);
817  this.invokeRequestCallbacks_(request, status, opt_volumeInfo);
818  delete this.requests_[key];
819};
820
821/**
822 * @param {Object} request Structure created in |startRequest_|.
823 * @param {VolumeManagerCommon.VolumeError|string} status If status ===
824 *     'success' success callbacks are called.
825 * @param {VolumeInfo=} opt_volumeInfo Volume info of the mounted volume.
826 * @private
827 */
828VolumeManager.prototype.invokeRequestCallbacks_ = function(
829    request, status, opt_volumeInfo) {
830  var callEach = function(callbacks, self, args) {
831    for (var i = 0; i < callbacks.length; i++) {
832      callbacks[i].apply(self, args);
833    }
834  };
835  if (status === 'success') {
836    callEach(request.successCallbacks, this, [opt_volumeInfo]);
837  } else {
838    volumeManagerUtil.validateError(status);
839    callEach(request.errorCallbacks, this, [status]);
840  }
841};
842
843/**
844 * Location information which shows where the path points in FileManager's
845 * file system.
846 *
847 * @param {!VolumeInfo} volumeInfo Volume information.
848 * @param {VolumeManagerCommon.RootType} rootType Root type.
849 * @param {boolean} isRootEntry Whether the entry is root entry or not.
850 * @param {boolean} isReadOnly Whether the entry is read only or not.
851 * @constructor
852 */
853function EntryLocation(volumeInfo, rootType, isRootEntry, isReadOnly) {
854  /**
855   * Volume information.
856   * @type {!VolumeInfo}
857   */
858  this.volumeInfo = volumeInfo;
859
860  /**
861   * Root type.
862   * @type {VolumeManagerCommon.RootType}
863   */
864  this.rootType = rootType;
865
866  /**
867   * Whether the entry is root entry or not.
868   * @type {boolean}
869   */
870  this.isRootEntry = isRootEntry;
871
872  /**
873   * Whether the location obtained from the fake entry correspond to special
874   * searches.
875   * @type {boolean}
876   */
877  this.isSpecialSearchRoot =
878      this.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE ||
879      this.rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
880      this.rootType === VolumeManagerCommon.RootType.DRIVE_RECENT;
881
882  /**
883   * Whether the location is under Google Drive or a special search root which
884   * represents a special search from Google Drive.
885   * @type {boolean}
886   */
887  this.isDriveBased =
888      this.rootType === VolumeManagerCommon.RootType.DRIVE ||
889      this.rootType === VolumeManagerCommon.RootType.DRIVE_OTHER ||
890      this.rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
891      this.rootType === VolumeManagerCommon.RootType.DRIVE_RECENT ||
892      this.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE;
893
894  /**
895   * Whether the given path can be a target path of folder shortcut.
896   * @type {boolean}
897   */
898  this.isEligibleForFolderShortcut =
899      !this.isSpecialSearchRoot &&
900      !this.isRootEntry &&
901      this.isDriveBased;
902
903  /**
904   * Whether the entry is read only or not.
905   * @type {boolean}
906   */
907  this.isReadOnly = isReadOnly;
908
909  Object.freeze(this);
910}
911