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