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.
6 * MetadataCache is a map from Entry to an object containing properties.
7 * Properties are divided by types, and all properties of one type are accessed
9 * Some of the properties:
11 * filesystem: size, modificationTime
13 * external: pinned, present, hosted, availableOffline, externalFileUrl
15 * Following are not fetched for non-present external files.
16 * media: artist, album, title, width, height, imageTransform, etc.
17 * thumbnail: url, transform
19 * Following are always fetched from content, and so force the downloading
20 * of external files. One should use this for required content metadata,
21 * i.e. image orientation.
22 * fetchedMedia: width, height, etc.
27 * cache.get([entry1, entry2], 'external|filesystem', function(metadata) {
28 * if (metadata[0].external.pinned && metadata[1].filesystem.size === 0)
29 * alert("Pinned and empty!");
32 * cache.set(entry, 'internal', {presence: 'deleted'});
34 * cache.clear([fileEntry1, fileEntry2], 'filesystem');
36 * // Getting fresh value.
37 * cache.clear(entry, 'thumbnail');
38 * cache.getOne(entry, 'thumbnail', function(thumbnail) {
39 * img.src = thumbnail.url;
42 * var cached = cache.getCached(entry, 'filesystem');
43 * var size = (cached && cached.size) || UNKNOWN_SIZE;
46 * @param {Array.<MetadataProvider>} providers Metadata providers.
49 function MetadataCache(providers) {
51 * Map from Entry (using Entry.toURL) to metadata. Metadata contains
52 * |properties| - an hierarchical object of values, and an object for each
53 * metadata provider: <prodiver-id>: {time, callbacks}
59 * List of metadata providers.
62 this.providers_ = providers;
65 * List of observers added. Each one is an object with fields:
66 * re - regexp of urls;
67 * type - metadata type;
68 * callback - the callback.
77 this.currentCacheSize_ = 0;
80 * Time of first get query of the current batch. Items updated later than this
81 * will not be evicted.
86 this.lastBatchStart_ = new Date();
92 * Observer type: it will be notified if the changed Entry is exactly the same
93 * as the observed Entry.
95 MetadataCache.EXACT = 0;
98 * Observer type: it will be notified if the changed Entry is an immediate child
99 * of the observed Entry.
101 MetadataCache.CHILDREN = 1;
104 * Observer type: it will be notified if the changed Entry is a descendant of
105 * of the observer Entry.
107 MetadataCache.DESCENDANTS = 2;
110 * Margin of the cache size. This amount of caches may be kept in addition.
112 MetadataCache.EVICTION_THRESHOLD_MARGIN = 500;
115 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
116 * @return {MetadataCache!} The cache with all providers.
118 MetadataCache.createFull = function(volumeManager) {
119 // ExternalProvider should be prior to FileSystemProvider, because it covers
120 // FileSystemProvider for files on the external backend, eg. Drive.
121 return new MetadataCache([
122 new ExternalProvider(volumeManager),
123 new FilesystemProvider(),
124 new ContentProvider()
129 * Clones metadata entry. Metadata entries may contain scalars, arrays,
130 * hash arrays and Date object. Other objects are not supported.
131 * @param {Object} metadata Metadata object.
132 * @return {Object} Cloned entry.
134 MetadataCache.cloneMetadata = function(metadata) {
135 if (metadata instanceof Array) {
137 for (var index = 0; index < metadata.length; index++) {
138 result[index] = MetadataCache.cloneMetadata(metadata[index]);
141 } else if (metadata instanceof Date) {
142 var result = new Date();
143 result.setTime(metadata.getTime());
145 } else if (metadata instanceof Object) { // Hash array only.
147 for (var property in metadata) {
148 if (metadata.hasOwnProperty(property))
149 result[property] = MetadataCache.cloneMetadata(metadata[property]);
158 * @return {boolean} Whether all providers are ready.
160 MetadataCache.prototype.isInitialized = function() {
161 for (var index = 0; index < this.providers_.length; index++) {
162 if (!this.providers_[index].isInitialized()) return false;
168 * Changes the size of cache by delta value. The actual cache size may be larger
169 * than the given value.
171 * @param {number} delta The delta size to be changed the cache size by.
173 MetadataCache.prototype.resizeBy = function(delta) {
174 this.currentCacheSize_ += delta;
175 if (this.currentCacheSize_ < 0)
176 this.currentCacheSize_ = 0;
178 if (this.totalCount_ > this.currentEvictionThreshold_())
183 * Returns the current threshold to evict caches. When the number of caches
184 * exceeds this, the cache should be evicted.
185 * @return {number} Threshold to evict caches.
188 MetadataCache.prototype.currentEvictionThreshold_ = function() {
189 return this.currentCacheSize_ * 2 + MetadataCache.EVICTION_THRESHOLD_MARGIN;
193 * Fetches the metadata, puts it in the cache, and passes to callback.
194 * If required metadata is already in the cache, does not fetch it again.
196 * @param {Array.<Entry>} entries The list of entries.
197 * @param {string} type The metadata type.
198 * @param {?function(Object)} callback The metadata is passed to callback.
199 * The callback is called asynchronously.
201 MetadataCache.prototype.get = function(entries, type, callback) {
202 this.getInternal_(entries, type, false, callback);
206 * Fetches the metadata, puts it in the cache, and passes to callback.
207 * Even if required metadata is already in the cache, fetches it again.
209 * @param {Array.<Entry>} entries The list of entries.
210 * @param {string} type The metadata type.
211 * @param {?function(Object)} callback The metadata is passed to callback.
212 * The callback is called asynchronously.
214 MetadataCache.prototype.getLatest = function(entries, type, callback) {
215 this.getInternal_(entries, type, true, callback);
219 * Fetches the metadata, puts it in the cache. This is only for internal use.
221 * @param {Array.<Entry>} entries The list of entries.
222 * @param {string} type The metadata type.
223 * @param {boolean} refresh True to get the latest value and refresh the cache,
224 * false to get the value from the cache.
225 * @param {?function(Object)} callback The metadata is passed to callback.
226 * The callback is called asynchronously.
229 MetadataCache.prototype.getInternal_ =
230 function(entries, type, refresh, callback) {
231 if (entries.length === 0) {
232 if (callback) setTimeout(callback.bind(null, []), 0);
237 var remaining = entries.length;
238 this.startBatchUpdates();
240 var onOneItem = function(index, value) {
241 result[index] = value;
243 if (remaining === 0) {
244 this.endBatchUpdates();
245 if (callback) callback(result);
249 for (var index = 0; index < entries.length; index++) {
251 this.getOneInternal_(entries[index],
254 onOneItem.bind(this, index));
259 * Fetches the metadata for one Entry. See comments to |get|.
260 * If required metadata is already in the cache, does not fetch it again.
262 * @param {Entry} entry The entry.
263 * @param {string} type Metadata type.
264 * @param {function(Object)} callback The metadata is passed to callback.
265 * The callback is called asynchronously.
267 MetadataCache.prototype.getOne = function(entry, type, callback) {
268 this.getOneInternal_(entry, type, false, callback);
272 * Fetches the metadata for one Entry. This is only for internal use.
274 * @param {Entry} entry The entry.
275 * @param {string} type Metadata type.
276 * @param {boolean} refresh True to get the latest value and refresh the cache,
277 * false to get the value from the cache.
278 * @param {function(Object)} callback The metadata is passed to callback.
279 * The callback is called asynchronously.
282 MetadataCache.prototype.getOneInternal_ =
283 function(entry, type, refresh, callback) {
284 if (type.indexOf('|') !== -1) {
285 var types = type.split('|');
287 var typesLeft = types.length;
289 var onOneType = function(requestedType, metadata) {
290 result[requestedType] = metadata;
292 if (typesLeft === 0) callback(result);
295 for (var index = 0; index < types.length; index++) {
296 this.getOneInternal_(entry, types[index], refresh,
297 onOneType.bind(null, types[index]));
302 callback = callback || function() {};
304 var entryURL = entry.toURL();
305 if (!(entryURL in this.cache_)) {
306 this.cache_[entryURL] = this.createEmptyItem_();
310 var item = this.cache_[entryURL];
312 if (!refresh && type in item.properties) {
313 // Uses cache, if available and not on the 'refresh' mode.
314 setTimeout(callback.bind(null, item.properties[type]), 0);
318 this.startBatchUpdates();
319 var providers = this.providers_.slice();
323 var queryProvider = function() {
324 var id = currentProvider.getId();
326 // If on 'refresh'-mode, replaces the callback array. The previous
327 // array may be remaining in the closure captured by previous tasks.
329 item[id].callbacks = [];
330 var fetchedCallbacks = item[id].callbacks;
332 var onFetched = function() {
333 if (type in item.properties) {
334 self.endBatchUpdates();
335 // Got properties from provider.
336 callback(item.properties[type]);
342 var onProviderProperties = function(properties) {
343 var callbacks = fetchedCallbacks.splice(0);
344 item.time = new Date();
345 self.mergeProperties_(entry, properties);
347 for (var index = 0; index < callbacks.length; index++) {
352 fetchedCallbacks.push(onFetched);
355 if (fetchedCallbacks.length === 1)
356 currentProvider.fetch(entry, type, onProviderProperties);
359 var tryNextProvider = function() {
360 if (providers.length === 0) {
361 // If not found, then mark the property as unavailable, so it's not
363 if (!(type in item.properties))
364 item.properties[type] = null;
366 self.endBatchUpdates();
367 setTimeout(callback.bind(null, item.properties[type]), 0);
371 currentProvider = providers.shift();
372 if (currentProvider.supportsEntry(entry) &&
373 currentProvider.providesType(type)) {
384 * Returns the cached metadata value, or |null| if not present.
385 * @param {Entry} entry Entry.
386 * @param {string} type The metadata type.
387 * @return {Object} The metadata or null.
389 MetadataCache.prototype.getCached = function(entry, type) {
390 // Entry.cachedUrl may be set in DirectoryContents.onNewEntries_().
391 // See the comment there for detail.
392 var entryURL = entry.cachedUrl || entry.toURL();
393 var cache = this.cache_[entryURL];
394 return cache ? (cache.properties[type] || null) : null;
398 * Puts the metadata into cache
399 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
401 * @param {string} type The metadata type.
402 * @param {Array.<Object>} values List of corresponding metadata values.
404 MetadataCache.prototype.set = function(entries, type, values) {
405 if (!(entries instanceof Array)) {
410 this.startBatchUpdates();
411 for (var index = 0; index < entries.length; index++) {
412 var entryURL = entries[index].toURL();
413 if (!(entryURL in this.cache_)) {
414 this.cache_[entryURL] = this.createEmptyItem_();
417 this.cache_[entryURL].properties[type] = values[index];
418 this.notifyObservers_(entries[index], type);
420 this.endBatchUpdates();
424 * Clears the cached metadata values.
425 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
427 * @param {string} type The metadata types or * for any type.
429 MetadataCache.prototype.clear = function(entries, type) {
430 if (!(entries instanceof Array))
434 entries.map(function(entry) { return entry.toURL(); }),
439 * Clears the cached metadata values. This method takes an URL since some items
440 * may be already removed and can't be fetches their entry.
442 * @param {Array.<string>} urls The list of URLs.
443 * @param {string} type The metadata types or * for any type.
445 MetadataCache.prototype.clearByUrl = function(urls, type) {
446 var types = type.split('|');
448 for (var index = 0; index < urls.length; index++) {
449 var entryURL = urls[index];
450 if (entryURL in this.cache_) {
452 this.cache_[entryURL].properties = {};
454 for (var j = 0; j < types.length; j++) {
456 delete this.cache_[entryURL].properties[type];
464 * Clears the cached metadata values recursively.
465 * @param {Entry} entry An entry to be cleared recursively from cache.
466 * @param {string} type The metadata types or * for any type.
468 MetadataCache.prototype.clearRecursively = function(entry, type) {
469 var types = type.split('|');
470 var keys = Object.keys(this.cache_);
471 var entryURL = entry.toURL();
473 for (var index = 0; index < keys.length; index++) {
474 var cachedEntryURL = keys[index];
475 if (cachedEntryURL.substring(0, entryURL.length) === entryURL) {
477 this.cache_[cachedEntryURL].properties = {};
479 for (var j = 0; j < types.length; j++) {
481 delete this.cache_[cachedEntryURL].properties[type];
489 * Adds an observer, which will be notified when metadata changes.
490 * @param {Entry} entry The root entry to look at.
491 * @param {number} relation This defines, which items will trigger the observer.
492 * See comments to |MetadataCache.EXACT| and others.
493 * @param {string} type The metadata type.
494 * @param {function(Array.<Entry>, Object.<string, Object>)} observer Map of
495 * entries and corresponding metadata values are passed to this callback.
496 * @return {number} The observer id, which can be used to remove it.
498 MetadataCache.prototype.addObserver = function(
499 entry, relation, type, observer) {
500 var entryURL = entry.toURL();
502 // Escape following regexp special characters:
504 var escapedEntryURL = entryURL.replace(
505 /([\\\^\$\.\*\+\?\|\&\{\}\[\]\(\)\<\>])/g,
509 if (relation === MetadataCache.CHILDREN)
510 re = escapedEntryURL + '(/[^/]*)?';
511 else if (relation === MetadataCache.DESCENDANTS)
512 re = escapedEntryURL + '(/.*)?';
514 re = escapedEntryURL;
516 var id = ++this.observerId_;
517 this.observers_.push({
518 re: new RegExp('^' + re + '$'),
529 * Removes the observer.
530 * @param {number} id Observer id.
531 * @return {boolean} Whether observer was removed or not.
533 MetadataCache.prototype.removeObserver = function(id) {
534 for (var index = 0; index < this.observers_.length; index++) {
535 if (this.observers_[index].id === id) {
536 this.observers_.splice(index, 1);
544 * Start batch updates.
546 MetadataCache.prototype.startBatchUpdates = function() {
548 if (this.batchCount_ === 1)
549 this.lastBatchStart_ = new Date();
553 * End batch updates. Notifies observers if all nested updates are finished.
555 MetadataCache.prototype.endBatchUpdates = function() {
557 if (this.batchCount_ !== 0) return;
558 if (this.totalCount_ > this.currentEvictionThreshold_())
560 for (var index = 0; index < this.observers_.length; index++) {
561 var observer = this.observers_[index];
564 for (var entryURL in observer.pending) {
565 if (observer.pending.hasOwnProperty(entryURL) &&
566 entryURL in this.cache_) {
567 var entry = observer.pending[entryURL];
569 properties[entryURL] =
570 this.cache_[entryURL].properties[observer.type] || null;
573 observer.pending = {};
574 if (entries.length > 0) {
575 observer.callback(entries, properties);
581 * Notifies observers or puts the data to pending list.
582 * @param {Entry} entry Changed entry.
583 * @param {string} type Metadata type.
586 MetadataCache.prototype.notifyObservers_ = function(entry, type) {
587 var entryURL = entry.toURL();
588 for (var index = 0; index < this.observers_.length; index++) {
589 var observer = this.observers_[index];
590 if (observer.type === type && observer.re.test(entryURL)) {
591 if (this.batchCount_ === 0) {
592 // Observer expects array of urls and map of properties.
594 property[entryURL] = this.cache_[entryURL].properties[type] || null;
598 observer.pending[entryURL] = entry;
605 * Removes the oldest items from the cache.
606 * This method never removes the items from last batch.
609 MetadataCache.prototype.evict_ = function() {
612 // We leave only a half of items, so we will not call evict_ soon again.
613 var desiredCount = this.currentEvictionThreshold_();
614 var removeCount = this.totalCount_ - desiredCount;
615 for (var url in this.cache_) {
616 if (this.cache_.hasOwnProperty(url) &&
617 this.cache_[url].time < this.lastBatchStart_) {
622 toRemove.sort(function(a, b) {
623 var aTime = this.cache_[a].time;
624 var bTime = this.cache_[b].time;
625 return aTime < bTime ? -1 : aTime > bTime ? 1 : 0;
628 removeCount = Math.min(removeCount, toRemove.length);
629 this.totalCount_ -= removeCount;
630 for (var index = 0; index < removeCount; index++) {
631 delete this.cache_[toRemove[index]];
636 * @return {Object} Empty cache item.
639 MetadataCache.prototype.createEmptyItem_ = function() {
640 var item = {properties: {}};
641 for (var index = 0; index < this.providers_.length; index++) {
642 item[this.providers_[index].getId()] = {callbacks: []};
648 * Caches all the properties from data to cache entry for the entry.
649 * @param {Entry} entry The file entry.
650 * @param {Object} data The properties.
653 MetadataCache.prototype.mergeProperties_ = function(entry, data) {
654 if (data === null) return;
655 var entryURL = entry.toURL();
656 if (!(entryURL in this.cache_)) {
657 this.cache_[entryURL] = this.createEmptyItem_();
660 var properties = this.cache_[entryURL].properties;
661 for (var type in data) {
662 if (data.hasOwnProperty(type)) {
663 properties[type] = data[type];
664 this.notifyObservers_(entry, type);
670 * Base class for metadata providers.
673 function MetadataProvider() {
677 * @param {Entry} entry The entry.
678 * @return {boolean} Whether this provider supports the entry.
680 MetadataProvider.prototype.supportsEntry = function(entry) { return false; };
683 * @param {string} type The metadata type.
684 * @return {boolean} Whether this provider provides this metadata.
686 MetadataProvider.prototype.providesType = function(type) { return false; };
689 * @return {string} Unique provider id.
691 MetadataProvider.prototype.getId = function() { return ''; };
694 * @return {boolean} Whether provider is ready.
696 MetadataProvider.prototype.isInitialized = function() { return true; };
699 * Fetches the metadata. It's suggested to return all the metadata this provider
701 * @param {Entry} entry File entry.
702 * @param {string} type Requested metadata type.
703 * @param {function(Object)} callback Callback expects a map from metadata type
704 * to metadata value. This callback must be called asynchronously.
706 MetadataProvider.prototype.fetch = function(entry, type, callback) {
707 throw new Error('Default metadata provider cannot fetch.');
712 * Provider of filesystem metadata.
713 * This provider returns the following objects:
714 * filesystem: { size, modificationTime }
716 * @extends {MetadataProvider}
718 function FilesystemProvider() {
719 MetadataProvider.call(this);
722 FilesystemProvider.prototype = {
723 __proto__: MetadataProvider.prototype
727 * @param {Entry} entry The entry.
728 * @return {boolean} Whether this provider supports the entry.
730 FilesystemProvider.prototype.supportsEntry = function(entry) {
735 * @param {string} type The metadata type.
736 * @return {boolean} Whether this provider provides this metadata.
738 FilesystemProvider.prototype.providesType = function(type) {
739 return type === 'filesystem';
743 * @return {string} Unique provider id.
745 FilesystemProvider.prototype.getId = function() { return 'filesystem'; };
748 * Fetches the metadata.
749 * @param {Entry} entry File entry.
750 * @param {string} type Requested metadata type.
751 * @param {function(Object)} callback Callback expects a map from metadata type
752 * to metadata value. This callback is called asynchronously.
754 FilesystemProvider.prototype.fetch = function(
755 entry, type, callback) {
756 function onError(error) {
760 function onMetadata(entry, metadata) {
763 size: (entry.isFile ? (metadata.size || 0) : -1),
764 modificationTime: metadata.modificationTime
769 entry.getMetadata(onMetadata.bind(null, entry), onError);
773 * Provider of metadata for entries on the external file system backend.
774 * This provider returns the following objects:
775 * external: { pinned, hosted, present, customIconUrl, etc. }
776 * thumbnail: { url, transform }
777 * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
779 * @extends {MetadataProvider}
781 function ExternalProvider(volumeManager) {
782 MetadataProvider.call(this);
785 * @type {VolumeManagerWrapper}
788 this.volumeManager_ = volumeManager;
790 // We batch metadata fetches into single API call.
792 this.callbacks_ = [];
793 this.scheduled_ = false;
795 this.callApiBound_ = this.callApi_.bind(this);
798 ExternalProvider.prototype = {
799 __proto__: MetadataProvider.prototype
803 * @param {Entry} entry The entry.
804 * @return {boolean} Whether this provider supports the entry.
806 ExternalProvider.prototype.supportsEntry = function(entry) {
807 var locationInfo = this.volumeManager_.getLocationInfo(entry);
810 return locationInfo.isDriveBased ||
811 locationInfo.rootType === VolumeManagerCommon.RootType.PROVIDED;
815 * @param {string} type The metadata type.
816 * @return {boolean} Whether this provider provides this metadata.
818 ExternalProvider.prototype.providesType = function(type) {
819 return type === 'external' || type === 'thumbnail' ||
820 type === 'media' || type === 'filesystem';
824 * @return {string} Unique provider id.
826 ExternalProvider.prototype.getId = function() { return 'external'; };
829 * Fetches the metadata.
830 * @param {Entry} entry File entry.
831 * @param {string} type Requested metadata type.
832 * @param {function(Object)} callback Callback expects a map from metadata type
833 * to metadata value. This callback is called asynchronously.
835 ExternalProvider.prototype.fetch = function(entry, type, callback) {
836 this.entries_.push(entry);
837 this.callbacks_.push(callback);
838 if (!this.scheduled_) {
839 this.scheduled_ = true;
840 setTimeout(this.callApiBound_, 0);
845 * Schedules the API call.
848 ExternalProvider.prototype.callApi_ = function() {
849 this.scheduled_ = false;
851 var entries = this.entries_;
852 var callbacks = this.callbacks_;
854 this.callbacks_ = [];
857 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
859 var entryURLs = util.entriesToURLs(entries);
860 chrome.fileManagerPrivate.getEntryProperties(
862 function(propertiesList) {
863 console.assert(propertiesList.length === callbacks.length);
864 for (var i = 0; i < callbacks.length; i++) {
865 callbacks[i](self.convert_(propertiesList[i], entries[i]));
871 * Converts API metadata to internal format.
872 * @param {Object} data Metadata from API call.
873 * @param {Entry} entry File entry.
874 * @return {Object} Metadata in internal format.
877 ExternalProvider.prototype.convert_ = function(data, entry) {
880 present: data.isPresent,
881 pinned: data.isPinned,
882 hosted: data.isHosted,
883 imageWidth: data.imageWidth,
884 imageHeight: data.imageHeight,
885 imageRotation: data.imageRotation,
886 availableOffline: data.isAvailableOffline,
887 availableWhenMetered: data.isAvailableWhenMetered,
888 customIconUrl: data.customIconUrl || '',
889 contentMimeType: data.contentMimeType || '',
890 sharedWithMe: data.sharedWithMe,
892 thumbnailUrl: data.thumbnailUrl, // Thumbnail passed from external server.
893 externalFileUrl: data.externalFileUrl
896 result.filesystem = {
897 size: (entry.isFile ? (data.fileSize || 0) : -1),
898 modificationTime: new Date(data.lastModifiedTime)
901 // TODO(mtomasz): Remove all of the if logic in the new metadata cache.
902 // If the file is not present, then use the thumbnail url instead of
903 // extracting the thumbnail from contents.
904 if (data.isPresent === false) {
905 if ('thumbnailUrl' in data) {
907 url: data.thumbnailUrl,
911 // Not present in cache, so do not allow to generate it by next providers.
912 result.thumbnail = {url: '', transform: null};
916 // If not present in cache, then do not allow to fetch media by next
918 if (data.isPresent === false)
926 * Provider of content metadata.
927 * This provider returns the following objects:
928 * thumbnail: { url, transform }
929 * media: { artist, album, title, width, height, imageTransform, etc. }
930 * fetchedMedia: { same fields here }
932 * @extends {MetadataProvider}
934 function ContentProvider() {
935 MetadataProvider.call(this);
937 // Pass all URLs to the metadata reader until we have a correct filter.
938 this.urlFilter_ = /.*/;
940 var dispatcher = new SharedWorker(ContentProvider.WORKER_SCRIPT).port;
941 dispatcher.onmessage = this.onMessage_.bind(this);
942 dispatcher.postMessage({verb: 'init'});
944 this.dispatcher_ = dispatcher;
946 // Initialization is not complete until the Worker sends back the
947 // 'initialized' message. See below.
948 this.initialized_ = false;
950 // Map from Entry.toURL() to callback.
951 // Note that simultaneous requests for same url are handled in MetadataCache.
952 this.callbacks_ = {};
956 * Path of a worker script.
960 ContentProvider.WORKER_SCRIPT =
961 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/' +
962 'foreground/js/metadata/metadata_dispatcher.js';
964 ContentProvider.prototype = {
965 __proto__: MetadataProvider.prototype
969 * @param {Entry} entry The entry.
970 * @return {boolean} Whether this provider supports the entry.
972 ContentProvider.prototype.supportsEntry = function(entry) {
973 return !!entry.toURL().match(this.urlFilter_);
977 * @param {string} type The metadata type.
978 * @return {boolean} Whether this provider provides this metadata.
980 ContentProvider.prototype.providesType = function(type) {
981 return type === 'thumbnail' || type === 'fetchedMedia' || type === 'media';
985 * @return {string} Unique provider id.
987 ContentProvider.prototype.getId = function() { return 'content'; };
990 * Fetches the metadata.
991 * @param {Entry} entry File entry.
992 * @param {string} type Requested metadata type.
993 * @param {function(Object)} callback Callback expects a map from metadata type
994 * to metadata value. This callback is called asynchronously.
996 ContentProvider.prototype.fetch = function(entry, type, callback) {
997 if (entry.isDirectory) {
998 setTimeout(callback.bind(null, {}), 0);
1001 var entryURL = entry.toURL();
1002 this.callbacks_[entryURL] = callback;
1003 this.dispatcher_.postMessage({verb: 'request', arguments: [entryURL]});
1007 * Dispatch a message from a metadata reader to the appropriate on* method.
1008 * @param {Object} event The event.
1011 ContentProvider.prototype.onMessage_ = function(event) {
1012 var data = event.data;
1015 'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_';
1017 if (!(methodName in this)) {
1018 console.error('Unknown message from metadata reader: ' + data.verb, data);
1022 this[methodName].apply(this, data.arguments);
1026 * @return {boolean} Whether provider is ready.
1028 ContentProvider.prototype.isInitialized = function() {
1029 return this.initialized_;
1033 * Handles the 'initialized' message from the metadata reader Worker.
1034 * @param {Object} regexp Regexp of supported urls.
1037 ContentProvider.prototype.onInitialized_ = function(regexp) {
1038 this.urlFilter_ = regexp;
1040 // Tests can monitor for this state with
1041 // ExtensionTestMessageListener listener("worker-initialized");
1042 // ASSERT_TRUE(listener.WaitUntilSatisfied());
1043 // Automated tests need to wait for this, otherwise we crash in
1044 // browser_test cleanup because the worker process still has
1045 // URL requests in-flight.
1046 util.testSendMessage('worker-initialized');
1047 this.initialized_ = true;
1051 * Converts content metadata from parsers to the internal format.
1052 * @param {Object} metadata The content metadata.
1053 * @param {Object=} opt_result The internal metadata object ot put result in.
1054 * @return {Object!} Converted metadata.
1056 ContentProvider.ConvertContentMetadata = function(metadata, opt_result) {
1057 var result = opt_result || {};
1059 if ('thumbnailURL' in metadata) {
1060 metadata.thumbnailTransform = metadata.thumbnailTransform || null;
1061 result.thumbnail = {
1062 url: metadata.thumbnailURL,
1063 transform: metadata.thumbnailTransform
1067 for (var key in metadata) {
1068 if (metadata.hasOwnProperty(key)) {
1071 result.media[key] = metadata[key];
1076 result.fetchedMedia = result.media;
1082 * Handles the 'result' message from the worker.
1083 * @param {string} url File url.
1084 * @param {Object} metadata The metadata.
1087 ContentProvider.prototype.onResult_ = function(url, metadata) {
1088 var callback = this.callbacks_[url];
1089 delete this.callbacks_[url];
1090 callback(ContentProvider.ConvertContentMetadata(metadata));
1094 * Handles the 'error' message from the worker.
1095 * @param {string} url File entry.
1096 * @param {string} step Step failed.
1097 * @param {string} error Error description.
1098 * @param {Object?} metadata The metadata, if available.
1101 ContentProvider.prototype.onError_ = function(url, step, error, metadata) {
1102 if (MetadataCache.log) // Avoid log spam by default.
1103 console.warn('metadata: ' + url + ': ' + step + ': ' + error);
1104 metadata = metadata || {};
1105 // Prevent asking for thumbnail again.
1106 metadata.thumbnailURL = '';
1107 this.onResult_(url, metadata);
1111 * Handles the 'log' message from the worker.
1112 * @param {Array.<*>} arglist Log arguments.
1115 ContentProvider.prototype.onLog_ = function(arglist) {
1116 if (MetadataCache.log) // Avoid log spam by default.
1117 console.log.apply(console, ['metadata:'].concat(arglist));