VulpesOS/gaia/apps/camera/js/controllers/activity.js
OverSu 1311bed5e9 Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
2026-10-13 00:19:45 +02:00

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 = media.blob.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();
}
);
});
}
};
});