tidepool xdrip port
This commit is contained in:
parent
e61cbecb2e
commit
dd637021f7
|
@ -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) {
|
||||
|
|
126
app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java
Normal file
126
app/src/main/java/com/eveningoutpost/dexdrip/Models/JoH.java
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.eveningoutpost.dexdrip.tidepool;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
/**
|
||||
* jamorham
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<EBloodGlucose> fromBloodTests(final List<BloodTest> bloodTestList) {
|
||||
if (bloodTestList == null) return null;
|
||||
final List<EBloodGlucose> results = new LinkedList<>();
|
||||
for (BloodTest bt : bloodTestList) {
|
||||
results.add(fromBloodTest(bt));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ESensorGlucose> fromBgReadings(final List<BgReading> bgReadingList) {
|
||||
if (bgReadingList == null) return null;
|
||||
final List<ESensorGlucose> results = new LinkedList<>();
|
||||
for (BgReading bgReading : bgReadingList) {
|
||||
results.add(fromBgReading(bgReading));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<String> emailList;
|
||||
@Expose
|
||||
@SerializedName("termsAccepted")
|
||||
String termsDate;
|
||||
@Expose
|
||||
@SerializedName("userid")
|
||||
String userid;
|
||||
@Expose
|
||||
@SerializedName("username")
|
||||
String username;
|
||||
|
||||
public String toS() {
|
||||
return JoH.defaultGsonInstance().toJson(this);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.eveningoutpost.dexdrip.tidepool;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
public class MCloseDatasetRequest extends BaseMessage {
|
||||
@Expose
|
||||
String dataState = "closed";
|
||||
|
||||
}
|
|
@ -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<String> deviceManufacturers;
|
||||
String deviceModel;
|
||||
String deviceSerialNumber;
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package com.eveningoutpost.dexdrip.tidepool;
|
||||
|
||||
public class MGetDatasetsRequest extends BaseMessage {
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.eveningoutpost.dexdrip.tidepool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MUploadReply {
|
||||
|
||||
List<String> data;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<T> implements Callback<T> {
|
||||
|
||||
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<T> setOnFailure(final Runnable runnable) {
|
||||
this.onFailure = runnable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call<T> call, Response<T> 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<T> 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<StatusItem> megaStatus() {
|
||||
|
||||
final KeyStore keyStore = FastStore.getInstance();
|
||||
final List<StatusItem> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* <p>
|
||||
* Tidepool Uploader
|
||||
* <p>
|
||||
* 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<MAuthReply> getLogin(@Header("Authorization") String secret);
|
||||
|
||||
@DELETE("/v1/users/{userId}/data")
|
||||
Call<MDatasetReply> deleteAllData(@Header(SESSION_TOKEN_HEADER) String token, @Path("userId") String id);
|
||||
|
||||
@DELETE("/v1/datasets/{dataSetId}")
|
||||
Call<MDatasetReply> deleteDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("dataSetId") String id);
|
||||
|
||||
@GET("/v1/users/{userId}/data_sets")
|
||||
Call<List<MDatasetReply>> 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<MDatasetReply> getDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("dataSetId") String id);
|
||||
|
||||
@POST("/v1/users/{userId}/data_sets")
|
||||
Call<MDatasetReply> openDataSet(@Header(SESSION_TOKEN_HEADER) String token, @Path("userId") String id, @Body RequestBody body);
|
||||
|
||||
@POST("/v1/datasets/{sessionId}/data")
|
||||
Call<MUploadReply> doUpload(@Header(SESSION_TOKEN_HEADER) String token, @Path("sessionId") String id, @Body RequestBody body);
|
||||
|
||||
@PUT("/v1/datasets/{sessionId}")
|
||||
Call<MDatasetReply> 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<MAuthReply> call = session.service.getLogin(session.authHeader);
|
||||
status("Connecting");
|
||||
if (fromUi) {
|
||||
// JoH.static_toast_long("Connecting to Tidepool");
|
||||
}
|
||||
|
||||
call.enqueue(new TidepoolCallback<MAuthReply>(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<MAuthReply> call = session.service.getLogin(session.authHeader);
|
||||
|
||||
try {
|
||||
Response<MAuthReply> 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<List<MDatasetReply>> datasetCall = session.service.getOpenDataSets(session.token,
|
||||
session.authReply.userid, BuildConfig.APPLICATION_ID, 1);
|
||||
|
||||
datasetCall.enqueue(new TidepoolCallback<List<MDatasetReply>>(session, "Get Open Datasets", () -> {
|
||||
if (session.datasetReply == null) {
|
||||
status("New data set");
|
||||
if (fromUi) {
|
||||
// JoH.static_toast_long("Creating new data set");
|
||||
}
|
||||
Call<MDatasetReply> call = session.service.openDataSet(session.token, session.authReply.userid, new MOpenDatasetRequest().getBody());
|
||||
call.enqueue(new TidepoolCallback<MDatasetReply>(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<MUploadReply> call = session.service.doUpload(session.token, session.datasetReply.getUploadId(), body);
|
||||
status("Uploading");
|
||||
call.enqueue(new TidepoolCallback<MUploadReply>(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<MDatasetReply> 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<MDatasetReply> 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<MDatasetReply> call = session.service.getDataSet(session.token, "bogus");
|
||||
call.enqueue(new TidepoolCallback<>(session, "Get Data", null));
|
||||
}
|
||||
|
||||
private static void deleteDataSet(final Session session) {
|
||||
Call<MDatasetReply> call = session.service.deleteDataSet(session.token, "bogus");
|
||||
call.enqueue(new TidepoolCallback<>(session, "Delete Data", null));
|
||||
}
|
||||
|
||||
}
|
|
@ -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<BaseElement> 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<BaseElement> getTreatments(final long start, final long end) {
|
||||
List<BaseElement> result = new LinkedList<>();
|
||||
final List<Treatments> 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<BgReading> bgReadingList = BgReading.latestForGraphAsc(1, start, end);
|
||||
if (bgReadingList != null && bgReadingList.size() > 0) {
|
||||
return bgReadingList.get(0).timestamp;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static List<EBloodGlucose> getBloodTests(final long start, final long end) {
|
||||
return EBloodGlucose.fromBloodTests(BloodTest.latestForGraph(1800, start, end));
|
||||
}
|
||||
|
||||
static List<ESensorGlucose> getBgReadings(final long start, final long end) {
|
||||
return ESensorGlucose.fromBgReadings(BgReading.latestForGraphAsc(15000, start, end));
|
||||
}
|
||||
|
||||
static List<EBasal> getBasals(final long start, final long end) {
|
||||
final List<EBasal> basals = new LinkedList<>();
|
||||
final List<APStatus> 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);
|
||||
}
|
||||
}
|
|
@ -1316,6 +1316,20 @@
|
|||
<string name="tomato">Tomato (MiaoMiao)</string>
|
||||
<string name="tomato_short">Tomato</string>
|
||||
|
||||
<string name="unit_second">second</string>
|
||||
<string name="unit_minute">minute</string>
|
||||
<string name="unit_hour">hour</string>
|
||||
<string name="unit_day">day"</string>
|
||||
<string name="unit_week">week"</string>
|
||||
<string name="unit_seconds">seconds</string>
|
||||
<string name="unit_minutes">minutes</string>
|
||||
<string name="unit_hours">hours</string>
|
||||
<string name="unit_days">days"</string>
|
||||
<string name="unit_weeks">weeks"</string>
|
||||
<string name="key_tidepool_username" translatable="false">tidepool_username</string>
|
||||
<string name="key_tidepool_password" translatable="false">tidepool_password</string>
|
||||
<string name="key_tidepool_dev_servers" translatable="false">tidepool_dev_servers</string>
|
||||
|
||||
<string name="key_smbmaxminutes" translatable="false">smbmaxminutes</string>
|
||||
<string name="dst_plugin_name" translatable="false">Dayligh Saving time</string>
|
||||
<string name="dst_in_24h_warning">Dayligh Saving time change in 24h or less</string>
|
||||
|
|
Loading…
Reference in a new issue