From b239100c139d1ad6ba657e708d21837c2d447960 Mon Sep 17 00:00:00 2001 From: TebbeUbben Date: Wed, 20 May 2020 21:49:01 +0200 Subject: [PATCH 01/23] Open Humans Uploader: Initial commit --- app/build.gradle | 19 +- app/src/main/AndroidManifest.xml | 13 + .../info/nightscout/androidaps/MainApp.java | 2 + .../androidaps/db/DatabaseHelper.java | 141 ++++- .../nightscout/androidaps/db/OHQueueItem.kt | 14 + .../info/nightscout/androidaps/logging/L.java | 2 + .../openhumans/AllowedPreferenceKeys.kt | 209 +++++++ .../general/openhumans/OHUploadWorker.kt | 24 + .../general/openhumans/OpenHumansAPI.kt | 183 ++++++ .../general/openhumans/OpenHumansFragment.kt | 23 + .../openhumans/OpenHumansLoginActivity.kt | 158 ++++++ .../general/openhumans/OpenHumansUploader.kt | 526 ++++++++++++++++++ .../info/nightscout/androidaps/utils/SP.java | 2 +- .../res/layout/activity_open_humans_login.xml | 42 ++ .../main/res/layout/fragment_open_humans.xml | 14 + app/src/main/res/values/strings.xml | 22 +- app/src/main/res/xml/pref_openhumans.xml | 18 + 17 files changed, 1403 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/db/OHQueueItem.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/AllowedPreferenceKeys.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OHUploadWorker.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansAPI.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansFragment.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansLoginActivity.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansUploader.kt create mode 100644 app/src/main/res/layout/activity_open_humans_login.xml create mode 100644 app/src/main/res/layout/fragment_open_humans.xml create mode 100644 app/src/main/res/xml/pref_openhumans.xml diff --git a/app/build.gradle b/app/build.gradle index 893f13632c..5a2f94c52d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -302,11 +302,11 @@ dependencies { // new for tidepool - implementation 'com.squareup.okhttp3:okhttp:4.2.2' - implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' - implementation "com.squareup.retrofit2:retrofit:2.6.2" - implementation "com.squareup.retrofit2:adapter-rxjava2:2.6.2" - implementation "com.squareup.retrofit2:converter-gson:2.6.2" + implementation 'com.squareup.okhttp3:okhttp:4.5.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.5.0' + implementation "com.squareup.retrofit2:retrofit:2.8.1" + implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1" + implementation "com.squareup.retrofit2:converter-gson:2.8.1" // Phone checker implementation 'com.scottyab:rootbeer-lib:0.0.7' @@ -316,6 +316,15 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0-alpha03' androidTestImplementation 'com.google.code.findbugs:jsr305:3.0.2' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' + + //WorkManager + implementation 'androidx.work:work-runtime:2.3.4' + implementation 'androidx.work:work-runtime-ktx:2.3.4' + implementation 'androidx.work:work-rxjava2:2.3.4' + + implementation 'com.google.androidbrowserhelper:androidbrowserhelper:1.1.0' + + implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e688720d78..1df838eab5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -282,6 +282,19 @@ android:label="@string/title_activity_rileylink_settings" android:theme="@style/Theme.AppCompat.NoTitle" /> + + + + + + + + + + diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index b1db737e62..e24d50c841 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -54,6 +54,7 @@ import info.nightscout.androidaps.plugins.general.maintenance.LoggerUtils; import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin; import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin; import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader; import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin; import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin; import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin; @@ -224,6 +225,7 @@ public class MainApp extends Application { pluginsList.add(StatuslinePlugin.initPlugin(this)); pluginsList.add(PersistentNotificationPlugin.getPlugin()); pluginsList.add(NSClientPlugin.getPlugin()); + pluginsList.add(OpenHumansUploader.INSTANCE); // if (engineeringMode) pluginsList.add(TidepoolPlugin.INSTANCE); pluginsList.add(MaintenancePlugin.initPlugin(this)); pluginsList.add(AutomationPlugin.INSTANCE); diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index 073f21e708..25c6939bbf 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper; import com.j256.ormlite.dao.CloseableIterator; import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.stmt.DeleteBuilder; import com.j256.ormlite.stmt.PreparedQuery; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.Where; @@ -23,6 +24,7 @@ import org.slf4j.LoggerFactory; import java.sql.SQLException; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.GregorianCalendar; import java.util.List; import java.util.concurrent.Executors; @@ -50,6 +52,7 @@ import info.nightscout.androidaps.interfaces.ProfileInterface; import info.nightscout.androidaps.logging.L; import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader; import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin; import info.nightscout.androidaps.plugins.iob.iobCobCalculator.events.EventNewHistoryData; import info.nightscout.androidaps.plugins.pump.danaR.activities.DanaRNSHistorySync; @@ -86,8 +89,9 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public static final String DATABASE_INSIGHT_HISTORY_OFFSETS = "InsightHistoryOffsets"; public static final String DATABASE_INSIGHT_BOLUS_IDS = "InsightBolusIDs"; public static final String DATABASE_INSIGHT_PUMP_IDS = "InsightPumpIDs"; + public static final String DATABASE_OPEN_HUMANS_QUEUE = "OpenHumansQueue"; - private static final int DATABASE_VERSION = 11; + private static final int DATABASE_VERSION = 12; public static Long earliestDataChange = null; @@ -135,6 +139,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTableIfNotExists(connectionSource, InsightHistoryOffset.class); TableUtils.createTableIfNotExists(connectionSource, InsightBolusID.class); TableUtils.createTableIfNotExists(connectionSource, InsightPumpID.class); + TableUtils.createTableIfNotExists(connectionSource, OHQueueItem.class); database.execSQL("INSERT INTO sqlite_sequence (name, seq) SELECT \"" + DATABASE_INSIGHT_BOLUS_IDS + "\", " + System.currentTimeMillis() + " " + "WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = \"" + DATABASE_INSIGHT_BOLUS_IDS + "\")"); database.execSQL("INSERT INTO sqlite_sequence (name, seq) SELECT \"" + DATABASE_INSIGHT_PUMP_IDS + "\", " + System.currentTimeMillis() + " " + @@ -174,6 +179,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { database.execSQL("UPDATE sqlite_sequence SET seq = " + System.currentTimeMillis() + " WHERE name = \"" + DATABASE_INSIGHT_BOLUS_IDS + "\""); database.execSQL("UPDATE sqlite_sequence SET seq = " + System.currentTimeMillis() + " WHERE name = \"" + DATABASE_INSIGHT_PUMP_IDS + "\""); } + TableUtils.createTableIfNotExists(connectionSource, OHQueueItem.class); } catch (SQLException e) { log.error("Can't drop databases", e); throw new RuntimeException(e); @@ -354,6 +360,10 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return getDao(InsightHistoryOffset.class); } + private Dao getDaoOpenHumansQueue() throws SQLException { + return getDao(OHQueueItem.class); + } + public static long roundDateToSec(long date) { long rounded = date - date % 1000; if (rounded != date) @@ -369,6 +379,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { BgReading old = getDaoBgReadings().queryForId(bgReading.date); if (old == null) { getDaoBgReadings().create(bgReading); + OpenHumansUploader.INSTANCE.queueBGReading(bgReading); if (L.isEnabled(L.DATABASE)) log.debug("BG: New record from: " + from + " " + bgReading.toString()); scheduleBgChange(bgReading); @@ -379,6 +390,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { log.debug("BG: Similiar found: " + old.toString()); old.copyFrom(bgReading); getDaoBgReadings().update(old); + OpenHumansUploader.INSTANCE.queueBGReading(old); if (L.isEnabled(L.DATABASE)) log.debug("BG: Updating record from: " + from + " New data: " + old.toString()); scheduleBgChange(bgReading); @@ -394,6 +406,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { bgReading.date = roundDateToSec(bgReading.date); try { getDaoBgReadings().update(bgReading); + OpenHumansUploader.INSTANCE.queueBGReading(bgReading); } catch (SQLException e) { log.error("Unhandled exception", e); } @@ -503,11 +516,21 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getAllBgReadings() { + try { + return getDaoBgReadings().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + // ------------------- TDD handling ----------------------- public void createOrUpdateTDD(TDD tdd) { try { Dao dao = getDaoTDD(); dao.createOrUpdate(tdd); + OpenHumansUploader.INSTANCE.queueTotalDailyDose(tdd); } catch (SQLException e) { ToastUtils.showToastInUiThread(MainApp.instance(), "createOrUpdate-Exception"); log.error("Unhandled exception", e); @@ -529,6 +552,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return tddList; } + public List getAllTDDs() { + try { + return getDaoTDD().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public List getTDDsForLastXDays(int days) { List tddList; GregorianCalendar gc = new GregorianCalendar(); @@ -636,6 +668,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getAllTempTargets() { + try { + return getDaoTempTargets().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public List getTemptargetsDataFromTime(long from, long to, boolean ascending) { try { Dao daoTempTargets = getDaoTempTargets(); @@ -665,6 +706,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoTempTargets().delete(old); // need to delete/create because date may change too old.copyFrom(tempTarget); getDaoTempTargets().create(old); + OpenHumansUploader.INSTANCE.queueTempTarget(old); if (L.isEnabled(L.DATABASE)) log.debug("TEMPTARGET: Updating record by date from: " + Source.getString(tempTarget.source) + " " + old.toString()); scheduleTemporaryTargetChange(); @@ -685,6 +727,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoTempTargets().delete(old); // need to delete/create because date may change too old.copyFrom(tempTarget); getDaoTempTargets().create(old); + OpenHumansUploader.INSTANCE.queueTempTarget(old); if (L.isEnabled(L.DATABASE)) log.debug("TEMPTARGET: Updating record by _id from: " + Source.getString(tempTarget.source) + " " + old.toString()); scheduleTemporaryTargetChange(); @@ -700,6 +743,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } if (tempTarget.source == Source.USER) { getDaoTempTargets().create(tempTarget); + OpenHumansUploader.INSTANCE.queueTempTarget(tempTarget); if (L.isEnabled(L.DATABASE)) log.debug("TEMPTARGET: New record from: " + Source.getString(tempTarget.source) + " " + tempTarget.toString()); scheduleTemporaryTargetChange(); @@ -714,6 +758,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(TempTarget tempTarget) { try { getDaoTempTargets().delete(tempTarget); + OpenHumansUploader.INSTANCE.queueTempTarget(tempTarget, true); scheduleTemporaryTargetChange(); } catch (SQLException e) { log.error("Unhandled exception", e); @@ -896,6 +941,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { log.debug("TEMPBASAL: Updated record with Pump Data : " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); getDaoTemporaryBasal().update(old); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(old); updateEarliestDataChange(tempBasal.date); scheduleTemporaryBasalChange(); @@ -904,6 +950,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } getDaoTemporaryBasal().create(tempBasal); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(tempBasal); if (L.isEnabled(L.DATABASE)) log.debug("TEMPBASAL: New record from: " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); updateEarliestDataChange(tempBasal.date); @@ -922,6 +969,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoTemporaryBasal().delete(old); // need to delete/create because date may change too old.copyFrom(tempBasal); getDaoTemporaryBasal().create(old); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(old); if (L.isEnabled(L.DATABASE)) log.debug("TEMPBASAL: Updating record by date from: " + Source.getString(tempBasal.source) + " " + old.toString()); updateEarliestDataChange(oldDate); @@ -945,6 +993,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoTemporaryBasal().delete(old); // need to delete/create because date may change too old.copyFrom(tempBasal); getDaoTemporaryBasal().create(old); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(old); if (L.isEnabled(L.DATABASE)) log.debug("TEMPBASAL: Updating record by _id from: " + Source.getString(tempBasal.source) + " " + old.toString()); updateEarliestDataChange(oldDate); @@ -955,6 +1004,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } } getDaoTemporaryBasal().create(tempBasal); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(tempBasal); if (L.isEnabled(L.DATABASE)) log.debug("TEMPBASAL: New record from: " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); updateEarliestDataChange(tempBasal.date); @@ -963,6 +1013,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } if (tempBasal.source == Source.USER) { getDaoTemporaryBasal().create(tempBasal); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(tempBasal); if (L.isEnabled(L.DATABASE)) log.debug("TEMPBASAL: New record from: " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); updateEarliestDataChange(tempBasal.date); @@ -978,6 +1029,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(TemporaryBasal tempBasal) { try { getDaoTemporaryBasal().delete(tempBasal); + OpenHumansUploader.INSTANCE.queueTemporaryBasal(tempBasal, true); updateEarliestDataChange(tempBasal.date); } catch (SQLException e) { log.error("Unhandled exception", e); @@ -985,6 +1037,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { scheduleTemporaryBasalChange(); } + public List getAllTemporaryBasals() { + try { + return getDaoTemporaryBasal().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public List getTemporaryBasalsDataFromTime(long mills, boolean ascending) { try { List tempbasals; @@ -1183,6 +1244,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { // and then is record updated with pumpId if (extendedBolus.pumpId == 0) { getDaoExtendedBolus().createOrUpdate(extendedBolus); + OpenHumansUploader.INSTANCE.queueExtendedBolus(extendedBolus); } else { QueryBuilder queryBuilder = getDaoExtendedBolus().queryBuilder(); Where where = queryBuilder.where(); @@ -1194,6 +1256,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return false; } getDaoExtendedBolus().createOrUpdate(extendedBolus); + OpenHumansUploader.INSTANCE.queueExtendedBolus(extendedBolus); } if (L.isEnabled(L.DATABASE)) log.debug("EXTENDEDBOLUS: New record from: " + Source.getString(extendedBolus.source) + " " + extendedBolus.log()); @@ -1209,6 +1272,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoExtendedBolus().delete(old); // need to delete/create because date may change too old.copyFrom(extendedBolus); getDaoExtendedBolus().create(old); + OpenHumansUploader.INSTANCE.queueExtendedBolus(old); if (L.isEnabled(L.DATABASE)) log.debug("EXTENDEDBOLUS: Updating record by date from: " + Source.getString(extendedBolus.source) + " " + old.log()); updateEarliestDataChange(oldDate); @@ -1232,6 +1296,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoExtendedBolus().delete(old); // need to delete/create because date may change too old.copyFrom(extendedBolus); getDaoExtendedBolus().create(old); + OpenHumansUploader.INSTANCE.queueExtendedBolus(old); if (L.isEnabled(L.DATABASE)) log.debug("EXTENDEDBOLUS: Updating record by _id from: " + Source.getString(extendedBolus.source) + " " + old.log()); updateEarliestDataChange(oldDate); @@ -1242,6 +1307,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } } getDaoExtendedBolus().create(extendedBolus); + OpenHumansUploader.INSTANCE.queueExtendedBolus(extendedBolus); if (L.isEnabled(L.DATABASE)) log.debug("EXTENDEDBOLUS: New record from: " + Source.getString(extendedBolus.source) + " " + extendedBolus.log()); updateEarliestDataChange(extendedBolus.date); @@ -1250,6 +1316,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } if (extendedBolus.source == Source.USER) { getDaoExtendedBolus().create(extendedBolus); + OpenHumansUploader.INSTANCE.queueExtendedBolus(extendedBolus); if (L.isEnabled(L.DATABASE)) log.debug("EXTENDEDBOLUS: New record from: " + Source.getString(extendedBolus.source) + " " + extendedBolus.log()); updateEarliestDataChange(extendedBolus.date); @@ -1262,6 +1329,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return false; } + public List getAllExtendedBoluses() { + try { + return getDaoExtendedBolus().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public ExtendedBolus getExtendedBolusByPumpId(long pumpId) { try { return getDaoExtendedBolus().queryBuilder() @@ -1276,6 +1352,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(ExtendedBolus extendedBolus) { try { getDaoExtendedBolus().delete(extendedBolus); + OpenHumansUploader.INSTANCE.queueExtendedBolus(extendedBolus, true); updateEarliestDataChange(extendedBolus.date); } catch (SQLException e) { log.error("Unhandled exception", e); @@ -1382,6 +1459,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { careportalEvent.date = careportalEvent.date - careportalEvent.date % 1000; try { getDaoCareportalEvents().createOrUpdate(careportalEvent); + OpenHumansUploader.INSTANCE.queueCareportalEvent(careportalEvent); } catch (SQLException e) { log.error("Unhandled exception", e); } @@ -1391,6 +1469,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(CareportalEvent careportalEvent) { try { getDaoCareportalEvents().delete(careportalEvent); + OpenHumansUploader.INSTANCE.queueCareportalEvent(careportalEvent, true); } catch (SQLException e) { log.error("Unhandled exception", e); } @@ -1406,6 +1485,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return null; } + public List getAllCareportalEvents() { + try { + return getDaoCareportalEvents().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + @Nullable public CareportalEvent getLastCareportalEvent(String event) { try { @@ -1608,6 +1696,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList<>(); } + public List getAllProfileSwitches() { + try { + return getDaoProfileSwitch().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + @Nullable private ProfileSwitch getLastProfileSwitchWithoutDuration() { try { @@ -1679,6 +1776,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { profileSwitch.profileName = old.profileName; // preserver profileName to prevent multiple CPP extension getDaoProfileSwitch().delete(old); // need to delete/create because date may change too getDaoProfileSwitch().create(profileSwitch); + OpenHumansUploader.INSTANCE.queueProfileSwitch(profileSwitch); if (L.isEnabled(L.DATABASE)) log.debug("PROFILESWITCH: Updating record by date from: " + Source.getString(profileSwitch.source) + " " + old.toString()); scheduleProfileSwitchChange(); @@ -1699,6 +1797,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoProfileSwitch().delete(old); // need to delete/create because date may change too old.copyFrom(profileSwitch); getDaoProfileSwitch().create(old); + OpenHumansUploader.INSTANCE.queueProfileSwitch(old); if (L.isEnabled(L.DATABASE)) log.debug("PROFILESWITCH: Updating record by _id from: " + Source.getString(profileSwitch.source) + " " + old.toString()); scheduleProfileSwitchChange(); @@ -1709,6 +1808,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { // look for already added percentage from NS profileSwitch.profileName = PercentageSplitter.pureName(profileSwitch.profileName); getDaoProfileSwitch().create(profileSwitch); + OpenHumansUploader.INSTANCE.queueProfileSwitch(profileSwitch); if (L.isEnabled(L.DATABASE)) log.debug("PROFILESWITCH: New record from: " + Source.getString(profileSwitch.source) + " " + profileSwitch.toString()); scheduleProfileSwitchChange(); @@ -1716,6 +1816,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } if (profileSwitch.source == Source.USER) { getDaoProfileSwitch().create(profileSwitch); + OpenHumansUploader.INSTANCE.queueProfileSwitch(profileSwitch); if (L.isEnabled(L.DATABASE)) log.debug("PROFILESWITCH: New record from: " + Source.getString(profileSwitch.source) + " " + profileSwitch.toString()); scheduleProfileSwitchChange(); @@ -1730,6 +1831,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(ProfileSwitch profileSwitch) { try { getDaoProfileSwitch().delete(profileSwitch); + OpenHumansUploader.INSTANCE.queueProfileSwitch(profileSwitch, true); scheduleProfileSwitchChange(); } catch (SQLException e) { log.error("Unhandled exception", e); @@ -1911,5 +2013,40 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return null; } - // ---------------- Food handling --------------- + // ---------------- Open Humans Queue handling --------------- + + public void clearOpenHumansQueue() { + try { + TableUtils.clearTable(connectionSource, OHQueueItem.class); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + } + + public void createOrUpdate(OHQueueItem item) { + try { + getDaoOpenHumansQueue().createOrUpdate(item); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + } + + public void removeAllOHQueueItemsWithIdSmallerThan(long id) { + try { + DeleteBuilder deleteBuilder = getDaoOpenHumansQueue().deleteBuilder(); + deleteBuilder.where().le("id", id); + deleteBuilder.delete(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + } + + public List getAllOHQueueItems() { + try { + return getDaoOpenHumansQueue().queryForAll(); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return Collections.emptyList(); + } } diff --git a/app/src/main/java/info/nightscout/androidaps/db/OHQueueItem.kt b/app/src/main/java/info/nightscout/androidaps/db/OHQueueItem.kt new file mode 100644 index 0000000000..1a99df9a47 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/db/OHQueueItem.kt @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.db + +import com.j256.ormlite.field.DatabaseField +import com.j256.ormlite.table.DatabaseTable + +@DatabaseTable(tableName = DatabaseHelper.DATABASE_OPEN_HUMANS_QUEUE) +data class OHQueueItem @JvmOverloads constructor( + @DatabaseField(generatedId = true) + val id: Long = 0, + @DatabaseField + val file: String = "", + @DatabaseField + val content: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/logging/L.java b/app/src/main/java/info/nightscout/androidaps/logging/L.java index 25b6750844..bc253d6ba2 100644 --- a/app/src/main/java/info/nightscout/androidaps/logging/L.java +++ b/app/src/main/java/info/nightscout/androidaps/logging/L.java @@ -100,6 +100,7 @@ public class L { public static final String UI = "UI"; public static final String LOCATION = "LOCATION"; public static final String SMS = "SMS"; + public static final String OPENHUMANS = "OPENHUMANS"; private static void initialize() { logElements = new ArrayList<>(); @@ -128,6 +129,7 @@ public class L { logElements.add(new LogElement(PUMPQUEUE, true)); logElements.add(new LogElement(SMS, true)); logElements.add(new LogElement(UI, true)); + logElements.add(new LogElement(OPENHUMANS, true)); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/AllowedPreferenceKeys.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/AllowedPreferenceKeys.kt new file mode 100644 index 0000000000..04045becdb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/AllowedPreferenceKeys.kt @@ -0,0 +1,209 @@ +package info.nightscout.androidaps.plugins.general.openhumans + +import java.util.* + +fun String.isAllowedKey() = if (startsWith("ConfigBuilder_")) true else allowedKeys.contains(this.toUpperCase(Locale.ROOT)) + +private val allowedKeys = """ + absorption + absorption_maxtime + openapsama_autosens_period + autosens_max + autosens_min + absorption + openapsama_min_5m_carbimpact + absorption_cutoff + autosens_max + autosens_min + absorption + openapsama_min_5m_carbimpact + absorption_cutoff + autosens_max + autosens_min + age + location + dexcomg5_nsupload + dexcomg5_xdripupload + dexcom_lognssensorchange + danars_bolusspeed + danar_useextended + danar_visualizeextendedaspercentage" + bt_watchdog + danar_useextended + danar_visualizeextendedaspercentage" + bt_watchdog + DanaRProfile + danarprofile_dia + blescannner + danars_bolusspeed + bt_watchdog + danars_bolusspeed + bt_watchdog + enable_fabric + insight_log_reservoir_changes + insight_log_tube_changes + insight_log_site_changes + insight_log_battery_changes + insight_log_operating_mode_changes + insight_log_alerts + insight_enable_tbr_emulation + insight_min_recovery_duration + insight_max_recovery_duration + insight_disconnect_delay + insight_log_reservoir_changes + insight_log_tube_changes + insight_log_site_changes + insight_log_battery_changes + insight_log_operating_mode_changes + insight_log_alerts + insight_enable_tbr_emulation + insight_min_recovery_duration + insight_max_recovery_duration + insight_disconnect_delay + InsulinOrefFreePeak + insulin_oref_peak + language + aps_general + aps_mode + loop_openmode_min_change + maintenance + maintenance_logs_amount + pref_medtronic_pump_type + pref_medtronic_frequency + pref_medtronic_max_basal + pref_medtronic_max_bolus + pref_medtronic_bolus_delay + pref_medtronic_encoding + pref_medtronic_battery_type + pref_medtronic_bolus_debug + ns_logappstartedevent + nsalarm_urgent_high + nsalarm_high + nsalarm_low + nsalarm_urgent_low + nsalarm_staledata + nsalarm_staledatavalue + nsalarm_urgent_staledata + nsalarm_urgent_staledatavalue + ns_wifionly + ns_wifi_ssids + ns_allowroaming + ns_chargingonly + ns_autobackfill + ns_create_announcements_from_errors + nsclient_localbroadcasts + ns_upload_only + ns_noupload + ns_sync_use_absolute + openapsama + openapsma_max_basal + openapsma_max_iob + openapsama_useautosens + autosens_adjust_targets + openapsama_min_5m_carbimpact + always_use_shortavg + openapsama_max_daily_safety_multiplier + openapsama_current_basal_safety_multiplier + bolussnooze_dia_divisor + openaps + openapsma_max_basal + openapsma_max_iob + always_use_shortavg + bolussnooze_dia_divisor + openapssmb + openapsma_max_basal + openapsmb_max_iob + openapsama_useautosens + use_smb + enableSMB_with_COB + enableSMB_with_temptarget + enableSMB_with_high_temptarget + enableSMB_always + enableSMB_after_carbs + smbmaxminutes + use_uam + high_temptarget_raises_sensitivity + low_temptarget_lowers_sensitivity + always_use_shortavg + openapsama_max_daily_safety_multiplier + openapsama_current_basal_safety_multiplier + others + eatingsoon_duration + eatingsoon_target + activity_duration + activity_target + hypo_duration + hypo_target + fill_button1 + fill_button2 + fill_button3 + low_mark + high_mark + short_tabtitles + enable_missed_bg_readings + missed_bg_readings_threshold + enable_pump_unreachable_alert + raise_urgent_alarms_as_android_notification + keep_screen_on + show_treatment_button + show_wizard_button + show_insulin_button + insulin_button_increment_1 + insulin_button_increment_2 + insulin_button_increment_3 + show_carbs_button + carbs_button_increment_1 + carbs_button_increment_2 + carbs_button_increment_3 + show_cgm_button + show_calibration_button + show_notes_entry_dialogs + quickwizard + key_advancedsettings + boluswizard_percentage + key_usersuperbolus + key_show_statuslights + key_show_statuslights_extended + key_statuslights_res_warning + key_statuslights_res_critical + key_statuslights_bat_warning + key_statuslights_bat_critical + dexcomg5_nsupload + dexcomg5_xdripupload + treatmentssafety + treatmentssafety_maxbolus + treatmentssafety_maxcarbs + smscommunicator + smscommunicator_remotecommandsallowed + tidepool_upload_screen + tidepool_upload_cgm + tidepool_upload_bolus + tidepool_upload_bg + tidepool_upload_tbr + tidepool_upload_profile + tidepool_dev_servers + tidepool_only_while_charging + tidepool_only_while_unmetered + virtualpump + virtualpump_uploadstatus + virtualpump_type + wearplugin + wearcontrol + wearplugin + wearwizard_bg + wearwizard_tt + wearwizard_trend + wearwizard_cob + wearwizard_bolusiob + wearwizard_basaliob + wearplugin + wear_detailediob + wear_detailed_delta + wear_showbgi + wear_predictions + wearplugin + wear_notifySMB + xdripstatus + xdripstatus_detailediob + xdripstatus_showbgi +""".trimIndent().split("\n").filterNot { it.isBlank() }.map { it.toUpperCase() } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OHUploadWorker.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OHUploadWorker.kt new file mode 100644 index 0000000000..44f6b70b49 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OHUploadWorker.kt @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.general.openhumans + +import android.content.Context +import android.net.wifi.WifiManager +import androidx.work.RxWorker +import androidx.work.WorkerParameters +import info.nightscout.androidaps.utils.SP +import io.reactivex.Single + +class OHUploadWorker( + val context: Context, + workerParameters: WorkerParameters +) : RxWorker(context, workerParameters) { + + override fun createWork() = Single.defer { + val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + if (SP.getBoolean("key_oh_wifi_only", true) && wifiManager.isWifiEnabled && wifiManager.connectionInfo.networkId != -1) + OpenHumansUploader.uploadData() + .andThen(Single.just(Result.success())) + .onErrorResumeNext { Single.just(Result.retry()) } + else Single.just(Result.retry()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansAPI.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansAPI.kt new file mode 100644 index 0000000000..235ee64174 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansAPI.kt @@ -0,0 +1,183 @@ +package info.nightscout.androidaps.plugins.general.openhumans + +import android.annotation.SuppressLint +import android.util.Base64 +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.disposables.Disposables +import okhttp3.* +import org.json.JSONArray +import org.json.JSONObject +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import okhttp3.RequestBody.Companion +import okio.BufferedSink + +class OpenHumansAPI( + private val baseUrl: String, + clientId: String, + clientSecret: String, + private val redirectUri: String +) { + + private val authHeader = "Basic " + Base64.encodeToString("$clientId:$clientSecret".toByteArray(), Base64.NO_WRAP) + private val client = OkHttpClient() + + fun exchangeAuthToken(code: String) = sendTokenRequest(FormBody.Builder() + .add("grant_type", "authorization_code") + .add("redirect_uri", redirectUri) + .add("code", code) + .build()) + + fun refreshAccessToken(refreshToken: String) = sendTokenRequest(FormBody.Builder() + .add("grant_type", "refresh_token") + .add("redirect_uri", redirectUri) + .add("code", refreshToken) + .build()) + + private fun sendTokenRequest(body: FormBody) = Request.Builder() + .url("$baseUrl/oauth2/token/") + .addHeader("Authorization", authHeader) + .post(body) + .build() + .toSingle() + .map { response -> + response.use { _ -> + val body = response.body + val jsonObject = body?.let { JSONObject(it.string()) } + if (!response.isSuccessful) throw OHHttpException(response.code, response.message, jsonObject?.getString("error")) + if (jsonObject == null) throw OHHttpException(response.code, response.message, "No body") + if (!jsonObject.has("expires_in")) throw OHMissingFieldException("expires_in") + OAuthTokens( + accessToken = jsonObject.getString("access_token") ?: throw OHMissingFieldException("access_token"), + refreshToken = jsonObject.getString("refresh_token") ?: throw OHMissingFieldException("refresh_token"), + expiresAt = response.sentRequestAtMillis + jsonObject.getInt("expires_in") * 1000L + ) + } + } + + fun getProjectMemberId(accessToken: String) = Request.Builder() + .url("$baseUrl/api/direct-sharing/project/exchange-member/?access_token=$accessToken") + .get() + .build() + .toSingle() + .map { it.jsonBody.getString("project_member_id") ?: throw OHMissingFieldException("project_member_id") } + + fun prepareFileUpload(accessToken: String, fileName: String, metadata: FileMetadata) = Request.Builder() + .url("$baseUrl/api/direct-sharing/project/files/upload/direct/?access_token=$accessToken") + .post(FormBody.Builder() + .add("filename", fileName) + .add("metadata", metadata.toJSON().toString()) + .build()) + .build() + .toSingle() + .map { + val json = it.jsonBody + PreparedUpload( + fileId = json.getString("id") ?: throw OHMissingFieldException("id"), + uploadURL = json.getString("url") ?: throw OHMissingFieldException("url") + ) + } + + fun uploadFile(url: String, content: ByteArray) = Request.Builder() + .url(url) + .put(object : RequestBody() { + override fun contentType(): MediaType? = null + + override fun contentLength() = content.size.toLong() + + override fun writeTo(sink: BufferedSink) { + sink.write(content) + } + }) + .build() + .toSingle() + .doOnSuccess { response -> + response.use { _ -> + if (!response.isSuccessful) throw OHHttpException(response.code, response.message, null) + } + } + .ignoreElement() + + fun completeFileUpload(accessToken: String, fileId: String) = Request.Builder() + .url("$baseUrl/api/direct-sharing/project/files/upload/complete/?access_token=$accessToken") + .post(FormBody.Builder() + .add("file_id", fileId) + .build()) + .build() + .toSingle() + .doOnSuccess { it.jsonBody } + .ignoreElement() + + + private fun Request.toSingle() = Single.create { + val call = client.newCall(this) + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + it.tryOnError(e) + } + + override fun onResponse(call: Call, response: Response) { + it.onSuccess(response) + } + }) + it.setDisposable(Disposables.fromRunnable { call.cancel() }) + } + + private val Response.jsonBody get() = use { _ -> + val jsonObject = body?.let { JSONObject(it.string()) } ?: throw OHHttpException(code, message, null) + if (!isSuccessful) throw OHHttpException(code, message, jsonObject.getString("detail")) + jsonObject + } + + data class OAuthTokens( + val accessToken: String, + val refreshToken: String, + val expiresAt: Long + ) + + data class FileMetadata( + val tags: List, + val description: String, + val md5: String? = null, + val creationDate: Long? = null, + val startDate: Long? = null, + val endDate: Long? = null + ) { + fun toJSON(): JSONObject { + val jsonObject = JSONObject() + jsonObject.put("tags", JSONArray().apply { tags.forEach { put(it) } }) + jsonObject.put("description", description) + jsonObject.put("md5", md5) + creationDate?.let { jsonObject.put("creation_date", iso8601DateFormatter.format(Date(it))) } + startDate?.let { jsonObject.put("start_date", iso8601DateFormatter.format(Date(it))) } + endDate?.let { jsonObject.put("end_date", iso8601DateFormatter.format(Date(it))) } + return jsonObject + } + } + + data class PreparedUpload( + val fileId: String, + val uploadURL: String + ) + + data class OHHttpException( + val code: Int, + val meaning: String, + val detail: String? + ) : RuntimeException() { + override val message: String get() = toString() + } + + data class OHMissingFieldException( + val name: String + ) : RuntimeException() { + override val message: String get() = toString() + } + + companion object { + @SuppressLint("SimpleDateFormat") + private val iso8601DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansFragment.kt new file mode 100644 index 0000000000..89ae94a813 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansFragment.kt @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.general.openhumans + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import info.nightscout.androidaps.R + +class OpenHumansFragment : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = inflater.inflate(R.layout.fragment_open_humans, container, false) + val button = view.findViewById