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'use strict';
6
7/**
8 * Watches for changes in the tracked directory, including local metadata
9 * changes.
10 *
11 * @param {MetadataCache} metadataCache Instance of MetadataCache.
12 * @extends {cr.EventTarget}
13 * @constructor
14 */
15function FileWatcher(metadataCache) {
16  this.queue_ = new AsyncUtil.Queue();
17  this.metadataCache_ = metadataCache;
18  this.watchedDirectoryEntry_ = null;
19
20  this.onDirectoryChangedBound_ = this.onDirectoryChanged_.bind(this);
21  chrome.fileManagerPrivate.onDirectoryChanged.addListener(
22      this.onDirectoryChangedBound_);
23
24  this.filesystemMetadataObserverId_ = null;
25  this.thumbnailMetadataObserverId_ = null;
26  this.externalMetadataObserverId_ = null;
27}
28
29/**
30 * FileWatcher extends cr.EventTarget.
31 */
32FileWatcher.prototype.__proto__ = cr.EventTarget.prototype;
33
34/**
35 * Stops watching (must be called before page unload).
36 */
37FileWatcher.prototype.dispose = function() {
38  chrome.fileManagerPrivate.onDirectoryChanged.removeListener(
39      this.onDirectoryChangedBound_);
40  if (this.watchedDirectoryEntry_)
41    this.resetWatchedEntry_(function() {}, function() {});
42};
43
44/**
45 * Called when a file in the watched directory is changed.
46 * @param {Event} event Change event.
47 * @private
48 */
49FileWatcher.prototype.onDirectoryChanged_ = function(event) {
50  if (this.watchedDirectoryEntry_ &&
51      event.entry.toURL() === this.watchedDirectoryEntry_.toURL()) {
52    var e = new Event('watcher-directory-changed');
53    e.changedFiles = event.changedFiles;
54    this.dispatchEvent(e);
55  }
56};
57
58/**
59 * Called when general metadata in the watched directory has been changed.
60 *
61 * @param {Array.<Entry>} entries Array of entries.
62 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
63 *     properties.
64 * @private
65 */
66FileWatcher.prototype.onFilesystemMetadataChanged_ = function(
67    entries, properties) {
68  this.dispatchMetadataEvent_('filesystem', entries, properties);
69};
70
71/**
72 * Called when thumbnail metadata in the watched directory has been changed.
73 *
74 * @param {Array.<Entry>} entries Array of entries.
75 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
76 *     properties.
77 * @private
78 */
79FileWatcher.prototype.onThumbnailMetadataChanged_ = function(
80    entries, properties) {
81  this.dispatchMetadataEvent_('thumbnail', entries, properties);
82};
83
84/**
85 * Called when external metadata in the watched directory has been changed.
86 *
87 * @param {Array.<Entry>} entries Array of entries.
88 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
89 *     properties.
90 * @private
91 */
92FileWatcher.prototype.onExternalMetadataChanged_ = function(
93    entries, properties) {
94  this.dispatchMetadataEvent_('external', entries, properties);
95};
96
97/**
98 * Dispatches an event about detected change in metadata within the tracked
99 * directory.
100 *
101 * @param {string} type Type of the metadata change.
102 * @param {Array.<Entry>} entries Array of entries.
103 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
104 *     properties.
105 * @private
106 */
107FileWatcher.prototype.dispatchMetadataEvent_ = function(
108    type, entries, properties) {
109  var e = new Event('watcher-metadata-changed');
110  e.metadataType = type;
111  e.entries = entries;
112  e.properties = properties;
113  this.dispatchEvent(e);
114};
115
116/**
117 * Changes the watched directory. In case of a fake entry, the watch is
118 * just released, since there is no reason to track a fake directory.
119 *
120 * @param {!DirectoryEntry|!Object} entry Directory entry to be tracked, or the
121 *     fake entry.
122 * @param {function()} callback Completion callback.
123 */
124FileWatcher.prototype.changeWatchedDirectory = function(entry, callback) {
125  if (!util.isFakeEntry(entry)) {
126    this.changeWatchedEntry_(
127        entry,
128        callback,
129        function() {
130          console.error(
131              'Unable to change the watched directory to: ' + entry.toURL());
132          callback();
133        });
134  } else {
135    this.resetWatchedEntry_(
136        callback,
137        function() {
138          console.error('Unable to reset the watched directory.');
139          callback();
140        });
141  }
142};
143
144/**
145 * Resets the watched entry to the passed directory.
146 *
147 * @param {function()} onSuccess Success callback.
148 * @param {function()} onError Error callback.
149 * @private
150 */
151FileWatcher.prototype.resetWatchedEntry_ = function(onSuccess, onError) {
152  // Run the tasks in the queue to avoid races.
153  this.queue_.run(function(callback) {
154    // Release the watched directory.
155    if (this.watchedDirectoryEntry_) {
156      chrome.fileManagerPrivate.removeFileWatch(
157          this.watchedDirectoryEntry_.toURL(),
158          function(result) {
159            this.watchedDirectoryEntry_ = null;
160            if (result)
161              onSuccess();
162            else
163              onError();
164            callback();
165          }.bind(this));
166      this.metadataCache_.removeObserver(this.filesystemMetadataObserverId_);
167      this.metadataCache_.removeObserver(this.thumbnailMetadataObserverId_);
168      this.metadataCache_.removeObserver(this.externalMetadataObserverId_);
169    } else {
170      onSuccess();
171      callback();
172    }
173  }.bind(this));
174};
175
176/**
177 * Sets the watched entry to the passed directory.
178 *
179 * @param {!DirectoryEntry} entry Directory to be watched.
180 * @param {function()} onSuccess Success callback.
181 * @param {function()} onError Error callback.
182 * @private
183 */
184FileWatcher.prototype.changeWatchedEntry_ = function(
185    entry, onSuccess, onError) {
186  var setEntryClosure = function() {
187    // Run the tasks in the queue to avoid races.
188    this.queue_.run(function(callback) {
189      chrome.fileManagerPrivate.addFileWatch(
190          entry.toURL(),
191          function(result) {
192            if (!result) {
193              this.watchedDirectoryEntry_ = null;
194              onError();
195            } else {
196              this.watchedDirectoryEntry_ = entry;
197              onSuccess();
198            }
199            callback();
200          }.bind(this));
201      this.filesystemMetadataObserverId_ = this.metadataCache_.addObserver(
202          entry,
203          MetadataCache.CHILDREN,
204          'filesystem',
205          this.onFilesystemMetadataChanged_.bind(this));
206      this.thumbnailMetadataObserverId_ = this.metadataCache_.addObserver(
207          entry,
208          MetadataCache.CHILDREN,
209          'thumbnail',
210          this.onThumbnailMetadataChanged_.bind(this));
211      this.externalMetadataObserverId_ = this.metadataCache_.addObserver(
212          entry,
213          MetadataCache.CHILDREN,
214          'external',
215          this.onExternalMetadataChanged_.bind(this));
216    }.bind(this));
217  }.bind(this);
218
219  // Reset the watched directory first, then set the new watched directory.
220  this.resetWatchedEntry_(setEntryClosure, onError);
221};
222
223/**
224 * @return {DirectoryEntry} Current watched directory entry.
225 */
226FileWatcher.prototype.getWatchedDirectoryEntry = function() {
227  return this.watchedDirectoryEntry_;
228};
229