1// Copyright 2013 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 * Thin wrapper for VolumeManager. This should be an interface proxy to talk
7 * to VolumeManager. This class also filters Drive related data/events if
8 * driveEnabled is set to false.
9 *
10 * @param {VolumeManagerWrapper.DriveEnabledStatus} driveEnabled DRIVE_ENABLED
11 *     if drive should be available. DRIVE_DISABLED if drive related
12 *     data/events should be hidden.
13 * @param {DOMWindow=} opt_backgroundPage Window object of the background
14 *     page. If this is specified, the class skips to get background page.
15 *     TOOD(hirono): Let all clients of the class pass the background page and
16 *     make the argument not optional.
17 * @constructor
18 * @extends {cr.EventTarget}
19 */
20function VolumeManagerWrapper(driveEnabled, opt_backgroundPage) {
21  cr.EventTarget.call(this);
22
23  this.driveEnabled_ = driveEnabled;
24  this.volumeInfoList = new cr.ui.ArrayDataModel([]);
25
26  this.volumeManager_ = null;
27  this.pendingTasks_ = [];
28  this.onEventBound_ = this.onEvent_.bind(this);
29  this.onVolumeInfoListUpdatedBound_ =
30      this.onVolumeInfoListUpdated_.bind(this);
31
32  this.disposed_ = false;
33
34  // Start initialize the VolumeManager.
35  var queue = new AsyncUtil.Queue();
36
37  if (opt_backgroundPage) {
38    this.backgroundPage_ = opt_backgroundPage;
39  } else {
40    queue.run(function(callNextStep) {
41      chrome.runtime.getBackgroundPage(function(backgroundPage) {
42        this.backgroundPage_ = backgroundPage;
43        callNextStep();
44      }.bind(this));
45    }.bind(this));
46  }
47
48  queue.run(function(callNextStep) {
49    this.backgroundPage_.VolumeManager.getInstance(function(volumeManager) {
50      this.onReady_(volumeManager);
51      callNextStep();
52    }.bind(this));
53  }.bind(this));
54}
55
56/**
57 * If the drive is enabled on the wrapper.
58 * @enum {boolean}
59 */
60VolumeManagerWrapper.DriveEnabledStatus = {
61  DRIVE_ENABLED: true,
62  DRIVE_DISABLED: false
63};
64
65/**
66 * Extends cr.EventTarget.
67 */
68VolumeManagerWrapper.prototype.__proto__ = cr.EventTarget.prototype;
69
70/**
71 * Called when the VolumeManager gets ready for post initialization.
72 * @param {VolumeManager} volumeManager The initialized VolumeManager instance.
73 * @private
74 */
75VolumeManagerWrapper.prototype.onReady_ = function(volumeManager) {
76  if (this.disposed_)
77    return;
78
79  this.volumeManager_ = volumeManager;
80
81  // Subscribe to VolumeManager.
82  this.volumeManager_.addEventListener(
83      'drive-connection-changed', this.onEventBound_);
84  this.volumeManager_.addEventListener(
85      'externally-unmounted', this.onEventBound_);
86
87  // Cache volumeInfoList.
88  var volumeInfoList = [];
89  for (var i = 0; i < this.volumeManager_.volumeInfoList.length; i++) {
90    var volumeInfo = this.volumeManager_.volumeInfoList.item(i);
91    // TODO(hidehiko): Filter mounted volumes located on Drive File System.
92    if (!this.driveEnabled_ && volumeInfo.volumeType ===
93        VolumeManagerCommon.VolumeType.DRIVE)
94      continue;
95    volumeInfoList.push(volumeInfo);
96  }
97  this.volumeInfoList.splice.apply(
98      this.volumeInfoList,
99      [0, this.volumeInfoList.length].concat(volumeInfoList));
100
101  // Subscribe to VolumeInfoList.
102  // In VolumeInfoList, we only use 'splice' event.
103  this.volumeManager_.volumeInfoList.addEventListener(
104      'splice', this.onVolumeInfoListUpdatedBound_);
105
106  // Run pending tasks.
107  var pendingTasks = this.pendingTasks_;
108  this.pendingTasks_ = null;
109  for (var i = 0; i < pendingTasks.length; i++)
110    pendingTasks[i]();
111};
112
113/**
114 * Disposes the instance. After the invocation of this method, any other
115 * method should not be called.
116 */
117VolumeManagerWrapper.prototype.dispose = function() {
118  this.disposed_ = true;
119
120  if (!this.volumeManager_)
121    return;
122  this.volumeManager_.removeEventListener(
123      'drive-connection-changed', this.onEventBound_);
124  this.volumeManager_.removeEventListener(
125      'externally-unmounted', this.onEventBound_);
126  this.volumeManager_.volumeInfoList.removeEventListener(
127      'splice', this.onVolumeInfoListUpdatedBound_);
128};
129
130/**
131 * Called on events sent from VolumeManager. This has responsibility to
132 * re-dispatch the event to the listeners.
133 * @param {Event} event Event object sent from VolumeManager.
134 * @private
135 */
136VolumeManagerWrapper.prototype.onEvent_ = function(event) {
137  if (!this.driveEnabled_) {
138    // If the drive is disabled, ignore all drive related events.
139    if (event.type === 'drive-connection-changed' ||
140        (event.type === 'externally-unmounted' &&
141         event.volumeInfo.volumeType ===
142             VolumeManagerCommon.VolumeType.DRIVE)) {
143      return;
144    }
145  }
146
147  this.dispatchEvent(event);
148};
149
150/**
151 * Called on events of modifying VolumeInfoList.
152 * @param {Event} event Event object sent from VolumeInfoList.
153 * @private
154 */
155VolumeManagerWrapper.prototype.onVolumeInfoListUpdated_ = function(event) {
156  if (this.driveEnabled_) {
157    // Apply the splice as is.
158    this.volumeInfoList.splice.apply(
159        this.volumeInfoList,
160        [event.index, event.removed.length].concat(event.added));
161  } else {
162    // Filters drive related volumes.
163    var index = event.index;
164    for (var i = 0; i < event.index; i++) {
165      if (this.volumeManager_.volumeInfoList.item(i).volumeType ===
166          VolumeManagerCommon.VolumeType.DRIVE)
167        index--;
168    }
169
170    var numRemovedVolumes = 0;
171    for (var i = 0; i < event.removed.length; i++) {
172      if (event.removed[i].volumeType !== VolumeManagerCommon.VolumeType.DRIVE)
173        numRemovedVolumes++;
174    }
175
176    var addedVolumes = [];
177    for (var i = 0; i < event.added.length; i++) {
178      var volumeInfo = event.added[i];
179      if (volumeInfo.volumeType !== VolumeManagerCommon.VolumeType.DRIVE)
180        addedVolumes.push(volumeInfo);
181    }
182
183    this.volumeInfoList.splice.apply(
184        this.volumeInfoList,
185        [index, numRemovedVolumes].concat(addedVolumes));
186  }
187};
188
189/**
190 * Ensures the VolumeManager is initialized, and then invokes callback.
191 * If the VolumeManager is already initialized, callback will be called
192 * immediately.
193 * @param {function()} callback Called on initialization completion.
194 */
195VolumeManagerWrapper.prototype.ensureInitialized = function(callback) {
196  if (this.pendingTasks_) {
197    this.pendingTasks_.push(this.ensureInitialized.bind(this, callback));
198    return;
199  }
200
201  callback();
202};
203
204/**
205 * @return {VolumeManagerCommon.DriveConnectionType} Current drive connection
206 *     state.
207 */
208VolumeManagerWrapper.prototype.getDriveConnectionState = function() {
209  if (!this.driveEnabled_ || !this.volumeManager_) {
210    return {
211      type: VolumeManagerCommon.DriveConnectionType.OFFLINE,
212      reason: VolumeManagerCommon.DriveConnectionReason.NO_SERVICE
213    };
214  }
215
216  return this.volumeManager_.getDriveConnectionState();
217};
218
219/**
220 * Obtains a volume info containing the passed entry.
221 * @param {Entry} entry Entry on the volume to be returned.
222 * @return {VolumeInfo} The VolumeInfo instance or null if not found.
223 */
224VolumeManagerWrapper.prototype.getVolumeInfo = function(entry) {
225  return this.filterDisabledDriveVolume_(
226      this.volumeManager_ && this.volumeManager_.getVolumeInfo(entry));
227};
228
229/**
230 * Obtains a volume information of the current profile.
231 * @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
232 * @return {VolumeInfo} Found volume info.
233 */
234VolumeManagerWrapper.prototype.getCurrentProfileVolumeInfo =
235    function(volumeType) {
236  return this.filterDisabledDriveVolume_(
237      this.volumeManager_ &&
238      this.volumeManager_.getCurrentProfileVolumeInfo(volumeType));
239};
240
241/**
242 * Obtains the default display root entry.
243 * @param {function(Entry)} callback Callback passed the default display root.
244 */
245VolumeManagerWrapper.prototype.getDefaultDisplayRoot =
246    function(callback) {
247  this.ensureInitialized(function() {
248    var defaultVolume = this.getCurrentProfileVolumeInfo(
249        VolumeManagerCommon.VolumeType.DOWNLOADS);
250    defaultVolume.resolveDisplayRoot(callback, function() {
251      // defaultVolume is DOWNLOADS and resolveDisplayRoot should succeed.
252      throw new Error(
253          'Unexpectedly failed to obtain the default display root.');
254    });
255  }.bind(this));
256};
257
258/**
259 * Obtains location information from an entry.
260 *
261 * @param {Entry} entry File or directory entry.
262 * @return {EntryLocation} Location information.
263 */
264VolumeManagerWrapper.prototype.getLocationInfo = function(entry) {
265  var locationInfo =
266      this.volumeManager_ && this.volumeManager_.getLocationInfo(entry);
267  if (!locationInfo)
268    return null;
269  if (!this.filterDisabledDriveVolume_(locationInfo.volumeInfo))
270    return null;
271  return locationInfo;
272};
273
274/**
275 * Requests to mount the archive file.
276 * @param {string} fileUrl The path to the archive file to be mounted.
277 * @param {function(VolumeInfo)} successCallback Called with the VolumeInfo
278 *     instance.
279 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Called when
280 *     an error occurs.
281 */
282VolumeManagerWrapper.prototype.mountArchive = function(
283    fileUrl, successCallback, errorCallback) {
284  if (this.pendingTasks_) {
285    this.pendingTasks_.push(
286        this.mountArchive.bind(this, fileUrl, successCallback, errorCallback));
287    return;
288  }
289
290  this.volumeManager_.mountArchive(fileUrl, successCallback, errorCallback);
291};
292
293/**
294 * Requests unmount the specified volume.
295 * @param {!VolumeInfo} volumeInfo Volume to be unmounted.
296 * @param {function()} successCallback Called on success.
297 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Called when
298 *     an error occurs.
299 */
300VolumeManagerWrapper.prototype.unmount = function(
301    volumeInfo, successCallback, errorCallback) {
302  if (this.pendingTasks_) {
303    this.pendingTasks_.push(
304        this.unmount.bind(this, volumeInfo, successCallback, errorCallback));
305    return;
306  }
307
308  this.volumeManager_.unmount(volumeInfo, successCallback, errorCallback);
309};
310
311/**
312 * Filters volume info by referring driveEnabled.
313 *
314 * @param {VolumeInfo} volumeInfo Volume info.
315 * @return {VolumeInfo} Null if the drive is disabled and the given volume is
316 *     drive. Otherwise just returns the volume.
317 * @private
318 */
319VolumeManagerWrapper.prototype.filterDisabledDriveVolume_ =
320    function(volumeInfo) {
321  var isDrive = volumeInfo && volumeInfo.volumeType ===
322      VolumeManagerCommon.VolumeType.DRIVE;
323  return this.driveEnabled_ || !isDrive ? volumeInfo : null;
324};
325