Open Humans Uploader: Initial commit

This commit is contained in:
TebbeUbben 2020-05-20 21:49:01 +02:00
parent 7970661221
commit b239100c13
17 changed files with 1403 additions and 9 deletions

View file

@ -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'
}

View file

@ -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"/>

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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 = ""
)

View file

@ -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));
}
}

View file

@ -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() }

View file

@ -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())
}
}

View file

@ -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")
}
}

View file

@ -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
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View file

@ -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();

View 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>

View 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>

View file

@ -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>

View 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>