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