Esempio n. 1
0
File: app.js Progetto: szatkus/gaia
define(function(require, exports, module) {
/*jshint laxbreak:true*/
'use strict';

/**
 * Dependencies
 */

var performanceTesting = require('performanceTesting');
var constants = require('config/camera');
var bindAll = require('utils/bindAll');
var lockscreen = require('lockscreen');
var broadcast = require('broadcast');
var debug = require('debug')('app');
var LazyL10n = require('LazyL10n');
var bind = require('utils/bind');
var evt = require('vendor/evt');
var dcf = require('dcf');

/**
 * Locals
 */

var LOCATION_PROMPT_DELAY = constants.PROMPT_DELAY;
evt.mix(App.prototype);
var unbind = bind.unbind;

/**
 * Exports
 */

module.exports = App;

/**
 * Initialize a new `App`
 *
 * Options:
 *
 *   - `root` The node to inject content into
 *
 * @param {Object} options
 * @constructor
 */
function App(options) {
  this.el = options.el;
  this.win = options.win;
  this.doc = options.doc;
  this.inSecureMode = (this.win.location.hash === '#secure');
  this.geolocation = options.geolocation;
  this.activity = options.activity;
  this.filmstrip = options.filmstrip;
  this.storage = options.storage;
  this.camera = options.camera;
  this.sounds = options.sounds;
  this.views = options.views;
  this.controllers = options.controllers;
  bindAll(this);
  debug('initialized');
}

/**
 * Runs all the methods
 * to boot the app.
 *
 */
App.prototype.boot = function() {
  debug('boot');
  this.filmstrip = this.filmstrip(this);
  this.runControllers();
  this.injectContent();
  this.bindEvents();
  this.miscStuff();
  this.emit('boot');
  debug('booted');
};

App.prototype.teardown = function() {
  this.unbindEvents();
};

/**
 * Runs controllers to glue all
 * the parts of the app together.
 *
 */
App.prototype.runControllers = function() {
  debug('running controllers');
  this.controllers.camera(this);
  this.controllers.viewfinder(this);
  this.controllers.controls(this);
  this.controllers.confirm(this);
  this.controllers.overlay(this);
  this.controllers.hud(this);
  debug('controllers run');
};

/**
 * Injects view DOM into
 * designated root node.
 *
 * @return {[type]} [description]
 */
App.prototype.injectContent = function() {
  this.views.hud.appendTo(this.el);
  this.views.controls.appendTo(this.el);
  this.views.viewfinder.appendTo(this.el);
  this.views.focusRing.appendTo(this.el);
  debug('content injected');
};

/**
 * Attaches event handlers.
 *
 */
App.prototype.bindEvents = function() {
  this.camera.once('configured', this.storage.check);
  this.storage.once('checked:healthy', this.geolocationWatch);
  bind(this.doc, 'visibilitychange', this.onVisibilityChange);
  bind(this.win, 'beforeunload', this.onBeforeUnload);
  this.on('focus', this.onFocus);
  this.on('blur', this.onBlur);
  debug('events bound');
};

/**
 * Detaches event handlers.
 */
App.prototype.unbindEvents = function() {
  unbind(this.doc, 'visibilitychange', this.onVisibilityChange);
  unbind(this.win, 'beforeunload', this.onBeforeUnload);
  this.off('focus', this.onFocus);
  this.off('blur', this.onBlur);
  debug('events unbound');
};

/**
 * Tasks to run when the
 * app becomes visible.
 *
 * Check the storage again as users
 * may have made changes since the
 * app was minimised
 */
App.prototype.onFocus = function() {
  var ms = LOCATION_PROMPT_DELAY;
  setTimeout(this.geolocationWatch, ms);
  this.storage.check();
  debug('focus');
};

/**
 * Tasks to run when the
 * app is minimised/hidden.
 */
App.prototype.onBlur = function() {
  this.geolocation.stopWatching();
  this.activity.cancel();
  debug('blur');
};

/**
 * Begins watching location
 * if not within a pending
 * activity and the app
 * isn't currently hidden.
 *
 */
App.prototype.geolocationWatch = function() {
  var shouldWatch = !this.activity.active && !this.doc.hidden;
  if (shouldWatch) {
    this.geolocation.watch();
    debug('geolocation watched');
  }
};

/**
 * Responds to the `visibilitychange`
 * event, emitting useful app events
 * that allow us to perform relate
 * work elsewhere,
 *
 */
App.prototype.onVisibilityChange = function() {
  if (this.doc.hidden) {
    this.emit('blur');
  } else {
    this.emit('focus');
  }
};

/**
 * Runs just before the
 * app is destroyed.
 *
 */
App.prototype.onBeforeUnload = function() {
  this.emit('beforeunload');
  debug('beforeunload');
};

/**
 * Miscalaneous tasks to be
 * run when the app first
 * starts.
 *
 * TODO: Eventually this function
 * will be removed, and all this
 * logic will sit in specific places.
 *
 */
App.prototype.miscStuff = function() {
  var camera = this.camera;
  var focusTimeout;
  var self = this;

  // TODO: Should probably be
  // moved to a focusRing controller
  camera.on('change:focus', function(value) {
    self.views.focusRing.setState(value);
    clearTimeout(focusTimeout);

    if (value === 'fail') {
      focusTimeout = setTimeout(function() {
        self.views.focusRing.setState(null);
      }, 1000);
    }
  });


  if (!navigator.mozCameras) {
    // TODO: Need to clarify what we
    // should do in this condition.
  }

  performanceTesting.dispatch('initialising-camera-preview');

  // Prevent the phone
  // from going to sleep.
  lockscreen.disableTimeout();

  // This must be tidied, but the
  // important thing is it's out
  // of camera.js
  LazyL10n.get(function() {
    dcf.init();
    performanceTesting.dispatch('startup-path-done');
  });

  // The screen wakelock should be on
  // at all times except when the
  // filmstrip preview is shown.
  broadcast.on('filmstripItemPreview', function() {
    lockscreen.enableTimeout();
  });

  // When the filmstrip preview is hidden
  // we can enable the  again.
  broadcast.on('filmstripPreviewHide', function() {
    lockscreen.disableTimeout();
  });

  debug('misc stuff done');
};

});
Esempio n. 2
0
File: model.js Progetto: AlinT/gaia
define(function(require) {
  'use strict';

  var evt = require('vendor/evt');

  var Model = function(properties) {
    this._properties = properties || {};
  };

  Model.prototype = evt.mix({
    _properties: null,

    get: function(key) {

      // Get bulk properties
      if (!key) {
        return this._properties;
      }

      // Get single property
      return this._properties[key];
    },

    set: function(keyOrProperties, value) {

      // Set bulk properties
      if (typeof keyOrProperties === 'object') {
        var didChange = false;

        for (var key in keyOrProperties) {

          // Skip setting property if it hasn't changed
          if (this._properties[key] === keyOrProperties[key]) {
            continue;
          }

          this._properties[key] = keyOrProperties[key];
          this.emit('change:' + key, keyOrProperties[key]);

          didChange = true;
        }

        if (didChange) {
          this.emit('change');
        }

        return;
      }

      // Skip setting property if it hasn't changed
      if (this._properties[keyOrProperties] === value) {
        return;
      }

      // Set single property
      this._properties[keyOrProperties] = value;

      this.emit('change');
      this.emit('change:' + keyOrProperties, value);
    }
  });

  return Model;
});
Esempio n. 3
0
File: camera.js Progetto: AlinT/gaia
define(function(require, exports, module) {
/*global CONFIG_MAX_IMAGE_PIXEL_SIZE*/
/*jshint laxbreak:true*/
'use strict';

/**
 * Dependencies
 */

var createVideoPosterImage = require('lib/create-video-poster-image');
var constants = require('config/camera');
var orientation = require('orientation');
var broadcast = require('broadcast');
var Model = require('vendor/model');
var evt = require('vendor/evt');
var dcf = require('dcf');

/**
 * Locals
 */

var CAMERA = constants.CAMERA_MODE_TYPE.CAMERA;
var VIDEO = constants.CAMERA_MODE_TYPE.VIDEO;
var FOCUS_MODE_TYPE = constants.FOCUS_MODE_TYPE;
var RECORD_SPACE_MIN = constants.RECORD_SPACE_MIN;
var RECORD_SPACE_PADDING = constants.RECORD_SPACE_PADDING;
var ESTIMATED_JPEG_FILE_SIZE = constants.ESTIMATED_JPEG_FILE_SIZE;
var MIN_RECORDING_TIME = constants.MIN_RECORDING_TIME;
var proto = evt.mix(Camera.prototype);

/**
 * Exports
 */

module.exports = Camera;

/**
 * The interface to
 * the camera hardware.
 *
 * TODO:
 *
 * - Move all state into this.state model.
 * - Introduce a camera controller to
 *   wire the camera into the app.
 *
 * @constructor
 */
function Camera() {
  this.state = new Model({
    mode: null,
    cameraNumber: 0,
    autoFocusSupported: false,
    manuallyFocused: false,
    recording: false,
    previewActive: false
  });

  this.videoTimer = null;

  // file path relative
  // to video root directory
  this._videoPath = null;

  // video root directory string
  this._videoRootDir = null;
  this._autoFocusSupport = {};
  this._cameraObj = null;
  this._pictureSize = null;
  this._previewConfig = null;

  // We can recieve multiple
  // 'FileSizeLimitReached' events
  // when recording; since we stop
  // recording on this event only
  // show one alert per recording
  this._sizeLimitAlertActive = false;

  // Holds the configuration
  // and current flash state.
  this.flash = {

    // Our predetermined configuration
    // for camera and video flash
    config: {
      camera: {
        defaultMode: 'auto',
        supports: ['off', 'auto', 'on']
      },
      video: {
        defaultMode: 'off',
        supports: ['off', 'torch']
      }
    },

    // All flash hardware modes
    // on this current camera.
    all: [],

    // Flash modes currently
    // available with the current
    // combination of hardware
    // and capture mode.
    available: [],

    // The index of the current
    // flash in the avaiable list.
    current: null
  };

  this.fileFormat = 'jpeg';
  this.preferredRecordingSizes = null;

  this._pendingPick = null;
  this._savedMedia = null;

  // Bind context
  this.updateVideoElapsed = this.updateVideoElapsed.bind(this);
  this.onStorageChange = this.onStorageChange.bind(this);
  this.storageCheck = this.storageCheck.bind(this);

  // Whenever the camera is
  // configured, we run a storage
  // check to determine whether
  // we have enough space to
  // accomodate a photograph.
  this.on('configured', this.storageCheck);

  this.configureStorage();
}

proto.configureStorage = function() {
  this._pictureStorage = navigator.getDeviceStorage('pictures');
  this._videoStorage = navigator.getDeviceStorage('videos'),
  this._pictureStorage.addEventListener('change', this.onStorageChange);
};

/**
 * Returns the current
 * capture mode.
 *
 * @return {String} 'camera'|'video'
 */
proto.getMode = function() {
  return this.state.get('mode');
};

/**
 * States if the camera is
 * in 'camera' capture mode.
 *
 * @return {Boolean}
 */
proto.isCameraMode = function() {
  return this.getMode() === CAMERA;
};

/**
 * States if the camera is
 * in 'video' capture mode.
 *
 * @return {Boolean}
 */
proto.isVideoMode = function() {
  return this.getMode() === VIDEO;
};

/**
 * Toggles between 'camera'
 * and 'video' capture modes.
 *
 * @return {String}
 */
proto.toggleMode = function() {
  var newMode = this.isCameraMode() ? VIDEO : CAMERA;
  this.setCaptureMode(newMode);
  this.configureFlashModes(this.flash.all);
  return newMode;
};

/**
 * Sets the capture mode.
 *
 * @param {String} mode
 *
 */
proto.setCaptureMode = function(mode) {
  this.state.set('mode', mode);
  return mode;
};

/**
 * Toggles the camera number
 * between back (0) and front(1).
 *
 * @return {Number}
 */
proto.toggleCamera = function() {
  var cameraNumber = 1 - this.state.get('cameraNumber');
  this.state.set('cameraNumber', cameraNumber);
  return cameraNumber;
};

/**
 * Cycles through flash
 * modes available for the
 * current camera (0/1) and
 * capture mode ('camera'/'video')
 * combination.
 *
 * @return {String}
 */
proto.toggleFlash = function() {
  var available = this.flash.available;
  var current = this.flash.current;
  var l = available.length;
  var next = (current + 1) % l;
  var name = available[next];

  this.setFlashMode(next);
  return name;
};

/**
 * Gets the name of the
 * current flash mode.
 *
 * @return {String}
 */
proto.getFlashMode = function() {
  var index = this.flash.current;
  return this.flash.available[index];
};

/**
 * Sets the current flash mode,
 * both on the Camera instance
 * and on the cameraObj hardware.
 *
 * @param {Number} index
 */
proto.setFlashMode = function(index) {
  var name = this.flash.available[index];
  this._cameraObj.flashMode = name;
  this.flash.current = index;
};

/**
 * States if the current
 * device has a front camera.
 *
 * @return {Boolean}
 */
proto.hasFrontCamera = function() {
  return this.numCameras > 1;
};

proto.setFocusMode = function() {
  this._callAutoFocus = false;

  // Camera
  if (this.isCameraMode()) {
    if (this._autoFocusSupport[FOCUS_MODE_TYPE.CONTINUOUS_CAMERA]) {
      this._cameraObj.focusMode = FOCUS_MODE_TYPE.CONTINUOUS_CAMERA;
      return;
    }

  // Video
  } else {
    if (this._autoFocusSupport[FOCUS_MODE_TYPE.CONTINUOUS_VIDEO]) {
      this._cameraObj.focusMode = FOCUS_MODE_TYPE.CONTINUOUS_VIDEO;
      return;
    }
  }

  if (this._autoFocusSupport[FOCUS_MODE_TYPE.MANUALLY_TRIGGERED]) {
    this._cameraObj.focusMode = FOCUS_MODE_TYPE.MANUALLY_TRIGGERED;
    this._callAutoFocus = true;
  }
};

/**
 * Takes a photo, or begins/ends
 * a video capture session.
 *
 * Options:
 *
 *   - `position` {Object} - geolocation to store in EXIF
 *
 * @param  {Object} options
 *  public
 */
proto.capture = function(options) {
  var self = this;

  // Camera
  if (this.isCameraMode()) {
    this.prepareTakePicture(function() {
      self.takePicture(options);
    });
  }

  // Video (stop)
  else if (this.state.get('recording')) {
    this.stopRecording();
  }

  // Video (start)
  else {
    this.startRecording();
  }
};

proto.startRecording = function() {
  var self = this;

  this._sizeLimitAlertActive = false;

  dcf.createDCFFilename(
    this._videoStorage,
    'video',
    onFileNameCreated);

  function onFileNameCreated(path, name) {
    self._videoPath = path + name;

    // The CameraControl API will not automatically create directories
    // for the new file if they do not exist, so write a dummy file
    // to the same directory via DeviceStorage to ensure that the directory
    // exists before recording starts.
    var dummyblob = new Blob([''], {type: 'video/3gpp'});
    var dummyfilename = path + '.' + name;
    var req = self._videoStorage.addNamed(dummyblob, dummyfilename);

    req.onerror = onError;
    req.onsuccess = function(e) {

      // Extract video
      // root directory string
      var absolutePath = e.target.result;
      var rootDirLength = absolutePath.length - dummyfilename.length;
      self._videoRootDir = absolutePath.substring(0, rootDirLength);

      // No need to wait for success
      self._videoStorage.delete(absolutePath);

      // Determine the number
      // of bytes available on disk.
      var spaceReq = self._videoStorage.freeSpace();
      spaceReq.onerror = onError;
      spaceReq.onsuccess = function() {
        startRecording(spaceReq.result);
      };
    };
  }

  function onError() {
    var id = 'error-recording';
    alert(
      navigator.mozL10n.get(id + '-title') + '. ' +
      navigator.mozL10n.get(id + '-text'));
  }

  function onSuccess() {
    self.state.set('recording', true);
    self.startRecordingTimer();

    // User closed app while
    // recording was trying to start
    if (document.hidden) {
      self.stopRecording();
    }

    // If the duration is too short,
    // the nno track may have been recorded.
    // That creates corrupted video files.
    // Because media file needs some samples.
    //
    // To have more information on video track,
    // we wait for 500ms to have few video and
    // audio samples, see bug 899864.
    window.setTimeout(function() {

      // TODO: Disable then re-enable
      // capture button after 500ms

    }, MIN_RECORDING_TIME);
  }

  function startRecording(freeBytes) {
    if (freeBytes < RECORD_SPACE_MIN) {
      onError('nospace');
      return;
    }

    var pickData = self._pendingPick && self._pendingPick.source.data;
    var maxFileSizeBytes = pickData && pickData.maxFileSizeBytes;
    var config = {
      rotation: orientation.get(),
      maxFileSizeBytes: freeBytes - RECORD_SPACE_PADDING
    };

    // If this camera session was
    // instantiated by a 'pick' activity,
    // it may have specified a maximum
    // file size. If so, use it.
    if (maxFileSizeBytes) {
      config.maxFileSizeBytes = Math.min(
        config.maxFileSizeBytes,
        maxFileSizeBytes);
    }

    // Finally begin recording
    self._cameraObj.startRecording(
      config,
      self._videoStorage,
      self._videoPath,
      onSuccess,
      onError);

    self.emit('recordingstart');
  }
};

/**
 * Sets a start time and begins
 * updating the elapsed time
 * every second.
 *
 */
proto.startRecordingTimer = function() {
  this.state.set('videoStart', new Date().getTime());
  this.videoTimer = setInterval(this.updateVideoElapsed, 1000);
  this.updateVideoElapsed();
};

/**
 * Calculates the elapse time
 * and updateds the 'videoElapsed'
 * value.
 *
 * We listen for the 'change:'
 * event emitted elsewhere to
 * update the UI accordingly.
 *
 */
proto.updateVideoElapsed = function() {
  var now = new Date().getTime();
  var start = this.state.get('videoStart');
  this.state.set('videoElapsed', (now - start));
};

proto.stopRecording = function() {
  var filename = this._videoRootDir + this._videoPath;
  var videoStorage = this._videoStorage;
  var self = this;

  this._cameraObj.stopRecording();
  this.state.set('recording', false);
  clearInterval(this.videoTimer);
  this.emit('recordingend');

  // Register a listener for writing
  // completion of current video file
  videoStorage.addEventListener('change', onVideoStorageChange);

  function onVideoStorageChange(e) {

    // Regard the modification as
    // video file writing completion
    // if e.path matches current video
    // filename. Note e.path is absolute path.
    if (e.reason === 'modified' && e.path === filename) {
      // Un-register the listener
      videoStorage.removeEventListener('change', onVideoStorageChange);
      getBlobFromStorage();
    }
  }

  function getBlobFromStorage() {
    var req = videoStorage.get(filename);

    req.onsuccess = onSuccess;
    req.onerror = onError;

    function onSuccess() {
      var blob = req.result;

      createVideoPosterImage(blob, filename, function(err, data) {
        if (err) {
          // We need to delete all corrupted
          // video files, those of them may be
          // tracks without samples (Bug 899864).
          self._videoStorage.delete(filename);
          return;
        }

        self.emit('newvideo', {
          blob: blob,
          filename: filename,
          poster: data.poster,
          width: data.width,
          height: data.height,
          rotation: data.rotation
        });
      });
    }

    function onError() {
      console.warn('getBlobFromStorage:', filename);
    }
  }
};

/**
 * Loads a camera stream
 * into a given video element.
 *
 * @param  {Element}   videoEl
 * @param  {Function} done
 */
proto.loadStreamInto = function(videoEl, done) {
  var cameraNumber = this.state.get('cameraNumber');
  var self = this;

  this.loadCameraPreview(cameraNumber, function(stream) {
    videoEl.mozSrcObject = stream;

    // Even though we have the stream now,
    // the camera hardware hasn't started
    // displaying it yet.
    //
    // We need to wait until the preview
    // has actually started displaying
    // before calling the callback.
    //
    // Bug 890427.
    self._cameraObj.onPreviewStateChange = function(state) {
      if (state === 'started') {
        self._cameraObj.onPreviewStateChange = null;
        done();
      }
    };
  });
};

proto.loadCameraPreview = function(cameraNumber, callback) {
  var mozCameras = navigator.mozCameras;
  var cameras = mozCameras.getListOfCameras();
  var self = this;

  // Store camera count
  this.numCameras = cameras.length;

  function gotPreviewScreen(stream) {
    self.state.set('previewActive', true);

    if (callback) {
      callback(stream);
    }
  }

  function gotCamera(camera) {
    var availableThumbnailSizes = camera.capabilities.thumbnailSizes;
    var focusModes = camera.capabilities.focusModes;
    var autoFocusSupported = !!~focusModes.indexOf('auto');
    var thumbnailSize;

    // Store the Gecko
    // camera interface
    self._cameraObj = camera;

    self.state.set('autoFocusSupported', autoFocusSupported);
    self.pickPictureSize(camera);

    thumbnailSize = self.selectThumbnailSize(
      availableThumbnailSizes,
      self._pictureSize);

    if (thumbnailSize) {
      camera.thumbnailSize = thumbnailSize;
    }

    self.getPreferredSizes(function() {
      var recorderProfiles = camera.capabilities.recorderProfiles;
      var videoProfile = self.pickVideoProfile(recorderProfiles);

      // 'Video' Mode
      if (self.isVideoMode()) {
        videoProfile.rotation = orientation.get();
        camera.getPreviewStreamVideoMode(videoProfile, gotPreviewScreen);
      }
    });

    self.enableCameraFeatures(camera.capabilities);

    camera.onShutter = function() {
      self.emit('shutter');
    };

    // 'Camera' Mode
    if (self.isCameraMode()) {
      camera.getPreviewStream(
        self._previewConfig,
        gotPreviewScreen.bind(self));
    }

    // This allows viewfinder to update
    // the size of the video element.
    self.emit('cameraChange', camera);
  }

  // If there is already a
  // camera, we would have
  // to release it first.
  if (this._cameraObj) {
    this.release(getCamera);
  } else {
    getCamera();
  }

  function getCamera() {
    var config = { camera: cameras[cameraNumber] };
    navigator.mozCameras.getCamera(config, gotCamera);
  }
};

proto.recordingStateChanged = function(msg) {
  if (msg === 'FileSizeLimitReached' && !this.sizeLimitAlertActive) {
    this.stopRecording();
    this.sizeLimitAlertActive = true;
    var alertText = this._pendingPick ? 'activity-size-limit-reached' :
      'storage-size-limit-reached';
    alert(navigator.mozL10n.get(alertText));
    this.sizeLimitAlertActive = false;
  }
};

proto.configureFlashModes = function(allModes) {
  this.flash.all = allModes || [];

  var cameraMode = this.getMode();
  var config = this.flash.config[cameraMode];
  var supported = config.supports;
  var index;

  this.flash.available = this.flash.all.filter(function(mode) {
    return !!~supported.indexOf(mode);
  });

  // Decide on the initial mode
  index = this.flash.available.indexOf(config.defaultMode);
  if (!~index) { index = 0; }

  this.setFlashMode(index);
};

proto.configureFocusModes = function(focusModes) {
  if (!focusModes) {
    return;
  }

  var MANUALLY_TRIGGERED = FOCUS_MODE_TYPE.MANUALLY_TRIGGERED;
  var CONTINUOUS_CAMERA = FOCUS_MODE_TYPE.CONTINUOUS_CAMERA;
  var CONTINUOUS_VIDEO = FOCUS_MODE_TYPE.CONTINUOUS_VIDEO;
  var support = this._autoFocusSupport;

  support[MANUALLY_TRIGGERED] = !!~focusModes.indexOf(MANUALLY_TRIGGERED);
  support[CONTINUOUS_CAMERA] = !!~focusModes.indexOf(CONTINUOUS_CAMERA);
  support[CONTINUOUS_VIDEO] = !!~focusModes.indexOf(CONTINUOUS_VIDEO);
};

proto.enableCameraFeatures = function(capabilities) {
  this.configureFlashModes(capabilities.flashModes);
  this.configureFocusModes(capabilities.focusModes);
  this.emit('configured');
};

proto.startPreview = function() {
  var cameraNumber = this.state.get('cameraNumber');
  this.loadCameraPreview(cameraNumber, null);
};

proto.resumePreview = function() {
  this._cameraObj.resumePreview();
  this.state.set('previewActive', true);
  this.emit('previewResumed');
};

proto.takePictureError = function() {
  alert(
    navigator.mozL10n.get('error-saving-title') + '. ' +
    navigator.mozL10n.get('error-saving-text'));
};

proto.takePictureSuccess = function(blob) {
  this.state.set({
    manuallyFocused: false,
    focusState: 'none'
  });

  this.emit('newimage', { blob: blob });
};

proto._addPictureToStorage = function(blob, callback) {
  var self = this;

  dcf.createDCFFilename(
    this._pictureStorage,
    'image',
    onFilenameCreated);

  function onFilenameCreated(path, name) {
    var addreq = self._pictureStorage.addNamed(blob, path + name);

    addreq.onerror = self.takePictureError;
    addreq.onsuccess = function(e) {
      var absolutePath = e.target.result;
      callback(path + name, absolutePath);
    };
  }
};

proto._resizeBlobIfNeeded = function(blob, callback) {
  var pickData = this._pendingPick.source.data;
  var pickWidth = pickData.width;
  var pickHeight = pickData.height;

  if (!pickWidth || !pickHeight) {
    callback(blob);
    return;
  }

  var img = new Image();
  img.onload = function resizeImg() {
    var canvas = document.createElement('canvas');
    canvas.width = pickWidth;
    canvas.height = pickHeight;
    var ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, pickWidth, pickHeight);
    canvas.toBlob(function toBlobSuccess(resized_blob) {
      callback(resized_blob);
    }, 'image/jpeg');
  };
  img.src = window.URL.createObjectURL(blob);
};

proto.storageCheck = function(done) {
  done = done || function() {};

  var self = this;

  this.getDeviceStorageState(function(result) {
    self.setStorageState(result);
    if (!self.storageAvailable()) {
      return done();
    }

    self.isSpaceOnStorage(function(result) {
      if (!result) {
        self.setStorageState('nospace');
      }
      done();
    });
  });
};

proto.getDeviceStorageState = function(done) {
  var req = this._pictureStorage.available();
  req.onsuccess = function(e) {
    done(e.target.result);
  };
};

proto.storageAvailable = function() {
  return this.state.get('storage') === 'available';
};

proto.isSpaceOnStorage = function(done) {
  var pictureWidth = this._pictureSize.width;
  var pictureHeight = this._pictureSize.height;
  var MAX_IMAGE_SIZE = (pictureWidth * pictureHeight * 4) + 4096;
  var req = this._pictureStorage.freeSpace();
  req.onsuccess = function(e) {
    var freeSpace = e.target.result;
    done(freeSpace > MAX_IMAGE_SIZE);
  };
};

proto.onStorageChange = function(e) {
  var value = e.reason;

  // Remove filmstrip item if its
  // correspondent file is deleted
  if (value === 'deleted') {
    broadcast.emit('itemDeleted', { path: e.path });
  } else {
    this.setStorageState(value);
  }

  // Check storage
  // has spare capacity
  this.storageCheck();
};

proto.setStorageState = function(value) {
  this.state.set('storage', value);
};

proto.prepareTakePicture = function(done) {
  var self = this;

  this.emit('preparingToTakePicture');

  if (!this._autoFocusSupport[FOCUS_MODE_TYPE.MANUALLY_TRIGGERED]) {
    done();
    return;
  }

  this.state.set('focusState', 'focusing');
  this._cameraObj.autoFocus(function(success) {
    if (!success) {
      self.state.set('focusState', 'fail');
      self.emit('focusFailed');

      window.setTimeout(function() {
        self.state.set('focusState', 'none');
      }, 1000);

      return;
    }

    self.state.set('focusState', 'focused');
    done();
  });
};

proto.takePicture = function(options) {
  var position = options && options.position;
  var config = {
    rotation: orientation.get(),
    dateTime: Date.now() / 1000,
    fileFormat: this.fileFormat
  };

  // If position has been
  // passed in, add it to
  // the config object.
  if (position) {
    config.position = position;
  }

  this._cameraObj.pictureSize = this._pictureSize;

  this._cameraObj.takePicture(
    config,
    this.takePictureSuccess.bind(this),
    this.takePictureError);
};

proto.selectThumbnailSize = function(thumbnailSizes, pictureSize) {
  var screenWidth = window.innerWidth * window.devicePixelRatio;
  var screenHeight = window.innerHeight * window.devicePixelRatio;
  var pictureAspectRatio = pictureSize.width / pictureSize.height;
  var currentThumbnailSize;
  var i;

  // Coping the array to not modify the original
  thumbnailSizes = thumbnailSizes.slice(0);
  if (!thumbnailSizes || !pictureSize) {
    return;
  }

  function imageSizeFillsScreen(pixelsWidth, pixelsHeight) {
    return ((pixelsWidth >= screenWidth || // portrait
             pixelsHeight >= screenHeight) &&
            (pixelsWidth >= screenHeight || // landscape
             pixelsHeight >= screenWidth));
  }

  // Removes the sizes with the wrong aspect ratio
  thumbnailSizes = thumbnailSizes.filter(function(thumbnailSize) {
    var thumbnailAspectRatio = thumbnailSize.width / thumbnailSize.height;
    return Math.abs(thumbnailAspectRatio - pictureAspectRatio) < 0.05;
  });

  if (thumbnailSizes.length === 0) {
    console.error('Error while selecting thumbnail size. ' +
      'There are no thumbnail sizes that match the ratio of ' +
      'the selected picture size: ' + JSON.stringify(pictureSize));
    return;
  }

  // Sorting the array from smaller to larger sizes
  thumbnailSizes.sort(function(a, b) {
    return a.width * a.height - b.width * b.height;
  });

  for (i = 0; i < thumbnailSizes.length; ++i) {
    currentThumbnailSize = thumbnailSizes[i];
    if (imageSizeFillsScreen(currentThumbnailSize.width,
                             currentThumbnailSize.height)) {
      return currentThumbnailSize;
    }
  }

  return thumbnailSizes[thumbnailSizes.length - 1];
};

proto.pickPictureSize = function(camera) {
  var targetSize = null;
  var targetFileSize = 0;
  var pictureSizes = camera.capabilities.pictureSizes;

  if (this._pendingPick && this._pendingPick.source.data.maxFileSizeBytes) {

    // We use worse case of all
    // compression method: gif, jpg, png
    targetFileSize = this._pendingPick.source.data.maxFileSizeBytes;
  }

  if (this._pendingPick && this._pendingPick.source.data.width &&
      this._pendingPick.source.data.height) {

    // if we have pendingPick
    // with width and height,
    // set it as target size.
    targetSize = {
      width: this._pendingPick.source.data.width,
      height: this._pendingPick.source.data.height
    };
  }

  // CONFIG_MAX_IMAGE_PIXEL_SIZE is
  // maximum image resolution for still
  // photos taken with camera.
  //
  // It's from config.js which is
  // generatedin build time, 5 megapixels
  // by default (see build/application-data.js).
  // It should be synced with Gallery app
  // and update carefully.
  var maxRes = CONFIG_MAX_IMAGE_PIXEL_SIZE;
  var size = pictureSizes.reduce(function(acc, size) {
    var mp = size.width * size.height;

    // we don't need the
    // resolution larger
    // than maxRes
    if (mp > maxRes) {
      return acc;
    }

    // We assume the relationship
    // between MP to file size is
    // linear. This may be
    // inaccurate on all cases.
    var estimatedFileSize = mp * ESTIMATED_JPEG_FILE_SIZE / maxRes;
    if (targetFileSize > 0 && estimatedFileSize > targetFileSize) {
      return acc;
    }

    if (targetSize) {

      // find a resolution both width
      // and height are large than pick size
      if (size.width < targetSize.width || size.height < targetSize.height) {
        return acc;
      }

      // it's first pictureSize.
      if (!acc.width || acc.height) {
        return size;
      }

      // find large enough but
      // as small as possible.
      return (mp < acc.width * acc.height) ? size : acc;
    } else {

      // no target size, find
      // as large as possible.
      return (mp > acc.width * acc.height && mp <= maxRes) ? size : acc;
    }
  }, {width: 0, height: 0});

  if (size.width === 0 && size.height === 0) {
    this._pictureSize = pictureSizes[0];
  } else {
    this._pictureSize = size;
  }
};

proto.pickVideoProfile = function(profiles) {
  var matchedProfileName;
  var profileName;

  if (this.preferredRecordingSizes) {
    for (var i = 0; i < this.preferredRecordingSizes.length; i++) {
      if (this.preferredRecordingSizes[i] in profiles) {
        matchedProfileName = this.preferredRecordingSizes[i];
        break;
      }
    }
  }

  // Attempt to find low resolution profile if accessed via pick activity
  if (this._pendingPick && this._pendingPick.source.data.maxFileSizeBytes &&
      'qcif' in profiles) {
    profileName = 'qcif';
  } else if (matchedProfileName) {
    profileName = matchedProfileName;
  // Default to cif profile
  } else if ('cif' in profiles) {
    profileName = 'cif';
  // Fallback to first valid profile if none found
  } else {
    profileName = Object.keys(profiles)[0];
  }

  return {
    profile: profileName,
    rotation: 0,
    width: profiles[profileName].video.width,
    height: profiles[profileName].video.height
  };
};

/**
 * Releases the camera hardware.
 *
 * @param  {Function} done
 *
 */
proto.release = function(done) {
  done = done || function() {};

  if (!this._cameraObj) {
    return;
  }

  var self = this;
  this._cameraObj.release(onSuccess, onError);

  function onSuccess() {
    self._cameraObj = null;
    done();
  }

  function onError() {
    console.warn('Camera: failed to release hardware?');
    done();
  }
};

proto.getPreferredSizes = function(done) {
  done = done || function() {};

  var key = 'camera.recording.preferredSizes';
  var self = this;

  if (this.preferredRecordingSizes) {
    done(this.preferredRecordingSizes);
    return;
  }

  var req = navigator.mozSettings.createLock().get(key);
  req.onsuccess = function() {
    self.preferredRecordingSizes = req.result[key] || [];
    done(self.preferredRecordingSizes);
  };
};

});