// 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. /** * 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 {!LRUCache.<{ * data: string, width:number, height:number, timestamp: ?number}>} * @private */ this.cache_ = new LRUCache(ImageLoaderClient.CACHE_MEMORY_LIMIT); } /** * Image loader's extension id. * @const * @type {string} */ ImageLoaderClient.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp'; /** * Returns a singleton instance. * @return {ImageLoaderClient} 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) {}; chrome.runtime.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({status: string, data:string, width:number, height:number})} * callback Callback used to return response. Width and height in the * response is the size of image (data), i.e. When the image is resized, * these values are resized width and height. * @param {Object=} opt_options Loader options, such as: scale, maxHeight, * width, height and/or cache. * @param {function(): boolean=} 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 = /** @type {{cache: (boolean|undefined)}} */(opt_options || {}); opt_isValid = opt_isValid || function() { return true; }; // Record cache usage. ImageLoaderClient.recordPercentage('Cache.Usage', this.cache_.size() / ImageLoaderClient.CACHE_MEMORY_LIMIT * 100.0); // 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(parseInt(taskKey, 10)); 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.createKey(url, opt_options); if (cacheKey) { if (opt_options.cache) { // Load from cache. ImageLoaderClient.recordBinary('Cached', true); var cachedValue = this.cache_.get(cacheKey); // Check if the image in cache is up to date. If not, then remove it. if (cachedValue && cachedValue.timestamp != opt_options.timestamp) { this.cache_.remove(cacheKey); cachedValue = null; } if (cachedValue && cachedValue.data && cachedValue.width && cachedValue.height) { ImageLoaderClient.recordBinary('Cache.HitMiss', true); callback({ status: 'success', data: cachedValue.data, width: cachedValue.width, height: cachedValue.height }); return null; } else { ImageLoaderClient.recordBinary('Cache.HitMiss', false); } } else { // Remove from cache. ImageLoaderClient.recordBinary('Cached', false); this.cache_.remove(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 (cacheKey && result.status == 'success' && opt_options.cache) { var value = { timestamp: opt_options.timestamp ? opt_options.timestamp : null, data: result.data, width: result.width, height: result.height }; this.cache_.put(cacheKey, value, result.data.length); } 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}); }; /** * 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. It may return null if the class does not provide * caches for the URL. (e.g. Data URL) */ ImageLoaderClient.createKey = function(url, opt_options) { if (/^data:/i.test(url)) return null; 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}); }; // 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 {HTMLImageElement} 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(): boolean=} 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); };