Upstream version 8.37.180.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  */
154 VolumeInfo.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  */
181 var 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  */
190 volumeManagerUtil.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  */
204 volumeManagerUtil.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  */
272 volumeManagerUtil.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  */
294 volumeManagerUtil.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  */
310 function 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
325 VolumeInfoList.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  */
334 VolumeInfoList.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  */
343 VolumeInfoList.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  */
352 VolumeInfoList.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  */
364 VolumeInfoList.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  */
375 VolumeInfoList.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  */
388 VolumeInfoList.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  */
409 VolumeInfoList.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  */
419 function 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  */
457 VolumeManager.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  */
468 VolumeManager.prototype.getDriveConnectionState = function() {
469   return this.driveConnectionState_;
470 };
471
472 /**
473  * VolumeManager extends cr.EventTarget.
474  */
475 VolumeManager.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  */
481 VolumeManager.TIMEOUT = 15 * 60 * 1000;
482
483 /**
484  * Queue to run getInstance sequentially.
485  * @type {AsyncUtil.Queue}
486  * @private
487  */
488 VolumeManager.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  */
496 VolumeManager.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  */
504 VolumeManager.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  */
526 VolumeManager.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  */
567 VolumeManager.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  */
638 VolumeManager.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  */
648 VolumeManager.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  */
665 VolumeManager.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  */
678 VolumeManager.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  */
688 VolumeManager.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  */
704 VolumeManager.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  */
776 VolumeManager.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  */
798 VolumeManager.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  */
811 VolumeManager.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  */
828 VolumeManager.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  */
853 function 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 }