• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 * MetadataCache is a map from Entry to an object containing properties.
9 * Properties are divided by types, and all properties of one type are accessed
10 * at once.
11 * Some of the properties:
12 * {
13 *   filesystem: size, modificationTime
14 *   internal: presence
15 *   external: pinned, present, hosted, availableOffline
16 *
17 *   Following are not fetched for non-present external files.
18 *   media: artist, album, title, width, height, imageTransform, etc.
19 *   thumbnail: url, transform
20 *
21 *   Following are always fetched from content, and so force the downloading
22 *   of external files. One should use this for required content metadata,
23 *   i.e. image orientation.
24 *   fetchedMedia: width, height, etc.
25 * }
26 *
27 * Typical usages:
28 * {
29 *   cache.get([entry1, entry2], 'external|filesystem', function(metadata) {
30 *     if (metadata[0].external.pinned && metadata[1].filesystem.size === 0)
31 *       alert("Pinned and empty!");
32 *   });
33 *
34 *   cache.set(entry, 'internal', {presence: 'deleted'});
35 *
36 *   cache.clear([fileEntry1, fileEntry2], 'filesystem');
37 *
38 *   // Getting fresh value.
39 *   cache.clear(entry, 'thumbnail');
40 *   cache.getOne(entry, 'thumbnail', function(thumbnail) {
41 *     img.src = thumbnail.url;
42 *   });
43 *
44 *   var cached = cache.getCached(entry, 'filesystem');
45 *   var size = (cached && cached.size) || UNKNOWN_SIZE;
46 * }
47 *
48 * @param {Array.<MetadataProvider>} providers Metadata providers.
49 * @constructor
50 */
51function MetadataCache(providers) {
52  /**
53   * Map from Entry (using Entry.toURL) to metadata. Metadata contains
54   * |properties| - an hierarchical object of values, and an object for each
55   * metadata provider: <prodiver-id>: {time, callbacks}
56   * @private
57   */
58  this.cache_ = {};
59
60  /**
61   * List of metadata providers.
62   * @private
63   */
64  this.providers_ = providers;
65
66  /**
67   * List of observers added. Each one is an object with fields:
68   *   re - regexp of urls;
69   *   type - metadata type;
70   *   callback - the callback.
71   * @private
72   */
73  this.observers_ = [];
74  this.observerId_ = 0;
75
76  this.batchCount_ = 0;
77  this.totalCount_ = 0;
78
79  this.currentCacheSize_ = 0;
80
81  /**
82   * Time of first get query of the current batch. Items updated later than this
83   * will not be evicted.
84   *
85   * @type {Date}
86   * @private
87   */
88  this.lastBatchStart_ = new Date();
89
90  Object.seal(this);
91}
92
93/**
94 * Observer type: it will be notified if the changed Entry is exactly the same
95 * as the observed Entry.
96 */
97MetadataCache.EXACT = 0;
98
99/**
100 * Observer type: it will be notified if the changed Entry is an immediate child
101 * of the observed Entry.
102 */
103MetadataCache.CHILDREN = 1;
104
105/**
106 * Observer type: it will be notified if the changed Entry is a descendant of
107 * of the observer Entry.
108 */
109MetadataCache.DESCENDANTS = 2;
110
111/**
112 * Margin of the cache size. This amount of caches may be kept in addition.
113 */
114MetadataCache.EVICTION_THRESHOLD_MARGIN = 500;
115
116/**
117 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
118 * @return {MetadataCache!} The cache with all providers.
119 */
120MetadataCache.createFull = function(volumeManager) {
121  // ExternalProvider should be prior to FileSystemProvider, because it covers
122  // FileSystemProvider for files on the external backend, eg. Drive.
123  return new MetadataCache([
124    new ExternalProvider(volumeManager),
125    new FilesystemProvider(),
126    new ContentProvider()
127  ]);
128};
129
130/**
131 * Clones metadata entry. Metadata entries may contain scalars, arrays,
132 * hash arrays and Date object. Other objects are not supported.
133 * @param {Object} metadata Metadata object.
134 * @return {Object} Cloned entry.
135 */
136MetadataCache.cloneMetadata = function(metadata) {
137  if (metadata instanceof Array) {
138    var result = [];
139    for (var index = 0; index < metadata.length; index++) {
140      result[index] = MetadataCache.cloneMetadata(metadata[index]);
141    }
142    return result;
143  } else if (metadata instanceof Date) {
144    var result = new Date();
145    result.setTime(metadata.getTime());
146    return result;
147  } else if (metadata instanceof Object) {  // Hash array only.
148    var result = {};
149    for (var property in metadata) {
150      if (metadata.hasOwnProperty(property))
151        result[property] = MetadataCache.cloneMetadata(metadata[property]);
152    }
153    return result;
154  } else {
155    return metadata;
156  }
157};
158
159/**
160 * @return {boolean} Whether all providers are ready.
161 */
162MetadataCache.prototype.isInitialized = function() {
163  for (var index = 0; index < this.providers_.length; index++) {
164    if (!this.providers_[index].isInitialized()) return false;
165  }
166  return true;
167};
168
169/**
170 * Changes the size of cache by delta value. The actual cache size may be larger
171 * than the given value.
172 *
173 * @param {number} delta The delta size to be changed the cache size by.
174 */
175MetadataCache.prototype.resizeBy = function(delta) {
176  this.currentCacheSize_ += delta;
177  if (this.currentCacheSize_ < 0)
178    this.currentCacheSize_ = 0;
179
180  if (this.totalCount_ > this.currentEvictionThreshold_())
181    this.evict_();
182};
183
184/**
185 * Returns the current threshold to evict caches. When the number of caches
186 * exceeds this, the cache should be evicted.
187 * @return {number} Threshold to evict caches.
188 * @private
189 */
190MetadataCache.prototype.currentEvictionThreshold_ = function() {
191  return this.currentCacheSize_ * 2 + MetadataCache.EVICTION_THRESHOLD_MARGIN;
192};
193
194/**
195 * Fetches the metadata, puts it in the cache, and passes to callback.
196 * If required metadata is already in the cache, does not fetch it again.
197 *
198 * @param {Array.<Entry>} entries The list of entries.
199 * @param {string} type The metadata type.
200 * @param {function(Object)} callback The metadata is passed to callback.
201 *     The callback is called asynchronously.
202 */
203MetadataCache.prototype.get = function(entries, type, callback) {
204  this.getInternal_(entries, type, false, callback);
205};
206
207/**
208 * Fetches the metadata, puts it in the cache, and passes to callback.
209 * Even if required metadata is already in the cache, fetches it again.
210 *
211 * @param {Array.<Entry>} entries The list of entries.
212 * @param {string} type The metadata type.
213 * @param {function(Object)} callback The metadata is passed to callback.
214 *     The callback is called asynchronously.
215 */
216MetadataCache.prototype.getLatest = function(entries, type, callback) {
217  this.getInternal_(entries, type, true, callback);
218};
219
220/**
221 * Fetches the metadata, puts it in the cache. This is only for internal use.
222 *
223 * @param {Array.<Entry>} entries The list of entries.
224 * @param {string} type The metadata type.
225 * @param {boolean} refresh True to get the latest value and refresh the cache,
226 *     false to get the value from the cache.
227 * @param {function(Object)} callback The metadata is passed to callback.
228 *     The callback is called asynchronously.
229 * @private
230 */
231MetadataCache.prototype.getInternal_ =
232    function(entries, type, refresh, callback) {
233  if (entries.length === 0) {
234    if (callback) setTimeout(callback.bind(null, []), 0);
235    return;
236  }
237
238  var result = [];
239  var remaining = entries.length;
240  this.startBatchUpdates();
241
242  var onOneItem = function(index, value) {
243    result[index] = value;
244    remaining--;
245    if (remaining === 0) {
246      this.endBatchUpdates();
247      if (callback) callback(result);
248    }
249  };
250
251  for (var index = 0; index < entries.length; index++) {
252    result.push(null);
253    this.getOneInternal_(entries[index],
254                         type,
255                         refresh,
256                         onOneItem.bind(this, index));
257  }
258};
259
260/**
261 * Fetches the metadata for one Entry. See comments to |get|.
262 * If required metadata is already in the cache, does not fetch it again.
263 *
264 * @param {Entry} entry The entry.
265 * @param {string} type Metadata type.
266 * @param {function(Object)} callback The metadata is passed to callback.
267 *     The callback is called asynchronously.
268 */
269MetadataCache.prototype.getOne = function(entry, type, callback) {
270  this.getOneInternal_(entry, type, false, callback);
271};
272
273/**
274 * Fetches the metadata for one Entry. This is only for internal use.
275 *
276 * @param {Entry} entry The entry.
277 * @param {string} type Metadata type.
278 * @param {boolean} refresh True to get the latest value and refresh the cache,
279 *     false to get the value from the cache.
280 * @param {function(Object)} callback The metadata is passed to callback.
281 *     The callback is called asynchronously.
282 * @private
283 */
284MetadataCache.prototype.getOneInternal_ =
285    function(entry, type, refresh, callback) {
286  if (type.indexOf('|') !== -1) {
287    var types = type.split('|');
288    var result = {};
289    var typesLeft = types.length;
290
291    var onOneType = function(requestedType, metadata) {
292      result[requestedType] = metadata;
293      typesLeft--;
294      if (typesLeft === 0) callback(result);
295    };
296
297    for (var index = 0; index < types.length; index++) {
298      this.getOneInternal_(entry, types[index], refresh,
299          onOneType.bind(null, types[index]));
300    }
301    return;
302  }
303
304  callback = callback || function() {};
305
306  var entryURL = entry.toURL();
307  if (!(entryURL in this.cache_)) {
308    this.cache_[entryURL] = this.createEmptyItem_();
309    this.totalCount_++;
310  }
311
312  var item = this.cache_[entryURL];
313
314  if (!refresh && type in item.properties) {
315    // Uses cache, if available and not on the 'refresh' mode.
316    setTimeout(callback.bind(null, item.properties[type]), 0);
317    return;
318  }
319
320  this.startBatchUpdates();
321  var providers = this.providers_.slice();
322  var currentProvider;
323  var self = this;
324
325  var queryProvider = function() {
326    var id = currentProvider.getId();
327
328    // If on 'refresh'-mode, replaces the callback array. The previous
329    // array may be remaining in the closure captured by previous tasks.
330    if (refresh)
331      item[id].callbacks = [];
332    var fetchedCallbacks = item[id].callbacks;
333
334    var onFetched = function() {
335      if (type in item.properties) {
336        self.endBatchUpdates();
337        // Got properties from provider.
338        callback(item.properties[type]);
339      } else {
340        tryNextProvider();
341      }
342    };
343
344    var onProviderProperties = function(properties) {
345      var callbacks = fetchedCallbacks.splice(0);
346      item.time = new Date();
347      self.mergeProperties_(entry, properties);
348
349      for (var index = 0; index < callbacks.length; index++) {
350        callbacks[index]();
351      }
352    };
353
354    fetchedCallbacks.push(onFetched);
355
356    // Querying now.
357    if (fetchedCallbacks.length === 1)
358      currentProvider.fetch(entry, type, onProviderProperties);
359  };
360
361  var tryNextProvider = function() {
362    if (providers.length === 0) {
363      self.endBatchUpdates();
364      setTimeout(callback.bind(null, item.properties[type] || null), 0);
365      return;
366    }
367
368    currentProvider = providers.shift();
369    if (currentProvider.supportsEntry(entry) &&
370        currentProvider.providesType(type)) {
371      queryProvider();
372    } else {
373      tryNextProvider();
374    }
375  };
376
377  tryNextProvider();
378};
379
380/**
381 * Returns the cached metadata value, or |null| if not present.
382 * @param {Entry} entry Entry.
383 * @param {string} type The metadata type.
384 * @return {Object} The metadata or null.
385 */
386MetadataCache.prototype.getCached = function(entry, type) {
387  // Entry.cachedUrl may be set in DirectoryContents.onNewEntries_().
388  // See the comment there for detail.
389  var entryURL = entry.cachedUrl || entry.toURL();
390  var cache = this.cache_[entryURL];
391  return cache ? (cache.properties[type] || null) : null;
392};
393
394/**
395 * Puts the metadata into cache
396 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
397 *     single entry.
398 * @param {string} type The metadata type.
399 * @param {Array.<Object>} values List of corresponding metadata values.
400 */
401MetadataCache.prototype.set = function(entries, type, values) {
402  if (!(entries instanceof Array)) {
403    entries = [entries];
404    values = [values];
405  }
406
407  this.startBatchUpdates();
408  for (var index = 0; index < entries.length; index++) {
409    var entryURL = entries[index].toURL();
410    if (!(entryURL in this.cache_)) {
411      this.cache_[entryURL] = this.createEmptyItem_();
412      this.totalCount_++;
413    }
414    this.cache_[entryURL].properties[type] = values[index];
415    this.notifyObservers_(entries[index], type);
416  }
417  this.endBatchUpdates();
418};
419
420/**
421 * Clears the cached metadata values.
422 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
423 *     single entry.
424 * @param {string} type The metadata types or * for any type.
425 */
426MetadataCache.prototype.clear = function(entries, type) {
427  if (!(entries instanceof Array))
428    entries = [entries];
429
430  this.clearByUrl(
431      entries.map(function(entry) { return entry.toURL(); }),
432      type);
433};
434
435/**
436 * Clears the cached metadata values. This method takes an URL since some items
437 * may be already removed and can't be fetches their entry.
438 *
439 * @param {Array.<string>} urls The list of URLs.
440 * @param {string} type The metadata types or * for any type.
441 */
442MetadataCache.prototype.clearByUrl = function(urls, type) {
443  var types = type.split('|');
444
445  for (var index = 0; index < urls.length; index++) {
446    var entryURL = urls[index];
447    if (entryURL in this.cache_) {
448      if (type === '*') {
449        this.cache_[entryURL].properties = {};
450      } else {
451        for (var j = 0; j < types.length; j++) {
452          var type = types[j];
453          delete this.cache_[entryURL].properties[type];
454        }
455      }
456    }
457  }
458};
459
460/**
461 * Clears the cached metadata values recursively.
462 * @param {Entry} entry An entry to be cleared recursively from cache.
463 * @param {string} type The metadata types or * for any type.
464 */
465MetadataCache.prototype.clearRecursively = function(entry, type) {
466  var types = type.split('|');
467  var keys = Object.keys(this.cache_);
468  var entryURL = entry.toURL();
469
470  for (var index = 0; index < keys.length; index++) {
471    var cachedEntryURL = keys[index];
472    if (cachedEntryURL.substring(0, entryURL.length) === entryURL) {
473      if (type === '*') {
474        this.cache_[cachedEntryURL].properties = {};
475      } else {
476        for (var j = 0; j < types.length; j++) {
477          var type = types[j];
478          delete this.cache_[cachedEntryURL].properties[type];
479        }
480      }
481    }
482  }
483};
484
485/**
486 * Adds an observer, which will be notified when metadata changes.
487 * @param {Entry} entry The root entry to look at.
488 * @param {number} relation This defines, which items will trigger the observer.
489 *     See comments to |MetadataCache.EXACT| and others.
490 * @param {string} type The metadata type.
491 * @param {function(Array.<Entry>, Object.<string, Object>)} observer Map of
492 *     entries and corresponding metadata values are passed to this callback.
493 * @return {number} The observer id, which can be used to remove it.
494 */
495MetadataCache.prototype.addObserver = function(
496    entry, relation, type, observer) {
497  var entryURL = entry.toURL();
498  var re;
499  if (relation === MetadataCache.CHILDREN)
500    re = entryURL + '(/[^/]*)?';
501  else if (relation === MetadataCache.DESCENDANTS)
502    re = entryURL + '(/.*)?';
503  else
504    re = entryURL;
505
506  var id = ++this.observerId_;
507  this.observers_.push({
508    re: new RegExp('^' + re + '$'),
509    type: type,
510    callback: observer,
511    id: id,
512    pending: {}
513  });
514
515  return id;
516};
517
518/**
519 * Removes the observer.
520 * @param {number} id Observer id.
521 * @return {boolean} Whether observer was removed or not.
522 */
523MetadataCache.prototype.removeObserver = function(id) {
524  for (var index = 0; index < this.observers_.length; index++) {
525    if (this.observers_[index].id === id) {
526      this.observers_.splice(index, 1);
527      return true;
528    }
529  }
530  return false;
531};
532
533/**
534 * Start batch updates.
535 */
536MetadataCache.prototype.startBatchUpdates = function() {
537  this.batchCount_++;
538  if (this.batchCount_ === 1)
539    this.lastBatchStart_ = new Date();
540};
541
542/**
543 * End batch updates. Notifies observers if all nested updates are finished.
544 */
545MetadataCache.prototype.endBatchUpdates = function() {
546  this.batchCount_--;
547  if (this.batchCount_ !== 0) return;
548  if (this.totalCount_ > this.currentEvictionThreshold_())
549    this.evict_();
550  for (var index = 0; index < this.observers_.length; index++) {
551    var observer = this.observers_[index];
552    var entries = [];
553    var properties = {};
554    for (var entryURL in observer.pending) {
555      if (observer.pending.hasOwnProperty(entryURL) &&
556          entryURL in this.cache_) {
557        var entry = observer.pending[entryURL];
558        entries.push(entry);
559        properties[entryURL] =
560            this.cache_[entryURL].properties[observer.type] || null;
561      }
562    }
563    observer.pending = {};
564    if (entries.length > 0) {
565      observer.callback(entries, properties);
566    }
567  }
568};
569
570/**
571 * Notifies observers or puts the data to pending list.
572 * @param {Entry} entry Changed entry.
573 * @param {string} type Metadata type.
574 * @private
575 */
576MetadataCache.prototype.notifyObservers_ = function(entry, type) {
577  var entryURL = entry.toURL();
578  for (var index = 0; index < this.observers_.length; index++) {
579    var observer = this.observers_[index];
580    if (observer.type === type && observer.re.test(entryURL)) {
581      if (this.batchCount_ === 0) {
582        // Observer expects array of urls and map of properties.
583        var property = {};
584        property[entryURL] = this.cache_[entryURL].properties[type] || null;
585        observer.callback(
586            [entry], property);
587      } else {
588        observer.pending[entryURL] = entry;
589      }
590    }
591  }
592};
593
594/**
595 * Removes the oldest items from the cache.
596 * This method never removes the items from last batch.
597 * @private
598 */
599MetadataCache.prototype.evict_ = function() {
600  var toRemove = [];
601
602  // We leave only a half of items, so we will not call evict_ soon again.
603  var desiredCount = this.currentEvictionThreshold_();
604  var removeCount = this.totalCount_ - desiredCount;
605  for (var url in this.cache_) {
606    if (this.cache_.hasOwnProperty(url) &&
607        this.cache_[url].time < this.lastBatchStart_) {
608      toRemove.push(url);
609    }
610  }
611
612  toRemove.sort(function(a, b) {
613    var aTime = this.cache_[a].time;
614    var bTime = this.cache_[b].time;
615    return aTime < bTime ? -1 : aTime > bTime ? 1 : 0;
616  }.bind(this));
617
618  removeCount = Math.min(removeCount, toRemove.length);
619  this.totalCount_ -= removeCount;
620  for (var index = 0; index < removeCount; index++) {
621    delete this.cache_[toRemove[index]];
622  }
623};
624
625/**
626 * @return {Object} Empty cache item.
627 * @private
628 */
629MetadataCache.prototype.createEmptyItem_ = function() {
630  var item = {properties: {}};
631  for (var index = 0; index < this.providers_.length; index++) {
632    item[this.providers_[index].getId()] = {callbacks: []};
633  }
634  return item;
635};
636
637/**
638 * Caches all the properties from data to cache entry for the entry.
639 * @param {Entry} entry The file entry.
640 * @param {Object} data The properties.
641 * @private
642 */
643MetadataCache.prototype.mergeProperties_ = function(entry, data) {
644  if (data === null) return;
645  var entryURL = entry.toURL();
646  if (!(entryURL in this.cache_)) {
647    this.cache_[entryURL] = this.createEmptyItem_();
648    this.totalCount_++;
649  }
650  var properties = this.cache_[entryURL].properties;
651  for (var type in data) {
652    if (data.hasOwnProperty(type)) {
653      properties[type] = data[type];
654      this.notifyObservers_(entry, type);
655    }
656  }
657};
658
659/**
660 * Base class for metadata providers.
661 * @constructor
662 */
663function MetadataProvider() {
664}
665
666/**
667 * @param {Entry} entry The entry.
668 * @return {boolean} Whether this provider supports the entry.
669 */
670MetadataProvider.prototype.supportsEntry = function(entry) { return false; };
671
672/**
673 * @param {string} type The metadata type.
674 * @return {boolean} Whether this provider provides this metadata.
675 */
676MetadataProvider.prototype.providesType = function(type) { return false; };
677
678/**
679 * @return {string} Unique provider id.
680 */
681MetadataProvider.prototype.getId = function() { return ''; };
682
683/**
684 * @return {boolean} Whether provider is ready.
685 */
686MetadataProvider.prototype.isInitialized = function() { return true; };
687
688/**
689 * Fetches the metadata. It's suggested to return all the metadata this provider
690 * can fetch at once.
691 * @param {Entry} entry File entry.
692 * @param {string} type Requested metadata type.
693 * @param {function(Object)} callback Callback expects a map from metadata type
694 *     to metadata value. This callback must be called asynchronously.
695 */
696MetadataProvider.prototype.fetch = function(entry, type, callback) {
697  throw new Error('Default metadata provider cannot fetch.');
698};
699
700
701/**
702 * Provider of filesystem metadata.
703 * This provider returns the following objects:
704 * filesystem: { size, modificationTime }
705 * @constructor
706 */
707function FilesystemProvider() {
708  MetadataProvider.call(this);
709}
710
711FilesystemProvider.prototype = {
712  __proto__: MetadataProvider.prototype
713};
714
715/**
716 * @param {Entry} entry The entry.
717 * @return {boolean} Whether this provider supports the entry.
718 */
719FilesystemProvider.prototype.supportsEntry = function(entry) {
720  return true;
721};
722
723/**
724 * @param {string} type The metadata type.
725 * @return {boolean} Whether this provider provides this metadata.
726 */
727FilesystemProvider.prototype.providesType = function(type) {
728  return type === 'filesystem';
729};
730
731/**
732 * @return {string} Unique provider id.
733 */
734FilesystemProvider.prototype.getId = function() { return 'filesystem'; };
735
736/**
737 * Fetches the metadata.
738 * @param {Entry} entry File entry.
739 * @param {string} type Requested metadata type.
740 * @param {function(Object)} callback Callback expects a map from metadata type
741 *     to metadata value. This callback is called asynchronously.
742 */
743FilesystemProvider.prototype.fetch = function(
744    entry, type, callback) {
745  function onError(error) {
746    callback(null);
747  }
748
749  function onMetadata(entry, metadata) {
750    callback({
751      filesystem: {
752        size: (entry.isFile ? (metadata.size || 0) : -1),
753        modificationTime: metadata.modificationTime
754      }
755    });
756  }
757
758  entry.getMetadata(onMetadata.bind(null, entry), onError);
759};
760
761/**
762 * Provider of metadata for entries on the external file system backend.
763 * This provider returns the following objects:
764 *     external: { pinned, hosted, present, customIconUrl, etc. }
765 *     thumbnail: { url, transform }
766 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
767 * @constructor
768 */
769function ExternalProvider(volumeManager) {
770  MetadataProvider.call(this);
771
772  /**
773   * @type {VolumeManagerWrapper}
774   * @private
775   */
776  this.volumeManager_ = volumeManager;
777
778  // We batch metadata fetches into single API call.
779  this.entries_ = [];
780  this.callbacks_ = [];
781  this.scheduled_ = false;
782
783  this.callApiBound_ = this.callApi_.bind(this);
784}
785
786ExternalProvider.prototype = {
787  __proto__: MetadataProvider.prototype
788};
789
790/**
791 * @param {Entry} entry The entry.
792 * @return {boolean} Whether this provider supports the entry.
793 */
794ExternalProvider.prototype.supportsEntry = function(entry) {
795  var locationInfo = this.volumeManager_.getLocationInfo(entry);
796  // TODO(mtomasz): Add support for provided file systems.
797  return locationInfo && locationInfo.isDriveBased;
798};
799
800/**
801 * @param {string} type The metadata type.
802 * @return {boolean} Whether this provider provides this metadata.
803 */
804ExternalProvider.prototype.providesType = function(type) {
805  return type === 'external' || type === 'thumbnail' ||
806      type === 'media' || type === 'filesystem';
807};
808
809/**
810 * @return {string} Unique provider id.
811 */
812ExternalProvider.prototype.getId = function() { return 'external'; };
813
814/**
815 * Fetches the metadata.
816 * @param {Entry} entry File entry.
817 * @param {string} type Requested metadata type.
818 * @param {function(Object)} callback Callback expects a map from metadata type
819 *     to metadata value. This callback is called asynchronously.
820 */
821ExternalProvider.prototype.fetch = function(entry, type, callback) {
822  this.entries_.push(entry);
823  this.callbacks_.push(callback);
824  if (!this.scheduled_) {
825    this.scheduled_ = true;
826    setTimeout(this.callApiBound_, 0);
827  }
828};
829
830/**
831 * Schedules the API call.
832 * @private
833 */
834ExternalProvider.prototype.callApi_ = function() {
835  this.scheduled_ = false;
836
837  var entries = this.entries_;
838  var callbacks = this.callbacks_;
839  this.entries_ = [];
840  this.callbacks_ = [];
841  var self = this;
842
843  // TODO(mtomasz): Move conversion from entry to url to custom bindings.
844  // crbug.com/345527.
845  var entryURLs = util.entriesToURLs(entries);
846  chrome.fileManagerPrivate.getEntryProperties(
847      entryURLs,
848      function(propertiesList) {
849        console.assert(propertiesList.length === callbacks.length);
850        for (var i = 0; i < callbacks.length; i++) {
851          callbacks[i](self.convert_(propertiesList[i], entries[i]));
852        }
853      });
854};
855
856/**
857 * Converts API metadata to internal format.
858 * @param {Object} data Metadata from API call.
859 * @param {Entry} entry File entry.
860 * @return {Object} Metadata in internal format.
861 * @private
862 */
863ExternalProvider.prototype.convert_ = function(data, entry) {
864  var result = {};
865  result.external = {
866    present: data.isPresent,
867    pinned: data.isPinned,
868    hosted: data.isHosted,
869    imageWidth: data.imageWidth,
870    imageHeight: data.imageHeight,
871    imageRotation: data.imageRotation,
872    availableOffline: data.isAvailableOffline,
873    availableWhenMetered: data.isAvailableWhenMetered,
874    customIconUrl: data.customIconUrl || '',
875    contentMimeType: data.contentMimeType || '',
876    sharedWithMe: data.sharedWithMe,
877    shared: data.shared,
878    thumbnailUrl: data.thumbnailUrl  // Thumbnail passed from external server.
879  };
880
881  result.filesystem = {
882    size: (entry.isFile ? (data.fileSize || 0) : -1),
883    modificationTime: new Date(data.lastModifiedTime)
884  };
885
886  if (data.isPresent) {
887    // If the file is present, don't fill the thumbnail here and allow to
888    // generate it by next providers.
889    result.thumbnail = null;
890  } else if ('thumbnailUrl' in data) {
891    result.thumbnail = {
892      url: data.thumbnailUrl,
893      transform: null
894    };
895  } else {
896    // Not present in cache, so do not allow to generate it by next providers.
897    result.thumbnail = {url: '', transform: null};
898  }
899
900  // If present in cache, then allow to fetch media by next providers.
901  result.media = data.isPresent ? null : {};
902
903  return result;
904};
905
906
907/**
908 * Provider of content metadata.
909 * This provider returns the following objects:
910 * thumbnail: { url, transform }
911 * media: { artist, album, title, width, height, imageTransform, etc. }
912 * fetchedMedia: { same fields here }
913 * @constructor
914 */
915function ContentProvider() {
916  MetadataProvider.call(this);
917
918  // Pass all URLs to the metadata reader until we have a correct filter.
919  this.urlFilter_ = /.*/;
920
921  var dispatcher = new SharedWorker(ContentProvider.WORKER_SCRIPT).port;
922  dispatcher.onmessage = this.onMessage_.bind(this);
923  dispatcher.postMessage({verb: 'init'});
924  dispatcher.start();
925  this.dispatcher_ = dispatcher;
926
927  // Initialization is not complete until the Worker sends back the
928  // 'initialized' message.  See below.
929  this.initialized_ = false;
930
931  // Map from Entry.toURL() to callback.
932  // Note that simultaneous requests for same url are handled in MetadataCache.
933  this.callbacks_ = {};
934}
935
936/**
937 * Path of a worker script.
938 * @type {string}
939 * @const
940 */
941ContentProvider.WORKER_SCRIPT =
942    'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/' +
943    'foreground/js/metadata/metadata_dispatcher.js';
944
945ContentProvider.prototype = {
946  __proto__: MetadataProvider.prototype
947};
948
949/**
950 * @param {Entry} entry The entry.
951 * @return {boolean} Whether this provider supports the entry.
952 */
953ContentProvider.prototype.supportsEntry = function(entry) {
954  return entry.toURL().match(this.urlFilter_);
955};
956
957/**
958 * @param {string} type The metadata type.
959 * @return {boolean} Whether this provider provides this metadata.
960 */
961ContentProvider.prototype.providesType = function(type) {
962  return type === 'thumbnail' || type === 'fetchedMedia' || type === 'media';
963};
964
965/**
966 * @return {string} Unique provider id.
967 */
968ContentProvider.prototype.getId = function() { return 'content'; };
969
970/**
971 * Fetches the metadata.
972 * @param {Entry} entry File entry.
973 * @param {string} type Requested metadata type.
974 * @param {function(Object)} callback Callback expects a map from metadata type
975 *     to metadata value. This callback is called asynchronously.
976 */
977ContentProvider.prototype.fetch = function(entry, type, callback) {
978  if (entry.isDirectory) {
979    setTimeout(callback.bind(null, {}), 0);
980    return;
981  }
982  var entryURL = entry.toURL();
983  this.callbacks_[entryURL] = callback;
984  this.dispatcher_.postMessage({verb: 'request', arguments: [entryURL]});
985};
986
987/**
988 * Dispatch a message from a metadata reader to the appropriate on* method.
989 * @param {Object} event The event.
990 * @private
991 */
992ContentProvider.prototype.onMessage_ = function(event) {
993  var data = event.data;
994
995  var methodName =
996      'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_';
997
998  if (!(methodName in this)) {
999    console.error('Unknown message from metadata reader: ' + data.verb, data);
1000    return;
1001  }
1002
1003  this[methodName].apply(this, data.arguments);
1004};
1005
1006/**
1007 * @return {boolean} Whether provider is ready.
1008 */
1009ContentProvider.prototype.isInitialized = function() {
1010  return this.initialized_;
1011};
1012
1013/**
1014 * Handles the 'initialized' message from the metadata reader Worker.
1015 * @param {Object} regexp Regexp of supported urls.
1016 * @private
1017 */
1018ContentProvider.prototype.onInitialized_ = function(regexp) {
1019  this.urlFilter_ = regexp;
1020
1021  // Tests can monitor for this state with
1022  // ExtensionTestMessageListener listener("worker-initialized");
1023  // ASSERT_TRUE(listener.WaitUntilSatisfied());
1024  // Automated tests need to wait for this, otherwise we crash in
1025  // browser_test cleanup because the worker process still has
1026  // URL requests in-flight.
1027  util.testSendMessage('worker-initialized');
1028  this.initialized_ = true;
1029};
1030
1031/**
1032 * Converts content metadata from parsers to the internal format.
1033 * @param {Object} metadata The content metadata.
1034 * @param {Object=} opt_result The internal metadata object ot put result in.
1035 * @return {Object!} Converted metadata.
1036 */
1037ContentProvider.ConvertContentMetadata = function(metadata, opt_result) {
1038  var result = opt_result || {};
1039
1040  if ('thumbnailURL' in metadata) {
1041    metadata.thumbnailTransform = metadata.thumbnailTransform || null;
1042    result.thumbnail = {
1043      url: metadata.thumbnailURL,
1044      transform: metadata.thumbnailTransform
1045    };
1046  }
1047
1048  for (var key in metadata) {
1049    if (metadata.hasOwnProperty(key)) {
1050      if (!result.media)
1051        result.media = {};
1052      result.media[key] = metadata[key];
1053    }
1054  }
1055
1056  if (result.media)
1057    result.fetchedMedia = result.media;
1058
1059  return result;
1060};
1061
1062/**
1063 * Handles the 'result' message from the worker.
1064 * @param {string} url File url.
1065 * @param {Object} metadata The metadata.
1066 * @private
1067 */
1068ContentProvider.prototype.onResult_ = function(url, metadata) {
1069  var callback = this.callbacks_[url];
1070  delete this.callbacks_[url];
1071  callback(ContentProvider.ConvertContentMetadata(metadata));
1072};
1073
1074/**
1075 * Handles the 'error' message from the worker.
1076 * @param {string} url File entry.
1077 * @param {string} step Step failed.
1078 * @param {string} error Error description.
1079 * @param {Object?} metadata The metadata, if available.
1080 * @private
1081 */
1082ContentProvider.prototype.onError_ = function(url, step, error, metadata) {
1083  if (MetadataCache.log)  // Avoid log spam by default.
1084    console.warn('metadata: ' + url + ': ' + step + ': ' + error);
1085  metadata = metadata || {};
1086  // Prevent asking for thumbnail again.
1087  metadata.thumbnailURL = '';
1088  this.onResult_(url, metadata);
1089};
1090
1091/**
1092 * Handles the 'log' message from the worker.
1093 * @param {Array.<*>} arglist Log arguments.
1094 * @private
1095 */
1096ContentProvider.prototype.onLog_ = function(arglist) {
1097  if (MetadataCache.log)  // Avoid log spam by default.
1098    console.log.apply(console, ['metadata:'].concat(arglist));
1099};
1100