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