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