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