| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * Client used to connect to the remote ImageLoader extension. Client class runs |
| * in the extension, where the client.js is included (eg. Files.app). |
| * It sends remote requests using IPC to the ImageLoader class and forwards |
| * its responses. |
| * |
| * Implements cache, which is stored in the calling extension. |
| * |
| * @constructor |
| */ |
| function ImageLoaderClient() { |
| /** |
| * Hash array with active tasks. |
| * @type {Object} |
| * @private |
| */ |
| this.tasks_ = {}; |
| |
| /** |
| * @type {number} |
| * @private |
| */ |
| this.lastTaskId_ = 0; |
| |
| /** |
| * LRU cache for images. |
| * @type {ImageLoaderClient.Cache} |
| * @private |
| */ |
| this.cache_ = new ImageLoaderClient.Cache(); |
| } |
| |
| /** |
| * Image loader's extension id. |
| * @const |
| * @type {string} |
| */ |
| ImageLoaderClient.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp'; |
| |
| /** |
| * Returns a singleton instance. |
| * @return {Client} Client instance. |
| */ |
| ImageLoaderClient.getInstance = function() { |
| if (!ImageLoaderClient.instance_) |
| ImageLoaderClient.instance_ = new ImageLoaderClient(); |
| return ImageLoaderClient.instance_; |
| }; |
| |
| /** |
| * Records binary metrics. Counts for true and false are stored as a histogram. |
| * @param {string} name Histogram's name. |
| * @param {boolean} value True or false. |
| */ |
| ImageLoaderClient.recordBinary = function(name, value) { |
| chrome.metricsPrivate.recordValue( |
| { metricName: 'ImageLoader.Client.' + name, |
| type: 'histogram-linear', |
| min: 1, // According to histogram.h, this should be 1 for enums. |
| max: 2, // Maximum should be exclusive. |
| buckets: 3 }, // Number of buckets: 0, 1 and overflowing 2. |
| value ? 1 : 0); |
| }; |
| |
| /** |
| * Records percent metrics, stored as a histogram. |
| * @param {string} name Histogram's name. |
| * @param {number} value Value (0..100). |
| */ |
| ImageLoaderClient.recordPercentage = function(name, value) { |
| chrome.metricsPrivate.recordPercentage('ImageLoader.Client.' + name, |
| Math.round(value)); |
| }; |
| |
| /** |
| * Sends a message to the Image Loader extension. |
| * @param {Object} request Hash array with request data. |
| * @param {function(Object)=} opt_callback Response handling callback. |
| * The response is passed as a hash array. |
| * @private |
| */ |
| ImageLoaderClient.sendMessage_ = function(request, opt_callback) { |
| opt_callback = opt_callback || function(response) {}; |
| var sendMessage = chrome.runtime ? chrome.runtime.sendMessage : |
| chrome.extension.sendMessage; |
| sendMessage(ImageLoaderClient.EXTENSION_ID, request, opt_callback); |
| }; |
| |
| /** |
| * Handles a message from the remote image loader and calls the registered |
| * callback to pass the response back to the requester. |
| * |
| * @param {Object} message Response message as a hash array. |
| * @private |
| */ |
| ImageLoaderClient.prototype.handleMessage_ = function(message) { |
| if (!(message.taskId in this.tasks_)) { |
| // This task has been canceled, but was already fetched, so it's result |
| // should be discarded anyway. |
| return; |
| } |
| |
| var task = this.tasks_[message.taskId]; |
| |
| // Check if the task is still valid. |
| if (task.isValid()) |
| task.accept(message); |
| |
| delete this.tasks_[message.taskId]; |
| }; |
| |
| /** |
| * Loads and resizes and image. Use opt_isValid to easily cancel requests |
| * which are not valid anymore, which will reduce cpu consumption. |
| * |
| * @param {string} url Url of the requested image. |
| * @param {function} callback Callback used to return response. |
| * @param {Object=} opt_options Loader options, such as: scale, maxHeight, |
| * width, height and/or cache. |
| * @param {function=} opt_isValid Function returning false in case |
| * a request is not valid anymore, eg. parent node has been detached. |
| * @return {?number} Remote task id or null if loaded from cache. |
| */ |
| ImageLoaderClient.prototype.load = function( |
| url, callback, opt_options, opt_isValid) { |
| opt_options = opt_options || {}; |
| opt_isValid = opt_isValid || function() { return true; }; |
| |
| // Record cache usage. |
| ImageLoaderClient.recordPercentage('Cache.Usage', this.cache_.getUsage()); |
| |
| // Cancel old, invalid tasks. |
| var taskKeys = Object.keys(this.tasks_); |
| for (var index = 0; index < taskKeys.length; index++) { |
| var taskKey = taskKeys[index]; |
| var task = this.tasks_[taskKey]; |
| if (!task.isValid()) { |
| // Cancel this task since it is not valid anymore. |
| this.cancel(taskKey); |
| delete this.tasks_[taskKey]; |
| } |
| } |
| |
| // Replace the extension id. |
| var sourceId = chrome.i18n.getMessage('@@extension_id'); |
| var targetId = ImageLoaderClient.EXTENSION_ID; |
| |
| url = url.replace('filesystem:chrome-extension://' + sourceId, |
| 'filesystem:chrome-extension://' + targetId); |
| |
| // Try to load from cache, if available. |
| var cacheKey = ImageLoaderClient.Cache.createKey(url, opt_options); |
| if (opt_options.cache) { |
| // Load from cache. |
| ImageLoaderClient.recordBinary('Cached', 1); |
| var cachedData = this.cache_.loadImage(cacheKey, opt_options.timestamp); |
| if (cachedData) { |
| ImageLoaderClient.recordBinary('Cache.HitMiss', 1); |
| callback({status: 'success', data: cachedData}); |
| return null; |
| } else { |
| ImageLoaderClient.recordBinary('Cache.HitMiss', 0); |
| } |
| } else { |
| // Remove from cache. |
| ImageLoaderClient.recordBinary('Cached', 0); |
| this.cache_.removeImage(cacheKey); |
| } |
| |
| // Not available in cache, performing a request to a remote extension. |
| var request = opt_options; |
| this.lastTaskId_++; |
| var task = {isValid: opt_isValid}; |
| this.tasks_[this.lastTaskId_] = task; |
| |
| request.url = url; |
| request.taskId = this.lastTaskId_; |
| request.timestamp = opt_options.timestamp; |
| |
| ImageLoaderClient.sendMessage_( |
| request, |
| function(result) { |
| // Save to cache. |
| if (result.status == 'success' && opt_options.cache) |
| this.cache_.saveImage(cacheKey, result.data, opt_options.timestamp); |
| callback(result); |
| }.bind(this)); |
| return request.taskId; |
| }; |
| |
| /** |
| * Cancels the request. |
| * @param {number} taskId Task id returned by ImageLoaderClient.load(). |
| */ |
| ImageLoaderClient.prototype.cancel = function(taskId) { |
| ImageLoaderClient.sendMessage_({taskId: taskId, cancel: true}); |
| }; |
| |
| /** |
| * Least Recently Used (LRU) cache implementation to be used by |
| * Client class. It has memory constraints, so it will never |
| * exceed specified memory limit defined in MEMORY_LIMIT. |
| * |
| * @constructor |
| */ |
| ImageLoaderClient.Cache = function() { |
| this.images_ = []; |
| this.size_ = 0; |
| }; |
| |
| /** |
| * Memory limit for images data in bytes. |
| * |
| * @const |
| * @type {number} |
| */ |
| ImageLoaderClient.Cache.MEMORY_LIMIT = 20 * 1024 * 1024; // 20 MB. |
| |
| /** |
| * Creates a cache key. |
| * |
| * @param {string} url Image url. |
| * @param {Object=} opt_options Loader options as a hash array. |
| * @return {string} Cache key. |
| */ |
| ImageLoaderClient.Cache.createKey = function(url, opt_options) { |
| opt_options = opt_options || {}; |
| return JSON.stringify({url: url, |
| orientation: opt_options.orientation, |
| scale: opt_options.scale, |
| width: opt_options.width, |
| height: opt_options.height, |
| maxWidth: opt_options.maxWidth, |
| maxHeight: opt_options.maxHeight}); |
| }; |
| |
| /** |
| * Evicts the least used elements in cache to make space for a new image. |
| * |
| * @param {number} size Requested size. |
| * @private |
| */ |
| ImageLoaderClient.Cache.prototype.evictCache_ = function(size) { |
| // Sort from the most recent to the oldest. |
| this.images_.sort(function(a, b) { |
| return b.lastLoadTimestamp - a.lastLoadTimestamp; |
| }); |
| |
| while (this.images_.length > 0 && |
| (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < size)) { |
| var entry = this.images_.pop(); |
| this.size_ -= entry.data.length; |
| } |
| }; |
| |
| /** |
| * Saves an image in the cache. |
| * |
| * @param {string} key Cache key. |
| * @param {string} data Image data. |
| * @param {number=} opt_timestamp Last modification timestamp. Used to detect |
| * if the cache entry becomes out of date. |
| */ |
| ImageLoaderClient.Cache.prototype.saveImage = function( |
| key, data, opt_timestamp) { |
| // If the image is currently in cache, then remove it. |
| if (this.images_[key]) |
| this.removeImage(key); |
| |
| if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < data.length) { |
| ImageLoaderClient.recordBinary('Evicted', 1); |
| this.evictCache_(data.length); |
| } else { |
| ImageLoaderClient.recordBinary('Evicted', 0); |
| } |
| |
| if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ >= data.length) { |
| this.images_[key] = {lastLoadTimestamp: Date.now(), |
| timestamp: opt_timestamp ? opt_timestamp : null, |
| data: data}; |
| this.size_ += data.length; |
| } |
| }; |
| |
| /** |
| * Loads an image from the cache (if available) or returns null. |
| * |
| * @param {string} key Cache key. |
| * @param {number=} opt_timestamp Last modification timestamp. If different |
| * that the one in cache, then the entry will be invalidated. |
| * @return {?string} Data of the loaded image or null. |
| */ |
| ImageLoaderClient.Cache.prototype.loadImage = function(key, opt_timestamp) { |
| if (!(key in this.images_)) |
| return null; |
| |
| var entry = this.images_[key]; |
| entry.lastLoadTimestamp = Date.now(); |
| |
| // Check if the image in cache is up to date. If not, then remove it and |
| // return null. |
| if (entry.timestamp != opt_timestamp) { |
| this.removeImage(key); |
| return null; |
| } |
| |
| return entry.data; |
| }; |
| |
| /** |
| * Returns cache usage. |
| * @return {number} Value in percent points (0..100). |
| */ |
| ImageLoaderClient.Cache.prototype.getUsage = function() { |
| return this.size_ / ImageLoaderClient.Cache.MEMORY_LIMIT * 100.0; |
| }; |
| |
| /** |
| * Removes the image from the cache. |
| * @param {string} key Cache key. |
| */ |
| ImageLoaderClient.Cache.prototype.removeImage = function(key) { |
| if (!(key in this.images_)) |
| return; |
| |
| var entry = this.images_[key]; |
| this.size_ -= entry.data.length; |
| delete this.images_[key]; |
| }; |
| |
| // Helper functions. |
| |
| /** |
| * Loads and resizes and image. Use opt_isValid to easily cancel requests |
| * which are not valid anymore, which will reduce cpu consumption. |
| * |
| * @param {string} url Url of the requested image. |
| * @param {Image} image Image node to load the requested picture into. |
| * @param {Object} options Loader options, such as: orientation, scale, |
| * maxHeight, width, height and/or cache. |
| * @param {function=} onSuccess Callback for success. |
| * @param {function=} onError Callback for failure. |
| * @param {function=} opt_isValid Function returning false in case |
| * a request is not valid anymore, eg. parent node has been detached. |
| * @return {?number} Remote task id or null if loaded from cache. |
| */ |
| ImageLoaderClient.loadToImage = function( |
| url, image, options, onSuccess, onError, opt_isValid) { |
| var callback = function(result) { |
| if (result.status == 'error') { |
| onError(); |
| return; |
| } |
| image.src = result.data; |
| onSuccess(); |
| }; |
| |
| return ImageLoaderClient.getInstance().load( |
| url, callback, options, opt_isValid); |
| }; |