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