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