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