Fix search results being clipped in app list.
[chromium-blink-merge.git] / ui / file_manager / image_loader / image_loader_client.js
blobe83362ed4ad078b19126f9bb3a312c42b88be12d
1 // Copyright 2013 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.
5 /**
6  * Client used to connect to the remote ImageLoader extension. Client class runs
7  * in the extension, where the client.js is included (eg. Files.app).
8  * It sends remote requests using IPC to the ImageLoader class and forwards
9  * its responses.
10  *
11  * Implements cache, which is stored in the calling extension.
12  *
13  * @constructor
14  */
15 function ImageLoaderClient() {
16   /**
17    * Hash array with active tasks.
18    * @type {!Object}
19    * @private
20    */
21   this.tasks_ = {};
23   /**
24    * @type {number}
25    * @private
26    */
27   this.lastTaskId_ = 0;
29   /**
30    * LRU cache for images.
31    * @type {!LRUCache.<{
32    *     data: string, width:number, height:number, timestamp: ?number}>}
33    * @private
34    */
35   this.cache_ = new LRUCache(ImageLoaderClient.CACHE_MEMORY_LIMIT);
38 /**
39  * Image loader's extension id.
40  * @const
41  * @type {string}
42  */
43 ImageLoaderClient.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp';
45 /**
46  * Returns a singleton instance.
47  * @return {ImageLoaderClient} Client instance.
48  */
49 ImageLoaderClient.getInstance = function() {
50   if (!ImageLoaderClient.instance_)
51     ImageLoaderClient.instance_ = new ImageLoaderClient();
52   return ImageLoaderClient.instance_;
55 /**
56  * Records binary metrics. Counts for true and false are stored as a histogram.
57  * @param {string} name Histogram's name.
58  * @param {boolean} value True or false.
59  */
60 ImageLoaderClient.recordBinary = function(name, value) {
61   chrome.metricsPrivate.recordValue(
62       { metricName: 'ImageLoader.Client.' + name,
63         type: 'histogram-linear',
64         min: 1,  // According to histogram.h, this should be 1 for enums.
65         max: 2,  // Maximum should be exclusive.
66         buckets: 3 },  // Number of buckets: 0, 1 and overflowing 2.
67       value ? 1 : 0);
70 /**
71  * Records percent metrics, stored as a histogram.
72  * @param {string} name Histogram's name.
73  * @param {number} value Value (0..100).
74  */
75 ImageLoaderClient.recordPercentage = function(name, value) {
76   chrome.metricsPrivate.recordPercentage('ImageLoader.Client.' + name,
77                                          Math.round(value));
80 /**
81  * Sends a message to the Image Loader extension.
82  * @param {Object} request Hash array with request data.
83  * @param {function(Object)=} opt_callback Response handling callback.
84  *     The response is passed as a hash array.
85  * @private
86  */
87 ImageLoaderClient.sendMessage_ = function(request, opt_callback) {
88   opt_callback = opt_callback || function(response) {};
89   chrome.runtime.sendMessage(
90       ImageLoaderClient.EXTENSION_ID, request, opt_callback);
93 /**
94  * Handles a message from the remote image loader and calls the registered
95  * callback to pass the response back to the requester.
96  *
97  * @param {Object} message Response message as a hash array.
98  * @private
99  */
100 ImageLoaderClient.prototype.handleMessage_ = function(message) {
101   if (!(message.taskId in this.tasks_)) {
102     // This task has been canceled, but was already fetched, so it's result
103     // should be discarded anyway.
104     return;
105   }
107   var task = this.tasks_[message.taskId];
109   // Check if the task is still valid.
110   if (task.isValid())
111     task.accept(message);
113   delete this.tasks_[message.taskId];
117  * Loads and resizes and image. Use opt_isValid to easily cancel requests
118  * which are not valid anymore, which will reduce cpu consumption.
120  * @param {string} url Url of the requested image.
121  * @param {function({status: string, data:string, width:number, height:number})}
122  *     callback Callback used to return response. Width and height in the
123  *     response is the size of image (data), i.e. When the image is resized,
124  *     these values are resized width and height.
125  * @param {Object=} opt_options Loader options, such as: scale, maxHeight,
126  *     width, height and/or cache.
127  * @param {function(): boolean=} opt_isValid Function returning false in case
128  *     a request is not valid anymore, eg. parent node has been detached.
129  * @return {?number} Remote task id or null if loaded from cache.
130  */
131 ImageLoaderClient.prototype.load = function(
132     url, callback, opt_options, opt_isValid) {
133   opt_options = /** @type {{cache: (boolean|undefined)}} */(opt_options || {});
134   opt_isValid = opt_isValid || function() { return true; };
136   // Record cache usage.
137   ImageLoaderClient.recordPercentage('Cache.Usage',
138       this.cache_.size() / ImageLoaderClient.CACHE_MEMORY_LIMIT * 100.0);
140   // Cancel old, invalid tasks.
141   var taskKeys = Object.keys(this.tasks_);
142   for (var index = 0; index < taskKeys.length; index++) {
143     var taskKey = taskKeys[index];
144     var task = this.tasks_[taskKey];
145     if (!task.isValid()) {
146       // Cancel this task since it is not valid anymore.
147       this.cancel(parseInt(taskKey, 10));
148       delete this.tasks_[taskKey];
149     }
150   }
152   // Replace the extension id.
153   var sourceId = chrome.i18n.getMessage('@@extension_id');
154   var targetId = ImageLoaderClient.EXTENSION_ID;
156   url = url.replace('filesystem:chrome-extension://' + sourceId,
157                     'filesystem:chrome-extension://' + targetId);
159   // Try to load from cache, if available.
160   var cacheKey = ImageLoaderClient.createKey(url, opt_options);
161   if (cacheKey) {
162     if (opt_options.cache) {
163       // Load from cache.
164       ImageLoaderClient.recordBinary('Cached', true);
165       var cachedValue = this.cache_.get(cacheKey);
166       // Check if the image in cache is up to date. If not, then remove it.
167       if (cachedValue && cachedValue.timestamp != opt_options.timestamp) {
168         this.cache_.remove(cacheKey);
169         cachedValue = null;
170       }
171       if (cachedValue && cachedValue.data &&
172           cachedValue.width && cachedValue.height) {
173         ImageLoaderClient.recordBinary('Cache.HitMiss', true);
174         callback({
175           status: 'success', data: cachedValue.data,
176           width: cachedValue.width, height: cachedValue.height
177         });
178         return null;
179       } else {
180         ImageLoaderClient.recordBinary('Cache.HitMiss', false);
181       }
182     } else {
183       // Remove from cache.
184       ImageLoaderClient.recordBinary('Cached', false);
185       this.cache_.remove(cacheKey);
186     }
187   }
189   // Not available in cache, performing a request to a remote extension.
190   var request = opt_options;
191   this.lastTaskId_++;
192   var task = {isValid: opt_isValid};
193   this.tasks_[this.lastTaskId_] = task;
195   request.url = url;
196   request.taskId = this.lastTaskId_;
197   request.timestamp = opt_options.timestamp;
199   ImageLoaderClient.sendMessage_(
200       request,
201       function(result) {
202         // Save to cache.
203         if (cacheKey && result.status == 'success' && opt_options.cache) {
204           var value = {
205             timestamp: opt_options.timestamp ? opt_options.timestamp : null,
206             data: result.data, width: result.width, height: result.height
207           };
208           this.cache_.put(cacheKey, value, result.data.length);
209         }
210         callback(result);
211       }.bind(this));
212   return request.taskId;
216  * Cancels the request.
217  * @param {number} taskId Task id returned by ImageLoaderClient.load().
218  */
219 ImageLoaderClient.prototype.cancel = function(taskId) {
220   ImageLoaderClient.sendMessage_({taskId: taskId, cancel: true});
224  * Memory limit for images data in bytes.
226  * @const
227  * @type {number}
228  */
229 ImageLoaderClient.CACHE_MEMORY_LIMIT = 20 * 1024 * 1024;  // 20 MB.
232  * Creates a cache key.
234  * @param {string} url Image url.
235  * @param {Object=} opt_options Loader options as a hash array.
236  * @return {?string} Cache key. It may return null if the class does not provide
237  *     caches for the URL. (e.g. Data URL)
238  */
239 ImageLoaderClient.createKey = function(url, opt_options) {
240   if (/^data:/i.test(url))
241     return null;
242   opt_options = opt_options || {};
243   return JSON.stringify({
244     url: url,
245     orientation: opt_options.orientation,
246     scale: opt_options.scale,
247     width: opt_options.width,
248     height: opt_options.height,
249     maxWidth: opt_options.maxWidth,
250     maxHeight: opt_options.maxHeight});
253 // Helper functions.
256  * Loads and resizes and image. Use opt_isValid to easily cancel requests
257  * which are not valid anymore, which will reduce cpu consumption.
259  * @param {string} url Url of the requested image.
260  * @param {HTMLImageElement} image Image node to load the requested picture
261  *     into.
262  * @param {Object} options Loader options, such as: orientation, scale,
263  *     maxHeight, width, height and/or cache.
264  * @param {function()} onSuccess Callback for success.
265  * @param {function()} onError Callback for failure.
266  * @param {function(): boolean=} opt_isValid Function returning false in case
267  *     a request is not valid anymore, eg. parent node has been detached.
268  * @return {?number} Remote task id or null if loaded from cache.
269  */
270 ImageLoaderClient.loadToImage = function(
271     url, image, options, onSuccess, onError, opt_isValid) {
272   var callback = function(result) {
273     if (result.status == 'error') {
274       onError();
275       return;
276     }
277     image.src = result.data;
278     onSuccess();
279   };
281   return ImageLoaderClient.getInstance().load(
282       url, callback, options, opt_isValid);