diff --git a/app/build.gradle b/app/build.gradle index c633d46524..203319de43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -352,6 +352,15 @@ dependencies { kapt "com.google.dagger:dagger-compiler:$dagger_version" 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 531e88e83a..96763d8274 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -260,6 +260,19 @@ android:exported="true" /> + + + + + + + + + + diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index e01ea6da78..d7ffdc5358 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -32,6 +32,7 @@ import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin import info.nightscout.androidaps.plugins.general.wear.WearPlugin @@ -58,6 +59,7 @@ import info.nightscout.androidaps.utils.sharedPreferences.SP import javax.inject.Inject class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener, HasAndroidInjector { + private var pluginId = -1 @Inject lateinit var rxBus: RxBusWrapper @@ -98,6 +100,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang @Inject lateinit var passwordCheck: PasswordCheck @Inject lateinit var nsSettingStatus: NSSettingsStatus + @Inject lateinit var openHumansUploader: OpenHumansUploader // TODO why? @Inject lateinit var androidInjector: DispatchingAndroidInjector @@ -183,6 +186,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang addPreferencesFromResource(R.xml.pref_alerts, rootKey) // TODO not organized well addPreferencesFromResource(R.xml.pref_datachoices, rootKey) addPreferencesFromResourceIfEnabled(maintenancePlugin, rootKey) + addPreferencesFromResourceIfEnabled(openHumansUploader, rootKey) } initSummary(preferenceScreen, pluginId != -1) preprocessPreferences() 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 d96fda8d3b..12103067c5 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; @@ -21,6 +22,7 @@ import org.json.JSONObject; 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; @@ -53,6 +55,8 @@ import info.nightscout.androidaps.logging.LTag; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; import info.nightscout.androidaps.plugins.iob.iobCobCalculator.events.EventNewHistoryBgData; +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.insight.database.InsightBolusID; import info.nightscout.androidaps.plugins.pump.insight.database.InsightHistoryOffset; @@ -74,6 +78,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { @Inject AAPSLogger aapsLogger; @Inject RxBusWrapper rxBus; @Inject VirtualPumpPlugin virtualPumpPlugin; + @Inject OpenHumansUploader openHumansUploader; public static final String DATABASE_NAME = "AndroidAPSDb"; public static final String DATABASE_BGREADINGS = "BgReadings"; @@ -87,8 +92,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 = 12; + private static final int DATABASE_VERSION = 13; public static Long earliestDataChange = null; @@ -141,6 +147,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTableIfNotExists(connectionSource, InsightBolusID.class); TableUtils.createTableIfNotExists(connectionSource, InsightPumpID.class); TableUtils.createTableIfNotExists(connectionSource, OmnipodHistoryRecord.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() + " " + @@ -180,6 +187,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) { aapsLogger.error("Can't drop databases", e); throw new RuntimeException(e); @@ -366,6 +374,10 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return getDao(OmnipodHistoryRecord.class); } + private Dao getDaoOpenHumansQueue() throws SQLException { + return getDao(OHQueueItem.class); + } + public long roundDateToSec(long date) { long rounded = date - date % 1000; if (rounded != date) @@ -380,6 +392,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { BgReading old = getDaoBgReadings().queryForId(bgReading.date); if (old == null) { getDaoBgReadings().create(bgReading); + openHumansUploader.enqueueBGReading(bgReading); aapsLogger.debug(LTag.DATABASE, "BG: New record from: " + from + " " + bgReading.toString()); scheduleBgChange(bgReading); return true; @@ -388,6 +401,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { aapsLogger.debug(LTag.DATABASE, "BG: Similiar found: " + old.toString()); old.copyFrom(bgReading); getDaoBgReadings().update(old); + openHumansUploader.enqueueBGReading(old); aapsLogger.debug(LTag.DATABASE, "BG: Updating record from: " + from + " New data: " + old.toString()); scheduleBgHistoryChange(old.date); // trigger cache invalidation return false; @@ -402,6 +416,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { bgReading.date = roundDateToSec(bgReading.date); try { getDaoBgReadings().update(bgReading); + openHumansUploader.enqueueBGReading(bgReading); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); } @@ -496,11 +511,21 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getAllBgReadings() { + try { + return getDaoBgReadings().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + // ------------------- TDD handling ----------------------- public void createOrUpdateTDD(TDD tdd) { try { Dao dao = getDaoTDD(); dao.createOrUpdate(tdd); + openHumansUploader.enqueueTotalDailyDose(tdd); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); } @@ -521,6 +546,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return tddList; } + public List getAllTDDs() { + try { + return getDaoTDD().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public List getTDDsForLastXDays(int days) { List tddList; GregorianCalendar gc = new GregorianCalendar(); @@ -628,6 +662,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getAllTempTargets() { + try { + return getDaoTempTargets().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public List getTemptargetsDataFromTime(long from, long to, boolean ascending) { try { Dao daoTempTargets = getDaoTempTargets(); @@ -657,6 +700,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.enqueueTempTarget(old); aapsLogger.debug(LTag.DATABASE, "TEMPTARGET: Updating record by date from: " + Source.getString(tempTarget.source) + " " + old.toString()); scheduleTemporaryTargetChange(); return true; @@ -676,6 +720,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.enqueueTempTarget(old); aapsLogger.debug(LTag.DATABASE, "TEMPTARGET: Updating record by _id from: " + Source.getString(tempTarget.source) + " " + old.toString()); scheduleTemporaryTargetChange(); return true; @@ -689,6 +734,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } if (tempTarget.source == Source.USER) { getDaoTempTargets().create(tempTarget); + openHumansUploader.enqueueTempTarget(tempTarget); aapsLogger.debug(LTag.DATABASE, "TEMPTARGET: New record from: " + Source.getString(tempTarget.source) + " " + tempTarget.toString()); scheduleTemporaryTargetChange(); return true; @@ -702,6 +748,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(TempTarget tempTarget) { try { getDaoTempTargets().delete(tempTarget); + openHumansUploader.enqueueTempTarget(tempTarget, true); scheduleTemporaryTargetChange(); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); @@ -856,6 +903,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { aapsLogger.debug(LTag.DATABASE, "TEMPBASAL: Updated record with Pump Data : " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); getDaoTemporaryBasal().update(old); + openHumansUploader.enqueueTemporaryBasal(old); updateEarliestDataChange(tempBasal.date); scheduleTemporaryBasalChange(); @@ -864,6 +912,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } getDaoTemporaryBasal().create(tempBasal); + openHumansUploader.enqueueTemporaryBasal(tempBasal); aapsLogger.debug(LTag.DATABASE, "TEMPBASAL: New record from: " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); updateEarliestDataChange(tempBasal.date); scheduleTemporaryBasalChange(); @@ -881,6 +930,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.enqueueTemporaryBasal(old); aapsLogger.debug(LTag.DATABASE, "TEMPBASAL: Updating record by date from: " + Source.getString(tempBasal.source) + " " + old.toString()); updateEarliestDataChange(oldDate); updateEarliestDataChange(old.date); @@ -903,6 +953,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.enqueueTemporaryBasal(old); aapsLogger.debug(LTag.DATABASE, "TEMPBASAL: Updating record by _id from: " + Source.getString(tempBasal.source) + " " + old.toString()); updateEarliestDataChange(oldDate); updateEarliestDataChange(old.date); @@ -912,6 +963,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } } getDaoTemporaryBasal().create(tempBasal); + openHumansUploader.enqueueTemporaryBasal(tempBasal); aapsLogger.debug(LTag.DATABASE, "TEMPBASAL: New record from: " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); updateEarliestDataChange(tempBasal.date); scheduleTemporaryBasalChange(); @@ -919,6 +971,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } if (tempBasal.source == Source.USER) { getDaoTemporaryBasal().create(tempBasal); + openHumansUploader.enqueueTemporaryBasal(tempBasal); aapsLogger.debug(LTag.DATABASE, "TEMPBASAL: New record from: " + Source.getString(tempBasal.source) + " " + tempBasal.toString()); updateEarliestDataChange(tempBasal.date); scheduleTemporaryBasalChange(); @@ -933,6 +986,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(TemporaryBasal tempBasal) { try { getDaoTemporaryBasal().delete(tempBasal); + openHumansUploader.enqueueTemporaryBasal(tempBasal, true); updateEarliestDataChange(tempBasal.date); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); @@ -940,6 +994,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { scheduleTemporaryBasalChange(); } + public List getAllTemporaryBasals() { + try { + return getDaoTemporaryBasal().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public List getTemporaryBasalsDataFromTime(long mills, boolean ascending) { try { List tempbasals; @@ -1135,6 +1198,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { // and then is record updated with pumpId if (extendedBolus.pumpId == 0) { getDaoExtendedBolus().createOrUpdate(extendedBolus); + openHumansUploader.enqueueExtendedBolus(extendedBolus); } else { QueryBuilder queryBuilder = getDaoExtendedBolus().queryBuilder(); Where where = queryBuilder.where(); @@ -1146,6 +1210,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return false; } getDaoExtendedBolus().createOrUpdate(extendedBolus); + openHumansUploader.enqueueExtendedBolus(extendedBolus); } aapsLogger.debug(LTag.DATABASE, "EXTENDEDBOLUS: New record from: " + Source.getString(extendedBolus.source) + " " + extendedBolus.log()); updateEarliestDataChange(extendedBolus.date); @@ -1161,6 +1226,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { old.copyFrom(extendedBolus); getDaoExtendedBolus().create(old); aapsLogger.debug(LTag.DATABASE, "EXTENDEDBOLUS: Updating record by date from: " + Source.getString(extendedBolus.source) + " " + old.log()); + openHumansUploader.enqueueExtendedBolus(old); updateEarliestDataChange(oldDate); updateEarliestDataChange(old.date); scheduleExtendedBolusChange(); @@ -1183,6 +1249,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { old.copyFrom(extendedBolus); getDaoExtendedBolus().create(old); aapsLogger.debug(LTag.DATABASE, "EXTENDEDBOLUS: Updating record by _id from: " + Source.getString(extendedBolus.source) + " " + old.log()); + openHumansUploader.enqueueExtendedBolus(old); updateEarliestDataChange(oldDate); updateEarliestDataChange(old.date); scheduleExtendedBolusChange(); @@ -1192,6 +1259,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } getDaoExtendedBolus().create(extendedBolus); aapsLogger.debug(LTag.DATABASE, "EXTENDEDBOLUS: New record from: " + Source.getString(extendedBolus.source) + " " + extendedBolus.log()); + openHumansUploader.enqueueExtendedBolus(extendedBolus); updateEarliestDataChange(extendedBolus.date); scheduleExtendedBolusChange(); return true; @@ -1199,6 +1267,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { if (extendedBolus.source == Source.USER) { getDaoExtendedBolus().create(extendedBolus); aapsLogger.debug(LTag.DATABASE, "EXTENDEDBOLUS: New record from: " + Source.getString(extendedBolus.source) + " " + extendedBolus.log()); + openHumansUploader.enqueueExtendedBolus(extendedBolus); updateEarliestDataChange(extendedBolus.date); scheduleExtendedBolusChange(); return true; @@ -1209,6 +1278,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return false; } + public List getAllExtendedBoluses() { + try { + return getDaoExtendedBolus().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + public ExtendedBolus getExtendedBolusByPumpId(long pumpId) { try { return getDaoExtendedBolus().queryBuilder() @@ -1223,6 +1301,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(ExtendedBolus extendedBolus) { try { getDaoExtendedBolus().delete(extendedBolus); + openHumansUploader.enqueueExtendedBolus(extendedBolus, true); updateEarliestDataChange(extendedBolus.date); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); @@ -1343,6 +1422,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { careportalEvent.date = careportalEvent.date - careportalEvent.date % 1000; try { getDaoCareportalEvents().createOrUpdate(careportalEvent); + openHumansUploader.enqueueCareportalEvent(careportalEvent); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); } @@ -1352,6 +1432,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(CareportalEvent careportalEvent) { try { getDaoCareportalEvents().delete(careportalEvent); + openHumansUploader.enqueueCareportalEvent(careportalEvent, true); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); } @@ -1367,6 +1448,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return null; } + public List getAllCareportalEvents() { + try { + return getDaoCareportalEvents().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + @Nullable public CareportalEvent getLastCareportalEvent(String event) { try { @@ -1571,6 +1661,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return false; } + public List getAllProfileSwitches() { + try { + return getDaoProfileSwitch().queryForAll(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } @Nullable private ProfileSwitch getLastProfileSwitchWithoutDuration() { try { @@ -1643,6 +1741,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getDaoProfileSwitch().delete(old); // need to delete/create because date may change too getDaoProfileSwitch().create(profileSwitch); aapsLogger.debug(LTag.DATABASE, "PROFILESWITCH: Updating record by date from: " + Source.getString(profileSwitch.source) + " " + old.toString()); + openHumansUploader.enqueueProfileSwitch(profileSwitch); scheduleProfileSwitchChange(); return true; } @@ -1662,6 +1761,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { old.copyFrom(profileSwitch); getDaoProfileSwitch().create(old); aapsLogger.debug(LTag.DATABASE, "PROFILESWITCH: Updating record by _id from: " + Source.getString(profileSwitch.source) + " " + old.toString()); + openHumansUploader.enqueueProfileSwitch(old); scheduleProfileSwitchChange(); return true; } @@ -1671,12 +1771,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { profileSwitch.profileName = PercentageSplitter.pureName(profileSwitch.profileName); getDaoProfileSwitch().create(profileSwitch); aapsLogger.debug(LTag.DATABASE, "PROFILESWITCH: New record from: " + Source.getString(profileSwitch.source) + " " + profileSwitch.toString()); + openHumansUploader.enqueueProfileSwitch(profileSwitch); scheduleProfileSwitchChange(); return true; } if (profileSwitch.source == Source.USER) { getDaoProfileSwitch().create(profileSwitch); aapsLogger.debug(LTag.DATABASE, "PROFILESWITCH: New record from: " + Source.getString(profileSwitch.source) + " " + profileSwitch.toString()); + openHumansUploader.enqueueProfileSwitch(profileSwitch); scheduleProfileSwitchChange(); return true; } @@ -1689,6 +1791,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public void delete(ProfileSwitch profileSwitch) { try { getDaoProfileSwitch().delete(profileSwitch); + openHumansUploader.enqueueProfileSwitch(profileSwitch, true); scheduleProfileSwitchChange(); } catch (SQLException e) { aapsLogger.error("Unhandled exception", e); @@ -1938,4 +2041,69 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return arrow; } + // ---------------- Open Humans Queue handling --------------- + + public void clearOpenHumansQueue() { + try { + TableUtils.clearTable(connectionSource, OHQueueItem.class); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + } + + public void createOrUpdate(OHQueueItem item) { + try { + getDaoOpenHumansQueue().createOrUpdate(item); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + } + + public void removeAllOHQueueItemsWithIdSmallerThan(long id) { + try { + DeleteBuilder deleteBuilder = getDaoOpenHumansQueue().deleteBuilder(); + deleteBuilder.where().le("id", id); + deleteBuilder.delete(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + } + + public List getAllOHQueueItems(Long maxEntries) { + try { + return getDaoOpenHumansQueue() + .queryBuilder() + .orderBy("id", true) + .limit(maxEntries) + .query(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return Collections.emptyList(); + } + + public long getOHQueueSize() { + try { + return getDaoOpenHumansQueue().countOf(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return 0L; + } + + public long getCountOfAllRows() { + try { + return getDaoBgReadings().countOf() + + getDaoCareportalEvents().countOf() + + getDaoExtendedBolus().countOf() + + getDaoCareportalEvents().countOf() + + getDaoProfileSwitch().countOf() + + getDaoTDD().countOf() + + getDaoTemporaryBasal().countOf() + + getDaoTempTargets().countOf(); + } catch (SQLException e) { + aapsLogger.error("Unhandled exception", e); + } + return 0L; + } } 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/dependencyInjection/ActivitiesModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt index 3c2e89b348..8edce1ece4 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt @@ -7,6 +7,7 @@ import info.nightscout.androidaps.activities.* import info.nightscout.androidaps.historyBrowser.HistoryBrowseActivity import info.nightscout.androidaps.plugins.general.maintenance.activities.LogSettingActivity import info.nightscout.androidaps.plugins.general.maintenance.activities.PrefImportListActivity +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansLoginActivity import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity import info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity import info.nightscout.androidaps.plugins.pump.common.dialog.RileyLinkBLEScanActivity @@ -40,4 +41,6 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesSurveyActivity(): SurveyActivity @ContributesAndroidInjector abstract fun contributesDefaultProfileActivity(): ProfileHelperActivity @ContributesAndroidInjector abstract fun contributesPrefImportListActivity(): PrefImportListActivity + @ContributesAndroidInjector abstract fun contributesOpenHumansLoginActivity(): OpenHumansLoginActivity + } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt index a748e57eb1..9edf530e56 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt @@ -37,7 +37,8 @@ import javax.inject.Singleton CoreModule::class, DanaModule::class, DanaRModule::class, - DanaRSModule::class + DanaRSModule::class, + OHUploaderModule::class ] ) interface AppComponent : AndroidInjector { diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt index 07c8cd1495..0af077c632 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt @@ -20,6 +20,8 @@ import info.nightscout.androidaps.plugins.general.automation.dialogs.EditTrigger import info.nightscout.androidaps.plugins.general.food.FoodFragment import info.nightscout.androidaps.plugins.general.maintenance.MaintenanceFragment import info.nightscout.androidaps.plugins.general.nsclient.NSClientFragment +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansFragment +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansLoginActivity import info.nightscout.androidaps.plugins.general.overview.OverviewFragment import info.nightscout.androidaps.plugins.general.overview.dialogs.EditQuickWizardDialog import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorFragment @@ -67,20 +69,29 @@ abstract class FragmentsModule { @ContributesAndroidInjector abstract fun contributesMedtronicFragment(): MedtronicFragment @ContributesAndroidInjector abstract fun contributesNSProfileFragment(): NSProfileFragment @ContributesAndroidInjector abstract fun contributesNSClientFragment(): NSClientFragment - @ContributesAndroidInjector abstract fun contributesSmsCommunicatorFragment(): SmsCommunicatorFragment + @ContributesAndroidInjector + abstract fun contributesSmsCommunicatorFragment(): SmsCommunicatorFragment @ContributesAndroidInjector abstract fun contributesWearFragment(): WearFragment @ContributesAndroidInjector abstract fun contributesTidepoolFragment(): TidepoolFragment @ContributesAndroidInjector abstract fun contributesTreatmentsFragment(): TreatmentsFragment - @ContributesAndroidInjector abstract fun contributesTreatmentsBolusFragment(): TreatmentsBolusFragment - @ContributesAndroidInjector abstract fun contributesTreatmentsTemporaryBasalsFragment(): TreatmentsTemporaryBasalsFragment - @ContributesAndroidInjector abstract fun contributesTreatmentsTempTargetFragment(): TreatmentsTempTargetFragment - @ContributesAndroidInjector abstract fun contributesTreatmentsExtendedBolusesFragment(): TreatmentsExtendedBolusesFragment - @ContributesAndroidInjector abstract fun contributesTreatmentsCareportalFragment(): TreatmentsCareportalFragment - @ContributesAndroidInjector abstract fun contributesTreatmentsProfileSwitchFragment(): TreatmentsProfileSwitchFragment + @ContributesAndroidInjector + abstract fun contributesTreatmentsBolusFragment(): TreatmentsBolusFragment + @ContributesAndroidInjector + abstract fun contributesTreatmentsTemporaryBasalsFragment(): TreatmentsTemporaryBasalsFragment + @ContributesAndroidInjector + abstract fun contributesTreatmentsTempTargetFragment(): TreatmentsTempTargetFragment + @ContributesAndroidInjector + abstract fun contributesTreatmentsExtendedBolusesFragment(): TreatmentsExtendedBolusesFragment + @ContributesAndroidInjector + abstract fun contributesTreatmentsCareportalFragment(): TreatmentsCareportalFragment + @ContributesAndroidInjector + abstract fun contributesTreatmentsProfileSwitchFragment(): TreatmentsProfileSwitchFragment @ContributesAndroidInjector abstract fun contributesVirtualPumpFragment(): VirtualPumpFragment + @ContributesAndroidInjector abstract fun contributesOpenHumansFragment(): OpenHumansFragment + @ContributesAndroidInjector abstract fun contributesCalibrationDialog(): CalibrationDialog @ContributesAndroidInjector abstract fun contributesCarbsDialog(): CarbsDialog @ContributesAndroidInjector abstract fun contributesCareDialog(): CareDialog @@ -104,9 +115,15 @@ abstract class FragmentsModule { @ContributesAndroidInjector abstract fun contributesWizardDialog(): WizardDialog @ContributesAndroidInjector abstract fun contributesWizardInfoDialog(): WizardInfoDialog + @ContributesAndroidInjector + abstract fun contributesExchangeAuthTokenDialot(): OpenHumansLoginActivity.ExchangeAuthTokenDialog + @ContributesAndroidInjector abstract fun contributesPasswordCheck(): PasswordCheck - @ContributesAndroidInjector abstract fun contributesRileyLinkStatusGeneral(): RileyLinkStatusGeneralFragment - @ContributesAndroidInjector abstract fun contributesRileyLinkStatusHistoryFragment(): RileyLinkStatusHistoryFragment - @ContributesAndroidInjector abstract fun contributesRileyLinkStatusDeviceMedtronic(): RileyLinkStatusDeviceMedtronic + @ContributesAndroidInjector + abstract fun contributesRileyLinkStatusGeneral(): RileyLinkStatusGeneralFragment + @ContributesAndroidInjector + abstract fun contributesRileyLinkStatusHistoryFragment(): RileyLinkStatusHistoryFragment + @ContributesAndroidInjector + abstract fun contributesRileyLinkStatusDeviceMedtronic(): RileyLinkStatusDeviceMedtronic } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/OHUploaderModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/OHUploaderModule.kt new file mode 100644 index 0000000000..bbf0765f96 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/OHUploaderModule.kt @@ -0,0 +1,12 @@ +package info.nightscout.androidaps.dependencyInjection + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import info.nightscout.androidaps.plugins.general.openhumans.OHUploadWorker + +@Module +@Suppress("unused") +abstract class OHUploaderModule { + + @ContributesAndroidInjector abstract fun contributesOHUploadWorkerInjector(): OHUploadWorker +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt index c7033eec2d..75c834977a 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt @@ -25,6 +25,7 @@ import info.nightscout.androidaps.plugins.general.dataBroadcaster.DataBroadcastP import info.nightscout.androidaps.plugins.general.food.FoodPlugin import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin +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 @@ -334,9 +335,15 @@ abstract class PluginsModule { abstract fun bindRandomBgPlugin(plugin: RandomBgPlugin): PluginBase @Binds - @AllConfigs + @NotNSClient @IntoMap @IntKey(480) + abstract fun bindOpenHumansPlugin(plugin: OpenHumansUploader): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(490) abstract fun bindConfigBuilderPlugin(plugin: ConfigBuilderPlugin): PluginBase @Qualifier diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.java index c6b5afbdeb..a5b4fdff9b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import javax.annotation.Nullable; import javax.inject.Inject; +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader; import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.Constants; import info.nightscout.androidaps.R; @@ -45,6 +46,7 @@ public class DetermineBasalAdapterAMAJS { @Inject SP sp; @Inject ProfileFunction profileFunction; @Inject TreatmentsPlugin treatmentsPlugin; + @Inject OpenHumansUploader openHumansUploader; private ScriptReader mScriptReader; @@ -132,7 +134,9 @@ public class DetermineBasalAdapterAMAJS { String result = NativeJSON.stringify(rhino, scope, jsResult, null, null).toString(); aapsLogger.debug(LTag.APS, "Result: " + result); try { - determineBasalResultAMA = new DetermineBasalResultAMA(injector, jsResult, new JSONObject(result)); + JSONObject resultJson = new JSONObject(result); + openHumansUploader.enqueueAMAData(mProfile, mGlucoseStatus, mIobData, mMealData, mCurrentTemp, mAutosensData, resultJson); + determineBasalResultAMA = new DetermineBasalResultAMA(injector, jsResult, resultJson); } catch (JSONException e) { aapsLogger.error(LTag.APS, "Unhandled exception", e); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.java index 35ba0d3abe..058ae596bd 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import javax.annotation.Nullable; import javax.inject.Inject; +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader; import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.Constants; import info.nightscout.androidaps.R; @@ -51,6 +52,7 @@ public class DetermineBasalAdapterSMBJS { @Inject ProfileFunction profileFunction; @Inject TreatmentsPlugin treatmentsPlugin; @Inject ActivePluginProvider activePluginProvider; + @Inject OpenHumansUploader openHumansUploader; private ScriptReader mScriptReader; @@ -160,7 +162,9 @@ public class DetermineBasalAdapterSMBJS { String result = NativeJSON.stringify(rhino, scope, jsResult, null, null).toString(); aapsLogger.debug(LTag.APS, "Result: " + result); try { - determineBasalResultSMB = new DetermineBasalResultSMB(injector, new JSONObject(result)); + JSONObject resultJson = new JSONObject(result); + openHumansUploader.enqueueSMBData(mProfile, mGlucoseStatus, mIobData, mMealData, mCurrentTemp, mAutosensData, mMicrobolusAllowed, mSMBAlwaysAllowed, resultJson); + determineBasalResultSMB = new DetermineBasalResultSMB(injector, resultJson); } catch (JSONException e) { aapsLogger.error(LTag.APS, "Unhandled exception", e); } 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..7ce4d784e2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OHUploadWorker.kt @@ -0,0 +1,67 @@ +package info.nightscout.androidaps.plugins.general.openhumans + +import android.app.Notification +import android.content.Context +import android.net.wifi.WifiManager +import androidx.core.app.NotificationCompat +import androidx.work.ForegroundInfo +import androidx.work.RxWorker +import androidx.work.WorkerParameters +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader.Companion.NOTIFICATION_CHANNEL +import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader.Companion.UPLOAD_NOTIFICATION_ID +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import io.reactivex.Single +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class OHUploadWorker(context: Context, workerParameters: WorkerParameters) + : RxWorker(context, workerParameters) { + + @Inject + lateinit var sp: SP + + @Inject + lateinit var openHumansUploader: OpenHumansUploader + + @Inject + lateinit var resourceHelper: ResourceHelper + + override fun createWork(): Single = Single.defer { + + // Here we inject every time we create work + // We could build our own WorkerFactory with dagger but this will create conflicts with other Workers + // (see https://medium.com/wonderquill/how-to-pass-custom-parameters-to-rxworker-worker-using-dagger-2-f4cfbc9892ba) + // This class will be replaced with new DB + + (applicationContext as MainApp).androidInjector().inject(this) + + val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager + val wifiOnly = sp.getBoolean("key_oh_wifi_only", true) + val isConnectedToWifi = wifiManager?.isWifiEnabled ?: false && wifiManager?.connectionInfo?.networkId != -1 + if (!wifiOnly || (wifiOnly && isConnectedToWifi)) { + setForegroundAsync(createForegroundInfo()) + openHumansUploader.uploadDataSegmentally() + .andThen(Single.just(Result.success())) + .onErrorResumeNext { Single.just(Result.retry()) } + } else { + Single.just(Result.retry()) + } + } + + private fun createForegroundInfo(): ForegroundInfo { + val title = resourceHelper.gs(info.nightscout.androidaps.R.string.open_humans) + + val notification: Notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL) + .setContentTitle(title) + .setTicker(title) + .setContentText(resourceHelper.gs(info.nightscout.androidaps.R.string.your_phone_is_upload_data)) + .setSmallIcon(info.nightscout.androidaps.R.drawable.notif_icon) + .setOngoing(true) + .setProgress(0, 0 , true) + .build() + return ForegroundInfo(UPLOAD_NOTIFICATION_ID, notification) + } + +} \ 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..70dc24f533 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansAPI.kt @@ -0,0 +1,191 @@ +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 okio.BufferedSink +import org.json.JSONArray +import org.json.JSONObject +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +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): Single = sendTokenRequest(FormBody.Builder() + .add("grant_type", "authorization_code") + .add("redirect_uri", redirectUri) + .add("code", code) + .build()) + + fun refreshAccessToken(refreshToken: String): Single = sendTokenRequest(FormBody.Builder() + .add("grant_type", "refresh_token") + .add("redirect_uri", redirectUri) + .add("refresh_token", 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): Single = 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): Single = 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): Completable = 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): Completable = 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..e22130d998 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansFragment.kt @@ -0,0 +1,120 @@ +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 android.widget.TextView +import androidx.lifecycle.Observer +import androidx.work.WorkInfo +import androidx.work.WorkManager +import dagger.android.support.DaggerFragment +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.events.Event +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.utils.alertDialogs.OKDialog +import info.nightscout.androidaps.utils.extensions.plusAssign +import info.nightscout.androidaps.utils.resources.ResourceHelper +import io.reactivex.BackpressureStrategy +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class OpenHumansFragment : DaggerFragment() { + + private var viewsCreated = false + private var login: Button? = null + private var logout: Button? = null + private var memberId: TextView? = null + private var queueSize: TextView? = null + private var workerState: TextView? = null + private var queueSizeValue = 0L + private val compositeDisposable = CompositeDisposable() + + @Inject + lateinit var rxBus: RxBusWrapper + + @Inject + lateinit var openHumansUploader: OpenHumansUploader + + @Inject + lateinit var resourceHelper: ResourceHelper + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + compositeDisposable += Single.fromCallable { MainApp.getDbHelper().ohQueueSize } + .subscribeOn(Schedulers.io()) + .repeatWhen { rxBus.toObservable(UpdateViewEvent::class.java) + .cast(Any::class.java) + .mergeWith(rxBus.toObservable(UpdateQueueEvent::class.java) + .throttleLatest(5, TimeUnit.SECONDS)) + .toFlowable(BackpressureStrategy.LATEST) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + queueSizeValue = it + updateGUI() + }, {}) + context?.applicationContext?.let { appContext -> + WorkManager.getInstance(appContext).getWorkInfosForUniqueWorkLiveData(OpenHumansUploader.WORK_NAME).observe(this, Observer> { + val workInfo = it.lastOrNull() + if (workInfo == null) { + workerState?.visibility = View.GONE + } else { + workerState?.visibility = View.VISIBLE + workerState?.text = getString(R.string.worker_state, workInfo.state.toString()) + } + }) + } + } + + override fun onDestroy() { + compositeDisposable.clear() + super.onDestroy() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = inflater.inflate(R.layout.fragment_open_humans, container, false) + login = view.findViewById(R.id.login) + logout = view.findViewById(R.id.logout) + memberId = view.findViewById(R.id.member_id) + queueSize = view.findViewById(R.id.queue_size) + workerState = view.findViewById(R.id.worker_state) + login!!.setOnClickListener { startActivity(Intent(context, OpenHumansLoginActivity::class.java)) } + logout!!.setOnClickListener { + activity?.let { activity -> OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.oh_logout_confirmation), Runnable { openHumansUploader.logout() }) } + } + viewsCreated = true + updateGUI() + return view + } + + override fun onDestroyView() { + viewsCreated = false + login = null + logout = null + memberId = null + queueSize = null + super.onDestroyView() + } + + fun updateGUI() { + if (viewsCreated) { + queueSize!!.text = getString(R.string.queue_size, queueSizeValue) + val projectMemberId = openHumansUploader.projectMemberId + memberId!!.text = getString(R.string.project_member_id, projectMemberId + ?: getString(R.string.not_logged_in)) + login!!.visibility = if (projectMemberId == null) View.VISIBLE else View.GONE + logout!!.visibility = if (projectMemberId != null) View.VISIBLE else View.GONE + } + } + + object UpdateViewEvent : Event() + + object UpdateQueueEvent : Event() +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansLoginActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansLoginActivity.kt new file mode 100644 index 0000000000..9220457bbb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/openhumans/OpenHumansLoginActivity.kt @@ -0,0 +1,138 @@ +package info.nightscout.androidaps.plugins.general.openhumans + +import android.app.Activity +import android.app.Dialog +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Button +import android.widget.CheckBox +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.DialogFragment +import dagger.android.support.DaggerDialogFragment +import info.nightscout.androidaps.R +import info.nightscout.androidaps.activities.NoSplashAppCompatActivity +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class OpenHumansLoginActivity : NoSplashAppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_open_humans_login) + val button = findViewById