diff --git a/app/build.gradle b/app/build.gradle index 321863d615cb36775b630d58e4edd1721a5f7a06..2d2734cdf908f6db8dfc2d17acdc0a24663db37f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'io.fabric.tools:gradle:1.21.6' - classpath 'io.realm:realm-gradle-plugin:1.1.1' + classpath 'io.realm:realm-gradle-plugin:2.3.1' classpath 'org.ajoberstar:grgit:1.5.0' } } @@ -161,4 +161,13 @@ dependencies { compile 'com.google.code.gson:gson:2.7' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.squareup.okhttp3:okhttp:3.3.1' + compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' + testCompile 'junit:junit:4.12' + testCompile 'org.powermock:powermock:1.6.5' + testCompile 'org.powermock:powermock-module-junit4:1.6.5' + testCompile 'org.powermock:powermock-api-mockito:1.6.5' + testCompile 'org.json:json:20140107' + + } \ No newline at end of file diff --git a/app/gradle.properties b/app/gradle.properties index 1dd0e26d2b59d84aa9b32ab72e3c5b99065efabe..c2066d43f1cea3abc965486bf1b3ce582535dfc0 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1 +1,2 @@ -version=0.5.0-SNAPSHOT +#version=0.5.0-SNAPSHOT +version=0.5.1-SNAPSHOT diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 80e56888d3b01828035953934ed4cb6cb4d761ef..1aae2ae9be366f96f9bef939cce177454d503f14 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -88,7 +88,7 @@ <receiver android:name=".medtronic.service.MedtronicCnlAlarmReceiver"></receiver> <meta-data android:name="io.fabric.ApiKey" - android:value="aa26e770bd4f7480eed7cabb63a84363ecd12009" /> + android:value="YOUR_FABRIC_KEY" /> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java index b958a2c3ac004079815a46121313dc9e74cf097a..b58190a910fc975082a7fd085d04d592d0a0aaf2 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java @@ -286,7 +286,7 @@ public class MedtronicCnlIntentService extends IntentService { MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); } } - + // reliable wake alarm manager wake up for all android versions public static void wakeUpIntent(Context context, long wakeTime, PendingIntent pendingIntent) { final AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java index ab95d50f6d346d506d3a6f03a992e53cfaa00804..d1f1f3ffc08b31c04357b90567a3b794869f59c7 100644 --- a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java @@ -81,6 +81,10 @@ public class PumpStatusEvent extends RealmObject { return CGM_TREND.valueOf(cgmTrend); } + public String getCgmTrendString() { + return cgmTrend; + } + public void setCgmTrend(CGM_TREND cgmTrend) { this.cgmTrend = cgmTrend.name(); } diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/StatusEvent.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/StatusEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..7a99616c93aaa6cd6b2ce081e5fb0f00502dc3aa --- /dev/null +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/StatusEvent.java @@ -0,0 +1,301 @@ +package info.nightscout.android.model.medtronicNg; + +import java.util.Date; + +// Pump Data without Realm +// So that we can unit test the upload code in a modular fashion, +// without the Realm and Android Context dependencies. + +public class StatusEvent { + + public StatusEvent(PumpStatusEvent pumpStatusEvent) { + eventDate = pumpStatusEvent.getEventDate(); + pumpDate = pumpStatusEvent.getPumpDate(); + deviceName = pumpStatusEvent.getDeviceName(); + suspended = pumpStatusEvent.isSuspended(); + bolusing = pumpStatusEvent.isBolusing(); + deliveringInsulin = pumpStatusEvent.isDeliveringInsulin(); + tempBasalActive = pumpStatusEvent.isTempBasalActive(); + cgmActive = pumpStatusEvent.isCgmActive(); + activeBasalPattern = pumpStatusEvent.getActiveBasalPattern(); + basalRate = pumpStatusEvent.getBasalRate(); + tempBasalRate = pumpStatusEvent.getTempBasalRate(); + tempBasalPercentage = pumpStatusEvent.getTempBasalPercentage(); + tempBasalMinutesRemaining = pumpStatusEvent.getTempBasalMinutesRemaining(); + basalUnitsDeliveredToday = pumpStatusEvent.getBasalUnitsDeliveredToday(); + batteryPercentage = pumpStatusEvent.getBatteryPercentage(); + reservoirAmount = pumpStatusEvent.getReservoirAmount(); + minutesOfInsulinRemaining = pumpStatusEvent.getMinutesOfInsulinRemaining(); + activeInsulin = pumpStatusEvent.getActiveInsulin(); + sgv = pumpStatusEvent.getSgv(); + sgvDate = pumpStatusEvent.getSgvDate(); + lowSuspendActive = pumpStatusEvent.isLowSuspendActive(); + cgmTrend = pumpStatusEvent.getCgmTrendString(); + } + + public StatusEvent() { + } + + + private Date eventDate; // The actual time of the event (assume the capture device eventDate/time is accurate) + private Date pumpDate; // The eventDate/time on the pump at the time of the event + private String deviceName; + + // Data from the Medtronic Pump Status message + private boolean suspended; + private boolean bolusing; + private boolean deliveringInsulin; + private boolean tempBasalActive; + private boolean cgmActive; + private byte activeBasalPattern; + private float basalRate; + private float tempBasalRate; + private byte tempBasalPercentage; + private short tempBasalMinutesRemaining; + private float basalUnitsDeliveredToday; + private short batteryPercentage; + private float reservoirAmount; + private short minutesOfInsulinRemaining; // 25h == "more than 1 day" + private float activeInsulin; + private int sgv; + private Date sgvDate; + private boolean lowSuspendActive; + private String cgmTrend; + + private boolean recentBolusWizard; // Whether a bolus wizard has been run recently + private int bolusWizardBGL; // in mg/dL. 0 means no recent bolus wizard reading. + + private long pumpTimeOffset; // millis the pump is ahead + + private boolean uploaded = false; + + public Date getEventDate() { + return eventDate; + } + + public void setEventDate(Date eventDate) { + this.eventDate = eventDate; + } + + public Date getPumpDate() { + return pumpDate; + } + + public void setPumpDate(Date pumpDate) { + this.pumpDate = pumpDate; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public int getSgv() { + return sgv; + } + + public void setSgv(int sgv) { + this.sgv = sgv; + } + + public CGM_TREND getCgmTrend() { + return CGM_TREND.valueOf(cgmTrend); + } + + public void setCgmTrend(CGM_TREND cgmTrend) { + this.cgmTrend = cgmTrend.name(); + } + + public void setCgmTrend(String cgmTrend) { + this.cgmTrend = cgmTrend; + } + + public float getActiveInsulin() { + return activeInsulin; + } + + public void setActiveInsulin(float activeInsulin) { + this.activeInsulin = activeInsulin; + } + + public short getBatteryPercentage() { + return batteryPercentage; + } + + public void setBatteryPercentage(short batteryPercentage) { + this.batteryPercentage = batteryPercentage; + } + + public float getReservoirAmount() { + return reservoirAmount; + } + + public void setReservoirAmount(float reservoirAmount) { + this.reservoirAmount = reservoirAmount; + } + + public boolean hasRecentBolusWizard() { + return recentBolusWizard; + } + + public int getBolusWizardBGL() { + return bolusWizardBGL; + } + + public void setBolusWizardBGL(int bolusWizardBGL) { + this.bolusWizardBGL = bolusWizardBGL; + } + + public boolean isUploaded() { + return uploaded; + } + + public void setUploaded(boolean uploaded) { + this.uploaded = uploaded; + } + + public boolean isSuspended() { + return suspended; + } + + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + + public boolean isBolusing() { + return bolusing; + } + + public void setBolusing(boolean bolusing) { + this.bolusing = bolusing; + } + + public boolean isDeliveringInsulin() { + return deliveringInsulin; + } + + public void setDeliveringInsulin(boolean deliveringInsulin) { + this.deliveringInsulin = deliveringInsulin; + } + + public boolean isTempBasalActive() { + return tempBasalActive; + } + + public void setTempBasalActive(boolean tempBasalActive) { + this.tempBasalActive = tempBasalActive; + } + + public boolean isCgmActive() { + return cgmActive; + } + + public void setCgmActive(boolean cgmActive) { + this.cgmActive = cgmActive; + } + + public byte getActiveBasalPattern() { + return activeBasalPattern; + } + + public void setActiveBasalPattern(byte activeBasalPattern) { + this.activeBasalPattern = activeBasalPattern; + } + + public float getBasalRate() { + return basalRate; + } + + public void setBasalRate(float basalRate) { + this.basalRate = basalRate; + } + + public float getTempBasalRate() { + return tempBasalRate; + } + + public void setTempBasalRate(float tempBasalRate) { + this.tempBasalRate = tempBasalRate; + } + + public byte getTempBasalPercentage() { + return tempBasalPercentage; + } + + public void setTempBasalPercentage(byte tempBasalPercentage) { + this.tempBasalPercentage = tempBasalPercentage; + } + + public short getTempBasalMinutesRemaining() { + return tempBasalMinutesRemaining; + } + + public void setTempBasalMinutesRemaining(short tempBasalMinutesRemaining) { + this.tempBasalMinutesRemaining = tempBasalMinutesRemaining; + } + + public float getBasalUnitsDeliveredToday() { + return basalUnitsDeliveredToday; + } + + public void setBasalUnitsDeliveredToday(float basalUnitsDeliveredToday) { + this.basalUnitsDeliveredToday = basalUnitsDeliveredToday; + } + + public short getMinutesOfInsulinRemaining() { + return minutesOfInsulinRemaining; + } + + public void setMinutesOfInsulinRemaining(short minutesOfInsulinRemaining) { + this.minutesOfInsulinRemaining = minutesOfInsulinRemaining; + } + + public Date getSgvDate() { + return sgvDate; + } + + public void setSgvDate(Date sgvDate) { + this.sgvDate = sgvDate; + } + + public boolean isLowSuspendActive() { + return lowSuspendActive; + } + + public void setLowSuspendActive(boolean lowSuspendActive) { + this.lowSuspendActive = lowSuspendActive; + } + + public boolean isRecentBolusWizard() { + return recentBolusWizard; + } + + public void setRecentBolusWizard(boolean recentBolusWizard) { + this.recentBolusWizard = recentBolusWizard; + } + + public long getPumpTimeOffset() { + return pumpTimeOffset; + } + + public void setPumpTimeOffset(long pumpTimeOffset) { + this.pumpTimeOffset = pumpTimeOffset; + } + + public enum CGM_TREND { + NONE, + DOUBLE_UP, + SINGLE_UP, + FOURTY_FIVE_UP, + FLAT, + FOURTY_FIVE_DOWN, + SINGLE_DOWN, + DOUBLE_DOWN, + NOT_COMPUTABLE, + RATE_OUT_OF_RANGE, + NOT_SET + } +} diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed98bda91ef45a82af41b18e5c8917e7b223882 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java @@ -0,0 +1,166 @@ +package info.nightscout.android.upload.nightscout; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import info.nightscout.android.model.medtronicNg.StatusEvent; +import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; + +import android.support.annotation.NonNull; +import info.nightscout.api.UploadApi; +import info.nightscout.api.GlucoseEndpoints; +import info.nightscout.api.BolusEndpoints.BolusEntry; +import info.nightscout.api.GlucoseEndpoints.GlucoseEntry; +import info.nightscout.api.BolusEndpoints; +import info.nightscout.api.DeviceEndpoints; +import info.nightscout.api.DeviceEndpoints.Iob; +import info.nightscout.api.DeviceEndpoints.Battery; +import info.nightscout.api.DeviceEndpoints.PumpStatus; +import info.nightscout.api.DeviceEndpoints.PumpInfo; +import info.nightscout.api.DeviceEndpoints.DeviceStatus; + +public class NightScoutUpload { + + private static final String TAG = NightscoutUploadIntentService.class.getSimpleName(); + private static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + + public NightScoutUpload () { + + } + + public Boolean doRESTUpload(String url, + String secret, + int uploaderBatteryLevel, + List<StatusEvent> records) { + Boolean success = false; + try { + success = isUploaded(records, url, secret, uploaderBatteryLevel); + } catch (Exception e) { + Log.e(TAG, "Unable to do REST API Upload to: " + url, e); + } + return success; + } + + + private boolean isUploaded(List<StatusEvent> records, + String baseURL, + String secret, + int uploaderBatteryLevel) throws Exception { + + UploadApi uploadApi = new UploadApi(baseURL, formToken(secret)); + + boolean eventsUploaded = uploadEvents(uploadApi.getGlucoseEndpoints(), + uploadApi.getBolusApi(), + records ); + + boolean deviceStatusUploaded = uploadDeviceStatus(uploadApi.getDeviceEndpoints(), + uploaderBatteryLevel, records); + + return eventsUploaded && deviceStatusUploaded; + } + + private boolean uploadEvents(GlucoseEndpoints glucoseEndpoints, + BolusEndpoints bolusEndpoints, + List<StatusEvent> records ) throws Exception { + + + List<GlucoseEntry> glucoseEntries = new ArrayList<>(); + List<BolusEntry> bolusEntries = new ArrayList<>(); + + for (StatusEvent record : records) { + + GlucoseEntry glucoseEntry = new GlucoseEntry(); + + glucoseEntry.setType("sgv"); + glucoseEntry.setDirection(EntriesSerializer.getDirectionStringStatus(record.getCgmTrend())); + glucoseEntry.setDevice(record.getDeviceName()); + glucoseEntry.setSgv(record.getSgv()); + glucoseEntry.setDate(record.getEventDate().getTime()); + glucoseEntry.setDateString(record.getEventDate().toString()); + + glucoseEntries.add(glucoseEntry); + glucoseEndpoints.sendEntries(glucoseEntries).execute(); + + BolusEntry bolusEntry = new BolusEntry(); + + bolusEntry.setType("mbg"); + bolusEntry.setDate(record.getEventDate().getTime()); + bolusEntry.setDateString(record.getEventDate().toString()); + bolusEntry.setDevice(record.getDeviceName()); + bolusEntry.setMbg(record.getBolusWizardBGL()); + + bolusEntries.add(bolusEntry); + bolusEndpoints.sendEntries(bolusEntries).execute(); + + } + + return true; + } + + private boolean uploadDeviceStatus(DeviceEndpoints deviceEndpoints, + int uploaderBatteryLevel, + List<StatusEvent> records) throws Exception { + + + List<DeviceStatus> deviceEntries = new ArrayList<>(); + for (StatusEvent record : records) { + + Iob iob = new Iob(record.getPumpDate(), record.getActiveInsulin()); + Battery battery = new Battery(record.getBatteryPercentage()); + PumpStatus pumpstatus; + if (record.isBolusing()) { + pumpstatus = new PumpStatus(true, false, ""); + + } else if (record.isSuspended()) { + pumpstatus = new PumpStatus(false, true, ""); + } else { + pumpstatus = new PumpStatus(false, false, "normal"); + } + + PumpInfo pumpInfo = new PumpInfo( + ISO8601_DATE_FORMAT.format(record.getPumpDate()), + new BigDecimal(record.getReservoirAmount()).setScale(3, BigDecimal.ROUND_HALF_UP), + iob, + battery, + pumpstatus + ); + + DeviceStatus deviceStatus = new DeviceStatus( + uploaderBatteryLevel, + record.getDeviceName(), + ISO8601_DATE_FORMAT.format(record.getPumpDate()), + pumpInfo + ); + + deviceEntries.add(deviceStatus); + } + + for (DeviceStatus status: deviceEntries) { + deviceEndpoints.sendDeviceStatus(status).execute(); + } + + return true ; + } + + @NonNull + private String formToken(String secret) throws NoSuchAlgorithmException, UnsupportedEncodingException { + 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)); + } + return sb.toString(); + } + +} diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java index c613dfbb5112dad61676c85cd04ebe520c9b56af..07cba2e8fdbe2f7bc0ae100d436fdb619b78d910 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java @@ -2,7 +2,6 @@ package info.nightscout.android.upload.nightscout; import retrofit2.Call; import retrofit2.http.GET; - /** * Created by lgoedhart on 26/06/2016. */ diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java index 6ac7129878dfdcf45118b3f1f4cf83538766de24..0c7c4e382a8624d4eb5da64452643b66f5ef98de 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java @@ -9,43 +9,25 @@ import android.net.NetworkInfo; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; - -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -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.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.math.BigDecimal; -import java.net.URL; -import java.security.MessageDigest; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import info.nightscout.android.R; import info.nightscout.android.medtronic.MainActivity; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; -import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; +import info.nightscout.android.model.medtronicNg.StatusEvent; import io.realm.Realm; import io.realm.RealmResults; public class NightscoutUploadIntentService extends IntentService { private static final String TAG = NightscoutUploadIntentService.class.getSimpleName(); - private static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); - private static final int SOCKET_TIMEOUT = 60 * 1000; - private static final int CONNECTION_TIMEOUT = 30 * 1000; + Context mContext; private Realm mRealm; + private NightScoutUpload mNightScoutUpload; public NightscoutUploadIntentService() { super(NightscoutUploadIntentService.class.getName()); @@ -64,6 +46,7 @@ public class NightscoutUploadIntentService extends IntentService { Log.i(TAG, "onCreate called"); mContext = this.getBaseContext(); + mNightScoutUpload = new NightScoutUpload(); } @Override @@ -85,7 +68,18 @@ public class NightscoutUploadIntentService extends IntentService { if (enableRESTUpload) { long start = System.currentTimeMillis(); Log.i(TAG, String.format("Starting upload of %s record using a REST API", records.size())); - doRESTUpload(prefs, records); + String urlSetting = prefs.getString(mContext.getString(R.string.preference_nightscout_url), ""); + String secretSetting = prefs.getString(mContext.getString(R.string.preference_api_secret), "YOURAPISECRET"); + List<StatusEvent> statusEvents = getStatusEvents(records); + int uploaderBatteryLevel = MainActivity.batLevel; + Boolean uploadSuccess = mNightScoutUpload.doRESTUpload(urlSetting, secretSetting, uploaderBatteryLevel, statusEvents); + if (uploadSuccess) { + mRealm.beginTransaction(); + for (PumpStatusEvent updateRecord : records) { + updateRecord.setUploaded(true); + } + mRealm.commitTransaction(); + } Log.i(TAG, String.format("Finished upload of %s record using a REST API in %s ms", records.size(), System.currentTimeMillis() - start)); } } catch (Exception e) { @@ -98,216 +92,16 @@ public class NightscoutUploadIntentService extends IntentService { NightscoutUploadReceiver.completeWakefulIntent(intent); } - private void doRESTUpload(SharedPreferences prefs, RealmResults<PumpStatusEvent> records) { - String apiScheme = "https://"; - String apiUrl = ""; - String apiSecret = prefs.getString(mContext.getString(R.string.preference_api_secret), "YOURAPISECRET"); - - // TODO - this code needs to go to the Settings Activity. - // Add the extra match for "KEY@" to support the previous single field - Pattern p = Pattern.compile("(.*\\/\\/)?(.*@)?([^\\/]*)(.*)"); - Matcher m = p.matcher(prefs.getString(mContext.getString(R.string.preference_nightscout_url), "")); - - if (m.find()) { - apiUrl = m.group(3); - - // Only override apiSecret from URL (the "old" way), if the API secret preference is empty - if (apiSecret.equals("YOURAPISECRET") || apiSecret.equals("")) { - apiSecret = (m.group(2) == null) ? "" : m.group(2).replace("@", ""); - } - - // Override the URI scheme if it's been provided in the preference) - if (m.group(1) != null && !m.group(1).equals("")) { - apiScheme = m.group(1); - } - } - - // Update the preferences to match what we expect. Only really used from converting from the - // old format to the new format. Aren't we nice for managing backward compatibility? - prefs.edit().putString(mContext.getString(R.string.preference_api_secret), apiSecret).apply(); - prefs.edit().putString(mContext.getString(R.string.preference_nightscout_url), String.format("%s%s", apiScheme, apiUrl)).apply(); - - String uploadUrl = String.format("%s%s@%s/api/v1/", apiScheme, apiSecret, apiUrl); - - try { - doRESTUploadTo(uploadUrl, records); - } catch (Exception e) { - Log.e(TAG, "Unable to do REST API Upload to: " + uploadUrl, e); + private List<StatusEvent> getStatusEvents(RealmResults<PumpStatusEvent> records) { + List<StatusEvent> statusEvents = new ArrayList<>(); + for (PumpStatusEvent record : records) { + StatusEvent event = new StatusEvent(record); + statusEvents.add(event); } - } - - private void doRESTUploadTo(String baseURI, RealmResults<PumpStatusEvent> records) { - try { - String baseURL; - String secret = null; - String[] uriParts = baseURI.split("@"); - - if (uriParts.length == 1) { - throw new Exception("Starting with API v1, a pass phase is required"); - } else if (uriParts.length == 2) { - secret = uriParts[0]; - baseURL = uriParts[1]; - - // new format URL! - if (secret.contains("http")) { - if (secret.contains("https")) { - baseURL = "https://" + baseURL; - } else { - baseURL = "http://" + baseURL; - } - String[] uriParts2 = secret.split("//"); - secret = uriParts2[1]; - } - } else { - throw new Exception(String.format("Unexpected baseURI: %s, uriParts.length: %s", baseURI, uriParts.length)); - } - - JSONArray devicestatusBody = new JSONArray(); - JSONArray entriesBody = new JSONArray(); - - for (PumpStatusEvent record : records) { - addDeviceStatus(devicestatusBody, record); - addSgvEntry(entriesBody, record); - addMbgEntry(entriesBody, record); - } - - boolean isUploaded = uploadToNightscout(new URL(baseURL + "/entries"), secret, entriesBody); - - for(int i = 0; isUploaded && i < devicestatusBody.length(); i++) { - isUploaded &= uploadToNightscout(new URL(baseURL + "/devicestatus"), secret, devicestatusBody.getJSONObject(i)); - } - - if (isUploaded) { - // Yay! We uploaded. Tell Realm - // FIXME - check the upload succeeded! - mRealm.beginTransaction(); - for (PumpStatusEvent updateRecord : records) { - updateRecord.setUploaded(true); - } - mRealm.commitTransaction(); - } - - } catch (Exception e) { - Log.e(TAG, "Unable to post data", e); - } - } - - private boolean uploadToNightscout(URL endpoint, String secret, JSONObject httpBody) throws Exception { - return uploadToNightscout(endpoint, secret, httpBody.toString()); - } - private boolean uploadToNightscout(URL endpoint, String secret, JSONArray httpBody) throws Exception { - return uploadToNightscout(endpoint, secret, httpBody.toString()); + return statusEvents; } - private boolean uploadToNightscout(URL endpoint, String secret, String httpBody) throws Exception { - Log.i(TAG, "postURL: " + endpoint.toString()); - - HttpPost post = new HttpPost(endpoint.toString()); - - 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(); - post.setHeader("api-secret", token); - } - - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - - DefaultHttpClient httpclient = new DefaultHttpClient(params); - - Log.i(TAG, "Upload JSON: " + httpBody); - - try { - StringEntity se = new StringEntity(httpBody); - 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 to: '" + post.getURI().toString() + "'", e); - return false; - } - - return true; - } - - private void addDeviceStatus(JSONArray devicestatusArray, PumpStatusEvent record) throws Exception { - JSONObject json = new JSONObject(); - json.put("uploaderBattery", MainActivity.batLevel); - json.put("device", record.getDeviceName()); - json.put("created_at", ISO8601_DATE_FORMAT.format(record.getPumpDate())); - - JSONObject pumpInfo = new JSONObject(); - pumpInfo.put("clock", ISO8601_DATE_FORMAT.format(record.getPumpDate())); - pumpInfo.put("reservoir", new BigDecimal(record.getReservoirAmount()).setScale(3, BigDecimal.ROUND_HALF_UP)); - - JSONObject iob = new JSONObject(); - iob.put("timestamp", record.getPumpDate()); - iob.put("bolusiob", record.getActiveInsulin()); - - JSONObject status = new JSONObject(); - if (record.isBolusing()) { - status.put("bolusing", true); - } else if (record.isSuspended()) { - status.put("suspended", true); - } else { - status.put("status", "normal"); - } - - JSONObject battery = new JSONObject(); - battery.put("percent", record.getBatteryPercentage()); - - pumpInfo.put("iob", iob); - pumpInfo.put("battery", battery); - pumpInfo.put("status", status); - - json.put("pump", pumpInfo); - String jsonString = json.toString(); - Log.i(TAG, "Device Status JSON: " + jsonString); - - devicestatusArray.put(json); - } - - private void addSgvEntry(JSONArray entriesArray, PumpStatusEvent pumpRecord) throws Exception { - JSONObject json = new JSONObject(); - // TODO replace with Retrofit/EntriesSerializer - json.put("sgv", pumpRecord.getSgv()); - json.put("direction", EntriesSerializer.getDirectionString(pumpRecord.getCgmTrend())); - json.put("device", pumpRecord.getDeviceName()); - json.put("type", "sgv"); - json.put("date", pumpRecord.getEventDate().getTime()); - json.put("dateString", pumpRecord.getEventDate()); - - entriesArray.put(json); - } - - private void addMbgEntry(JSONArray entriesArray, PumpStatusEvent pumpRecord) throws Exception { - if (pumpRecord.hasRecentBolusWizard()) { - JSONObject json = new JSONObject(); - - // TODO replace with Retrofit/EntriesSerializer - json.put("type", "mbg"); - json.put("mbg", pumpRecord.getBolusWizardBGL()); - json.put("device", pumpRecord.getDeviceName()); - json.put("date", pumpRecord.getEventDate().getTime()); - json.put("dateString", pumpRecord.getEventDate()); - - entriesArray.put(json); - } - } private boolean isOnline() { ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -320,4 +114,6 @@ public class NightscoutUploadIntentService extends IntentService { public static final String EXTENDED_DATA = "info.nightscout.android.upload.nightscout.DATA"; } + + } diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java b/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java index 260b8c3b2acab0a24e81dd6c35dee9f9872f4e21..61785e93360db26a00d83a922beb25ed6c24ce2b 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java @@ -8,6 +8,7 @@ import com.google.gson.JsonSerializer; import java.lang.reflect.Type; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; +import info.nightscout.android.model.medtronicNg.StatusEvent; /** * Created by lgoedhart on 26/06/2016. @@ -42,6 +43,35 @@ public class EntriesSerializer implements JsonSerializer<PumpStatusEvent> { } } + public static String getDirectionStringStatus(StatusEvent.CGM_TREND trend) { + switch( trend ) { + case NONE: + return "NONE"; + case DOUBLE_UP: + return "DoubleUp"; + case SINGLE_UP: + return "SingleUp"; + case FOURTY_FIVE_UP: + return "FortyFiveUp"; + case FLAT: + return "Flat"; + case FOURTY_FIVE_DOWN: + return "FortyFiveDown"; + case SINGLE_DOWN: + return "SingleDown"; + case DOUBLE_DOWN: + return "DoubleDown"; + case NOT_COMPUTABLE: + return "NOT COMPUTABLE"; + case RATE_OUT_OF_RANGE: + return "RATE OUT OF RANGE"; + case NOT_SET: + return "NONE"; + default: + return "NOT COMPUTABLE"; // TODO - should this be something else? + } + } + @Override public JsonElement serialize(PumpStatusEvent src, Type typeOfSrc, JsonSerializationContext context) { final JsonObject jsonObject = new JsonObject(); diff --git a/app/src/main/java/info/nightscout/api/BolusEndpoints.java b/app/src/main/java/info/nightscout/api/BolusEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..67d2291198532976c64d25eae16718087cb28f43 --- /dev/null +++ b/app/src/main/java/info/nightscout/api/BolusEndpoints.java @@ -0,0 +1,76 @@ +package info.nightscout.api; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface BolusEndpoints { + + class BolusEntry { + String type; + String dateString; + float date; + float mbg; + String device; + + public BolusEntry() { } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDateString() { + return dateString; + } + + public void setDateString(String dateString) { + this.dateString = dateString; + } + + public float getDate() { + return date; + } + + public void setDate(float date) { + this.date = date; + } + + public float getMbg() { + return mbg; + } + + public void setMbg(float mbg) { + this.mbg = mbg; + } + + public String getDevice() { + return device; + } + + public void setDevice(String device) { + this.device = device; + } + + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/entries") + Call<ResponseBody> sendEntries(@Body List<BolusEntry> entries); + +} + + + diff --git a/app/src/main/java/info/nightscout/api/DeviceEndpoints.java b/app/src/main/java/info/nightscout/api/DeviceEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..1bf66b24336e790d69c464ed1acb419c029d433d --- /dev/null +++ b/app/src/main/java/info/nightscout/api/DeviceEndpoints.java @@ -0,0 +1,97 @@ +package info.nightscout.api; + +import java.math.BigDecimal; +import java.util.Date; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface DeviceEndpoints { + + class Iob { + final Date timestamp; + final float bolusiob; + public Iob (Date timestamp, + float bolusiob) { + this.timestamp = timestamp; + this.bolusiob = bolusiob; + } + } + + class Battery { + final short percent; + public Battery(short percent) { + this.percent = percent; + } + } + + class PumpStatus { + final Boolean bolusing; + final Boolean suspended; + final String status; + public PumpStatus( + Boolean bolusing, + Boolean suspended, + String status + + ) { + this.bolusing = bolusing; + this.suspended = suspended; + this.status = status; + + } + } + + class PumpInfo { + final String clock; + final BigDecimal reservoir; + final Iob iob; + final Battery battery; + final PumpStatus status; + + public PumpInfo(String clock, + BigDecimal reservoir, + Iob iob, + Battery battery, + PumpStatus status) { + this.clock = clock; + this.reservoir = reservoir; + this.iob = iob; + this.battery = battery; + this.status = status; + + } + } + + class DeviceStatus { + final Integer uploaderBattery; + final String device; + final String created_at; + final PumpInfo pump; + + public DeviceStatus(Integer uploaderBattery, + String device, + String created_at, + PumpInfo pump) { + this.uploaderBattery = uploaderBattery; + this.device = device; + this.created_at = created_at; + this.pump = pump; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/devicestatus") + Call<ResponseBody> sendDeviceStatus(@Body DeviceStatus deviceStatus); + +} + + + diff --git a/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java b/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..07c1c10a8c2e60705ba348d7c9a0758216f74b1a --- /dev/null +++ b/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java @@ -0,0 +1,89 @@ +package info.nightscout.api; + + + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface GlucoseEndpoints { + + class GlucoseEntry { + + String type; + String dateString; + float date; + float sgv; + String direction; + String device; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDateString() { + return dateString; + } + + public void setDateString(String dateString) { + this.dateString = dateString; + } + + public float getDate() { + return date; + } + + public void setDate(float date) { + this.date = date; + } + + public float getSgv() { + return sgv; + } + + public void setSgv(float sgv) { + this.sgv = sgv; + } + + public String getDirection() { + return direction; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + public String getDevice() { + return device; + } + + public void setDevice(String device) { + this.device = device; + } + + public GlucoseEntry() { } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/entries") + Call<ResponseBody> sendEntries(@Body List<GlucoseEntry> entries); + +} + + + diff --git a/app/src/main/java/info/nightscout/api/UploadApi.java b/app/src/main/java/info/nightscout/api/UploadApi.java new file mode 100644 index 0000000000000000000000000000000000000000..1c429b7379d21f7342a199b7c50d1ff87aebe043 --- /dev/null +++ b/app/src/main/java/info/nightscout/api/UploadApi.java @@ -0,0 +1,78 @@ +package info.nightscout.api; + + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class UploadApi { + private Retrofit retrofit; + private GlucoseEndpoints glucoseEndpoints; + private BolusEndpoints bolusApi; + private DeviceEndpoints deviceEndpoints; + + public GlucoseEndpoints getGlucoseEndpoints() { + return glucoseEndpoints; + } + + public BolusEndpoints getBolusApi() { + return bolusApi; + } + + public DeviceEndpoints getDeviceEndpoints() { + return deviceEndpoints; + } + + public UploadApi(String baseURL, String token) { + + class AddAuthHeader implements Interceptor { + + private String token; + + public AddAuthHeader(String token) { + this.token = token; + } + + @Override + public Response intercept(Interceptor.Chain chain) throws IOException { + Request original = chain.request(); + + Request.Builder requestBuilder = original.newBuilder() + .header("api-secret", token) + .method( original.method(), original.body()); + + Request request = requestBuilder.build(); + return chain.proceed(request); + } + }; + + OkHttpClient.Builder okHttpClient = new OkHttpClient().newBuilder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS); + + okHttpClient.addInterceptor(new AddAuthHeader(token)); + + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + logging.setLevel(HttpLoggingInterceptor.Level.BODY); + okHttpClient.addInterceptor(logging); + + retrofit = new Retrofit.Builder() + .baseUrl(baseURL) + .client(okHttpClient.build()) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + glucoseEndpoints = retrofit.create(GlucoseEndpoints.class); + bolusApi = retrofit.create(BolusEndpoints.class); + deviceEndpoints = retrofit.create(DeviceEndpoints.class); + + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89751b6c042dc6aca1482597467d1d6cfffab63f..8665fb4b355532d11319c8d9ee3a86311eae30af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,8 +31,6 @@ <string name="error_class_not_found_exception">Application code error.</string> <string name="error_http_response">Server responded with error. Could be username or password problem.</string> <string name="error_field_required">This field is required</string> - <string name="preference_nightscout_url">Nightscout URL</string> - <string name="preference_api_secret">API SECRET</string> <string name="prompt_carelink_username_password">Please enter your CareLink details.\nThey will not be stored.</string> <string name="close">Close</string> <string name="register_contour_next_link">Registered Devices</string> @@ -55,4 +53,6 @@ <string name="dummy_button">Dummy Button</string> <string name="dummy_content">DUMMY\nCONTENT</string> <string name="menu_name_status">Status</string> + <string name="preference_api_secret">YOUR.API.SECRET</string> + <string name="preference_nightscout_url">YOUR.NIGHTSCOUT.URL</string> </resources> diff --git a/app/src/testDebug/java/info/nightscout/android/upload/nightscout/NightScoutUploadTest.java b/app/src/testDebug/java/info/nightscout/android/upload/nightscout/NightScoutUploadTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b46150ac0bbd03f7debe5699e9868efc28ad960b --- /dev/null +++ b/app/src/testDebug/java/info/nightscout/android/upload/nightscout/NightScoutUploadTest.java @@ -0,0 +1,79 @@ +package info.nightscout.android.upload.nightscout; + +import android.util.Log; + +import info.nightscout.android.model.medtronicNg.PumpStatusEvent; +import info.nightscout.android.model.medtronicNg.StatusEvent; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({Log.class}) +public class NightScoutUploadTest { + + NightScoutUpload mUploader; + List<StatusEvent> mEvents; + + @Before + public void doSetup() { + + mUploader = new NightScoutUpload(); + + mEvents = new ArrayList<StatusEvent>(); + + StatusEvent event1 = new StatusEvent(); + Date date = new Date(); + event1.setEventDate(date); + event1.setPumpDate(date); + event1.setDeviceName("MyDevice"); + event1.setSgv(100); + event1.setCgmTrend(StatusEvent.CGM_TREND.FLAT); + event1.setActiveInsulin(5.0f); + event1.setBatteryPercentage((short)100); + event1.setReservoirAmount(50.0f); + event1.setRecentBolusWizard(false); + event1.setBolusWizardBGL(100); + event1.setUploaded(false); + event1.setSuspended(false); + event1.setDeliveringInsulin(false); + event1.setTempBasalActive(false); + event1.setCgmActive(false); + event1.setActiveBasalPattern((byte)42); + event1.setBasalRate(1.0f); + event1.setTempBasalPercentage((byte)80); + event1.setTempBasalMinutesRemaining((short)30); + event1.setBasalUnitsDeliveredToday(30.0f); + event1.setMinutesOfInsulinRemaining((short)45); + event1.setSgvDate(date); + event1.setLowSuspendActive(false); + event1.setPumpTimeOffset(100000); + + mEvents.add(event1); + + } + + @Test + public void doTest() { + PowerMockito.mockStatic(Log.class); + String url = "https://my.azurewebsites.net"; + String secret = "SECRET"; + int uploaderBatteryLevel = 50; + + Boolean success = mUploader.doRESTUpload(url, secret, uploaderBatteryLevel, mEvents); + assertEquals(success, true); + } + +} \ No newline at end of file