From dd637021f7d60b34419897a4134b44e32c0ba0e7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Jun 2019 00:48:37 +0200 Subject: [PATCH 01/39] tidepool xdrip port --- app/build.gradle | 10 +- .../eveningoutpost/dexdrip/Models/JoH.java | 126 +++++++ .../dexdrip/tidepool/BaseElement.java | 40 ++ .../dexdrip/tidepool/BaseMessage.java | 24 ++ .../dexdrip/tidepool/DateUtil.java | 46 +++ .../dexdrip/tidepool/EBasal.java | 43 +++ .../dexdrip/tidepool/EBloodGlucose.java | 45 +++ .../dexdrip/tidepool/EBolus.java | 26 ++ .../dexdrip/tidepool/ESensorGlucose.java | 37 ++ .../dexdrip/tidepool/EWizard.java | 34 ++ .../tidepool/GzipRequestInterceptor.java | 48 +++ .../dexdrip/tidepool/InfoInterceptor.java | 32 ++ .../dexdrip/tidepool/MAuthReply.java | 36 ++ .../dexdrip/tidepool/MAuthRequest.java | 24 ++ .../tidepool/MCloseDatasetRequest.java | 9 + .../dexdrip/tidepool/MDatasetReply.java | 47 +++ .../dexdrip/tidepool/MGetDatasetsRequest.java | 4 + .../dexdrip/tidepool/MOpenDatasetRequest.java | 63 ++++ .../dexdrip/tidepool/MUploadReply.java | 9 + .../dexdrip/tidepool/Session.java | 54 +++ .../dexdrip/tidepool/TidepoolCallback.java | 67 ++++ .../dexdrip/tidepool/TidepoolEntry.java | 28 ++ .../dexdrip/tidepool/TidepoolStatus.java | 31 ++ .../dexdrip/tidepool/TidepoolUploader.java | 342 ++++++++++++++++++ .../dexdrip/tidepool/UploadChunk.java | 182 ++++++++++ app/src/main/res/values/strings.xml | 14 + 26 files changed, 1420 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java diff --git a/app/build.gradle b/app/build.gradle index cef63558b9..b374db47b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -250,7 +250,7 @@ dependencies { // excluding org.json which is provided by Android exclude group: "org.json", module: "json" } - implementation "com.google.code.gson:gson:2.8.2" + implementation "com.google.code.gson:gson:2.4" implementation "com.google.guava:guava:24.1-jre" implementation "net.danlew:android.joda:2.9.9.1" @@ -277,6 +277,14 @@ dependencies { androidTestImplementation "com.google.dexmaker:dexmaker:${dexmakerVersion}" androidTestImplementation "com.google.dexmaker:dexmaker-mockito:${dexmakerVersion}" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + // xDrip-plus port + implementation 'com.activeandroid:thread-safe-active-android:3.1.1' + implementation 'com.squareup.retrofit2:retrofit:2.4.0' + implementation 'com.squareup.okhttp3:okhttp:3.10.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' + implementation 'com.squareup.retrofit2:converter-gson:2.4.0' + } task unzip(type: Copy) { diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java b/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java new file mode 100644 index 0000000000..df801512fe --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java @@ -0,0 +1,126 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.GregorianCalendar; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; + +public class JoH { + + private final static String TAG = "jamorham JoH"; + + public static String dateTimeText(long timestamp) { + return android.text.format.DateFormat.format("yyyy-MM-dd kk:mm:ss", timestamp).toString(); + } + + public static String niceTimeScalar(long t) { + String unit = MainApp.gs(R.string.unit_second); + t = t / 1000; + if (t != 1) unit = MainApp.gs(R.string.unit_seconds); + if (t > 59) { + unit = MainApp.gs(R.string.unit_minute); + t = t / 60; + if (t != 1) unit = MainApp.gs(R.string.unit_minutes); + if (t > 59) { + unit = MainApp.gs(R.string.unit_hour); + t = t / 60; + if (t != 1) unit = MainApp.gs(R.string.unit_hours); + if (t > 24) { + unit = MainApp.gs(R.string.unit_day); + t = t / 24; + if (t != 1) unit = MainApp.gs(R.string.unit_days); + if (t > 28) { + unit = MainApp.gs(R.string.unit_week); + t = t / 7; + if (t != 1) unit = MainApp.gs(R.string.unit_weeks); + } + } + } + } + //if (t != 1) unit = unit + "s"; //implemented plurality in every step, because in other languages plurality of time is not every time adding the same character + return qs((double) t, 0) + " " + unit; + } + + // singletons to avoid repeated allocation + private static DecimalFormatSymbols dfs; + private static DecimalFormat df; + public static String qs(double x, int digits) { + + if (digits == -1) { + digits = 0; + if (((int) x != x)) { + digits++; + if ((((int) x * 10) / 10 != x)) { + digits++; + if ((((int) x * 100) / 100 != x)) digits++; + } + } + } + + if (dfs == null) { + final DecimalFormatSymbols local_dfs = new DecimalFormatSymbols(); + local_dfs.setDecimalSeparator('.'); + dfs = local_dfs; // avoid race condition + } + + final DecimalFormat this_df; + // use singleton if on ui thread otherwise allocate new as DecimalFormat is not thread safe + if (Thread.currentThread().getId() == 1) { + if (df == null) { + final DecimalFormat local_df = new DecimalFormat("#", dfs); + local_df.setMinimumIntegerDigits(1); + df = local_df; // avoid race condition + } + this_df = df; + } else { + this_df = new DecimalFormat("#", dfs); + } + + this_df.setMaximumFractionDigits(digits); + return this_df.format(x); + } + + public static long getTimeZoneOffsetMs() { + return new GregorianCalendar().getTimeZone().getRawOffset(); + } + + public static boolean emptyString(final String str) { + return str == null || str.length() == 0; + } + + public static PowerManager.WakeLock getWakeLock(final String name, int millis) { + final PowerManager pm = (PowerManager) MainApp.instance().getSystemService(Context.POWER_SERVICE); + final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + wl.acquire(millis); + Log.d(TAG, "getWakeLock: " + name + " " + wl.toString()); + return wl; + } + + public static void releaseWakeLock(PowerManager.WakeLock wl) { + Log.d(TAG, "releaseWakeLock: " + wl.toString()); + if (wl == null) return; + if (wl.isHeld()) { + try { + wl.release(); + } catch (Exception e) { + Log.e(TAG, "Error releasing wakelock: " + e); + } + } + } + + public static PowerManager.WakeLock fullWakeLock(final String name, long millis) { + final PowerManager pm = (PowerManager) MainApp.instance().getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, name); + wl.acquire(millis); + Log.d(TAG, "fullWakeLock: " + name + " " + wl.toString()); + return wl; + } + + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java new file mode 100644 index 0000000000..0bf0bb6fa9 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java @@ -0,0 +1,40 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.google.gson.annotations.Expose; + +/** + * jamorham + *

+ * common element base + */ + +public abstract class BaseElement { + @Expose + public String deviceTime; + @Expose + public String time; + @Expose + public int timezoneOffset; + @Expose + public String type; + @Expose + public Origin origin; + + + BaseElement populate(final long timestamp, final String uuid) { + deviceTime = DateUtil.toFormatNoZone(timestamp); + time = DateUtil.toFormatAsUTC(timestamp); + timezoneOffset = DateUtil.getTimeZoneOffsetMinutes(timestamp); // TODO + origin = new Origin(uuid); + return this; + } + + public class Origin { + @Expose + String id; + + Origin(String id) { + this.id = id; + } + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java new file mode 100644 index 0000000000..c96d3da1c4 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java @@ -0,0 +1,24 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import okhttp3.MediaType; +import okhttp3.RequestBody; + +/** + * jamorham + * + * message base + */ + +public abstract class BaseMessage { + + public String toS() { + return JoH.defaultGsonInstance().toJson(this); + } + + public RequestBody getBody() { + return RequestBody.create(MediaType.parse("application/json"), this.toS()); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java new file mode 100644 index 0000000000..84f4076f75 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java @@ -0,0 +1,46 @@ +package com.eveningoutpost.dexdrip.tidepool; + +/** + * jamorham + * + * Date utilities for preparing items for Tidepool upload + */ + +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; + +public class DateUtil { + + static String toFormatAsUTC(final long timestamp) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'0000Z'", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.format(timestamp); + } + + static String toFormatWithZone2(final long timestamp) { + // ISO 8601 not introduced till api 24 - so we have to do some gymnastics + final SimpleDateFormat formatIso8601 = new SimpleDateFormat("Z", Locale.US); + formatIso8601.setTimeZone(TimeZone.getDefault()); + String zone = formatIso8601.format(timestamp); + zone = zone.substring(0, zone.length() - 2) + ":" + zone.substring(zone.length() - 2); + if (zone.substring(0, 1).equals("+")) { + zone = zone.substring(1); + } + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z" + zone + "'", Locale.US); + format.setTimeZone(TimeZone.getDefault()); + return format.format(timestamp); + } + + + static String toFormatNoZone(final long timestamp) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getDefault()); + return format.format(timestamp); + } + + static int getTimeZoneOffsetMinutes(final long timestamp) { + return TimeZone.getDefault().getOffset(timestamp) / 60000; + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java new file mode 100644 index 0000000000..96819ba31c --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java @@ -0,0 +1,43 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.google.gson.annotations.Expose; + +// jamorham + +public class EBasal extends BaseElement { + + long timestamp; // not exposed + + @Expose + String deliveryType = "automated"; + @Expose + long duration; + @Expose + double rate = -1; + @Expose + String scheduleName = "AAPS"; + @Expose + long clockDriftOffset = 0; + @Expose + long conversionOffset = 0; + + { + type = "basal"; + } + + EBasal(double rate, long timeStart, long duration, String uuid) { + this.timestamp = timeStart; + this.rate = rate; + this.duration = duration; + populate(timeStart, uuid); + } + + boolean isValid() { + return (rate > -1 && duration > 0); + } + + String toS() { + return rate + " Start: " + JoH.dateTimeText(timestamp) + " for: " + JoH.niceTimeScalar(duration); + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java new file mode 100644 index 0000000000..493c16ec3b --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java @@ -0,0 +1,45 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +import com.eveningoutpost.dexdrip.Models.BloodTest; +import com.google.gson.annotations.Expose; + +import java.util.LinkedList; +import java.util.List; + +class EBloodGlucose extends BaseElement { + + @Expose + String subType; + @Expose + String units; + @Expose + int value; + + EBloodGlucose() { + this.type = "smbg"; + this.units = "mg/dL"; + } + + + static EBloodGlucose fromBloodTest(final BloodTest bloodtest) { + final EBloodGlucose bg = new EBloodGlucose(); + bg.populate(bloodtest.timestamp, bloodtest.uuid); + + bg.subType = "manual"; // TODO + bg.value = (int) bloodtest.mgdl; + return bg; + } + + static List fromBloodTests(final List bloodTestList) { + if (bloodTestList == null) return null; + final List results = new LinkedList<>(); + for (BloodTest bt : bloodTestList) { + results.add(fromBloodTest(bt)); + } + return results; + } + +} + diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java new file mode 100644 index 0000000000..285f0b013b --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java @@ -0,0 +1,26 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.google.gson.annotations.Expose; + +// jamorham + +public class EBolus extends BaseElement { + + @Expose + public final String subType = "normal"; + @Expose + public final double normal; + @Expose + public final double expectedNormal; + + { + type = "bolus"; + } + + EBolus(double insulinDelivered, double insulinExpected, long timestamp, String uuid) { + this.normal = insulinDelivered; + this.expectedNormal = insulinExpected; + populate(timestamp, uuid); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java new file mode 100644 index 0000000000..857c7905ad --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java @@ -0,0 +1,37 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +import com.google.gson.annotations.Expose; + +public class ESensorGlucose extends BaseElement { + + + @Expose + String units; + @Expose + int value; + + ESensorGlucose() { + this.type = "cbg"; + this.units = "mg/dL"; + } + +/* + static ESensorGlucose fromBgReading(final BgReading bgReading) { + final ESensorGlucose sensorGlucose = new ESensorGlucose(); + sensorGlucose.populate(bgReading.timestamp, bgReading.uuid); + sensorGlucose.value = (int) bgReading.calculated_value; // TODO best glucose? + return sensorGlucose; + } + + static List fromBgReadings(final List bgReadingList) { + if (bgReadingList == null) return null; + final List results = new LinkedList<>(); + for (BgReading bgReading : bgReadingList) { + results.add(fromBgReading(bgReading)); + } + return results; + } +*/ +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java new file mode 100644 index 0000000000..08de1a390c --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java @@ -0,0 +1,34 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.google.gson.annotations.Expose; + +// jamorham + +public class EWizard extends BaseElement { + + @Expose + public String units = "mg/dL"; + @Expose + public double carbInput; + @Expose + public double insulinCarbRatio; + @Expose + public EBolus bolus; + + EWizard() { + type = "wizard"; + } +/* + public static EWizard fromTreatment(final Treatments treatment) { + final EWizard result = (EWizard)new EWizard().populate(treatment.timestamp, treatment.uuid); + result.carbInput = treatment.carbs; + result.insulinCarbRatio = Profile.getCarbRatio(treatment.timestamp); + if (treatment.insulin > 0) { + result.bolus = new EBolus(treatment.insulin, treatment.insulin, treatment.timestamp, treatment.uuid); + } else { + result.bolus = new EBolus(0.0001,0.0001, treatment.timestamp, treatment.uuid); // fake insulin record + } + return result; + } +*/ +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java new file mode 100644 index 0000000000..7dc3c156e9 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java @@ -0,0 +1,48 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.GzipSink; +import okio.Okio; + +class GzipRequestInterceptor implements Interceptor { + @Override + public okhttp3.Response intercept(Chain chain) throws IOException { + final Request originalRequest = chain.request(); + if (originalRequest.body() == null + || originalRequest.header("Content-Encoding") != null) + { + return chain.proceed(originalRequest); + } + + final Request compressedRequest = originalRequest.newBuilder() + .header("Content-Encoding", "gzip") + .method(originalRequest.method(), gzip(originalRequest.body())) + .build(); + return chain.proceed(compressedRequest); + } + + private RequestBody gzip(final RequestBody body) { + return new RequestBody() { + @Override public MediaType contentType() { + return body.contentType(); + } + + @Override public long contentLength() { + return -1; // We don't know the compressed length in advance! + } + + @Override public void writeTo(BufferedSink sink) throws IOException { + BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); + body.writeTo(gzipSink); + gzipSink.close(); + } + }; + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java new file mode 100644 index 0000000000..5feb804566 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java @@ -0,0 +1,32 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import android.support.annotation.NonNull; +import android.util.Log; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +// jamorham + +public class InfoInterceptor implements Interceptor { + + private String tag = "interceptor"; + + public InfoInterceptor(String tag) { + this.tag = tag; + } + + @Override + public Response intercept(@NonNull final Chain chain) throws IOException { + final Request request = chain.request(); + if (request != null && request.body() != null) { + Log.d(tag, "Interceptor Body size: " + request.body().contentLength()); + //} else { + // UserError.Log.d(tag,"Null request body in InfoInterceptor"); + } + return chain.proceed(request); + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java new file mode 100644 index 0000000000..77dc6e0739 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java @@ -0,0 +1,36 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class MAuthReply { + + @Expose + @SerializedName("emailVerified") + Boolean emailVerified; + @Expose + @SerializedName("emails") + List emailList; + @Expose + @SerializedName("termsAccepted") + String termsDate; + @Expose + @SerializedName("userid") + String userid; + @Expose + @SerializedName("username") + String username; + + public String toS() { + return JoH.defaultGsonInstance().toJson(this); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java new file mode 100644 index 0000000000..6c5ae762c9 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java @@ -0,0 +1,24 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.utils.SP; +import okhttp3.Credentials; + +import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; + +public class MAuthRequest extends BaseMessage { + + public static String getAuthRequestHeader() { + + final String username = SP.getString(R.string.key_tidepool_username, null); + final String password = SP.getString(R.string.key_tidepool_password, null); + + if (emptyString(username) || emptyString(password)) return null; + return Credentials.basic(username.trim(), password); + } +} + + + diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java new file mode 100644 index 0000000000..d12ec48060 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java @@ -0,0 +1,9 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.google.gson.annotations.Expose; + +public class MCloseDatasetRequest extends BaseMessage { + @Expose + String dataState = "closed"; + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java new file mode 100644 index 0000000000..9e4752ff1f --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java @@ -0,0 +1,47 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import java.util.List; + +public class MDatasetReply { + + Data data; + + public class Data { + String createdTime; + String deviceId; + String id; + String time; + String timezone; + int timezoneOffset; + String type; + String uploadId; + Client client; + String computerTime; + String dataSetType; + List deviceManufacturers; + String deviceModel; + String deviceSerialNumber; + List deviceTags; + String timeProcessing; + String version; + // meta + } + + public class Client { + String name; + String version; + + } + + + // openDataSet and others return this in the root of the json reply it seems + String id; + String uploadId; + + public String getUploadId() { + return (data != null && data.uploadId != null) ? data.uploadId : uploadId; + } + + + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java new file mode 100644 index 0000000000..8a72992e5d --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java @@ -0,0 +1,4 @@ +package com.eveningoutpost.dexdrip.tidepool; + +public class MGetDatasetsRequest extends BaseMessage { +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java new file mode 100644 index 0000000000..caf62794f5 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java @@ -0,0 +1,63 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import com.google.gson.annotations.Expose; + +import java.util.TimeZone; + +import info.nightscout.androidaps.BuildConfig; +import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.utils.T; + +import static com.eveningoutpost.dexdrip.Models.JoH.getTimeZoneOffsetMs; + +public class MOpenDatasetRequest extends BaseMessage { + + static final String UPLOAD_TYPE = "continuous"; + + @Expose + public String deviceId; + @Expose + public String time = DateUtil.toFormatAsUTC(info.nightscout.androidaps.utils.DateUtil.now()); + @Expose + public int timezoneOffset = (int) (getTimeZoneOffsetMs() / T.mins(1).msecs()); + @Expose + public String type = "upload"; + //public String byUser; + @Expose + public ClientInfo client = new ClientInfo(); + @Expose + public String computerTime = DateUtil.toFormatNoZone(info.nightscout.androidaps.utils.DateUtil.now()); + @Expose + public String dataSetType = UPLOAD_TYPE; // omit for "normal" + @Expose + public String[] deviceManufacturers = {((PluginBase) (ConfigBuilderPlugin.getPlugin().getActiveBgSource())).getName()}; + @Expose + public String deviceModel = ((PluginBase) (ConfigBuilderPlugin.getPlugin().getActiveBgSource())).getName(); + @Expose + public String[] deviceTags = {"bgm", "cgm", "insulin-pump"}; + @Expose + public Deduplicator deduplicator = new Deduplicator(); + @Expose + public String timeProcessing = "none"; + @Expose + public String timezone = TimeZone.getDefault().getID(); + @Expose + public String version = BuildConfig.VERSION_NAME; + + class ClientInfo { + @Expose + final String name = BuildConfig.APPLICATION_ID; + @Expose + final String version = "0.1.0"; // TODO: const it + } + + class Deduplicator { + @Expose + final String name = "org.tidepool.deduplicator.dataset.delete.origin"; + } + + static boolean isNormal() { + return UPLOAD_TYPE.equals("normal"); + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java new file mode 100644 index 0000000000..bdcf164396 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java @@ -0,0 +1,9 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import java.util.List; + +public class MUploadReply { + + List data; + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java new file mode 100644 index 0000000000..83bf20d036 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java @@ -0,0 +1,54 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +// Manages the session data + +import java.util.List; + +import okhttp3.Headers; + +public class Session { + + private final String SESSION_TOKEN_HEADER; + final TidepoolUploader.Tidepool service = TidepoolUploader.getRetrofitInstance().create(TidepoolUploader.Tidepool.class); + final String authHeader; + + String token; + MAuthReply authReply; + MDatasetReply datasetReply; + long start; + long end; + volatile int iterations; + + + Session(String authHeader, String session_token_header) { + this.authHeader = authHeader; + this.SESSION_TOKEN_HEADER = session_token_header; + } + + void populateHeaders(final Headers headers) { + if (this.token == null) { + this.token = headers.get(SESSION_TOKEN_HEADER); + } + } + + void populateBody(final Object obj) { + if (obj == null) return; + if (obj instanceof MAuthReply) { + authReply = (MAuthReply) obj; + } else if (obj instanceof List) { + List list = (List)obj; + if (list.size() > 0 && list.get(0) instanceof MDatasetReply) { + datasetReply = (MDatasetReply) list.get(0); + } + } else if (obj instanceof MDatasetReply) { + datasetReply = (MDatasetReply) obj; + } + } + + boolean exceededIterations() { + return iterations > 50; + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java new file mode 100644 index 0000000000..d99afdc1fa --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java @@ -0,0 +1,67 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import android.util.Log; + +import com.eveningoutpost.dexdrip.store.FastStore; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +// jamorham + +// Callback template to reduce boiler plate + +class TidepoolCallback implements Callback { + + final Session session; + final String name; + final Runnable onSuccess; + + Runnable onFailure; + + public TidepoolCallback(Session session, String name, Runnable onSuccess) { + this.session = session; + this.name = name; + this.onSuccess = onSuccess; + } + + TidepoolCallback setOnFailure(final Runnable runnable) { + this.onFailure = runnable; + return this; + } + + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + Log.d(TidepoolUploader.TAG, name + " success"); + session.populateBody(response.body()); + session.populateHeaders(response.headers()); + if (onSuccess != null) { + onSuccess.run(); + } + } else { + final String msg = name + " was not successful: " + response.code() + " " + response.message(); + Log.e(TidepoolUploader.TAG, msg); + status(msg); + if (onFailure != null) { + onFailure.run(); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + final String msg = name + " Failed: " + t; + Log.e(TidepoolUploader.TAG, msg); + status(msg); + if (onFailure != null) { + onFailure.run(); + } + } + + + private static void status(final String status) { + FastStore.getInstance().putS(TidepoolUploader.STATUS_KEY, status); + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java new file mode 100644 index 0000000000..1cd5ef5e25 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java @@ -0,0 +1,28 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +// lightweight class entry point + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; + +import static com.eveningoutpost.dexdrip.Models.JoH.isLANConnected; +import static com.eveningoutpost.dexdrip.utils.PowerStateReceiver.is_power_connected; + +public class TidepoolEntry { + + + public static boolean enabled() { + return Pref.getBooleanDefaultFalse("cloud_storage_tidepool_enable"); + } + + public static void newData() { + if (enabled() + && (!Pref.getBooleanDefaultFalse("tidepool_only_while_charging") || is_power_connected()) + && (!Pref.getBooleanDefaultFalse("tidepool_only_while_unmetered") || isLANConnected()) + && JoH.pratelimit("tidepool-new-data-upload", 1200)) { + TidepoolUploader.doLogin(false); + } + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java new file mode 100644 index 0000000000..82e76c4d1f --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java @@ -0,0 +1,31 @@ +package com.eveningoutpost.dexdrip.tidepool; + +// jamorham + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; +import com.eveningoutpost.dexdrip.store.FastStore; +import com.eveningoutpost.dexdrip.store.KeyStore; + +import java.util.ArrayList; +import java.util.List; + +import static com.eveningoutpost.dexdrip.Models.JoH.msSince; +import static com.eveningoutpost.dexdrip.Models.JoH.niceTimeScalar; + +public class TidepoolStatus { + + // data for MegaStatus + public static List megaStatus() { + + final KeyStore keyStore = FastStore.getInstance(); + final List l = new ArrayList<>(); + + l.add(new StatusItem("Tidepool Synced to", niceTimeScalar(msSince(UploadChunk.getLastEnd())) + " ago")); // TODO needs generic message format string + final String status = keyStore.getS(TidepoolUploader.STATUS_KEY); + if (!JoH.emptyString(status)) { + l.add(new StatusItem("Tidepool Status", status)); + } + return l; + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java new file mode 100644 index 0000000000..52b4ef9200 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java @@ -0,0 +1,342 @@ +package com.eveningoutpost.dexdrip.tidepool; + +import android.os.PowerManager; +import android.util.Log; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; +import com.eveningoutpost.dexdrip.store.FastStore; + +import java.util.List; + +import info.nightscout.androidaps.BuildConfig; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.utils.SP; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +/** + * jamorham + *

+ * Tidepool Uploader + *

+ * huge thanks to bassettb for a working c# reference implementation upon which this is based + */ + +public class TidepoolUploader { + + protected static final String TAG = "TidepoolUploader"; + protected static final String STATUS_KEY = "Tidepool-Status"; + private static final boolean D = true; + private static final boolean REPEAT = false; + + private static Retrofit retrofit; + private static final String INTEGRATION_BASE_URL = "https://int-api.tidepool.org"; + private static final String PRODUCTION_BASE_URL = "https://api.tidepool.org"; + private static final String SESSION_TOKEN_HEADER = "x-tidepool-session-token"; + + private static PowerManager.WakeLock wl; + + public interface Tidepool { + @Headers({ + "User-Agent: AAPS- " + BuildConfig.VERSION_NAME, + "X-Tidepool-Client-Name: " + BuildConfig.APPLICATION_ID, + "X-Tidepool-Client-Version: 0.1.0", // TODO: const it + }) + + @POST("/auth/login") + Call getLogin(@Header("Authorization") String secret); + + @DELETE("/v1/users/{userId}/data") + Call deleteAllData(@Header(SESSION_TOKEN_HEADER) String token, @Path("userId") String id); + + @DELETE("/v1/datasets/{dataSetId}") + Call deleteDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("dataSetId") String id); + + @GET("/v1/users/{userId}/data_sets") + Call> getOpenDataSets(@Header(SESSION_TOKEN_HEADER) String token, + @Path("userId") String id, + @Query("client.name") String clientName, + @Query("size") int size); + + @GET("/v1/datasets/{dataSetId}") + Call getDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("dataSetId") String id); + + @POST("/v1/users/{userId}/data_sets") + Call openDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("userId") String id, @Body RequestBody body); + + @POST("/v1/datasets/{sessionId}/data") + Call doUpload(@Header(SESSION_TOKEN_HEADER) String token, @Path("sessionId") String id, @Body RequestBody body); + + @PUT("/v1/datasets/{sessionId}") + Call closeDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("sessionId") String id, @Body RequestBody body); + + } + + + public static Retrofit getRetrofitInstance() { + if (retrofit == null) { + + final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); + if (D) { + httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + } + final OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(new InfoInterceptor(TAG)) + // .addInterceptor(new GzipRequestInterceptor()) + .build(); + + retrofit = new Retrofit.Builder() + .baseUrl(SP.getBoolean(R.string.key_tidepool_dev_servers, false) ? INTEGRATION_BASE_URL : PRODUCTION_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + } + return retrofit; + } + + public static void resetInstance() { + retrofit = null; + Log.d(TAG, "Instance reset"); + } + + public static void doLoginFromUi() { + doLogin(true); + } + + public static synchronized void doLogin(final boolean fromUi) { + if (!TidepoolEntry.enabled()) { + Log.d(TAG, "Cannot login as disabled by preference"); + if (fromUi) { +// JoH.static_toast_long("Cannot login as Tidepool feature not enabled"); + } + return; + } + // TODO failure backoff +// if (JoH.ratelimit("tidepool-login", 10)) { + extendWakeLock(30000); + final Session session = new Session(MAuthRequest.getAuthRequestHeader(), SESSION_TOKEN_HEADER); + if (session.authHeader != null) { + final Call call = session.service.getLogin(session.authHeader); + status("Connecting"); + if (fromUi) { +// JoH.static_toast_long("Connecting to Tidepool"); + } + + call.enqueue(new TidepoolCallback(session, "Login", () -> startSession(session, fromUi)) + .setOnFailure(() -> loginFailed(fromUi))); + } else { + Log.e(TAG, "Cannot do login as user credentials have not been set correctly"); + status("Invalid credentials"); + if (fromUi) { +// JoH.static_toast_long("Cannot login as Tidepool credentials have not been set correctly"); + } + releaseWakeLock(); + } + // } + } + + private static void loginFailed(boolean fromUi) { + if (fromUi) { +// JoH.static_toast_long("Login failed - see event log for details"); + } + releaseWakeLock(); + } + +/* public static void testLogin(Context rootContext) { + if (JoH.ratelimit("tidepool-login", 1)) { + + String message = "Failed to log into Tidepool.\n" + + "Check that your user name and password are correct."; + + final Session session = new Session(MAuthRequest.getAuthRequestHeader(), SESSION_TOKEN_HEADER); + if (session.authHeader != null) { + final Call call = session.service.getLogin(session.authHeader); + + try { + Response response = call.execute(); + UserError.Log.e(TAG, "Header: " + response.code()); + message = "Successfully logged into Tidepool."; + } catch (IOException e) { + e.printStackTrace(); + } + } else { + UserError.Log.e(TAG,"Cannot do login as user credentials have not been set correctly"); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(rootContext); + + builder.setTitle("Tidepool Login"); + + builder.setMessage(message); + + builder.setPositiveButton("OK", (dialog, id) -> { + dialog.dismiss(); + }); + + final AlertDialog alert = builder.create(); + alert.show(); + } + }*/ + + + private static void startSession(final Session session, boolean fromUi) { +// if (JoH.ratelimit("tidepool-start-session", 60)) { + extendWakeLock(30000); + if (session.authReply.userid != null) { + // See if we already have an open data set to write to + Call> datasetCall = session.service.getOpenDataSets(session.token, + session.authReply.userid, BuildConfig.APPLICATION_ID, 1); + + datasetCall.enqueue(new TidepoolCallback>(session, "Get Open Datasets", () -> { + if (session.datasetReply == null) { + status("New data set"); + if (fromUi) { +// JoH.static_toast_long("Creating new data set"); + } + Call call = session.service.openDataSet(session.token, session.authReply.userid, new MOpenDatasetRequest().getBody()); + call.enqueue(new TidepoolCallback(session, "Open New Dataset", () -> doUpload(session)) + .setOnFailure(TidepoolUploader::releaseWakeLock)); + } else { + Log.d(TAG, "Existing Dataset: " + session.datasetReply.getUploadId()); + // TODO: Wouldn't need to do this if we could block on the above `call.enqueue`. + // ie, do the openDataSet conditionally, and then do `doUpload` either way. + status("Appending"); + if (fromUi) { +// JoH.static_toast_long("Found existing remote data set"); + } + doUpload(session); + } + }).setOnFailure(TidepoolUploader::releaseWakeLock)); + } else { + Log.wtf(TAG, "Got login response but cannot determine userid - cannot proceed"); + if (fromUi) { +// JoH.static_toast_long("Error: Cannot determine userid"); + } + status("Error userid"); + releaseWakeLock(); + } +// } else { +// status("Cool Down Wait"); +// if (fromUi) { +// JoH.static_toast_long("In cool down period, please wait 1 minute"); +// } +// } + } + + + private static void doUpload(final Session session) { + if (!TidepoolEntry.enabled()) { + Log.e(TAG, "Cannot upload - preference disabled"); + return; + } + extendWakeLock(60000); + session.iterations++; + final String chunk = UploadChunk.getNext(session); + if (chunk != null) { + if (chunk.length() == 2) { + Log.d(TAG, "Empty data set - marking as succeeded"); + doCompleted(session); + } else { + final RequestBody body = RequestBody.create(MediaType.parse("application/json"), chunk); + + final Call call = session.service.doUpload(session.token, session.datasetReply.getUploadId(), body); + status("Uploading"); + call.enqueue(new TidepoolCallback(session, "Data Upload", () -> { + UploadChunk.setLastEnd(session.end); + + if (REPEAT && !session.exceededIterations()) { + status("Queued Next"); + Log.d(TAG, "Scheduling next upload"); + Inevitable.task("Tidepool-next", 10000, () -> doUpload(session)); + } else { + + if (MOpenDatasetRequest.isNormal()) { + doClose(session); + } else { + doCompleted(session); + } + } + }).setOnFailure(TidepoolUploader::releaseWakeLock)); + } + } else { + Log.e(TAG, "Upload chunk is null, cannot proceed"); + releaseWakeLock(); + } + } + + + private static void doClose(final Session session) { + status("Closing"); + extendWakeLock(20000); + final Call call = session.service.closeDataSet(session.token, session.datasetReply.getUploadId(), new MCloseDatasetRequest().getBody()); + call.enqueue(new TidepoolCallback<>(session, "Session Stop", TidepoolUploader::closeSuccess)); + } + + private static void closeSuccess() { + status("Closed"); + Log.d(TAG, "Close success"); + releaseWakeLock(); + } + + private static void doCompleted(final Session session) { + status("Completed OK"); + Log.d(TAG, "ALL COMPLETED OK!"); + releaseWakeLock(); + } + + private static void status(final String status) { + FastStore.getInstance().putS(STATUS_KEY, status); + } + + private static synchronized void extendWakeLock(long ms) { + if (wl == null) { + wl = JoH.getWakeLock("tidepool-uploader", (int) ms); + } else { + JoH.releaseWakeLock(wl); // lets not get too messy + wl.acquire(ms); + } + } + + protected static synchronized void releaseWakeLock() { + Log.d(TAG, "Releasing wakelock"); + JoH.releaseWakeLock(wl); + } + + // experimental - not used + + private static void deleteData(final Session session) { + if (session.authReply.userid != null) { + Call call = session.service.deleteAllData(session.token, session.authReply.userid); + call.enqueue(new TidepoolCallback<>(session, "Delete Data", null)); + } else { + Log.wtf(TAG, "Got login response but cannot determine userid - cannot proceed"); + } + } + + private static void getDataSet(final Session session) { + Call call = session.service.getDataSet(session.token, "bogus"); + call.enqueue(new TidepoolCallback<>(session, "Get Data", null)); + } + + private static void deleteDataSet(final Session session) { + Call call = session.service.deleteDataSet(session.token, "bogus"); + call.enqueue(new TidepoolCallback<>(session, "Delete Data", null)); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java new file mode 100644 index 0000000000..9d8a580bbe --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java @@ -0,0 +1,182 @@ +package com.eveningoutpost.dexdrip.tidepool; + + +import com.eveningoutpost.dexdrip.Models.APStatus; +import com.eveningoutpost.dexdrip.Models.BgReading; +import com.eveningoutpost.dexdrip.Models.BloodTest; +import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.Models.Profile; +import com.eveningoutpost.dexdrip.Models.Treatments; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.UtilityModels.Constants; +import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; +import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import com.eveningoutpost.dexdrip.utils.LogSlider; +import com.eveningoutpost.dexdrip.utils.NamedSliderProcessor; + +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import static com.eveningoutpost.dexdrip.Models.JoH.dateTimeText; + +/** + * jamorham + * + * This class gets the next time slice of all data to upload + */ + +public class UploadChunk implements NamedSliderProcessor { + + private static final String TAG = "TidepoolUploadChunk"; + private static final String LAST_UPLOAD_END_PREF = "tidepool-last-end"; + + private static final long MAX_UPLOAD_SIZE = Constants.DAY_IN_MS * 7; // don't change this + private static final long DEFAULT_WINDOW_OFFSET = Constants.MINUTE_IN_MS * 15; + private static final long MAX_LATENCY_THRESHOLD_MINUTES = 1440; // minutes per day + + + public static String getNext(final Session session) { + session.start = getLastEnd(); + session.end = maxWindow(session.start); + + final String result = get(session.start, session.end); + if (result != null && result.length() < 3) { + UserError.Log.d(TAG, "No records in this time period, setting start to best end time"); + setLastEnd(Math.max(session.end, getOldestRecordTimeStamp())); + } + return result; + } + + public static String get(final long start, final long end) { + + UserError.Log.uel(TAG, "Syncing data between: " + dateTimeText(start) + " -> " + dateTimeText(end)); + if (end <= start) { + UserError.Log.e(TAG, "End is <= start: " + dateTimeText(start) + " " + dateTimeText(end)); + return null; + } + if (end - start > MAX_UPLOAD_SIZE) { + UserError.Log.e(TAG, "More than max range - rejecting"); + return null; + } + + final List records = new LinkedList<>(); + + records.addAll(getTreatments(start, end)); + records.addAll(getBloodTests(start, end)); + records.addAll(getBasals(start, end)); + records.addAll(getBgReadings(start, end)); + + return JoH.defaultGsonInstance().toJson(records); + } + + private static long getWindowSizePreference() { + try { + long value = (long) getLatencySliderValue(Pref.getInt("tidepool_window_latency", 0)); + return Math.max(value * Constants.MINUTE_IN_MS, DEFAULT_WINDOW_OFFSET); + } catch (Exception e) { + UserError.Log.e(TAG, "Reverting to default of 15 minutes due to Window Size exception: " + e); + return DEFAULT_WINDOW_OFFSET; // default + } + } + + private static long maxWindow(final long last_end) { + //UserError.Log.d(TAG, "Max window is: " + getWindowSizePreference()); + return Math.min(last_end + MAX_UPLOAD_SIZE, JoH.tsl() - getWindowSizePreference()); + } + + public static long getLastEnd() { + long result = PersistentStore.getLong(LAST_UPLOAD_END_PREF); + return Math.max(result, JoH.tsl() - Constants.MONTH_IN_MS * 2); + } + + public static void setLastEnd(final long when) { + if (when > getLastEnd()) { + PersistentStore.setLong(LAST_UPLOAD_END_PREF, when); + UserError.Log.d(TAG, "Updating last end to: " + dateTimeText(when)); + } else { + UserError.Log.e(TAG, "Cannot set last end to: " + dateTimeText(when) + " vs " + dateTimeText(getLastEnd())); + } + } + + static List getTreatments(final long start, final long end) { + List result = new LinkedList<>(); + final List treatments = Treatments.latestForGraph(1800, start, end); + for (Treatments treatment : treatments) { + if (treatment.carbs > 0) { + result.add(EWizard.fromTreatment(treatment)); + } else if (treatment.insulin > 0) { + result.add(EBolus.fromTreatment(treatment)); + } else { + // note only TODO + } + } + return result; + } + + + // numeric limits must match max time windows + + static long getOldestRecordTimeStamp() { + // TODO we could make sure we include records older than the first bg record for completeness + + final long start = 0; + final long end = JoH.tsl(); + + final List bgReadingList = BgReading.latestForGraphAsc(1, start, end); + if (bgReadingList != null && bgReadingList.size() > 0) { + return bgReadingList.get(0).timestamp; + } + return -1; + } + + static List getBloodTests(final long start, final long end) { + return EBloodGlucose.fromBloodTests(BloodTest.latestForGraph(1800, start, end)); + } + + static List getBgReadings(final long start, final long end) { + return ESensorGlucose.fromBgReadings(BgReading.latestForGraphAsc(15000, start, end)); + } + + static List getBasals(final long start, final long end) { + final List basals = new LinkedList<>(); + final List aplist = APStatus.latestForGraph(15000, start, end); + EBasal current = null; + for (APStatus apStatus : aplist) { + final double this_rate = Profile.getBasalRate(apStatus.timestamp) * apStatus.basal_percent / 100d; + + if (current != null) { + if (this_rate != current.rate) { + current.duration = apStatus.timestamp - current.timestamp; + UserError.Log.d(TAG, "Adding current: " + current.toS()); + if (current.isValid()) { + basals.add(current); + } else { + UserError.Log.e(TAG, "Current basal is invalid: " + current.toS()); + } + current = null; + } else { + UserError.Log.d(TAG, "Same rate as previous basal record: " + current.rate + " " + apStatus.toS()); + } + } + if (current == null) { + current = new EBasal(this_rate, apStatus.timestamp, 0, UUID.nameUUIDFromBytes(("tidepool-basal" + apStatus.timestamp).getBytes()).toString()); // start duration is 0 + } + } + return basals; + + } + + @Override + public int interpolate(final String name, final int position) { + switch (name) { + case "latency": + return getLatencySliderValue(position); + } + throw new RuntimeException("name not matched in interpolate"); + } + + private static int getLatencySliderValue(final int position) { + return (int) LogSlider.calc(0, 300, 15, MAX_LATENCY_THRESHOLD_MINUTES, position); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9620b64a50..67c746535f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1316,6 +1316,20 @@ Tomato (MiaoMiao) Tomato + second + minute + hour + day" + week" + seconds + minutes + hours + days" + weeks" + tidepool_username + tidepool_password + tidepool_dev_servers + smbmaxminutes Dayligh Saving time Dayligh Saving time change in 24h or less From 9b1acf6958356ceacadcb498fafbd793feb0f45d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Jun 2019 18:40:33 +0200 Subject: [PATCH 02/39] add missing classes. convert to use AAPS data --- app/build.gradle | 5 + .../dexdrip/Models/BloodTest.java | 550 ++++++++++++++++++ .../eveningoutpost/dexdrip/Models/JoH.java | 57 ++ .../dexdrip/UtilityModels/Inevitable.java | 120 ++++ .../UtilityModels/PersistentStore.java | 138 +++++ .../dexdrip/UtilityModels/StatusItem.java | 98 ++++ .../dexdrip/store/FastStore.java | 53 ++ .../dexdrip/store/KeyStore.java | 28 + .../dexdrip/tidepool/EBolus.java | 6 + .../dexdrip/tidepool/ESensorGlucose.java | 13 +- .../dexdrip/tidepool/EWizard.java | 17 +- .../dexdrip/tidepool/TidepoolEntry.java | 15 +- .../dexdrip/tidepool/UploadChunk.java | 83 +-- .../dexdrip/utils/LogSlider.java | 13 + .../dexdrip/utils/NamedSliderProcessor.java | 11 + .../info/nightscout/androidaps/MainApp.java | 2 + .../androidaps/db/DatabaseHelper.java | 35 +- .../general/tidepool/TidepoolPlugin.java | 39 ++ .../plugins/treatments/TreatmentService.java | 17 + .../receivers/ChargingStateReceiver.java | 6 + .../info/nightscout/androidaps/utils/T.java | 6 + app/src/main/res/values/strings.xml | 18 + app/src/main/res/xml/pref_tidepool.xml | 49 ++ 23 files changed, 1322 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java create mode 100644 app/src/main/res/xml/pref_tidepool.xml diff --git a/app/build.gradle b/app/build.gradle index b374db47b3..86f6e22678 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -283,8 +283,13 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' + implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' + // you will want to install the android studio lombok plugin + compileOnly 'org.projectlombok:lombok:1.16.20' + // compileOnly 'javax.annotation:javax.annotation-api:1.3.2' + annotationProcessor "org.projectlombok:lombok:1.16.20" } task unzip(type: Copy) { diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java b/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java new file mode 100644 index 0000000000..a106e8e48a --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java @@ -0,0 +1,550 @@ +package com.eveningoutpost.dexdrip.Models; + +import android.provider.BaseColumns; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.google.gson.annotations.Expose; + +/** + * Created by jamorham on 11/12/2016. + */ + +@Table(name = "BloodTest", id = BaseColumns._ID) +public class BloodTest extends Model { + + public static final long STATE_VALID = 1 << 0; + public static final long STATE_CALIBRATION = 1 << 1; + public static final long STATE_NOTE = 1 << 2; + public static final long STATE_UNDONE = 1 << 3; + public static final long STATE_OVERWRITTEN = 1 << 4; + + private static long highest_timestamp = 0; + private static boolean patched = false; + private final static String TAG = "BloodTest"; + private final static String LAST_BT_AUTO_CALIB_UUID = "last-bt-auto-calib-uuid"; + private final static boolean d = false; + + @Expose + @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public long timestamp; + + @Expose + @Column(name = "mgdl") + public double mgdl; + + @Expose + @Column(name = "created_timestamp") + public long created_timestamp; + + @Expose + @Column(name = "state") + public long state; // bitfield + + @Expose + @Column(name = "source") + public String source; + + @Expose + @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) + public String uuid; + +/* + public GlucoseReadingRx glucoseReadingRx; + + // patches and saves + public Long saveit() { + fixUpTable(); + return save(); + } + + public void addState(long flag) { + state |= flag; + save(); + } + + public void removeState(long flag) { + state &= ~flag; + save(); + } + + public String toS() { + final Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + return gson.toJson(this); + } + + private BloodTestMessage toMessageNative() { + return new BloodTestMessage.Builder() + .timestamp(timestamp) + .mgdl(mgdl) + .created_timestamp(created_timestamp) + .state(state) + .source(source) + .uuid(uuid) + .build(); + } + + public byte[] toMessage() { + final List btl = new ArrayList<>(); + btl.add(this); + return toMultiMessage(btl); + } + + + // static methods + private static final long CLOSEST_READING_MS = 30000; // 30 seconds + + public static BloodTest create(long timestamp_ms, double mgdl, String source) { + return create(timestamp_ms, mgdl, source, null); + } + + public static BloodTest create(long timestamp_ms, double mgdl, String source, String suggested_uuid) { + + if ((timestamp_ms == 0) || (mgdl == 0)) { + UserError.Log.e(TAG, "Either timestamp or mgdl is zero - cannot create reading"); + return null; + } + + if (timestamp_ms < 1487759433000L) { + UserError.Log.d(TAG, "Timestamp really too far in the past @ " + timestamp_ms); + return null; + } + + final long now = JoH.tsl(); + if (timestamp_ms > now) { + if ((timestamp_ms - now) > 600000) { + UserError.Log.wtf(TAG, "Timestamp is > 10 minutes in the future! Something is wrong: " + JoH.dateTimeText(timestamp_ms)); + return null; + } + timestamp_ms = now; // force to now if it showed up to 10 mins in the future + } + + final BloodTest match = getForPreciseTimestamp(timestamp_ms, CLOSEST_READING_MS); + if (match == null) { + final BloodTest bt = new BloodTest(); + bt.timestamp = timestamp_ms; + bt.mgdl = mgdl; + bt.uuid = suggested_uuid == null ? UUID.randomUUID().toString() : suggested_uuid; + bt.created_timestamp = JoH.tsl(); + bt.state = STATE_VALID; + bt.source = source; + bt.saveit(); + if (UploaderQueue.newEntry("insert", bt) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + + if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { + if ((JoH.msSince(bt.timestamp) < Constants.MINUTE_IN_MS * 5) && (JoH.msSince(bt.timestamp) > 0)) { + UserError.Log.d(TAG, "Blood test value recent enough to send to G5"); + //Ob1G5StateMachine.addCalibration((int) bt.mgdl, timestamp_ms); + NativeCalibrationPipe.addCalibration((int) bt.mgdl, timestamp_ms); + } + } + + return bt; + } else { + UserError.Log.d(TAG, "Not creating new reading as timestamp is too close"); + } + return null; + } + + public static BloodTest createFromCal(double bg, double timeoffset, String source) { + return createFromCal(bg, timeoffset, source, null); + } + + public static BloodTest createFromCal(double bg, double timeoffset, String source, String suggested_uuid) { + final String unit = Pref.getString("units", "mgdl"); + + if (unit.compareTo("mgdl") != 0) { + bg = bg * Constants.MMOLL_TO_MGDL; + } + + if ((bg < 40) || (bg > 400)) { + Log.wtf(TAG, "Invalid out of range bloodtest glucose mg/dl value of: " + bg); + JoH.static_toast_long("Bloodtest out of range: " + bg + " mg/dl"); + return null; + } + + return create((long) (new Date().getTime() - timeoffset), bg, source, suggested_uuid); + } + + public static void pushBloodTestSyncToWatch(BloodTest bt, boolean is_new) { + Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue."); + if (Pref.getBooleanDefaultFalse("wear_sync")) { + if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bt) != null) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + + public static BloodTest last() { + final List btl = last(1); + if ((btl != null) && (btl.size() > 0)) { + return btl.get(0); + } else { + return null; + } + } + + public static List last(int num) { + try { + return new Select() + .from(BloodTest.class) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static List lastMatching(int num, String match) { + try { + return new Select() + .from(BloodTest.class) + .where("source like ?", match) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static BloodTest lastValid() { + final List btl = lastValid(1); + if ((btl != null) && (btl.size() > 0)) { + return btl.get(0); + } else { + return null; + } + } + + public static List lastValid(int num) { + try { + return new Select() + .from(BloodTest.class) + .where("state & ? != 0", BloodTest.STATE_VALID) + .orderBy("timestamp desc") + .limit(num) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + + public static BloodTest byUUID(String uuid) { + if (uuid == null) return null; + try { + return new Select() + .from(BloodTest.class) + .where("uuid = ?", uuid) + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static BloodTest byid(long id) { + try { + return new Select() + .from(BloodTest.class) + .where("_ID = ?", id) + .executeSingle(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return null; + } + } + + public static byte[] toMultiMessage(List btl) { + if (btl == null) return null; + final List BloodTestMessageList = new ArrayList<>(); + for (BloodTest bt : btl) { + BloodTestMessageList.add(bt.toMessageNative()); + } + return BloodTestMultiMessage.ADAPTER.encode(new BloodTestMultiMessage(BloodTestMessageList)); + } + + private static void processFromMessage(BloodTestMessage btm) { + if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) { + boolean is_new = false; + BloodTest bt = byUUID(btm.uuid); + if (bt == null) { + bt = getForPreciseTimestamp(Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS); + if (bt != null) { + UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bt.uuid + " vs " + btm.uuid + " skipping!"); + return; + } + bt = new BloodTest(); + is_new = true; + } else { + if (bt.state != Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE)) { + is_new = true; + } + } + bt.timestamp = Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP); + bt.mgdl = Wire.get(btm.mgdl, BloodTestMessage.DEFAULT_MGDL); + bt.created_timestamp = Wire.get(btm.created_timestamp, BloodTestMessage.DEFAULT_CREATED_TIMESTAMP); + bt.state = Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE); + bt.source = Wire.get(btm.source, BloodTestMessage.DEFAULT_SOURCE); + bt.uuid = btm.uuid; + bt.saveit(); // de-dupe by uuid + if (is_new) { // cannot handle updates yet + if (UploaderQueue.newEntry(is_new ? "insert" : "update", bt) != null) { + if (JoH.quietratelimit("start-sync-service", 5)) { + SyncService.startSyncService(3000); // sync in 3 seconds + } + } + } + } else { + UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid"); + } + } + + public static void processFromMultiMessage(byte[] payload) { + try { + final BloodTestMultiMessage btmm = BloodTestMultiMessage.ADAPTER.decode(payload); + if ((btmm != null) && (btmm.bloodtest_message != null)) { + for (BloodTestMessage btm : btmm.bloodtest_message) { + processFromMessage(btm); + } + Home.staticRefreshBGCharts(); + } + } catch (IOException | NullPointerException | IllegalStateException e) { + UserError.Log.e(TAG, "exception processFromMessage: " + e); + } + } + + public static BloodTest fromJSON(String json) { + if ((json == null) || (json.length() == 0)) { + UserError.Log.d(TAG, "Empty json received in bloodtest fromJson"); + return null; + } + try { + UserError.Log.d(TAG, "Processing incoming json: " + json); + return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, BloodTest.class); + } catch (Exception e) { + UserError.Log.d(TAG, "Got exception parsing bloodtest json: " + e.toString()); + Home.toaststaticnext("Error on Bloodtest sync, probably decryption key mismatch"); + return null; + } + } + + public static BloodTest getForPreciseTimestamp(long timestamp, long precision) { + BloodTest bloodTest = new Select() + .from(BloodTest.class) + .where("timestamp <= ?", (timestamp + precision)) + .where("timestamp >= ?", (timestamp - precision)) + .orderBy("abs(timestamp - " + timestamp + ") asc") + .executeSingle(); + if ((bloodTest != null) && (Math.abs(bloodTest.timestamp - timestamp) < precision)) { + return bloodTest; + } + return null; + } + + public static List latestForGraph(int number, double startTime) { + return latestForGraph(number, (long) startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime) { + return latestForGraph(number, startTime, Long.MAX_VALUE); + } + + public static List latestForGraph(int number, long startTime, long endTime) { + try { + return new Select() + .from(BloodTest.class) + .where("state & ? != 0", BloodTest.STATE_VALID) + .where("timestamp >= " + Math.max(startTime, 0)) + .where("timestamp <= " + endTime) + .orderBy("timestamp asc") // warn asc! + .limit(number) + .execute(); + } catch (android.database.sqlite.SQLiteException e) { + fixUpTable(); + return new ArrayList<>(); + } + } + + synchronized static void opportunisticCalibration() { + if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { + final BloodTest bt = lastValid(); + if (bt == null) { + Log.d(TAG, "opportunistic: No blood tests"); + return; + } + if (JoH.msSince(bt.timestamp) > (Constants.HOUR_IN_MS * 8)) { + Log.d(TAG, "opportunistic: Blood test older than 8 hours ago"); + return; + } + + if ((bt.uuid == null) || (bt.uuid.length() < 8)) { + Log.d(TAG, "opportunisitic: invalid uuid"); + return; + } + + if ((bt.uuid != null) && (bt.uuid.length() > 1) && PersistentStore.getString(LAST_BT_AUTO_CALIB_UUID).equals(bt.uuid)) { + Log.d(TAG, "opportunistic: Already processed uuid: " + bt.uuid); + return; + } + + final Calibration calibration = Calibration.lastValid(); + if (calibration == null) { + Log.d(TAG, "opportunistic: No calibrations"); + // TODO do we try to initial calibrate using this? + return; + } + + if (JoH.msSince(calibration.timestamp) < Constants.HOUR_IN_MS) { + Log.d(TAG, "opportunistic: Last calibration less than 1 hour ago"); + return; + } + + if (bt.timestamp <= calibration.timestamp) { + Log.d(TAG, "opportunistic: Blood test isn't more recent than last calibration"); + return; + } + + // get closest bgreading - must be within dexcom period and locked to sensor + final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD); + if (bgReading == null) { + Log.d(TAG, "opportunistic: No matching bg reading"); + return; + } + + if (bt.timestamp > highest_timestamp) { + Accuracy.create(bt, bgReading, "xDrip Original"); + final CalibrationAbstract plugin = PluggableCalibration.getCalibrationPluginFromPreferences(); + final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null; + if (plugin != null) { + BgReading pluginBgReading = plugin.getBgReadingFromBgReading(bgReading, cd); + Accuracy.create(bt, pluginBgReading, plugin.getAlgorithmName()); + } + highest_timestamp = bt.timestamp; + } + + if (!CalibrationRequest.isSlopeFlatEnough(bgReading)) { + Log.d(TAG, "opportunistic: Slope is not flat enough at: " + JoH.dateTimeText(bgReading.timestamp)); + return; + } + + // TODO store evaluation failure for this record in cache for future optimization + + // TODO Check we have prior reading as well perhaps + JoH.clearCache(); + UserError.Log.ueh(TAG, "Opportunistic calibration for Blood Test at " + JoH.dateTimeText(bt.timestamp) + " of " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " matching sensor slope at: " + JoH.dateTimeText(bgReading.timestamp) + " from source " + bt.source); + final long time_since = JoH.msSince(bt.timestamp); + + + Log.d(TAG, "opportunistic: attempting auto calibration"); + PersistentStore.setString(LAST_BT_AUTO_CALIB_UUID, bt.uuid); + Home.startHomeWithExtra(xdrip.getAppContext(), + Home.BLUETOOTH_METER_CALIBRATION, + BgGraphBuilder.unitized_string_static(bt.mgdl), + Long.toString(time_since), + "auto"); + } + } + + public static String evaluateAccuracy(long period) { + + // CACHE?? + + final List bloodTests = latestForGraph(1000, JoH.tsl() - period, JoH.tsl() - AddCalibration.estimatedInterstitialLagSeconds); + final List difference = new ArrayList<>(); + final List plugin_difference = new ArrayList<>(); + if ((bloodTests == null) || (bloodTests.size() == 0)) return null; + + final boolean show_plugin = true; + final CalibrationAbstract plugin = (show_plugin) ? PluggableCalibration.getCalibrationPluginFromPreferences() : null; + + + for (BloodTest bt : bloodTests) { + final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD); + + if (bgReading != null) { + final Calibration calibration = bgReading.calibration; + if (calibration == null) { + Log.d(TAG, "Calibration for bgReading is null! @ " + JoH.dateTimeText(bgReading.timestamp)); + continue; + } + final double diff = Math.abs(bgReading.calculated_value - bt.mgdl); + difference.add(diff); + if (d) { + Log.d(TAG, "Evaluate Accuracy: difference: " + JoH.qs(diff)); + } + final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null; + if ((plugin != null) && (cd != null)) { + final double plugin_diff = Math.abs(bt.mgdl - plugin.getGlucoseFromBgReading(bgReading, cd)); + plugin_difference.add(plugin_diff); + if (d) + Log.d(TAG, "Evaluate Plugin Accuracy: " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + " difference: " + JoH.qs(plugin_diff) + "/" + JoH.qs(plugin_diff * Constants.MGDL_TO_MMOLL, 2) + " calibration: " + JoH.qs(cd.slope, 2) + " " + JoH.qs(cd.intercept, 2)); + } + } + } + + if (difference.size() == 0) return null; + double avg = DoubleMath.mean(difference); + Log.d(TAG, "Average accuracy: " + accuracyAsString(avg) + " (" + JoH.qs(avg, 5) + ")"); + + if (plugin_difference.size() > 0) { + double plugin_avg = DoubleMath.mean(plugin_difference); + Log.d(TAG, "Plugin Average accuracy: " + accuracyAsString(plugin_avg) + " (" + JoH.qs(plugin_avg, 5) + ")"); + return accuracyAsString(plugin_avg) + " / " + accuracyAsString(avg); + } + return accuracyAsString(avg); + } + + public static String accuracyAsString(double avg) { + final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl"); + // +- symbol + return "\u00B1" + (!domgdl ? JoH.qs(avg * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(avg, 1) + " mgdl"); + } + + public static List cleanup(int retention_days) { + return new Delete() + .from(BloodTest.class) + .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS)) + .execute(); + } + + // create the table ourselves without worrying about model versioning and downgrading + private static void fixUpTable() { + if (patched) return; + final String[] patchup = { + "CREATE TABLE BloodTest (_id INTEGER PRIMARY KEY AUTOINCREMENT);", + "ALTER TABLE BloodTest ADD COLUMN timestamp INTEGER;", + "ALTER TABLE BloodTest ADD COLUMN created_timestamp INTEGER;", + "ALTER TABLE BloodTest ADD COLUMN state INTEGER;", + "ALTER TABLE BloodTest ADD COLUMN mgdl REAL;", + "ALTER TABLE BloodTest ADD COLUMN source TEXT;", + "ALTER TABLE BloodTest ADD COLUMN uuid TEXT;", + "CREATE UNIQUE INDEX index_Bloodtest_uuid on BloodTest(uuid);", + "CREATE UNIQUE INDEX index_Bloodtest_timestamp on BloodTest(timestamp);", + "CREATE INDEX index_Bloodtest_created_timestamp on BloodTest(created_timestamp);", + "CREATE INDEX index_Bloodtest_state on BloodTest(state);"}; + + for (String patch : patchup) { + try { + SQLiteUtils.execSql(patch); + // UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); + } catch (Exception e) { + // UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); + } + } + patched = true; + } + +*/ +} + diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java b/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java index df801512fe..3d8593df34 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java @@ -1,15 +1,23 @@ package com.eveningoutpost.dexdrip.Models; import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.PowerManager; +import android.util.Base64; import android.util.Log; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.UnsupportedEncodingException; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.GregorianCalendar; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.utils.DateUtil; public class JoH { @@ -19,6 +27,14 @@ public class JoH { return android.text.format.DateFormat.format("yyyy-MM-dd kk:mm:ss", timestamp).toString(); } + public static long msSince(long when) { + return (DateUtil.now() - when); + } + + public static long msTill(long when) { + return (when - DateUtil.now()); + } + public static String niceTimeScalar(long t) { String unit = MainApp.gs(R.string.unit_second); t = t / 1000; @@ -122,5 +138,46 @@ public class JoH { return wl; } + public static boolean isLANConnected() { + final ConnectivityManager cm = + (ConnectivityManager) MainApp.instance().getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + final boolean isConnected = activeNetwork != null && + activeNetwork.isConnected(); + return isConnected && ((activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) + || (activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET) + || (activeNetwork.getType() == ConnectivityManager.TYPE_BLUETOOTH)); + } + + private static Gson gson_instance; + public static Gson defaultGsonInstance() { + if (gson_instance == null) { + gson_instance = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + //.registerTypeAdapter(Date.class, new DateTypeAdapter()) + // .serializeSpecialFloatingPointValues() + .create(); + } + return gson_instance; + } + + public static String base64encodeBytes(byte[] input) { + try { + return new String(Base64.encode(input, Base64.NO_WRAP), "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Got unsupported encoding: " + e); + return "encode-error"; + } + } + + public static byte[] base64decodeBytes(String input) { + try { + return Base64.decode(input.getBytes("UTF-8"), Base64.NO_WRAP); + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + Log.e(TAG, "Got unsupported encoding: " + e); + return new byte[0]; + } + } + } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java new file mode 100644 index 0000000000..c3efae80b4 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java @@ -0,0 +1,120 @@ +package com.eveningoutpost.dexdrip.UtilityModels; + +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Log; + +import com.eveningoutpost.dexdrip.Models.JoH; + +import java.util.concurrent.ConcurrentHashMap; + +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.T; + +/** + * Created by jamorham on 07/03/2018. + *

+ * Tasks which are fired from events can be scheduled here and only execute when they become idle + * and are not being rescheduled within their wait window. + */ + +public class Inevitable { + + private static final String TAG = Inevitable.class.getSimpleName(); + private static final int MAX_QUEUE_TIME = (int) T.mins(6).msecs(); + private static final boolean d = true; + + private static final ConcurrentHashMap tasks = new ConcurrentHashMap<>(); + + public static synchronized void task(final String id, long idle_for, Runnable runnable) { + if (idle_for > MAX_QUEUE_TIME) { + throw new RuntimeException(id + " Requested time: " + idle_for + " beyond max queue time"); + } + final Task task = tasks.get(id); + if (task != null) { + // if it already exists then extend the time + task.extendTime(idle_for); + + if (d) + Log.d(TAG, "Extending time for: " + id + " to " + JoH.dateTimeText(task.when)); + } else { + // otherwise create new task + if (runnable == null) return; // extension only if already exists + tasks.put(id, new Task(id, idle_for, runnable)); + + if (d) + Log.d(TAG, "Creating task: " + id + " due: " + JoH.dateTimeText(tasks.get(id).when)); + + // create a thread to wait and execute in background + final Thread t = new Thread(() -> { + final PowerManager.WakeLock wl = JoH.getWakeLock(id, MAX_QUEUE_TIME + 5000); + try { + boolean running = true; + // wait for task to be due or killed + while (running) { + SystemClock.sleep(500); + final Task thisTask = tasks.get(id); + running = thisTask != null && !thisTask.poll(); + } + } finally { + JoH.releaseWakeLock(wl); + } + }); + t.setPriority(Thread.MIN_PRIORITY); + //t.setDaemon(true); + t.start(); + } + } + + public static synchronized void stackableTask(String id, long idle_for, Runnable runnable) { + int stack = 0; + while (tasks.get(id = id + "-" + stack) != null) { + stack++; + } + if (stack > 0) { + Log.d(TAG, "Task stacked to: " + id); + } + task(id, idle_for, runnable); + } + + public static void kill(final String id) { + tasks.remove(id); + } + + public static boolean waiting(final String id) { + return tasks.containsKey(id); + } + + private static class Task { + private long when; + private final Runnable what; + private final String id; + + Task(String id, long offset, Runnable what) { + this.what = what; + this.id = id; + extendTime(offset); + } + + public void extendTime(long offset) { + this.when = DateUtil.now() + offset; + } + + public boolean poll() { + final long till = JoH.msTill(when); + if (till < 1) { + if (d) Log.d(TAG, "Executing task! " + this.id); + tasks.remove(this.id); // early remove to allow overlapping scheduling + what.run(); + return true; + } else if (till > MAX_QUEUE_TIME) { + Log.wtf(TAG, "Task: " + this.id + " In queue too long: " + till); + tasks.remove(this.id); + return true; + } + return false; + } + + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java new file mode 100644 index 0000000000..5e2cfe6fed --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java @@ -0,0 +1,138 @@ +package com.eveningoutpost.dexdrip.UtilityModels; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; + +import com.eveningoutpost.dexdrip.Models.JoH; +import com.google.common.primitives.Bytes; + +import info.nightscout.androidaps.MainApp; + +/** + * Created by jamorham on 23/09/2016. + *

+ * This is for internal data which is never backed up, + * separate file means it doesn't clutter prefs + * we can afford to lose it, it is for internal states + * and is alternative to static variables which get + * flushed when classes are destroyed by garbage collection + *

+ * It is suitable for cache type variables where losing + * state will cause problems. Obviously it will be slower than + * pure in-memory state variables. + */ + + +public class PersistentStore { + + private static final String DATA_STORE_INTERNAL = "persist_internal_store"; + private static SharedPreferences prefs; + private static final boolean d = false; // debug flag + + public static String getString(final String name) { + return prefs.getString(name, ""); + } + + static { + try { + prefs = MainApp.instance() + .getSharedPreferences(DATA_STORE_INTERNAL, Context.MODE_PRIVATE); + } catch (NullPointerException e) { + android.util.Log.e("PersistentStore", "Failed to get context on init!!! nothing will work"); + } + } + + public static void setString(final String name, String value) { + prefs.edit().putString(name, value).apply(); + } + + // if string is different to what we have stored then update and return true + public static boolean updateStringIfDifferent(final String name, final String current) { + if (current == null) return false; // can't handle nulls + if (PersistentStore.getString(name).equals(current)) return false; + PersistentStore.setString(name, current); + return true; + } + + public static void appendString(String name, String value) { + setString(name, getString(name) + value); + } + + public static void appendString(String name, String value, String delimiter) { + String current = getString(name); + if (current.length() > 0) current += delimiter; + setString(name, current + value); + } + + public static void appendBytes(String name, byte[] value) { + setBytes(name, Bytes.concat(getBytes(name), value)); + } + + public static byte[] getBytes(String name) { + return JoH.base64decodeBytes(getString(name)); + } + + public static byte getByte(String name) { + return (byte) getLong(name); + } + + public static void setBytes(String name, byte[] value) { + setString(name, JoH.base64encodeBytes(value)); + } + + public static void setByte(String name, byte value) { + setLong(name, value); + } + + public static long getLong(String name) { + return prefs.getLong(name, 0); + } + + public static float getFloat(String name) { + return prefs.getFloat(name, 0); + } + + public static void setLong(String name, long value) { + prefs.edit().putLong(name, value).apply(); + } + + public static void setFloat(String name, float value) { + prefs.edit().putFloat(name, value).apply(); + } + + public static void setDouble(String name, double value) { + setLong(name, Double.doubleToRawLongBits(value)); + } + + public static double getDouble(String name) { + return Double.longBitsToDouble(getLong(name)); + } + + public static boolean getBoolean(String name) { + return prefs.getBoolean(name, false); + } + + public static boolean getBoolean(String name, boolean value) { + return prefs.getBoolean(name, value); + } + + public static void setBoolean(String name, boolean value) { + prefs.edit().putBoolean(name, value).apply(); + } + + public static long incrementLong(String name) { + final long val = getLong(name) + 1; + setLong(name, val); + return val; + } + + public static void setLongZeroIfSet(String name) { + if (getLong(name) > 0) setLong(name, 0); + } + + @SuppressLint("ApplySharedPref") + public static void commit() { + prefs.edit().commit(); + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java new file mode 100644 index 0000000000..745b0ae6e4 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java @@ -0,0 +1,98 @@ +package com.eveningoutpost.dexdrip.UtilityModels; + +import android.graphics.Color; +import android.support.annotation.ColorInt; + +import com.google.common.base.MoreObjects; + +import java.util.HashMap; + +import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.BAD; +import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.CRITICAL; +import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.GOOD; +import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NORMAL; +import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NOTICE; + +/** + * Created by jamorham on 14/01/2017. + *

+ * For representing row items suitable for MegaStatus + */ + +public class StatusItem { + + private static final HashMap colorHints = new HashMap<>(); + + static { + colorHints.put(NORMAL, Color.TRANSPARENT); + colorHints.put(GOOD, Color.parseColor("#003000")); + colorHints.put(BAD, Color.parseColor("#480000")); + colorHints.put(NOTICE, Color.parseColor("#403000")); + colorHints.put(CRITICAL, Color.parseColor("#770000")); + } + + public enum Highlight { + NORMAL, + GOOD, + BAD, + NOTICE, + CRITICAL; + + @ColorInt + public int color() { + return colorHint(this); + } + + } + + public String name; + public String value; + public Highlight highlight; + public String button_name; + public Runnable runnable; + + + public StatusItem(String name, String value) { + this(name, value, NORMAL); + } + + public StatusItem() { + this("line-break", "", NORMAL); + } + + public StatusItem(String name, Highlight highlight) { + this("heading-break", name, highlight); + } + + public StatusItem(String name, Runnable runnable) { + this("button-break", "", NORMAL, name, runnable); + } + + public StatusItem(String name, String value, Highlight highlight) { + this(name, value, highlight, null, null); + } + + public StatusItem(String name, String value, Highlight highlight, String button_name, Runnable runnable) { + this.name = name; + this.value = value; + this.highlight = highlight; + this.button_name = button_name; + this.runnable = runnable; + } + + public StatusItem(String name, Integer value) { + this(name, value, NORMAL); + } + + public StatusItem(String name, Integer value, Highlight highlight) { + this.name = name; + this.value = Integer.toString(value); + this.highlight = highlight; + } + + @ColorInt + public static int colorHint(final Highlight highlight) { + return MoreObjects.firstNonNull(colorHints.get(highlight), Color.TRANSPARENT); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java new file mode 100644 index 0000000000..3cb4e15e88 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java @@ -0,0 +1,53 @@ +package com.eveningoutpost.dexdrip.store; + +import java.util.HashMap; + +/** + * Created by jamorham on 08/11/2017. + *

+ * Fast implementation of KeyStore interface + * Uses an in-memory database for short lived data elements + * Content is expired as per normal garbage collection + * Neutral defaults favoured over null return values + * Static creation for fastest shared instance access + */ + +public class FastStore implements KeyStore { + + private static final FastStore mFastStore = new FastStore(); + private static final HashMap stringStore = new HashMap<>(); + private static final HashMap longStore = new HashMap<>(); + + // we trade substitution flexibility at the expense of some object creation + + private FastStore() { + // use getInstance! + } + + public static FastStore getInstance() { + return mFastStore; + } + + // interface methods + + public String getS(String key) { + if (stringStore.containsKey(key)) return stringStore.get(key); + return ""; + } + + public void putS(String key, String value) { + stringStore.put(key, value); + } + + public long getL(String key) { + if (longStore.containsKey(key)) return longStore.get(key); + return 0; + } + + public void putL(String key, long value) { + longStore.put(key, value); + } + +} + + diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java new file mode 100644 index 0000000000..3ea19214f6 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java @@ -0,0 +1,28 @@ +package com.eveningoutpost.dexdrip.store; + +/** + * Created by jamorham on 08/11/2017. + * + * KeyStore is a persistence interface allowing storage and retrieval of primitive data types + * referenced by a String based key. + * + * Implementations may choose how long data is retained for. + * Typical usage might include caching expensive function results + * + */ + +public interface KeyStore { + + // java type erasure prevents us using generics and then implementing multiple generic interfaces + // so storage types we are interested in get their own interface methods + + void putS(String key, String value); + + String getS(String key); + + void putL(String key, long value); + + long getL(String key); + + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java index 285f0b013b..384942e859 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java @@ -2,6 +2,8 @@ package com.eveningoutpost.dexdrip.tidepool; import com.google.gson.annotations.Expose; +import info.nightscout.androidaps.plugins.treatments.Treatment; + // jamorham public class EBolus extends BaseElement { @@ -23,4 +25,8 @@ public class EBolus extends BaseElement { populate(timestamp, uuid); } + public static EBolus fromTreatment(Treatment treatment) { + return new EBolus(treatment.insulin, treatment.insulin, treatment.date, "uuid-AAPS"); + } + } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java index 857c7905ad..45a4ba7732 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java @@ -4,6 +4,11 @@ package com.eveningoutpost.dexdrip.tidepool; import com.google.gson.annotations.Expose; +import java.util.LinkedList; +import java.util.List; + +import info.nightscout.androidaps.db.BgReading; + public class ESensorGlucose extends BaseElement { @@ -17,11 +22,11 @@ public class ESensorGlucose extends BaseElement { this.units = "mg/dL"; } -/* + static ESensorGlucose fromBgReading(final BgReading bgReading) { final ESensorGlucose sensorGlucose = new ESensorGlucose(); - sensorGlucose.populate(bgReading.timestamp, bgReading.uuid); - sensorGlucose.value = (int) bgReading.calculated_value; // TODO best glucose? + sensorGlucose.populate(bgReading.date, "uuid-AAPS"); + sensorGlucose.value = (int) bgReading.value; // TODO best glucose? return sensorGlucose; } @@ -33,5 +38,5 @@ public class ESensorGlucose extends BaseElement { } return results; } -*/ + } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java index 08de1a390c..64d5b87c34 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java @@ -2,6 +2,9 @@ package com.eveningoutpost.dexdrip.tidepool; import com.google.gson.annotations.Expose; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; +import info.nightscout.androidaps.plugins.treatments.Treatment; + // jamorham public class EWizard extends BaseElement { @@ -18,17 +21,17 @@ public class EWizard extends BaseElement { EWizard() { type = "wizard"; } -/* - public static EWizard fromTreatment(final Treatments treatment) { - final EWizard result = (EWizard)new EWizard().populate(treatment.timestamp, treatment.uuid); + + public static EWizard fromTreatment(final Treatment treatment) { + final EWizard result = (EWizard)new EWizard().populate(treatment.date, "uuid-AAPS"); result.carbInput = treatment.carbs; - result.insulinCarbRatio = Profile.getCarbRatio(treatment.timestamp); + result.insulinCarbRatio = ProfileFunctions.getInstance().getProfile(treatment.date).getIc(); if (treatment.insulin > 0) { - result.bolus = new EBolus(treatment.insulin, treatment.insulin, treatment.timestamp, treatment.uuid); + result.bolus = new EBolus(treatment.insulin, treatment.insulin, treatment.date, "uuid-AAPS"); } else { - result.bolus = new EBolus(0.0001,0.0001, treatment.timestamp, treatment.uuid); // fake insulin record + result.bolus = new EBolus(0.0001,0.0001, treatment.date, "uuid-AAPS"); // fake insulin record } return result; } -*/ + } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java index 1cd5ef5e25..26f7901cda 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java @@ -4,24 +4,25 @@ package com.eveningoutpost.dexdrip.tidepool; // lightweight class entry point -import com.eveningoutpost.dexdrip.Models.JoH; -import com.eveningoutpost.dexdrip.UtilityModels.Pref; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.receivers.ChargingStateReceiver; +import info.nightscout.androidaps.utils.SP; import static com.eveningoutpost.dexdrip.Models.JoH.isLANConnected; -import static com.eveningoutpost.dexdrip.utils.PowerStateReceiver.is_power_connected; public class TidepoolEntry { public static boolean enabled() { - return Pref.getBooleanDefaultFalse("cloud_storage_tidepool_enable"); + return SP.getBoolean(R.string.key_cloud_storage_tidepool_enable, false); } public static void newData() { if (enabled() - && (!Pref.getBooleanDefaultFalse("tidepool_only_while_charging") || is_power_connected()) - && (!Pref.getBooleanDefaultFalse("tidepool_only_while_unmetered") || isLANConnected()) - && JoH.pratelimit("tidepool-new-data-upload", 1200)) { + && (!SP.getBoolean(R.string.key_tidepool_only_while_charging, false) || ChargingStateReceiver.isCharging()) + && (!SP.getBoolean(R.string.key_tidepool_only_while_unmetered, false) || isLANConnected()) + // && JoH.pratelimit("tidepool-new-data-upload", 1200) + ) { TidepoolUploader.doLogin(false); } } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java index 9d8a580bbe..61b734bc3b 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java @@ -1,28 +1,34 @@ package com.eveningoutpost.dexdrip.tidepool; -import com.eveningoutpost.dexdrip.Models.APStatus; -import com.eveningoutpost.dexdrip.Models.BgReading; -import com.eveningoutpost.dexdrip.Models.BloodTest; +import android.util.Log; + import com.eveningoutpost.dexdrip.Models.JoH; -import com.eveningoutpost.dexdrip.Models.Profile; -import com.eveningoutpost.dexdrip.Models.Treatments; -import com.eveningoutpost.dexdrip.Models.UserError; -import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; -import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.utils.LogSlider; import com.eveningoutpost.dexdrip.utils.NamedSliderProcessor; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.UUID; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.db.BgReading; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; +import info.nightscout.androidaps.plugins.treatments.Treatment; +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.utils.T; + import static com.eveningoutpost.dexdrip.Models.JoH.dateTimeText; /** * jamorham - * + *

* This class gets the next time slice of all data to upload */ @@ -31,8 +37,8 @@ public class UploadChunk implements NamedSliderProcessor { private static final String TAG = "TidepoolUploadChunk"; private static final String LAST_UPLOAD_END_PREF = "tidepool-last-end"; - private static final long MAX_UPLOAD_SIZE = Constants.DAY_IN_MS * 7; // don't change this - private static final long DEFAULT_WINDOW_OFFSET = Constants.MINUTE_IN_MS * 15; + private static final long MAX_UPLOAD_SIZE = T.days(7).msecs(); // don't change this + private static final long DEFAULT_WINDOW_OFFSET = T.mins(15).msecs(); private static final long MAX_LATENCY_THRESHOLD_MINUTES = 1440; // minutes per day @@ -42,7 +48,7 @@ public class UploadChunk implements NamedSliderProcessor { final String result = get(session.start, session.end); if (result != null && result.length() < 3) { - UserError.Log.d(TAG, "No records in this time period, setting start to best end time"); + Log.d(TAG, "No records in this time period, setting start to best end time"); setLastEnd(Math.max(session.end, getOldestRecordTimeStamp())); } return result; @@ -50,13 +56,13 @@ public class UploadChunk implements NamedSliderProcessor { public static String get(final long start, final long end) { - UserError.Log.uel(TAG, "Syncing data between: " + dateTimeText(start) + " -> " + dateTimeText(end)); + Log.e(TAG, "Syncing data between: " + dateTimeText(start) + " -> " + dateTimeText(end)); if (end <= start) { - UserError.Log.e(TAG, "End is <= start: " + dateTimeText(start) + " " + dateTimeText(end)); + Log.e(TAG, "End is <= start: " + dateTimeText(start) + " " + dateTimeText(end)); return null; } if (end - start > MAX_UPLOAD_SIZE) { - UserError.Log.e(TAG, "More than max range - rejecting"); + Log.e(TAG, "More than max range - rejecting"); return null; } @@ -72,37 +78,37 @@ public class UploadChunk implements NamedSliderProcessor { private static long getWindowSizePreference() { try { - long value = (long) getLatencySliderValue(Pref.getInt("tidepool_window_latency", 0)); - return Math.max(value * Constants.MINUTE_IN_MS, DEFAULT_WINDOW_OFFSET); + long value = (long) getLatencySliderValue(SP.getInt(R.string.key_tidepool_window_latency, 0)); + return Math.max(T.mins(value).msecs(), DEFAULT_WINDOW_OFFSET); } catch (Exception e) { - UserError.Log.e(TAG, "Reverting to default of 15 minutes due to Window Size exception: " + e); + Log.e(TAG, "Reverting to default of 15 minutes due to Window Size exception: " + e); return DEFAULT_WINDOW_OFFSET; // default } } private static long maxWindow(final long last_end) { - //UserError.Log.d(TAG, "Max window is: " + getWindowSizePreference()); - return Math.min(last_end + MAX_UPLOAD_SIZE, JoH.tsl() - getWindowSizePreference()); + //Log.d(TAG, "Max window is: " + getWindowSizePreference()); + return Math.min(last_end + MAX_UPLOAD_SIZE, DateUtil.now() - getWindowSizePreference()); } public static long getLastEnd() { long result = PersistentStore.getLong(LAST_UPLOAD_END_PREF); - return Math.max(result, JoH.tsl() - Constants.MONTH_IN_MS * 2); + return Math.max(result, DateUtil.now() - T.months(2).msecs()); } public static void setLastEnd(final long when) { if (when > getLastEnd()) { PersistentStore.setLong(LAST_UPLOAD_END_PREF, when); - UserError.Log.d(TAG, "Updating last end to: " + dateTimeText(when)); + Log.d(TAG, "Updating last end to: " + dateTimeText(when)); } else { - UserError.Log.e(TAG, "Cannot set last end to: " + dateTimeText(when) + " vs " + dateTimeText(getLastEnd())); + Log.e(TAG, "Cannot set last end to: " + dateTimeText(when) + " vs " + dateTimeText(getLastEnd())); } } static List getTreatments(final long start, final long end) { List result = new LinkedList<>(); - final List treatments = Treatments.latestForGraph(1800, start, end); - for (Treatments treatment : treatments) { + final List treatments = TreatmentsPlugin.getPlugin().getService().getTreatmentDataFromTime(start, end, true); + for (Treatment treatment : treatments) { if (treatment.carbs > 0) { result.add(EWizard.fromTreatment(treatment)); } else if (treatment.insulin > 0) { @@ -121,46 +127,47 @@ public class UploadChunk implements NamedSliderProcessor { // TODO we could make sure we include records older than the first bg record for completeness final long start = 0; - final long end = JoH.tsl(); + final long end = DateUtil.now(); - final List bgReadingList = BgReading.latestForGraphAsc(1, start, end); + final List bgReadingList = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, false); if (bgReadingList != null && bgReadingList.size() > 0) { - return bgReadingList.get(0).timestamp; + return bgReadingList.get(0).date; } return -1; } static List getBloodTests(final long start, final long end) { - return EBloodGlucose.fromBloodTests(BloodTest.latestForGraph(1800, start, end)); + return new ArrayList<>(); +// return EBloodGlucose.fromBloodTests(BloodTest.latestForGraph(1800, start, end)); } static List getBgReadings(final long start, final long end) { - return ESensorGlucose.fromBgReadings(BgReading.latestForGraphAsc(15000, start, end)); + return ESensorGlucose.fromBgReadings(MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true)); } static List getBasals(final long start, final long end) { final List basals = new LinkedList<>(); - final List aplist = APStatus.latestForGraph(15000, start, end); + final List aplist = MainApp.getDbHelper().getTemporaryBasalsDataFromTime(start, end, true); EBasal current = null; - for (APStatus apStatus : aplist) { - final double this_rate = Profile.getBasalRate(apStatus.timestamp) * apStatus.basal_percent / 100d; + for (TemporaryBasal temporaryBasal : aplist) { + final double this_rate = temporaryBasal.tempBasalConvertedToAbsolute(temporaryBasal.date, ProfileFunctions.getInstance().getProfile(temporaryBasal.date)); if (current != null) { if (this_rate != current.rate) { - current.duration = apStatus.timestamp - current.timestamp; - UserError.Log.d(TAG, "Adding current: " + current.toS()); + current.duration = temporaryBasal.date - current.timestamp; + Log.d(TAG, "Adding current: " + current.toS()); if (current.isValid()) { basals.add(current); } else { - UserError.Log.e(TAG, "Current basal is invalid: " + current.toS()); + Log.e(TAG, "Current basal is invalid: " + current.toS()); } current = null; } else { - UserError.Log.d(TAG, "Same rate as previous basal record: " + current.rate + " " + apStatus.toS()); + Log.d(TAG, "Same rate as previous basal record: " + current.rate + " " + temporaryBasal.toStringFull()); } } if (current == null) { - current = new EBasal(this_rate, apStatus.timestamp, 0, UUID.nameUUIDFromBytes(("tidepool-basal" + apStatus.timestamp).getBytes()).toString()); // start duration is 0 + current = new EBasal(this_rate, temporaryBasal.date, 0, UUID.nameUUIDFromBytes(("tidepool-basal" + temporaryBasal.date).getBytes()).toString()); // start duration is 0 } } return basals; diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java new file mode 100644 index 0000000000..014d085ae4 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java @@ -0,0 +1,13 @@ +package com.eveningoutpost.dexdrip.utils; + +// jamorham + +public class LogSlider { + + // logarithmic slider with positions start - end representing values start - end, calculate value at selected position + public static double calc(int sliderStart, int sliderEnd, double valueStart, double valueEnd, int position) { + valueStart = Math.log(Math.max(1, valueStart)); + valueEnd = Math.log(Math.max(1, valueEnd)); + return Math.exp(valueStart + (valueEnd - valueStart) / (sliderEnd - sliderStart) * (position - sliderStart)); + } +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java new file mode 100644 index 0000000000..a13e4b05b1 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java @@ -0,0 +1,11 @@ +package com.eveningoutpost.dexdrip.utils; + +// jamorham + +// interpolate a slider by name from a supporting class + +public interface NamedSliderProcessor { + + int interpolate(String name, int position); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index 720e07dd15..7f1b527d00 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -50,6 +50,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.receivers.DBAccessRec import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin; import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin; import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin; +import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin; import info.nightscout.androidaps.plugins.general.versionChecker.VersionCheckerPlugin; import info.nightscout.androidaps.plugins.general.wear.WearPlugin; import info.nightscout.androidaps.plugins.general.xdripStatusline.StatuslinePlugin; @@ -204,6 +205,7 @@ public class MainApp extends Application { pluginsList.add(StatuslinePlugin.initPlugin(this)); pluginsList.add(PersistentNotificationPlugin.getPlugin()); pluginsList.add(NSClientPlugin.getPlugin()); + pluginsList.add(TidepoolPlugin.getPlugin()); pluginsList.add(MaintenancePlugin.initPlugin(this)); pluginsList.add(ConfigBuilderPlugin.getPlugin()); diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index eb51f5e2ba..4bddd714eb 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -596,7 +596,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } } - // -------------------- TREATMENT HANDLING ------------------- + // -------------------- TEMPTARGET HANDLING ------------------- public static void updateEarliestDataChange(long newDate) { if (earliestDataChange == null) { @@ -627,6 +627,23 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getTemptargetsDataFromTime(long from, long to, boolean ascending) { + try { + Dao daoTempTargets = getDaoTempTargets(); + List tempTargets; + QueryBuilder queryBuilder = daoTempTargets.queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + tempTargets = daoTempTargets.query(preparedQuery); + return tempTargets; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList(); + } + public boolean createOrUpdate(TempTarget tempTarget) { try { TempTarget old; @@ -950,6 +967,22 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getTemporaryBasalsDataFromTime(long from, long to, boolean ascending) { + try { + List tempbasals; + QueryBuilder queryBuilder = getDaoTemporaryBasal().queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + tempbasals = getDaoTemporaryBasal().query(preparedQuery); + return tempbasals; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList(); + } + private static void scheduleTemporaryBasalChange() { class PostRunnable implements Runnable { public void run() { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java new file mode 100644 index 0000000000..2d00825820 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.general.tidepool; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.interfaces.PluginDescription; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.plugins.source.BGSourceFragment; + +/** + * Created by mike on 28.11.2017. + */ + +public class TidepoolPlugin extends PluginBase { + private static Logger log = LoggerFactory.getLogger(L.DATABASE); + + private static TidepoolPlugin plugin = null; + + public static TidepoolPlugin getPlugin() { + if (plugin == null) + plugin = new TidepoolPlugin(); + return plugin; + } + + private TidepoolPlugin() { + super(new PluginDescription() + .mainType(PluginType.BGSOURCE) + .fragmentClass(BGSourceFragment.class.getName()) + .pluginName(R.string.tidepool) + .shortName(R.string.tidepool_shortname) + .preferencesId(R.xml.pref_tidepool) + .description(R.string.description_tidepool) + ); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java index 502b7f38b7..842c01d9ee 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java @@ -500,6 +500,23 @@ public class TreatmentService extends OrmLiteBaseService { return new ArrayList<>(); } + public List getTreatmentDataFromTime(long from, long to, boolean ascending) { + try { + Dao daoTreatments = getDao(); + List treatments; + QueryBuilder queryBuilder = daoTreatments.queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + treatments = daoTreatments.query(preparedQuery); + return treatments; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList<>(); + } + @Nullable @Override public IBinder onBind(Intent intent) { diff --git a/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java b/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java index b10c2e99e5..349e1771b9 100644 --- a/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java +++ b/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java @@ -10,12 +10,15 @@ import info.nightscout.androidaps.events.EventChargingState; public class ChargingStateReceiver extends BroadcastReceiver { + private static EventChargingState lastEvent; + @Override public void onReceive(Context context, Intent intent) { EventChargingState event = grabChargingState(context); if (event != null) MainApp.bus().post(event); + lastEvent = event; } public EventChargingState grabChargingState(Context context) { @@ -32,4 +35,7 @@ public class ChargingStateReceiver extends BroadcastReceiver { return event; } + static public boolean isCharging() { + return lastEvent != null && lastEvent.isCharging; + } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/T.java b/app/src/main/java/info/nightscout/androidaps/utils/T.java index 2a9bcfc42c..5671f0725c 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/T.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/T.java @@ -43,6 +43,12 @@ public class T { return t; } + public static T months(long month) { + T t = new T(); + t.time = month * 31 * 24 * 60 * 60 * 1000L; + return t; + } + public long msecs() { return time; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67c746535f..798c076d05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1329,6 +1329,21 @@ tidepool_username tidepool_password tidepool_dev_servers + tidepool_window_latency + tidepool_test_login + tidepool_only_while_charging + tidepool_only_while_unmetered + cloud_storage_tidepool_enable + Upload data to the Tidepool service + Sync to Tidepool + Your Tidepool login user name, normally your email address + Login User Name + Your Tidepool login password + Login Password + Test Tidepool Login + Data Age Mins + If enabled, uploads will go to https://int-app.tidepool.org instead of the regular https://app.tidepool.org/ + Use Integration (test) servers smbmaxminutes Dayligh Saving time @@ -1348,6 +1363,9 @@ old version very old version New version for at least %1$d days available! Fallback to LGS after 60 days, loop will be disabled after 90 days + Tidepool + TDP + Uploads data to Tidepool %1$d day diff --git a/app/src/main/res/xml/pref_tidepool.xml b/app/src/main/res/xml/pref_tidepool.xml new file mode 100644 index 0000000000..a01b600df1 --- /dev/null +++ b/app/src/main/res/xml/pref_tidepool.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + From 6f4d79bca871f5bd7894e4ab3f34c9590b6a0fc6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Jun 2019 18:51:41 +0200 Subject: [PATCH 03/39] fix copy paste errors --- app/build.gradle | 3 +-- .../androidaps/plugins/general/tidepool/TidepoolPlugin.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 86f6e22678..d2e5a3ff7f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -250,7 +250,7 @@ dependencies { // excluding org.json which is provided by Android exclude group: "org.json", module: "json" } - implementation "com.google.code.gson:gson:2.4" + implementation "com.google.code.gson:gson:2.8.5" implementation "com.google.guava:guava:24.1-jre" implementation "net.danlew:android.joda:2.9.9.1" @@ -283,7 +283,6 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' - implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' // you will want to install the android studio lombok plugin diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java index 2d00825820..580b602deb 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java @@ -27,8 +27,7 @@ public class TidepoolPlugin extends PluginBase { private TidepoolPlugin() { super(new PluginDescription() - .mainType(PluginType.BGSOURCE) - .fragmentClass(BGSourceFragment.class.getName()) + .mainType(PluginType.GENERAL) .pluginName(R.string.tidepool) .shortName(R.string.tidepool_shortname) .preferencesId(R.xml.pref_tidepool) From 42ceaac0f7ddde283e608dd7b0df1a1182f15762 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Jun 2019 22:29:57 +0200 Subject: [PATCH 04/39] remove activeandroid --- app/build.gradle | 1 - .../eveningoutpost/dexdrip/Models/BloodTest.java | 14 +------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d2e5a3ff7f..b9acadd2af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -279,7 +279,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // xDrip-plus port - implementation 'com.activeandroid:thread-safe-active-android:3.1.1' implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java b/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java index a106e8e48a..bd8d80dd65 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java @@ -1,18 +1,12 @@ package com.eveningoutpost.dexdrip.Models; -import android.provider.BaseColumns; - -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; import com.google.gson.annotations.Expose; /** * Created by jamorham on 11/12/2016. */ -@Table(name = "BloodTest", id = BaseColumns._ID) -public class BloodTest extends Model { +public class BloodTest { public static final long STATE_VALID = 1 << 0; public static final long STATE_CALIBRATION = 1 << 1; @@ -27,27 +21,21 @@ public class BloodTest extends Model { private final static boolean d = false; @Expose - @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) public long timestamp; @Expose - @Column(name = "mgdl") public double mgdl; @Expose - @Column(name = "created_timestamp") public long created_timestamp; @Expose - @Column(name = "state") public long state; // bitfield @Expose - @Column(name = "source") public String source; @Expose - @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) public String uuid; /* From e2364561ed2dac498612535aa1bd562012b6e090 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 2 Jun 2019 16:24:51 +0200 Subject: [PATCH 05/39] transform to kotlin, cleanup, sort --- app/build.gradle | 10 +- .../dexdrip/Models/BloodTest.java | 538 ------------------ .../eveningoutpost/dexdrip/Models/JoH.java | 183 ------ .../dexdrip/UtilityModels/Inevitable.java | 120 ---- .../UtilityModels/PersistentStore.java | 138 ----- .../dexdrip/UtilityModels/StatusItem.java | 98 ---- .../dexdrip/store/FastStore.java | 53 -- .../dexdrip/store/KeyStore.java | 28 - .../dexdrip/tidepool/BaseElement.java | 40 -- .../dexdrip/tidepool/BaseMessage.java | 24 - .../dexdrip/tidepool/DateUtil.java | 46 -- .../dexdrip/tidepool/EBasal.java | 43 -- .../dexdrip/tidepool/EBloodGlucose.java | 45 -- .../dexdrip/tidepool/EBolus.java | 32 -- .../dexdrip/tidepool/ESensorGlucose.java | 42 -- .../dexdrip/tidepool/EWizard.java | 37 -- .../tidepool/GzipRequestInterceptor.java | 48 -- .../dexdrip/tidepool/InfoInterceptor.java | 32 -- .../dexdrip/tidepool/MAuthReply.java | 36 -- .../dexdrip/tidepool/MAuthRequest.java | 24 - .../tidepool/MCloseDatasetRequest.java | 9 - .../dexdrip/tidepool/MDatasetReply.java | 47 -- .../dexdrip/tidepool/MGetDatasetsRequest.java | 4 - .../dexdrip/tidepool/MOpenDatasetRequest.java | 63 -- .../dexdrip/tidepool/MUploadReply.java | 9 - .../dexdrip/tidepool/Session.java | 54 -- .../dexdrip/tidepool/TidepoolCallback.java | 67 --- .../dexdrip/tidepool/TidepoolEntry.java | 29 - .../dexdrip/tidepool/TidepoolStatus.java | 31 - .../dexdrip/tidepool/UploadChunk.java | 189 ------ .../dexdrip/utils/LogSlider.java | 13 - .../dexdrip/utils/NamedSliderProcessor.java | 11 - .../info/nightscout/androidaps/MainApp.java | 2 +- .../info/nightscout/androidaps/logging/L.java | 2 + .../general/tidepool/TidepoolPlugin.java | 38 -- .../general/tidepool/TidepoolPlugin.kt | 57 ++ .../tidepool/TidepoolUploaded.java.txt} | 112 ++-- .../general/tidepool/comm/InfoInterceptor.kt | 26 + .../plugins/general/tidepool/comm/Session.kt | 58 ++ .../tidepool/comm/TidepoolApiService.kt | 48 ++ .../general/tidepool/comm/TidepoolCallback.kt | 38 ++ .../general/tidepool/comm/TidepoolUploader.kt | 232 ++++++++ .../general/tidepool/comm/UploadChunk.kt | 169 ++++++ .../general/tidepool/elements/BasalElement.kt | 42 ++ .../general/tidepool/elements/BaseElement.kt | 30 + .../tidepool/elements/BloodGlucoseElement.kt | 33 ++ .../general/tidepool/elements/BolusElement.kt | 30 + .../tidepool/elements/SensorGlucoseElement.kt | 34 ++ .../tidepool/elements/WizardElement.kt | 37 ++ .../tidepool/events/EventTidepoolStatus.kt | 6 + .../tidepool/messages/AuthReplyMessage.kt | 29 + .../tidepool/messages/AuthRequestMessage.kt | 17 + .../general/tidepool/messages/BaseMessage.kt | 16 + .../messages/CloseDatasetRequestMessage.kt | 8 + .../tidepool/messages/DatasetReplyMessage.kt | 42 ++ .../messages/OpenDatasetRequestMessage.kt | 63 ++ .../tidepool/messages/UploadReplyMessage.kt | 6 + .../general/tidepool/utils/GsonInstance.kt | 17 + .../general/tidepool/utils/LogSlider.kt | 12 + .../general/tidepool/utils/RateLimit.kt | 29 + .../nightscout/androidaps/utils/DateUtil.java | 90 +++ .../androidaps/utils/StringUtils.java | 5 + app/src/main/res/values/strings.xml | 7 +- 63 files changed, 1247 insertions(+), 2231 deletions(-) delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java delete mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java delete mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt rename app/src/main/java/{com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java => info/nightscout/androidaps/plugins/general/tidepool/TidepoolUploaded.java.txt} (77%) create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/LogSlider.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt diff --git a/app/build.gradle b/app/build.gradle index b9acadd2af..e6b06601bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -278,16 +278,22 @@ dependencies { androidTestImplementation "com.google.dexmaker:dexmaker-mockito:${dexmakerVersion}" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + // xDrip-plus port - implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' - implementation 'com.squareup.retrofit2:converter-gson:2.4.0' // you will want to install the android studio lombok plugin compileOnly 'org.projectlombok:lombok:1.16.20' // compileOnly 'javax.annotation:javax.annotation-api:1.3.2' annotationProcessor "org.projectlombok:lombok:1.16.20" + + // new for tidepool + implementation "com.squareup.retrofit2:retrofit:2.4.0" + implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0" + implementation "com.squareup.retrofit2:converter-gson:2.4.0" + + implementation "io.reactivex.rxjava2:rxandroid:2.0.1" } task unzip(type: Copy) { diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java b/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java deleted file mode 100644 index bd8d80dd65..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/Models/BloodTest.java +++ /dev/null @@ -1,538 +0,0 @@ -package com.eveningoutpost.dexdrip.Models; - -import com.google.gson.annotations.Expose; - -/** - * Created by jamorham on 11/12/2016. - */ - -public class BloodTest { - - public static final long STATE_VALID = 1 << 0; - public static final long STATE_CALIBRATION = 1 << 1; - public static final long STATE_NOTE = 1 << 2; - public static final long STATE_UNDONE = 1 << 3; - public static final long STATE_OVERWRITTEN = 1 << 4; - - private static long highest_timestamp = 0; - private static boolean patched = false; - private final static String TAG = "BloodTest"; - private final static String LAST_BT_AUTO_CALIB_UUID = "last-bt-auto-calib-uuid"; - private final static boolean d = false; - - @Expose - public long timestamp; - - @Expose - public double mgdl; - - @Expose - public long created_timestamp; - - @Expose - public long state; // bitfield - - @Expose - public String source; - - @Expose - public String uuid; - -/* - public GlucoseReadingRx glucoseReadingRx; - - // patches and saves - public Long saveit() { - fixUpTable(); - return save(); - } - - public void addState(long flag) { - state |= flag; - save(); - } - - public void removeState(long flag) { - state &= ~flag; - save(); - } - - public String toS() { - final Gson gson = new GsonBuilder() - .excludeFieldsWithoutExposeAnnotation() - .create(); - return gson.toJson(this); - } - - private BloodTestMessage toMessageNative() { - return new BloodTestMessage.Builder() - .timestamp(timestamp) - .mgdl(mgdl) - .created_timestamp(created_timestamp) - .state(state) - .source(source) - .uuid(uuid) - .build(); - } - - public byte[] toMessage() { - final List btl = new ArrayList<>(); - btl.add(this); - return toMultiMessage(btl); - } - - - // static methods - private static final long CLOSEST_READING_MS = 30000; // 30 seconds - - public static BloodTest create(long timestamp_ms, double mgdl, String source) { - return create(timestamp_ms, mgdl, source, null); - } - - public static BloodTest create(long timestamp_ms, double mgdl, String source, String suggested_uuid) { - - if ((timestamp_ms == 0) || (mgdl == 0)) { - UserError.Log.e(TAG, "Either timestamp or mgdl is zero - cannot create reading"); - return null; - } - - if (timestamp_ms < 1487759433000L) { - UserError.Log.d(TAG, "Timestamp really too far in the past @ " + timestamp_ms); - return null; - } - - final long now = JoH.tsl(); - if (timestamp_ms > now) { - if ((timestamp_ms - now) > 600000) { - UserError.Log.wtf(TAG, "Timestamp is > 10 minutes in the future! Something is wrong: " + JoH.dateTimeText(timestamp_ms)); - return null; - } - timestamp_ms = now; // force to now if it showed up to 10 mins in the future - } - - final BloodTest match = getForPreciseTimestamp(timestamp_ms, CLOSEST_READING_MS); - if (match == null) { - final BloodTest bt = new BloodTest(); - bt.timestamp = timestamp_ms; - bt.mgdl = mgdl; - bt.uuid = suggested_uuid == null ? UUID.randomUUID().toString() : suggested_uuid; - bt.created_timestamp = JoH.tsl(); - bt.state = STATE_VALID; - bt.source = source; - bt.saveit(); - if (UploaderQueue.newEntry("insert", bt) != null) { - SyncService.startSyncService(3000); // sync in 3 seconds - } - - if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { - if ((JoH.msSince(bt.timestamp) < Constants.MINUTE_IN_MS * 5) && (JoH.msSince(bt.timestamp) > 0)) { - UserError.Log.d(TAG, "Blood test value recent enough to send to G5"); - //Ob1G5StateMachine.addCalibration((int) bt.mgdl, timestamp_ms); - NativeCalibrationPipe.addCalibration((int) bt.mgdl, timestamp_ms); - } - } - - return bt; - } else { - UserError.Log.d(TAG, "Not creating new reading as timestamp is too close"); - } - return null; - } - - public static BloodTest createFromCal(double bg, double timeoffset, String source) { - return createFromCal(bg, timeoffset, source, null); - } - - public static BloodTest createFromCal(double bg, double timeoffset, String source, String suggested_uuid) { - final String unit = Pref.getString("units", "mgdl"); - - if (unit.compareTo("mgdl") != 0) { - bg = bg * Constants.MMOLL_TO_MGDL; - } - - if ((bg < 40) || (bg > 400)) { - Log.wtf(TAG, "Invalid out of range bloodtest glucose mg/dl value of: " + bg); - JoH.static_toast_long("Bloodtest out of range: " + bg + " mg/dl"); - return null; - } - - return create((long) (new Date().getTime() - timeoffset), bg, source, suggested_uuid); - } - - public static void pushBloodTestSyncToWatch(BloodTest bt, boolean is_new) { - Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue."); - if (Pref.getBooleanDefaultFalse("wear_sync")) { - if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bt) != null) { - SyncService.startSyncService(3000); // sync in 3 seconds - } - } - } - - public static BloodTest last() { - final List btl = last(1); - if ((btl != null) && (btl.size() > 0)) { - return btl.get(0); - } else { - return null; - } - } - - public static List last(int num) { - try { - return new Select() - .from(BloodTest.class) - .orderBy("timestamp desc") - .limit(num) - .execute(); - } catch (android.database.sqlite.SQLiteException e) { - fixUpTable(); - return null; - } - } - - public static List lastMatching(int num, String match) { - try { - return new Select() - .from(BloodTest.class) - .where("source like ?", match) - .orderBy("timestamp desc") - .limit(num) - .execute(); - } catch (android.database.sqlite.SQLiteException e) { - fixUpTable(); - return null; - } - } - - public static BloodTest lastValid() { - final List btl = lastValid(1); - if ((btl != null) && (btl.size() > 0)) { - return btl.get(0); - } else { - return null; - } - } - - public static List lastValid(int num) { - try { - return new Select() - .from(BloodTest.class) - .where("state & ? != 0", BloodTest.STATE_VALID) - .orderBy("timestamp desc") - .limit(num) - .execute(); - } catch (android.database.sqlite.SQLiteException e) { - fixUpTable(); - return null; - } - } - - - public static BloodTest byUUID(String uuid) { - if (uuid == null) return null; - try { - return new Select() - .from(BloodTest.class) - .where("uuid = ?", uuid) - .executeSingle(); - } catch (android.database.sqlite.SQLiteException e) { - fixUpTable(); - return null; - } - } - - public static BloodTest byid(long id) { - try { - return new Select() - .from(BloodTest.class) - .where("_ID = ?", id) - .executeSingle(); - } catch (android.database.sqlite.SQLiteException e) { - fixUpTable(); - return null; - } - } - - public static byte[] toMultiMessage(List btl) { - if (btl == null) return null; - final List BloodTestMessageList = new ArrayList<>(); - for (BloodTest bt : btl) { - BloodTestMessageList.add(bt.toMessageNative()); - } - return BloodTestMultiMessage.ADAPTER.encode(new BloodTestMultiMessage(BloodTestMessageList)); - } - - private static void processFromMessage(BloodTestMessage btm) { - if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) { - boolean is_new = false; - BloodTest bt = byUUID(btm.uuid); - if (bt == null) { - bt = getForPreciseTimestamp(Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS); - if (bt != null) { - UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bt.uuid + " vs " + btm.uuid + " skipping!"); - return; - } - bt = new BloodTest(); - is_new = true; - } else { - if (bt.state != Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE)) { - is_new = true; - } - } - bt.timestamp = Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP); - bt.mgdl = Wire.get(btm.mgdl, BloodTestMessage.DEFAULT_MGDL); - bt.created_timestamp = Wire.get(btm.created_timestamp, BloodTestMessage.DEFAULT_CREATED_TIMESTAMP); - bt.state = Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE); - bt.source = Wire.get(btm.source, BloodTestMessage.DEFAULT_SOURCE); - bt.uuid = btm.uuid; - bt.saveit(); // de-dupe by uuid - if (is_new) { // cannot handle updates yet - if (UploaderQueue.newEntry(is_new ? "insert" : "update", bt) != null) { - if (JoH.quietratelimit("start-sync-service", 5)) { - SyncService.startSyncService(3000); // sync in 3 seconds - } - } - } - } else { - UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid"); - } - } - - public static void processFromMultiMessage(byte[] payload) { - try { - final BloodTestMultiMessage btmm = BloodTestMultiMessage.ADAPTER.decode(payload); - if ((btmm != null) && (btmm.bloodtest_message != null)) { - for (BloodTestMessage btm : btmm.bloodtest_message) { - processFromMessage(btm); - } - Home.staticRefreshBGCharts(); - } - } catch (IOException | NullPointerException | IllegalStateException e) { - UserError.Log.e(TAG, "exception processFromMessage: " + e); - } - } - - public static BloodTest fromJSON(String json) { - if ((json == null) || (json.length() == 0)) { - UserError.Log.d(TAG, "Empty json received in bloodtest fromJson"); - return null; - } - try { - UserError.Log.d(TAG, "Processing incoming json: " + json); - return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, BloodTest.class); - } catch (Exception e) { - UserError.Log.d(TAG, "Got exception parsing bloodtest json: " + e.toString()); - Home.toaststaticnext("Error on Bloodtest sync, probably decryption key mismatch"); - return null; - } - } - - public static BloodTest getForPreciseTimestamp(long timestamp, long precision) { - BloodTest bloodTest = new Select() - .from(BloodTest.class) - .where("timestamp <= ?", (timestamp + precision)) - .where("timestamp >= ?", (timestamp - precision)) - .orderBy("abs(timestamp - " + timestamp + ") asc") - .executeSingle(); - if ((bloodTest != null) && (Math.abs(bloodTest.timestamp - timestamp) < precision)) { - return bloodTest; - } - return null; - } - - public static List latestForGraph(int number, double startTime) { - return latestForGraph(number, (long) startTime, Long.MAX_VALUE); - } - - public static List latestForGraph(int number, long startTime) { - return latestForGraph(number, startTime, Long.MAX_VALUE); - } - - public static List latestForGraph(int number, long startTime, long endTime) { - try { - return new Select() - .from(BloodTest.class) - .where("state & ? != 0", BloodTest.STATE_VALID) - .where("timestamp >= " + Math.max(startTime, 0)) - .where("timestamp <= " + endTime) - .orderBy("timestamp asc") // warn asc! - .limit(number) - .execute(); - } catch (android.database.sqlite.SQLiteException e) { - fixUpTable(); - return new ArrayList<>(); - } - } - - synchronized static void opportunisticCalibration() { - if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { - final BloodTest bt = lastValid(); - if (bt == null) { - Log.d(TAG, "opportunistic: No blood tests"); - return; - } - if (JoH.msSince(bt.timestamp) > (Constants.HOUR_IN_MS * 8)) { - Log.d(TAG, "opportunistic: Blood test older than 8 hours ago"); - return; - } - - if ((bt.uuid == null) || (bt.uuid.length() < 8)) { - Log.d(TAG, "opportunisitic: invalid uuid"); - return; - } - - if ((bt.uuid != null) && (bt.uuid.length() > 1) && PersistentStore.getString(LAST_BT_AUTO_CALIB_UUID).equals(bt.uuid)) { - Log.d(TAG, "opportunistic: Already processed uuid: " + bt.uuid); - return; - } - - final Calibration calibration = Calibration.lastValid(); - if (calibration == null) { - Log.d(TAG, "opportunistic: No calibrations"); - // TODO do we try to initial calibrate using this? - return; - } - - if (JoH.msSince(calibration.timestamp) < Constants.HOUR_IN_MS) { - Log.d(TAG, "opportunistic: Last calibration less than 1 hour ago"); - return; - } - - if (bt.timestamp <= calibration.timestamp) { - Log.d(TAG, "opportunistic: Blood test isn't more recent than last calibration"); - return; - } - - // get closest bgreading - must be within dexcom period and locked to sensor - final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD); - if (bgReading == null) { - Log.d(TAG, "opportunistic: No matching bg reading"); - return; - } - - if (bt.timestamp > highest_timestamp) { - Accuracy.create(bt, bgReading, "xDrip Original"); - final CalibrationAbstract plugin = PluggableCalibration.getCalibrationPluginFromPreferences(); - final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null; - if (plugin != null) { - BgReading pluginBgReading = plugin.getBgReadingFromBgReading(bgReading, cd); - Accuracy.create(bt, pluginBgReading, plugin.getAlgorithmName()); - } - highest_timestamp = bt.timestamp; - } - - if (!CalibrationRequest.isSlopeFlatEnough(bgReading)) { - Log.d(TAG, "opportunistic: Slope is not flat enough at: " + JoH.dateTimeText(bgReading.timestamp)); - return; - } - - // TODO store evaluation failure for this record in cache for future optimization - - // TODO Check we have prior reading as well perhaps - JoH.clearCache(); - UserError.Log.ueh(TAG, "Opportunistic calibration for Blood Test at " + JoH.dateTimeText(bt.timestamp) + " of " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " matching sensor slope at: " + JoH.dateTimeText(bgReading.timestamp) + " from source " + bt.source); - final long time_since = JoH.msSince(bt.timestamp); - - - Log.d(TAG, "opportunistic: attempting auto calibration"); - PersistentStore.setString(LAST_BT_AUTO_CALIB_UUID, bt.uuid); - Home.startHomeWithExtra(xdrip.getAppContext(), - Home.BLUETOOTH_METER_CALIBRATION, - BgGraphBuilder.unitized_string_static(bt.mgdl), - Long.toString(time_since), - "auto"); - } - } - - public static String evaluateAccuracy(long period) { - - // CACHE?? - - final List bloodTests = latestForGraph(1000, JoH.tsl() - period, JoH.tsl() - AddCalibration.estimatedInterstitialLagSeconds); - final List difference = new ArrayList<>(); - final List plugin_difference = new ArrayList<>(); - if ((bloodTests == null) || (bloodTests.size() == 0)) return null; - - final boolean show_plugin = true; - final CalibrationAbstract plugin = (show_plugin) ? PluggableCalibration.getCalibrationPluginFromPreferences() : null; - - - for (BloodTest bt : bloodTests) { - final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD); - - if (bgReading != null) { - final Calibration calibration = bgReading.calibration; - if (calibration == null) { - Log.d(TAG, "Calibration for bgReading is null! @ " + JoH.dateTimeText(bgReading.timestamp)); - continue; - } - final double diff = Math.abs(bgReading.calculated_value - bt.mgdl); - difference.add(diff); - if (d) { - Log.d(TAG, "Evaluate Accuracy: difference: " + JoH.qs(diff)); - } - final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null; - if ((plugin != null) && (cd != null)) { - final double plugin_diff = Math.abs(bt.mgdl - plugin.getGlucoseFromBgReading(bgReading, cd)); - plugin_difference.add(plugin_diff); - if (d) - Log.d(TAG, "Evaluate Plugin Accuracy: " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + " difference: " + JoH.qs(plugin_diff) + "/" + JoH.qs(plugin_diff * Constants.MGDL_TO_MMOLL, 2) + " calibration: " + JoH.qs(cd.slope, 2) + " " + JoH.qs(cd.intercept, 2)); - } - } - } - - if (difference.size() == 0) return null; - double avg = DoubleMath.mean(difference); - Log.d(TAG, "Average accuracy: " + accuracyAsString(avg) + " (" + JoH.qs(avg, 5) + ")"); - - if (plugin_difference.size() > 0) { - double plugin_avg = DoubleMath.mean(plugin_difference); - Log.d(TAG, "Plugin Average accuracy: " + accuracyAsString(plugin_avg) + " (" + JoH.qs(plugin_avg, 5) + ")"); - return accuracyAsString(plugin_avg) + " / " + accuracyAsString(avg); - } - return accuracyAsString(avg); - } - - public static String accuracyAsString(double avg) { - final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl"); - // +- symbol - return "\u00B1" + (!domgdl ? JoH.qs(avg * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(avg, 1) + " mgdl"); - } - - public static List cleanup(int retention_days) { - return new Delete() - .from(BloodTest.class) - .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS)) - .execute(); - } - - // create the table ourselves without worrying about model versioning and downgrading - private static void fixUpTable() { - if (patched) return; - final String[] patchup = { - "CREATE TABLE BloodTest (_id INTEGER PRIMARY KEY AUTOINCREMENT);", - "ALTER TABLE BloodTest ADD COLUMN timestamp INTEGER;", - "ALTER TABLE BloodTest ADD COLUMN created_timestamp INTEGER;", - "ALTER TABLE BloodTest ADD COLUMN state INTEGER;", - "ALTER TABLE BloodTest ADD COLUMN mgdl REAL;", - "ALTER TABLE BloodTest ADD COLUMN source TEXT;", - "ALTER TABLE BloodTest ADD COLUMN uuid TEXT;", - "CREATE UNIQUE INDEX index_Bloodtest_uuid on BloodTest(uuid);", - "CREATE UNIQUE INDEX index_Bloodtest_timestamp on BloodTest(timestamp);", - "CREATE INDEX index_Bloodtest_created_timestamp on BloodTest(created_timestamp);", - "CREATE INDEX index_Bloodtest_state on BloodTest(state);"}; - - for (String patch : patchup) { - try { - SQLiteUtils.execSql(patch); - // UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch); - } catch (Exception e) { - // UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString()); - } - } - patched = true; - } - -*/ -} - diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java b/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java deleted file mode 100644 index 3d8593df34..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.eveningoutpost.dexdrip.Models; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.PowerManager; -import android.util.Base64; -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.UnsupportedEncodingException; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.util.GregorianCalendar; - -import info.nightscout.androidaps.MainApp; -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.utils.DateUtil; - -public class JoH { - - private final static String TAG = "jamorham JoH"; - - public static String dateTimeText(long timestamp) { - return android.text.format.DateFormat.format("yyyy-MM-dd kk:mm:ss", timestamp).toString(); - } - - public static long msSince(long when) { - return (DateUtil.now() - when); - } - - public static long msTill(long when) { - return (when - DateUtil.now()); - } - - public static String niceTimeScalar(long t) { - String unit = MainApp.gs(R.string.unit_second); - t = t / 1000; - if (t != 1) unit = MainApp.gs(R.string.unit_seconds); - if (t > 59) { - unit = MainApp.gs(R.string.unit_minute); - t = t / 60; - if (t != 1) unit = MainApp.gs(R.string.unit_minutes); - if (t > 59) { - unit = MainApp.gs(R.string.unit_hour); - t = t / 60; - if (t != 1) unit = MainApp.gs(R.string.unit_hours); - if (t > 24) { - unit = MainApp.gs(R.string.unit_day); - t = t / 24; - if (t != 1) unit = MainApp.gs(R.string.unit_days); - if (t > 28) { - unit = MainApp.gs(R.string.unit_week); - t = t / 7; - if (t != 1) unit = MainApp.gs(R.string.unit_weeks); - } - } - } - } - //if (t != 1) unit = unit + "s"; //implemented plurality in every step, because in other languages plurality of time is not every time adding the same character - return qs((double) t, 0) + " " + unit; - } - - // singletons to avoid repeated allocation - private static DecimalFormatSymbols dfs; - private static DecimalFormat df; - public static String qs(double x, int digits) { - - if (digits == -1) { - digits = 0; - if (((int) x != x)) { - digits++; - if ((((int) x * 10) / 10 != x)) { - digits++; - if ((((int) x * 100) / 100 != x)) digits++; - } - } - } - - if (dfs == null) { - final DecimalFormatSymbols local_dfs = new DecimalFormatSymbols(); - local_dfs.setDecimalSeparator('.'); - dfs = local_dfs; // avoid race condition - } - - final DecimalFormat this_df; - // use singleton if on ui thread otherwise allocate new as DecimalFormat is not thread safe - if (Thread.currentThread().getId() == 1) { - if (df == null) { - final DecimalFormat local_df = new DecimalFormat("#", dfs); - local_df.setMinimumIntegerDigits(1); - df = local_df; // avoid race condition - } - this_df = df; - } else { - this_df = new DecimalFormat("#", dfs); - } - - this_df.setMaximumFractionDigits(digits); - return this_df.format(x); - } - - public static long getTimeZoneOffsetMs() { - return new GregorianCalendar().getTimeZone().getRawOffset(); - } - - public static boolean emptyString(final String str) { - return str == null || str.length() == 0; - } - - public static PowerManager.WakeLock getWakeLock(final String name, int millis) { - final PowerManager pm = (PowerManager) MainApp.instance().getSystemService(Context.POWER_SERVICE); - final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); - wl.acquire(millis); - Log.d(TAG, "getWakeLock: " + name + " " + wl.toString()); - return wl; - } - - public static void releaseWakeLock(PowerManager.WakeLock wl) { - Log.d(TAG, "releaseWakeLock: " + wl.toString()); - if (wl == null) return; - if (wl.isHeld()) { - try { - wl.release(); - } catch (Exception e) { - Log.e(TAG, "Error releasing wakelock: " + e); - } - } - } - - public static PowerManager.WakeLock fullWakeLock(final String name, long millis) { - final PowerManager pm = (PowerManager) MainApp.instance().getSystemService(Context.POWER_SERVICE); - PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, name); - wl.acquire(millis); - Log.d(TAG, "fullWakeLock: " + name + " " + wl.toString()); - return wl; - } - - public static boolean isLANConnected() { - final ConnectivityManager cm = - (ConnectivityManager) MainApp.instance().getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - final boolean isConnected = activeNetwork != null && - activeNetwork.isConnected(); - return isConnected && ((activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) - || (activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET) - || (activeNetwork.getType() == ConnectivityManager.TYPE_BLUETOOTH)); - } - - private static Gson gson_instance; - public static Gson defaultGsonInstance() { - if (gson_instance == null) { - gson_instance = new GsonBuilder() - .excludeFieldsWithoutExposeAnnotation() - //.registerTypeAdapter(Date.class, new DateTypeAdapter()) - // .serializeSpecialFloatingPointValues() - .create(); - } - return gson_instance; - } - - public static String base64encodeBytes(byte[] input) { - try { - return new String(Base64.encode(input, Base64.NO_WRAP), "UTF-8"); - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Got unsupported encoding: " + e); - return "encode-error"; - } - } - - public static byte[] base64decodeBytes(String input) { - try { - return Base64.decode(input.getBytes("UTF-8"), Base64.NO_WRAP); - } catch (UnsupportedEncodingException | IllegalArgumentException e) { - Log.e(TAG, "Got unsupported encoding: " + e); - return new byte[0]; - } - } - - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java deleted file mode 100644 index c3efae80b4..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/Inevitable.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.eveningoutpost.dexdrip.UtilityModels; - -import android.os.PowerManager; -import android.os.SystemClock; -import android.util.Log; - -import com.eveningoutpost.dexdrip.Models.JoH; - -import java.util.concurrent.ConcurrentHashMap; - -import info.nightscout.androidaps.utils.DateUtil; -import info.nightscout.androidaps.utils.T; - -/** - * Created by jamorham on 07/03/2018. - *

- * Tasks which are fired from events can be scheduled here and only execute when they become idle - * and are not being rescheduled within their wait window. - */ - -public class Inevitable { - - private static final String TAG = Inevitable.class.getSimpleName(); - private static final int MAX_QUEUE_TIME = (int) T.mins(6).msecs(); - private static final boolean d = true; - - private static final ConcurrentHashMap tasks = new ConcurrentHashMap<>(); - - public static synchronized void task(final String id, long idle_for, Runnable runnable) { - if (idle_for > MAX_QUEUE_TIME) { - throw new RuntimeException(id + " Requested time: " + idle_for + " beyond max queue time"); - } - final Task task = tasks.get(id); - if (task != null) { - // if it already exists then extend the time - task.extendTime(idle_for); - - if (d) - Log.d(TAG, "Extending time for: " + id + " to " + JoH.dateTimeText(task.when)); - } else { - // otherwise create new task - if (runnable == null) return; // extension only if already exists - tasks.put(id, new Task(id, idle_for, runnable)); - - if (d) - Log.d(TAG, "Creating task: " + id + " due: " + JoH.dateTimeText(tasks.get(id).when)); - - // create a thread to wait and execute in background - final Thread t = new Thread(() -> { - final PowerManager.WakeLock wl = JoH.getWakeLock(id, MAX_QUEUE_TIME + 5000); - try { - boolean running = true; - // wait for task to be due or killed - while (running) { - SystemClock.sleep(500); - final Task thisTask = tasks.get(id); - running = thisTask != null && !thisTask.poll(); - } - } finally { - JoH.releaseWakeLock(wl); - } - }); - t.setPriority(Thread.MIN_PRIORITY); - //t.setDaemon(true); - t.start(); - } - } - - public static synchronized void stackableTask(String id, long idle_for, Runnable runnable) { - int stack = 0; - while (tasks.get(id = id + "-" + stack) != null) { - stack++; - } - if (stack > 0) { - Log.d(TAG, "Task stacked to: " + id); - } - task(id, idle_for, runnable); - } - - public static void kill(final String id) { - tasks.remove(id); - } - - public static boolean waiting(final String id) { - return tasks.containsKey(id); - } - - private static class Task { - private long when; - private final Runnable what; - private final String id; - - Task(String id, long offset, Runnable what) { - this.what = what; - this.id = id; - extendTime(offset); - } - - public void extendTime(long offset) { - this.when = DateUtil.now() + offset; - } - - public boolean poll() { - final long till = JoH.msTill(when); - if (till < 1) { - if (d) Log.d(TAG, "Executing task! " + this.id); - tasks.remove(this.id); // early remove to allow overlapping scheduling - what.run(); - return true; - } else if (till > MAX_QUEUE_TIME) { - Log.wtf(TAG, "Task: " + this.id + " In queue too long: " + till); - tasks.remove(this.id); - return true; - } - return false; - } - - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java deleted file mode 100644 index 5e2cfe6fed..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/PersistentStore.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.eveningoutpost.dexdrip.UtilityModels; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.SharedPreferences; - -import com.eveningoutpost.dexdrip.Models.JoH; -import com.google.common.primitives.Bytes; - -import info.nightscout.androidaps.MainApp; - -/** - * Created by jamorham on 23/09/2016. - *

- * This is for internal data which is never backed up, - * separate file means it doesn't clutter prefs - * we can afford to lose it, it is for internal states - * and is alternative to static variables which get - * flushed when classes are destroyed by garbage collection - *

- * It is suitable for cache type variables where losing - * state will cause problems. Obviously it will be slower than - * pure in-memory state variables. - */ - - -public class PersistentStore { - - private static final String DATA_STORE_INTERNAL = "persist_internal_store"; - private static SharedPreferences prefs; - private static final boolean d = false; // debug flag - - public static String getString(final String name) { - return prefs.getString(name, ""); - } - - static { - try { - prefs = MainApp.instance() - .getSharedPreferences(DATA_STORE_INTERNAL, Context.MODE_PRIVATE); - } catch (NullPointerException e) { - android.util.Log.e("PersistentStore", "Failed to get context on init!!! nothing will work"); - } - } - - public static void setString(final String name, String value) { - prefs.edit().putString(name, value).apply(); - } - - // if string is different to what we have stored then update and return true - public static boolean updateStringIfDifferent(final String name, final String current) { - if (current == null) return false; // can't handle nulls - if (PersistentStore.getString(name).equals(current)) return false; - PersistentStore.setString(name, current); - return true; - } - - public static void appendString(String name, String value) { - setString(name, getString(name) + value); - } - - public static void appendString(String name, String value, String delimiter) { - String current = getString(name); - if (current.length() > 0) current += delimiter; - setString(name, current + value); - } - - public static void appendBytes(String name, byte[] value) { - setBytes(name, Bytes.concat(getBytes(name), value)); - } - - public static byte[] getBytes(String name) { - return JoH.base64decodeBytes(getString(name)); - } - - public static byte getByte(String name) { - return (byte) getLong(name); - } - - public static void setBytes(String name, byte[] value) { - setString(name, JoH.base64encodeBytes(value)); - } - - public static void setByte(String name, byte value) { - setLong(name, value); - } - - public static long getLong(String name) { - return prefs.getLong(name, 0); - } - - public static float getFloat(String name) { - return prefs.getFloat(name, 0); - } - - public static void setLong(String name, long value) { - prefs.edit().putLong(name, value).apply(); - } - - public static void setFloat(String name, float value) { - prefs.edit().putFloat(name, value).apply(); - } - - public static void setDouble(String name, double value) { - setLong(name, Double.doubleToRawLongBits(value)); - } - - public static double getDouble(String name) { - return Double.longBitsToDouble(getLong(name)); - } - - public static boolean getBoolean(String name) { - return prefs.getBoolean(name, false); - } - - public static boolean getBoolean(String name, boolean value) { - return prefs.getBoolean(name, value); - } - - public static void setBoolean(String name, boolean value) { - prefs.edit().putBoolean(name, value).apply(); - } - - public static long incrementLong(String name) { - final long val = getLong(name) + 1; - setLong(name, val); - return val; - } - - public static void setLongZeroIfSet(String name) { - if (getLong(name) > 0) setLong(name, 0); - } - - @SuppressLint("ApplySharedPref") - public static void commit() { - prefs.edit().commit(); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java b/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java deleted file mode 100644 index 745b0ae6e4..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/StatusItem.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.eveningoutpost.dexdrip.UtilityModels; - -import android.graphics.Color; -import android.support.annotation.ColorInt; - -import com.google.common.base.MoreObjects; - -import java.util.HashMap; - -import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.BAD; -import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.CRITICAL; -import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.GOOD; -import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NORMAL; -import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NOTICE; - -/** - * Created by jamorham on 14/01/2017. - *

- * For representing row items suitable for MegaStatus - */ - -public class StatusItem { - - private static final HashMap colorHints = new HashMap<>(); - - static { - colorHints.put(NORMAL, Color.TRANSPARENT); - colorHints.put(GOOD, Color.parseColor("#003000")); - colorHints.put(BAD, Color.parseColor("#480000")); - colorHints.put(NOTICE, Color.parseColor("#403000")); - colorHints.put(CRITICAL, Color.parseColor("#770000")); - } - - public enum Highlight { - NORMAL, - GOOD, - BAD, - NOTICE, - CRITICAL; - - @ColorInt - public int color() { - return colorHint(this); - } - - } - - public String name; - public String value; - public Highlight highlight; - public String button_name; - public Runnable runnable; - - - public StatusItem(String name, String value) { - this(name, value, NORMAL); - } - - public StatusItem() { - this("line-break", "", NORMAL); - } - - public StatusItem(String name, Highlight highlight) { - this("heading-break", name, highlight); - } - - public StatusItem(String name, Runnable runnable) { - this("button-break", "", NORMAL, name, runnable); - } - - public StatusItem(String name, String value, Highlight highlight) { - this(name, value, highlight, null, null); - } - - public StatusItem(String name, String value, Highlight highlight, String button_name, Runnable runnable) { - this.name = name; - this.value = value; - this.highlight = highlight; - this.button_name = button_name; - this.runnable = runnable; - } - - public StatusItem(String name, Integer value) { - this(name, value, NORMAL); - } - - public StatusItem(String name, Integer value, Highlight highlight) { - this.name = name; - this.value = Integer.toString(value); - this.highlight = highlight; - } - - @ColorInt - public static int colorHint(final Highlight highlight) { - return MoreObjects.firstNonNull(colorHints.get(highlight), Color.TRANSPARENT); - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java deleted file mode 100644 index 3cb4e15e88..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/store/FastStore.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.eveningoutpost.dexdrip.store; - -import java.util.HashMap; - -/** - * Created by jamorham on 08/11/2017. - *

- * Fast implementation of KeyStore interface - * Uses an in-memory database for short lived data elements - * Content is expired as per normal garbage collection - * Neutral defaults favoured over null return values - * Static creation for fastest shared instance access - */ - -public class FastStore implements KeyStore { - - private static final FastStore mFastStore = new FastStore(); - private static final HashMap stringStore = new HashMap<>(); - private static final HashMap longStore = new HashMap<>(); - - // we trade substitution flexibility at the expense of some object creation - - private FastStore() { - // use getInstance! - } - - public static FastStore getInstance() { - return mFastStore; - } - - // interface methods - - public String getS(String key) { - if (stringStore.containsKey(key)) return stringStore.get(key); - return ""; - } - - public void putS(String key, String value) { - stringStore.put(key, value); - } - - public long getL(String key) { - if (longStore.containsKey(key)) return longStore.get(key); - return 0; - } - - public void putL(String key, long value) { - longStore.put(key, value); - } - -} - - diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java deleted file mode 100644 index 3ea19214f6..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/store/KeyStore.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.eveningoutpost.dexdrip.store; - -/** - * Created by jamorham on 08/11/2017. - * - * KeyStore is a persistence interface allowing storage and retrieval of primitive data types - * referenced by a String based key. - * - * Implementations may choose how long data is retained for. - * Typical usage might include caching expensive function results - * - */ - -public interface KeyStore { - - // java type erasure prevents us using generics and then implementing multiple generic interfaces - // so storage types we are interested in get their own interface methods - - void putS(String key, String value); - - String getS(String key); - - void putL(String key, long value); - - long getL(String key); - - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java deleted file mode 100644 index 0bf0bb6fa9..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseElement.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.google.gson.annotations.Expose; - -/** - * jamorham - *

- * common element base - */ - -public abstract class BaseElement { - @Expose - public String deviceTime; - @Expose - public String time; - @Expose - public int timezoneOffset; - @Expose - public String type; - @Expose - public Origin origin; - - - BaseElement populate(final long timestamp, final String uuid) { - deviceTime = DateUtil.toFormatNoZone(timestamp); - time = DateUtil.toFormatAsUTC(timestamp); - timezoneOffset = DateUtil.getTimeZoneOffsetMinutes(timestamp); // TODO - origin = new Origin(uuid); - return this; - } - - public class Origin { - @Expose - String id; - - Origin(String id) { - this.id = id; - } - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java deleted file mode 100644 index c96d3da1c4..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/BaseMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.eveningoutpost.dexdrip.Models.JoH; - -import okhttp3.MediaType; -import okhttp3.RequestBody; - -/** - * jamorham - * - * message base - */ - -public abstract class BaseMessage { - - public String toS() { - return JoH.defaultGsonInstance().toJson(this); - } - - public RequestBody getBody() { - return RequestBody.create(MediaType.parse("application/json"), this.toS()); - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java deleted file mode 100644 index 84f4076f75..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/DateUtil.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -/** - * jamorham - * - * Date utilities for preparing items for Tidepool upload - */ - -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.TimeZone; - -public class DateUtil { - - static String toFormatAsUTC(final long timestamp) { - final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'0000Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.format(timestamp); - } - - static String toFormatWithZone2(final long timestamp) { - // ISO 8601 not introduced till api 24 - so we have to do some gymnastics - final SimpleDateFormat formatIso8601 = new SimpleDateFormat("Z", Locale.US); - formatIso8601.setTimeZone(TimeZone.getDefault()); - String zone = formatIso8601.format(timestamp); - zone = zone.substring(0, zone.length() - 2) + ":" + zone.substring(zone.length() - 2); - if (zone.substring(0, 1).equals("+")) { - zone = zone.substring(1); - } - final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z" + zone + "'", Locale.US); - format.setTimeZone(TimeZone.getDefault()); - return format.format(timestamp); - } - - - static String toFormatNoZone(final long timestamp) { - final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); - format.setTimeZone(TimeZone.getDefault()); - return format.format(timestamp); - } - - static int getTimeZoneOffsetMinutes(final long timestamp) { - return TimeZone.getDefault().getOffset(timestamp) / 60000; - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java deleted file mode 100644 index 96819ba31c..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBasal.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.eveningoutpost.dexdrip.Models.JoH; -import com.google.gson.annotations.Expose; - -// jamorham - -public class EBasal extends BaseElement { - - long timestamp; // not exposed - - @Expose - String deliveryType = "automated"; - @Expose - long duration; - @Expose - double rate = -1; - @Expose - String scheduleName = "AAPS"; - @Expose - long clockDriftOffset = 0; - @Expose - long conversionOffset = 0; - - { - type = "basal"; - } - - EBasal(double rate, long timeStart, long duration, String uuid) { - this.timestamp = timeStart; - this.rate = rate; - this.duration = duration; - populate(timeStart, uuid); - } - - boolean isValid() { - return (rate > -1 && duration > 0); - } - - String toS() { - return rate + " Start: " + JoH.dateTimeText(timestamp) + " for: " + JoH.niceTimeScalar(duration); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java deleted file mode 100644 index 493c16ec3b..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBloodGlucose.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -import com.eveningoutpost.dexdrip.Models.BloodTest; -import com.google.gson.annotations.Expose; - -import java.util.LinkedList; -import java.util.List; - -class EBloodGlucose extends BaseElement { - - @Expose - String subType; - @Expose - String units; - @Expose - int value; - - EBloodGlucose() { - this.type = "smbg"; - this.units = "mg/dL"; - } - - - static EBloodGlucose fromBloodTest(final BloodTest bloodtest) { - final EBloodGlucose bg = new EBloodGlucose(); - bg.populate(bloodtest.timestamp, bloodtest.uuid); - - bg.subType = "manual"; // TODO - bg.value = (int) bloodtest.mgdl; - return bg; - } - - static List fromBloodTests(final List bloodTestList) { - if (bloodTestList == null) return null; - final List results = new LinkedList<>(); - for (BloodTest bt : bloodTestList) { - results.add(fromBloodTest(bt)); - } - return results; - } - -} - diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java deleted file mode 100644 index 384942e859..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EBolus.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.google.gson.annotations.Expose; - -import info.nightscout.androidaps.plugins.treatments.Treatment; - -// jamorham - -public class EBolus extends BaseElement { - - @Expose - public final String subType = "normal"; - @Expose - public final double normal; - @Expose - public final double expectedNormal; - - { - type = "bolus"; - } - - EBolus(double insulinDelivered, double insulinExpected, long timestamp, String uuid) { - this.normal = insulinDelivered; - this.expectedNormal = insulinExpected; - populate(timestamp, uuid); - } - - public static EBolus fromTreatment(Treatment treatment) { - return new EBolus(treatment.insulin, treatment.insulin, treatment.date, "uuid-AAPS"); - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java deleted file mode 100644 index 45a4ba7732..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/ESensorGlucose.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -import com.google.gson.annotations.Expose; - -import java.util.LinkedList; -import java.util.List; - -import info.nightscout.androidaps.db.BgReading; - -public class ESensorGlucose extends BaseElement { - - - @Expose - String units; - @Expose - int value; - - ESensorGlucose() { - this.type = "cbg"; - this.units = "mg/dL"; - } - - - static ESensorGlucose fromBgReading(final BgReading bgReading) { - final ESensorGlucose sensorGlucose = new ESensorGlucose(); - sensorGlucose.populate(bgReading.date, "uuid-AAPS"); - sensorGlucose.value = (int) bgReading.value; // TODO best glucose? - return sensorGlucose; - } - - static List fromBgReadings(final List bgReadingList) { - if (bgReadingList == null) return null; - final List results = new LinkedList<>(); - for (BgReading bgReading : bgReadingList) { - results.add(fromBgReading(bgReading)); - } - return results; - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java deleted file mode 100644 index 64d5b87c34..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/EWizard.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.google.gson.annotations.Expose; - -import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; -import info.nightscout.androidaps.plugins.treatments.Treatment; - -// jamorham - -public class EWizard extends BaseElement { - - @Expose - public String units = "mg/dL"; - @Expose - public double carbInput; - @Expose - public double insulinCarbRatio; - @Expose - public EBolus bolus; - - EWizard() { - type = "wizard"; - } - - public static EWizard fromTreatment(final Treatment treatment) { - final EWizard result = (EWizard)new EWizard().populate(treatment.date, "uuid-AAPS"); - result.carbInput = treatment.carbs; - result.insulinCarbRatio = ProfileFunctions.getInstance().getProfile(treatment.date).getIc(); - if (treatment.insulin > 0) { - result.bolus = new EBolus(treatment.insulin, treatment.insulin, treatment.date, "uuid-AAPS"); - } else { - result.bolus = new EBolus(0.0001,0.0001, treatment.date, "uuid-AAPS"); // fake insulin record - } - return result; - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java deleted file mode 100644 index 7dc3c156e9..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/GzipRequestInterceptor.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import java.io.IOException; - -import okhttp3.Interceptor; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okio.BufferedSink; -import okio.GzipSink; -import okio.Okio; - -class GzipRequestInterceptor implements Interceptor { - @Override - public okhttp3.Response intercept(Chain chain) throws IOException { - final Request originalRequest = chain.request(); - if (originalRequest.body() == null - || originalRequest.header("Content-Encoding") != null) - { - return chain.proceed(originalRequest); - } - - final Request compressedRequest = originalRequest.newBuilder() - .header("Content-Encoding", "gzip") - .method(originalRequest.method(), gzip(originalRequest.body())) - .build(); - return chain.proceed(compressedRequest); - } - - private RequestBody gzip(final RequestBody body) { - return new RequestBody() { - @Override public MediaType contentType() { - return body.contentType(); - } - - @Override public long contentLength() { - return -1; // We don't know the compressed length in advance! - } - - @Override public void writeTo(BufferedSink sink) throws IOException { - BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); - body.writeTo(gzipSink); - gzipSink.close(); - } - }; - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java deleted file mode 100644 index 5feb804566..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/InfoInterceptor.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import android.support.annotation.NonNull; -import android.util.Log; - -import java.io.IOException; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -// jamorham - -public class InfoInterceptor implements Interceptor { - - private String tag = "interceptor"; - - public InfoInterceptor(String tag) { - this.tag = tag; - } - - @Override - public Response intercept(@NonNull final Chain chain) throws IOException { - final Request request = chain.request(); - if (request != null && request.body() != null) { - Log.d(tag, "Interceptor Body size: " + request.body().contentLength()); - //} else { - // UserError.Log.d(tag,"Null request body in InfoInterceptor"); - } - return chain.proceed(request); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java deleted file mode 100644 index 77dc6e0739..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthReply.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -import com.eveningoutpost.dexdrip.Models.JoH; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -import java.util.List; - -import lombok.AllArgsConstructor; - -@AllArgsConstructor -public class MAuthReply { - - @Expose - @SerializedName("emailVerified") - Boolean emailVerified; - @Expose - @SerializedName("emails") - List emailList; - @Expose - @SerializedName("termsAccepted") - String termsDate; - @Expose - @SerializedName("userid") - String userid; - @Expose - @SerializedName("username") - String username; - - public String toS() { - return JoH.defaultGsonInstance().toJson(this); - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java deleted file mode 100644 index 6c5ae762c9..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MAuthRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.utils.SP; -import okhttp3.Credentials; - -import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; - -public class MAuthRequest extends BaseMessage { - - public static String getAuthRequestHeader() { - - final String username = SP.getString(R.string.key_tidepool_username, null); - final String password = SP.getString(R.string.key_tidepool_password, null); - - if (emptyString(username) || emptyString(password)) return null; - return Credentials.basic(username.trim(), password); - } -} - - - diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java deleted file mode 100644 index d12ec48060..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MCloseDatasetRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.google.gson.annotations.Expose; - -public class MCloseDatasetRequest extends BaseMessage { - @Expose - String dataState = "closed"; - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java deleted file mode 100644 index 9e4752ff1f..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MDatasetReply.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import java.util.List; - -public class MDatasetReply { - - Data data; - - public class Data { - String createdTime; - String deviceId; - String id; - String time; - String timezone; - int timezoneOffset; - String type; - String uploadId; - Client client; - String computerTime; - String dataSetType; - List deviceManufacturers; - String deviceModel; - String deviceSerialNumber; - List deviceTags; - String timeProcessing; - String version; - // meta - } - - public class Client { - String name; - String version; - - } - - - // openDataSet and others return this in the root of the json reply it seems - String id; - String uploadId; - - public String getUploadId() { - return (data != null && data.uploadId != null) ? data.uploadId : uploadId; - } - - - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java deleted file mode 100644 index 8a72992e5d..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MGetDatasetsRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -public class MGetDatasetsRequest extends BaseMessage { -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java deleted file mode 100644 index caf62794f5..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MOpenDatasetRequest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import com.google.gson.annotations.Expose; - -import java.util.TimeZone; - -import info.nightscout.androidaps.BuildConfig; -import info.nightscout.androidaps.interfaces.PluginBase; -import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; -import info.nightscout.androidaps.utils.T; - -import static com.eveningoutpost.dexdrip.Models.JoH.getTimeZoneOffsetMs; - -public class MOpenDatasetRequest extends BaseMessage { - - static final String UPLOAD_TYPE = "continuous"; - - @Expose - public String deviceId; - @Expose - public String time = DateUtil.toFormatAsUTC(info.nightscout.androidaps.utils.DateUtil.now()); - @Expose - public int timezoneOffset = (int) (getTimeZoneOffsetMs() / T.mins(1).msecs()); - @Expose - public String type = "upload"; - //public String byUser; - @Expose - public ClientInfo client = new ClientInfo(); - @Expose - public String computerTime = DateUtil.toFormatNoZone(info.nightscout.androidaps.utils.DateUtil.now()); - @Expose - public String dataSetType = UPLOAD_TYPE; // omit for "normal" - @Expose - public String[] deviceManufacturers = {((PluginBase) (ConfigBuilderPlugin.getPlugin().getActiveBgSource())).getName()}; - @Expose - public String deviceModel = ((PluginBase) (ConfigBuilderPlugin.getPlugin().getActiveBgSource())).getName(); - @Expose - public String[] deviceTags = {"bgm", "cgm", "insulin-pump"}; - @Expose - public Deduplicator deduplicator = new Deduplicator(); - @Expose - public String timeProcessing = "none"; - @Expose - public String timezone = TimeZone.getDefault().getID(); - @Expose - public String version = BuildConfig.VERSION_NAME; - - class ClientInfo { - @Expose - final String name = BuildConfig.APPLICATION_ID; - @Expose - final String version = "0.1.0"; // TODO: const it - } - - class Deduplicator { - @Expose - final String name = "org.tidepool.deduplicator.dataset.delete.origin"; - } - - static boolean isNormal() { - return UPLOAD_TYPE.equals("normal"); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java deleted file mode 100644 index bdcf164396..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/MUploadReply.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import java.util.List; - -public class MUploadReply { - - List data; - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java deleted file mode 100644 index 83bf20d036..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/Session.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -// Manages the session data - -import java.util.List; - -import okhttp3.Headers; - -public class Session { - - private final String SESSION_TOKEN_HEADER; - final TidepoolUploader.Tidepool service = TidepoolUploader.getRetrofitInstance().create(TidepoolUploader.Tidepool.class); - final String authHeader; - - String token; - MAuthReply authReply; - MDatasetReply datasetReply; - long start; - long end; - volatile int iterations; - - - Session(String authHeader, String session_token_header) { - this.authHeader = authHeader; - this.SESSION_TOKEN_HEADER = session_token_header; - } - - void populateHeaders(final Headers headers) { - if (this.token == null) { - this.token = headers.get(SESSION_TOKEN_HEADER); - } - } - - void populateBody(final Object obj) { - if (obj == null) return; - if (obj instanceof MAuthReply) { - authReply = (MAuthReply) obj; - } else if (obj instanceof List) { - List list = (List)obj; - if (list.size() > 0 && list.get(0) instanceof MDatasetReply) { - datasetReply = (MDatasetReply) list.get(0); - } - } else if (obj instanceof MDatasetReply) { - datasetReply = (MDatasetReply) obj; - } - } - - boolean exceededIterations() { - return iterations > 50; - } - -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java deleted file mode 100644 index d99afdc1fa..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolCallback.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -import android.util.Log; - -import com.eveningoutpost.dexdrip.store.FastStore; - -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -// jamorham - -// Callback template to reduce boiler plate - -class TidepoolCallback implements Callback { - - final Session session; - final String name; - final Runnable onSuccess; - - Runnable onFailure; - - public TidepoolCallback(Session session, String name, Runnable onSuccess) { - this.session = session; - this.name = name; - this.onSuccess = onSuccess; - } - - TidepoolCallback setOnFailure(final Runnable runnable) { - this.onFailure = runnable; - return this; - } - - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - Log.d(TidepoolUploader.TAG, name + " success"); - session.populateBody(response.body()); - session.populateHeaders(response.headers()); - if (onSuccess != null) { - onSuccess.run(); - } - } else { - final String msg = name + " was not successful: " + response.code() + " " + response.message(); - Log.e(TidepoolUploader.TAG, msg); - status(msg); - if (onFailure != null) { - onFailure.run(); - } - } - } - - @Override - public void onFailure(Call call, Throwable t) { - final String msg = name + " Failed: " + t; - Log.e(TidepoolUploader.TAG, msg); - status(msg); - if (onFailure != null) { - onFailure.run(); - } - } - - - private static void status(final String status) { - FastStore.getInstance().putS(TidepoolUploader.STATUS_KEY, status); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java deleted file mode 100644 index 26f7901cda..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolEntry.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -// lightweight class entry point - -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.receivers.ChargingStateReceiver; -import info.nightscout.androidaps.utils.SP; - -import static com.eveningoutpost.dexdrip.Models.JoH.isLANConnected; - -public class TidepoolEntry { - - - public static boolean enabled() { - return SP.getBoolean(R.string.key_cloud_storage_tidepool_enable, false); - } - - public static void newData() { - if (enabled() - && (!SP.getBoolean(R.string.key_tidepool_only_while_charging, false) || ChargingStateReceiver.isCharging()) - && (!SP.getBoolean(R.string.key_tidepool_only_while_unmetered, false) || isLANConnected()) - // && JoH.pratelimit("tidepool-new-data-upload", 1200) - ) { - TidepoolUploader.doLogin(false); - } - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java deleted file mode 100644 index 82e76c4d1f..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolStatus.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - -// jamorham - -import com.eveningoutpost.dexdrip.Models.JoH; -import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; -import com.eveningoutpost.dexdrip.store.FastStore; -import com.eveningoutpost.dexdrip.store.KeyStore; - -import java.util.ArrayList; -import java.util.List; - -import static com.eveningoutpost.dexdrip.Models.JoH.msSince; -import static com.eveningoutpost.dexdrip.Models.JoH.niceTimeScalar; - -public class TidepoolStatus { - - // data for MegaStatus - public static List megaStatus() { - - final KeyStore keyStore = FastStore.getInstance(); - final List l = new ArrayList<>(); - - l.add(new StatusItem("Tidepool Synced to", niceTimeScalar(msSince(UploadChunk.getLastEnd())) + " ago")); // TODO needs generic message format string - final String status = keyStore.getS(TidepoolUploader.STATUS_KEY); - if (!JoH.emptyString(status)) { - l.add(new StatusItem("Tidepool Status", status)); - } - return l; - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java b/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java deleted file mode 100644 index 61b734bc3b..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/UploadChunk.java +++ /dev/null @@ -1,189 +0,0 @@ -package com.eveningoutpost.dexdrip.tidepool; - - -import android.util.Log; - -import com.eveningoutpost.dexdrip.Models.JoH; -import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; -import com.eveningoutpost.dexdrip.utils.LogSlider; -import com.eveningoutpost.dexdrip.utils.NamedSliderProcessor; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.UUID; - -import info.nightscout.androidaps.MainApp; -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.db.BgReading; -import info.nightscout.androidaps.db.TemporaryBasal; -import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; -import info.nightscout.androidaps.plugins.treatments.Treatment; -import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; -import info.nightscout.androidaps.utils.DateUtil; -import info.nightscout.androidaps.utils.SP; -import info.nightscout.androidaps.utils.T; - -import static com.eveningoutpost.dexdrip.Models.JoH.dateTimeText; - -/** - * jamorham - *

- * This class gets the next time slice of all data to upload - */ - -public class UploadChunk implements NamedSliderProcessor { - - private static final String TAG = "TidepoolUploadChunk"; - private static final String LAST_UPLOAD_END_PREF = "tidepool-last-end"; - - private static final long MAX_UPLOAD_SIZE = T.days(7).msecs(); // don't change this - private static final long DEFAULT_WINDOW_OFFSET = T.mins(15).msecs(); - private static final long MAX_LATENCY_THRESHOLD_MINUTES = 1440; // minutes per day - - - public static String getNext(final Session session) { - session.start = getLastEnd(); - session.end = maxWindow(session.start); - - final String result = get(session.start, session.end); - if (result != null && result.length() < 3) { - Log.d(TAG, "No records in this time period, setting start to best end time"); - setLastEnd(Math.max(session.end, getOldestRecordTimeStamp())); - } - return result; - } - - public static String get(final long start, final long end) { - - Log.e(TAG, "Syncing data between: " + dateTimeText(start) + " -> " + dateTimeText(end)); - if (end <= start) { - Log.e(TAG, "End is <= start: " + dateTimeText(start) + " " + dateTimeText(end)); - return null; - } - if (end - start > MAX_UPLOAD_SIZE) { - Log.e(TAG, "More than max range - rejecting"); - return null; - } - - final List records = new LinkedList<>(); - - records.addAll(getTreatments(start, end)); - records.addAll(getBloodTests(start, end)); - records.addAll(getBasals(start, end)); - records.addAll(getBgReadings(start, end)); - - return JoH.defaultGsonInstance().toJson(records); - } - - private static long getWindowSizePreference() { - try { - long value = (long) getLatencySliderValue(SP.getInt(R.string.key_tidepool_window_latency, 0)); - return Math.max(T.mins(value).msecs(), DEFAULT_WINDOW_OFFSET); - } catch (Exception e) { - Log.e(TAG, "Reverting to default of 15 minutes due to Window Size exception: " + e); - return DEFAULT_WINDOW_OFFSET; // default - } - } - - private static long maxWindow(final long last_end) { - //Log.d(TAG, "Max window is: " + getWindowSizePreference()); - return Math.min(last_end + MAX_UPLOAD_SIZE, DateUtil.now() - getWindowSizePreference()); - } - - public static long getLastEnd() { - long result = PersistentStore.getLong(LAST_UPLOAD_END_PREF); - return Math.max(result, DateUtil.now() - T.months(2).msecs()); - } - - public static void setLastEnd(final long when) { - if (when > getLastEnd()) { - PersistentStore.setLong(LAST_UPLOAD_END_PREF, when); - Log.d(TAG, "Updating last end to: " + dateTimeText(when)); - } else { - Log.e(TAG, "Cannot set last end to: " + dateTimeText(when) + " vs " + dateTimeText(getLastEnd())); - } - } - - static List getTreatments(final long start, final long end) { - List result = new LinkedList<>(); - final List treatments = TreatmentsPlugin.getPlugin().getService().getTreatmentDataFromTime(start, end, true); - for (Treatment treatment : treatments) { - if (treatment.carbs > 0) { - result.add(EWizard.fromTreatment(treatment)); - } else if (treatment.insulin > 0) { - result.add(EBolus.fromTreatment(treatment)); - } else { - // note only TODO - } - } - return result; - } - - - // numeric limits must match max time windows - - static long getOldestRecordTimeStamp() { - // TODO we could make sure we include records older than the first bg record for completeness - - final long start = 0; - final long end = DateUtil.now(); - - final List bgReadingList = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, false); - if (bgReadingList != null && bgReadingList.size() > 0) { - return bgReadingList.get(0).date; - } - return -1; - } - - static List getBloodTests(final long start, final long end) { - return new ArrayList<>(); -// return EBloodGlucose.fromBloodTests(BloodTest.latestForGraph(1800, start, end)); - } - - static List getBgReadings(final long start, final long end) { - return ESensorGlucose.fromBgReadings(MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true)); - } - - static List getBasals(final long start, final long end) { - final List basals = new LinkedList<>(); - final List aplist = MainApp.getDbHelper().getTemporaryBasalsDataFromTime(start, end, true); - EBasal current = null; - for (TemporaryBasal temporaryBasal : aplist) { - final double this_rate = temporaryBasal.tempBasalConvertedToAbsolute(temporaryBasal.date, ProfileFunctions.getInstance().getProfile(temporaryBasal.date)); - - if (current != null) { - if (this_rate != current.rate) { - current.duration = temporaryBasal.date - current.timestamp; - Log.d(TAG, "Adding current: " + current.toS()); - if (current.isValid()) { - basals.add(current); - } else { - Log.e(TAG, "Current basal is invalid: " + current.toS()); - } - current = null; - } else { - Log.d(TAG, "Same rate as previous basal record: " + current.rate + " " + temporaryBasal.toStringFull()); - } - } - if (current == null) { - current = new EBasal(this_rate, temporaryBasal.date, 0, UUID.nameUUIDFromBytes(("tidepool-basal" + temporaryBasal.date).getBytes()).toString()); // start duration is 0 - } - } - return basals; - - } - - @Override - public int interpolate(final String name, final int position) { - switch (name) { - case "latency": - return getLatencySliderValue(position); - } - throw new RuntimeException("name not matched in interpolate"); - } - - private static int getLatencySliderValue(final int position) { - return (int) LogSlider.calc(0, 300, 15, MAX_LATENCY_THRESHOLD_MINUTES, position); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java deleted file mode 100644 index 014d085ae4..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/utils/LogSlider.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eveningoutpost.dexdrip.utils; - -// jamorham - -public class LogSlider { - - // logarithmic slider with positions start - end representing values start - end, calculate value at selected position - public static double calc(int sliderStart, int sliderEnd, double valueStart, double valueEnd, int position) { - valueStart = Math.log(Math.max(1, valueStart)); - valueEnd = Math.log(Math.max(1, valueEnd)); - return Math.exp(valueStart + (valueEnd - valueStart) / (sliderEnd - sliderStart) * (position - sliderStart)); - } -} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java deleted file mode 100644 index a13e4b05b1..0000000000 --- a/app/src/main/java/com/eveningoutpost/dexdrip/utils/NamedSliderProcessor.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.eveningoutpost.dexdrip.utils; - -// jamorham - -// interpolate a slider by name from a supporting class - -public interface NamedSliderProcessor { - - int interpolate(String name, int position); - -} diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index 7f1b527d00..9acd222b55 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -205,7 +205,7 @@ public class MainApp extends Application { pluginsList.add(StatuslinePlugin.initPlugin(this)); pluginsList.add(PersistentNotificationPlugin.getPlugin()); pluginsList.add(NSClientPlugin.getPlugin()); - pluginsList.add(TidepoolPlugin.getPlugin()); + pluginsList.add(TidepoolPlugin.INSTANCE); pluginsList.add(MaintenancePlugin.initPlugin(this)); pluginsList.add(ConfigBuilderPlugin.getPlugin()); diff --git a/app/src/main/java/info/nightscout/androidaps/logging/L.java b/app/src/main/java/info/nightscout/androidaps/logging/L.java index f685bbd424..362e03a0c7 100644 --- a/app/src/main/java/info/nightscout/androidaps/logging/L.java +++ b/app/src/main/java/info/nightscout/androidaps/logging/L.java @@ -87,6 +87,7 @@ public class L { public static final String DATAFOOD = "DATAFOOD"; public static final String DATATREATMENTS = "DATATREATMENTS"; public static final String NSCLIENT = "NSCLIENT"; + public static final String TIDEPOOL = "TIDEPOOL"; public static final String CONSTRAINTS = "CONSTRAINTS"; public static final String PUMP = "PUMP"; public static final String PUMPQUEUE = "PUMPQUEUE"; @@ -114,6 +115,7 @@ public class L { logElements.add(new LogElement(EVENTS, false, true)); logElements.add(new LogElement(NOTIFICATION, true)); logElements.add(new LogElement(NSCLIENT, true)); + logElements.add(new LogElement(TIDEPOOL, true)); logElements.add(new LogElement(OVERVIEW, true)); logElements.add(new LogElement(PROFILE, true)); logElements.add(new LogElement(PUMP, true)); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java deleted file mode 100644 index 580b602deb..0000000000 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.java +++ /dev/null @@ -1,38 +0,0 @@ -package info.nightscout.androidaps.plugins.general.tidepool; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.interfaces.PluginBase; -import info.nightscout.androidaps.interfaces.PluginDescription; -import info.nightscout.androidaps.interfaces.PluginType; -import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.source.BGSourceFragment; - -/** - * Created by mike on 28.11.2017. - */ - -public class TidepoolPlugin extends PluginBase { - private static Logger log = LoggerFactory.getLogger(L.DATABASE); - - private static TidepoolPlugin plugin = null; - - public static TidepoolPlugin getPlugin() { - if (plugin == null) - plugin = new TidepoolPlugin(); - return plugin; - } - - private TidepoolPlugin() { - super(new PluginDescription() - .mainType(PluginType.GENERAL) - .pluginName(R.string.tidepool) - .shortName(R.string.tidepool_shortname) - .preferencesId(R.xml.pref_tidepool) - .description(R.string.description_tidepool) - ); - } - -} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt new file mode 100644 index 0000000000..724c35b606 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt @@ -0,0 +1,57 @@ +package info.nightscout.androidaps.plugins.general.tidepool + +import com.squareup.otto.Subscribe +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.events.EventNetworkChange +import info.nightscout.androidaps.events.EventNewBG +import info.nightscout.androidaps.interfaces.PluginBase +import info.nightscout.androidaps.interfaces.PluginDescription +import info.nightscout.androidaps.interfaces.PluginType +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import info.nightscout.androidaps.plugins.general.tidepool.utils.RateLimit +import info.nightscout.androidaps.receivers.ChargingStateReceiver +import info.nightscout.androidaps.utils.SP +import org.slf4j.LoggerFactory + +object TidepoolPlugin : PluginBase(PluginDescription() + .mainType(PluginType.GENERAL) + .pluginName(R.string.tidepool) + .shortName(R.string.tidepool_shortname) + .preferencesId(R.xml.pref_tidepool) + .description(R.string.description_tidepool) +) { + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + private var wifiConnected = false + + override fun onStart() { + MainApp.bus().register(this) + super.onStart() + } + + override fun onStop() { + MainApp.bus().unregister(this) + super.onStop() + } + + @Suppress("UNUSED_PARAMETER") + @Subscribe + fun onStatusEvent(ev: EventNewBG) { + if (enabled() + && (!SP.getBoolean(R.string.key_tidepool_only_while_charging, false) || ChargingStateReceiver.isCharging()) + && (!SP.getBoolean(R.string.key_tidepool_only_while_unmetered, false) || wifiConnected) + && RateLimit.ratelimit("tidepool-new-data-upload", 1200)) + TidepoolUploader.doLogin() + } + + @Subscribe + fun onEventNetworkChange(ev: EventNetworkChange) { + wifiConnected = ev.wifiConnected + } + + fun enabled(): Boolean { + return isEnabled(PluginType.GENERAL) && SP.getBoolean(R.string.key_cloud_storage_tidepool_enable, false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolUploaded.java.txt similarity index 77% rename from app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java rename to app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolUploaded.java.txt index 52b4ef9200..9cef0e726f 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/tidepool/TidepoolUploader.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolUploaded.java.txt @@ -3,14 +3,18 @@ package com.eveningoutpost.dexdrip.tidepool; import android.os.PowerManager; import android.util.Log; -import com.eveningoutpost.dexdrip.Models.JoH; -import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; -import com.eveningoutpost.dexdrip.store.FastStore; - import java.util.List; import info.nightscout.androidaps.BuildConfig; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.tidepool.comm.InfoInterceptor; +import info.nightscout.androidaps.plugins.general.tidepool.comm.Session; +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolCallback; +import info.nightscout.androidaps.plugins.general.tidepool.messages.AuthReplyMessage; +import info.nightscout.androidaps.plugins.general.tidepool.messages.AuthRequestMessage; +import info.nightscout.androidaps.plugins.general.tidepool.messages.DatasetReplyMessage; +import info.nightscout.androidaps.plugins.general.tidepool.messages.OpenDatasetRequestMessage; +import info.nightscout.androidaps.plugins.general.tidepool.messages.UploadReplyMessage; import info.nightscout.androidaps.utils.SP; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -129,26 +133,26 @@ public class TidepoolUploader { } // TODO failure backoff // if (JoH.ratelimit("tidepool-login", 10)) { - extendWakeLock(30000); - final Session session = new Session(MAuthRequest.getAuthRequestHeader(), SESSION_TOKEN_HEADER); - if (session.authHeader != null) { - final Call call = session.service.getLogin(session.authHeader); - status("Connecting"); - if (fromUi) { + extendWakeLock(30000); + final Session session = new Session(MAuthRequest.getAuthRequestHeader(), SESSION_TOKEN_HEADER); + if (session.authHeader() != null) { + final Call call = session.service().getLogin(session.authHeader()); + status("Connecting"); + if (fromUi) { // JoH.static_toast_long("Connecting to Tidepool"); - } - - call.enqueue(new TidepoolCallback(session, "Login", () -> startSession(session, fromUi)) - .setOnFailure(() -> loginFailed(fromUi))); - } else { - Log.e(TAG, "Cannot do login as user credentials have not been set correctly"); - status("Invalid credentials"); - if (fromUi) { -// JoH.static_toast_long("Cannot login as Tidepool credentials have not been set correctly"); - } - releaseWakeLock(); } - // } + + call.enqueue(new TidepoolCallback(session, "Login", () -> startSession(session, fromUi)) + .setOnFailure(() -> loginFailed(fromUi))); + } else { + Log.e(TAG, "Cannot do login as user credentials have not been set correctly"); + status("Invalid credentials"); + if (fromUi) { +// JoH.static_toast_long("Cannot login as Tidepool credentials have not been set correctly"); + } + releaseWakeLock(); + } + // } } private static void loginFailed(boolean fromUi) { @@ -158,7 +162,7 @@ public class TidepoolUploader { releaseWakeLock(); } -/* public static void testLogin(Context rootContext) { + public static void testLogin(Context rootContext) { if (JoH.ratelimit("tidepool-login", 1)) { String message = "Failed to log into Tidepool.\n" + @@ -176,7 +180,7 @@ public class TidepoolUploader { e.printStackTrace(); } } else { - UserError.Log.e(TAG,"Cannot do login as user credentials have not been set correctly"); + UserError.Log.e(TAG, "Cannot do login as user credentials have not been set correctly"); } AlertDialog.Builder builder = new AlertDialog.Builder(rootContext); @@ -192,45 +196,45 @@ public class TidepoolUploader { final AlertDialog alert = builder.create(); alert.show(); } - }*/ + } private static void startSession(final Session session, boolean fromUi) { // if (JoH.ratelimit("tidepool-start-session", 60)) { - extendWakeLock(30000); - if (session.authReply.userid != null) { - // See if we already have an open data set to write to - Call> datasetCall = session.service.getOpenDataSets(session.token, - session.authReply.userid, BuildConfig.APPLICATION_ID, 1); + extendWakeLock(30000); + if (session.authReply.userid != null) { + // See if we already have an open data set to write to + Call> datasetCall = session.service.getOpenDataSets(session.token, + session.authReply.userid, BuildConfig.APPLICATION_ID, 1); - datasetCall.enqueue(new TidepoolCallback>(session, "Get Open Datasets", () -> { - if (session.datasetReply == null) { - status("New data set"); - if (fromUi) { + datasetCall.enqueue(new TidepoolCallback>(session, "Get Open Datasets", () -> { + if (session.datasetReply == null) { + status("New data set"); + if (fromUi) { // JoH.static_toast_long("Creating new data set"); - } - Call call = session.service.openDataSet(session.token, session.authReply.userid, new MOpenDatasetRequest().getBody()); - call.enqueue(new TidepoolCallback(session, "Open New Dataset", () -> doUpload(session)) - .setOnFailure(TidepoolUploader::releaseWakeLock)); - } else { - Log.d(TAG, "Existing Dataset: " + session.datasetReply.getUploadId()); - // TODO: Wouldn't need to do this if we could block on the above `call.enqueue`. - // ie, do the openDataSet conditionally, and then do `doUpload` either way. - status("Appending"); - if (fromUi) { -// JoH.static_toast_long("Found existing remote data set"); - } - doUpload(session); } - }).setOnFailure(TidepoolUploader::releaseWakeLock)); - } else { - Log.wtf(TAG, "Got login response but cannot determine userid - cannot proceed"); - if (fromUi) { -// JoH.static_toast_long("Error: Cannot determine userid"); + Call call = session.service.openDataSet(session.token, session.authReply.userid, new MOpenDatasetRequest().getBody()); + call.enqueue(new TidepoolCallback(session, "Open New Dataset", () -> doUpload(session)) + .setOnFailure(TidepoolUploader::releaseWakeLock)); + } else { + Log.d(TAG, "Existing Dataset: " + session.datasetReply.getUploadId()); + // TODO: Wouldn't need to do this if we could block on the above `call.enqueue`. + // ie, do the openDataSet conditionally, and then do `doUpload` either way. + status("Appending"); + if (fromUi) { +// JoH.static_toast_long("Found existing remote data set"); + } + doUpload(session); } - status("Error userid"); - releaseWakeLock(); + }).setOnFailure(TidepoolUploader::releaseWakeLock)); + } else { + Log.wtf(TAG, "Got login response but cannot determine userid - cannot proceed"); + if (fromUi) { +// JoH.static_toast_long("Error: Cannot determine userid"); } + status("Error userid"); + releaseWakeLock(); + } // } else { // status("Cool Down Wait"); // if (fromUi) { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt new file mode 100644 index 0000000000..0cd7448153 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.logging.L +import okhttp3.Interceptor +import okhttp3.Response +import org.slf4j.LoggerFactory +import java.io.IOException + +class InfoInterceptor(tag: String) : Interceptor { + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + private var tag = "interceptor" + + init { + this.tag = tag + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + if (request != null && request.body() != null) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Interceptor Body size: " + request.body()!!.contentLength()) + } + return chain.proceed(request!!) + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt new file mode 100644 index 0000000000..4b888bf765 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt @@ -0,0 +1,58 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.plugins.general.tidepool.messages.AuthReplyMessage +import info.nightscout.androidaps.plugins.general.tidepool.messages.DatasetReplyMessage +import okhttp3.Headers + +class Session (authHeader: String?, session_token_header: String) { + var SESSION_TOKEN_HEADER: String + var authHeader: String? + + val service = TidepoolUploader.getRetrofitInstance()?.create(TidepoolApiService::class.java) + + internal var token: String? = null + internal var authReply: AuthReplyMessage? = null + internal var datasetReply: DatasetReplyMessage? = null + internal var start: Long = 0 + internal var end: Long = 0 + @Volatile + internal var iterations: Int = 0 + + + init { + this.authHeader = authHeader + this.SESSION_TOKEN_HEADER = session_token_header + } + + fun populateHeaders(headers: Headers) { + if (this.token == null) { + this.token = headers.get(SESSION_TOKEN_HEADER) + } + } + + fun populateBody(obj: Any?) { + if (obj == null) return + if (obj is AuthReplyMessage) { + authReply = obj + } else if (obj is List<*>) { + val list = obj as List<*>? + if (list!!.size > 0 && list[0] is DatasetReplyMessage) { + datasetReply = list[0] as DatasetReplyMessage + } + } else if (obj is DatasetReplyMessage) { + datasetReply = obj + } + } + + internal fun exceededIterations(): Boolean { + return iterations > 50 + } + + fun authHeader(): String? { + return authHeader; + } + + fun service(): TidepoolApiService? { + return service; + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt new file mode 100644 index 0000000000..45f4d92243 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.plugins.general.tidepool.messages.AuthReplyMessage +import info.nightscout.androidaps.plugins.general.tidepool.messages.DatasetReplyMessage +import info.nightscout.androidaps.plugins.general.tidepool.messages.UploadReplyMessage +import okhttp3.RequestBody +import retrofit2.Call +import retrofit2.http.* + +const val SESSION_TOKEN_HEADER: String = "x-tidepool-session-token" + +interface TidepoolApiService { + + @Headers( + "User-Agent: AAPS- " + BuildConfig.VERSION_NAME, + "X-Tidepool-Client-Name: info.nightscout.androidaps" + BuildConfig.APPLICATION_ID, + "X-Tidepool-Client-Version: 0.1.0" + ) + + @POST("/auth/login") + abstract fun getLogin(@Header("Authorization") secret: String): Call + + @DELETE("/v1/users/{userId}/data") + abstract fun deleteAllData(@Header(SESSION_TOKEN_HEADER) token: String, @Path("userId") id: String): Call + + @DELETE("/v1/datasets/{dataSetId}") + abstract fun deleteDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("dataSetId") id: String): Call + + @GET("/v1/users/{userId}/data_sets") + abstract fun getOpenDataSets(@Header(SESSION_TOKEN_HEADER) token: String, + @Path("userId") id: String, + @Query("client.name") clientName: String, + @Query("size") size: Int): Call> + + @GET("/v1/datasets/{dataSetId}") + abstract fun getDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("dataSetId") id: String): Call + + @POST("/v1/users/{userId}/data_sets") + abstract fun openDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("userId") id: String, @Body body: RequestBody): Call + + @POST("/v1/datasets/{sessionId}/data") + abstract fun doUpload(@Header(SESSION_TOKEN_HEADER) token: String, @Path("sessionId") id: String, @Body body: RequestBody): Call + + @PUT("/v1/datasets/{sessionId}") + abstract fun closeDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("sessionId") id: String, @Body body: RequestBody): Call + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt new file mode 100644 index 0000000000..89e9b49c71 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolStatus +import org.slf4j.LoggerFactory +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +internal class TidepoolCallback(val session: Session, val name: String, val onSucc: () -> Unit, val onFail: () -> Unit) : Callback { + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful && response.body() != null) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("$name success") + session.populateBody(response.body()) + session.populateHeaders(response.headers()) + onSucc() + } else { + val msg = name + " was not successful: " + response.code() + " " + response.message() + if (L.isEnabled(L.TIDEPOOL)) log.debug(msg) + status(msg) + onFail() + } + } + + override fun onFailure(call: Call, t: Throwable) { + val msg = "$name Failed: $t" + if (L.isEnabled(L.TIDEPOOL)) log.debug(msg) + status(msg) + onFail() + } + + private fun status(status: String) { + MainApp.bus().post(EventTidepoolStatus(status)) + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt new file mode 100644 index 0000000000..7c0939e53d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt @@ -0,0 +1,232 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import android.content.Context +import android.os.PowerManager +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolStatus +import info.nightscout.androidaps.plugins.general.tidepool.messages.* +import info.nightscout.androidaps.utils.OKDialog +import info.nightscout.androidaps.utils.SP +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.logging.HttpLoggingInterceptor +import org.slf4j.LoggerFactory +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object TidepoolUploader { + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + private var wl: PowerManager.WakeLock? = null + + + private const val INTEGRATION_BASE_URL = "https://int-api.tidepool.org" + private const val PRODUCTION_BASE_URL = "https://api.tidepool.org" + + private var retrofit: Retrofit? = null + + fun getRetrofitInstance(): Retrofit? { + if (retrofit == null) { + + val httpLoggingInterceptor = HttpLoggingInterceptor() + httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + + val client = OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(InfoInterceptor(TidepoolUploader::class.java.name)) + .build() + + retrofit = Retrofit.Builder() + .baseUrl(if (SP.getBoolean(R.string.key_tidepool_dev_servers, false)) INTEGRATION_BASE_URL else PRODUCTION_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + return retrofit + } + + // TODO: call on preference change + fun resetInstance() { + retrofit = null + if (L.isEnabled(L.TIDEPOOL)) + log.debug("Instance reset") + } + + @Synchronized + fun doLogin() { + if (!SP.getBoolean(R.string.key_cloud_storage_tidepool_enable, false)) { + log.debug("Cannot login as disabled by preference") + return + } + // TODO failure backoff + extendWakeLock(30000) + val session = Session(AuthRequestMessage.getAuthRequestHeader(), SESSION_TOKEN_HEADER) + if (session.authHeader != null) { + val call = session.service?.getLogin(session.authHeader!!) + status("Connecting") + + call?.enqueue(TidepoolCallback(session, "Login", { startSession(session) }, { loginFailed() })) + } else { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot do login as user credentials have not been set correctly") + status("Invalid credentials") + releaseWakeLock() + } + } + + fun testLogin(rootContext: Context) { + + var message = "Failed to log into Tidepool.\n" + "Check that your user name and password are correct." + + val session = Session(AuthRequestMessage.getAuthRequestHeader(), SESSION_TOKEN_HEADER) + if (session.authHeader != null) { + val call = session.service!!.getLogin(session.authHeader!!) + + val response = call.execute() + if (L.isEnabled(L.TIDEPOOL)) log.debug("Header: " + response.code()) + message = "Successfully logged into Tidepool." + + } else { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot do login as user credentials have not been set correctly") + } + + OKDialog.show(rootContext, MainApp.gs(R.string.tidepool), message, null); + } + + + private fun loginFailed() { + releaseWakeLock() + } + + private fun startSession(session: Session) { + extendWakeLock(30000) + if (session.authReply?.userid != null) { + // See if we already have an open data set to write to + val datasetCall = session.service!!.getOpenDataSets(session.token!!, + session.authReply!!.userid!!, BuildConfig.APPLICATION_ID, 1) + + datasetCall.enqueue(TidepoolCallback>(session, "Get Open Datasets", { + if (session.datasetReply == null) { + status("New data set") + val call = session.service.openDataSet(session.token!!, session.authReply!!.userid!!, OpenDatasetRequestMessage().getBody()) + call.enqueue(TidepoolCallback(session, "Open New Dataset", { doUpload(session) }, { releaseWakeLock() })) + } else { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Existing Dataset: " + session.datasetReply!!.getUploadId()) + // TODO: Wouldn't need to do this if we could block on the above `call.enqueue`. + // ie, do the openDataSet conditionally, and then do `doUpload` either way. + status("Appending") + doUpload(session) + } + }, { releaseWakeLock() })) + } else { + log.error("Got login response but cannot determine userid - cannot proceed") + status("Error userid") + releaseWakeLock() + } + } + + private fun doUpload(session: Session) { + if (!TidepoolPlugin.enabled()) { + if (L.isEnabled(L.TIDEPOOL)) + log.debug("Cannot upload - preference disabled") + return + } + extendWakeLock(60000) + session.iterations++ + val chunk = UploadChunk.getNext(session) + if (chunk != null) { + if (chunk.length == 2) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Empty data set - marking as succeeded") + doCompleted() + } else { + val body = RequestBody.create(MediaType.parse("application/json"), chunk) + + val call = session.service!!.doUpload(session.token!!, session.datasetReply!!.getUploadId()!!, body) + status("Uploading") + call.enqueue(TidepoolCallback(session, "Data Upload", { + UploadChunk.setLastEnd(session.end) + if (OpenDatasetRequestMessage.isNormal()) { + doClose(session) + } else { + doCompleted() + } + }, { releaseWakeLock() })) + } + } else { + log.error("Upload chunk is null, cannot proceed") + releaseWakeLock() + } + } + + private fun status(status: String) { + MainApp.bus().post(EventTidepoolStatus(status)) + } + + private fun doCompleted() { + status("Completed OK") + if (L.isEnabled(L.TIDEPOOL)) log.debug("ALL COMPLETED OK!") + releaseWakeLock() + } + + private fun doClose(session: Session) { + status("Closing") + extendWakeLock(20000) + val call = session.service!!.closeDataSet(session.token!!, session.datasetReply!!.getUploadId()!!, CloseDatasetRequestMessage().getBody()) + call.enqueue(TidepoolCallback(session, "Session Stop", { closeSuccess() }, {})) + } + + private fun closeSuccess() { + status("Closed") + if (L.isEnabled(L.TIDEPOOL)) log.debug("Close success") + releaseWakeLock() + } + + private fun deleteData(session: Session) { + if (session.authReply!!.userid != null) { + val call = session.service!!.deleteAllData(session.token!!, session.authReply!!.userid!!) + call.enqueue(TidepoolCallback(session, "Delete Data", {}, {})) + } else { + log.error("Got login response but cannot determine userid - cannot proceed") + } + } + + private fun getDataSet(session: Session) { + val call = session.service!!.getDataSet(session.token!!, "bogus") + call.enqueue(TidepoolCallback(session, "Get Data", {}, {})) + } + + private fun deleteDataSet(session: Session) { + val call = session.service!!.deleteDataSet(session.token!!, "bogus") + call.enqueue(TidepoolCallback(session, "Delete Data", {}, {})) + } + + @Synchronized + private fun extendWakeLock(ms: Long) { + if (wl == null) { + val pm = MainApp.instance().getSystemService(Context.POWER_SERVICE) as PowerManager + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AndroidAPS:TidepoolUploader") + wl?.acquire(ms) + } else { + releaseWakeLock() // lets not get too messy + wl?.acquire(ms) + } + } + + @Synchronized + private fun releaseWakeLock() { + if (wl == null) return + if (wl!!.isHeld()) { + try { + wl!!.release() + } catch (e: Exception) { + log.error("Error releasing wakelock: $e") + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt new file mode 100644 index 0000000000..0fa1da3ecc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt @@ -0,0 +1,169 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import android.util.Log +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions +import info.nightscout.androidaps.plugins.general.tidepool.elements.* +import info.nightscout.androidaps.plugins.general.tidepool.utils.GsonInstance +import info.nightscout.androidaps.plugins.general.tidepool.utils.LogSlider +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.SP +import info.nightscout.androidaps.utils.T +import java.util.* + +object UploadChunk { + + private val TAG = "TidepoolUploadChunk" + + private val MAX_UPLOAD_SIZE = T.days(7).msecs() // don't change this + private val DEFAULT_WINDOW_OFFSET = T.mins(15).msecs() + private val MAX_LATENCY_THRESHOLD_MINUTES: Long = 1440 // minutes per day + + + fun getNext(session: Session): String? { + session.start = getLastEnd() + session.end = maxWindow(session.start) + + val result = get(session.start, session.end) + if (result != null && result.length < 3) { + Log.d(TAG, "No records in this time period, setting start to best end time") + setLastEnd(Math.max(session.end, getOldestRecordTimeStamp())) + } + return result + } + + operator fun get(start: Long, end: Long): String? { + + Log.e(TAG, "Syncing data between: " + DateUtil.dateAndTimeFullString(start) + " -> " + DateUtil.dateAndTimeFullString(end)) + if (end <= start) { + Log.e(TAG, "End is <= start: " + DateUtil.dateAndTimeFullString(start) + " " + DateUtil.dateAndTimeFullString(end)) + return null + } + if (end - start > MAX_UPLOAD_SIZE) { + Log.e(TAG, "More than max range - rejecting") + return null + } + + val records = LinkedList() + + records.addAll(getTreatments(start, end)) + records.addAll(getBloodTests(start, end)) + records.addAll(getBasals(start, end)) + records.addAll(getBgReadings(start, end)) + + return GsonInstance.defaultGsonInstance().toJson(records) + } + + private fun getWindowSizePreference(): Long { + try { + val value = getLatencySliderValue(SP.getInt(R.string.key_tidepool_window_latency, 0)).toLong() + return Math.max(T.mins(value).msecs(), DEFAULT_WINDOW_OFFSET) + } catch (e: Exception) { + Log.e(TAG, "Reverting to default of 15 minutes due to Window Size exception: $e") + return DEFAULT_WINDOW_OFFSET // default + } + + } + + private fun maxWindow(last_end: Long): Long { + //Log.d(TAG, "Max window is: " + getWindowSizePreference()); + return Math.min(last_end + MAX_UPLOAD_SIZE, DateUtil.now() - getWindowSizePreference()) + } + + fun getLastEnd(): Long { + val result = SP.getLong(R.string.key_tidepool_last_end, 0) + return Math.max(result, DateUtil.now() - T.months(2).msecs()) + } + + fun setLastEnd(time: Long) { + if (time > getLastEnd()) { + SP.putLong(R.string.key_tidepool_last_end, time) + Log.d(TAG, "Updating last end to: " + DateUtil.dateAndTimeFullString(time)) + } else { + Log.e(TAG, "Cannot set last end to: " + DateUtil.dateAndTimeFullString(time) + " vs " + DateUtil.dateAndTimeFullString(getLastEnd())) + } + } + + internal fun getTreatments(start: Long, end: Long): List { + val result = LinkedList() + val treatments = TreatmentsPlugin.getPlugin().service.getTreatmentDataFromTime(start, end, true) + for (treatment in treatments) { + if (treatment.carbs > 0) { + result.add(WizardElement.fromTreatment(treatment)) + } else if (treatment.insulin > 0) { + result.add(BolusElement.fromTreatment(treatment)) + } else { + // note only TODO + } + } + return result + } + + + // numeric limits must match max time windows + + internal fun getOldestRecordTimeStamp(): Long { + // TODO we could make sure we include records older than the first bg record for completeness + + val start: Long = 0 + val end = DateUtil.now() + + val bgReadingList = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, false) + return if (bgReadingList != null && bgReadingList.size > 0) { + bgReadingList[0].date + } else -1 + } + + @Suppress("UNUSED_PARAMETER") + internal fun getBloodTests(start: Long, end: Long): List { + return ArrayList() + // return BloodGlucoseElement.fromBloodTests(BloodTest.latestForGraph(1800, start, end)); + } + + internal fun getBgReadings(start: Long, end: Long): List { + return SensorGlucoseElement.fromBgReadings(MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true)) + } + + internal fun getBasals(start: Long, end: Long): List { + val basals = LinkedList() + val aplist = MainApp.getDbHelper().getTemporaryBasalsDataFromTime(start, end, true) + var current: BasalElement? = null + for (temporaryBasal in aplist) { + val this_rate = temporaryBasal.tempBasalConvertedToAbsolute(temporaryBasal.date, ProfileFunctions.getInstance().getProfile(temporaryBasal.date)) + + if (current != null) { + if (this_rate != current.rate) { + current.duration = temporaryBasal.date - current.timestamp + Log.d(TAG, "Adding current: " + current.toS()) + if (current.isValid()) { + basals.add(current) + } else { + Log.e(TAG, "Current basal is invalid: " + current.toS()) + } + current = null + } else { + Log.d(TAG, "Same rate as previous basal record: " + current.rate + " " + temporaryBasal.toStringFull()) + } + } + if (current == null) { + current = BasalElement().create(this_rate, temporaryBasal.date, 0, UUID.nameUUIDFromBytes(("tidepool-basal" + temporaryBasal.date).toByteArray()).toString()) // start duration is 0 + } + } + return basals + + } + + fun interpolate(name: String, position: Int): Int { + when (name) { + "latency" -> return getLatencySliderValue(position) + } + throw RuntimeException("name not matched in interpolate") + } + + private fun getLatencySliderValue(position: Int): Int { + return LogSlider.calc(0, 300, 15.0, MAX_LATENCY_THRESHOLD_MINUTES.toDouble(), position).toInt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt new file mode 100644 index 0000000000..65f28e6c51 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.utils.DateUtil + +class BasalElement : BaseElement() { + + internal var timestamp: Long = 0 // not exposed + + @Expose + internal var deliveryType = "automated" + @Expose + internal var duration: Long = 0 + @Expose + internal var rate = -1.0 + @Expose + internal var scheduleName = "AAPS" + @Expose + internal var clockDriftOffset: Long = 0 + @Expose + internal var conversionOffset: Long = 0 + + init { + type = "basal"; + } + + fun create(rate: Double, timeStart: Long, duration: Long, uuid: String) : BasalElement { + this.timestamp = timeStart + this.rate = rate + this.duration = duration + populate(timeStart, uuid) + return this + } + + internal fun isValid(): Boolean { + return rate > -1 && duration > 0 + } + + internal fun toS(): String { + return rate.toString() + " Start: " + DateUtil.dateAndTimeFullString(timestamp) + " for: " + DateUtil.niceTimeScalar(duration) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt new file mode 100644 index 0000000000..6357c7a367 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt @@ -0,0 +1,30 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.utils.DateUtil + +open class BaseElement { + @Expose + var deviceTime: String = "" + @Expose + var time: String = "" + @Expose + var timezoneOffset: Int = 0 + @Expose + var type: String? = null + @Expose + var origin: Origin? = null + + + internal fun populate(timestamp: Long, uuid: String): BaseElement { + deviceTime = DateUtil.toISONoZone(timestamp) + time = DateUtil.toISOAsUTC(timestamp) + timezoneOffset = DateUtil.getTimeZoneOffsetMinutes(timestamp) // TODO + origin = Origin(uuid) + return this + } + + inner class Origin internal constructor(@field:Expose + internal var id: String) + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt new file mode 100644 index 0000000000..6057c5cd68 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose + +class BloodGlucoseElement : BaseElement() { + + @Expose + var subType: String = "manual" + @Expose + var units: String = "mg/dL" + @Expose + var value: Int = 0 + +/* TODO: from careportal ???? + fun fromBloodTest(bloodtest: BloodTest): BloodGlucoseElement { + val bg = BloodGlucoseElement() + bg.populate(bloodtest.timestamp, bloodtest.uuid) + + bg.subType = "manual" // TODO + bg.value = bloodtest.mgdl.toInt() + return bg + } + + fun fromBloodTests(bloodTestList: List?): List? { + if (bloodTestList == null) return null + val results = LinkedList() + for (bt in bloodTestList) { + results.add(fromBloodTest(bt)) + } + return results + } + */ +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt new file mode 100644 index 0000000000..eaa58320ee --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt @@ -0,0 +1,30 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.plugins.treatments.Treatment + +class BolusElement : BaseElement() { + @Expose + var subType = "normal" + @Expose + var normal: Double = 0.0 + @Expose + var expectedNormal: Double = 0.0 + + init { + type = "bolus"; + } + + fun create(insulinDelivered: Double, timestamp: Long, uuid: String): BolusElement { + this.normal = insulinDelivered + this.expectedNormal = insulinDelivered + populate(timestamp, uuid) + return this + } + + companion object { + fun fromTreatment(treatment: Treatment): BolusElement { + return BolusElement().create(treatment.insulin, treatment.date, "uuid-AAPS") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt new file mode 100644 index 0000000000..da51ee0e0e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.db.BgReading +import java.util.* + +class SensorGlucoseElement : BaseElement() { + + @Expose + internal var units: String = "mg/dL" + @Expose + internal var value: Int = 0 + + init { + this.type = "cbg" + } + + companion object { + internal fun fromBgReading(bgReading: BgReading): SensorGlucoseElement { + val sensorGlucose = SensorGlucoseElement() + sensorGlucose.populate(bgReading.date, "uuid-AAPS") + sensorGlucose.value = bgReading.value.toInt() + return sensorGlucose + } + + internal fun fromBgReadings(bgReadingList: List): List { + val results = LinkedList() + for (bgReading in bgReadingList) { + results.add(fromBgReading(bgReading)) + } + return results + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt new file mode 100644 index 0000000000..7c22bec8d6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions +import info.nightscout.androidaps.plugins.treatments.Treatment + +class WizardElement internal constructor() : BaseElement() { + + @Expose + var units = "mg/dL" + @Expose + var carbInput: Double = 0.toDouble() + @Expose + var insulinCarbRatio: Double = 0.toDouble() + @Expose + var bolus: BolusElement? = null + + init { + type = "wizard" + } + + companion object { + + fun fromTreatment(treatment: Treatment): WizardElement { + val result = WizardElement().populate(treatment.date, "uuid-AAPS") as WizardElement + result.carbInput = treatment.carbs + result.insulinCarbRatio = ProfileFunctions.getInstance().getProfile(treatment.date)!!.ic + if (treatment.insulin > 0) { + result.bolus = BolusElement().create(treatment.insulin, treatment.date, "uuid-AAPS") + } else { + result.bolus = BolusElement().create(0.0001, treatment.date, "uuid-AAPS") // fake insulin record + } + return result + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt new file mode 100644 index 0000000000..44feea7406 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +import info.nightscout.androidaps.events.Event + +class EventTidepoolStatus (val status: String) : Event() { +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt new file mode 100644 index 0000000000..c72b1916d6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import info.nightscout.androidaps.plugins.general.tidepool.utils.GsonInstance + +class AuthReplyMessage { + + @Expose + @SerializedName("emailVerified") + internal var emailVerified: Boolean? = null + @Expose + @SerializedName("emails") + internal var emailList: List? = null + @Expose + @SerializedName("termsAccepted") + internal var termsDate: String? = null + @Expose + @SerializedName("userid") + internal var userid: String? = null + @Expose + @SerializedName("username") + internal var username: String? = null + + fun toS(): String { + return GsonInstance.defaultGsonInstance().toJson(this) + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt new file mode 100644 index 0000000000..33c230ea75 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.SP +import info.nightscout.androidaps.utils.StringUtils +import okhttp3.Credentials + +class AuthRequestMessage : BaseMessage() { + companion object { + fun getAuthRequestHeader(): String? { + val username = SP.getString(R.string.key_tidepool_username, null) + val password = SP.getString(R.string.key_tidepool_password, null) + + return if (StringUtils.emptyString(username) || StringUtils.emptyString(password)) null else Credentials.basic(username.trim { it <= ' ' }, password) + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt new file mode 100644 index 0000000000..501e7f94a7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt @@ -0,0 +1,16 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import info.nightscout.androidaps.plugins.general.tidepool.utils.GsonInstance +import okhttp3.MediaType +import okhttp3.RequestBody + +open class BaseMessage { + fun toS(): String { + return GsonInstance.defaultGsonInstance().toJson(this) ?: "null" + } + + fun getBody(): RequestBody { + return RequestBody.create(MediaType.parse("application/json"), this.toS()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt new file mode 100644 index 0000000000..f8f6780971 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import com.google.gson.annotations.Expose + +class CloseDatasetRequestMessage : BaseMessage() { + @Expose + internal var dataState = "closed" +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt new file mode 100644 index 0000000000..7809ddbce0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + + +class DatasetReplyMessage { + + internal var data: Data? = null + + // openDataSet and others return this in the root of the json reply it seems + internal var id: String? = null + internal var uploadId: String? = null + + inner class Data { + internal var createdTime: String? = null + internal var deviceId: String? = null + internal var id: String? = null + internal var time: String? = null + internal var timezone: String? = null + internal var timezoneOffset: Int = 0 + internal var type: String? = null + internal var uploadId: String? = null + internal var client: Client? = null + internal var computerTime: String? = null + internal var dataSetType: String? = null + internal var deviceManufacturers: List? = null + internal var deviceModel: String? = null + internal var deviceSerialNumber: String? = null + internal var deviceTags: List? = null + internal var timeProcessing: String? = null + internal var version: String? = null + // meta + } + + inner class Client { + internal var name: String? = null + internal var version: String? = null + + } + + fun getUploadId(): String? { + return if (data != null && data!!.uploadId != null) data!!.uploadId else uploadId + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt new file mode 100644 index 0000000000..cca39ff9c2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.interfaces.PluginBase +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.T +import java.util.* + +class OpenDatasetRequestMessage : BaseMessage() { + + @Expose + var deviceId: String? = null + @Expose + var time = DateUtil.toISOAsUTC(DateUtil.now()) + @Expose + var timezoneOffset = (DateUtil.getTimeZoneOffsetMs() / T.mins(1).msecs()).toInt() + @Expose + var type = "upload" + //public String byUser; + @Expose + var client = ClientInfo() + @Expose + var computerTime = DateUtil.toISONoZone(DateUtil.now()) + @Expose + var dataSetType = UPLOAD_TYPE // omit for "normal" + @Expose + var deviceManufacturers = arrayOf((ConfigBuilderPlugin.getPlugin().activeBgSource as PluginBase).name) + @Expose + var deviceModel = (ConfigBuilderPlugin.getPlugin().activeBgSource as PluginBase).name + @Expose + var deviceTags = arrayOf("bgm", "cgm", "insulin-pump") + @Expose + var deduplicator = Deduplicator() + @Expose + var timeProcessing = "none" + @Expose + var timezone = TimeZone.getDefault().id + @Expose + var version = BuildConfig.VERSION_NAME + + inner class ClientInfo { + @Expose + val name = BuildConfig.APPLICATION_ID + @Expose + val version = "0.1.0" // TODO: const it + } + + inner class Deduplicator { + @Expose + val name = "org.tidepool.deduplicator.dataset.delete.origin" + } + + companion object { + internal val UPLOAD_TYPE = "continuous" + + fun isNormal(): Boolean { + return UPLOAD_TYPE == "normal" + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt new file mode 100644 index 0000000000..2054eb237a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +class UploadReplyMessage { + + internal var data: List? = null +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt new file mode 100644 index 0000000000..2c7ceb81d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.general.tidepool.utils + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +object GsonInstance { + private var gson_instance: Gson? = null + + fun defaultGsonInstance(): Gson { + if (gson_instance == null) { + gson_instance = GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create() + } + return gson_instance as Gson + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/LogSlider.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/LogSlider.kt new file mode 100644 index 0000000000..f4515b328b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/LogSlider.kt @@ -0,0 +1,12 @@ +package info.nightscout.androidaps.plugins.general.tidepool.utils + +object LogSlider { + // logarithmic slider with positions start - end representing values start - end, calculate value at selected position + fun calc(sliderStart: Int, sliderEnd: Int, start: Double, end: Double, position: Int): Double { + var valueStart = start + var valueEnd = end + valueStart = Math.log(Math.max(1.0, valueStart)) + valueEnd = Math.log(Math.max(1.0, valueEnd)) + return Math.exp(valueStart + (valueEnd - valueStart) / (sliderEnd - sliderStart) * (position - sliderStart)) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt new file mode 100644 index 0000000000..416cf2f9f2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.general.tidepool.utils + +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.T +import org.slf4j.LoggerFactory +import java.util.* + +object RateLimit { + + private val rateLimits = HashMap() + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + // return true if below rate limit + @Synchronized + fun ratelimit(name: String, seconds: Int): Boolean { + // check if over limit + if (rateLimits.containsKey(name) && DateUtil.now() - rateLimits.get(name)!! < T.secs(seconds.toLong()).msecs()) { + if (L.isEnabled(L.TIDEPOOL)) + log.debug("$name rate limited: $seconds seconds") + return false + } + // not over limit + rateLimits.put(name, DateUtil.now()) + return true + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java b/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java index 7cc4424244..8e875e6dca 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java @@ -8,6 +8,8 @@ import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -71,6 +73,18 @@ public class DateUtil { return toISOString(new Date(date), FORMAT_DATE_ISO_OUT, TimeZone.getTimeZone("UTC")); } + public static String toISOAsUTC(final long timestamp) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'0000Z'", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.format(timestamp); + } + + public static String toISONoZone(final long timestamp) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getDefault()); + return format.format(timestamp); + } + public static Date toDate(Integer seconds) { Calendar calendar = new GregorianCalendar(); calendar.set(Calendar.MONTH, 0); // Set january to be sure we miss DST changing @@ -187,4 +201,80 @@ public class DateUtil { long diff = Math.abs(date - now()); return diff < T.mins(2).msecs(); } + + public static long getTimeZoneOffsetMs() { + return new GregorianCalendar().getTimeZone().getRawOffset(); + } + + public static int getTimeZoneOffsetMinutes(final long timestamp) { + return TimeZone.getDefault().getOffset(timestamp) / 60000; + } + + public static String niceTimeScalar(long t) { + String unit = MainApp.gs(R.string.unit_second); + t = t / 1000; + if (t != 1) unit = MainApp.gs(R.string.unit_seconds); + if (t > 59) { + unit = MainApp.gs(R.string.unit_minute); + t = t / 60; + if (t != 1) unit = MainApp.gs(R.string.unit_minutes); + if (t > 59) { + unit = MainApp.gs(R.string.unit_hour); + t = t / 60; + if (t != 1) unit = MainApp.gs(R.string.unit_hours); + if (t > 24) { + unit = MainApp.gs(R.string.unit_day); + t = t / 24; + if (t != 1) unit = MainApp.gs(R.string.unit_days); + if (t > 28) { + unit = MainApp.gs(R.string.unit_week); + t = t / 7; + if (t != 1) unit = MainApp.gs(R.string.unit_weeks); + } + } + } + } + //if (t != 1) unit = unit + "s"; //implemented plurality in every step, because in other languages plurality of time is not every time adding the same character + return qs((double) t, 0) + " " + unit; + } + + // singletons to avoid repeated allocation + private static DecimalFormatSymbols dfs; + private static DecimalFormat df; + public static String qs(double x, int digits) { + + if (digits == -1) { + digits = 0; + if (((int) x != x)) { + digits++; + if ((((int) x * 10) / 10 != x)) { + digits++; + if ((((int) x * 100) / 100 != x)) digits++; + } + } + } + + if (dfs == null) { + final DecimalFormatSymbols local_dfs = new DecimalFormatSymbols(); + local_dfs.setDecimalSeparator('.'); + dfs = local_dfs; // avoid race condition + } + + final DecimalFormat this_df; + // use singleton if on ui thread otherwise allocate new as DecimalFormat is not thread safe + if (Thread.currentThread().getId() == 1) { + if (df == null) { + final DecimalFormat local_df = new DecimalFormat("#", dfs); + local_df.setMinimumIntegerDigits(1); + df = local_df; // avoid race condition + } + this_df = df; + } else { + this_df = new DecimalFormat("#", dfs); + } + + this_df.setMaximumFractionDigits(digits); + return this_df.format(x); + } + } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java b/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java index cad2b76291..37a3b7b7bf 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java @@ -17,4 +17,9 @@ public class StringUtils { return string; } + + public static boolean emptyString(final String str) { + return str == null || str.length() == 0; + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 798c076d05..89a32876be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1344,6 +1344,10 @@ Data Age Mins If enabled, uploads will go to https://int-app.tidepool.org instead of the regular https://app.tidepool.org/ Use Integration (test) servers + Tidepool + TDP + Uploads data to Tidepool + tidepool_last_end smbmaxminutes Dayligh Saving time @@ -1363,9 +1367,6 @@ old version very old version New version for at least %1$d days available! Fallback to LGS after 60 days, loop will be disabled after 90 days - Tidepool - TDP - Uploads data to Tidepool %1$d day From 048ea4d489566b4e88bb5312e2d5276d7ce4745f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 3 Jun 2019 00:03:06 +0200 Subject: [PATCH 06/39] Tidepool test UI --- app/build.gradle | 2 +- .../general/tidepool/TidepoolFragment.kt | 41 +++++++++++++++ .../tidepool/TidepoolJavaFragment.java | 36 +++++++++++++ .../general/tidepool/TidepoolPlugin.kt | 31 ++++++++++- .../general/tidepool/comm/TidepoolUploader.kt | 30 +++++------ .../general/tidepool/comm/UploadChunk.kt | 32 +++++++----- .../tidepool/events/EventTidepoolDoUpload.kt | 4 ++ .../tidepool/events/EventTidepoolResetData.kt | 6 +++ app/src/main/res/layout/tidepool_fragment.xml | 51 +++++++++++++++++++ 9 files changed, 202 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolJavaFragment.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt create mode 100644 app/src/main/res/layout/tidepool_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index e6b06601bf..08f92dc239 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -104,7 +104,7 @@ android { targetSdkVersion 25 multiDexEnabled true versionCode 1500 - version "2.3.1-dev" + version "2.3.1-tidepool" buildConfigField "String", "VERSION", '"' + version + '"' buildConfigField "String", "BUILDVERSION", '"' + generateGitBuild() + '-' + generateDate() + '"' buildConfigField "String", "REMOTE", '"' + generateGitRemote() + '"' diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt new file mode 100644 index 0000000000..de6cda31ff --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.general.tidepool + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.squareup.otto.Subscribe +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.common.SubscriberFragment +import info.nightscout.androidaps.plugins.general.nsclient.events.EventNSClientUpdateGUI +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import kotlinx.android.synthetic.main.tidepool_fragment.* + +class TidepoolFragment : SubscriberFragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.tidepool_fragment, container, false) + + tidepool_login.setOnClickListener { + TidepoolUploader.doLogin() + } + tidepool_removeall.setOnClickListener { } + tidepool_uploadnow.setOnClickListener { } + return view + } + + @Subscribe + fun onStatusEvent(ev: EventNSClientUpdateGUI) { + updateGUI() + } + + override fun updateGUI() { + val activity = activity + activity?.runOnUiThread { +// TidepoolPlugin.updateLog() +// tidepool_log.text = TidepoolPlugin.textLog + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolJavaFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolJavaFragment.java new file mode 100644 index 0000000000..24b465bda6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolJavaFragment.java @@ -0,0 +1,36 @@ +package info.nightscout.androidaps.plugins.general.tidepool; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.common.SubscriberFragment; +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader; +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolResetData; + +public class TidepoolJavaFragment extends SubscriberFragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.tidepool_fragment, container, false); + + Button login = view.findViewById(R.id.tidepool_login); + login.setOnClickListener(v -> { + TidepoolUploader.INSTANCE.doLogin(); + }); + Button removeall = view.findViewById(R.id.tidepool_removeall); + removeall.setOnClickListener(v -> { + MainApp.bus().post(new EventTidepoolResetData()); + }); + return view; + } + + @Override + protected void updateGUI() { + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt index 724c35b606..51698112e5 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt @@ -9,7 +9,10 @@ import info.nightscout.androidaps.interfaces.PluginBase import info.nightscout.androidaps.interfaces.PluginDescription import info.nightscout.androidaps.interfaces.PluginType import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.general.tidepool.comm.Session import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolDoUpload +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolResetData import info.nightscout.androidaps.plugins.general.tidepool.utils.RateLimit import info.nightscout.androidaps.receivers.ChargingStateReceiver import info.nightscout.androidaps.utils.SP @@ -19,12 +22,15 @@ object TidepoolPlugin : PluginBase(PluginDescription() .mainType(PluginType.GENERAL) .pluginName(R.string.tidepool) .shortName(R.string.tidepool_shortname) + .fragmentClass(TidepoolJavaFragment::class.java.name) .preferencesId(R.xml.pref_tidepool) .description(R.string.description_tidepool) ) { private val log = LoggerFactory.getLogger(L.TIDEPOOL) private var wifiConnected = false + var session: Session? = null + override fun onStart() { MainApp.bus().register(this) super.onStart() @@ -35,6 +41,12 @@ object TidepoolPlugin : PluginBase(PluginDescription() super.onStop() } + fun doUpload() { + if (session == null) + session = TidepoolUploader.doLogin() + else TidepoolUploader.doUpload(session!!) + } + @Suppress("UNUSED_PARAMETER") @Subscribe fun onStatusEvent(ev: EventNewBG) { @@ -42,7 +54,24 @@ object TidepoolPlugin : PluginBase(PluginDescription() && (!SP.getBoolean(R.string.key_tidepool_only_while_charging, false) || ChargingStateReceiver.isCharging()) && (!SP.getBoolean(R.string.key_tidepool_only_while_unmetered, false) || wifiConnected) && RateLimit.ratelimit("tidepool-new-data-upload", 1200)) - TidepoolUploader.doLogin() + doUpload() + } + + @Suppress("UNUSED_PARAMETER") + @Subscribe + fun onEventTidepoolDoUpload(ev: EventTidepoolDoUpload) { + doUpload() + } + + @Suppress("UNUSED_PARAMETER") + @Subscribe + fun onEventTidepoolResetData(ev: EventTidepoolResetData) { + if (session == null) + session = TidepoolUploader.doLogin() + if (session != null) { + TidepoolUploader.deleteDataSet(session!!) + TidepoolUploader.startSession(session!!) + } } @Subscribe diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt index 7c0939e53d..3bfe9a7aab 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt @@ -59,10 +59,10 @@ object TidepoolUploader { } @Synchronized - fun doLogin() { + fun doLogin(): Session? { if (!SP.getBoolean(R.string.key_cloud_storage_tidepool_enable, false)) { log.debug("Cannot login as disabled by preference") - return + return null } // TODO failure backoff extendWakeLock(30000) @@ -72,10 +72,12 @@ object TidepoolUploader { status("Connecting") call?.enqueue(TidepoolCallback(session, "Login", { startSession(session) }, { loginFailed() })) + return session } else { if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot do login as user credentials have not been set correctly") status("Invalid credentials") releaseWakeLock() + return null } } @@ -103,7 +105,7 @@ object TidepoolUploader { releaseWakeLock() } - private fun startSession(session: Session) { + fun startSession(session: Session) { extendWakeLock(30000) if (session.authReply?.userid != null) { // See if we already have an open data set to write to @@ -130,7 +132,7 @@ object TidepoolUploader { } } - private fun doUpload(session: Session) { + fun doUpload(session: Session) { if (!TidepoolPlugin.enabled()) { if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot upload - preference disabled") @@ -164,6 +166,7 @@ object TidepoolUploader { } private fun status(status: String) { + log.debug("New status: $status") MainApp.bus().post(EventTidepoolStatus(status)) } @@ -186,23 +189,18 @@ object TidepoolUploader { releaseWakeLock() } - private fun deleteData(session: Session) { - if (session.authReply!!.userid != null) { - val call = session.service!!.deleteAllData(session.token!!, session.authReply!!.userid!!) - call.enqueue(TidepoolCallback(session, "Delete Data", {}, {})) - } else { - log.error("Got login response but cannot determine userid - cannot proceed") - } - } - private fun getDataSet(session: Session) { val call = session.service!!.getDataSet(session.token!!, "bogus") call.enqueue(TidepoolCallback(session, "Get Data", {}, {})) } - private fun deleteDataSet(session: Session) { - val call = session.service!!.deleteDataSet(session.token!!, "bogus") - call.enqueue(TidepoolCallback(session, "Delete Data", {}, {})) + fun deleteDataSet(session: Session) { + if (session.datasetReply?.id != null) { + val call = session.service?.deleteDataSet(session.token!!, session.datasetReply!!.id!!) + call?.enqueue(TidepoolCallback(session, "Delete Dataset", {}, {})) + } else { + log.error("Got login response but cannot determine dataseId - cannot proceed") + } } @Synchronized diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt index 0fa1da3ecc..bcedcf246d 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt @@ -1,8 +1,8 @@ package info.nightscout.androidaps.plugins.general.tidepool.comm -import android.util.Log import info.nightscout.androidaps.MainApp import info.nightscout.androidaps.R +import info.nightscout.androidaps.logging.L import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions import info.nightscout.androidaps.plugins.general.tidepool.elements.* import info.nightscout.androidaps.plugins.general.tidepool.utils.GsonInstance @@ -11,6 +11,7 @@ import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin import info.nightscout.androidaps.utils.DateUtil import info.nightscout.androidaps.utils.SP import info.nightscout.androidaps.utils.T +import org.slf4j.LoggerFactory import java.util.* object UploadChunk { @@ -21,6 +22,7 @@ object UploadChunk { private val DEFAULT_WINDOW_OFFSET = T.mins(15).msecs() private val MAX_LATENCY_THRESHOLD_MINUTES: Long = 1440 // minutes per day + private val log = LoggerFactory.getLogger(L.TIDEPOOL) fun getNext(session: Session): String? { session.start = getLastEnd() @@ -28,7 +30,7 @@ object UploadChunk { val result = get(session.start, session.end) if (result != null && result.length < 3) { - Log.d(TAG, "No records in this time period, setting start to best end time") + if (L.isEnabled(L.TIDEPOOL)) log.debug("No records in this time period, setting start to best end time") setLastEnd(Math.max(session.end, getOldestRecordTimeStamp())) } return result @@ -36,13 +38,13 @@ object UploadChunk { operator fun get(start: Long, end: Long): String? { - Log.e(TAG, "Syncing data between: " + DateUtil.dateAndTimeFullString(start) + " -> " + DateUtil.dateAndTimeFullString(end)) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Syncing data between: " + DateUtil.dateAndTimeFullString(start) + " -> " + DateUtil.dateAndTimeFullString(end)) if (end <= start) { - Log.e(TAG, "End is <= start: " + DateUtil.dateAndTimeFullString(start) + " " + DateUtil.dateAndTimeFullString(end)) + if (L.isEnabled(L.TIDEPOOL)) log.debug("End is <= start: " + DateUtil.dateAndTimeFullString(start) + " " + DateUtil.dateAndTimeFullString(end)) return null } if (end - start > MAX_UPLOAD_SIZE) { - Log.e(TAG, "More than max range - rejecting") + if (L.isEnabled(L.TIDEPOOL)) log.debug("More than max range - rejecting") return null } @@ -61,7 +63,7 @@ object UploadChunk { val value = getLatencySliderValue(SP.getInt(R.string.key_tidepool_window_latency, 0)).toLong() return Math.max(T.mins(value).msecs(), DEFAULT_WINDOW_OFFSET) } catch (e: Exception) { - Log.e(TAG, "Reverting to default of 15 minutes due to Window Size exception: $e") + if (L.isEnabled(L.TIDEPOOL)) log.debug("Reverting to default of 15 minutes due to Window Size exception: $e") return DEFAULT_WINDOW_OFFSET // default } @@ -79,10 +81,11 @@ object UploadChunk { fun setLastEnd(time: Long) { if (time > getLastEnd()) { - SP.putLong(R.string.key_tidepool_last_end, time) - Log.d(TAG, "Updating last end to: " + DateUtil.dateAndTimeFullString(time)) + //TODO SP.putLong(R.string.key_tidepool_last_end, time) + SP.putLong(R.string.key_tidepool_last_end, 0) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Updating last end to: " + DateUtil.dateAndTimeFullString(time)) } else { - Log.e(TAG, "Cannot set last end to: " + DateUtil.dateAndTimeFullString(time) + " vs " + DateUtil.dateAndTimeFullString(getLastEnd())) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot set last end to: " + DateUtil.dateAndTimeFullString(time) + " vs " + DateUtil.dateAndTimeFullString(getLastEnd())) } } @@ -123,7 +126,10 @@ object UploadChunk { } internal fun getBgReadings(start: Long, end: Long): List { - return SensorGlucoseElement.fromBgReadings(MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true)) + val readings = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true) + if (L.isEnabled(L.TIDEPOOL)) + log.debug("${readings.size} selected for upload") + return SensorGlucoseElement.fromBgReadings(readings) } internal fun getBasals(start: Long, end: Long): List { @@ -136,15 +142,15 @@ object UploadChunk { if (current != null) { if (this_rate != current.rate) { current.duration = temporaryBasal.date - current.timestamp - Log.d(TAG, "Adding current: " + current.toS()) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Adding current: " + current.toS()) if (current.isValid()) { basals.add(current) } else { - Log.e(TAG, "Current basal is invalid: " + current.toS()) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Current basal is invalid: " + current.toS()) } current = null } else { - Log.d(TAG, "Same rate as previous basal record: " + current.rate + " " + temporaryBasal.toStringFull()) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Same rate as previous basal record: " + current.rate + " " + temporaryBasal.toStringFull()) } } if (current == null) { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt new file mode 100644 index 0000000000..a77d3232d6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt @@ -0,0 +1,4 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +class EventTidepoolDoUpload { +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt new file mode 100644 index 0000000000..73dac259c3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +import info.nightscout.androidaps.events.Event + +class EventTidepoolResetData :Event() { +} \ No newline at end of file diff --git a/app/src/main/res/layout/tidepool_fragment.xml b/app/src/main/res/layout/tidepool_fragment.xml new file mode 100644 index 0000000000..975dad589e --- /dev/null +++ b/app/src/main/res/layout/tidepool_fragment.xml @@ -0,0 +1,51 @@ + + + + + +