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