fdc0d232a99b0448995b88c6a18d8558ed9d5545
[platform/framework/web/crosswalk.git] / src / ui / file_manager / image_loader / image_loader_client.js
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.
4
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_ = {};
22
23   /**
24    * @type {number}
25    * @private
26    */
27   this.lastTaskId_ = 0;
28
29   /**
30    * LRU cache for images.
31    * @type {ImageLoaderClient.Cache}
32    * @private
33    */
34   this.cache_ = new ImageLoaderClient.Cache();
35 }
36
37 /**
38  * Image loader's extension id.
39  * @const
40  * @type {string}
41  */
42 ImageLoaderClient.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp';
43
44 /**
45  * Returns a singleton instance.
46  * @return {ImageLoaderClient} Client instance.
47  */
48 ImageLoaderClient.getInstance = function() {
49   if (!ImageLoaderClient.instance_)
50     ImageLoaderClient.instance_ = new ImageLoaderClient();
51   return ImageLoaderClient.instance_;
52 };
53
54 /**
55  * Records binary metrics. Counts for true and false are stored as a histogram.
56  * @param {string} name Histogram's name.
57  * @param {boolean} value True or false.
58  */
59 ImageLoaderClient.recordBinary = function(name, value) {
60   chrome.metricsPrivate.recordValue(
61       { metricName: 'ImageLoader.Client.' + name,
62         type: 'histogram-linear',
63         min: 1,  // According to histogram.h, this should be 1 for enums.
64         max: 2,  // Maximum should be exclusive.
65         buckets: 3 },  // Number of buckets: 0, 1 and overflowing 2.
66       value ? 1 : 0);
67 };
68
69 /**
70  * Records percent metrics, stored as a histogram.
71  * @param {string} name Histogram's name.
72  * @param {number} value Value (0..100).
73  */
74 ImageLoaderClient.recordPercentage = function(name, value) {
75   chrome.metricsPrivate.recordPercentage('ImageLoader.Client.' + name,
76                                          Math.round(value));
77 };
78
79 /**
80  * Sends a message to the Image Loader extension.
81  * @param {Object} request Hash array with request data.
82  * @param {function(Object)=} opt_callback Response handling callback.
83  *     The response is passed as a hash array.
84  * @private
85  */
86 ImageLoaderClient.sendMessage_ = function(request, opt_callback) {
87   opt_callback = opt_callback || function(response) {};
88   chrome.runtime.sendMessage(
89       ImageLoaderClient.EXTENSION_ID, request, opt_callback);
90 };
91
92 /**
93  * Handles a message from the remote image loader and calls the registered
94  * callback to pass the response back to the requester.
95  *
96  * @param {Object} message Response message as a hash array.
97  * @private
98  */
99 ImageLoaderClient.prototype.handleMessage_ = function(message) {
100   if (!(message.taskId in this.tasks_)) {
101     // This task has been canceled, but was already fetched, so it's result
102     // should be discarded anyway.
103     return;
104   }
105
106   var task = this.tasks_[message.taskId];
107
108   // Check if the task is still valid.
109   if (task.isValid())
110     task.accept(message);
111
112   delete this.tasks_[message.taskId];
113 };
114
115 /**
116  * Loads and resizes and image. Use opt_isValid to easily cancel requests
117  * which are not valid anymore, which will reduce cpu consumption.
118  *
119  * @param {string} url Url of the requested image.
120  * @param {function(Object)} callback Callback used to return response.
121  * @param {Object=} opt_options Loader options, such as: scale, maxHeight,
122  *     width, height and/or cache.
123  * @param {function(): boolean=} opt_isValid Function returning false in case
124  *     a request is not valid anymore, eg. parent node has been detached.
125  * @return {?number} Remote task id or null if loaded from cache.
126  */
127 ImageLoaderClient.prototype.load = function(
128     url, callback, opt_options, opt_isValid) {
129   opt_options = /** @type {{cache: (boolean|undefined)}} */(opt_options || {});
130   opt_isValid = opt_isValid || function() { return true; };
131
132   // Record cache usage.
133   ImageLoaderClient.recordPercentage('Cache.Usage', this.cache_.getUsage());
134
135   // Cancel old, invalid tasks.
136   var taskKeys = Object.keys(this.tasks_);
137   for (var index = 0; index < taskKeys.length; index++) {
138     var taskKey = taskKeys[index];
139     var task = this.tasks_[taskKey];
140     if (!task.isValid()) {
141       // Cancel this task since it is not valid anymore.
142       this.cancel(parseInt(taskKey, 10));
143       delete this.tasks_[taskKey];
144     }
145   }
146
147   // Replace the extension id.
148   var sourceId = chrome.i18n.getMessage('@@extension_id');
149   var targetId = ImageLoaderClient.EXTENSION_ID;
150
151   url = url.replace('filesystem:chrome-extension://' + sourceId,
152                     'filesystem:chrome-extension://' + targetId);
153
154   // Try to load from cache, if available.
155   var cacheKey = ImageLoaderClient.Cache.createKey(url, opt_options);
156   if (opt_options.cache) {
157     // Load from cache.
158     ImageLoaderClient.recordBinary('Cached', true);
159     var cachedData = this.cache_.loadImage(cacheKey, opt_options.timestamp);
160     if (cachedData) {
161       ImageLoaderClient.recordBinary('Cache.HitMiss', true);
162       callback({status: 'success', data: cachedData});
163       return null;
164     } else {
165       ImageLoaderClient.recordBinary('Cache.HitMiss', false);
166     }
167   } else {
168     // Remove from cache.
169     ImageLoaderClient.recordBinary('Cached', false);
170     this.cache_.removeImage(cacheKey);
171   }
172
173   // Not available in cache, performing a request to a remote extension.
174   var request = opt_options;
175   this.lastTaskId_++;
176   var task = {isValid: opt_isValid};
177   this.tasks_[this.lastTaskId_] = task;
178
179   request.url = url;
180   request.taskId = this.lastTaskId_;
181   request.timestamp = opt_options.timestamp;
182
183   ImageLoaderClient.sendMessage_(
184       request,
185       function(result) {
186         // Save to cache.
187         if (result.status == 'success' && opt_options.cache)
188           this.cache_.saveImage(cacheKey, result.data, opt_options.timestamp);
189         callback(result);
190       }.bind(this));
191   return request.taskId;
192 };
193
194 /**
195  * Cancels the request.
196  * @param {number} taskId Task id returned by ImageLoaderClient.load().
197  */
198 ImageLoaderClient.prototype.cancel = function(taskId) {
199   ImageLoaderClient.sendMessage_({taskId: taskId, cancel: true});
200 };
201
202 /**
203  * Least Recently Used (LRU) cache implementation to be used by
204  * Client class. It has memory constraints, so it will never
205  * exceed specified memory limit defined in MEMORY_LIMIT.
206  *
207  * @constructor
208  */
209 ImageLoaderClient.Cache = function() {
210   this.images_ = [];
211   this.size_ = 0;
212 };
213
214 /**
215  * Memory limit for images data in bytes.
216  *
217  * @const
218  * @type {number}
219  */
220 ImageLoaderClient.Cache.MEMORY_LIMIT = 20 * 1024 * 1024;  // 20 MB.
221
222 /**
223  * Creates a cache key.
224  *
225  * @param {string} url Image url.
226  * @param {Object=} opt_options Loader options as a hash array.
227  * @return {string} Cache key.
228  */
229 ImageLoaderClient.Cache.createKey = function(url, opt_options) {
230   opt_options = opt_options || {};
231   return JSON.stringify({
232     url: url,
233     orientation: opt_options.orientation,
234     scale: opt_options.scale,
235     width: opt_options.width,
236     height: opt_options.height,
237     maxWidth: opt_options.maxWidth,
238     maxHeight: opt_options.maxHeight});
239 };
240
241 /**
242  * Evicts the least used elements in cache to make space for a new image.
243  *
244  * @param {number} size Requested size.
245  * @private
246  */
247 ImageLoaderClient.Cache.prototype.evictCache_ = function(size) {
248   // Sort from the most recent to the oldest.
249   this.images_.sort(function(a, b) {
250     return b.lastLoadTimestamp - a.lastLoadTimestamp;
251   });
252
253   while (this.images_.length > 0 &&
254          (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < size)) {
255     var entry = this.images_.pop();
256     this.size_ -= entry.data.length;
257   }
258 };
259
260 /**
261  * Saves an image in the cache.
262  *
263  * @param {string} key Cache key.
264  * @param {string} data Image data.
265  * @param {number=} opt_timestamp Last modification timestamp. Used to detect
266  *     if the cache entry becomes out of date.
267  */
268 ImageLoaderClient.Cache.prototype.saveImage = function(
269     key, data, opt_timestamp) {
270   // If the image is currently in cache, then remove it.
271   if (this.images_[key])
272     this.removeImage(key);
273
274   if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ < data.length) {
275     ImageLoaderClient.recordBinary('Evicted', true);
276     this.evictCache_(data.length);
277   } else {
278     ImageLoaderClient.recordBinary('Evicted', false);
279   }
280
281   if (ImageLoaderClient.Cache.MEMORY_LIMIT - this.size_ >= data.length) {
282     this.images_[key] = {
283       lastLoadTimestamp: Date.now(),
284       timestamp: opt_timestamp ? opt_timestamp : null,
285       data: data
286     };
287     this.size_ += data.length;
288   }
289 };
290
291 /**
292  * Loads an image from the cache (if available) or returns null.
293  *
294  * @param {string} key Cache key.
295  * @param {number=} opt_timestamp Last modification timestamp. If different
296  *     that the one in cache, then the entry will be invalidated.
297  * @return {?string} Data of the loaded image or null.
298  */
299 ImageLoaderClient.Cache.prototype.loadImage = function(key, opt_timestamp) {
300   if (!(key in this.images_))
301     return null;
302
303   var entry = this.images_[key];
304   entry.lastLoadTimestamp = Date.now();
305
306   // Check if the image in cache is up to date. If not, then remove it and
307   // return null.
308   if (entry.timestamp != opt_timestamp) {
309     this.removeImage(key);
310     return null;
311   }
312
313   return entry.data;
314 };
315
316 /**
317  * Returns cache usage.
318  * @return {number} Value in percent points (0..100).
319  */
320 ImageLoaderClient.Cache.prototype.getUsage = function() {
321   return this.size_ / ImageLoaderClient.Cache.MEMORY_LIMIT * 100.0;
322 };
323
324 /**
325  * Removes the image from the cache.
326  * @param {string} key Cache key.
327  */
328 ImageLoaderClient.Cache.prototype.removeImage = function(key) {
329   if (!(key in this.images_))
330     return;
331
332   var entry = this.images_[key];
333   this.size_ -= entry.data.length;
334   delete this.images_[key];
335 };
336
337 // Helper functions.
338
339 /**
340  * Loads and resizes and image. Use opt_isValid to easily cancel requests
341  * which are not valid anymore, which will reduce cpu consumption.
342  *
343  * @param {string} url Url of the requested image.
344  * @param {HTMLImageElement} image Image node to load the requested picture
345  *     into.
346  * @param {Object} options Loader options, such as: orientation, scale,
347  *     maxHeight, width, height and/or cache.
348  * @param {function()} onSuccess Callback for success.
349  * @param {function()} onError Callback for failure.
350  * @param {function(): boolean=} opt_isValid Function returning false in case
351  *     a request is not valid anymore, eg. parent node has been detached.
352  * @return {?number} Remote task id or null if loaded from cache.
353  */
354 ImageLoaderClient.loadToImage = function(
355     url, image, options, onSuccess, onError, opt_isValid) {
356   var callback = function(result) {
357     if (result.status == 'error') {
358       onError();
359       return;
360     }
361     image.src = result.data;
362     onSuccess();
363   };
364
365   return ImageLoaderClient.getInstance().load(
366       url, callback, options, opt_isValid);
367 };