Sort unlaunched apps on app list start page by apps grid order.
[chromium-blink-merge.git] / ui / file_manager / image_loader / request.js
blob49fdd37e0f278cb7dbc470e56de837836f207b1b
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  * @typedef {{
7  *   cache: (boolean|undefined),
8  *   priority: (number|undefined),
9  *   taskId: number,
10  *   timestamp: (number|undefined),
11  *   url: string
12  * }}
13  */
14 var LoadImageRequest;
16 /**
17  * Creates and starts downloading and then resizing of the image. Finally,
18  * returns the image using the callback.
19  *
20  * @param {string} id Request ID.
21  * @param {Cache} cache Cache object.
22  * @param {LoadImageRequest} request Request message as a hash array.
23  * @param {function(Object)} callback Callback used to send the response.
24  * @constructor
25  */
26 function Request(id, cache, request, callback) {
27   /**
28    * @type {string}
29    * @private
30    */
31   this.id_ = id;
33   /**
34    * @type {Cache}
35    * @private
36    */
37   this.cache_ = cache;
39   /**
40    * @type {LoadImageRequest}
41    * @private
42    */
43   this.request_ = request;
45   /**
46    * @type {function(Object)}
47    * @private
48    */
49   this.sendResponse_ = callback;
51   /**
52    * Temporary image used to download images.
53    * @type {Image}
54    * @private
55    */
56   this.image_ = new Image();
58   /**
59    * MIME type of the fetched image.
60    * @type {?string}
61    * @private
62    */
63   this.contentType_ = null;
65   /**
66    * Used to download remote images using http:// or https:// protocols.
67    * @type {AuthorizedXHR}
68    * @private
69    */
70   this.xhr_ = new AuthorizedXHR();
72   /**
73    * Temporary canvas used to resize and compress the image.
74    * @type {HTMLCanvasElement}
75    * @private
76    */
77   this.canvas_ =
78       /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
80   /**
81    * @type {CanvasRenderingContext2D}
82    * @private
83    */
84   this.context_ =
85       /** @type {CanvasRenderingContext2D} */ (this.canvas_.getContext('2d'));
87   /**
88    * Callback to be called once downloading is finished.
89    * @type {?function()}
90    * @private
91    */
92   this.downloadCallback_ = null;
95 /**
96  * Returns ID of the request.
97  * @return {string} Request ID.
98  */
99 Request.prototype.getId = function() {
100   return this.id_;
104  * Returns priority of the request. The higher priority, the faster it will
105  * be handled. The highest priority is 0. The default one is 2.
107  * @return {number} Priority.
108  */
109 Request.prototype.getPriority = function() {
110   return (this.request_.priority !== undefined) ? this.request_.priority : 2;
114  * Tries to load the image from cache if exists and sends the response.
116  * @param {function()} onSuccess Success callback.
117  * @param {function()} onFailure Failure callback.
118  */
119 Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
120   this.loadFromCache_(
121       function(data, width, height) {  // Found in cache.
122         this.sendImageData_(data, width, height);
123         onSuccess();
124       }.bind(this),
125       onFailure);  // Not found in cache.
129  * Tries to download the image, resizes and sends the response.
130  * @param {function()} callback Completion callback.
131  */
132 Request.prototype.downloadAndProcess = function(callback) {
133   if (this.downloadCallback_)
134     throw new Error('Downloading already started.');
136   this.downloadCallback_ = callback;
137   this.downloadOriginal_(this.onImageLoad_.bind(this),
138                          this.onImageError_.bind(this));
142  * Fetches the image from the persistent cache.
144  * @param {function(string, number, number)} onSuccess Success callback.
145  * @param {function()} onFailure Failure callback.
146  * @private
147  */
148 Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
149   var cacheKey = Cache.createKey(this.request_);
151   if (!cacheKey) {
152     // Cache key is not provided for the request.
153     onFailure();
154     return;
155   }
157   if (!this.request_.cache) {
158     // Cache is disabled for this request; therefore, remove it from cache
159     // if existed.
160     this.cache_.removeImage(cacheKey);
161     onFailure();
162     return;
163   }
165   if (!this.request_.timestamp) {
166     // Persistent cache is available only when a timestamp is provided.
167     onFailure();
168     return;
169   }
171   this.cache_.loadImage(cacheKey,
172                         this.request_.timestamp,
173                         onSuccess,
174                         onFailure);
178  * Saves the image to the persistent cache.
180  * @param {string} data The image's data.
181  * @param {number} width Image width.
182  * @param {number} height Image height.
183  * @private
184  */
185 Request.prototype.saveToCache_ = function(data, width, height) {
186   if (!this.request_.cache || !this.request_.timestamp) {
187     // Persistent cache is available only when a timestamp is provided.
188     return;
189   }
191   var cacheKey = Cache.createKey(this.request_);
192   if (!cacheKey) {
193     // Cache key is not provided for the request.
194     return;
195   }
197   this.cache_.saveImage(cacheKey,
198                         data,
199                         width,
200                         height,
201                         this.request_.timestamp);
205  * Downloads an image directly or for remote resources using the XmlHttpRequest.
207  * @param {function()} onSuccess Success callback.
208  * @param {function()} onFailure Failure callback.
209  * @private
210  */
211 Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
212   this.image_.onload = function() {
213     URL.revokeObjectURL(this.image_.src);
214     onSuccess();
215   }.bind(this);
216   this.image_.onerror = function() {
217     URL.revokeObjectURL(this.image_.src);
218     onFailure();
219   }.bind(this);
221   // Download data urls directly since they are not supported by XmlHttpRequest.
222   var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
223   if (dataUrlMatches) {
224     this.image_.src = this.request_.url;
225     this.contentType_ = dataUrlMatches[1];
226     return;
227   }
229   // Fetch the image via authorized XHR and parse it.
230   var parseImage = function(contentType, blob) {
231     if (contentType)
232       this.contentType_ = contentType;
234     this.image_.src = URL.createObjectURL(blob);
235   }.bind(this);
237   // Request raw data via XHR.
238   this.xhr_.load(this.request_.url, parseImage, onFailure);
242  * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
243  * @constructor
244  */
245 function AuthorizedXHR() {
246   this.xhr_ = null;
247   this.aborted_ = false;
251  * A map which is used to estimate content type from extension.
252  * @enum {string}
253  */
254 AuthorizedXHR.ExtensionContentTypeMap = {
255   gif: 'image/gif',
256   png: 'image/png',
257   svg: 'image/svg',
258   bmp: 'image/bmp',
259   jpg: 'image/jpeg',
260   jpeg: 'image/jpeg'
264  * Aborts the current request (if running).
265  */
266 AuthorizedXHR.prototype.abort = function() {
267   this.aborted_ = true;
268   if (this.xhr_)
269     this.xhr_.abort();
273  * Loads an image using a OAuth2 token. If it fails, then tries to retry with
274  * a refreshed OAuth2 token.
276  * @param {string} url URL to the resource to be fetched.
277  * @param {function(string, Blob)} onSuccess Success callback with the content
278  *     type and the fetched data.
279  * @param {function()} onFailure Failure callback.
280  */
281 AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
282   this.aborted_ = false;
284   // Do not call any callbacks when aborting.
285   var onMaybeSuccess = /** @type {function(string, Blob)} */ (
286       function(contentType, response) {
287         // When content type is not available, try to estimate it from url.
288         if (!contentType) {
289           contentType = AuthorizedXHR.ExtensionContentTypeMap[
290               this.extractExtension_(url)];
291         }
293         if (!this.aborted_)
294           onSuccess(contentType, response);
295       }.bind(this));
297   var onMaybeFailure = /** @type {function(number=)} */ (
298       function(opt_code) {
299         if (!this.aborted_)
300           onFailure();
301       }.bind(this));
303   // Fetches the access token and makes an authorized call. If refresh is true,
304   // then forces refreshing the access token.
305   var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
306     chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
307       if (this.aborted_)
308         return;
309       if (!token) {
310         onInnerFailure();
311         return;
312       }
313       this.xhr_ = AuthorizedXHR.load_(
314           token, url, onInnerSuccess, onInnerFailure);
315     }.bind(this));
316   }.bind(this);
318   // Refreshes the access token and retries the request.
319   var maybeRetryCall = function(code) {
320     if (this.aborted_)
321       return;
322     requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
323   }.bind(this);
325   // Do not request a token for local resources, since it is not necessary.
326   if (/^filesystem:/.test(url)) {
327     // The query parameter is workaround for
328     // crbug.com/379678, which force to obtain the latest contents of the image.
329     var noCacheUrl = url + '?nocache=' + Date.now();
330     this.xhr_ = AuthorizedXHR.load_(
331         null,
332         noCacheUrl,
333         onMaybeSuccess,
334         onMaybeFailure);
335     return;
336   }
338   // Make the request with reusing the current token. If it fails, then retry.
339   requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
343  * Extracts extension from url.
344  * @param {string} url Url.
345  * @return {string} Extracted extensiion, e.g. png.
346  */
347 AuthorizedXHR.prototype.extractExtension_ = function(url) {
348   var result = (/\.([a-zA-Z]+)$/i).exec(url);
349   return result ? result[1] : '';
353  * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
354  * If the token is invalid, the request will fail.
356  * @param {?string} token OAuth2 token to be injected to the request. Null for
357  *     no token.
358  * @param {string} url URL to the resource to be fetched.
359  * @param {function(string, Blob)} onSuccess Success callback with the content
360  *     type and the fetched data.
361  * @param {function(number=)} onFailure Failure callback with the error code
362  *     if available.
363  * @return {XMLHttpRequest} XHR instance.
364  * @private
365  */
366 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
367   var xhr = new XMLHttpRequest();
368   xhr.responseType = 'blob';
370   xhr.onreadystatechange = function() {
371     if (xhr.readyState != 4)
372       return;
373     if (xhr.status != 200) {
374       onFailure(xhr.status);
375       return;
376     }
377     var contentType = xhr.getResponseHeader('Content-Type');
378     onSuccess(contentType, /** @type {Blob} */ (xhr.response));
379   }.bind(this);
381   // Perform a xhr request.
382   try {
383     xhr.open('GET', url, true);
384     if (token)
385       xhr.setRequestHeader('Authorization', 'Bearer ' + token);
386     xhr.send();
387   } catch (e) {
388     onFailure();
389   }
391   return xhr;
395  * Sends the resized image via the callback. If the image has been changed,
396  * then packs the canvas contents, otherwise sends the raw image data.
398  * @param {boolean} imageChanged Whether the image has been changed.
399  * @private
400  */
401 Request.prototype.sendImage_ = function(imageChanged) {
402   var imageData;
403   var width;
404   var height;
405   if (!imageChanged) {
406     // The image hasn't been processed, so the raw data can be directly
407     // forwarded for speed (no need to encode the image again).
408     imageData = this.image_.src;
409     width = this.image_.width;
410     height = this.image_.height;
411   } else {
412     // The image has been resized or rotated, therefore the canvas has to be
413     // encoded to get the correct compressed image data.
414     width = this.canvas_.width;
415     height = this.canvas_.height;
417     switch (this.contentType_) {
418       case 'image/gif':
419       case 'image/png':
420       case 'image/svg':
421       case 'image/bmp':
422         imageData = this.canvas_.toDataURL('image/png');
423         break;
424       case 'image/jpeg':
425       default:
426         imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
427     }
428   }
430   // Send and store in the persistent cache.
431   this.sendImageData_(imageData, width, height);
432   this.saveToCache_(imageData, width, height);
436  * Sends the resized image via the callback.
437  * @param {string} data Compressed image data.
438  * @param {number} width Width.
439  * @param {number} height Height.
440  * @private
441  */
442 Request.prototype.sendImageData_ = function(data, width, height) {
443   this.sendResponse_({
444     status: 'success', data: data, width: width, height: height,
445     taskId: this.request_.taskId
446   });
450  * Handler, when contents are loaded into the image element. Performs resizing
451  * and finalizes the request process.
452  * @private
453  */
454 Request.prototype.onImageLoad_ = function() {
455   // Perform processing if the url is not a data url, or if there are some
456   // operations requested.
457   if (!this.request_.url.match(/^data/) ||
458       ImageLoader.shouldProcess(this.image_.width,
459                                 this.image_.height,
460                                 this.request_)) {
461     ImageLoader.resize(this.image_, this.canvas_, this.request_);
462     this.sendImage_(true);  // Image changed.
463   } else {
464     this.sendImage_(false);  // Image not changed.
465   }
466   this.cleanup_();
467   this.downloadCallback_();
471  * Handler, when loading of the image fails. Sends a failure response and
472  * finalizes the request process.
473  * @private
474  */
475 Request.prototype.onImageError_ = function() {
476   this.sendResponse_(
477       {status: 'error', taskId: this.request_.taskId});
478   this.cleanup_();
479   this.downloadCallback_();
483  * Cancels the request.
484  */
485 Request.prototype.cancel = function() {
486   this.cleanup_();
488   // If downloading has started, then call the callback.
489   if (this.downloadCallback_)
490     this.downloadCallback_();
494  * Cleans up memory used by this request.
495  * @private
496  */
497 Request.prototype.cleanup_ = function() {
498   this.image_.onerror = function() {};
499   this.image_.onload = function() {};
501   // Transparent 1x1 pixel gif, to force garbage collecting.
502   this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
503       'ABAAEAAAICTAEAOw==';
505   this.xhr_.onload = function() {};
506   this.xhr_.abort();
508   // Dispose memory allocated by Canvas.
509   this.canvas_.width = 0;
510   this.canvas_.height = 0;