package org.sana.android.service;
import java.util.PriorityQueue;
import org.sana.android.provider.Encounters;
import org.sana.android.provider.Patients;
import org.sana.android.provider.Procedures;
import org.sana.android.net.MDSInterface;
import org.sana.android.net.MDSInterface2;
import org.sana.android.util.SanaUtil;
import android.app.Application;
import android.app.Service;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.Gravity;
import android.widget.Toast;
/**
* Background service to upload pending cases when data service is available.
* This class will try to upload pending cases when a connection is available.
*
* @author Sana Development Team
*/
public class BackgroundUploader extends Service {
private static final String TAG = BackgroundUploader.class.getSimpleName();
/**
* Available states of authorization status.
*
* @author Sana Development Team
*
*/
public enum CredentialStatus {
UNKNOWN,
VALID,
INVALID
}
private PriorityQueue<Uri> queue = null;
private CredentialStatus credentialStatus = CredentialStatus.VALID;
//private CheckCredentialsTask checkCredentialsTask = null;
/**
* Provides a Binder to the BackgoundUploader Service.
*
* @author Sana Development Team
*
*/
public class LocalBinder extends Binder {
public BackgroundUploader getService() {
return BackgroundUploader.this;
}
}
private final IBinder mBinder = new LocalBinder();
/**
* DataConnectionListener is a listener that waits for a data connection.
* Once a data connection is available to the phone, it notifies the
* BackgroundUploader that it should process its queue of cases and update
* their status.
*/
private class DataConnectionListener extends PhoneStateListener {
@Override
public void onDataConnectionStateChanged(int state) {
if (state == TelephonyManager.DATA_CONNECTED) {
Log.i("TAG", "Data is now connected");
} else {
Log.i("TAG", "Data is now disconnected");
}
// Tell the BackgroundUploader the data connection state changed.
BackgroundUploader.this.onConnectionChanged();
}
}
@Override
public void onCreate() {
super.onCreate();
Log.v(TAG, "onCreate()");
try {
queue = QueueManager.initQueue(this);
// Try to process the upload queue. Will check credentials if necessary.
processUploadQueue();
} catch (Exception e) {
Log.e(TAG, "Exception creating background uploading service: "
+ e.toString());
e.printStackTrace();
}
TelephonyManager telephony = (TelephonyManager)getSystemService(
Application.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(new DataConnectionListener(),
PhoneStateListener.LISTEN_DATA_CONNECTION_STATE);
}
}
@Override
public void onStart(Intent data, int startId) {
Log.v(TAG, "onStart() intent " + data + " start ID: " + startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.v(TAG, "onDestroy()");
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private static int getUploadStatusForCredentialStatus(
CredentialStatus credentialStatus)
{
int status;
switch (credentialStatus) {
case INVALID:
status = QueueManager.UPLOAD_STATUS_CREDENTIALS_INVALID;
break;
case UNKNOWN:
case VALID:
default:
status = QueueManager.UPLOAD_STATUS_WAITING;
break;
}
return status;
}
/**
* Update queue status for items in the queue
*/
private boolean updateQueueStatusAndCheckConnection() {
try {
boolean hasConnection = SanaUtil.checkConnection(this);
if (hasConnection) {
try {
Log.i(TAG, "Credential status: " + credentialStatus);
int status = getUploadStatusForCredentialStatus(
credentialStatus);
QueueManager.setProceduresUploadStatus(this, queue, status);
} catch (Exception e) {
Log.e(TAG, "Exception updating upload status in database: "
+ e.toString());
}
return true;
} else {
try {
// Signify procedures waiting for connectivity to upload
QueueManager.setProceduresUploadStatus(this, queue,
QueueManager.UPLOAD_NO_CONNECTIVITY);
} catch (Exception e) {
Log.e(TAG, "Exception updating upload status in database: "
+ e.toString());
}
return false;
}
} catch (Exception e) {
Log.e(TAG, "Exception in checkConnection(): " + e.toString());
return false;
}
}
public void addProcedureToQueue(Uri procedureUri) {
if (QueueManager.isInQueue(queue, procedureUri)) {
Log.i(TAG, "Procedure " + procedureUri + " is already in the queue."
+"Skipping add request.");
return;
}
Log.i(TAG, "Adding " + procedureUri + " to the upload queue.");
QueueManager.addToQueue(this, queue, procedureUri);
Log.i(TAG, "Queue is now: " + queue.toString());
int status = getUploadStatusForCredentialStatus(credentialStatus);
QueueManager.setProcedureUploadStatus(getApplicationContext(),
procedureUri, status);
// Start the upload process if possible. Does its work in an AsyncTask
processUploadQueue();
}
//Only check credentials with openMRS when username or password have changed
// in settings
public void onCredentialsChanged(boolean credentials) {
credentialStatus = credentials ? CredentialStatus.VALID :
CredentialStatus.INVALID;
Log.i(TAG, "Setting credential status to " + credentialStatus);
// Now that the credentials have changed, try to run through the queue.
processUploadQueue();
}
public void onConnectionChanged() {
// Since the connection status changed, try to run through the queue.
processUploadQueue();
}
class UploadResult {
public UploadResult(Uri procedure, boolean uploaded, String message) {
this.procedure = procedure;
this.uploaded = uploaded;
this.message = message;
}
Uri procedure;
boolean uploaded;
String message;
}
AsyncTask<Void, UploadResult, Void> uploadTask = null;
private void processUploadQueue() {
Log.i(TAG, "processUploadQueue()");
// check if there are pending transfers in the database
// if so, then spawn a thread to upload the first one
boolean credentialsValid = CredentialStatus.VALID.equals(
credentialStatus);
boolean connectionAvailable = updateQueueStatusAndCheckConnection();
if (!queue.isEmpty() && connectionAvailable) {
Log.i(TAG, "Queue not empty and connection is available, so " +
"spawning upload worker.");
new AsyncTask<Void, UploadResult, Void>() {
@Override
protected void onProgressUpdate(UploadResult... results) {
for (UploadResult result : results) {
if (result.uploaded) {
onUploadSuccess(result.procedure);
} else {
onUploadFailure(result.procedure, result.message);
}
}
}
@Override
protected Void doInBackground(Void... params) {
while (!queue.isEmpty() && updateQueueStatusAndCheckConnection()) {
Uri procedure = queue.element();
Log.i(TAG,"Uploading procedure " + procedure);
try {
// Signify procedure upload in progress
QueueManager.setProcedureUploadStatus(
BackgroundUploader.this, procedure,
QueueManager.UPLOAD_STATUS_IN_PROGRESS);
boolean uploadResult =
MDSInterface2.postProcedureToDjangoServer(
procedure, BackgroundUploader.this);
if (uploadResult) {
// Remove the procedure from the queue after it
// has been successfully uploaded
QueueManager.removeFromQueue(
BackgroundUploader.this, queue,
procedure,
QueueManager.UPLOAD_STATUS_SUCCESS);
} else {
// Remove the procedure from the queue so it
// does not keep trying to upload
QueueManager.removeFromQueue(
BackgroundUploader.this,
queue, procedure,
QueueManager.UPLOAD_STATUS_FAILURE);
}
UploadResult result = new UploadResult(procedure,
uploadResult, "");
publishProgress(result);
} catch (OutOfMemoryError e) {
Log.e(TAG, "While uploading procedure, " +
"got Out of Memory error.");
e.printStackTrace();
UploadResult result = new UploadResult(procedure,
false, "Out of Memory");
publishProgress(result);
} catch (Exception e) {
Log.e(TAG, "While uploading procedure + " +
procedure + " got exception: "
+ e.toString());
e.printStackTrace();
UploadResult result = new UploadResult(procedure,
false, "");
publishProgress(result);
}
}
return null;
}
}.execute();
} else {
Log.i(TAG, "Either queue is empty or connection is not available, " +
"so not spawning upload worker.");
}
}
private String getProcedureTitle(Uri procedure) {
Cursor cursor = null;
String procedureTitle = "Unknown Procedure";
try {
cursor = getContentResolver().query(procedure, new String [] {
Encounters.Contract._ID,
Encounters.Contract.PROCEDURE,
Encounters.Contract.STATE }, null, null,null);
cursor.moveToFirst();
long savedProcedureId = cursor.getLong(cursor.getColumnIndex(
Encounters.Contract._ID));
long procedureId = cursor.getLong(cursor.getColumnIndex(
Encounters.Contract.PROCEDURE));
cursor.close();
Uri procedureUri = ContentUris.withAppendedId(
Procedures.CONTENT_URI, procedureId);;
cursor = getContentResolver().query(procedureUri, new String[] {
Procedures.Contract.TITLE }, null, null, null);
cursor.moveToFirst();
procedureTitle = cursor.getString(cursor.getColumnIndex(
Procedures.Contract.TITLE));
} catch (Exception e) {
Log.e(TAG, "Failed to get procedure title for procedure "
+ procedure + ". " + e.toString());
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return procedureTitle;
}
private void onUploadSuccess(Uri procedure) {
Log.i(TAG, "onUploadSuccess for " + procedure);
String procedureTitle = getProcedureTitle(procedure);
String patientId = ""; // TODO
String msg = "Successfully sent " + procedureTitle + " for patient "
+ patientId + "\n";
//String msg = "Successfully sent " + procedureTitle + " procedure\nwith ID = " + savedProcedureId;
//String msg = "Successfully sent procedure\nwith ID = " + savedProcedureId;
int sizeOfQueue = queue.size();
if (sizeOfQueue != 0) {
msg += "\nThere are still " + sizeOfQueue+"\ncases to be uploaded.";
}
else {
msg += "\nAll cases are done uploading.";
}
Toast toast = Toast.makeText(getApplicationContext(), msg,
Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
}
private void onUploadFailure(Uri procedure, String message) {
Log.i(TAG, "onUploadFailure for " + procedure);
String procedureTitle = getProcedureTitle(procedure);
String patientId = ""; // TODO
String msg = "Upload of " + procedureTitle + " for patient "
+ patientId + " failed.\n";
msg += message;
//String msg = "Successfully sent " + procedureTitle + " procedure\nwith ID = " + savedProcedureId;
//String msg = "Successfully sent procedure\nwith ID = " + savedProcedureId;
int sizeOfQueue = queue.size();
if (sizeOfQueue != 0) {
msg += "\nThere are still " + sizeOfQueue + "\ncases to be uploaded.";
}
else {
}
Toast toast = Toast.makeText(getApplicationContext(), msg,
Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
}
/**
* Dispatch a POST request to create a new record in the backend server.
*
* @param uri A content style uri
* @throws IllegalArgumentException if the Uri scheme is not content
* @throws UnsupportedOperationException if the content type is not supported for this
* operation.
*/
public void create(Uri uri){
String type = getContentResolver().getType(uri);
if(uri.getScheme().compareTo("content") != 0)
throw new IllegalArgumentException("Must be content style uri: " + uri);
if (type.compareTo(Patients.CONTENT_TYPE) == 0){
} else if (type.compareTo(Encounters.CONTENT_ITEM_TYPE) == 0){
addProcedureToQueue(uri);
} else {
throw new UnsupportedOperationException("POST support not available for " + type);
}
}
/**
* Dispatch a GET request to fetch a record from the backend server
*
* @param uri A content style uri
* @throws IllegalArgumentException if the Uri scheme is not content
* @throws UnsupportedOperationException if the content type is not supported for this
* operation.
*/
public void read(Uri uri){
String type = getContentResolver().getType(uri);
if(uri.getScheme().compareTo("content") != 0)
throw new IllegalArgumentException("Must be content style uri: " + uri);
throw new UnsupportedOperationException("POST support not available for " + type);
}
/**
* Dispatch a PUT request to update a record on the backend server.
*
* @param uri A content style uri
* @throws IllegalArgumentException if the Uri scheme is not content
* @throws UnsupportedOperationException if the content type is not supported for this
* operation.
*/
public void update(Uri uri){
String type = getContentResolver().getType(uri);
if(uri.getScheme().compareTo("content") != 0)
throw new IllegalArgumentException("Must be content style uri: " + uri);
throw new UnsupportedOperationException("POST support not available for " + type);
}
/**
* Dispatch a DELETE request to remove a record on the backend server.
*
* @param uri A content style uri
* @throws IllegalArgumentException if the Uri scheme is not content
* @throws UnsupportedOperationException if the content type is not supported for this
* operation.
*/
public void delete(Uri uri){
String type = getContentResolver().getType(uri);
if(uri.getScheme().compareTo("content") != 0)
throw new IllegalArgumentException("Must be content style uri: " + uri);
throw new UnsupportedOperationException("POST support not available for " + type);
}
}