Open Humans Uploader: Initial commit
This commit is contained in:
parent
7970661221
commit
b239100c13
17 changed files with 1403 additions and 9 deletions
|
@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -282,6 +282,19 @@
|
|||
android:label="@string/title_activity_rileylink_settings"
|
||||
android:theme="@style/Theme.AppCompat.NoTitle" />
|
||||
<activity android:name=".plugins.pump.medtronic.dialog.MedtronicHistoryActivity" />
|
||||
<activity android:name=".plugins.general.openhumans.OpenHumansLoginActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="setup-openhumans"
|
||||
android:scheme="androidaps" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<OHQueueItem, Long> 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<BgReading>();
|
||||
}
|
||||
|
||||
public List<BgReading> 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<TDD, String> 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<TDD> getAllTDDs() {
|
||||
try {
|
||||
return getDaoTDD().queryForAll();
|
||||
} catch (SQLException e) {
|
||||
log.error("Unhandled exception", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<TDD> getTDDsForLastXDays(int days) {
|
||||
List<TDD> tddList;
|
||||
GregorianCalendar gc = new GregorianCalendar();
|
||||
|
@ -636,6 +668,15 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
|
|||
return new ArrayList<TempTarget>();
|
||||
}
|
||||
|
||||
public List<TempTarget> getAllTempTargets() {
|
||||
try {
|
||||
return getDaoTempTargets().queryForAll();
|
||||
} catch (SQLException e) {
|
||||
log.error("Unhandled exception", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<TempTarget> getTemptargetsDataFromTime(long from, long to, boolean ascending) {
|
||||
try {
|
||||
Dao<TempTarget, Long> 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<TemporaryBasal> getAllTemporaryBasals() {
|
||||
try {
|
||||
return getDaoTemporaryBasal().queryForAll();
|
||||
} catch (SQLException e) {
|
||||
log.error("Unhandled exception", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<TemporaryBasal> getTemporaryBasalsDataFromTime(long mills, boolean ascending) {
|
||||
try {
|
||||
List<TemporaryBasal> 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<ExtendedBolus, Long> 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<ExtendedBolus> 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<CareportalEvent> 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<ProfileSwitch> 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<OHQueueItem, Long> deleteBuilder = getDaoOpenHumansQueue().deleteBuilder();
|
||||
deleteBuilder.where().le("id", id);
|
||||
deleteBuilder.delete();
|
||||
} catch (SQLException e) {
|
||||
log.error("Unhandled exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<OHQueueItem> getAllOHQueueItems() {
|
||||
try {
|
||||
return getDaoOpenHumansQueue().queryForAll();
|
||||
} catch (SQLException e) {
|
||||
log.error("Unhandled exception", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = ""
|
||||
)
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() }
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Response> {
|
||||
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<String>,
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -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<Button>(R.id.login)
|
||||
button.setOnClickListener {
|
||||
startActivity(Intent(context, OpenHumansLoginActivity::class.java))
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ComponentName
|
||||
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.CustomTabsCallback
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.activities.NoSplashAppCompatActivity
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class OpenHumansLoginActivity : NoSplashAppCompatActivity() {
|
||||
|
||||
private lateinit var customTabsClient: CustomTabsClient
|
||||
private lateinit var customTabsSession: CustomTabsSession
|
||||
|
||||
private val connection = object : CustomTabsServiceConnection() {
|
||||
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
|
||||
customTabsClient = client
|
||||
customTabsClient.warmup(0)
|
||||
customTabsSession = customTabsClient.newSession(CustomTabsCallback())!!
|
||||
customTabsSession.mayLaunchUrl(Uri.parse(OpenHumansUploader.AUTH_URL), null, null)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
CustomTabsClient.bindCustomTabsService(this, "com.android.chrome", connection)
|
||||
setContentView(R.layout.activity_open_humans_login)
|
||||
val button = findViewById<Button>(R.id.button)
|
||||
val checkbox = findViewById<CheckBox>(R.id.checkbox)
|
||||
|
||||
button.setOnClickListener { _ ->
|
||||
if (checkbox.isChecked) {
|
||||
CustomTabsIntent.Builder().setSession(customTabsSession).build().launchUrl(this, Uri.parse(OpenHumansUploader.AUTH_URL))
|
||||
} else {
|
||||
Toast.makeText(this, R.string.you_need_to_accept_the_of_use_first, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (supportFragmentManager.fragments.size == 0 && code != null) {
|
||||
ExchangeAuthTokenDialog(code).show(supportFragmentManager, "ExchangeAuthTokenDialog")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExchangeAuthTokenDialog : DialogFragment() {
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
init {
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.completing_login)
|
||||
.setMessage(R.string.please_wait)
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
disposable = OpenHumansUploader.login(arguments?.getString("authToken")!!).subscribeOn(Schedulers.io()).subscribe({
|
||||
dismiss()
|
||||
SetupDoneDialog().show(fragmentManager!!, "SetupDoneDialog")
|
||||
}, {
|
||||
dismiss()
|
||||
ErrorDialog(it.message).show(fragmentManager!!, "ErrorDialog")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
disposable?.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
operator fun invoke(authToken: String): ExchangeAuthTokenDialog {
|
||||
val dialog = ExchangeAuthTokenDialog()
|
||||
val args = Bundle()
|
||||
args.putString("authToken", authToken)
|
||||
dialog.arguments = args
|
||||
return dialog
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ErrorDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val message = arguments?.getString("message")
|
||||
val shownMessage = if (message == null) getString(R.string.there_was_an_error)
|
||||
else "${getString(R.string.there_was_an_error)}\n\n$message"
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(shownMessage)
|
||||
.setPositiveButton(R.string.close, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
operator fun invoke(message: String?): ErrorDialog {
|
||||
val dialog = ErrorDialog()
|
||||
val args = Bundle()
|
||||
args.putString("message", message)
|
||||
dialog.arguments = args
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SetupDoneDialog : DialogFragment() {
|
||||
|
||||
init {
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.successfully_logged_in)
|
||||
.setMessage(R.string.setup_will_continue_in_background)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.close) { _, _ ->
|
||||
activity!!.run {
|
||||
setResult(Activity.RESULT_OK)
|
||||
activity!!.finish()
|
||||
}
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,526 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.*
|
||||
import info.nightscout.androidaps.BuildConfig
|
||||
import info.nightscout.androidaps.MainApp
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.db.*
|
||||
import info.nightscout.androidaps.interfaces.PluginBase
|
||||
import info.nightscout.androidaps.interfaces.PluginDescription
|
||||
import info.nightscout.androidaps.interfaces.PluginType
|
||||
import info.nightscout.androidaps.logging.L
|
||||
import info.nightscout.androidaps.utils.SP
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object OpenHumansUploader : PluginBase(
|
||||
PluginDescription()
|
||||
.mainType(PluginType.GENERAL)
|
||||
.pluginName(R.string.open_humans)
|
||||
.shortName(R.string.open_humans_short)
|
||||
.description(R.string.donate_your_data_to_science)
|
||||
.fragmentClass(OpenHumansFragment::class.qualifiedName)
|
||||
.preferencesId(R.xml.pref_openhumans)
|
||||
), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val log = LoggerFactory.getLogger(L.OPENHUMANS)
|
||||
|
||||
const val OPEN_HUMANS_URL = "https://www.openhumans.org"
|
||||
const val CLIENT_ID = "oie6DvnaEOagTxSoD6BukkLPwDhVr6cMlN74Ihz1"
|
||||
const val CLIENT_SECRET = "jR0N8pkH1jOwtozHc7CsB1UPcJzFN95ldHcK4VGYIApecr8zGJox0v06xLwPLMASScngT12aIaIHXAVCJeKquEXAWG1XekZdbubSpccgNiQBmuVmIF8nc1xSKSNJltCf"
|
||||
const val REDIRECT_URL = "androidaps://setup-openhumans"
|
||||
const val AUTH_URL = "https://www.openhumans.org/direct-sharing/projects/oauth2/authorize/?client_id=$CLIENT_ID&response_type=code"
|
||||
const val WORK_NAME = "Open Humans"
|
||||
const val NOTIFICATION_ID = 3122
|
||||
|
||||
private val openHumansAPI = OpenHumansAPI(OPEN_HUMANS_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL)
|
||||
private val FILE_NAME_DATE_FORMAT = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
private var isSetup
|
||||
get() = SP.getBoolean("openhumans_is_setup", false)
|
||||
set(value) = SP.putBoolean("openhumans_is_setup", value)
|
||||
private var oAuthTokens: OpenHumansAPI.OAuthTokens?
|
||||
get() {
|
||||
return if (SP.contains("openhumans_access_token") && SP.contains("openhumans_refresh_token") && SP.contains("openhumans_expires_at")) {
|
||||
OpenHumansAPI.OAuthTokens(
|
||||
accessToken = SP.getString("openhumans_access_token", null)!!,
|
||||
refreshToken = SP.getString("openhumans_refresh_token", null)!!,
|
||||
expiresAt = SP.getLong("openhumans_expires_at", 0)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
if (value != null) {
|
||||
SP.putString("openhumans_access_token", value.accessToken)
|
||||
SP.putString("openhumans_refresh_token", value.refreshToken)
|
||||
SP.putLong("openhumans_expires_at", value.expiresAt)
|
||||
} else {
|
||||
SP.remove("openhumans_access_token")
|
||||
SP.remove("openhumans_refresh_token")
|
||||
SP.remove("openhumans_expires_at")
|
||||
}
|
||||
}
|
||||
private var projectMemberId: String?
|
||||
get() = SP.getString("openhumans_project_member_id", null)
|
||||
set(value) {
|
||||
if (value == null) SP.remove("openhumans_project_member_id")
|
||||
else SP.putString("openhumans_project_member_id", value)
|
||||
}
|
||||
private var uploadCounter: Int
|
||||
get() = SP.getInt("openhumans_counter", 1)
|
||||
set(value) = SP.putInt("openhumans_counter", value)
|
||||
private val appId: UUID
|
||||
get() {
|
||||
val id = SP.getString("openhumans_appid", null)
|
||||
if (id == null) {
|
||||
val generated = UUID.randomUUID()
|
||||
SP.putString("openhumans_appid", generated.toString())
|
||||
return generated
|
||||
} else {
|
||||
return UUID.fromString(id)
|
||||
}
|
||||
}
|
||||
|
||||
private var copyDisposable: Disposable? = null
|
||||
|
||||
private val wakeLock = (MainApp.instance().getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AndroidAPS::OpenHumans")
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setupNotificationChannel()
|
||||
if (isSetup) scheduleWorker(false)
|
||||
SP.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
copyDisposable?.dispose()
|
||||
cancelWorker()
|
||||
SP.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
fun queueBGReading(bgReading: BgReading) = insertQueueItem("BgReadings") {
|
||||
put("date", bgReading.date)
|
||||
put("isValid", bgReading.isValid)
|
||||
put("value", bgReading.value)
|
||||
put("direction", bgReading.direction)
|
||||
put("raw", bgReading.raw)
|
||||
put("source", bgReading.source)
|
||||
put("nsId", bgReading._id)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun queueCareportalEvent(careportalEvent: CareportalEvent, deleted: Boolean = false) = insertQueueItem("CareportalEvents") {
|
||||
put("date", careportalEvent.date)
|
||||
put("isValid", careportalEvent.isValid)
|
||||
put("source", careportalEvent.source)
|
||||
put("nsId", careportalEvent._id)
|
||||
put("eventType", careportalEvent.eventType)
|
||||
val data = JSONObject(careportalEvent.json)
|
||||
val reducedData = JSONObject()
|
||||
if (data.has("mgdl")) reducedData.put("mgdl", data.getDouble("mgdl"))
|
||||
if (data.has("glucose")) reducedData.put("glucose", data.getDouble("glucose"))
|
||||
if (data.has("units")) reducedData.put("units", data.getString("units"))
|
||||
if (data.has("created_at")) reducedData.put("created_at", data.getString("created_at"))
|
||||
if (data.has("glucoseType")) reducedData.put("glucoseType", data.getString("glucoseType"))
|
||||
if (data.has("duration")) reducedData.put("duration", data.getInt("duration"))
|
||||
if (data.has("mills")) reducedData.put("mills", data.getLong("mills"))
|
||||
if (data.has("eventType")) reducedData.put("eventType", data.getString("eventType"))
|
||||
put("data", reducedData)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun queueExtendedBolus(extendedBolus: ExtendedBolus, deleted: Boolean = false) = insertQueueItem("ExtendedBoluses") {
|
||||
put("date", extendedBolus.date)
|
||||
put("isValid", extendedBolus.isValid)
|
||||
put("source", extendedBolus.source)
|
||||
put("nsId", extendedBolus._id)
|
||||
put("pumpId", extendedBolus.pumpId)
|
||||
put("insulin", extendedBolus.insulin)
|
||||
put("durationInMinutes", extendedBolus.durationInMinutes)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun queueProfileSwitch(profileSwitch: ProfileSwitch, deleted: Boolean = false) = insertQueueItem("ProfileSwitches") {
|
||||
put("date", profileSwitch.date)
|
||||
put("isValid", profileSwitch.isValid)
|
||||
put("source", profileSwitch.source)
|
||||
put("nsId", profileSwitch._id)
|
||||
put("isCPP", profileSwitch.isCPP)
|
||||
put("timeshift", profileSwitch.timeshift)
|
||||
put("percentage", profileSwitch.percentage)
|
||||
put("profile", JSONObject(profileSwitch.profileJson))
|
||||
put("profilePlugin", profileSwitch.profilePlugin)
|
||||
put("durationInMinutes", profileSwitch.durationInMinutes)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
|
||||
fun queueTotalDailyDose(tdd: TDD) = insertQueueItem("TotalDailyDoses") {
|
||||
put("double", tdd.date)
|
||||
put("double", tdd.bolus)
|
||||
put("double", tdd.basal)
|
||||
put("double", tdd.total)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun queueTemporaryBasal(temporaryBasal: TemporaryBasal, deleted: Boolean = false) = insertQueueItem("TemporaryBasals") {
|
||||
put("date", temporaryBasal.date)
|
||||
put("isValid", temporaryBasal.isValid)
|
||||
put("source", temporaryBasal.source)
|
||||
put("nsId", temporaryBasal._id)
|
||||
put("pumpId", temporaryBasal.pumpId)
|
||||
put("durationInMinutes", temporaryBasal.durationInMinutes)
|
||||
put("durationInMinutes", temporaryBasal.durationInMinutes)
|
||||
put("isAbsolute", temporaryBasal.isAbsolute)
|
||||
put("percentRate", temporaryBasal.percentRate)
|
||||
put("absoluteRate", temporaryBasal.absoluteRate)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun queueTempTarget(tempTarget: TempTarget, deleted: Boolean = false) = insertQueueItem("TempTargets") {
|
||||
put("date", tempTarget.date)
|
||||
put("isValid", tempTarget.isValid)
|
||||
put("source", tempTarget.source)
|
||||
put("nsId", tempTarget._id)
|
||||
put("low", tempTarget.low)
|
||||
put("high", tempTarget.high)
|
||||
put("reason", tempTarget.reason)
|
||||
put("durationInMinutes", tempTarget.durationInMinutes)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
|
||||
private fun insertQueueItem(file: String, structureVersion: Int = 1, generator: JSONObject.() -> Unit) {
|
||||
if (oAuthTokens != null && this.isEnabled(PluginType.GENERAL)) {
|
||||
try {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("structureVersion", structureVersion)
|
||||
jsonObject.put("queuedOn", System.currentTimeMillis())
|
||||
generator(jsonObject)
|
||||
val queueItem = OHQueueItem(
|
||||
file = file,
|
||||
content = jsonObject.toString()
|
||||
)
|
||||
MainApp.getDbHelper().createOrUpdate(queueItem)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(authCode: String) =
|
||||
openHumansAPI.exchangeAuthToken(authCode)
|
||||
.doOnSuccess {
|
||||
oAuthTokens = it
|
||||
}
|
||||
.flatMap { openHumansAPI.getProjectMemberId(it.accessToken) }
|
||||
.doOnSuccess {
|
||||
projectMemberId = it
|
||||
copyExistingDataToQueue()
|
||||
}
|
||||
.ignoreElement()
|
||||
|
||||
fun logout() {
|
||||
cancelWorker()
|
||||
copyDisposable?.dispose()
|
||||
isSetup = false
|
||||
oAuthTokens = null
|
||||
projectMemberId = null
|
||||
MainApp.getDbHelper().clearOpenHumansQueue()
|
||||
}
|
||||
|
||||
private fun copyExistingDataToQueue() {
|
||||
copyDisposable?.dispose()
|
||||
copyDisposable = Completable.fromCallable { MainApp.getDbHelper().clearOpenHumansQueue() }
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allBgReadings) })
|
||||
.map { queueBGReading(it) }
|
||||
.ignoreElements()
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allCareportalEvents) })
|
||||
.map { queueCareportalEvent(it) }
|
||||
.ignoreElements()
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allExtendedBoluses) })
|
||||
.map { queueExtendedBolus(it) }
|
||||
.ignoreElements()
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allProfileSwitches) })
|
||||
.map { queueProfileSwitch(it) }
|
||||
.ignoreElements()
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allTDDs) })
|
||||
.map { queueTotalDailyDose(it) }
|
||||
.ignoreElements()
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allTemporaryBasals) })
|
||||
.map { queueTemporaryBasal(it) }
|
||||
.ignoreElements()
|
||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allTempTargets) })
|
||||
.map { queueTempTarget(it) }
|
||||
.ignoreElements()
|
||||
.doOnSubscribe {
|
||||
wakeLock.acquire()
|
||||
showOngoingNotification()
|
||||
}
|
||||
.doOnComplete {
|
||||
isSetup = true
|
||||
scheduleWorker(false)
|
||||
showSetupFinishedNotification()
|
||||
}
|
||||
.doOnError {
|
||||
showSetupFailedNotification()
|
||||
}
|
||||
.doFinally {
|
||||
copyDisposable = null
|
||||
wakeLock.release()
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun showOngoingNotification() {
|
||||
val notification = NotificationCompat.Builder(MainApp.instance(), "OpenHumans")
|
||||
.setContentTitle(MainApp.gs(R.string.finishing_open_humans_setup))
|
||||
.setContentText(MainApp.gs(R.string.this_may_take_a_while))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.build()
|
||||
NotificationManagerCompat.from(MainApp.instance()).notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showSetupFinishedNotification() {
|
||||
val notification = NotificationCompat.Builder(MainApp.instance(), "OpenHumans")
|
||||
.setContentTitle(MainApp.gs(R.string.setup_finished))
|
||||
.setContentText(MainApp.gs(R.string.your_phone_is_upload_data))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.build()
|
||||
val notificationManager = NotificationManagerCompat.from(MainApp.instance())
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showSetupFailedNotification() {
|
||||
val notification = NotificationCompat.Builder(MainApp.instance(), "OpenHumans")
|
||||
.setContentTitle(MainApp.gs(R.string.setup_failed))
|
||||
.setContentText(MainApp.gs(R.string.there_was_an_error))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.build()
|
||||
val notificationManager = NotificationManagerCompat.from(MainApp.instance())
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
fun uploadData() = gatherData()
|
||||
.flatMap { data -> refreshAccessTokensIfNeeded().map { accessToken -> accessToken to data } }
|
||||
.flatMap { uploadFile(it.first, it.second).andThen(Single.just(it.second)) }
|
||||
.flatMapCompletable {
|
||||
if (it.highestQueueId != null) {
|
||||
removeUploadedEntriesFromQueue(it.highestQueueId)
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
}
|
||||
.doOnError {
|
||||
if (it is OpenHumansAPI.OHHttpException && it.code == 401 && it.detail == "Invalid token.") {
|
||||
handleSignOut()
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadFile(accessToken: String, uploadData: UploadData) = Completable.defer {
|
||||
openHumansAPI.prepareFileUpload(accessToken, uploadData.fileName, uploadData.metadata)
|
||||
.flatMap { openHumansAPI.uploadFile(it.uploadURL, uploadData.content).andThen(Single.just(it.fileId)) }
|
||||
.flatMapCompletable { openHumansAPI.completeFileUpload(accessToken, it) }
|
||||
}
|
||||
|
||||
private fun refreshAccessTokensIfNeeded() = Single.defer {
|
||||
val oAuthTokens = this.oAuthTokens!!
|
||||
if (oAuthTokens.expiresAt <= System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)) {
|
||||
openHumansAPI.refreshAccessToken(oAuthTokens.refreshToken)
|
||||
.doOnSuccess { this.oAuthTokens = it }
|
||||
.map { it.accessToken }
|
||||
} else {
|
||||
Single.just(oAuthTokens.accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
private fun gatherData() = Single.defer {
|
||||
val items = MainApp.getDbHelper().allOHQueueItems
|
||||
val baos = ByteArrayOutputStream()
|
||||
val zos = ZipOutputStream(baos)
|
||||
val tags = mutableListOf<String>()
|
||||
|
||||
items.groupBy { it.file }.forEach { entry ->
|
||||
tags.add(entry.key)
|
||||
val jsonArray = JSONArray()
|
||||
entry.value.map { it.content }.forEach { jsonArray.put(JSONObject(it)) }
|
||||
zos.writeFile("${entry.key}.json", jsonArray.toString().toByteArray())
|
||||
}
|
||||
|
||||
val applicationInfo = JSONObject()
|
||||
applicationInfo.put("versionName", BuildConfig.VERSION_NAME)
|
||||
applicationInfo.put("versionCode", BuildConfig.VERSION_CODE)
|
||||
val hasGitInfo = !BuildConfig.HEAD.endsWith("NoGitSystemAvailable", true)
|
||||
val customRemote = !BuildConfig.REMOTE.equals("https://github.com/MilosKozak/AndroidAPS.git", true)
|
||||
applicationInfo.put("hasGitInfo", hasGitInfo)
|
||||
applicationInfo.put("customRemote", customRemote)
|
||||
applicationInfo.put("applicationId", appId.toString())
|
||||
|
||||
val deviceInfo = JSONObject()
|
||||
deviceInfo.put("brand", Build.BRAND)
|
||||
deviceInfo.put("device", Build.DEVICE)
|
||||
deviceInfo.put("manufacturer", Build.MANUFACTURER)
|
||||
deviceInfo.put("model", Build.MODEL)
|
||||
deviceInfo.put("product", Build.PRODUCT)
|
||||
zos.writeFile("DeviceInfo.json", deviceInfo.toString().toByteArray())
|
||||
tags.add("DeviceInfo")
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
(MainApp.instance().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(displayMetrics)
|
||||
val displayInfo = JSONObject()
|
||||
displayInfo.put("height", displayMetrics.heightPixels)
|
||||
displayInfo.put("width", displayMetrics.widthPixels)
|
||||
displayInfo.put("density", displayMetrics.density)
|
||||
displayInfo.put("scaledDensity", displayMetrics.scaledDensity)
|
||||
displayInfo.put("xdpi", displayMetrics.xdpi)
|
||||
displayInfo.put("ydpi", displayMetrics.ydpi)
|
||||
zos.writeFile("DisplayInfo.json", displayInfo.toString().toByteArray())
|
||||
tags.add("DisplayInfo")
|
||||
|
||||
val uploadNumber = this.uploadCounter++
|
||||
val uploadDate = Date()
|
||||
val uploadInfo = JSONObject()
|
||||
uploadInfo.put("fileVersion", 1)
|
||||
uploadInfo.put("counter", uploadNumber)
|
||||
uploadInfo.put("timestamp", uploadDate.time)
|
||||
uploadInfo.put("utcOffset", TimeZone.getDefault().getOffset(uploadDate.time))
|
||||
zos.writeFile("UploadInfo.json", uploadInfo.toString().toByteArray())
|
||||
tags.add("UploadInfo")
|
||||
|
||||
zos.close()
|
||||
val bytes = baos.toByteArray()
|
||||
|
||||
Single.just(UploadData(
|
||||
fileName = "upload-num$uploadNumber-ver1-date${FILE_NAME_DATE_FORMAT.format(uploadDate)}-appid${appId.toString().replace("- ", "")}.zip",
|
||||
metadata = OpenHumansAPI.FileMetadata(
|
||||
tags = tags,
|
||||
description = "AndroidAPS Database Upload",
|
||||
md5 = MessageDigest.getInstance("MD5").digest(bytes).toHexString(),
|
||||
creationDate = uploadDate.time
|
||||
),
|
||||
content = bytes,
|
||||
highestQueueId = items.map { it.id }.max()
|
||||
))
|
||||
}
|
||||
|
||||
private fun ZipOutputStream.writeFile(name: String, bytes: ByteArray) {
|
||||
putNextEntry(ZipEntry(name))
|
||||
write(bytes)
|
||||
closeEntry()
|
||||
}
|
||||
|
||||
private fun removeUploadedEntriesFromQueue(highestId: Long) = Completable.fromCallable {
|
||||
MainApp.getDbHelper().removeAllOHQueueItemsWithIdSmallerThan(highestId)
|
||||
}
|
||||
|
||||
private fun handleSignOut() {
|
||||
isSetup = false
|
||||
projectMemberId = null
|
||||
oAuthTokens = null
|
||||
cancelWorker()
|
||||
val notification = NotificationCompat.Builder(MainApp.instance(), "OpenHumans")
|
||||
.setContentTitle(MainApp.gs(R.string.you_have_been_signed_out_of_open_humans))
|
||||
.setContentText(MainApp.gs(R.string.click_here_to_sign_in_again_if_this_wasnt_on_purpose))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(PendingIntent.getActivity(
|
||||
MainApp.instance(),
|
||||
0,
|
||||
Intent(MainApp.instance(), OpenHumansLoginActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
},
|
||||
0
|
||||
))
|
||||
.build()
|
||||
NotificationManagerCompat.from(MainApp.instance()).notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun cancelWorker() {
|
||||
WorkManager.getInstance(MainApp.instance()).cancelUniqueWork(WORK_NAME)
|
||||
}
|
||||
|
||||
private fun scheduleWorker(replace: Boolean) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresCharging(SP.getBoolean("key_oh_charging_only", false))
|
||||
.build()
|
||||
val workRequest = PeriodicWorkRequestBuilder<OHUploadWorker>(1, TimeUnit.DAYS)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.HOURS)
|
||||
.build()
|
||||
WorkManager.getInstance(MainApp.instance()).enqueueUniquePeriodicWork(WORK_NAME, if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
||||
}
|
||||
|
||||
private fun setupNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManagerCompat = NotificationManagerCompat.from(MainApp.instance())
|
||||
notificationManagerCompat.createNotificationChannel(NotificationChannel(
|
||||
"OpenHumans",
|
||||
MainApp.gs(R.string.open_humans),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private class UploadData(
|
||||
val fileName: String,
|
||||
val metadata: OpenHumansAPI.FileMetadata,
|
||||
val content: ByteArray,
|
||||
val highestQueueId: Long?
|
||||
)
|
||||
|
||||
private val HEX_DIGITS = "0123456789ABCDEF".toCharArray()
|
||||
|
||||
private fun ByteArray.toHexString(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
map { it.toInt() }.forEach {
|
||||
stringBuilder.append(HEX_DIGITS[(it shr 4) and 0x0F])
|
||||
stringBuilder.append(HEX_DIGITS[it and 0x0F])
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
if (key == "key_oh_charging_only" && isSetup) scheduleWorker(true)
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import info.nightscout.androidaps.MainApp;
|
|||
*/
|
||||
|
||||
public class SP {
|
||||
private static SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainApp.instance().getApplicationContext());
|
||||
public static SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainApp.instance().getApplicationContext());
|
||||
|
||||
static public Map<String, ?> getAll() {
|
||||
return sharedPreferences.getAll();
|
||||
|
|
42
app/src/main/res/layout/activity_open_humans_login.xml
Normal file
42
app/src/main/res/layout/activity_open_humans_login.xml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/terms_of_use"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/open_humans_terms"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/i_understand_and_agree" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
14
app/src/main/res/layout/fragment_open_humans.xml
Normal file
14
app/src/main/res/layout/fragment_open_humans.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/login"
|
||||
android:text="@string/login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1699,5 +1699,25 @@
|
|||
<string name="loop_tbrrequest_time_label">Temp basal request time</string>
|
||||
<string name="loop_tbrexecution_time_label">Temp basal execution time</string>
|
||||
<string name="insight_alert_notification_channel">Insight Pump Alerts</string>
|
||||
|
||||
<string name="open_humans">Open Humans</string>
|
||||
<string name="finishing_open_humans_setup">Finishing Open Humans setup…</string>
|
||||
<string name="this_may_take_a_while">This may take a while. Do not turn your phone off.</string>
|
||||
<string name="setup_finished">Setup finished</string>
|
||||
<string name="your_phone_is_upload_data">Your phone is uploading data to Open Humans now.</string>
|
||||
<string name="setup_failed">Setup failed</string>
|
||||
<string name="there_was_an_error">There was an error.</string>
|
||||
<string name="open_humans_terms">This is an open source tool that will copy your data to Open Humans. We retain no rights to share your data with third parties without your explicit authorization. The data the project and app receive are identified via a random user ID and will only be securely transmitted to an Open Humans account with your authorization of that process. You can stop uploading and delete your upload data at any time via www.openhumans.org.</string>
|
||||
<string name="i_understand_and_agree">I understand and agree</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="terms_of_use">Terms of Use</string>
|
||||
<string name="you_need_to_accept_the_of_use_first">You need to accept the terms of use first.</string>
|
||||
<string name="successfully_logged_in">Successfully logged in</string>
|
||||
<string name="setup_will_continue_in_background">The setup will be completed in background now. Thanks for uploading your data.</string>
|
||||
<string name="completing_login">Completing login…</string>
|
||||
<string name="donate_your_data_to_science">Donate your data to science</string>
|
||||
<string name="open_humans_short">OH</string>
|
||||
<string name="you_have_been_signed_out_of_open_humans">You have been signed out of Open Humans</string>
|
||||
<string name="click_here_to_sign_in_again_if_this_wasnt_on_purpose">Click here to sign in a again if this wasn\'t on purpose.</string>
|
||||
<string name="only_upload_if_connected_to_wifi">Only upload if connected to WiFi</string>
|
||||
<string name="only_upload_if_charging">Only upload if charging</string>
|
||||
</resources>
|
||||
|
|
18
app/src/main/res/xml/pref_openhumans.xml
Normal file
18
app/src/main/res/xml/pref_openhumans.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:title="@string/open_humans">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:key="key_oh_wifi_only"
|
||||
android:title="@string/only_upload_if_connected_to_wifi" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="key_oh_charging_only"
|
||||
android:title="@string/only_upload_if_charging" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
Loading…
Reference in a new issue