package com.eveningoutpost.dexdrip.UtilityModels;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.BatteryManager;
import android.preference.PreferenceManager;
import android.util.Log;
import com.eveningoutpost.dexdrip.Models.BgReading;
import com.eveningoutpost.dexdrip.Models.Calibration;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.WriteConcern;
import org.apache.http.Header;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.json.JSONObject;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
* THIS CLASS WAS BUILT BY THE NIGHTSCOUT GROUP FOR THEIR NIGHTSCOUT ANDROID UPLOADER
* https://github.com/nightscout/android-uploader/
* I have modified this class to make it fit my needs
* Modifications include field remappings and lists instead of arrays
* A DTO would probably be a better future implementation
* -Stephen Black
*/
public class NightscoutUploader {
private static final String TAG = NightscoutUploader.class.getSimpleName();
private static final int SOCKET_TIMEOUT = 60000;
private static final int CONNECTION_TIMEOUT = 30000;
private Context mContext;
private Boolean enableRESTUpload;
private Boolean enableMongoUpload;
private SharedPreferences prefs;
public NightscoutUploader(Context context) {
mContext = context;
prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
enableRESTUpload = prefs.getBoolean("cloud_storage_api_enable", false);
enableMongoUpload = prefs.getBoolean("cloud_storage_mongodb_enable", false);
}
public boolean upload(BgReading glucoseDataSet, Calibration meterRecord, Calibration calRecord) {
List<BgReading> glucoseDataSets = new ArrayList<BgReading>();
glucoseDataSets.add(glucoseDataSet);
List<Calibration> meterRecords = new ArrayList<Calibration>();
meterRecords.add(meterRecord);
List<Calibration> calRecords = new ArrayList<Calibration>();
calRecords.add(calRecord);
return upload(glucoseDataSets, meterRecords, calRecords);
}
public boolean upload(List<BgReading> glucoseDataSets, List<Calibration> meterRecords, List<Calibration> calRecords) {
boolean mongoStatus = false;
boolean apiStatus = false;
if (enableRESTUpload) {
long start = System.currentTimeMillis();
Log.i(TAG, String.format("Starting upload of %s record using a REST API", glucoseDataSets.size()));
apiStatus = doRESTUpload(prefs, glucoseDataSets, meterRecords, calRecords);
Log.i(TAG, String.format("Finished upload of %s record using a REST API in %s ms", glucoseDataSets.size(), System.currentTimeMillis() - start));
}
if (enableMongoUpload) {
double start = new Date().getTime();
mongoStatus = doMongoUpload(prefs, glucoseDataSets, meterRecords, calRecords);
Log.i(TAG, String.format("Finished upload of %s record using a Mongo in %s ms", glucoseDataSets.size() + meterRecords.size(), System.currentTimeMillis() - start));
}
return apiStatus || mongoStatus;
}
private boolean doRESTUpload(SharedPreferences prefs, List<BgReading> glucoseDataSets, List<Calibration> meterRecords, List<Calibration> calRecords) {
String baseURLSettings = prefs.getString("cloud_storage_api_base", "");
ArrayList<String> baseURIs = new ArrayList<String>();
try {
for (String baseURLSetting : baseURLSettings.split(" ")) {
String baseURL = baseURLSetting.trim();
if (baseURL.isEmpty()) continue;
baseURIs.add(baseURL + (baseURL.endsWith("/") ? "" : "/"));
}
} catch (Exception e) {
Log.e(TAG, "Unable to process API Base URL");
return false;
}
for (String baseURI : baseURIs) {
try {
doRESTUploadTo(baseURI, glucoseDataSets, meterRecords, calRecords);
} catch (Exception e) {
Log.e(TAG, "Unable to do REST API Upload");
return false;
}
}
return true;
}
private void doRESTUploadTo(String baseURI, List<BgReading> glucoseDataSets, List<Calibration> meterRecords, List<Calibration> calRecords) {
try {
int apiVersion = 0;
if (baseURI.endsWith("/v1/")) apiVersion = 1;
String baseURL = null;
String secret = null;
String[] uriParts = baseURI.split("@");
if (uriParts.length == 1 && apiVersion == 0) {
baseURL = uriParts[0];
} else if (uriParts.length == 1 && apiVersion > 0) {
throw new Exception("Starting with API v1, a pass phase is required");
} else if (uriParts.length == 2 && apiVersion > 0) {
secret = uriParts[0];
baseURL = uriParts[1];
} else {
throw new Exception("Unexpected baseURI");
}
String postURL = baseURL + "entries";
Log.i(TAG, "postURL: " + postURL);
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
DefaultHttpClient httpclient = new DefaultHttpClient(params);
HttpPost post = new HttpPost(postURL);
Header apiSecretHeader = null;
if (apiVersion > 0) {
if (secret == null || secret.isEmpty()) {
throw new Exception("Starting with API v1, a pass phase is required");
} else {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = secret.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b: bytes) {
sb.append(String.format("%02x", b & 0xff));
}
String token = sb.toString();
apiSecretHeader = new BasicHeader("api-secret", token);
}
}
if (apiSecretHeader != null) {
post.setHeader(apiSecretHeader);
}
for (BgReading record : glucoseDataSets) {
JSONObject json = new JSONObject();
try {
if (apiVersion >= 1)
populateV1APIBGEntry(json, record);
else
populateLegacyAPIEntry(json, record);
} catch (Exception e) {
Log.w(TAG, "Unable to populate entry");
continue;
}
String jsonString = json.toString();
Log.i(TAG, "SGV JSON: " + jsonString);
try {
StringEntity se = new StringEntity(jsonString);
post.setEntity(se);
post.setHeader("Accept", "application/json");
post.setHeader("Content-type", "application/json");
ResponseHandler responseHandler = new BasicResponseHandler();
httpclient.execute(post, responseHandler);
} catch (Exception e) {
Log.w(TAG, "Unable to populate entry");
}
}
if (apiVersion >= 1) {
for (Calibration record : meterRecords) {
JSONObject json = new JSONObject();
try {
populateV1APIMeterReadingEntry(json, record);
} catch (Exception e) {
Log.w(TAG, "Unable to populate entry");
continue;
}
String jsonString = json.toString();
Log.i(TAG, "MBG JSON: " + jsonString);
try {
StringEntity se = new StringEntity(jsonString);
post.setEntity(se);
post.setHeader("Accept", "application/json");
post.setHeader("Content-type", "application/json");
ResponseHandler responseHandler = new BasicResponseHandler();
httpclient.execute(post, responseHandler);
} catch (Exception e) {
Log.w(TAG, "Unable to post data");
}
}
}
if (apiVersion >= 1) {
for (Calibration calRecord : calRecords) {
JSONObject json = new JSONObject();
try {
populateV1APICalibrationEntry(json, calRecord);
} catch (Exception e) {
Log.w(TAG, "Unable to populate entry");
continue;
}
String jsonString = json.toString();
Log.i(TAG, "CAL JSON: " + jsonString);
try {
StringEntity se = new StringEntity(jsonString);
post.setEntity(se);
post.setHeader("Accept", "application/json");
post.setHeader("Content-type", "application/json");
ResponseHandler responseHandler = new BasicResponseHandler();
httpclient.execute(post, responseHandler);
} catch (Exception e) {
Log.w(TAG, "Unable to post data");
}
}
}
// TODO: this is a quick port from the original code and needs to be checked before release
postDeviceStatus(baseURL, apiSecretHeader, httpclient);
} catch (Exception e) {
Log.w(TAG, "Unable to post data");
}
}
private void populateV1APIBGEntry(JSONObject json, BgReading record) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss a");
format.setTimeZone(TimeZone.getDefault());
json.put("device", "xDrip-"+prefs.getString("dex_collection_method", "BluetoothWixel"));
json.put("date", record.timestamp);
json.put("dateString", format.format(record.timestamp));
json.put("sgv", (int)record.calculated_value);
json.put("direction", record.slopeName());
json.put("type", "sgv");
json.put("filtered", record.filtered_data * 1000);
json.put("unfiltered", record.age_adjusted_raw_value * 1000);
json.put("rssi", 100);
json.put("noise", Integer.valueOf(record.noiseValue()));
}
private void populateLegacyAPIEntry(JSONObject json, BgReading record) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss a");
format.setTimeZone(TimeZone.getDefault());
json.put("device", "xDrip-"+prefs.getString("dex_collection_method", "BluetoothWixel"));
json.put("date", record.timestamp);
json.put("dateString", format.format(record.timestamp));
json.put("sgv", (int)record.calculated_value);
json.put("direction", record.slopeName());
}
private void populateV1APIMeterReadingEntry(JSONObject json, Calibration record) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss a");
format.setTimeZone(TimeZone.getDefault());
json.put("device", "xDrip-"+prefs.getString("dex_collection_method", "BluetoothWixel"));
json.put("type", "mbg");
json.put("date", record.timestamp);
json.put("dateString", format.format(record.timestamp));
json.put("mbg", record.bg);
}
private void populateV1APICalibrationEntry(JSONObject json, Calibration record) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss a");
format.setTimeZone(TimeZone.getDefault());
json.put("device", "xDrip-" + prefs.getString("dex_collection_method", "BluetoothWixel"));
json.put("type", "cal");
json.put("date", record.timestamp);
json.put("dateString", format.format(record.timestamp));
if(record.check_in) {
json.put("slope", (long) (record.first_slope));
json.put("intercept", (long) ((record.first_intercept)));
json.put("scale", record.first_scale);
} else {
json.put("slope", (long) (record.slope * 1000));
json.put("intercept", (long) ((record.intercept * -1000) / (record.slope * 1000)));
json.put("scale", 1);
}
}
// TODO: this is a quick port from original code and needs to be refactored before release
private void postDeviceStatus(String baseURL, Header apiSecretHeader, DefaultHttpClient httpclient) throws Exception {
String devicestatusURL = baseURL + "devicestatus";
Log.i(TAG, "devicestatusURL: " + devicestatusURL);
JSONObject json = new JSONObject();
json.put("uploaderBattery", getBatteryLevel());
String jsonString = json.toString();
HttpPost post = new HttpPost(devicestatusURL);
if (apiSecretHeader != null) {
post.setHeader(apiSecretHeader);
}
StringEntity se = new StringEntity(jsonString);
post.setEntity(se);
post.setHeader("Accept", "application/json");
post.setHeader("Content-type", "application/json");
ResponseHandler responseHandler = new BasicResponseHandler();
httpclient.execute(post, responseHandler);
}
private boolean doMongoUpload(SharedPreferences prefs, List<BgReading> glucoseDataSets,
List<Calibration> meterRecords, List<Calibration> calRecords) {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss a");
format.setTimeZone(TimeZone.getDefault());
String dbURI = prefs.getString("cloud_storage_mongodb_uri", null);
String collectionName = prefs.getString("cloud_storage_mongodb_collection", null);
String dsCollectionName = prefs.getString("cloud_storage_mongodb_device_status_collection", "devicestatus");
if (dbURI != null && collectionName != null) {
try {
// connect to db
MongoClientURI uri = new MongoClientURI(dbURI.trim());
MongoClient client = new MongoClient(uri);
// get db
DB db = client.getDB(uri.getDatabase());
// get collection
DBCollection dexcomData = db.getCollection(collectionName.trim());
Log.i(TAG, "The number of EGV records being sent to MongoDB is " + glucoseDataSets.size());
for (BgReading record : glucoseDataSets) {
// make db object
BasicDBObject testData = new BasicDBObject();
testData.put("device", "xDrip-"+prefs.getString("dex_collection_method", "BluetoothWixel"));
testData.put("date", record.timestamp);
testData.put("dateString", format.format(record.timestamp));
testData.put("sgv", Math.round(record.calculated_value));
testData.put("direction", record.slopeName());
testData.put("type", "sgv");
testData.put("filtered", record.filtered_data * 1000);
testData.put("unfiltered", record.age_adjusted_raw_value * 1000 );
testData.put("rssi", 100);
testData.put("noise", Integer.valueOf(record.noiseValue()));
dexcomData.update(testData, testData, true, false, WriteConcern.UNACKNOWLEDGED);
}
Log.i(TAG, "The number of MBG records being sent to MongoDB is " + meterRecords.size());
for (Calibration meterRecord : meterRecords) {
// make db object
BasicDBObject testData = new BasicDBObject();
testData.put("device", "xDrip-"+prefs.getString("dex_collection_method", "BluetoothWixel"));
testData.put("type", "mbg");
testData.put("date", meterRecord.timestamp);
testData.put("dateString", format.format(meterRecord.timestamp));
testData.put("mbg", meterRecord.bg);
dexcomData.update(testData, testData, true, false, WriteConcern.UNACKNOWLEDGED);
}
for (Calibration calRecord : calRecords) {
// make db object
BasicDBObject testData = new BasicDBObject();
testData.put("device", "xDrip-"+prefs.getString("dex_collection_method", "BluetoothWixel"));
testData.put("date", calRecord.timestamp);
testData.put("dateString", format.format(calRecord.timestamp));
if(calRecord.check_in) {
testData.put("slope", (long) (calRecord.first_slope));
testData.put("intercept", (long) ((calRecord.first_intercept)));
testData.put("scale", calRecord.first_scale);
} else {
testData.put("slope", (long) (calRecord.slope * 1000));
testData.put("intercept", (long) ((calRecord.intercept * -1000) / (calRecord.slope * 1000)));
testData.put("scale", 1);
}
testData.put("type", "cal");
dexcomData.update(testData, testData, true, false, WriteConcern.UNACKNOWLEDGED);
}
// TODO: quick port from original code, revisit before release
DBCollection dsCollection = db.getCollection(dsCollectionName);
BasicDBObject devicestatus = new BasicDBObject();
devicestatus.put("uploaderBattery", getBatteryLevel());
devicestatus.put("created_at", new Date());
dsCollection.insert(devicestatus, WriteConcern.UNACKNOWLEDGED);
client.close();
return true;
} catch (Exception e) {
Log.e(TAG, "Unable to upload data to mongo");
}
}
return false;
}
public int getBatteryLevel() {
Intent batteryIntent = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
if(level == -1 || scale == -1) {
return 50;
}
return (int)(((float)level / (float)scale) * 100.0f);
}
}