package org.sana.android.net;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.NameValuePair;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.sana.R;
import org.sana.core.Event;
import org.sana.net.MDSResult;
import org.sana.net.http.ClientFactory;
import org.sana.net.http.HttpTaskFactory;
import org.sana.net.http.ssl.EasySSLSocketFactory;
import org.sana.android.Constants;
import org.sana.android.content.Uris;
import org.sana.android.db.ModelWrapper;
import org.sana.android.db.SanaDB.BinarySQLFormat;
import org.sana.android.db.SanaDB.ImageSQLFormat;
import org.sana.android.db.SanaDB.SoundSQLFormat;
import org.sana.android.procedure.Procedure;
import org.sana.android.procedure.ProcedureElement;
import org.sana.android.procedure.ProcedureParseException;
import org.sana.android.procedure.ProcedureElement.ElementType;
import org.sana.android.provider.Encounters;
import org.sana.android.provider.Observations;
import org.sana.android.provider.Patients;
import org.sana.android.provider.Procedures;
import org.sana.android.util.SanaUtil;
import org.sana.android.util.UserDatabase;
import org.xml.sax.SAXException;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
/**
* Interface for uploading to the Sana Mobile Dispatch Server(MDS).
* <br/>
* This is where all of the packetization and http posting occurs. Other than
* some database interactions, it is fairly independent of Android.
* <br/>
* The process for uploading a procedure is as follows (item number two takes
* place on a remote server, the other steps take place in the code in this
* source file):
* <br/>
* <ol>
* <li>
* Post question/response pairs from completed procedure via http, tagging
* it with procedure, patient, and phone IDs.</li>
* <li>
* Sana Dispatch Server (MDS) parses the questions to see if they include
* any binary elements (i.e. a page in the procedure that asks to take a
* picture). If there are pending binary uploads, MDS knows to expect them and
* does not send the completed upload to OpenMRS until all parts are received.
* </li>
* <li>
* For each binary element, Sana uploads chunks of the element to the MDS. The
* size of these chunks starts at a default size. Each chunk is tagged with a
* procedure, patient, and phone ID as well as an element identifier and the
* start and end byte numbers (corresponding to the chunk location).
* </li>
* <li>
* If the first chunk successfully uploads, the chunk size for the next chunk
* transmission doubles. If the post fails, the chunk size halves.
* </li>
* <li>
* If the chunk size falls below a default "give up" threshold, the procedure
* is tagged as not- finished-uploading, and Sana waits to transmit the rest
* of the completed procedure at a later time. If the entire binary element
* is successfully transmitted, it moves on to the next element.
* </li>
* <li>
* It repeats steps 3-5 for subsequent elements, but instead of starting at the
* default chunk size for each transmission, it now has knowledge about the
* connection quality and uses the last successful transmission size from the
* last binary element as a starting point.
* </li>
* </ol>
*
* @author Sana Dev Team
*/
public class MDSInterface {
public static final String TAG = MDSInterface.class.getSimpleName();
public static String[] savedProcedureProjection = new String[] {
Encounters.Contract._ID,
Encounters.Contract.PROCEDURE,
Encounters.Contract.STATE,
Encounters.Contract.FINISHED,
Encounters.Contract.UUID,
Encounters.Contract.UPLOADED,
Encounters.Contract.SUBJECT,
Encounters.Contract.OBSERVER};
/**
* Http request url for validating MRS credentials
* @param mdsURL host url
* @return the url as a string
*/
private static String constructValidateCredentialsURL(String mdsURL) {
return mdsURL + Constants.VALIDATE_CREDENTIALS_PATTERN;
}
/**
* Http request url for submitting procedures
* @param mdsURL host url
* @return the url as a string
*/
private static String constructProcedureSubmitURL(String mdsURL) {
return mdsURL + Constants.PROCEDURE_SUBMIT_PATTERN;
}
/**
* Http request url for submitting binary chunks
* @param mdsURL host url
* @return the url as a string
*/
private static String constructBinaryChunkSubmitURL(String mdsURL) {
return mdsURL + Constants.BINARYCHUNK_SUBMIT_PATTERN;
}
/**
* Http request url for submitting binary chunks as base64 encoded text
* @param mdsURL host url
* @return the url as a string
*/
private static String constructBinaryChunkHackSubmitURL(String mdsURL) {
return mdsURL + Constants.BINARYCHUNK_HACK_SUBMIT_PATTERN;
}
/**
* Http request url for getting updated patient data(all patients)
* @param mdsURL host url
* @return the url as a string
*/
private static String constructDatabaseDownloadURL(String mdsURL) {
return mdsURL + Constants.DATABASE_DOWNLOAD_PATTERN;
}
/**
* Http request url for requesting patient info
* @param mdsURL host url
* @return the url as a string
*/
private static String constructUserInfoURL(String mdsURL, String id) {
return mdsURL + Constants.USERINFO_DOWNLOAD_PATTERN + id + "/";
}
/**
* Http request url for submitting events
* @param mdsURL host url
* @return the url as a string
*/
private static String constructEventLogUrl(String mdsUrl) {
return mdsUrl + Constants.EVENTLOG_SUBMIT_PATTERN;
}
/**
* Handles legacy url requests
* @param mdsUrl
* @return
*/
private static String checkMDSUrl(String mdsUrl) {
if ("http://moca.media.mit.edu/mds".equals(mdsUrl)) {
return "http://demo.sana.csail.mit.edu/mds";
}
return mdsUrl;
}
/**
* Gets the value in the MDS url setting and add the correct scheme, i.e.
* http or https, depending on the value of the use secure transmission
* setting.
* @param ctx The application context.
* @return The mds url with correct scheme.
*/
private static String getMDSUrl(Context context){
String host = context.getString(R.string.host_mds);
String root = context.getString(R.string.path_root);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
host = preferences.getString(Constants.PREFERENCE_MDS_URL, host);
boolean useSecure = preferences.getBoolean(
Constants.PREFERENCE_SECURE_TRANSMISSION, true);
String scheme = (useSecure)? "https": "http";
/*
String host = context.getString(R.string.host_mds);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
host = preferences.getString(Constants.PREFERENCE_MDS_URL,
Constants.DEFAULT_DISPATCH_SERVER);
// Takes care of legacy issues
//host.replace("moca.mit.edu", "demo.sana.csail.mit.edu");
boolean useSecure = preferences.getBoolean(
Constants.PREFERENCE_SECURE_TRANSMISSION, true);
String scheme = (useSecure)? "https": "http";
*/
String url = scheme + "://" + host +"/"+ root;
Log.d(TAG, "mds url: " + url);
return url;
}
/**
* Executes a POST method. Provides a wrapper around doExecute by
* preparing the PostMethod.
*
* @param ctx the current Context
* @param mUrl the request url
* @param postData the form data.
* @return
* @throws UnsupportedEncodingException
*/
protected static MDSResult doPost(Context ctx, String mUrl,
List<NameValuePair> postData) throws UnsupportedEncodingException
{
HttpPost post = new HttpPost(mUrl);
Log.d(TAG, "doPost(): " + mUrl + ", " + postData.size());
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(postData, "UTF-8");
post.setEntity(entity);
return MDSInterface.doExecute(ctx, post);
}
/**
* Executes a POST method. Provides a wrapper around doExecute by
* preparing the PostMethod.
*
* @param ctx the current Context
* @param mUrl the request url
* @param parts the form data.
* @return
*/
protected static MDSResult doPost(Context ctx, String mUrl, HttpEntity entity)
{
HttpPost post = new HttpPost(mUrl);
post.setEntity(entity);
return MDSInterface.doExecute(ctx, post);
}
/**
* Executes a client HttpMethod.
*
* @param ctx The context which the method will be executed in
* @param method The Http
* @return
*/
protected static MDSResult doExecute(Context ctx, HttpUriRequest method){
HttpClient client = HttpTaskFactory.CLIENT_FACTORY.produce();
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(ctx);
MDSResult response = null;
HttpResponse httpResponse = null;
String responseString = null;
// If there's a proxy enabled, use it.
String proxyHost = preferences.getString(
Constants.PREFERENCE_PROXY_HOST, "");
String sProxyPort = preferences.getString(
Constants.PREFERENCE_PROXY_PORT, "0");
boolean useSecure = preferences.getBoolean(
Constants.PREFERENCE_SECURE_TRANSMISSION, true);
int proxyPort = 0;
try {
if (!"".equals(sProxyPort))
proxyPort = Integer.parseInt(sProxyPort);
} catch(NumberFormatException e) {
Log.w(TAG, "Invalid proxy port: " + sProxyPort);
}
//TODO Fix this
/*
if (!"".equals(proxyHost) && proxyPort != 0) {
Log.i(TAG, "Setting proxy to " + proxyHost + ":" + proxyPort);
HostConfiguration hc = new HostConfiguration();
hc.setProxy(proxyHost, (int)proxyPort);
client.setHostConfiguration(hc);
}
*/
// execute the Http/https method
try {
/*
if(useSecure){
SSLSocketFactory ssl = SimpleSSLProtocolSocketFactory.getSocketFactory();
HttpsUrlConnection https = new Protocol("https", ssl, 443);
Protocol.registerProtocol("https", https);
}
*/
httpResponse = client.execute(method);
Log.d(TAG, "postResponses got response code " + httpResponse.getStatusLine().getStatusCode());
char buf[] = new char[20560];
responseString = EntityUtils.toString(httpResponse.getEntity());
Log.d(TAG, "Received from MDS:" + responseString.length()+" chars");
Gson gson = new Gson();
response = gson.fromJson(responseString, MDSResult.class);
} catch (IOException e1) {
Log.e(TAG, e1.toString());
e1.printStackTrace();
} catch (JsonParseException e) {
Log.e(TAG, "postResponses(): Error parsing MDS JSON response: "
+ e.getMessage());
}
return response;
}
/**
* Posts the text responses from a procedure to the Mobile Dispatch Server
* <br/>
* We don't packetize the raw text responses since, generally speaking, the
* total transmission size will be fairly small (probably less than the
* default starting packet size).
*
* @param savedProcedureGuid the encounter unique identifier
* @param responses the encounter text
* @return true if upload succeeds, otherwise false
* @throws UnsupportedEncodingException
*/
private static boolean postResponses(Context c, String savedProcedureGuid,
String jsonResponses) throws UnsupportedEncodingException
{
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String mdsURL = getMDSUrl(c);
Log.d(TAG, "mds url: " + mdsURL);
//mdsURL = checkMDSUrl(mdsURL);
String mUrl = constructProcedureSubmitURL(mdsURL);
String phoneId = preferences.getString("s_phone_name",
Constants.PHONE_ID);
String username = preferences.getString(
Constants.PREFERENCE_EMR_USERNAME, Constants.DEFAULT_USERNAME);
String password = preferences.getString(
Constants.PREFERENCE_EMR_PASSWORD, Constants.DEFAULT_PASSWORD);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("savedproc_guid", savedProcedureGuid));
postData.add(new BasicNameValuePair("procedure_guid", Integer.toString(0)));
postData.add(new BasicNameValuePair("phone", phoneId));
postData.add(new BasicNameValuePair("username", username));
postData.add(new BasicNameValuePair("password", password));
postData.add(new BasicNameValuePair("responses", jsonResponses));
MDSResult postResponse = MDSInterface.doPost(c, mUrl, postData);
return (postResponse != null)? postResponse.succeeded(): false;
}
/**
* Executes an Http POST call with a binary chunk as base64 encoded text
*
* @param c the current context
* @param savedProcedureId The encounter id
* @param elementId the observation id
* @param fileGuid the unique id of the file
* @param type the observation type
* @param fileSize total byte count
* @param start offset from 0 of this chunk
* @param end offset + size
* @param byte_data the binary chunk that is being sent
* @return true if successful
* @throws UnsupportedEncodingException
*/
private static boolean postBinaryAsEncodedText(Context c,
String savedProcedureId, String elementId, String fileGuid,
ElementType type, int fileSize, int start, int end,
byte byte_data[]) throws UnsupportedEncodingException
{
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String mdsURL = getMDSUrl(c);
mdsURL = checkMDSUrl(mdsURL);
String mUrl = constructBinaryChunkHackSubmitURL(mdsURL);
List<NameValuePair> post = new ArrayList<NameValuePair>();
post.add(new BasicNameValuePair("procedure_guid", savedProcedureId));
post.add(new BasicNameValuePair("element_id", elementId));
post.add(new BasicNameValuePair("binary_guid", fileGuid));
post.add(new BasicNameValuePair("element_type", type.toString()));
post.add(new BasicNameValuePair("file_size", Integer.toString(fileSize)));
post.add(new BasicNameValuePair("byte_start", Integer.toString(start)));
post.add(new BasicNameValuePair("byte_end", Integer.toString(end)));
// Encode byte_data in Base64
byte[] encoded_data = new Base64().encode(byte_data);
post.add(new BasicNameValuePair("byte_data", new String(encoded_data)));
//execute
MDSResult postResponse = MDSInterface.doPost(c, mUrl, post);
return (postResponse != null)? postResponse.succeeded(): false;
}
/**
* A chunk of byte data and filename
*
* @author Sana Development Team
*/
/*
private static class BytePartSource implements PartSource {
private String filename;
private byte[] data;
public BytePartSource(byte[] data, String filename) {
this.data = data;
this.filename = filename;
}
@Override
public InputStream createInputStream() throws IOException {
return new ByteArrayInputStream(data);
}
@Override
public String getFileName() {
return filename;
}
public long getLength() {
return data.length;
}
}
*/
/**
* Executes an Http POST call with a binary chunk as a file
*
* @param c the current context
* @param savedProcedureId The encounter id
* @param elementId the observation id
* @param fileGuid the unique id of the file
* @param type the observation type
* @param fileSize total byte count
* @param start offset from 0 of this chunk
* @param end offset + size
* @param byte_data the binary chunk that is being sent
* @return true if successful
* @throws UnsupportedEncodingException
*/
private static boolean postBinaryAsFile(Context c, String savedProcedureId,
String elementId, String fileGuid, ElementType type, int fileSize,
int start, int end, byte byte_data[]) throws UnsupportedEncodingException
{
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(c);
String mdsUrl = getMDSUrl(c);
mdsUrl = checkMDSUrl(mdsUrl);
String mUrl = constructBinaryChunkSubmitURL(mdsUrl);
// this is the compat layer
String gid = String.format("%s_%s", elementId, fileGuid);
Log.d(TAG,"Posting to: " + mUrl);
Log.d(TAG,"....file chunk: " + elementId +", guid:" + fileGuid);
MultipartEntity entity = new MultipartEntity();
entity.addPart("procedure_guid", new StringBody(savedProcedureId));
entity.addPart("element_id", new StringBody(elementId));
entity.addPart("binary_guid", new StringBody(fileGuid));
entity.addPart("element_type", new StringBody(type.toString()));
entity.addPart("file_size", new StringBody(Integer.toString(fileSize)));
entity.addPart("byte_start", new StringBody(Integer.toString(start)));
entity.addPart("byte_end", new StringBody(Integer.toString(end)));
entity.addPart("byte_data", new ByteArrayBody(byte_data, type.getFilename()));
//execute
MDSResult postResponse = MDSInterface.doPost(c, mUrl, entity);
return (postResponse != null)? postResponse.succeeded(): false;
}
/**
* Posts a single chunk of a binary file.
*
* @param c current context
* @param savedProcedureId The encounter id
* @param elementId The observation within the encounter
* @param type binary type (ie picture, sound, etc.)
* @param start first byte index in binary file (since this presumably is a
* chunk of a larger file)
* @param end last byte index in binary file of the chunk being uploaded
* @param byte_data a byte array containing the file chunk data
* @return true on successful upload, otherwise false
* @throws UnsupportedEncodingException
*/
private static boolean postBinary(Context c, String savedProcedureId,
String elementId, String fileGuid, ElementType type, int fileSize,
int start, int end, byte byte_data[]) throws UnsupportedEncodingException
{
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
boolean hacksMode = preferences.getBoolean(
Constants.PREFERENCE_UPLOAD_HACK, false);
// check if we want to post as base64 encoded text
if(hacksMode) {
return postBinaryAsEncodedText(c, savedProcedureId, elementId,
fileGuid, type, fileSize, start, end, byte_data);
} else {
return postBinaryAsFile(c, savedProcedureId, elementId, fileGuid,
type, fileSize, start, end, byte_data);
}
}
/**
* Checks whether an encounter is already uploaded
*
* @param uri The saved procedure uri
* @param context current context
* @return
*/
public static boolean isProcedureAlreadyUploaded(Uri uri, Context context) {
Cursor cursor = context.getContentResolver().query(uri,
savedProcedureProjection, null, null, null);
// First get the saved procedure...
cursor.moveToFirst();
int procedureId = cursor.getInt(1);
String answersJson = cursor.getString(cursor.getColumnIndex(
Encounters.Contract.STATE));
boolean savedProcedureUploaded = cursor.getInt(cursor.getColumnIndex(
Encounters.Contract.UPLOADED)) != 0;
String subject = cursor.getString(cursor.getColumnIndex(
Encounters.Contract.SUBJECT));
cursor.close();
Uri procedureUri = ContentUris.withAppendedId(
Procedures.CONTENT_URI, procedureId);
Log.i(TAG, "Getting procedure " + procedureUri.toString());
cursor = context.getContentResolver().query(procedureUri,
new String[] { Procedures.Contract.PROCEDURE,
Procedures.Contract.UUID
},
null,null,null);
cursor.moveToFirst();
String procedureXml = cursor.getString(cursor.getColumnIndex(Procedures.Contract.PROCEDURE));
String procedureUuid = cursor.getString(cursor.getColumnIndex(Procedures.Contract.UUID));
cursor.close();
if (!savedProcedureUploaded) return false;
Map<String, Map<String,String>> elementMap = null;
try {
Procedure p = Procedure.fromXMLString(procedureXml);
p.setInstanceUri(uri);
JSONTokener tokener = new JSONTokener(answersJson);
JSONObject answersDict = new JSONObject(tokener);
Map<String,String> answersMap = new HashMap<String,String>();
Iterator<?> it = answersDict.keys();
while(it.hasNext()) {
String key = (String)it.next();
answersMap.put(key, answersDict.getString(key));
Log.i(TAG, "onCreate() : answer '" + key + "' : '"
+ answersDict.getString(key) +"'");
}
Log.i(TAG, "onCreate() : restoreAnswers");
p.restoreAnswers(answersMap);
elementMap = p.toElementMap();
} catch (IOException e2) {
Log.e(TAG, e2.toString());
} catch (ParserConfigurationException e2) {
Log.e(TAG, e2.toString());
} catch (SAXException e2) {
Log.e(TAG, e2.toString());
} catch (ProcedureParseException e2) {
Log.e(TAG, e2.toString());
} catch (JSONException e) {
Log.e(TAG, e.toString());
}
if(elementMap == null) {
Log.i(TAG, "Empty answers from " + uri + ". Not uploading.");
return false;
}
class ElementAnswer {
public String answer;
public String type;
public ElementAnswer(String id, String answer, String type) {
this.answer = answer;
this.type = type;
}
}
int totalBinaries = 0;
List<ElementAnswer> binaries = new ArrayList<ElementAnswer>();
for(Entry<String,Map<String,String>> e : elementMap.entrySet()) {
String id = e.getKey();
String type = e.getValue().get("type");
String answer = e.getValue().get("answer");
// Find elements that require binary uploads
if(type.equals(ElementType.PICTURE.toString()) ||
type.equals(ElementType.BINARYFILE.toString()) ||
type.equals(ElementType.SOUND.toString()) ||
type.equals(ElementType.PLUGIN.toString())) {
binaries.add(new ElementAnswer(id, answer, type));
if(!"".equals(answer)) {
String[] ids = answer.split(",");
totalBinaries += ids.length;
}
}
}
// upload each binary file
for(ElementAnswer e : binaries) {
if("".equals(e.answer))
continue;
String[] ids = e.answer.split(",");
for(String binaryId : ids) {
Uri binUri = null;
ElementType type = ElementType.INVALID;
try {
type = ElementType.valueOf(e.type);
} catch(IllegalArgumentException ex) {
}
if (type == ElementType.PICTURE) {
binUri = ContentUris.withAppendedId(
ImageSQLFormat.CONTENT_URI,
Long.parseLong(binaryId));
} else if (type == ElementType.SOUND) {
binUri = ContentUris.withAppendedId(
SoundSQLFormat.CONTENT_URI,
Long.parseLong(binaryId));
} else if (type == ElementType.PLUGIN) {
binUri = ContentUris.withAppendedId(
BinarySQLFormat.CONTENT_URI,
Long.parseLong(binaryId));
} else if (type == ElementType.BINARYFILE) {
binUri = Uri.fromFile(new File(e.answer));
// We can't tell if a BINARYFILE has been uploaded before.
// Maybe if we grab the mtime/filesize on the file and store
// it when we upload it.
}
try {
Log.i(TAG, "Checking if " + binUri + " has been uploaded");
// reset the new packet size each time to the last
// successful transmission size
boolean alreadyUploaded = false;
Cursor cur = null;
switch(type) {
case PICTURE:
cur = context.getContentResolver().query(binUri,
new String[] { ImageSQLFormat.UPLOADED }, null,
null, null);
cur.moveToFirst();
alreadyUploaded = cur.getInt(0) != 0;
if (!alreadyUploaded) return false;
if (cur != null)
cur.close();
break;
case SOUND:
cur = context.getContentResolver().query(binUri,
new String[] { SoundSQLFormat.UPLOADED }, null,
null, null);
cur.moveToFirst();
alreadyUploaded = cur.getInt(0) != 0;
if (!alreadyUploaded) return false;
cur.deactivate();
break;
case PLUGIN:
cur = context.getContentResolver().query(binUri,
new String[] { BinarySQLFormat.UPLOADED }, null,
null, null);
cur.moveToFirst();
alreadyUploaded = cur.getInt(0) != 0;
if (!alreadyUploaded) return false;
cur.deactivate();
break;
case BINARYFILE:
default:
// Can't do anything since its not in the DB. Sigh.
break;
}
} catch (Exception x) {
Log.i(TAG, "Error checking if the binary files have been "
+"uploaded: " + x.toString());
return false;
}
}
}
return true;
}
/**
* Send the entire completed procedure to the Sana Mobile Dispatch Server
* (MDS). This procedure sends the answer/response pairs and all the binary
* data (sounds, pictures, etc.) to the MDS in a packetized fashion.
*
* @param uri uri of procedure in database
* @param context current context
* @return true if upload was successful, false if not
* @throws UnsupportedEncodingException
*/
public static boolean postProcedureToDjangoServer(Uri uri, Context context) throws UnsupportedEncodingException {
Log.i(TAG, "In Post procedure to Django server for background uploading service.");
Cursor cursor = context.getContentResolver().query(
uri, savedProcedureProjection, null,
null, null);
// First get the saved procedure...
cursor.moveToFirst();
int savedProcedureId = cursor.getInt(0);
int procedureId = cursor.getInt(1);
String answersJson = cursor.getString(2);
boolean finished = cursor.getInt(3) != 0;
String savedProcedureGUID = cursor.getString(4);
boolean savedProcedureUploaded = cursor.getInt(5) != 0;
cursor.close();
Uri procedureUri = ContentUris.withAppendedId(
Procedures.CONTENT_URI, procedureId);
Log.i(TAG, "Getting procedure " + procedureUri.toString());
cursor = context.getContentResolver().query(procedureUri,
new String[] { Procedures.Contract.TITLE,
Procedures.Contract.PROCEDURE,
Procedures.Contract.UUID},
null, null, null);
cursor.moveToFirst();
String procedureTitle = cursor.getString(
cursor.getColumnIndex(Procedures.Contract.TITLE));
String procedureXml = cursor.getString(
cursor.getColumnIndex(Procedures.Contract.PROCEDURE));
String procedureUUID = cursor.getString(
cursor.getColumnIndex(Procedures.Contract.UUID));
cursor.close();
if(!finished) {
Log.i(TAG, "Not finished. Not uploading. (just kidding)"
+ uri.toString());
//return false;
}
// Map of all of the Procedure Elements; i.e. observation data
// binaries get parsed out later
Map<String, Map<String,String>> elementMap = null;
try {
Procedure p = Procedure.fromXMLString(procedureXml);
p.setInstanceUri(uri);
JSONTokener tokener = new JSONTokener(answersJson);
JSONObject answersDict = new JSONObject(tokener);
Map<String,String> answersMap = new HashMap<String,String>();
Iterator<?> it = answersDict.keys();
while(it.hasNext()) {
String key = (String)it.next();
answersMap.put(key, answersDict.getString(key));
Log.i(TAG, "onCreate() : answer '" + key + "' : '"
+ answersDict.getString(key) +"'");
}
Log.i(TAG, "onCreate() : restoreAnswers");
p.restoreAnswers(answersMap);
elementMap = p.toElementMap();
} catch (IOException e2) {
Log.e(TAG, e2.toString());
} catch (ParserConfigurationException e2) {
Log.e(TAG, e2.toString());
} catch (SAXException e2) {
Log.e(TAG, e2.toString());
} catch (ProcedureParseException e2) {
Log.e(TAG, e2.toString());
} catch (JSONException e) {
Log.e(TAG, e.toString());
}
// check that we don't have empty map
if(elementMap == null) {
Log.i(TAG, "Could not encounter text " + uri + ". Not uploading.");
return false;
}
// Add in procedureTitle as a fake answer
/*
Map<String,String> titleMap = new HashMap<String,String>();
titleMap.put("answer", procedureTitle);
titleMap.put("id", "procedureTitle");
titleMap.put("type", "HIDDEN");
elementMap.put("procedureTitle", titleMap);
*/
/*
// We need a String -> String map to convert to JSON
Map<String,String> enrolledMap = new HashMap<String,String>();
enrolledMap.put("answer", "Yes");
enrolledMap.put("id", "patientEnrolled");
enrolledMap.put("type", ProcedureElement.ElementType.RADIO.toString());
enrolledMap.put("question", "Does the patient already have an ID card?");
elementMap.put("patientEnrolled", enrolledMap);
*/
// Utility wrapper for answers
class ElementAnswer {
public String id;
public String answer;
public String type;
public ElementAnswer(String id, String answer, String type) {
this.id = id;
this.answer = answer;
this.type = type;
}
}
// Convert saved procedure to JSON
JSONObject jsono = new JSONObject();
int totalBinaries = 0;
ArrayList<ElementAnswer> binaries = new ArrayList<ElementAnswer>();
for(Entry<String,Map<String,String>> e : elementMap.entrySet()) {
try {
jsono.put(e.getKey(), new JSONObject(e.getValue()));
} catch (JSONException e1) {
Log.e(TAG, "JSON conversion fail: " + e1.getMessage());
}
String id = e.getKey();
String type = e.getValue().get("type");
String answer = e.getValue().get("answer");
if (id == null || type == null || answer == null)
continue;
// Find elements that require binary uploads
if(type.equals(ElementType.PICTURE.toString()) ||
type.equals(ElementType.BINARYFILE.toString()) ||
type.equals(ElementType.SOUND.toString()) ||
type.equals(ElementType.PLUGIN.toString())){
binaries.add(new ElementAnswer(id, answer, type));
if(!"".equals(answer)) {
String[] ids = answer.split(",");
totalBinaries += ids.length;
}
}
}
Log.i(TAG, "About to post responses.");
// check if it is already uploaded
if(savedProcedureUploaded) {
Log.i(TAG, "Responses have already been sent to MDS, not posting.");
} else {
// upload the question and answer pairs text, without packetization
String json = jsono.toString();
Log.i(TAG, "json string: " + json);
// try repeating upload on fail to some preset number
int tries = 0;
final int MAX_TRIES = 5;
while(tries < MAX_TRIES) {
if (postResponses(context, savedProcedureGUID, json)) {
// Mark the procedure text as uploaded in the database
ContentValues cv = new ContentValues();
cv.put(Encounters.Contract.UPLOADED, true);
context.getContentResolver().update(uri, cv, null, null);
Log.i(TAG, "Responses were uploaded successfully.");
break;
}
tries++;
}
// if tries >= maximum we bail and try again later
if(tries == MAX_TRIES) {
Log.e(TAG, "Could not post responses, bailing.");
return false;
}
}
Log.i(TAG, "Posted responses, now sending " + totalBinaries
+ " binaries.");
// lookup starting packet size
int newPacketSize;
try {
newPacketSize = Integer.parseInt(
PreferenceManager.getDefaultSharedPreferences(context)
.getString("s_packet_init_size",
Integer.toString(Constants.DEFAULT_INIT_PACKET_SIZE)));
} catch (NumberFormatException e) {
newPacketSize = Constants.DEFAULT_INIT_PACKET_SIZE;
}
// adjust from KB to bytes
newPacketSize *= 1000;
int totalProgress = 1+totalBinaries;
int thisProgress = 2;
// upload each binary file where each binary should be represented by
// one value in a comma separated list of ints starting
for(ElementAnswer e : binaries) {
if(TextUtils.isEmpty(e.answer))
continue;
// parse csv list
String[] ids = e.answer.split(",");
// loop over each value
for(String binaryId : ids) {
Uri binUri = null;
ElementType type = ElementType.INVALID;
try {
type = ElementType.valueOf(e.type);
} catch(IllegalArgumentException ex) {
Log.e(TAG, ex.getMessage());
}
if (type == ElementType.PICTURE) {
binUri = ContentUris.withAppendedId(
ImageSQLFormat.CONTENT_URI,
Long.parseLong(binaryId));
} else if (type == ElementType.SOUND) {
binUri = ContentUris.withAppendedId(
SoundSQLFormat.CONTENT_URI,
Long.parseLong(binaryId));
} else if (type == ElementType.PLUGIN) {
binUri = ContentUris.withAppendedId(
BinarySQLFormat.CONTENT_URI,
Long.parseLong(binaryId));
} else if (type == ElementType.BINARYFILE) {
binUri = Uri.fromFile(new File(e.answer));
// We can't tell if a BINARYFILE has been uploaded before.
// Maybe if we grab the mtime/filesize on the file and store
// it when we upload it.
}
try {
Log.i(TAG, "Uploading " + binUri);
// reset the new packet size each time to the last
// successful transmission size
newPacketSize = transmitBinary(context, savedProcedureGUID,
e.id, binaryId, type, binUri,
newPacketSize);
// Delete the file!
switch(type) {
case PICTURE:
case SOUND:
//This was deleting the pictures after upload - should
// not happen, leave commented out!
//context.getContentResolver().delete(binUri,null,null);
break;
default:
}
} catch (Exception x) {
Log.i(TAG, "Uploading " + binUri + " failed : "
+ x.toString());
return false;
}
thisProgress++;
}
}
logObservations(context,savedProcedureGUID);
// TODO Tag entire procedure in db as done transmitting
return true;
}
/**
* Sends an entire binary file in a packetized fashion. This method is where
* the automatic ramping packetization takes place. Uploading occurs in the
* background
*
* @param c current Context
* @param savedProcedureId the unique identifier of the procedure within
* the phone domain
* @param elementId the id attribute of the Element within a Procedure
* @param type binary type (ie picture, sound, etc.)
* @param binaryUri uri of the file to be transmitted
* @param startPacketSize the starting packet size for each chunk; this will
* be throttled up or down depending on connection strength
* @return the last successful chunk transmission size on success so that it
* can be used for future transmissions as the startPacketSize
* @throws Exception on upload failure
*/
protected static int transmitBinary(Context c, String savedProcedureId,
String elementId, String binaryGuid, ElementType type,
Uri binaryUri, int startPacketSize) throws Exception
{
Log.i(TAG,String.format("transmitBinary(): "
+ "encounter: %s, "
+ "elementId: %s, "
+ "binaryGuid: %s, "
+ "type:%s", savedProcedureId,elementId,binaryGuid,type.toString()));
int packetSize, fileSize;
ContentValues cv = new ContentValues();
packetSize = startPacketSize;
boolean alreadyUploaded = false;
int currPosition = 0;
Cursor cur = null;
switch(type) {
case PICTURE:
cur = c.getContentResolver().query(binaryUri, new String[] {
ImageSQLFormat.UPLOADED, ImageSQLFormat.UPLOAD_PROGRESS },
null, null, null);
cur.moveToFirst();
alreadyUploaded = cur.getInt(0) != 0;
currPosition = cur.getInt(1);
break;
case SOUND:
cur = c.getContentResolver().query(binaryUri, new String[] {
SoundSQLFormat.UPLOADED, SoundSQLFormat.UPLOAD_PROGRESS },
null, null, null);
cur.moveToFirst();
alreadyUploaded = cur.getInt(0) != 0;
currPosition = cur.getInt(1);
break;
case PLUGIN:
//TODO Add the BinaryProvider
cur = c.getContentResolver().query(binaryUri, new String[] {
BinarySQLFormat.UPLOADED, BinarySQLFormat.UPLOAD_PROGRESS },
null, null, null);
cur.moveToFirst();
alreadyUploaded = cur.getInt(0) != 0;
currPosition = cur.getInt(1);
break;
case BINARYFILE:
default:
// Can't do anything since its not in the DB. Sigh.
break;
}
if (cur != null){
cur.close();
}
if(alreadyUploaded) {
Log.i(TAG, binaryUri + " was already uploaded. Skipping.");
return startPacketSize;
}
// Ope the input stream
InputStream is = c.getContentResolver().openInputStream(binaryUri);
fileSize = is.available();
// Skip forward by the progress we've made previously.
is.skip(currPosition);
int progress = (int)(100.0 * currPosition / fileSize);
int bytesRemaining = fileSize - currPosition;
Log.i(TAG, "transmitBinary uploading " + binaryUri + " "
+ bytesRemaining + " total bytes remaining. Starting at "
+ packetSize + " packet size");
// reference packet rate byte/msec
double basePacketRate = 0.0;
while(bytesRemaining > 0) {
// get starting time of packet transmission
long transmitStartTime = new Date().getTime();
// if transmission rate is acceptable
// (comparison between currPacketRate and basePacketRate)
boolean efficient = false;
int bytesToRead = Math.min(packetSize, bytesRemaining);
byte[] chunk = new byte[bytesToRead];
int bytesRead = is.read(chunk, 0, bytesToRead);
boolean success = false;
while(!success) {
Log.i(TAG, "Trying to upload " + bytesRead + " bytes for "
+ savedProcedureId + ":" + elementId + "build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3/res");
success = postBinary(c, savedProcedureId, elementId, binaryGuid,
type, fileSize, currPosition,
currPosition+bytesRead, chunk);
efficient = false;
// new rate is compared to 80% of previous rate
basePacketRate *= 0.8;
if(success) {
long transmitEndTime = new Date().getTime();
// get new packet rate
double currPacketRate = (double)packetSize/
(double)(transmitEndTime-transmitStartTime);
Log.i(TAG, "packet rate = (current) " + currPacketRate
+ ", (base) " + basePacketRate);
if(currPacketRate > basePacketRate) {
basePacketRate = currPacketRate;
efficient = true;
}
}
// update packet size
if(efficient) {
packetSize *= 2;
Log.i(TAG, "Shifting packet size *2 =" + packetSize);
} else {
packetSize /= 2;
Log.i(TAG, "Shifting packet size /2 =" + packetSize);
}
// close if packet size becomes too small
if(packetSize < Constants.MIN_PACKET_SIZE * 1000) {
// TODO(rryan) : fail at some point
is.close();
throw new IOException("Could not upload " + binaryUri
+". failed after " + (fileSize-bytesRemaining)
+ " bytes.");
}
}
// update progress
bytesRemaining -= bytesRead;
currPosition += bytesRead;
progress = (int)(100.0 * currPosition / fileSize);
// write current progress to database
cv.clear();
switch(type) {
case PICTURE:
cv.put(ImageSQLFormat.UPLOAD_PROGRESS, currPosition);
c.getContentResolver().update(binaryUri, cv, null, null);
break;
case SOUND:
cv.put(SoundSQLFormat.UPLOAD_PROGRESS, currPosition);
c.getContentResolver().update(binaryUri, cv, null, null);
break;
}
}
// Mark file as uploaded in the database
cv.clear();
switch(type) {
case PICTURE:
cv.put(ImageSQLFormat.UPLOADED, true);
c.getContentResolver().update(binaryUri, cv, null, null);
break;
case SOUND:
cv.put(SoundSQLFormat.UPLOADED, true);
c.getContentResolver().update(binaryUri, cv, null, null);
break;
}
is.close();
return packetSize;
}
/**
* Validates authorization credentials with permanent record store.
*
* @param c the current Context
* @return true if the dispatch server reports success
* @throws IOException
*/
public static boolean validateCredentials(Context c) throws IOException {
Log.i(TAG, "validateCredentials()");
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String mdsURL = getMDSUrl(c);
mdsURL = checkMDSUrl(mdsURL);
String mUrl = constructValidateCredentialsURL(mdsURL);
String username = preferences.getString(
Constants.PREFERENCE_EMR_USERNAME, Constants.DEFAULT_USERNAME);
String password = preferences.getString(
Constants.PREFERENCE_EMR_PASSWORD, Constants.DEFAULT_PASSWORD);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("username", username));
postData.add(new BasicNameValuePair("password", password));
MDSResult postResponse = MDSInterface.doPost(c, mUrl, postData);
boolean result = (postResponse != null)? postResponse.succeeded():false;
Log.i(TAG, "MDS reports " + (result ? "success" : "failure")
+ " for credentials");
return result;
}
/**
* Sync patient database on phone with permanent record store.
*
* @param c the current Context
* @return true if successfully updated
* @throws UnsupportedEncodingException
*/
public static boolean updatePatientDatabase(Context c, ContentResolver cr) throws UnsupportedEncodingException {
Log.i(TAG, "updatePatientDatabase():");
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String mdsURL = getMDSUrl(c);
mdsURL = checkMDSUrl(mdsURL);
String mUrl = constructDatabaseDownloadURL(mdsURL);
String username = preferences.getString(
Constants.PREFERENCE_EMR_USERNAME, Constants.DEFAULT_USERNAME);
String password = preferences.getString(
Constants.PREFERENCE_EMR_PASSWORD, Constants.DEFAULT_PASSWORD);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("username", username));
postData.add(new BasicNameValuePair("password", password));
MDSResult postResponse = MDSInterface.doPost(c, mUrl, postData);
boolean result = (postResponse != null)? postResponse.succeeded():false;
try{
if (result){
String toparse = postResponse.getData();
SanaUtil.clearPatientData(c);
//the following line needs to be uncommented eventually
UserDatabase.addDataToUsers(cr, toparse);
cr.notifyChange(Patients.CONTENT_URI,null);
}
} catch (Exception e){
result = false;
Log.e(TAG, "updatePatientDatabase(): " + e.getMessage());
}
return result;
}
public static HttpUriRequest updatePatientDatabase(Context c) {
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String scheme = getScheme(preferences);
String host = getHost(preferences);
String path = c.getString(R.string.path_root) + c.getString(R.string.path_subject_sync);
int port = getPort(c);
String mdsURL = getMDSUrl(c);
mdsURL = checkMDSUrl(mdsURL);
String username = preferences.getString(
Constants.PREFERENCE_EMR_USERNAME, Constants.DEFAULT_USERNAME);
String password = preferences.getString(
Constants.PREFERENCE_EMR_PASSWORD, Constants.DEFAULT_PASSWORD);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("username", username));
postData.add(new BasicNameValuePair("password", password));
return HttpRequestFactory.getPostRequest(scheme, host, port, path, postData);
}
/**
* Gets patient database from MRS
*
* @param c the current Context
* @return The string representation of a patient
* @throws UnsupportedEncodingException
*/
public static String getUserInfo(Context c, String userid) throws UnsupportedEncodingException {
Log.i(TAG, "getUserInfo(): " + userid);
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String mdsURL = getMDSUrl(c);
mdsURL = checkMDSUrl(mdsURL);
String mUrl = constructUserInfoURL(mdsURL,userid);
String username = preferences.getString(
Constants.PREFERENCE_EMR_USERNAME, Constants.DEFAULT_USERNAME);
String password = preferences.getString(
Constants.PREFERENCE_EMR_PASSWORD, Constants.DEFAULT_PASSWORD);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("username", username));
postData.add(new BasicNameValuePair("password", password));
MDSResult postResponse = MDSInterface.doPost(c, mUrl, postData);
String result = (postResponse != null)? postResponse.getData(): null;
Log.i(TAG, "getUserInfo(): Patient data: " + result);
return result;
}
/**
* Checks whether patient exists in the permanent record store.
*
* @param c the application Context
* @param userid the id to verify
* @return true if the id is not in use
* @throws UnsupportedEncodingException
*/
public static boolean isNewPatientIdValid(Context c, String userid) throws UnsupportedEncodingException {
Log.i(TAG, "isNewPatientValid(): " + userid);
String data = MDSInterface.getUserInfo(c, userid);
if (!TextUtils.isEmpty(data)) {
Log.i(TAG, "isNewPatientValid(): Id already in use.");
return false;
}
Log.i(TAG, "isNewPatientValid(): Id is not in use and is valid");
return true;
}
/**
* Sends a list of events to the dispatch server
*
* @param c the application Context
* @param eventsList a list of events
* @return true if successfully sent
* @throws UnsupportedEncodingException
*/
public static boolean submitEvents(Context c, List<Event> eventsList) throws UnsupportedEncodingException {
Log.i(TAG, "submitEvents(): " + eventsList.size());
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String mdsURL = getMDSUrl(c);
mdsURL = checkMDSUrl(mdsURL);
String mUrl = constructEventLogUrl(mdsURL);
String phoneId = preferences.getString("s_phone_name",
Constants.PHONE_ID);
String username = preferences.getString(
Constants.PREFERENCE_EMR_USERNAME, Constants.DEFAULT_USERNAME);
String password = preferences.getString(
Constants.PREFERENCE_EMR_PASSWORD, Constants.DEFAULT_PASSWORD);
List<NameValuePair> post = new ArrayList<NameValuePair>();
post.add(new BasicNameValuePair("username", username));
post.add(new BasicNameValuePair("password", password));
post.add(new BasicNameValuePair("client_id", phoneId));
Gson g = new Gson();
post.add(new BasicNameValuePair("events", g.toJson(eventsList)));
MDSResult postResponse = MDSInterface.doPost(c, mUrl, post);
return (postResponse != null)? postResponse.succeeded(): false;
}
// returns the scheme basef on the "Use secure transmission" setting.
static String getScheme(SharedPreferences preferences){
if(preferences.getBoolean(Constants.PREFERENCE_SECURE_TRANSMISSION, true))
return "https";
else
return "http";
}
// returns the host which can be set in the preferences
static String getHost(SharedPreferences preferences){
return preferences.getString(Constants.PREFERENCE_MDS_URL,
Constants.DEFAULT_DISPATCH_SERVER);
}
static String getHost(SharedPreferences preferences, String val){
return preferences.getString(Constants.PREFERENCE_MDS_URL, val);
}
// Retuns the port value from net.xml
static int getPort(Context c){
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
if(preferences.getBoolean(Constants.PREFERENCE_SECURE_TRANSMISSION, true))
return 443;
else
return c.getResources().getInteger(R.integer.port_mds);
}
public static URI getRoot(Context c) throws URISyntaxException{
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String scheme = getScheme(preferences);
String host = preferences.getString(Constants.PREFERENCE_MDS_URL,
c.getString(R.string.host_mds));
int port = getPort(c);
String path = c.getString(R.string.path_root);
return new URI(scheme, null, host, port,path, null, null);
}
/**
* Generates a POST request to mds.
*
* @param c the Context used for getting request params
* @param username
* @param password
* @return a POST request.
*/
public static HttpPost createSessionRequest(Context c, String username,
String password)
{
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String scheme = getScheme(preferences);
String host = getHost(preferences, c.getString(R.string.host_mds));
int port = getPort(c);
String path = c.getString(R.string.path_root) + c.getString(R.string.path_session);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
postData.add(new BasicNameValuePair("username", username));
postData.add(new BasicNameValuePair("password", password));
Log.d(TAG, String.format("createSessionRequest(): %s://%s:%d/%s/", scheme, host, port, path));
return HttpRequestFactory.getPostRequest(scheme, host, port, path, postData);
}
/**
* Generates a POST request to mds.
*
* @param c the Context used for getting request params
* @param username
* @param password
* @return a POST request.
*/
public static HttpGet createSubjectRequest(Context c)
{
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(c);
String scheme = getScheme(preferences);
String host = getHost(preferences, c.getString(R.string.host_mds));
int port = getPort(c);
String path = c.getString(R.string.path_root) + c.getString(R.string.path_subject);
List<NameValuePair> postData = new ArrayList<NameValuePair>();
return HttpRequestFactory.getHttpGetRequest(scheme, host, port, path, postData, null);
}
/**
* Generates an observation list from the old SavedProcedures
* @param context
* @param encounter
* @return
*/
public static List<?> generateObservations(Context context, Uri encounter){
return null;
}
/**
* Generates an
* @param ctx
* @param savedProcedure
* @return
*/
public static List<NameValuePair> generateEncounter(Context ctx, Uri encounter){
List<NameValuePair> postData = new ArrayList<NameValuePair>();
return postData;
}
public static void logObservations(Context context, String uuid){
Cursor cursor = context.getContentResolver().query(
Observations.CONTENT_URI,
null,
Observations.Contract.ENCOUNTER + " = ?",
new String[]{ uuid } ,
Observations.Contract.ID + " ASC");
StringBuilder obs = new StringBuilder("{ 'encounter': "+ uuid + ", 'observations' : [");
if(cursor != null){
while(cursor.moveToNext()){
obs.append("{");
obs.append("'"+ Observations.Contract._ID+ "': " + cursor.getLong(cursor.getColumnIndex(Observations.Contract._ID)) +", ");
obs.append("'"+ Observations.Contract.ID+ "': " + cursor.getString(cursor.getColumnIndex(Observations.Contract.ID)) +", ");
obs.append("'"+ Observations.Contract.CONCEPT+ "': " + cursor.getString(cursor.getColumnIndex(Observations.Contract.CONCEPT)) +", ");
obs.append("'"+ Observations.Contract.VALUE+ "': " + cursor.getString(cursor.getColumnIndex(Observations.Contract.VALUE)) +", ");
obs.append("}");
}
}
obs.append("]}");
Log.i(TAG, obs.toString());
}
}