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