Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
308 lines
7.6 KiB
JavaScript
Executable file
308 lines
7.6 KiB
JavaScript
Executable file
define(function(require, exports, module) {
|
|
'use strict';
|
|
|
|
/**
|
|
* Dependencies
|
|
*/
|
|
|
|
var debug = require('debug')('controller:activity');
|
|
var bytesToPixels = require('lib/bytes-to-pixels');
|
|
var bindAll = require('lib/bind-all');
|
|
|
|
/**
|
|
* Exports
|
|
*/
|
|
|
|
module.exports = function(app) { return new ActivityController(app); };
|
|
module.exports.ActivityController = ActivityController;
|
|
|
|
/**
|
|
* Initialize new `ActivityController`
|
|
*
|
|
* @param {App} app
|
|
*/
|
|
function ActivityController(app) {
|
|
bindAll(this);
|
|
this.app = app;
|
|
this.win = app.win;
|
|
this.settings = app.settings;
|
|
this.configure();
|
|
this.bindEvents();
|
|
debug('initialized');
|
|
}
|
|
|
|
/**
|
|
* Supported activity types.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
ActivityController.prototype.types = {
|
|
pick: 'pick',
|
|
record: 'record'
|
|
};
|
|
|
|
/**
|
|
* Initial configuration.
|
|
*
|
|
* @private
|
|
*/
|
|
ActivityController.prototype.configure = function() {
|
|
this.name = this.getName();
|
|
this.app.activity[this.name] = true;
|
|
};
|
|
|
|
/**
|
|
* Filter down pictureSizes and
|
|
* recorderProfiles to match activity
|
|
* parameters each time the settings
|
|
* are configured.
|
|
*
|
|
* @private
|
|
*/
|
|
ActivityController.prototype.bindEvents = function() {
|
|
this.app.on('activitycanceled', this.onActivityCanceled);
|
|
this.app.on('confirm:selected', this.onActivityConfirmed);
|
|
|
|
// If an activity name was found, we must bind
|
|
// the listener straight away so we don't miss
|
|
// the event, otherwise we can bind more lazily.
|
|
if (this.name) { this.setupListener(); }
|
|
else { this.app.once('criticalpathdone', this.setupListener); }
|
|
};
|
|
|
|
ActivityController.prototype.setupListener = function() {
|
|
debug('setup listener');
|
|
navigator.mozSetMessageHandler('activity', this.onMessage);
|
|
debug('listener setup');
|
|
};
|
|
|
|
/**
|
|
* Get activity name from the hash fragment.
|
|
*
|
|
* This is used only so that some parts
|
|
* of the app can prepare for an incoming
|
|
* activity before the message arrives.
|
|
*
|
|
* @private
|
|
*/
|
|
ActivityController.prototype.getName = function() {
|
|
var hash = this.win.location.hash;
|
|
var name = hash && hash.substr(1);
|
|
return this.types[name];
|
|
};
|
|
|
|
/**
|
|
* Responds to incoming activity message.
|
|
*
|
|
* Configures the mode then emits an
|
|
* event so that other parts of
|
|
* the app can update accordingly.
|
|
*
|
|
* @param {MozActivity} activity
|
|
*/
|
|
ActivityController.prototype.onMessage = function(activity) {
|
|
debug('incoming activity', activity);
|
|
var name = activity.source.name;
|
|
var supported = this.types[name];
|
|
|
|
// Exit if this activity isn't supported
|
|
if (!supported) { return; }
|
|
|
|
var data = {
|
|
name: name,
|
|
maxPixelSize: this.getMaxPixelSize(activity),
|
|
maxFileSizeBytes: activity.source.data.maxFileSizeBytes
|
|
};
|
|
|
|
this.activity = activity;
|
|
this.configureMode(activity);
|
|
this.app.emit('activity', data);
|
|
this.app.emit('activity:' + name, data);
|
|
};
|
|
|
|
/**
|
|
* Configures the mode setting based
|
|
* on the parameters supplied by
|
|
* the incoming activity.
|
|
*
|
|
* @param {MozActivity} activity
|
|
* @private
|
|
*/
|
|
ActivityController.prototype.configureMode = function(activity) {
|
|
var type = activity.source.data.type;
|
|
var name = activity.source.name;
|
|
var modes = (name === 'pick') ?
|
|
this.getModesForPick(type) :
|
|
this.getModesForRecord(type);
|
|
|
|
this.settings.mode.filterOptions(modes);
|
|
this.settings.mode.select(modes[0]);
|
|
debug('configured mode', modes);
|
|
};
|
|
|
|
/**
|
|
* Get a max pixel size estimate based
|
|
* on the parameters supplied by the
|
|
* incoming activity.
|
|
*
|
|
* Activities don't always supply pixel
|
|
* size restrictions.
|
|
*
|
|
* NOTE: There
|
|
*
|
|
* @param {MozActivity} activity
|
|
* @return {Number|null}
|
|
*/
|
|
ActivityController.prototype.getMaxPixelSize = function(activity) {
|
|
var data = activity.source.data;
|
|
var bytes = data.maxFileSizeBytes;
|
|
var maxPickPixelSize = this.settings.activity.get('maxPickPixelSize') || 0;
|
|
var maxPixelSize;
|
|
|
|
// If bytes were specified then derive
|
|
// a maxPixelSize from that, else we
|
|
// calculate the maxPixelSize using
|
|
// supplied dimensions.
|
|
if (bytes) {
|
|
maxPixelSize = bytesToPixels(bytes);
|
|
} else if (data.width || data.height) {
|
|
maxPixelSize = this.getMaxPixelsFromSize(data);
|
|
} else {
|
|
maxPixelSize = maxPickPixelSize;
|
|
}
|
|
|
|
// If the Camera app has been configured to have a max pixel size
|
|
// for pick activities, ensure we are at or below that value.
|
|
if (maxPickPixelSize > 0) {
|
|
maxPixelSize = Math.min(maxPixelSize, maxPickPixelSize);
|
|
}
|
|
|
|
debug('maxPixelsSize: %s', maxPixelSize);
|
|
return maxPixelSize;
|
|
};
|
|
|
|
ActivityController.prototype.getMaxPixelsFromSize = function(size) {
|
|
var scale = this.settings.activity.get('maxPixelSizeScaleFactor');
|
|
var aspect = 4 / 3;
|
|
|
|
// In the event that only one dimension has
|
|
// been supplied, calculate the largest the
|
|
// other edge could be based on a 4:3 aspect.
|
|
var width = size.width || size.height * aspect;
|
|
var height = size.height || size.width * aspect;
|
|
var pixels = width * height;
|
|
|
|
// Take the actual total number of
|
|
// pixels asked for and bump it by the
|
|
// `scale` to cover pictureSizes above
|
|
// the pixels asked for. We later resize
|
|
// the image post-capture to the exact size
|
|
// requested (data.width * data.height).
|
|
//
|
|
// This is to avoid us picking a pictureSize
|
|
// smaller than the number of pixels requested
|
|
// and then to scaling up post-capture,
|
|
// resulting in a shitty image.
|
|
return pixels * scale;
|
|
};
|
|
|
|
/**
|
|
* Parse types given by 'pick' activity
|
|
* and return a list of modes.
|
|
*
|
|
* @param {Array|String} types
|
|
* @return {Array}
|
|
*/
|
|
ActivityController.prototype.getModesForPick = function(types) {
|
|
types = [].concat(types || []);
|
|
var modes = [];
|
|
|
|
types.forEach(function(item) {
|
|
var type = item.split('/')[0];
|
|
var mode = type === 'image' ? 'picture' : type;
|
|
|
|
if (modes.indexOf(mode) === -1) {
|
|
modes.push(mode);
|
|
}
|
|
});
|
|
|
|
if (modes.length === 0) {
|
|
modes = ['picture', 'video'];
|
|
}
|
|
|
|
return modes;
|
|
};
|
|
|
|
/**
|
|
* Parse types given by 'record' activity
|
|
* and return a list of modes.
|
|
*
|
|
* @param {String} type
|
|
* @return {Array}
|
|
*/
|
|
ActivityController.prototype.getModesForRecord = function(type) {
|
|
return type === 'videos' ?
|
|
['video', 'picture'] :
|
|
['picture', 'video'];
|
|
};
|
|
|
|
/**
|
|
* Respond to activity cancelation
|
|
* events and send the error call
|
|
* via the original acitity object.
|
|
*
|
|
* @private
|
|
*/
|
|
ActivityController.prototype.onActivityCanceled = function() {
|
|
if (!this.activity) { return; }
|
|
this.activity.postError('pick cancelled');
|
|
};
|
|
|
|
// TODO: Messy, tidy up!
|
|
ActivityController.prototype.onActivityConfirmed = function(newMedia) {
|
|
var self = this;
|
|
var activity = this.activity;
|
|
var media = {
|
|
blob: newMedia.blob
|
|
};
|
|
|
|
// In low end devices resizing can be slow.
|
|
// We display a spinner
|
|
this.app.showSpinner();
|
|
|
|
// Video
|
|
if (newMedia.isVideo) {
|
|
media.type = 'video/3gpp';
|
|
media.poster = newMedia.poster.blob;
|
|
activity.postResult(media);
|
|
this.app.clearSpinner();
|
|
}
|
|
|
|
// Image
|
|
else {
|
|
media.type = 'image/jpeg';
|
|
|
|
// We need to call resizeImageAndSave() unconditionally even if
|
|
// the image does not need to be resized because if the camera
|
|
// produces images with EXIF orientation, we need to rotate the
|
|
// image before returning it from the pick activity. If the image
|
|
// does not need to be resized or rotated, then resizeImageAndSave
|
|
// will return it unchanged, and will not re-save it.
|
|
require(['lib/resize-image-and-save'], function(resizeImageAndSave) {
|
|
resizeImageAndSave(
|
|
{
|
|
blob: newMedia.blob,
|
|
width: activity.source.data.width,
|
|
height: activity.source.data.height
|
|
},
|
|
function onImageResized(resizedBlob) {
|
|
media.blob = resizedBlob;
|
|
activity.postResult(media);
|
|
self.app.clearSpinner();
|
|
}
|
|
);
|
|
});
|
|
}
|
|
};
|
|
|
|
});
|