Skip to content

Instantly share code, notes, and snippets.

@sharpred
Created October 16, 2012 08:11
Show Gist options
  • Save sharpred/3897974 to your computer and use it in GitHub Desktop.
Save sharpred/3897974 to your computer and use it in GitHub Desktop.
background service
/*global L */
/*jslint nomen: true, sloppy : true, plusplus: true, vars: true, newcap: true*/
/**
* Background Services. File and data uploads as well as claim status updates are all processed
* using the background services. They feed off items written to the dataqueue table by the UI
* during normal operation. The background service is sandboxed from the main app therefore there is
* a degree of code duplication here with copies of functions used elsewhere in the app duplicated here.
* @module bgService
*/
//TODO refactor all these helper functions as commonJS so we can remove all the duplicated code.
Ti.include("/3rdparty/libs/joli.js", "/cust/model/models.js");
var dbname = Titanium.App.Properties.getString('db');
var _ = require('3rdparty/libs/underscore')._;
var now;
var version = Titanium.App.getVersion();
var keychain, user, pass, credentials;
var nickname = Titanium.App.Properties.getString('nickname');
joli.connection = new joli.SecureConnection(dbname);
joli.models.initialize();
Ti.App.fireEvent('debugevent', {
value : 'BG service is about to start'
});
/**
* The notification code here only runs when the app is sent to background
*/
var q = new joli.query().count().from('dataqueue').where('status=?', 'failed');
var failedCount = q.execute();
q = new joli.query().count().from('dataqueue').where('status=?', 'pending');
var pendingCount = q.execute();
q = null;
var total = pendingCount + failedCount;
pendingCount = null;
failedCount = null;
now = new Date();
if (total && total > 0) {
var notification = Ti.App.iOS.scheduleLocalNotification({
alertBody : "App was closed with pending uploads",
alertAction : "Re-Open",
userInfo : {
"relaunch" : true
},
badge : total,
date : now.setMinutes(now.getMinutes() + 1)
});
Titanium.UI.iPhone.setAppBadge(total);
}
now = null;
/**
* set to true or false by the connect function to ensure that connect does not flood the remote server
* with connection requests, especially files. There are two threads of upload activity, one for data and
* one for files. uploadInProgress status of true or false ensures there are only ever one of each active
* at any one point.
* @property uploadInProgress
* @type Boolean
* @default false
*/
var uploadInProgress = false;
/**
* Sets all data in the dataqueue to pending so that they are all uploaded by the background service.
* @method reload
*/
var reload = function() {
/**
* Causes all items in the dataqueue to be set to pending status and thus automatically uploaded to the
* server again. Only works in the simulator.
* @property reload
* @type Bool
*/
Titanium.App.Properties.setBool('reload', false);
var reloadcheck = Titanium.App.Properties.getBool('reload');
var data = {};
data.status = 'pending';
if (reloadcheck && Titanium.Platform.model === 'Simulator') {
var q = new joli.query().update('dataqueue').set(data);
var update = q.execute();
Ti.App.fireEvent('debugevent', {
value : 'reloading uploads'
});
}
};
/**
* Returns the server URL as pushed to device and stored in titanium properties. Defaults to current BVS production server
* if property not set.
* @method getServerURL
* @return {String} the server URL
*/
//REFACTOR this is duplicated code. Refactor original function and remove this one.
var getServerURL = function() {
var url = Titanium.App.Properties.getString('url');
if (url) {
return url;
}
return 'https://stepup.bvs-metrix.co.uk';
};
/**
* Returns the username as pushed to the device and stored in titanium properties. Defaults to 'remoteuser'
* if property not set.
* @method getUserName
* @return {String} the server URL
*/
//REFACTOR this is duplicated code. Refactor original function and remove this one.
var getUserName = function() {
var username = Titanium.App.Properties.getString('username');
if (username) {
return username;
}
return "remoteuser";
};
/**
* The background service
* @class bgService
*/
var bgService = function() {
var cust = {};
var counter = 0;
var url = getServerURL();
Ti.App.fireEvent('debugevent', {
value : 'uploading to ' + url
});
keychain = require('com.0x82.key.chain');
Ti.API.info("module is => " + keychain);
reload();
//reloads files in the simulator for testing uploads
var claimid;
/**
* The database update service
* @method updateDB
* @return a response object from the Joli function used to update the database
*/
//REFACTOR this is duplicated code. Refactor original function and remove this one.
var updateDB = function(_args) {
Ti.App.fireEvent('debugevent', {
value : "updateDB " + _args.json
});
var data = JSON.parse(_args.json);
var form = data.formid;
//remove formid as it is not a db field
delete data.formid;
if (!data.status) {
data.status = 'updated';
}
var update;
// arrival model
var q;
// joli query
if (data.id === "") {
delete data.id;
//delete data.undefined;
q = new joli.query().insertInto(form).values(data);
update = q.execute();
Ti.App.fireEvent('debugevent', {
value : 'New ' + form + ' Record Added: ' + update
});
} else {
q = new joli.query().update(form).set(data).where('id =?', data.id);
update = q.execute();
Ti.App.fireEvent('debugevent', {
value : 'updating update for ' + data.id
});
}
return update;
};
/**
* The http connection function
* @method connect
* @creates and httpclient connection to upload files or data to the server
*/
var connect = function(args) {
uploadInProgress = true;
var request = Titanium.Network.createHTTPClient();
var data = {};
//used to populate object for updating dataqueue table for upload outcome
var authstr = 'Basic ' + Titanium.Utils.base64encode(args.usernamepassword);
data.id = args.id;
var claimid = args.claimid;
data.formid = 'dataqueue';
var updateProgress = false;
// used to determine whether to update claim progress (non sow upload)
var update = {};
var progressData = {};
var progressUpdate = {};
var claimstatus;
var id;
var callBack = function() {
try {
now = new Date().toString();
var status = this.status;
if ((status === 200) || (status === 201) || (status === 202) || (status === 302)) {
Ti.App.fireEvent('debugevent', {
value : 'upload succeeded: claim ' + claimid + ' id: ' + data.id + ' request status: ' + this.status + ' url: ' + args.url
});
data.status = 'uploaded';
updateProgress = true;
} else if (status === 403) {
Ti.App.fireEvent('debugevent', {
value : 'upload failed permission error: claim ' + claimid + ' id: ' + data.id + ' request status: ' + this.status + ' url: ' + args.url
});
data.status = 'failed';
updateProgress = true;
} else {
if (status === 404 && args.itemtype && args.itemtype === 'claim') {
/**
* Issue #93. A status of 404 for a claim edit is not an issue. It just means the claim has been removed, so no need to
* update the claim status, we just need to set the upload to true and remove it from the queue
*/
Ti.App.fireEvent('debugevent', {
value : 'claim not on server: claim ' + claimid + ' id: ' + data.id + ' request status: ' + this.status + ' url: ' + args.url
});
data.status = 'uploaded';
updateProgress = true;
} else {
Ti.App.fireEvent('debugevent', {
value : 'upload failed: claim ' + claimid + ' id: ' + data.id + ' request status: ' + this.status + ' url: ' + args.url
});
data.status = 'failed';
updateProgress = true;
}
}
// check the status of the claim
var q = new joli.query().select('claims.*').from('claims').where('claimid=?', claimid);
var result = q.execute();
var _id;
if (result[0]) {
claimstatus = result[0].status;
id = result[0].id;
_id = result[0]._id;
}
if (claimstatus === 'ready') {// ready means the form has been marked as completed. otherwise this is just an sow upload with no progress reporting
updateProgress = true;
} else {
updateProgress = false;
}
Ti.App.fireEvent('debugevent', {
value : 'claim status: claim ' + claimid + ' id: ' + data.id + ' status: ' + claimstatus
});
//update the progress of the claim in the claims table by looping through all the uploads
if (updateProgress) {
q = new joli.query().count().from('dataqueue').where('status=?', 'uploaded').where('claimid=?', claimid);
var uploaded = q.execute();
q = new joli.query().count().from('dataqueue').where('status=?', 'pending').where('claimid=?', claimid);
var pending = q.execute();
q = new joli.query().count().from('dataqueue').where('claimid=?', claimid);
var total = q.execute();
var progress = ((total - pending) / total * 100);
progressData.formid = 'claims';
progressData.id = id;
if (total > 0 && total === (uploaded + 1)) {// uploaded plus one as if it is the last upload would be one short otherwise
progressData.status = 'uploaded';
progress = 100;
Ti.App.fireEvent('claim.uploadComplete', {
'claimid' : claimid
});
} else if (total > 0 && total !== (uploaded + 1)) {
progressData.status = 'ready';
// need to set this to ready or updateDB will update to 'updated'
Ti.App.fireEvent('claim.uploadProgress', {
'claimid' : claimid,
progress : progress
});
}
Ti.App.fireEvent('debugevent', {
value : 'upload progress: claim ' + claimid + ' total: ' + total + ' progress: ' + progress
});
progressData.progress = progress;
progressUpdate.json = JSON.stringify(progressData);
//update claims table with new progress percentage (used by progress bar on schedule view)
updateDB(progressUpdate);
if (progress === 100) {
var updateDate = new Date();
var claimUpdate = {
_id : _id,
status : 'Uploaded',
modified : {
usec : (updateDate.getTime() / 1000), //mongo date is seconds since midnight getTime is milliseconds
sec : updateDate.getMilliseconds()
}
};
var uploadedData = {};
var uploadedUpdate = {};
uploadedData.id = "";
// work around for sus.updateDb
uploadedData.claimid = claimid;
uploadedData.data = JSON.stringify(claimUpdate);
uploadedData.status = 'pending';
uploadedData.formid = 'dataqueue';
uploadedData.type = 'claim';
uploadedUpdate.json = JSON.stringify(uploadedData);
// correct format for sus.updateDB
updateDB(uploadedUpdate);
var debugMessage = ('updating server for download of : ' + claimid);
Ti.App.fireEvent('debugevent', {
value : debugMessage
});
}
}
// update dataqueue table with results of upload (failed or uploaded)
update.json = JSON.stringify(data);
updateDB(update);
request = null;
// kill off the request just in case
uploadInProgress = false;
} catch (ex) {
request = null;
data.status = 'failed';
update.json = JSON.stringify(data);
updateDB(update);
uploadInProgress = false;
Ti.App.fireEvent('debugevent', {
value : 'update failed' + ex
});
}
};
try {
request.open(args.method, args.url, true);
request.setRequestHeader('Content-Type', args.content_type);
request.setRequestHeader('enctype', 'multipart/form-data');
request.validatesSecureCertificate = false;
if (args.authenticate === true) {
request.setRequestHeader('Authorization', authstr);
}
request.timeout = Titanium.App.Properties.getInt('httpTimeout') || 120000;
/* experimental two minute timeout*/
request.send(args.formdata);
/**
* request callback function handles the response object received by the server. Updates the
* database with response data and fires events to log error data when the upload fails
* @method onload
*
*/
request.onload = callBack;
request.onerror = callBack;
/**
* request state change callback function logs state data when uploads take a long time.
* @method onreadystatechange
*
*/
request.onreadystatechange = function() {
Ti.App.fireEvent('debugevent', {
value : claimid + ' upload, readystate changed to: ' + this.readyState
});
};
} catch (ex) {
Ti.App.fireEvent('debugevent', {
value : ex
});
Ti.App.fireEvent('claim.uploadError', {
'claimid' : claimid
});
Ti.App.fireEvent('debugevent', {
value : 'problem with: ' + claimid
});
}
};
/**
* Checks for presence of a network and if so, checks if there is any data to be uploaded. Calls the connect function if there
* is data to be uploaded
* @method uploadTransactionsFromQueue
* @param {Object} status
*/
var uploadTransactionsFromQueue = function(status) {
if (uploadInProgress) {
return;
}
var networkstatus = Titanium.Network.online;
var data;
var _id, i, l, item, items, claimid, id, formdata, conn;
try {
if (networkstatus === true) {
var q = new joli.query().select('dataqueue.*').from('dataqueue').where('status=?', status).order(['status desc', 'id asc']);
var result = q.execute();
if (result) {
// separate out data and files
items = _.groupBy(result, function(obj) {
return obj.type;
});
}
/**
* Items are grouped into data, files and claims. Data relates to information captured by the user on the device.
* Files relates to photos and dictation captured for a claim. Claim relates status updates for when the app
* has downloaded and processes new claims from the server. Data is always processed first and thus takes
* priority over files and claims data.
*/
if (items) {
if (items.data) {
Ti.App.fireEvent('debugevent', {
value : 'Pending/Failed data uploads: ' + items.data.length
});
if (items.data[0]) {
item = items.data[0]._data;
data = JSON.parse(item.data);
data.appversion = version;
item.data = data;
// we dont want to upload this as a json string we want it in an array so the data to be searchable in a mongo data cursor
item.devicepin = nickname;
/**
* issue #89: create an MD5 of the contents. PHP will check it before adding it.
*/
item.md5 = Titanium.Utils.md5HexDigest(JSON.stringify(item));
formdata = JSON.stringify(item);
claimid = item.claimid;
id = item.id;
Ti.App.fireEvent('debugevent', {
value : 'uploading ' + formdata
});
conn = connect({
method : 'POST',
url : url + '/BVSForms/api/uploads/add',
authenticate : true,
usernamepassword : credentials,
content_type : 'application/json',
formdata : formdata,
claimid : claimid,
id : id,
itemtype : 'data'
});
return;
}
}
if (items.claim) {
Ti.App.fireEvent('debugevent', {
value : 'Pending/Failed claim uploads: ' + items.claim.length
});
if (items.claim[0]) {
item = items.claim[0]._data;
data = JSON.parse(item.data);
_id = data._id;
item.data = data;
// we dont want to upload this as a json string we want it in an array so the data to be searchable in a mongo data cursor
formdata = JSON.stringify(item.data);
//different to data upload - we just want to change the status
claimid = item.claimid;
id = item.id;
Ti.App.fireEvent('debugevent', {
value : 'updating claim: ' + formdata
});
conn = connect({
method : 'POST',
url : url + '/BVSForms/api/claims/edit/' + _id,
authenticate : true,
usernamepassword : credentials,
content_type : 'application/json',
formdata : formdata,
claimid : claimid,
id : id,
itemtype : 'claim'
});
return;
}
}
if (items.file) {
Ti.App.fireEvent('debugevent', {
value : 'Pending/Failed file uploads: ' + items.file.length
});
if (items.file[0]) {
var form = items.file[0]._data;
claimid = form.claimid;
data = JSON.parse(form.data);
data.appversion = version;
delete form.data;
form.claimid = claimid;
var filename = Titanium.Filesystem.applicationDataDirectory + data.filename;
var uploadFile = Titanium.Filesystem.getFile(filename);
if (uploadFile.exists()) {
delete data.filename;
var content = uploadFile.read();
form.name = filename;
form.file = content;
// need to enclose it in quotes
content = null;
uploadFile = null;
//remove some extraneous guff from form data
delete form.status;
delete form.type;
delete form.name;
//add some relevant data
form.formtype = data.formtype;
form.fieldname = data.fieldname;
form.keyid = data.keyid;
form.description = data.description;
form.appversion = version;
if (data.orientation) {
form.orientation = data.orientation;
}
id = form.id;
form.devicepin = Titanium.App.Properties.getString('nickname');
conn = connect({
method : 'POST',
url : url + '/BVSForms/api/uploads/addFile',
authenticate : true,
usernamepassword : credentials,
Content_Type : 'multipart/form-data',
formdata : form,
claimid : claimid,
id : id,
itemtype : 'file'
});
} else {
data.filename = "MISSING: " + data.filename;
Ti.App.fireEvent('debugevent', {
value : 'file missing before upload' + data.filename
});
var updatejson = {};
updatejson.id = form.id;
updatejson.claimid = form.claimid;
updatejson.type = 'file';
updatejson.status = 'failed';
updatejson.formid = 'dataqueue';
//update dbqueue to show failed
var update = {};
update.json = JSON.stringify(updatejson);
updateDB(update);
}
return;
}
}
} else {
Ti.App.fireEvent('debugevent', {
value : 'no ' + status + ' transactions to upload'
});
Ti.UI.iPhone.setAppBadge(null);
}
} else {
Ti.App.fireEvent('debugevent', {
value : 'no network so: ' + status + ' uploads not processed'
});
}
} catch (ex) {
Ti.App.fireEvent('debugevent', {
value : ex + 'unable to upload ' + status + ' transactions'
});
}
};
/**
* The exported method for bgService class. Calls itself in a two second timer to check the dataqueue database for transactions
* to upload. Calls uploadTransactionsFromQueue twice, once for pending data and once for failed data.
* These are called separately so that failed transactions do not become a bottleneck for pending (new) data.
* @method service
*/
var service = function() {
//Ti.App.fireEvent('debugevent', {
// value : 'service has run ' + counter + ' times'
//});
counter++;
user = getUserName();
pass = keychain.getPasswordForService('server', user);
credentials = user + ':' + pass;
uploadTransactionsFromQueue('pending');
uploadTransactionsFromQueue('failed');
setTimeout(service, 2000);
};
Ti.App.addEventListener('interruptUpload', function() {
uploadInProgress = false;
});
return {
service : service
};
}();
bgService.service();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment