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