text: // 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.
  
  'use strict';
  
  /**
   * Creates and starts downloading and then resizing of the image. Finally,
   * returns the image using the callback.
   *
   * @param {string} id Request ID.
   * @param {Cache} cache Cache object.
   * @param {Object} request Request message as a hash array.
   * @param {function} callback Callback used to send the response.
   * @constructor
   */
  function Request(id, cache, request, callback) {
    /**
     * @type {string}
     * @private
     */
    this.id_ = id;
  
    /**
     * @type {Cache}
     * @private
     */
    this.cache_ = cache;
  
    /**
     * @type {Object}
     * @private
     */
    this.request_ = request;
  
    /**
     * @type {function}
     * @private
     */
    this.sendResponse_ = callback;
  
    /**
     * Temporary image used to download images.
     * @type {Image}
     * @private
     */
    this.image_ = new Image();
  
    /**
     * MIME type of the fetched image.
     * @type {string}
     * @private
     */
    this.contentType_ = null;
  
    /**
     * Used to download remote images using http:// or https:// protocols.
     * @type {AuthorizedXHR}
     * @private
     */
    this.xhr_ = new AuthorizedXHR();
  
    /**
     * Temporary canvas used to resize and compress the image.
     * @type {HTMLCanvasElement}
     * @private
     */
    this.canvas_ = document.createElement('canvas');
  
    /**
     * @type {CanvasRenderingContext2D}
     * @private
     */
    this.context_ = this.canvas_.getContext('2d');
  
    /**
     * Callback to be called once downloading is finished.
     * @type {function()}
     * @private
     */
    this.downloadCallback_ = null;
  }
  
  /**
   * Returns ID of the request.
   * @return {string} Request ID.
   */
  Request.prototype.getId = function() {
    return this.id_;
  };
  
  /**
   * Returns priority of the request. The higher priority, the faster it will
   * be handled. The highest priority is 0. The default one is 2.
   *
   * @return {number} Priority.
   */
  Request.prototype.getPriority = function() {
    return (this.request_.priority !== undefined) ? this.request_.priority : 2;
  };
  
  /**
   * Tries to load the image from cache if exists and sends the response.
   *
   * @param {function()} onSuccess Success callback.
   * @param {function()} onFailure Failure callback.
   */
  Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
    this.loadFromCache_(
        function(data) {  // Found in cache.
          this.sendImageData_(data);
          onSuccess();
        }.bind(this),
        onFailure);  // Not found in cache.
  };
  
  /**
   * Tries to download the image, resizes and sends the response.
   * @param {function()} callback Completion callback.
   */
  Request.prototype.downloadAndProcess = function(callback) {
    if (this.downloadCallback_)
      throw new Error('Downloading already started.');
  
    this.downloadCallback_ = callback;
    this.downloadOriginal_(this.onImageLoad_.bind(this),
                           this.onImageError_.bind(this));
  };
  
  /**
   * Fetches the image from the persistent cache.
   *
   * @param {function()} onSuccess Success callback.
   * @param {function()} onFailure Failure callback.
   * @private
   */
  Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
    var cacheKey = Cache.createKey(this.request_);
  
    if (!this.request_.cache) {
      // Cache is disabled for this request; therefore, remove it from cache
      // if existed.
      this.cache_.removeImage(cacheKey);
      onFailure();
      return;
    }
  
    if (!this.request_.timestamp) {
      // Persistent cache is available only when a timestamp is provided.
      onFailure();
      return;
    }
  
    this.cache_.loadImage(cacheKey,
                          this.request_.timestamp,
                          onSuccess,
                          onFailure);
  };
  
  /**
   * Saves the image to the persistent cache.
   *
   * @param {string} data The image's data.
   * @private
   */
  Request.prototype.saveToCache_ = function(data) {
    if (!this.request_.cache || !this.request_.timestamp) {
      // Persistent cache is available only when a timestamp is provided.
      return;
    }
  
    var cacheKey = Cache.createKey(this.request_);
    this.cache_.saveImage(cacheKey,
                          data,
                          this.request_.timestamp);
  };
  
  /**
   * Downloads an image directly or for remote resources using the XmlHttpRequest.
   *
   * @param {function()} onSuccess Success callback.
   * @param {function()} onFailure Failure callback.
   * @private
   */
  Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
    this.image_.onload = onSuccess;
    this.image_.onerror = onFailure;
  
    // Download data urls directly since they are not supported by XmlHttpRequest.
    var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
    if (dataUrlMatches) {
      this.image_.src = this.request_.url;
      this.contentType_ = dataUrlMatches[1];
      return;
    }
  
    // Fetch the image via authorized XHR and parse it.
    var parseImage = function(contentType, blob) {
      var reader = new FileReader();
      reader.onerror = onFailure;
      reader.onload = function(e) {
        this.image_.src = e.target.result;
      }.bind(this);
  
      // Load the data to the image as a data url.
      reader.readAsDataURL(blob);
    }.bind(this);
  
    // Request raw data via XHR.
    this.xhr_.load(this.request_.url, parseImage, onFailure);
  };
  
  /**
   * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
   * @constructor
   */
  function AuthorizedXHR() {
    this.xhr_ = null;
    this.aborted_ = false;
  }
  
  /**
   * Aborts the current request (if running).
   */
  AuthorizedXHR.prototype.abort = function() {
    this.aborted_ = true;
    if (this.xhr_)
      this.xhr_.abort();
  };
  
  /**
   * Loads an image using a OAuth2 token. If it fails, then tries to retry with
   * a refreshed OAuth2 token.
   *
   * @param {string} url URL to the resource to be fetched.
   * @param {function(string, Blob}) onSuccess Success callback with the content
   *     type and the fetched data.
   * @param {function()} onFailure Failure callback.
   */
  AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
    this.aborted_ = false;
  
    // Do not call any callbacks when aborting.
    var onMaybeSuccess = function(contentType, response) {
      if (!this.aborted_)
        onSuccess(contentType, response);
    }.bind(this);
    var onMaybeFailure = function(opt_code) {
      if (!this.aborted_)
        onFailure();
    }.bind(this);
  
    // Fetches the access token and makes an authorized call. If refresh is true,
    // then forces refreshing the access token.
    var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
      chrome.fileBrowserPrivate.requestAccessToken(refresh, function(token) {
        if (this.aborted_)
          return;
        if (!token) {
          onInnerFailure();
          return;
        }
        this.xhr_ = AuthorizedXHR.load_(
            token, url, onInnerSuccess, onInnerFailure);
      }.bind(this));
    }.bind(this);
  
    // Refreshes the access token and retries the request.
    var maybeRetryCall = function(code) {
      if (this.aborted_)
        return;
      requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
    }.bind(this);
  
    // Do not request a token for local resources, since it is not necessary.
    if (url.indexOf('filesystem:') === 0) {
      this.xhr_ = AuthorizedXHR.load_(null, url, onMaybeSuccess, onMaybeFailure);
      return;
    }
  
    // Make the request with reusing the current token. If it fails, then retry.
    requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
  };
  
  /**
   * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
   * If the token is invalid, the request will fail.
   *
   * @param {?string} token OAuth2 token to be injected to the request. Null for
   *     no token.
   * @param {string} url URL to the resource to be fetched.
   * @param {function(string, Blob}) onSuccess Success callback with the content
   *     type and the fetched data.
   * @param {function(number=)} onFailure Failure callback with the error code
   *     if available.
   * @return {AuthorizedXHR} XHR instance.
   * @private
   */
  AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';
  
    xhr.onreadystatechange = function() {
      if (xhr.readyState != 4)
        return;
      if (xhr.status != 200) {
        onFailure(xhr.status);
        return;
      }
      var contentType = xhr.getResponseHeader('Content-Type');
      onSuccess(contentType, xhr.response);
    }.bind(this);
  
    // Perform a xhr request.
    try {
      xhr.open('GET', url, true);
      if (token)
        xhr.setRequestHeader('Authorization', 'Bearer ' + token);
      xhr.send();
    } catch (e) {
      onFailure();
    }
  
    return xhr;
  };
  
  /**
   * Sends the resized image via the callback. If the image has been changed,
   * then packs the canvas contents, otherwise sends the raw image data.
   *
   * @param {boolean} imageChanged Whether the image has been changed.
   * @private
   */
  Request.prototype.sendImage_ = function(imageChanged) {
    var imageData;
    if (!imageChanged) {
      // The image hasn't been processed, so the raw data can be directly
      // forwarded for speed (no need to encode the image again).
      imageData = this.image_.src;
    } else {
      // The image has been resized or rotated, therefore the canvas has to be
      // encoded to get the correct compressed image data.
      switch (this.contentType_) {
        case 'image/gif':
        case 'image/png':
        case 'image/svg':
        case 'image/bmp':
          imageData = this.canvas_.toDataURL('image/png');
          break;
        case 'image/jpeg':
        default:
          imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
      }
    }
  
    // Send and store in the persistent cache.
    this.sendImageData_(imageData);
    this.saveToCache_(imageData);
  };
  
  /**
   * Sends the resized image via the callback.
   * @param {string} data Compressed image data.
   * @private
   */
  Request.prototype.sendImageData_ = function(data) {
    this.sendResponse_({status: 'success',
                        data: data,
                        taskId: this.request_.taskId});
  };
  
  /**
   * Handler, when contents are loaded into the image element. Performs resizing
   * and finalizes the request process.
   *
   * @param {function()} callback Completion callback.
   * @private
   */
  Request.prototype.onImageLoad_ = function(callback) {
    // Perform processing if the url is not a data url, or if there are some
    // operations requested.
    if (!this.request_.url.match(/^data/) ||
        ImageLoader.shouldProcess(this.image_.width,
                                  this.image_.height,
                                  this.request_)) {
      ImageLoader.resize(this.image_, this.canvas_, this.request_);
      this.sendImage_(true);  // Image changed.
    } else {
      this.sendImage_(false);  // Image not changed.
    }
    this.cleanup_();
    this.downloadCallback_();
  };
  
  /**
   * Handler, when loading of the image fails. Sends a failure response and
   * finalizes the request process.
   *
   * @param {function()} callback Completion callback.
   * @private
   */
  Request.prototype.onImageError_ = function(callback) {
    this.sendResponse_({status: 'error',
                        taskId: this.request_.taskId});
    this.cleanup_();
    this.downloadCallback_();
  };
  
  /**
   * Cancels the request.
   */
  Request.prototype.cancel = function() {
    this.cleanup_();
  
    // If downloading has started, then call the callback.
    if (this.downloadCallback_)
      this.downloadCallback_();
  };
  
  /**
   * Cleans up memory used by this request.
   * @private
   */
  Request.prototype.cleanup_ = function() {
    this.image_.onerror = function() {};
    this.image_.onload = function() {};
  
    // Transparent 1x1 pixel gif, to force garbage collecting.
    this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
        'ABAAEAAAICTAEAOw==';
  
    this.xhr_.onload = function() {};
    this.xhr_.abort();
  
    // Dispose memory allocated by Canvas.
    this.canvas_.width = 0;
    this.canvas_.height = 0;
  };
  