Created
October 16, 2012 08:11
-
-
Save sharpred/3897974 to your computer and use it in GitHub Desktop.
background service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*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