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