transform to kotlin, cleanup, sort
This commit is contained in:
parent
42ceaac0f7
commit
e2364561ed
63 changed files with 1247 additions and 2231 deletions
|
@ -278,16 +278,22 @@ dependencies {
|
||||||
androidTestImplementation "com.google.dexmaker:dexmaker-mockito:${dexmakerVersion}"
|
androidTestImplementation "com.google.dexmaker:dexmaker-mockito:${dexmakerVersion}"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
|
|
||||||
// xDrip-plus port
|
// xDrip-plus port
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor: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
|
// you will want to install the android studio lombok plugin
|
||||||
compileOnly 'org.projectlombok:lombok:1.16.20'
|
compileOnly 'org.projectlombok:lombok:1.16.20'
|
||||||
// compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
|
// compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
|
||||||
annotationProcessor "org.projectlombok:lombok:1.16.20"
|
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) {
|
task unzip(type: Copy) {
|
||||||
|
|
|
@ -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<BloodTest> 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<BloodTest> btl = last(1);
|
|
||||||
if ((btl != null) && (btl.size() > 0)) {
|
|
||||||
return btl.get(0);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<BloodTest> 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<BloodTest> 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<BloodTest> btl = lastValid(1);
|
|
||||||
if ((btl != null) && (btl.size() > 0)) {
|
|
||||||
return btl.get(0);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<BloodTest> 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<BloodTest> btl) {
|
|
||||||
if (btl == null) return null;
|
|
||||||
final List<BloodTestMessage> 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<BloodTest> latestForGraph(int number, double startTime) {
|
|
||||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<BloodTest> latestForGraph(int number, long startTime) {
|
|
||||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<BloodTest> 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<BloodTest> bloodTests = latestForGraph(1000, JoH.tsl() - period, JoH.tsl() - AddCalibration.estimatedInterstitialLagSeconds);
|
|
||||||
final List<Double> difference = new ArrayList<>();
|
|
||||||
final List<Double> 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<BloodTest> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
* <p>
|
|
||||||
* 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<String, Task> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
* <p>
|
|
||||||
* 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
|
|
||||||
* <p>
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
* <p>
|
|
||||||
* For representing row items suitable for MegaStatus
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class StatusItem {
|
|
||||||
|
|
||||||
private static final HashMap<Highlight, Integer> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package com.eveningoutpost.dexdrip.store;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by jamorham on 08/11/2017.
|
|
||||||
* <p>
|
|
||||||
* 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<String, String> stringStore = new HashMap<>();
|
|
||||||
private static final HashMap<String, Long> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.eveningoutpost.dexdrip.tidepool;
|
|
||||||
|
|
||||||
import com.google.gson.annotations.Expose;
|
|
||||||
|
|
||||||
public class MCloseDatasetRequest extends BaseMessage {
|
|
||||||
@Expose
|
|
||||||
String dataState = "closed";
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
package com.eveningoutpost.dexdrip.tidepool;
|
|
||||||
|
|
||||||
public class MGetDatasetsRequest extends BaseMessage {
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.eveningoutpost.dexdrip.tidepool;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MUploadReply {
|
|
||||||
|
|
||||||
List<String> data;
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
* <p>
|
|
||||||
* 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<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(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<BaseElement> getTreatments(final long start, final long end) {
|
|
||||||
List<BaseElement> result = new LinkedList<>();
|
|
||||||
final List<Treatment> 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<BgReading> bgReadingList = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, false);
|
|
||||||
if (bgReadingList != null && bgReadingList.size() > 0) {
|
|
||||||
return bgReadingList.get(0).date;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<EBloodGlucose> getBloodTests(final long start, final long end) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
// return EBloodGlucose.fromBloodTests(BloodTest.latestForGraph(1800, start, end));
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<ESensorGlucose> getBgReadings(final long start, final long end) {
|
|
||||||
return ESensorGlucose.fromBgReadings(MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<EBasal> getBasals(final long start, final long end) {
|
|
||||||
final List<EBasal> basals = new LinkedList<>();
|
|
||||||
final List<TemporaryBasal> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
|
@ -205,7 +205,7 @@ public class MainApp extends Application {
|
||||||
pluginsList.add(StatuslinePlugin.initPlugin(this));
|
pluginsList.add(StatuslinePlugin.initPlugin(this));
|
||||||
pluginsList.add(PersistentNotificationPlugin.getPlugin());
|
pluginsList.add(PersistentNotificationPlugin.getPlugin());
|
||||||
pluginsList.add(NSClientPlugin.getPlugin());
|
pluginsList.add(NSClientPlugin.getPlugin());
|
||||||
pluginsList.add(TidepoolPlugin.getPlugin());
|
pluginsList.add(TidepoolPlugin.INSTANCE);
|
||||||
pluginsList.add(MaintenancePlugin.initPlugin(this));
|
pluginsList.add(MaintenancePlugin.initPlugin(this));
|
||||||
|
|
||||||
pluginsList.add(ConfigBuilderPlugin.getPlugin());
|
pluginsList.add(ConfigBuilderPlugin.getPlugin());
|
||||||
|
|
|
@ -87,6 +87,7 @@ public class L {
|
||||||
public static final String DATAFOOD = "DATAFOOD";
|
public static final String DATAFOOD = "DATAFOOD";
|
||||||
public static final String DATATREATMENTS = "DATATREATMENTS";
|
public static final String DATATREATMENTS = "DATATREATMENTS";
|
||||||
public static final String NSCLIENT = "NSCLIENT";
|
public static final String NSCLIENT = "NSCLIENT";
|
||||||
|
public static final String TIDEPOOL = "TIDEPOOL";
|
||||||
public static final String CONSTRAINTS = "CONSTRAINTS";
|
public static final String CONSTRAINTS = "CONSTRAINTS";
|
||||||
public static final String PUMP = "PUMP";
|
public static final String PUMP = "PUMP";
|
||||||
public static final String PUMPQUEUE = "PUMPQUEUE";
|
public static final String PUMPQUEUE = "PUMPQUEUE";
|
||||||
|
@ -114,6 +115,7 @@ public class L {
|
||||||
logElements.add(new LogElement(EVENTS, false, true));
|
logElements.add(new LogElement(EVENTS, false, true));
|
||||||
logElements.add(new LogElement(NOTIFICATION, true));
|
logElements.add(new LogElement(NOTIFICATION, true));
|
||||||
logElements.add(new LogElement(NSCLIENT, true));
|
logElements.add(new LogElement(NSCLIENT, true));
|
||||||
|
logElements.add(new LogElement(TIDEPOOL, true));
|
||||||
logElements.add(new LogElement(OVERVIEW, true));
|
logElements.add(new LogElement(OVERVIEW, true));
|
||||||
logElements.add(new LogElement(PROFILE, true));
|
logElements.add(new LogElement(PROFILE, true));
|
||||||
logElements.add(new LogElement(PUMP, true));
|
logElements.add(new LogElement(PUMP, true));
|
||||||
|
|
|
@ -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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,14 +3,18 @@ package com.eveningoutpost.dexdrip.tidepool;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.util.Log;
|
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 java.util.List;
|
||||||
|
|
||||||
import info.nightscout.androidaps.BuildConfig;
|
import info.nightscout.androidaps.BuildConfig;
|
||||||
import info.nightscout.androidaps.R;
|
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 info.nightscout.androidaps.utils.SP;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
@ -131,8 +135,8 @@ public class TidepoolUploader {
|
||||||
// if (JoH.ratelimit("tidepool-login", 10)) {
|
// if (JoH.ratelimit("tidepool-login", 10)) {
|
||||||
extendWakeLock(30000);
|
extendWakeLock(30000);
|
||||||
final Session session = new Session(MAuthRequest.getAuthRequestHeader(), SESSION_TOKEN_HEADER);
|
final Session session = new Session(MAuthRequest.getAuthRequestHeader(), SESSION_TOKEN_HEADER);
|
||||||
if (session.authHeader != null) {
|
if (session.authHeader() != null) {
|
||||||
final Call<MAuthReply> call = session.service.getLogin(session.authHeader);
|
final Call<MAuthReply> call = session.service().getLogin(session.authHeader());
|
||||||
status("Connecting");
|
status("Connecting");
|
||||||
if (fromUi) {
|
if (fromUi) {
|
||||||
// JoH.static_toast_long("Connecting to Tidepool");
|
// JoH.static_toast_long("Connecting to Tidepool");
|
||||||
|
@ -158,7 +162,7 @@ public class TidepoolUploader {
|
||||||
releaseWakeLock();
|
releaseWakeLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* public static void testLogin(Context rootContext) {
|
public static void testLogin(Context rootContext) {
|
||||||
if (JoH.ratelimit("tidepool-login", 1)) {
|
if (JoH.ratelimit("tidepool-login", 1)) {
|
||||||
|
|
||||||
String message = "Failed to log into Tidepool.\n" +
|
String message = "Failed to log into Tidepool.\n" +
|
||||||
|
@ -176,7 +180,7 @@ public class TidepoolUploader {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
AlertDialog.Builder builder = new AlertDialog.Builder(rootContext);
|
||||||
|
@ -192,7 +196,7 @@ public class TidepoolUploader {
|
||||||
final AlertDialog alert = builder.create();
|
final AlertDialog alert = builder.create();
|
||||||
alert.show();
|
alert.show();
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void startSession(final Session session, boolean fromUi) {
|
private static void startSession(final Session session, boolean fromUi) {
|
|
@ -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!!)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AuthReplyMessage>
|
||||||
|
|
||||||
|
@DELETE("/v1/users/{userId}/data")
|
||||||
|
abstract fun deleteAllData(@Header(SESSION_TOKEN_HEADER) token: String, @Path("userId") id: String): Call<DatasetReplyMessage>
|
||||||
|
|
||||||
|
@DELETE("/v1/datasets/{dataSetId}")
|
||||||
|
abstract fun deleteDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("dataSetId") id: String): Call<DatasetReplyMessage>
|
||||||
|
|
||||||
|
@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<List<DatasetReplyMessage>>
|
||||||
|
|
||||||
|
@GET("/v1/datasets/{dataSetId}")
|
||||||
|
abstract fun getDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("dataSetId") id: String): Call<DatasetReplyMessage>
|
||||||
|
|
||||||
|
@POST("/v1/users/{userId}/data_sets")
|
||||||
|
abstract fun openDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("userId") id: String, @Body body: RequestBody): Call<DatasetReplyMessage>
|
||||||
|
|
||||||
|
@POST("/v1/datasets/{sessionId}/data")
|
||||||
|
abstract fun doUpload(@Header(SESSION_TOKEN_HEADER) token: String, @Path("sessionId") id: String, @Body body: RequestBody): Call<UploadReplyMessage>
|
||||||
|
|
||||||
|
@PUT("/v1/datasets/{sessionId}")
|
||||||
|
abstract fun closeDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("sessionId") id: String, @Body body: RequestBody): Call<DatasetReplyMessage>
|
||||||
|
|
||||||
|
}
|
|
@ -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<T>(val session: Session, val name: String, val onSucc: () -> Unit, val onFail: () -> Unit) : Callback<T> {
|
||||||
|
private val log = LoggerFactory.getLogger(L.TIDEPOOL)
|
||||||
|
|
||||||
|
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||||
|
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>, 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AuthReplyMessage>(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<List<DatasetReplyMessage>>(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<DatasetReplyMessage>(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<UploadReplyMessage>(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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<BaseElement>()
|
||||||
|
|
||||||
|
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<BaseElement> {
|
||||||
|
val result = LinkedList<BaseElement>()
|
||||||
|
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<BloodGlucoseElement> {
|
||||||
|
return ArrayList()
|
||||||
|
// return BloodGlucoseElement.fromBloodTests(BloodTest.latestForGraph(1800, start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getBgReadings(start: Long, end: Long): List<SensorGlucoseElement> {
|
||||||
|
return SensorGlucoseElement.fromBgReadings(MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getBasals(start: Long, end: Long): List<BasalElement> {
|
||||||
|
val basals = LinkedList<BasalElement>()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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<BloodTest>?): List<BloodGlucoseElement>? {
|
||||||
|
if (bloodTestList == null) return null
|
||||||
|
val results = LinkedList<BloodGlucoseElement>()
|
||||||
|
for (bt in bloodTestList) {
|
||||||
|
results.add(fromBloodTest(bt))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<BgReading>): List<SensorGlucoseElement> {
|
||||||
|
val results = LinkedList<SensorGlucoseElement>()
|
||||||
|
for (bgReading in bgReadingList) {
|
||||||
|
results.add(fromBgReading(bgReading))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package info.nightscout.androidaps.plugins.general.tidepool.events
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.events.Event
|
||||||
|
|
||||||
|
class EventTidepoolStatus (val status: String) : Event() {
|
||||||
|
}
|
|
@ -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<String>? = 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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<String>? = null
|
||||||
|
internal var deviceModel: String? = null
|
||||||
|
internal var deviceSerialNumber: String? = null
|
||||||
|
internal var deviceTags: List<String>? = 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package info.nightscout.androidaps.plugins.general.tidepool.messages
|
||||||
|
|
||||||
|
class UploadReplyMessage {
|
||||||
|
|
||||||
|
internal var data: List<String>? = null
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Long>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import org.joda.time.format.DateTimeFormatter;
|
||||||
import org.joda.time.format.ISODateTimeFormat;
|
import org.joda.time.format.ISODateTimeFormat;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -71,6 +73,18 @@ public class DateUtil {
|
||||||
return toISOString(new Date(date), FORMAT_DATE_ISO_OUT, TimeZone.getTimeZone("UTC"));
|
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) {
|
public static Date toDate(Integer seconds) {
|
||||||
Calendar calendar = new GregorianCalendar();
|
Calendar calendar = new GregorianCalendar();
|
||||||
calendar.set(Calendar.MONTH, 0); // Set january to be sure we miss DST changing
|
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());
|
long diff = Math.abs(date - now());
|
||||||
return diff < T.mins(2).msecs();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,9 @@ public class StringUtils {
|
||||||
|
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean emptyString(final String str) {
|
||||||
|
return str == null || str.length() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1344,6 +1344,10 @@
|
||||||
<string name="title_tidepool_window_latency">Data Age Mins</string>
|
<string name="title_tidepool_window_latency">Data Age Mins</string>
|
||||||
<string name="summary_tidepool_dev_servers">If enabled, uploads will go to https://int-app.tidepool.org instead of the regular https://app.tidepool.org/</string>
|
<string name="summary_tidepool_dev_servers">If enabled, uploads will go to https://int-app.tidepool.org instead of the regular https://app.tidepool.org/</string>
|
||||||
<string name="title_tidepool_dev_servers">Use Integration (test) servers</string>
|
<string name="title_tidepool_dev_servers">Use Integration (test) servers</string>
|
||||||
|
<string name="tidepool">Tidepool</string>
|
||||||
|
<string name="tidepool_shortname">TDP</string>
|
||||||
|
<string name="description_tidepool">Uploads data to Tidepool</string>
|
||||||
|
<string name="key_tidepool_last_end" translatable="false">tidepool_last_end</string>
|
||||||
|
|
||||||
<string name="key_smbmaxminutes" translatable="false">smbmaxminutes</string>
|
<string name="key_smbmaxminutes" translatable="false">smbmaxminutes</string>
|
||||||
<string name="dst_plugin_name" translatable="false">Dayligh Saving time</string>
|
<string name="dst_plugin_name" translatable="false">Dayligh Saving time</string>
|
||||||
|
@ -1363,9 +1367,6 @@
|
||||||
<string name="old_version">old version</string>
|
<string name="old_version">old version</string>
|
||||||
<string name="very_old_version">very old version</string>
|
<string name="very_old_version">very old version</string>
|
||||||
<string name="new_version_warning">New version for at least %1$d days available! Fallback to LGS after 60 days, loop will be disabled after 90 days</string>
|
<string name="new_version_warning">New version for at least %1$d days available! Fallback to LGS after 60 days, loop will be disabled after 90 days</string>
|
||||||
<string name="tidepool">Tidepool</string>
|
|
||||||
<string name="tidepool_shortname">TDP</string>
|
|
||||||
<string name="description_tidepool">Uploads data to Tidepool</string>
|
|
||||||
|
|
||||||
<plurals name="objective_days">
|
<plurals name="objective_days">
|
||||||
<item quantity="one">%1$d day</item>
|
<item quantity="one">%1$d day</item>
|
||||||
|
|
Loading…
Reference in a new issue