diff --git a/app/build.gradle b/app/build.gradle index 5b5a5cd5c3..d721714bca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -237,7 +237,11 @@ dependencies { implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.percentlayout:percentlayout:1.0.0' implementation "com.wdullaer:materialdatetimepicker:2.3.0" + + // Otto bus will be replaced by rx implementation "com.squareup:otto:1.3.7" + implementation "io.reactivex.rxjava2:rxandroid:2.1.1" + implementation "com.j256.ormlite:ormlite-core:${ormLiteVersion}" implementation "com.j256.ormlite:ormlite-android:${ormLiteVersion}" implementation("com.github.tony19:logback-android-classic:1.1.1-6") { @@ -294,6 +298,16 @@ dependencies { androidTestImplementation "com.google.dexmaker:dexmaker:${dexmakerVersion}" androidTestImplementation "com.google.dexmaker:dexmaker-mockito:${dexmakerVersion}" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + + // new for tidepool + implementation 'com.squareup.okhttp3:okhttp:3.10.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' + implementation "com.squareup.retrofit2:retrofit:2.4.0" + implementation "com.squareup.retrofit2:adapter-rxjava2:2.4.0" + implementation "com.squareup.retrofit2:converter-gson:2.4.0" + } diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index c126bd60b6..87ddf8de1d 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -56,6 +56,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.receivers.DBAccessRec import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin; import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin; import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin; +import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin; import info.nightscout.androidaps.plugins.general.versionChecker.VersionCheckerPlugin; import info.nightscout.androidaps.plugins.general.wear.WearPlugin; import info.nightscout.androidaps.plugins.general.xdripStatusline.StatuslinePlugin; @@ -214,6 +215,8 @@ public class MainApp extends Application { pluginsList.add(StatuslinePlugin.initPlugin(this)); pluginsList.add(PersistentNotificationPlugin.getPlugin()); pluginsList.add(NSClientPlugin.getPlugin()); + if (engineeringMode) + pluginsList.add(TidepoolPlugin.INSTANCE); pluginsList.add(MaintenancePlugin.initPlugin(this)); pluginsList.add(ConfigBuilderPlugin.getPlugin()); diff --git a/app/src/main/java/info/nightscout/androidaps/activities/PreferencesActivity.java b/app/src/main/java/info/nightscout/androidaps/activities/PreferencesActivity.java index aa5301ef8a..cd8d939a49 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/PreferencesActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/activities/PreferencesActivity.java @@ -15,12 +15,15 @@ import android.text.TextUtils; import info.nightscout.androidaps.Config; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.events.EventPreferenceChange; import info.nightscout.androidaps.events.EventRefreshGui; import info.nightscout.androidaps.interfaces.PluginBase; import info.nightscout.androidaps.interfaces.PluginType; import info.nightscout.androidaps.plugins.general.careportal.CareportalPlugin; import info.nightscout.androidaps.plugins.constraints.safety.SafetyPlugin; +import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin; +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader; import info.nightscout.androidaps.plugins.insulin.InsulinOrefFreePeakPlugin; import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin; @@ -64,6 +67,7 @@ public class PreferencesActivity extends PreferenceActivity implements SharedPre @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { MainApp.bus().post(new EventPreferenceChange(key)); + RxBus.INSTANCE.send(new EventPreferenceChange(key)); if (key.equals("language")) { String lang = sharedPreferences.getString("language", "en"); LocaleHelper.setLocale(getApplicationContext(), lang); @@ -184,6 +188,7 @@ public class PreferencesActivity extends PreferenceActivity implements SharedPre addPreferencesFromResourceIfEnabled(InsulinOrefFreePeakPlugin.getPlugin(), PluginType.INSULIN); addPreferencesFromResourceIfEnabled(NSClientPlugin.getPlugin(), PluginType.GENERAL); + addPreferencesFromResourceIfEnabled(TidepoolPlugin.INSTANCE, PluginType.GENERAL); addPreferencesFromResourceIfEnabled(SmsCommunicatorPlugin.getPlugin(), PluginType.GENERAL); addPreferencesFromResource(R.xml.pref_others); @@ -194,7 +199,7 @@ public class PreferencesActivity extends PreferenceActivity implements SharedPre } if (Config.NSCLIENT) { - PreferenceScreen scrnAdvancedSettings = (PreferenceScreen)findPreference(getString(R.string.key_advancedsettings)); + PreferenceScreen scrnAdvancedSettings = (PreferenceScreen) findPreference(getString(R.string.key_advancedsettings)); if (scrnAdvancedSettings != null) { scrnAdvancedSettings.removePreference(getPreference(getString(R.string.key_statuslights_res_warning))); scrnAdvancedSettings.removePreference(getPreference(getString(R.string.key_statuslights_res_critical))); @@ -205,6 +210,13 @@ public class PreferencesActivity extends PreferenceActivity implements SharedPre } initSummary(getPreferenceScreen()); + + final Preference tidepoolTestLogin = findPreference(MainApp.gs(R.string.key_tidepool_test_login)); + if (tidepoolTestLogin != null) + tidepoolTestLogin.setOnPreferenceClickListener(preference -> { + TidepoolUploader.INSTANCE.testLogin(getActivity()); + return false; + }); } @Override diff --git a/app/src/main/java/info/nightscout/androidaps/data/NonOverlappingIntervals.java b/app/src/main/java/info/nightscout/androidaps/data/NonOverlappingIntervals.java index 928833c481..c426aede40 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/NonOverlappingIntervals.java +++ b/app/src/main/java/info/nightscout/androidaps/data/NonOverlappingIntervals.java @@ -19,7 +19,7 @@ public class NonOverlappingIntervals extends Intervals { rawData = other.rawData.clone(); } - protected synchronized void merge() { + public synchronized void merge() { for (int index = 0; index < rawData.size() - 1; index++) { Interval i = rawData.valueAt(index); long startOfNewer = rawData.valueAt(index + 1).start(); diff --git a/app/src/main/java/info/nightscout/androidaps/data/Profile.java b/app/src/main/java/info/nightscout/androidaps/data/Profile.java index 945cf3c7c7..8524246d02 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/Profile.java +++ b/app/src/main/java/info/nightscout/androidaps/data/Profile.java @@ -400,6 +400,19 @@ public class Profile { return getValuesList(isf_v, null, new DecimalFormat("0.0"), getUnits() + MainApp.gs(R.string.profile_per_unit)); } + public ProfileValue[] getIsfs() { + if (isf_v == null) + isf_v = convertToSparseArray(ic); + ProfileValue[] ret = new ProfileValue[isf_v.size()]; + + for (Integer index = 0; index < isf_v.size(); index++) { + Integer tas = (int) isf_v.keyAt(index); + double value = isf_v.valueAt(index); + ret[index] = new ProfileValue(tas, value); + } + return ret; + } + public double getIc() { return getIcTimeFromMidnight(secondsFromMidnight()); } @@ -420,6 +433,19 @@ public class Profile { return getValuesList(ic_v, null, new DecimalFormat("0.0"), MainApp.gs(R.string.profile_carbs_per_unit)); } + public ProfileValue[] getIcs() { + if (ic_v == null) + ic_v = convertToSparseArray(ic); + ProfileValue[] ret = new ProfileValue[ic_v.size()]; + + for (Integer index = 0; index < ic_v.size(); index++) { + Integer tas = (int) ic_v.keyAt(index); + double value = ic_v.valueAt(index); + ret[index] = new ProfileValue(tas, value); + } + return ret; + } + public double getBasal() { return getBasalTimeFromMidnight(secondsFromMidnight()); } @@ -441,8 +467,8 @@ public class Profile { return getValuesList(basal_v, null, new DecimalFormat("0.00"), MainApp.gs(R.string.profile_ins_units_per_hout)); } - public class BasalValue { - public BasalValue(int timeAsSeconds, double value) { + public class ProfileValue { + public ProfileValue(int timeAsSeconds, double value) { this.timeAsSeconds = timeAsSeconds; this.value = value; } @@ -451,15 +477,15 @@ public class Profile { public double value; } - public synchronized BasalValue[] getBasalValues() { + public synchronized ProfileValue[] getBasalValues() { if (basal_v == null) basal_v = convertToSparseArray(basal); - BasalValue[] ret = new BasalValue[basal_v.size()]; + ProfileValue[] ret = new ProfileValue[basal_v.size()]; for (Integer index = 0; index < basal_v.size(); index++) { Integer tas = (int) basal_v.keyAt(index); double value = basal_v.valueAt(index); - ret[index] = new BasalValue(tas, value); + ret[index] = new ProfileValue(tas, value); } return ret; } @@ -500,6 +526,49 @@ public class Profile { return getValueToTime(targetHigh_v, timeAsSeconds); } + public class TargetValue { + public TargetValue(int timeAsSeconds, double low, double high) { + this.timeAsSeconds = timeAsSeconds; + this.low = low; + this.high = high; + } + + public int timeAsSeconds; + public double low; + public double high; + } + + public TargetValue[] getTargets() { + if (targetLow_v == null) + targetLow_v = convertToSparseArray(targetLow); + if (targetHigh_v == null) + targetHigh_v = convertToSparseArray(targetHigh); + TargetValue[] ret = new TargetValue[targetLow_v.size()]; + + for (Integer index = 0; index < targetLow_v.size(); index++) { + Integer tas = (int) targetLow_v.keyAt(index); + double low = targetLow_v.valueAt(index); + double high = targetHigh_v.valueAt(index); + ret[index] = new TargetValue(tas, low, high); + } + return ret; + } + + public ProfileValue[] getSingleTargets() { + if (targetLow_v == null) + targetLow_v = convertToSparseArray(targetLow); + if (targetHigh_v == null) + targetHigh_v = convertToSparseArray(targetHigh); + ProfileValue[] ret = new ProfileValue[targetLow_v.size()]; + + for (Integer index = 0; index < targetLow_v.size(); index++) { + Integer tas = (int) targetLow_v.keyAt(index); + double target = (targetLow_v.valueAt(index) + targetHigh_v.valueAt(index)) / 2; + ret[index] = new ProfileValue(tas, target); + } + return ret; + } + public String getTargetList() { if (targetLow_v == null) targetLow_v = convertToSparseArray(targetLow); diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index 7ae1c91a62..9fa3ab9929 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -31,6 +31,7 @@ import java.util.concurrent.TimeUnit; import info.nightscout.androidaps.Constants; import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.data.OverlappingIntervals; import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.data.ProfileStore; @@ -412,6 +413,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { if (L.isEnabled(L.DATABASE)) log.debug("Firing EventNewBg"); MainApp.bus().post(new EventNewBG(bgReading)); + RxBus.INSTANCE.send(new EventNewBG(bgReading)); scheduledBgPost = null; } } @@ -616,7 +618,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } } - // -------------------- TREATMENT HANDLING ------------------- + // -------------------- TEMPTARGET HANDLING ------------------- public static void updateEarliestDataChange(long newDate) { if (earliestDataChange == null) { @@ -647,6 +649,23 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getTemptargetsDataFromTime(long from, long to, boolean ascending) { + try { + Dao daoTempTargets = getDaoTempTargets(); + List tempTargets; + QueryBuilder queryBuilder = daoTempTargets.queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + tempTargets = daoTempTargets.query(preparedQuery); + return tempTargets; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList(); + } + public boolean createOrUpdate(TempTarget tempTarget) { try { TempTarget old; @@ -995,6 +1014,22 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + public List getTemporaryBasalsDataFromTime(long from, long to, boolean ascending) { + try { + List tempbasals; + QueryBuilder queryBuilder = getDaoTemporaryBasal().queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + tempbasals = getDaoTemporaryBasal().query(preparedQuery); + return tempbasals; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList(); + } + private static void scheduleTemporaryBasalChange() { class PostRunnable implements Runnable { public void run() { @@ -1422,6 +1457,23 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList<>(); } + public List getCareportalEvents(long start, long end, boolean ascending) { + try { + List careportalEvents; + QueryBuilder queryBuilder = getDaoCareportalEvents().queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", start, end); + PreparedQuery preparedQuery = queryBuilder.prepare(); + careportalEvents = getDaoCareportalEvents().query(preparedQuery); + preprocessOpenAPSOfflineEvents(careportalEvents); + return careportalEvents; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList<>(); + } + public void preprocessOpenAPSOfflineEvents(List list) { OverlappingIntervals offlineEvents = new OverlappingIntervals(); for (int i = 0; i < list.size(); i++) { @@ -1575,6 +1627,24 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList<>(); } + public List getProfileSwitchEventsFromTime(long from, long to, boolean ascending) { + try { + Dao daoProfileSwitch = getDaoProfileSwitch(); + List profileSwitches; + QueryBuilder queryBuilder = daoProfileSwitch.queryBuilder(); + queryBuilder.orderBy("date", ascending); + queryBuilder.limit(100L); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + profileSwitches = daoProfileSwitch.query(preparedQuery); + return profileSwitches; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList<>(); + } + public boolean createOrUpdate(ProfileSwitch profileSwitch) { try { ProfileSwitch old; diff --git a/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java b/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java index 65a2f8af84..4b40ba1b38 100644 --- a/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java +++ b/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java @@ -53,7 +53,9 @@ public interface PumpInterface { // Status to be passed to NS JSONObject getJSONStatus(Profile profile, String profileName); - String deviceID(); + String manufacter(); + String model(); + String serialNumber(); // Pump capabilities PumpDescription getPumpDescription(); diff --git a/app/src/main/java/info/nightscout/androidaps/interfaces/TreatmentsInterface.java b/app/src/main/java/info/nightscout/androidaps/interfaces/TreatmentsInterface.java index eb0fc7f50a..0bec22661c 100644 --- a/app/src/main/java/info/nightscout/androidaps/interfaces/TreatmentsInterface.java +++ b/app/src/main/java/info/nightscout/androidaps/interfaces/TreatmentsInterface.java @@ -5,6 +5,7 @@ import java.util.List; import info.nightscout.androidaps.data.DetailedBolusInfo; import info.nightscout.androidaps.data.IobTotal; import info.nightscout.androidaps.data.MealData; +import info.nightscout.androidaps.data.NonOverlappingIntervals; import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.db.ExtendedBolus; import info.nightscout.androidaps.db.ProfileSwitch; @@ -43,7 +44,7 @@ public interface TreatmentsInterface { // basal that can be faked by extended boluses boolean isTempBasalInProgress(); TemporaryBasal getTempBasalFromHistory(long time); - Intervals getTemporaryBasalsFromHistory(); + NonOverlappingIntervals getTemporaryBasalsFromHistory(); boolean isInHistoryExtendedBoluslInProgress(); ExtendedBolus getExtendedBolusFromHistory(long time); diff --git a/app/src/main/java/info/nightscout/androidaps/logging/L.java b/app/src/main/java/info/nightscout/androidaps/logging/L.java index f685bbd424..362e03a0c7 100644 --- a/app/src/main/java/info/nightscout/androidaps/logging/L.java +++ b/app/src/main/java/info/nightscout/androidaps/logging/L.java @@ -87,6 +87,7 @@ public class L { public static final String DATAFOOD = "DATAFOOD"; public static final String DATATREATMENTS = "DATATREATMENTS"; public static final String NSCLIENT = "NSCLIENT"; + public static final String TIDEPOOL = "TIDEPOOL"; public static final String CONSTRAINTS = "CONSTRAINTS"; public static final String PUMP = "PUMP"; public static final String PUMPQUEUE = "PUMPQUEUE"; @@ -114,6 +115,7 @@ public class L { logElements.add(new LogElement(EVENTS, false, true)); logElements.add(new LogElement(NOTIFICATION, true)); logElements.add(new LogElement(NSCLIENT, true)); + logElements.add(new LogElement(TIDEPOOL, true)); logElements.add(new LogElement(OVERVIEW, true)); logElements.add(new LogElement(PROFILE, true)); logElements.add(new LogElement(PUMP, true)); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/bus/RxBus.kt b/app/src/main/java/info/nightscout/androidaps/plugins/bus/RxBus.kt new file mode 100644 index 0000000000..1774df1471 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/bus/RxBus.kt @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.bus + +import info.nightscout.androidaps.events.Event +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject + +// Use object so we have a singleton instance +object RxBus { + + private val publisher = PublishSubject.create() + + fun send(event: Event) { + publisher.onNext(event) + } + + // Listen should return an Observable and not the publisher + // Using ofType we filter only events that match that class type + fun toObservable(eventType: Class): Observable = + publisher + .subscribeOn(Schedulers.io()) + .ofType(eventType) +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/ConfigBuilderPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/ConfigBuilderPlugin.java index a6c5209d5f..a29b893ef4 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/ConfigBuilderPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/ConfigBuilderPlugin.java @@ -232,26 +232,32 @@ public class ConfigBuilderPlugin extends PluginBase { return commandQueue; } + @Nullable public BgSourceInterface getActiveBgSource() { return activeBgSource; } + @Nullable public ProfileInterface getActiveProfileInterface() { return activeProfile; } + @Nullable public InsulinInterface getActiveInsulin() { return activeInsulin; } + @Nullable public APSInterface getActiveAPS() { return activeAPS; } + @Nullable public PumpInterface getActivePump() { return activePump; } + @Nullable public SensitivityInterface getActiveSensitivity() { return activeSensitivity; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NSClientPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NSClientPlugin.java index 1947c5911d..b5ff92ee0e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NSClientPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NSClientPlugin.java @@ -22,6 +22,7 @@ import info.nightscout.androidaps.Config; import info.nightscout.androidaps.Constants; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.events.EventAppExit; import info.nightscout.androidaps.events.EventChargingState; import info.nightscout.androidaps.events.EventNetworkChange; @@ -213,6 +214,7 @@ public class NSClientPlugin extends PluginBase { SP.putBoolean(R.string.key_nsclientinternal_paused, newState); paused = newState; MainApp.bus().post(new EventPreferenceChange(R.string.key_nsclientinternal_paused)); + RxBus.INSTANCE.send(new EventPreferenceChange(R.string.key_nsclientinternal_paused)); } public UploadQueue queue() { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NsClientReceiverDelegate.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NsClientReceiverDelegate.java index 8eeee8c43c..00d8d933ec 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NsClientReceiverDelegate.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/nsclient/NsClientReceiverDelegate.java @@ -10,6 +10,7 @@ import com.squareup.otto.Bus; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.events.EventChargingState; import info.nightscout.androidaps.events.EventNetworkChange; import info.nightscout.androidaps.events.EventPreferenceChange; @@ -99,6 +100,7 @@ class NsClientReceiverDelegate { if (newAllowedState != allowed) { allowed = newAllowedState; bus.post(new EventPreferenceChange(R.string.key_nsclientinternal_paused)); + RxBus.INSTANCE.send(new EventPreferenceChange(R.string.key_nsclientinternal_paused)); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt new file mode 100644 index 0000000000..7fc21ca510 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolFragment.kt @@ -0,0 +1,57 @@ +package info.nightscout.androidaps.plugins.general.tidepool + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ScrollView +import androidx.fragment.app.Fragment +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolDoUpload +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolResetData +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolUpdateGUI +import info.nightscout.androidaps.utils.SP +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import kotlinx.android.synthetic.main.tidepool_fragment.* + +class TidepoolFragment : Fragment() { + + private var disposable: CompositeDisposable = CompositeDisposable() + + operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + add(disposable) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.tidepool_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + tidepool_login.setOnClickListener { TidepoolUploader.doLogin(false) } + tidepool_uploadnow.setOnClickListener { RxBus.send(EventTidepoolDoUpload()) } + tidepool_removeall.setOnClickListener { RxBus.send(EventTidepoolResetData()) } + tidepool_resertstart.setOnClickListener { SP.putLong(R.string.key_tidepool_last_end, 0) } + + disposable.add(RxBus + .toObservable(EventTidepoolUpdateGUI::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + TidepoolPlugin.updateLog() + tidepool_log.text = TidepoolPlugin.textLog + tidepool_status.text = TidepoolUploader.connectionStatus.name + tidepool_log.text = TidepoolPlugin.textLog + tidepool_logscrollview.fullScroll(ScrollView.FOCUS_DOWN) + }, {}) + ) + } + + override fun onStop() { + super.onStop() + disposable.clear() + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt new file mode 100644 index 0000000000..90f60d0542 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/TidepoolPlugin.kt @@ -0,0 +1,147 @@ +package info.nightscout.androidaps.plugins.general.tidepool + +import android.text.Html +import android.text.Spanned +import info.nightscout.androidaps.Constants +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.db.BgReading +import info.nightscout.androidaps.events.EventNetworkChange +import info.nightscout.androidaps.events.EventNewBG +import info.nightscout.androidaps.events.EventPreferenceChange +import info.nightscout.androidaps.interfaces.PluginBase +import info.nightscout.androidaps.interfaces.PluginDescription +import info.nightscout.androidaps.interfaces.PluginType +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolDoUpload +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolResetData +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolStatus +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolUpdateGUI +import info.nightscout.androidaps.plugins.general.tidepool.utils.RateLimit +import info.nightscout.androidaps.receivers.ChargingStateReceiver +import info.nightscout.androidaps.receivers.NetworkChangeReceiver +import info.nightscout.androidaps.utils.SP +import info.nightscout.androidaps.utils.T +import info.nightscout.androidaps.utils.ToastUtils +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.slf4j.LoggerFactory +import java.util.* + +object TidepoolPlugin : PluginBase(PluginDescription() + .mainType(PluginType.GENERAL) + .pluginName(R.string.tidepool) + .shortName(R.string.tidepool_shortname) + .fragmentClass(TidepoolFragment::class.qualifiedName) + .preferencesId(R.xml.pref_tidepool) + .description(R.string.description_tidepool) +) { + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + private var disposable: CompositeDisposable = CompositeDisposable() + + private val listLog = ArrayList() + @Suppress("DEPRECATION") // API level 24 to replace call + var textLog: Spanned = Html.fromHtml("") + + operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + add(disposable) + } + + override fun onStart() { + super.onStart() + disposable += RxBus + .toObservable(EventTidepoolDoUpload::class.java) + .observeOn(Schedulers.io()) + .subscribe({ doUpload() }, {}) + disposable += RxBus + .toObservable(EventTidepoolResetData::class.java) + .observeOn(Schedulers.io()) + .subscribe({ + if (TidepoolUploader.connectionStatus != TidepoolUploader.ConnectionStatus.CONNECTED) { + log.debug("Not connected for delete Dataset") + } else { + TidepoolUploader.deleteDataSet() + SP.putLong(R.string.key_tidepool_last_end, 0) + TidepoolUploader.doLogin() + } + }, {}) + disposable += RxBus + .toObservable(EventTidepoolStatus::class.java) + .observeOn(Schedulers.io()) + .subscribe({ event -> addToLog(event) }, {}) + disposable += RxBus + .toObservable(EventNewBG::class.java) + .observeOn(Schedulers.io()) + .filter { it.bgReading != null } // better would be optional in API level >24 + .map { it.bgReading } + .subscribe { bgReading -> + if (bgReading!!.date < TidepoolUploader.getLastEnd()) + TidepoolUploader.setLastEnd(bgReading.date) + if (isEnabled(PluginType.GENERAL) + && (!SP.getBoolean(R.string.key_tidepool_only_while_charging, false) || ChargingStateReceiver.isCharging()) + && (!SP.getBoolean(R.string.key_tidepool_only_while_unmetered, false) || NetworkChangeReceiver.isWifiConnected()) + && RateLimit.rateLimit("tidepool-new-data-upload", T.mins(4).secs().toInt())) + doUpload() + } + disposable += RxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(Schedulers.io()) + .subscribe({ event -> + if (event.isChanged(R.string.key_tidepool_dev_servers) + || event.isChanged(R.string.key_tidepool_username) + || event.isChanged(R.string.key_tidepool_password) + ) + TidepoolUploader.resetInstance() + }, {}) + disposable += RxBus + .toObservable(EventNetworkChange::class.java) + .observeOn(Schedulers.io()) + .subscribe({}, {}) // TODO start upload on wifi connect + + } + + override fun onStop() { + disposable.clear() + super.onStop() + } + + private fun doUpload() { + if (TidepoolUploader.connectionStatus == TidepoolUploader.ConnectionStatus.DISCONNECTED) + TidepoolUploader.doLogin(true) + else + TidepoolUploader.doUpload() + } + + @Synchronized + private fun addToLog(ev: EventTidepoolStatus) { + synchronized(listLog) { + listLog.add(ev) + // remove the first line if log is too large + if (listLog.size >= Constants.MAX_LOG_LINES) { + listLog.removeAt(0) + } + } + RxBus.send(EventTidepoolUpdateGUI()) + } + + @Synchronized + fun updateLog() { + try { + val newTextLog = StringBuilder() + synchronized(listLog) { + for (log in listLog) { + newTextLog.append(log.toPreparedHtml()) + } + } + @Suppress("DEPRECATION") // API level 24 to replace call + textLog = Html.fromHtml(newTextLog.toString()) + } catch (e: OutOfMemoryError) { + ToastUtils.showToastInUiThread(MainApp.instance().applicationContext, "Out of memory!\nStop using this phone !!!", R.raw.error) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt new file mode 100644 index 0000000000..ca3c3e0483 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/InfoInterceptor.kt @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.logging.L +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import org.slf4j.LoggerFactory +import java.io.IOException + +class InfoInterceptor(tag: String) : Interceptor { + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + private var tag = "interceptor" + + init { + this.tag = tag + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + request?.body()?.let { + if (L.isEnabled(L.TIDEPOOL)) { + log.debug("Interceptor Body size: " + it.contentLength()) + val requestBuffer = Buffer() + it.writeTo(requestBuffer) + log.debug("Interceptor Body: " + requestBuffer.readUtf8()) + } + } + return chain.proceed(request) + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt new file mode 100644 index 0000000000..7067ddd073 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/Session.kt @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.plugins.general.tidepool.messages.AuthReplyMessage +import info.nightscout.androidaps.plugins.general.tidepool.messages.DatasetReplyMessage +import okhttp3.Headers + +class Session(val authHeader: String?, + private val sessionTokenHeader: String, + val service: TidepoolApiService?) { + + internal var token: String? = null + internal var authReply: AuthReplyMessage? = null + internal var datasetReply: DatasetReplyMessage? = null + internal var start: Long = 0 + internal var end: Long = 0 + @Volatile + internal var iterations: Int = 0 + + + fun populateHeaders(headers: Headers) { + if (this.token == null) { + this.token = headers.get(sessionTokenHeader) + } + } + + fun populateBody(obj: Any?) { + if (obj == null) return + if (obj is AuthReplyMessage) { + authReply = obj + } else if (obj is List<*>) { + val list = obj as? List<*>? + + list?.getOrNull(0)?.let { + if (it is DatasetReplyMessage) { + datasetReply = it + } + } + } else if (obj is DatasetReplyMessage) { + datasetReply = obj + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt new file mode 100644 index 0000000000..52adf539c5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolApiService.kt @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.plugins.general.tidepool.messages.AuthReplyMessage +import info.nightscout.androidaps.plugins.general.tidepool.messages.DatasetReplyMessage +import info.nightscout.androidaps.plugins.general.tidepool.messages.UploadReplyMessage +import okhttp3.RequestBody +import retrofit2.Call +import retrofit2.http.* + +const val SESSION_TOKEN_HEADER: String = "x-tidepool-session-token" + +interface TidepoolApiService { + + @Headers( + "User-Agent: AAPS- " + BuildConfig.VERSION_NAME, + "X-Tidepool-Client-Name: info.nightscout.androidaps" + BuildConfig.APPLICATION_ID, + "X-Tidepool-Client-Version: 0.1.0" + ) + + @POST("/auth/login") + fun getLogin(@Header("Authorization") secret: String): Call + + @DELETE("/v1/users/{userId}/data") + fun deleteAllData(@Header(SESSION_TOKEN_HEADER) token: String, @Path("userId") id: String): Call + + @DELETE("/v1/datasets/{dataSetId}") + fun deleteDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("dataSetId") id: String): Call + + @GET("/v1/users/{userId}/data_sets") + fun getOpenDataSets(@Header(SESSION_TOKEN_HEADER) token: String, + @Path("userId") id: String, + @Query("client.name") clientName: String, + @Query("size") size: Int): Call> + + @GET("/v1/datasets/{dataSetId}") + fun getDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("dataSetId") id: String): Call + + @POST("/v1/users/{userId}/data_sets") + fun openDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("userId") id: String, @Body body: RequestBody): Call + + @POST("/v1/datasets/{sessionId}/data") + fun doUpload(@Header(SESSION_TOKEN_HEADER) token: String, @Path("sessionId") id: String, @Body body: RequestBody): Call + + @PUT("/v1/datasets/{sessionId}") + fun closeDataSet(@Header(SESSION_TOKEN_HEADER) token: String, @Path("sessionId") id: String, @Body body: RequestBody): Call + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt new file mode 100644 index 0000000000..58ba913e91 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolCallback.kt @@ -0,0 +1,35 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolStatus +import org.slf4j.LoggerFactory +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +internal class TidepoolCallback(private val session: Session, val name: String, val onSuccess: () -> Unit, val onFail: () -> Unit) : Callback { + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful && response.body() != null) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("$name success") + session.populateBody(response.body()) + session.populateHeaders(response.headers()) + onSuccess() + } else { + val msg = name + " was not successful: " + response.code() + " " + response.message() + if (L.isEnabled(L.TIDEPOOL)) log.debug(msg) + RxBus.send(EventTidepoolStatus(msg)) + onFail() + } + } + + override fun onFailure(call: Call, t: Throwable) { + val msg = "$name Failed: $t" + if (L.isEnabled(L.TIDEPOOL)) log.debug(msg) + RxBus.send(EventTidepoolStatus(msg)) + onFail() + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt new file mode 100644 index 0000000000..5f6d5cc5d1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/TidepoolUploader.kt @@ -0,0 +1,314 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import android.content.Context +import android.os.PowerManager +import android.os.SystemClock +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolStatus +import info.nightscout.androidaps.plugins.general.tidepool.messages.* +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.OKDialog +import info.nightscout.androidaps.utils.SP +import info.nightscout.androidaps.utils.T +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.logging.HttpLoggingInterceptor +import org.slf4j.LoggerFactory +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object TidepoolUploader { + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + private var wl: PowerManager.WakeLock? = null + + private const val INTEGRATION_BASE_URL = "https://int-api.tidepool.org" + private const val PRODUCTION_BASE_URL = "https://api.tidepool.org" + + internal const val VERSION = "0.0.1" + + private var retrofit: Retrofit? = null + + private var session: Session? = null + + enum class ConnectionStatus { + DISCONNECTED, CONNECTING, CONNECTED, FAILED + } + + val PUMPTYPE = "Tandem" + + var connectionStatus: ConnectionStatus = TidepoolUploader.ConnectionStatus.DISCONNECTED + + fun getRetrofitInstance(): Retrofit? { + if (retrofit == null) { + + val httpLoggingInterceptor = HttpLoggingInterceptor() + httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + + val client = OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(InfoInterceptor(TidepoolUploader::class.java.name)) + .build() + + retrofit = Retrofit.Builder() + .baseUrl(if (SP.getBoolean(R.string.key_tidepool_dev_servers, false)) INTEGRATION_BASE_URL else PRODUCTION_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + return retrofit + } + + fun createSession(): Session { + val service = getRetrofitInstance()?.create(TidepoolApiService::class.java) + return Session(AuthRequestMessage.getAuthRequestHeader(), SESSION_TOKEN_HEADER, service) + } + + // TODO: call on preference change + fun resetInstance() { + retrofit = null + if (L.isEnabled(L.TIDEPOOL)) + log.debug("Instance reset") + connectionStatus = TidepoolUploader.ConnectionStatus.DISCONNECTED + } + + @Synchronized + fun doLogin(doUpload: Boolean = false) { + if (connectionStatus == TidepoolUploader.ConnectionStatus.CONNECTED || connectionStatus == TidepoolUploader.ConnectionStatus.CONNECTING) { + if (L.isEnabled(L.TIDEPOOL)) + log.debug("Already connected") + return + } + // TODO failure backoff + extendWakeLock(30000) + session = createSession() + val authHeader = session?.authHeader + if (authHeader != null) { + connectionStatus = TidepoolUploader.ConnectionStatus.CONNECTING + RxBus.send(EventTidepoolStatus(("Connecting"))) + val call = session?.service?.getLogin(authHeader) + + call?.enqueue(TidepoolCallback(session!!, "Login", { + startSession(session!!, doUpload) + }, { + connectionStatus = TidepoolUploader.ConnectionStatus.FAILED + loginFailed() + })) + return + } else { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot do login as user credentials have not been set correctly") + connectionStatus = TidepoolUploader.ConnectionStatus.FAILED + RxBus.send(EventTidepoolStatus(("Invalid credentials"))) + releaseWakeLock() + return + } + } + + fun testLogin(rootContext: Context) { + val session = createSession() + session.authHeader?.let { + val call = session.service?.getLogin(it) + + call?.enqueue(TidepoolCallback(session, "Login", { + OKDialog.show(rootContext, MainApp.gs(R.string.tidepool), "Successfully logged into Tidepool.", null) + }, { + OKDialog.show(rootContext, MainApp.gs(R.string.tidepool), "Failed to log into Tidepool.\nCheck that your user name and password are correct.", null) + })) + + } + ?: OKDialog.show(rootContext, MainApp.gs(R.string.tidepool), "Cannot do login as user credentials have not been set correctly", null) + + } + + private fun loginFailed() { + releaseWakeLock() + } + + private fun startSession(session: Session, doUpload: Boolean = false) { + extendWakeLock(30000) + if (session.authReply?.userid != null) { + // See if we already have an open data set to write to + val datasetCall = session.service!!.getOpenDataSets(session.token!!, + session.authReply!!.userid!!, BuildConfig.APPLICATION_ID, 1) + + datasetCall.enqueue(TidepoolCallback>(session, "Get Open Datasets", { + if (session.datasetReply == null) { + RxBus.send(EventTidepoolStatus(("Creating new dataset"))) + val call = session.service.openDataSet(session.token!!, session.authReply!!.userid!!, OpenDatasetRequestMessage().getBody()) + call.enqueue(TidepoolCallback(session, "Open New Dataset", { + connectionStatus = TidepoolUploader.ConnectionStatus.CONNECTED + RxBus.send(EventTidepoolStatus(("New dataset OK"))) + if (doUpload) doUpload() + else + releaseWakeLock() + }, { + RxBus.send(EventTidepoolStatus(("New dataset FAILED"))) + connectionStatus = TidepoolUploader.ConnectionStatus.FAILED + releaseWakeLock() + })) + } else { + if (L.isEnabled(L.TIDEPOOL)) + log.debug("Existing Dataset: " + session.datasetReply!!.getUploadId()) + // TODO: Wouldn't need to do this if we could block on the above `call.enqueue`. + // ie, do the openDataSet conditionally, and then do `doUpload` either way. + connectionStatus = TidepoolUploader.ConnectionStatus.CONNECTED + RxBus.send(EventTidepoolStatus(("Appending to existing dataset"))) + if (doUpload) doUpload() + else + releaseWakeLock() + } + }, { + connectionStatus = TidepoolUploader.ConnectionStatus.FAILED + RxBus.send(EventTidepoolStatus(("Open dataset FAILED"))) + releaseWakeLock() + })) + } else { + log.error("Got login response but cannot determine userId - cannot proceed") + connectionStatus = TidepoolUploader.ConnectionStatus.FAILED + RxBus.send(EventTidepoolStatus(("Error userId"))) + releaseWakeLock() + } + } + + @Synchronized + fun doUpload() { + session.let { session -> + if (session == null) { + log.error("Session is null, cannot proceed") + releaseWakeLock() + return + } + extendWakeLock(60000) + session.iterations++ + val chunk = UploadChunk.getNext(session) + when { + chunk == null -> { + log.error("Upload chunk is null, cannot proceed") + releaseWakeLock() + } + + chunk.length == 2 -> { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Empty dataset - marking as succeeded") + RxBus.send(EventTidepoolStatus(("No data to upload"))) + releaseWakeLock() + unploadNext() + } + + else -> { + val body = RequestBody.create(MediaType.parse("application/json"), chunk) + + RxBus.send(EventTidepoolStatus(("Uploading"))) + val call = session.service!!.doUpload(session.token!!, session.datasetReply!!.getUploadId()!!, body) + call.enqueue(TidepoolCallback(session, "Data Upload", { + setLastEnd(session.end) + RxBus.send(EventTidepoolStatus(("Upload completed OK"))) + releaseWakeLock() + unploadNext() + }, { + RxBus.send(EventTidepoolStatus(("Upload FAILED"))) + releaseWakeLock() + })) + } + } + } + } + + private fun unploadNext() { + if (getLastEnd() < DateUtil.now() - T.mins(1).msecs()) { + SystemClock.sleep(3000) + if (L.isEnabled(L.TIDEPOOL)) + log.debug("Restarting doUpload. Last: " + DateUtil.dateAndTimeString(getLastEnd())) + doUpload() + } + } + + fun deleteDataSet() { + if (session?.datasetReply?.id != null) { + extendWakeLock(60000) + val call = session!!.service?.deleteDataSet(session!!.token!!, session!!.datasetReply!!.id!!) + call?.enqueue(TidepoolCallback(session!!, "Delete Dataset", { + connectionStatus = TidepoolUploader.ConnectionStatus.DISCONNECTED + RxBus.send(EventTidepoolStatus(("Dataset removed OK"))) + releaseWakeLock() + }, { + connectionStatus = TidepoolUploader.ConnectionStatus.DISCONNECTED + RxBus.send(EventTidepoolStatus(("Dataset remove FAILED"))) + releaseWakeLock() + })) + } else { + log.error("Got login response but cannot determine datasetId - cannot proceed") + } + } + + fun deleteAllData() { + val session = this.session + val token = session?.token + val userid = session?.authReply?.userid + try { + requireNotNull(session) + requireNotNull(token) + requireNotNull(userid) + extendWakeLock(60000) + val call = session.service?.deleteAllData(token, userid) + call?.enqueue(TidepoolCallback(session, "Delete all data", { + connectionStatus = TidepoolUploader.ConnectionStatus.DISCONNECTED + RxBus.send(EventTidepoolStatus(("All data removed OK"))) + releaseWakeLock() + }, { + connectionStatus = TidepoolUploader.ConnectionStatus.DISCONNECTED + RxBus.send(EventTidepoolStatus(("All data remove FAILED"))) + releaseWakeLock() + })) + } catch (e: IllegalArgumentException) { + log.error("Got login response but cannot determine userId - cannot proceed") + } + } + + fun getLastEnd(): Long { + val result = SP.getLong(R.string.key_tidepool_last_end, 0) + return Math.max(result, DateUtil.now() - T.months(2).msecs()) + } + + fun setLastEnd(time: Long) { + if (time > getLastEnd()) { + SP.putLong(R.string.key_tidepool_last_end, time) + val friendlyEnd = DateUtil.dateAndTimeString(time) + RxBus.send(EventTidepoolStatus(("Marking uploaded data up to $friendlyEnd"))) + if (L.isEnabled(L.TIDEPOOL)) log.debug("Updating last end to: " + DateUtil.dateAndTimeString(time)) + } else { + if (L.isEnabled(L.TIDEPOOL)) log.debug("Cannot set last end to: " + DateUtil.dateAndTimeString(time) + " vs " + DateUtil.dateAndTimeString(getLastEnd())) + } + } + + @Synchronized + private fun extendWakeLock(ms: Long) { + if (wl == null) { + val pm = MainApp.instance().getSystemService(Context.POWER_SERVICE) as PowerManager + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AndroidAPS:TidepoolUploader") + wl?.acquire(ms) + } else { + releaseWakeLock() // lets not get too messy + wl?.acquire(ms) + } + } + + @Synchronized + private fun releaseWakeLock() { + wl?.let { + if (it.isHeld) { + try { + it.release() + } catch (e: Exception) { + log.error("Error releasing wakelock: $e") + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt new file mode 100644 index 0000000000..40cab06f4c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/comm/UploadChunk.kt @@ -0,0 +1,130 @@ +package info.nightscout.androidaps.plugins.general.tidepool.comm + +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.general.tidepool.elements.* +import info.nightscout.androidaps.plugins.general.tidepool.events.EventTidepoolStatus +import info.nightscout.androidaps.plugins.general.tidepool.utils.GsonInstance +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.SP +import info.nightscout.androidaps.utils.T +import org.slf4j.LoggerFactory +import java.util.* + +object UploadChunk { + + private val MAX_UPLOAD_SIZE = T.days(7).msecs() // don't change this + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + fun getNext(session: Session?): String? { + if (session == null) + return null + + session.start = TidepoolUploader.getLastEnd() + session.end = Math.min(session.start + MAX_UPLOAD_SIZE, DateUtil.now()) + + val result = get(session.start, session.end) + if (result.length < 3) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("No records in this time period, setting start to best end time") + TidepoolUploader.setLastEnd(Math.max(session.end, getOldestRecordTimeStamp())) + } + return result + } + + operator fun get(start: Long, end: Long): String { + + if (L.isEnabled(L.TIDEPOOL)) log.debug("Syncing data between: " + DateUtil.dateAndTimeString(start) + " -> " + DateUtil.dateAndTimeString(end)) + if (end <= start) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("End is <= start: " + DateUtil.dateAndTimeString(start) + " " + DateUtil.dateAndTimeString(end)) + return "" + } + if (end - start > MAX_UPLOAD_SIZE) { + if (L.isEnabled(L.TIDEPOOL)) log.debug("More than max range - rejecting") + return "" + } + + val records = LinkedList() + + if (SP.getBoolean(R.string.key_tidepool_upload_bolus, true)) + records.addAll(getTreatments(start, end)) + if (SP.getBoolean(R.string.key_tidepool_upload_bg, true)) + records.addAll(getBloodTests(start, end)) + if (SP.getBoolean(R.string.key_tidepool_upload_tbr, true)) + records.addAll(getBasals(start, end)) + if (SP.getBoolean(R.string.key_tidepool_upload_cgm, true)) + records.addAll(getBgReadings(start, end)) + if (SP.getBoolean(R.string.key_tidepool_upload_profile, true)) + records.addAll(getProfiles(start, end)) + + return GsonInstance.defaultGsonInstance().toJson(records) + } + + // numeric limits must match max time windows + + private fun getOldestRecordTimeStamp(): Long { + // TODO we could make sure we include records older than the first bg record for completeness + + val start: Long = 0 + val end = DateUtil.now() + + val bgReadingList = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true) + return if (bgReadingList.size > 0) + bgReadingList[0].date + else -1 + } + + private fun getTreatments(start: Long, end: Long): List { + val result = LinkedList() + val treatments = TreatmentsPlugin.getPlugin().service.getTreatmentDataFromTime(start, end, true) + for (treatment in treatments) { + if (treatment.carbs > 0) { + result.add(WizardElement(treatment)) + } else if (treatment.insulin > 0) { + result.add(BolusElement(treatment)) + } + } + return result + } + + private fun getBloodTests(start: Long, end: Long): List { + val readings = MainApp.getDbHelper().getCareportalEvents(start, end, true) + val selection = BloodGlucoseElement.fromCareportalEvents(readings) + if (selection.isNotEmpty()) + RxBus.send(EventTidepoolStatus("${selection.size} BGs selected for upload")) + return selection + + } + + internal fun getBgReadings(start: Long, end: Long): List { + val readings = MainApp.getDbHelper().getBgreadingsDataFromTime(start, end, true) + val selection = SensorGlucoseElement.fromBgReadings(readings) + if (selection.isNotEmpty()) + RxBus.send(EventTidepoolStatus("${selection.size} CGMs selected for upload")) + return selection + } + + private fun getBasals(start: Long, end: Long): List { + val tbrs = TreatmentsPlugin.getPlugin().temporaryBasalsFromHistory + tbrs.merge() + val selection = BasalElement.fromTemporaryBasals(tbrs, start, end) // TODO do not upload running TBR + if (selection.isNotEmpty()) + RxBus.send(EventTidepoolStatus("${selection.size} TBRs selected for upload")) + return selection + } + + private fun getProfiles(start: Long, end: Long): List { + val pss = MainApp.getDbHelper().getProfileSwitchEventsFromTime(start, end, true) + val selection = LinkedList() + for (ps in pss) { + ProfileElement.newInstanceOrNull(ps)?.let { selection.add(it) } + } + if (selection.size > 0) + RxBus.send(EventTidepoolStatus("${selection.size} ProfileSwitches selected for upload")) + return selection + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt new file mode 100644 index 0000000000..11478afa66 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BasalElement.kt @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.data.Intervals +import info.nightscout.androidaps.db.TemporaryBasal +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions +import java.util.* + +class BasalElement(tbr: TemporaryBasal) + : BaseElement(tbr.date, UUID.nameUUIDFromBytes(("AAPS-basal" + tbr.date).toByteArray()).toString()) { + + internal var timestamp: Long = 0 // not exposed + + @Expose + internal var deliveryType = "automated" + @Expose + internal var duration: Long = 0 + @Expose + internal var rate = -1.0 + @Expose + internal var scheduleName = "AAPS" + @Expose + internal var clockDriftOffset: Long = 0 + @Expose + internal var conversionOffset: Long = 0 + + init { + type = "basal" + timestamp = tbr.date + rate = tbr.tempBasalConvertedToAbsolute(tbr.date, ProfileFunctions.getInstance().getProfile(tbr.date)) + duration = tbr.end() - tbr.start() + } + + companion object { + internal fun fromTemporaryBasals(tbrList: Intervals, start: Long, end: Long): List { + val results = LinkedList() + for (tbr in tbrList.list) { + if (tbr.date >= start && tbr.date <= end && tbr.durationInMinutes != 0) + results.add(BasalElement(tbr)) + } + return results + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt new file mode 100644 index 0000000000..96900e62d6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BaseElement.kt @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.utils.DateUtil + +open class BaseElement(timestamp: Long, uuid: String) { + @Expose + var deviceTime: String = "" + @Expose + var time: String = "" + @Expose + var timezoneOffset: Int = 0 + @Expose + var type: String? = null + @Expose + var origin: Origin? = null + + init { + deviceTime = DateUtil.toISONoZone(timestamp) + time = DateUtil.toISOAsUTC(timestamp) + timezoneOffset = DateUtil.getTimeZoneOffsetMinutes(timestamp) // TODO + origin = Origin(uuid) + } + + inner class Origin internal constructor(@field:Expose + internal var id: String) +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt new file mode 100644 index 0000000000..548df0388d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BloodGlucoseElement.kt @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.Constants +import info.nightscout.androidaps.data.Profile +import info.nightscout.androidaps.db.CareportalEvent +import info.nightscout.androidaps.utils.JsonHelper +import org.json.JSONObject +import java.util.* + +class BloodGlucoseElement(careportalEvent: CareportalEvent) + : BaseElement(careportalEvent.date, UUID.nameUUIDFromBytes(("AAPS-bg" + careportalEvent.date).toByteArray()).toString()) { + + @Expose + var subType: String = "manual" + @Expose + var units: String = "mg/dL" + @Expose + var value: Int = 0 + + init { + type = "cbg" + subType = "manual" // TODO + val json = if (careportalEvent.json != null) JSONObject(careportalEvent.json) else JSONObject() + value = Profile.toMgdl(JsonHelper.safeGetDouble(json, "glucose"), JsonHelper.safeGetString(json, "units", Constants.MGDL)).toInt() + } + + companion object { + + fun fromCareportalEvents(careportalList: List): List { + val results = LinkedList() + for (bt in careportalList) { + if (bt.eventType == CareportalEvent.MBG || bt.eventType == CareportalEvent.BGCHECK) { + val bge = BloodGlucoseElement(bt) + if (bge.value > 0) + results.add(BloodGlucoseElement(bt)) + } + } + return results + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt new file mode 100644 index 0000000000..3d8dcbe964 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/BolusElement.kt @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.plugins.treatments.Treatment +import java.util.* + +class BolusElement(treatment: Treatment) + : BaseElement(treatment.date, UUID.nameUUIDFromBytes(("AAPS-bolus" + treatment.date).toByteArray()).toString()) { + + @Expose + var subType = "normal" + @Expose + var normal: Double = 0.0 + @Expose + var expectedNormal: Double = 0.0 + + init { + type = "bolus" + normal = treatment.insulin + expectedNormal = treatment.insulin + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/ProfileElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/ProfileElement.kt new file mode 100644 index 0000000000..93009ea7e1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/ProfileElement.kt @@ -0,0 +1,111 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.data.Profile +import info.nightscout.androidaps.db.ProfileSwitch +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import info.nightscout.androidaps.utils.InstanceId +import java.util.* +import kotlin.collections.ArrayList + +class ProfileElement private constructor(ps: ProfileSwitch) + : BaseElement(ps.date, UUID.nameUUIDFromBytes(("AAPS-profile" + ps.date).toByteArray()).toString()) { + + @Expose + internal var activeSchedule = "Normal" + @Expose + internal var basalSchedules: BasalProfile = BasalProfile() + @Expose + internal var units: Units = Units() + @Expose + internal var bgTargets: TargetProfile = TargetProfile() + @Expose + internal var carbRatios: IcProfile = IcProfile() + @Expose + internal var insulinSensitivities: IsfProfile = IsfProfile() + @Expose + internal var deviceId: String = TidepoolUploader.PUMPTYPE + ":" + (ConfigBuilderPlugin.getPlugin().activePump?.serialNumber() + ?: InstanceId.instanceId()) + @Expose + internal var deviceSerialNumber: String = ConfigBuilderPlugin.getPlugin().activePump?.serialNumber() + ?: InstanceId.instanceId() + @Expose + internal var clockDriftOffset: Long = 0 + @Expose + internal var conversionOffset: Long = 0 + + init { + type = "pumpSettings" + val profile: Profile? = ps.profileObject + checkNotNull(profile) + for (br in profile.basalValues) + basalSchedules.Normal.add(BasalRate(br.timeAsSeconds * 1000, br.value)) + for (target in profile.singleTargets) + bgTargets.Normal.add(Target(target.timeAsSeconds * 1000, Profile.toMgdl(target.value, profile.units))) + for (ic in profile.ics) + carbRatios.Normal.add(Ratio(ic.timeAsSeconds * 1000, ic.value)) + for (isf in profile.isfs) + insulinSensitivities.Normal.add(Ratio(isf.timeAsSeconds * 1000, Profile.toMgdl(isf.value, profile.units))) + } + + inner class BasalProfile internal constructor( + @field:Expose + internal var Normal: ArrayList = ArrayList() // must be the same var name as activeSchedule + ) + + inner class BasalRate internal constructor( + @field:Expose + internal var start: Int, + @field:Expose + internal var rate: Double + ) + + inner class Units internal constructor( + @field:Expose + internal var carb: String = "grams", + @field:Expose + internal var bg: String = "mg/dL" + ) + + inner class TargetProfile internal constructor( + @field:Expose + internal var Normal: ArrayList = ArrayList() // must be the same var name as activeSchedule + ) + + inner class Target internal constructor( + @field:Expose + internal var start: Int, + @field:Expose + internal var target: Double + ) + + inner class IcProfile internal constructor( + @field:Expose + internal var Normal: ArrayList = ArrayList() // must be the same var name as activeSchedule + ) + + inner class IsfProfile internal constructor( + @field:Expose + internal var Normal: ArrayList = ArrayList() // must be the same var name as activeSchedule + ) + + inner class Ratio internal constructor( + @field:Expose + internal var start: Int, + @field:Expose + internal var amount: Double + ) + + companion object { + @JvmStatic + fun newInstanceOrNull(ps: ProfileSwitch): ProfileElement? = try { + ProfileElement(ps) + } catch (e: Throwable) { + null + } + } + +} + + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt new file mode 100644 index 0000000000..ca2aceca1c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/SensorGlucoseElement.kt @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.db.BgReading +import java.util.* + +class SensorGlucoseElement(bgReading: BgReading) + : BaseElement(bgReading.date, UUID.nameUUIDFromBytes(("AAPS-cgm" + bgReading.date).toByteArray()).toString()) { + + @Expose + internal var units: String = "mg/dL" + @Expose + internal var value: Int = 0 + + init { + this.type = "cbg" + value = bgReading.value.toInt() + } + + companion object { + internal fun fromBgReadings(bgReadingList: List): List { + val results = LinkedList() + for (bgReading in bgReadingList) { + results.add(SensorGlucoseElement(bgReading)) + } + return results + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt new file mode 100644 index 0000000000..d7641862fe --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/elements/WizardElement.kt @@ -0,0 +1,68 @@ +package info.nightscout.androidaps.plugins.general.tidepool.elements + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.plugins.treatments.Treatment +import java.util.* + +class WizardElement(treatment: Treatment) + : BaseElement(treatment.date, UUID.nameUUIDFromBytes(("AAPS-wizard" + treatment.date).toByteArray()).toString()) { + + @Expose + var units = "mg/dL" + @Expose + var carbInput: Double = 0.toDouble() + @Expose + var insulinCarbRatio: Double = 0.toDouble() + @Expose + var bolus: BolusElement? = null + + init { + type = "wizard" + carbInput = treatment.carbs + insulinCarbRatio = treatment.ic + if (treatment.insulin > 0) { + bolus = BolusElement(treatment) + } else { + val fake = Treatment() + fake.insulin = 0.0001 + fake.date = treatment.date + bolus = BolusElement(fake) // fake insulin record + } + } +} + +/* TODO fill the rest +{ + "type": "wizard", + "bgInput": 16.152676653942503, + "bgTarget": { + "low": 3.6079861941795968, + "high": 6.938434988806917 + }, + "bolus": "22239d4d592b48ae920b28971cceb48b", + "carbInput": 57, + "insulinCarbRatio": 24, + "insulinOnBoard": 24.265, + "insulinSensitivity": 4.329583433015516, + "recommended": { + "carb": 2.5, + "correction": 2.25, + "net": 0 + }, + "units": "mmol/L", + "_active": true, + "_groupId": "abcdef", + "_schemaVersion": 0, + "_version": 0, + "clockDriftOffset": 0, + "conversionOffset": 0, + "createdTime": "2018-05-14T08:17:14.353Z", + "deviceId": "DevId0987654321", + "deviceTime": "2018-05-14T18:17:09", + "guid": "18d90ea0-5915-4e95-a8b2-cb22819ce696", + "id": "087c94ccdae84eb5a76b8205a244ec6b", + "time": "2018-05-14T08:17:09.353Z", + "timezoneOffset": 600, + "uploadId": "SampleUploadId" +} + */ \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt new file mode 100644 index 0000000000..d20868f348 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolDoUpload.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +import info.nightscout.androidaps.events.Event + +class EventTidepoolDoUpload : Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt new file mode 100644 index 0000000000..833353e6b0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolResetData.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +import info.nightscout.androidaps.events.Event + +class EventTidepoolResetData :Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt new file mode 100644 index 0000000000..aa69c11e95 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolStatus.kt @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +import info.nightscout.androidaps.events.Event +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.LocaleHelper +import org.slf4j.LoggerFactory +import java.text.SimpleDateFormat + +class EventTidepoolStatus(val status: String) : Event() { + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + var date: Long = DateUtil.now() + + init { + if (L.isEnabled(L.TIDEPOOL)) + log.debug("New status: $status") + } + + private var timeFormat = SimpleDateFormat("HH:mm:ss", LocaleHelper.getLocale()) + + fun toPreparedHtml(): StringBuilder { + val stringBuilder = StringBuilder() + stringBuilder.append(timeFormat.format(date)) + stringBuilder.append(" ") + stringBuilder.append(status) + stringBuilder.append(" ") + stringBuilder.append("
") + return stringBuilder + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolUpdateGUI.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolUpdateGUI.kt new file mode 100644 index 0000000000..de2b353dc0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/events/EventTidepoolUpdateGUI.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.general.tidepool.events + +import info.nightscout.androidaps.events.Event + +class EventTidepoolUpdateGUI : Event() diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt new file mode 100644 index 0000000000..57fa45c691 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthReplyMessage.kt @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +class AuthReplyMessage { + + @Expose + @SerializedName("emailVerified") + internal var emailVerified: Boolean? = null + @Expose + @SerializedName("emails") + internal var emailList: List? = null + @Expose + @SerializedName("termsAccepted") + internal var termsDate: String? = null + @Expose + @SerializedName("userid") + internal var userid: String? = null + @Expose + @SerializedName("username") + internal var username: String? = null +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt new file mode 100644 index 0000000000..33c230ea75 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/AuthRequestMessage.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.SP +import info.nightscout.androidaps.utils.StringUtils +import okhttp3.Credentials + +class AuthRequestMessage : BaseMessage() { + companion object { + fun getAuthRequestHeader(): String? { + val username = SP.getString(R.string.key_tidepool_username, null) + val password = SP.getString(R.string.key_tidepool_password, null) + + return if (StringUtils.emptyString(username) || StringUtils.emptyString(password)) null else Credentials.basic(username.trim { it <= ' ' }, password) + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt new file mode 100644 index 0000000000..d3cdffcf8c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/BaseMessage.kt @@ -0,0 +1,16 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import info.nightscout.androidaps.plugins.general.tidepool.utils.GsonInstance +import okhttp3.MediaType +import okhttp3.RequestBody + +open class BaseMessage { + private fun toS(): String { + return GsonInstance.defaultGsonInstance().toJson(this) ?: "null" + } + + fun getBody(): RequestBody { + return RequestBody.create(MediaType.parse("application/json"), this.toS()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt new file mode 100644 index 0000000000..f8f6780971 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/CloseDatasetRequestMessage.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import com.google.gson.annotations.Expose + +class CloseDatasetRequestMessage : BaseMessage() { + @Expose + internal var dataState = "closed" +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt new file mode 100644 index 0000000000..f19070624b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/DatasetReplyMessage.kt @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + + +class DatasetReplyMessage { + + internal var data: Data? = null + + // openDataSet and others return this in the root of the json reply it seems + internal var id: String? = null + internal var uploadId: String? = null + + inner class Data { + internal var createdTime: String? = null + internal var deviceId: String? = null + internal var id: String? = null + internal var time: String? = null + internal var timezone: String? = null + internal var timezoneOffset: Int = 0 + internal var type: String? = null + internal var uploadId: String? = null + internal var client: Client? = null + internal var computerTime: String? = null + internal var dataSetType: String? = null + internal var deviceManufacturers: List? = null + internal var deviceModel: String? = null + internal var deviceSerialNumber: String? = null + internal var deviceTags: List? = null + internal var timeProcessing: String? = null + internal var version: String? = null + // meta + } + + inner class Client { + internal var name: String? = null + internal var version: String? = null + + } + + fun getUploadId(): String? { + return data?.uploadId ?: uploadId + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt new file mode 100644 index 0000000000..38b93b2f7b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/OpenDatasetRequestMessage.kt @@ -0,0 +1,57 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +import com.google.gson.annotations.Expose +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin +import info.nightscout.androidaps.plugins.general.tidepool.comm.TidepoolUploader +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.InstanceId +import info.nightscout.androidaps.utils.T +import java.util.* + +class OpenDatasetRequestMessage : BaseMessage() { + + @Expose + var deviceId: String = TidepoolUploader.PUMPTYPE + ":" + (ConfigBuilderPlugin.getPlugin().activePump?.serialNumber() + ?: InstanceId.instanceId()) + @Expose + var time = DateUtil.toISOAsUTC(DateUtil.now()) + @Expose + var timezoneOffset = (DateUtil.getTimeZoneOffsetMs() / T.mins(1).msecs()).toInt() + @Expose + var type = "upload" + //public String byUser; + @Expose + var client = ClientInfo() + @Expose + var computerTime = DateUtil.toISONoZone(DateUtil.now()) + @Expose + var dataSetType = "continuous" + @Expose + var deviceManufacturers = arrayOf(TidepoolUploader.PUMPTYPE) + @Expose + var deviceModel = TidepoolUploader.PUMPTYPE + @Expose + var deviceTags = arrayOf("bgm", "cgm", "insulin-pump") + @Expose + var deduplicator = Deduplicator() + @Expose + var timeProcessing = "none" + @Expose + var timezone = TimeZone.getDefault().id + @Expose + var version = BuildConfig.VERSION_NAME + + inner class ClientInfo { + @Expose + val name = BuildConfig.APPLICATION_ID + @Expose + val version = TidepoolUploader.VERSION + } + + inner class Deduplicator { + @Expose + val name = "org.tidepool.deduplicator.dataset.delete.origin" + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt new file mode 100644 index 0000000000..2054eb237a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/messages/UploadReplyMessage.kt @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.general.tidepool.messages + +class UploadReplyMessage { + + internal var data: List? = null +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt new file mode 100644 index 0000000000..2c7ceb81d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/GsonInstance.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.general.tidepool.utils + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +object GsonInstance { + private var gson_instance: Gson? = null + + fun defaultGsonInstance(): Gson { + if (gson_instance == null) { + gson_instance = GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create() + } + return gson_instance as Gson + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt new file mode 100644 index 0000000000..88f09abdc2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/tidepool/utils/RateLimit.kt @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.general.tidepool.utils + +import info.nightscout.androidaps.logging.L +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.T +import org.slf4j.LoggerFactory +import java.util.* + +object RateLimit { + + private val rateLimits = HashMap() + + private val log = LoggerFactory.getLogger(L.TIDEPOOL) + + // return true if below rate limit + @Synchronized + fun rateLimit(name: String, seconds: Int): Boolean { + // check if over limit + rateLimits[name]?.let { + if (DateUtil.now() - it < T.secs(seconds.toLong()).msecs()) { + if (L.isEnabled(L.TIDEPOOL)) + log.debug("$name rate limited: $seconds seconds") + return false + } + } + // not over limit + rateLimits[name] = DateUtil.now() + return true + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/combo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/combo/ComboPlugin.java index 9f7c986d26..3fcf613edb 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/combo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/combo/ComboPlugin.java @@ -65,6 +65,7 @@ import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.treatments.Treatment; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.InstanceId; import info.nightscout.androidaps.utils.SP; /** * Created by mike on 05.08.2016. @@ -1312,10 +1313,20 @@ public class ComboPlugin extends PluginBase implements PumpInterface, Constraint } @Override - public String deviceID() { + public String manufacter() { + return "Roche"; + } + + @Override + public String model() { return "Combo"; } + @Override + public String serialNumber() { + return InstanceId.INSTANCE.instanceId(); // TODO replace by real serial + } + @Override public PumpDescription getPumpDescription() { return pumpDescription; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java index f879774c48..b9df956b0f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java @@ -12,13 +12,13 @@ import info.nightscout.androidaps.utils.Round; /** * Created by andy on 02/05/2018. - *

+ * * Most of this defintions is intended for VirtualPump only, but they can be used by other plugins. */ public enum PumpType { - GenericAAPS("Generic AAPS", 0.1d, null, // + GenericAAPS("Generic AAPS", "AndroidAPS", "VirutalPump", 0.1d, null, // new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Percent, // new DoseSettings(10, 30, 24 * 60, 0d, 500d), PumpCapability.BasalRate_Duration15and30minAllowed, // @@ -26,7 +26,7 @@ public enum PumpType { // Cellnovo - Cellnovo1("Cellnovo", 0.05d, null, // + Cellnovo1("Cellnovo", "Cellnovo", "Cellnovo", 0.05d, null, // new DoseSettings(0.05d, 30, 24 * 60, 1d, null), PumpTempBasalType.Percent, new DoseSettings(5, 30, 24 * 60, 0d, 200d), PumpCapability.BasalRate_Duration30minAllowed, // @@ -34,105 +34,107 @@ public enum PumpType { // Accu-Chek - AccuChekCombo("Accu-Chek Combo", 0.1d, null, // + AccuChekCombo("Accu-Chek Combo", "Roche", "Combo", 0.1d, null, // new DoseSettings(0.1d, 15, 12 * 60, 0.1d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 12 * 60, 0d, 500d), PumpCapability.BasalRate_Duration15and30minAllowed, // 0.01d, 0.01d, DoseStepSize.ComboBasal, PumpCapability.ComboCapabilities), // - AccuChekSpirit("Accu-Chek Spirit", 0.1d, null, // + AccuChekSpirit("Accu-Chek Spirit", "Roche", "Spirit", 0.1d, null, // new DoseSettings(0.1d, 15, 12 * 60, 0.1d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 12 * 60, 0d, 500d), PumpCapability.BasalRate_Duration15and30minAllowed, // 0.01d, 0.1d, null, PumpCapability.VirtualPumpCapabilities), // - AccuChekInsight("Accu-Chek Insight", 0.05d, DoseStepSize.InsightBolus, // + AccuChekInsight("Accu-Chek Insight", "Roche", "Insight", 0.05d, DoseStepSize.InsightBolus, // new DoseSettings(0.05d, 15, 24 * 60, 0.05d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 24 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // 0.02d, 0.01d, null, PumpCapability.InsightCapabilities), // - AccuChekInsightBluetooth("Accu-Chek Insight", 0.01d, null, // + AccuChekInsightBluetooth("Accu-Chek Insight", "Roche", "Insight", 0.01d, null, // new DoseSettings(0.01d, 15, 24 * 60, 0.05d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 24 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // 0.02d, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities), // // Animas - AnimasVibe("Animas Vibe", 0.05d, null, // AnimasBolus? + AnimasVibe("Animas Vibe","Animas", "Vibe", 0.05d, null, // AnimasBolus? new DoseSettings(0.05d, 30, 12 * 60, 0.05d), // PumpTempBasalType.Percent, // new DoseSettings(10, 30, 24 * 60, 0d, 300d), PumpCapability.BasalRate_Duration30minAllowed, // 0.025d, 5d, 0d, null, PumpCapability.VirtualPumpCapabilities), // - AnimasPing("Animas Ping", AnimasVibe), + AnimasPing("Animas Ping", "Ping", AnimasVibe), // Dana - DanaR("DanaR", 0.05d, null, // + DanaR("DanaR", "SOOIL", "DanaR", 0.05d, null, // new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Percent, // new DoseSettings(10d, 60, 24 * 60, 0d, 200d), PumpCapability.BasalRate_Duration15and30minNotAllowed, // 0.04d, 0.01d, null, PumpCapability.DanaCapabilities), - DanaRKorean("DanaR Korean", 0.05d, null, // + DanaRKorean("DanaR Korean", "SOOIL", "DanaRKorean", 0.05d, null, // new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Percent, // new DoseSettings(10d, 60, 24 * 60, 0d, 200d), PumpCapability.BasalRate_Duration15and30minNotAllowed, // 0.1d, 0.01d, null, PumpCapability.DanaCapabilities), - DanaRS("DanaRS", 0.05d, null, // + DanaRS("DanaRS", "SOOIL", "DanaRS", 0.05d, null, // new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Percent, // new DoseSettings(10d, 60, 24 * 60, 0d, 200d), PumpCapability.BasalRate_Duration15and30minAllowed, // 0.04d, 0.01d, null, PumpCapability.DanaWithHistoryCapabilities), - DanaRv2("DanaRv2", DanaRS), + DanaRv2("DanaRv2", "DanaRv2", DanaRS), // Insulet - Insulet_Omnipod("Insulet Omnipod", 0.05d, null, // + Insulet_Omnipod("Insulet Omnipod", "Insulet", "Omnipod", 0.05d, null, // new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Absolute, // new DoseSettings(0.05d, 30, 12 * 60, 0d, 30.0d), PumpCapability.BasalRate_Duration30minAllowed, // cannot exceed max basal rate 30u/hr 0.05d, 0.05d, null, PumpCapability.VirtualPumpCapabilities), // Medtronic - Medtronic_512_712("Medtronic 512/712", 0.05d, null, // - new DoseSettings(0.05d, 30, 8*60, 0.05d), // + Medtronic_512_712("Medtronic 512/712", "Medtronic", "512/712", 0.05d, null, // + new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Absolute, // new DoseSettings(0.05d, 30, 24*60, 0d, 35d), PumpCapability.BasalRate_Duration30minAllowed, // 0.05d, 0.05d, null, PumpCapability.MedtronicCapabilities), // - Medtronic_515_715("Medtronic 515/715", Medtronic_512_712), - Medtronic_522_722("Medtronic 522/722", Medtronic_512_712), + Medtronic_515_715("Medtronic 515/715", "515/715", Medtronic_512_712), + Medtronic_522_722("Medtronic 522/722", "522/722", Medtronic_512_712), - Medtronic_523_723_Revel("Medtronic 523/723 (Revel)", 0.05d, null, // - new DoseSettings(0.05d, 30, 8*60, 0.05d), // + Medtronic_523_723_Revel("Medtronic 523/723 (Revel)", "Medtronic", "523/723 (Revel)", 0.05d, null, // + new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Absolute, // new DoseSettings(0.05d, 30, 24*60, 0d, 35d), PumpCapability.BasalRate_Duration30minAllowed, // 0.025d, 0.025d, DoseStepSize.MedtronicVeoBasal, PumpCapability.MedtronicCapabilities), // - Medtronic_554_754_Veo("Medtronic 554/754 (Veo)", Medtronic_523_723_Revel), // + Medtronic_554_754_Veo("Medtronic 554/754 (Veo)", "554/754 (Veo)", Medtronic_523_723_Revel), // TODO - Medtronic_640G("Medtronic 640G", 0.025d, null, // + Medtronic_640G("Medtronic 640G", "Medtronic", "640G", 0.025d, null, // new DoseSettings(0.05d, 30, 8 * 60, 0.05d), // PumpTempBasalType.Absolute, // new DoseSettings(0.05d, 30, 24 * 60, 0d, 35d), PumpCapability.BasalRate_Duration30minAllowed, // 0.025d, 0.025d, DoseStepSize.MedtronicVeoBasal, PumpCapability.VirtualPumpCapabilities), // // Tandem - TandemTSlim("Tandem t:slim", 0.01d, null, // + TandemTSlim("Tandem t:slim", "Tandem", "t:slim", 0.01d, null, // new DoseSettings(0.01d, 15, 8 * 60, 0.4d), PumpTempBasalType.Percent, new DoseSettings(1, 15, 8 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // 0.1d, 0.001d, null, PumpCapability.VirtualPumpCapabilities), - TandemTFlex("Tandem t:flex", TandemTSlim), // - TandemTSlimG4("Tandem t:slim G4", TandemTSlim), // - TandemTSlimX2("Tandem t:slim X2", TandemTSlim), // + TandemTFlex("Tandem t:flex", "t:flex", TandemTSlim), // + TandemTSlimG4("Tandem t:slim G4", "t:slim G4", TandemTSlim), // + TandemTSlimX2("Tandem t:slim X2", "t:slim X2", TandemTSlim), // ; private String description; + private String manufacter; + private String model; private double bolusSize; private DoseStepSize specialBolusSize; private DoseSettings extendedBolusSettings; @@ -157,29 +159,36 @@ public enum PumpType { } - PumpType(String description, PumpType parent) { + PumpType(String description, String model, PumpType parent) + { this.description = description; this.parent = parent; + parent.model = model; } - PumpType(String description, PumpType parent, PumpCapability pumpCapability) { + PumpType(String description, String model, PumpType parent, PumpCapability pumpCapability) + { this.description = description; this.parent = parent; this.pumpCapability = pumpCapability; + parent.model = model; } - PumpType(String description, double bolusSize, DoseStepSize specialBolusSize, // + PumpType(String description, String manufacter, String model, double bolusSize, DoseStepSize specialBolusSize, // DoseSettings extendedBolusSettings, // - PumpTempBasalType pumpTempBasalType, DoseSettings tbrSettings, PumpCapability specialBasalDurations, // - double baseBasalMinValue, double baseBasalStep, DoseStepSize baseBasalSpecialSteps, PumpCapability pumpCapability) { - this(description, bolusSize, specialBolusSize, extendedBolusSettings, pumpTempBasalType, tbrSettings, specialBasalDurations, baseBasalMinValue, null, baseBasalStep, baseBasalSpecialSteps, pumpCapability); + PumpTempBasalType pumpTempBasalType, DoseSettings tbrSettings, PumpCapability specialBasalDurations, // + double baseBasalMinValue, double baseBasalStep, DoseStepSize baseBasalSpecialSteps, PumpCapability pumpCapability) + { + this(description, manufacter, model, bolusSize, specialBolusSize, extendedBolusSettings, pumpTempBasalType, tbrSettings, specialBasalDurations, baseBasalMinValue, null, baseBasalStep, baseBasalSpecialSteps, pumpCapability); } - PumpType(String description, double bolusSize, DoseStepSize specialBolusSize, // + PumpType(String description, String manufacter, String model, double bolusSize, DoseStepSize specialBolusSize, // DoseSettings extendedBolusSettings, // PumpTempBasalType pumpTempBasalType, DoseSettings tbrSettings, PumpCapability specialBasalDurations, // double baseBasalMinValue, Double baseBasalMaxValue, double baseBasalStep, DoseStepSize baseBasalSpecialSteps, PumpCapability pumpCapability) { this.description = description; + this.manufacter = manufacter; + this.model = model; this.bolusSize = bolusSize; this.specialBolusSize = specialBolusSize; this.extendedBolusSettings = extendedBolusSettings; @@ -198,6 +207,14 @@ public enum PumpType { return description; } + public String getManufacter() { + return isParentSet() ? parent.manufacter : manufacter; + } + + public String getModel() { + return isParentSet() ? parent.model : model; + } + public PumpCapability getPumpCapability() { if (isParentSet()) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/AbstractDanaRPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/AbstractDanaRPlugin.java index 7410dd7806..38c1f80b8d 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/AbstractDanaRPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/AbstractDanaRPlugin.java @@ -376,7 +376,12 @@ public abstract class AbstractDanaRPlugin extends PluginBase implements PumpInte } @Override - public String deviceID() { + public String manufacter() { + return "SOOIL"; + }; + + @Override + public String serialNumber() { return DanaRPump.getInstance().serialNumber; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/DanaRPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/DanaRPlugin.java index 4e38767619..cbb06656fd 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/DanaRPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaR/DanaRPlugin.java @@ -360,6 +360,11 @@ public class DanaRPlugin extends AbstractDanaRPlugin { return result; } + @Override + public String model() { + return "DanaR"; + } + private PumpEnactResult cancelRealTempBasal() { PumpEnactResult result = new PumpEnactResult(); TemporaryBasal runningTB = TreatmentsPlugin.getPlugin().getTempBasalFromHistory(System.currentTimeMillis()); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRKorean/DanaRKoreanPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRKorean/DanaRKoreanPlugin.java index 44eacef16c..84e9cdf1e1 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRKorean/DanaRKoreanPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRKorean/DanaRKoreanPlugin.java @@ -364,6 +364,11 @@ public class DanaRKoreanPlugin extends AbstractDanaRPlugin { return result; } + @Override + public String model() { + return "DanaRKorean"; + } + private PumpEnactResult cancelRealTempBasal() { PumpEnactResult result = new PumpEnactResult(); TemporaryBasal runningTB = TreatmentsPlugin.getPlugin().getTempBasalFromHistory(System.currentTimeMillis()); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRS/DanaRSPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRS/DanaRSPlugin.java index 4e2aa6c08d..2dd453f40e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRS/DanaRSPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRS/DanaRSPlugin.java @@ -769,7 +769,17 @@ public class DanaRSPlugin extends PluginBase implements PumpInterface, DanaRInte } @Override - public String deviceID() { + public String manufacter() { + return "SOOIL"; + } + + @Override + public String model() { + return "DanaRS"; + } + + @Override + public String serialNumber() { return DanaRPump.getInstance().serialNumber; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2Plugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2Plugin.java index 4fba649cae..13af8fff1b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2Plugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2Plugin.java @@ -402,6 +402,11 @@ public class DanaRv2Plugin extends AbstractDanaRPlugin { } } + @Override + public String model() { + return "DanaRv2"; + } + @Override public PumpEnactResult loadEvents() { return sExecutionService.loadEvents(); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/insight/LocalInsightPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/insight/LocalInsightPlugin.java index 42125690c0..b30266200e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/insight/LocalInsightPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/insight/LocalInsightPlugin.java @@ -417,8 +417,8 @@ public class LocalInsightPlugin extends PluginBase implements PumpInterface, Con MainApp.bus().post(new EventDismissNotification(Notification.PROFILE_NOT_SET_NOT_INITIALIZED)); List profileBlocks = new ArrayList<>(); for (int i = 0; i < profile.getBasalValues().length; i++) { - Profile.BasalValue basalValue = profile.getBasalValues()[i]; - Profile.BasalValue nextValue = null; + Profile.ProfileValue basalValue = profile.getBasalValues()[i]; + Profile.ProfileValue nextValue = null; if (profile.getBasalValues().length > i + 1) nextValue = profile.getBasalValues()[i + 1]; BasalProfileBlock profileBlock = new BasalProfileBlock(); @@ -471,8 +471,8 @@ public class LocalInsightPlugin extends PluginBase implements PumpInterface, Con if (activeBasalProfile != BasalProfile.PROFILE_1) return false; for (int i = 0; i < profileBlocks.size(); i++) { BasalProfileBlock profileBlock = profileBlocks.get(i); - Profile.BasalValue basalValue = profile.getBasalValues()[i]; - Profile.BasalValue nextValue = null; + Profile.ProfileValue basalValue = profile.getBasalValues()[i]; + Profile.ProfileValue nextValue = null; if (profile.getBasalValues().length > i + 1) nextValue = profile.getBasalValues()[i + 1]; if (profileBlock.getDuration() * 60 != (nextValue != null ? nextValue.timeAsSeconds : 24 * 60 * 60) - basalValue.timeAsSeconds) @@ -945,9 +945,19 @@ public class LocalInsightPlugin extends PluginBase implements PumpInterface, Con } @Override - public String deviceID() { - if (connectionService == null || alertService == null) return null; - return connectionService.getPumpSystemIdentification().getSerialNumber(); + public String manufacter() { + return "Roche"; + } + + @Override + public String model() { + return "Insight"; + } + + @Override + public String serialNumber() { + if (connectionService == null || alertService == null) return "Unknown"; + return connectionService.getPumpSystemIdentification().getSerialNumber(); } public PumpEnactResult stopPump() { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/mdi/MDIPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/mdi/MDIPlugin.java index ca5441eb3d..6e782dd6dd 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/mdi/MDIPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/mdi/MDIPlugin.java @@ -23,6 +23,7 @@ import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction; import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.InstanceId; /** @@ -236,10 +237,20 @@ public class MDIPlugin extends PluginBase implements PumpInterface { } @Override - public String deviceID() { + public String manufacter() { + return "AndroidAPS"; + } + + @Override + public String model() { return "MDI"; } + @Override + public String serialNumber() { + return InstanceId.INSTANCE.instanceId(); + } + @Override public PumpDescription getPumpDescription() { return pumpDescription; @@ -247,7 +258,7 @@ public class MDIPlugin extends PluginBase implements PumpInterface { @Override public String shortStatus(boolean veryShort) { - return deviceID(); + return model(); } @Override diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/virtual/VirtualPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/virtual/VirtualPumpPlugin.java index bb71f45d6f..122653fa3b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/virtual/VirtualPumpPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/virtual/VirtualPumpPlugin.java @@ -39,6 +39,7 @@ import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.virtual.events.EventVirtualPumpUpdateGui; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.InstanceId; import info.nightscout.androidaps.utils.SP; @@ -437,8 +438,18 @@ public class VirtualPumpPlugin extends PluginBase implements PumpInterface { } @Override - public String deviceID() { - return "VirtualPump"; + public String manufacter() { + return pumpDescription.pumpType.getManufacter(); + } + + @Override + public String model() { + return pumpDescription.pumpType.getModel(); + } + + @Override + public String serialNumber() { + return InstanceId.INSTANCE.instanceId(); } @Override diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/Treatment.java b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/Treatment.java index 871e558e9c..555af08255 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/Treatment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/Treatment.java @@ -17,9 +17,11 @@ import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; import info.nightscout.androidaps.data.Iob; import info.nightscout.androidaps.db.DbObjectBase; +import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.db.Source; import info.nightscout.androidaps.interfaces.InsulinInterface; import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin; import info.nightscout.androidaps.plugins.general.overview.graphExtensions.DataPointWithLabelInterface; import info.nightscout.androidaps.plugins.general.overview.graphExtensions.PointsWithLabelGraphSeries; @@ -166,6 +168,15 @@ public class Treatment implements DataPointWithLabelInterface, DbObjectBase { return null; } + public double getIc() { + JSONObject bw = getBoluscalc(); + if (bw == null || !bw.has("ic")) { + Profile profile = ProfileFunctions.getInstance().getProfile(date); + return profile.getIc(date); + } + return JsonHelper.safeGetDouble(bw, "ic"); + } + /* * mealBolus, _id and isSMB cannot be known coming from pump. Only compare rest * TODO: remove debug toasts diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java index eaa6f4fd31..309191fdc0 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentService.java @@ -686,6 +686,23 @@ public class TreatmentService extends OrmLiteBaseService { return new ArrayList<>(); } + public List getTreatmentDataFromTime(long from, long to, boolean ascending) { + try { + Dao daoTreatments = getDao(); + List treatments; + QueryBuilder queryBuilder = daoTreatments.queryBuilder(); + queryBuilder.orderBy("date", ascending); + Where where = queryBuilder.where(); + where.between("date", from, to); + PreparedQuery preparedQuery = queryBuilder.prepare(); + treatments = daoTreatments.query(preparedQuery); + return treatments; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList<>(); + } + @Nullable @Override public IBinder onBind(Intent intent) { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java index 1000b01999..fe555cdadc 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java @@ -495,7 +495,7 @@ public class TreatmentsPlugin extends PluginBase implements TreatmentsInterface } @Override - public Intervals getTemporaryBasalsFromHistory() { + public NonOverlappingIntervals getTemporaryBasalsFromHistory() { synchronized (tempBasals) { return new NonOverlappingIntervals<>(tempBasals); } diff --git a/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java b/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java index 79e737fb8d..7541d03cf1 100644 --- a/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java +++ b/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java @@ -386,10 +386,10 @@ public class CommandQueue { } // Compare with pump limits - Profile.BasalValue[] basalValues = profile.getBasalValues(); + Profile.ProfileValue[] basalValues = profile.getBasalValues(); PumpInterface pump = ConfigBuilderPlugin.getPlugin().getActivePump(); - for (Profile.BasalValue basalValue : basalValues) { + for (Profile.ProfileValue basalValue : basalValues) { if (basalValue.value < pump.getPumpDescription().basalMinimumRate) { Notification notification = new Notification(Notification.BASAL_VALUE_BELOW_MINIMUM, MainApp.gs(R.string.basalvaluebelowminimum), Notification.URGENT); MainApp.bus().post(new EventNewNotification(notification)); diff --git a/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java b/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java index b10c2e99e5..349e1771b9 100644 --- a/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java +++ b/app/src/main/java/info/nightscout/androidaps/receivers/ChargingStateReceiver.java @@ -10,12 +10,15 @@ import info.nightscout.androidaps.events.EventChargingState; public class ChargingStateReceiver extends BroadcastReceiver { + private static EventChargingState lastEvent; + @Override public void onReceive(Context context, Intent intent) { EventChargingState event = grabChargingState(context); if (event != null) MainApp.bus().post(event); + lastEvent = event; } public EventChargingState grabChargingState(Context context) { @@ -32,4 +35,7 @@ public class ChargingStateReceiver extends BroadcastReceiver { return event; } + static public boolean isCharging() { + return lastEvent != null && lastEvent.isCharging; + } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/receivers/NetworkChangeReceiver.java b/app/src/main/java/info/nightscout/androidaps/receivers/NetworkChangeReceiver.java index 05adc1af59..4a462906b3 100644 --- a/app/src/main/java/info/nightscout/androidaps/receivers/NetworkChangeReceiver.java +++ b/app/src/main/java/info/nightscout/androidaps/receivers/NetworkChangeReceiver.java @@ -21,6 +21,8 @@ public class NetworkChangeReceiver extends BroadcastReceiver { private static Logger log = LoggerFactory.getLogger(L.CORE); + private static EventNetworkChange lastEvent = null; + @Override public void onReceive(final Context context, final Intent intent) { EventNetworkChange event = grabNetworkStatus(context); @@ -61,6 +63,11 @@ public class NetworkChangeReceiver extends BroadcastReceiver { log.debug("NETCHANGE: Disconnected."); } + lastEvent = event; return event; } + + public static boolean isWifiConnected() { + return lastEvent != null && lastEvent.wifiConnected; + } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.java b/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.java index 4e8113c73c..6206cb0d9a 100644 --- a/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.java +++ b/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.java @@ -12,6 +12,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.events.EventPreferenceChange; import info.nightscout.androidaps.logging.L; import info.nightscout.androidaps.setupwizard.events.EventSWUpdate; @@ -98,6 +99,7 @@ public class SWItem { if (L.isEnabled(L.CORE)) log.debug("Firing EventPreferenceChange"); MainApp.bus().post(new EventPreferenceChange(preferenceId)); + RxBus.INSTANCE.send(new EventPreferenceChange(preferenceId)); MainApp.bus().post(new EventSWUpdate()); scheduledEventPost = null; } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/BolusWizard.kt b/app/src/main/java/info/nightscout/androidaps/utils/BolusWizard.kt index e810b689f7..e9a1b180c2 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/BolusWizard.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/BolusWizard.kt @@ -188,7 +188,7 @@ class BolusWizard @JvmOverloads constructor(val profile: Profile, calculatedTotalInsulin = 0.0 } - val bolusStep = ConfigBuilderPlugin.getPlugin().activePump.pumpDescription.bolusStep + val bolusStep = ConfigBuilderPlugin.getPlugin().activePump?.pumpDescription?.bolusStep ?: 0.1 calculatedTotalInsulin = Round.roundTo(calculatedTotalInsulin, bolusStep) insulinAfterConstraints = MainApp.getConstraintChecker().applyBolusConstraints(Constraint(calculatedTotalInsulin)).value() @@ -278,7 +278,7 @@ class BolusWizard @JvmOverloads constructor(val profile: Profile, val pump1 = ConfigBuilderPlugin.getPlugin().activePump - if (pump1.pumpDescription.tempBasalStyle == PumpDescription.ABSOLUTE) { + if (pump1?.pumpDescription?.tempBasalStyle == PumpDescription.ABSOLUTE) { ConfigBuilderPlugin.getPlugin().commandQueue.tempBasalAbsolute(0.0, 120, true, profile, object : Callback() { override fun run() { if (!result.success) { @@ -318,7 +318,7 @@ class BolusWizard @JvmOverloads constructor(val profile: Profile, detailedBolusInfo.boluscalc = nsJSON() detailedBolusInfo.source = Source.USER detailedBolusInfo.notes = notes - if (detailedBolusInfo.insulin > 0 || ConfigBuilderPlugin.getPlugin().activePump.pumpDescription.storesCarbInfo) { + if (detailedBolusInfo.insulin > 0 || ConfigBuilderPlugin.getPlugin().activePump?.pumpDescription?.storesCarbInfo == true) { ConfigBuilderPlugin.getPlugin().commandQueue.bolus(detailedBolusInfo, object : Callback() { override fun run() { if (!result.success) { diff --git a/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java b/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java index 9e5782d063..4b19c6d114 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java @@ -8,6 +8,8 @@ import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -71,6 +73,18 @@ public class DateUtil { return toISOString(new Date(date), FORMAT_DATE_ISO_OUT, TimeZone.getTimeZone("UTC")); } + public static String toISOAsUTC(final long timestamp) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'0000Z'", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.format(timestamp); + } + + public static String toISONoZone(final long timestamp) { + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getDefault()); + return format.format(timestamp); + } + public static Date toDate(Integer seconds) { Calendar calendar = new GregorianCalendar(); calendar.set(Calendar.MONTH, 0); // Set january to be sure we miss DST changing @@ -187,4 +201,80 @@ public class DateUtil { long diff = Math.abs(date - now()); return diff < T.mins(2).msecs(); } + + public static long getTimeZoneOffsetMs() { + return new GregorianCalendar().getTimeZone().getRawOffset(); + } + + public static int getTimeZoneOffsetMinutes(final long timestamp) { + return TimeZone.getDefault().getOffset(timestamp) / 60000; + } + + public static String niceTimeScalar(long t) { + String unit = MainApp.gs(R.string.unit_second); + t = t / 1000; + if (t != 1) unit = MainApp.gs(R.string.unit_seconds); + if (t > 59) { + unit = MainApp.gs(R.string.unit_minute); + t = t / 60; + if (t != 1) unit = MainApp.gs(R.string.unit_minutes); + if (t > 59) { + unit = MainApp.gs(R.string.unit_hour); + t = t / 60; + if (t != 1) unit = MainApp.gs(R.string.unit_hours); + if (t > 24) { + unit = MainApp.gs(R.string.unit_day); + t = t / 24; + if (t != 1) unit = MainApp.gs(R.string.unit_days); + if (t > 28) { + unit = MainApp.gs(R.string.unit_week); + t = t / 7; + if (t != 1) unit = MainApp.gs(R.string.unit_weeks); + } + } + } + } + //if (t != 1) unit = unit + "s"; //implemented plurality in every step, because in other languages plurality of time is not every time adding the same character + return qs((double) t, 0) + " " + unit; + } + + // singletons to avoid repeated allocation + private static DecimalFormatSymbols dfs; + private static DecimalFormat df; + public static String qs(double x, int digits) { + + if (digits == -1) { + digits = 0; + if (((int) x != x)) { + digits++; + if ((((int) x * 10) / 10 != x)) { + digits++; + if ((((int) x * 100) / 100 != x)) digits++; + } + } + } + + if (dfs == null) { + final DecimalFormatSymbols local_dfs = new DecimalFormatSymbols(); + local_dfs.setDecimalSeparator('.'); + dfs = local_dfs; // avoid race condition + } + + final DecimalFormat this_df; + // use singleton if on ui thread otherwise allocate new as DecimalFormat is not thread safe + if (Thread.currentThread().getId() == 1) { + if (df == null) { + final DecimalFormat local_df = new DecimalFormat("#", dfs); + local_df.setMinimumIntegerDigits(1); + df = local_df; // avoid race condition + } + this_df = df; + } else { + this_df = new DecimalFormat("#", dfs); + } + + this_df.setMaximumFractionDigits(digits); + return this_df.format(x); + } + } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/FabricPrivacy.java b/app/src/main/java/info/nightscout/androidaps/utils/FabricPrivacy.java index 54f06bfaf0..3dd7fe4fd9 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/FabricPrivacy.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/FabricPrivacy.java @@ -121,7 +121,7 @@ public class FabricPrivacy { .replace(".net/", ":"); MainApp.getFirebaseAnalytics().setUserProperty("Mode", BuildConfig.APPLICATION_ID + "-" + closedLoopEnabled); - MainApp.getFirebaseAnalytics().setUserProperty("Language", LocaleHelper.getLanguage(MainApp.instance())); + MainApp.getFirebaseAnalytics().setUserProperty("Language", LocaleHelper.getLanguage()); MainApp.getFirebaseAnalytics().setUserProperty("Version", BuildConfig.VERSION); MainApp.getFirebaseAnalytics().setUserProperty("HEAD", BuildConfig.HEAD); MainApp.getFirebaseAnalytics().setUserProperty("Remote", remote); diff --git a/app/src/main/java/info/nightscout/androidaps/utils/InstanceId.kt b/app/src/main/java/info/nightscout/androidaps/utils/InstanceId.kt new file mode 100644 index 0000000000..64c84fcaa9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/InstanceId.kt @@ -0,0 +1,10 @@ +package info.nightscout.androidaps.utils + +import com.google.firebase.iid.FirebaseInstanceId + +object InstanceId { + fun instanceId(): String { + var id = FirebaseInstanceId.getInstance().id + return id + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/LocaleHelper.java b/app/src/main/java/info/nightscout/androidaps/utils/LocaleHelper.java index bf0b2796ee..d2e4c0622b 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/LocaleHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/LocaleHelper.java @@ -21,17 +21,17 @@ public class LocaleHelper { private static final String SELECTED_LANGUAGE = "Locale.Helper.Selected.Language"; public static void onCreate(Context context) { - String lang = getPersistedData(context, Locale.getDefault().getLanguage()); + String lang = getPersistedData(Locale.getDefault().getLanguage()); setLocale(context, lang); } public static void onCreate(Context context, String defaultLanguage) { - String lang = getPersistedData(context, defaultLanguage); + String lang = getPersistedData(defaultLanguage); setLocale(context, lang); } - public static String getLanguage(Context context) { - return getPersistedData(context, Locale.getDefault().getLanguage()); + public static String getLanguage() { + return getPersistedData(Locale.getDefault().getLanguage()); } public static void setLocale(Context context, String language) { @@ -39,7 +39,7 @@ public class LocaleHelper { updateResources(context, language); } - private static String getPersistedData(Context context, String defaultLanguage) { + private static String getPersistedData(String defaultLanguage) { return SP.getString(SELECTED_LANGUAGE, defaultLanguage); } @@ -62,4 +62,8 @@ public class LocaleHelper { resources.updateConfiguration(configuration, resources.getDisplayMetrics()); } + + public static Locale getLocale() { + return new Locale(getPersistedData("en")); + } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java b/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java index d089d213d4..74e8da7c9d 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/StringUtils.java @@ -21,6 +21,10 @@ public class StringUtils { return string; } + public static boolean emptyString(final String str) { + return str == null || str.length() == 0; + } + public static String formatInsulin(double insulin) { return String.format(MainApp.gs(R.string.formatinsulinunits), insulin); } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/T.java b/app/src/main/java/info/nightscout/androidaps/utils/T.java index 2a9bcfc42c..5671f0725c 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/T.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/T.java @@ -43,6 +43,12 @@ public class T { return t; } + public static T months(long month) { + T t = new T(); + t.time = month * 31 * 24 * 60 * 60 * 1000L; + return t; + } + public long msecs() { return time; } diff --git a/app/src/main/res/layout/tidepool_fragment.xml b/app/src/main/res/layout/tidepool_fragment.xml new file mode 100644 index 0000000000..2640e96893 --- /dev/null +++ b/app/src/main/res/layout/tidepool_fragment.xml @@ -0,0 +1,74 @@ + + + + + +