diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/LoopDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/LoopDialog.kt new file mode 100644 index 0000000000..1ee243b72b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/LoopDialog.kt @@ -0,0 +1,373 @@ +package info.nightscout.androidaps.dialogs + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.ArrayAdapter +import android.widget.CompoundButton +import androidx.fragment.app.FragmentManager +import dagger.android.support.DaggerDialogFragment +import info.nightscout.androidaps.Constants +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.data.Profile +import info.nightscout.androidaps.db.BgReading +import info.nightscout.androidaps.interfaces.ActivePluginProvider +import info.nightscout.androidaps.interfaces.Constraint +import info.nightscout.androidaps.interfaces.ProfileFunction +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.events.EventAutosensCalculationFinished +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin +import info.nightscout.androidaps.utils.DecimalFormatter +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.SafeParse +import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.extensions.toVisibility +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import info.nightscout.androidaps.utils.wizard.BolusWizard +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.dialog_wizard.* +import java.text.DecimalFormat +import java.util.* +import javax.inject.Inject +import kotlin.math.abs + +class LoopDialog : DaggerDialogFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var constraintChecker: ConstraintChecker + @Inject lateinit var mainApp: MainApp + @Inject lateinit var sp: SP + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var treatmentsPlugin: TreatmentsPlugin + @Inject lateinit var activePlugin: ActivePluginProvider + @Inject lateinit var iobCobCalculatorPlugin: IobCobCalculatorPlugin + + private var wizard: BolusWizard? = null + + //one shot guards + private var okClicked: Boolean = false + + private val textWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + calculateInsulin() + } + } + + private var disposable: CompositeDisposable = CompositeDisposable() + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onSaveInstanceState(savedInstanceState: Bundle) { + super.onSaveInstanceState(savedInstanceState) + savedInstanceState.putDouble("treatments_wizard_bg_input", treatments_wizard_bg_input.value) + savedInstanceState.putDouble("treatments_wizard_carbs_input", treatments_wizard_carbs_input.value) + savedInstanceState.putDouble("treatments_wizard_correction_input", treatments_wizard_correction_input.value) + savedInstanceState.putDouble("treatments_wizard_carb_time_input", treatments_wizard_carb_time_input.value) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) + isCancelable = true + dialog?.setCanceledOnTouchOutside(false) + + return inflater.inflate(R.layout.dialog_wizard, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + loadCheckedStates() + processCobCheckBox() + treatments_wizard_sbcheckbox.visibility = sp.getBoolean(R.string.key_usesuperbolus, false).toVisibility() + treatments_wizard_notes_layout.visibility = sp.getBoolean(R.string.key_show_notes_entry_dialogs, false).toVisibility() + + val maxCarbs = constraintChecker.getMaxCarbsAllowed().value() + val maxCorrection = constraintChecker.getMaxBolusAllowed().value() + + if (profileFunction.getUnits() == Constants.MGDL) + treatments_wizard_bg_input.setParams(savedInstanceState?.getDouble("treatments_wizard_bg_input") + ?: 0.0, 0.0, 500.0, 1.0, DecimalFormat("0"), false, ok, textWatcher) + else + treatments_wizard_bg_input.setParams(savedInstanceState?.getDouble("treatments_wizard_bg_input") + ?: 0.0, 0.0, 30.0, 0.1, DecimalFormat("0.0"), false, ok, textWatcher) + treatments_wizard_carbs_input.setParams(savedInstanceState?.getDouble("treatments_wizard_carbs_input") + ?: 0.0, 0.0, maxCarbs.toDouble(), 1.0, DecimalFormat("0"), false, ok, textWatcher) + val bolusStep = activePlugin.activePump.pumpDescription.bolusStep + treatments_wizard_correction_input.setParams(savedInstanceState?.getDouble("treatments_wizard_correction_input") + ?: 0.0, -maxCorrection, maxCorrection, bolusStep, DecimalFormatter.pumpSupportedBolusFormat(activePlugin.activePump), false, ok, textWatcher) + treatments_wizard_carb_time_input.setParams(savedInstanceState?.getDouble("treatments_wizard_carb_time_input") + ?: 0.0, -60.0, 60.0, 5.0, DecimalFormat("0"), false, ok, textWatcher) + initDialog() + + treatments_wizard_percent_used.text = resourceHelper.gs(R.string.format_percent, sp.getInt(R.string.key_boluswizard_percentage, 100)) + // ok button + ok.setOnClickListener { + if (okClicked) { + aapsLogger.debug(LTag.UI, "guarding: ok already clicked") + } else { + okClicked = true + calculateInsulin() + context?.let { context -> + wizard?.confirmAndExecute(context) + } + } + dismiss() + } + // cancel button + cancel.setOnClickListener { dismiss() } + // checkboxes + treatments_wizard_bgcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + treatments_wizard_ttcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + treatments_wizard_cobcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + treatments_wizard_basaliobcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + treatments_wizard_bolusiobcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + treatments_wizard_bgtrendcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + treatments_wizard_sbcheckbox.setOnCheckedChangeListener(::onCheckedChanged) + + val showCalc = sp.getBoolean(resourceHelper.gs(R.string.key_wizard_calculation_visible), false) + treatments_wizard_delimiter.visibility = showCalc.toVisibility() + treatments_wizard_resulttable.visibility = showCalc.toVisibility() + treatments_wizard_calculationcheckbox.isChecked = showCalc + treatments_wizard_calculationcheckbox.setOnCheckedChangeListener { _, isChecked -> + run { + sp.putBoolean(resourceHelper.gs(R.string.key_wizard_calculation_visible), isChecked) + treatments_wizard_delimiter.visibility = isChecked.toVisibility() + treatments_wizard_resulttable.visibility = isChecked.toVisibility() + } + } + // profile spinner + treatments_wizard_profile.onItemSelectedListener = object : OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) { + ToastUtils.showToastInUiThread(mainApp, resourceHelper.gs(R.string.noprofileselected)) + ok.visibility = View.GONE + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + calculateInsulin() + ok.visibility = View.VISIBLE + } + } + // bus + disposable.add(rxBus + .toObservable(EventAutosensCalculationFinished::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + activity?.runOnUiThread { calculateInsulin() } + }, { fabricPrivacy.logException(it) }) + ) + + } + + override fun onDestroyView() { + super.onDestroyView() + disposable.clear() + } + + private fun onCheckedChanged(buttonView: CompoundButton, @Suppress("UNUSED_PARAMETER") state: Boolean) { + saveCheckedStates() + treatments_wizard_ttcheckbox.isEnabled = treatments_wizard_bgcheckbox.isChecked && treatmentsPlugin.tempTargetFromHistory != null + if (buttonView.id == treatments_wizard_cobcheckbox.id) + processCobCheckBox() + calculateInsulin() + } + + private fun processCobCheckBox() { + if (treatments_wizard_cobcheckbox.isChecked) { + treatments_wizard_bolusiobcheckbox.isEnabled = false + treatments_wizard_basaliobcheckbox.isEnabled = false + treatments_wizard_bolusiobcheckbox.isChecked = true + treatments_wizard_basaliobcheckbox.isChecked = true + } else { + treatments_wizard_bolusiobcheckbox.isEnabled = true + treatments_wizard_basaliobcheckbox.isEnabled = true + } + } + + private fun saveCheckedStates() { + sp.putBoolean(resourceHelper.gs(R.string.key_wizard_include_cob), treatments_wizard_cobcheckbox.isChecked) + sp.putBoolean(resourceHelper.gs(R.string.key_wizard_include_trend_bg), treatments_wizard_bgtrendcheckbox.isChecked) + } + + private fun loadCheckedStates() { + treatments_wizard_bgtrendcheckbox.isChecked = sp.getBoolean(resourceHelper.gs(R.string.key_wizard_include_trend_bg), false) + treatments_wizard_cobcheckbox.isChecked = sp.getBoolean(resourceHelper.gs(R.string.key_wizard_include_cob), false) + } + + private fun initDialog() { + val profile = profileFunction.getProfile() + val profileStore = activePlugin.activeProfileInterface.profile + + if (profile == null || profileStore == null) { + ToastUtils.showToastInUiThread(mainApp, resourceHelper.gs(R.string.noprofile)) + dismiss() + return + } + + val profileList: ArrayList + profileList = profileStore.getProfileList() + profileList.add(0, resourceHelper.gs(R.string.active)) + context?.let { context -> + val adapter = ArrayAdapter(context, R.layout.spinner_centered, profileList) + treatments_wizard_profile.adapter = adapter + } ?: return + + val units = profileFunction.getUnits() + treatments_wizard_bgunits.text = units + if (units == Constants.MGDL) + treatments_wizard_bg_input.setStep(1.0) + else + treatments_wizard_bg_input.setStep(0.1) + + // Set BG if not old + val lastBg = iobCobCalculatorPlugin.actualBg() + + if (lastBg != null) { + treatments_wizard_bg_input.value = lastBg.valueToUnits(units) + } else { + treatments_wizard_bg_input.value = 0.0 + } + treatments_wizard_ttcheckbox.isEnabled = treatmentsPlugin.tempTargetFromHistory != null + + // IOB calculation + treatmentsPlugin.updateTotalIOBTreatments() + val bolusIob = treatmentsPlugin.lastCalculationTreatments.round() + treatmentsPlugin.updateTotalIOBTempBasals() + val basalIob = treatmentsPlugin.lastCalculationTempBasals.round() + + treatments_wizard_bolusiobinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, -bolusIob.iob) + treatments_wizard_basaliobinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, -basalIob.basaliob) + + calculateInsulin() + + treatments_wizard_percent_used.visibility = (sp.getInt(R.string.key_boluswizard_percentage, 100) != 100).toVisibility() + } + + private fun calculateInsulin() { + val profileStore = activePlugin.activeProfileInterface.profile + if (treatments_wizard_profile?.selectedItem == null || profileStore == null) + return // not initialized yet + var profileName = treatments_wizard_profile.selectedItem.toString() + val specificProfile: Profile? + if (profileName == resourceHelper.gs(R.string.active)) { + specificProfile = profileFunction.getProfile() + profileName = profileFunction.getProfileName() + } else + specificProfile = profileStore.getSpecificProfile(profileName) + + if (specificProfile == null) return + + // Entered values + var bg = SafeParse.stringToDouble(treatments_wizard_bg_input.text) + val carbs = SafeParse.stringToInt(treatments_wizard_carbs_input.text) + val correction = SafeParse.stringToDouble(treatments_wizard_correction_input.text) + val carbsAfterConstraint = constraintChecker.applyCarbsConstraints(Constraint(carbs)).value() + if (abs(carbs - carbsAfterConstraint) > 0.01) { + treatments_wizard_carbs_input.value = 0.0 + ToastUtils.showToastInUiThread(mainApp, resourceHelper.gs(R.string.carbsconstraintapplied)) + return + } + + bg = if (treatments_wizard_bgcheckbox.isChecked) bg else 0.0 + val tempTarget = if (treatments_wizard_ttcheckbox.isChecked) treatmentsPlugin.tempTargetFromHistory else null + + // COB + var cob = 0.0 + if (treatments_wizard_cobcheckbox.isChecked) { + val cobInfo = iobCobCalculatorPlugin.getCobInfo(false, "Wizard COB") + cobInfo.displayCob?.let { cob = it } + } + + val carbTime = SafeParse.stringToInt(treatments_wizard_carb_time_input.text) + + wizard = BolusWizard(mainApp).doCalc(specificProfile, profileName, tempTarget, carbsAfterConstraint, cob, bg, correction, + sp.getInt(R.string.key_boluswizard_percentage, 100).toDouble(), + treatments_wizard_bgcheckbox.isChecked, + treatments_wizard_cobcheckbox.isChecked, + treatments_wizard_bolusiobcheckbox.isChecked, + treatments_wizard_basaliobcheckbox.isChecked, + treatments_wizard_sbcheckbox.isChecked, + treatments_wizard_ttcheckbox.isChecked, + treatments_wizard_bgtrendcheckbox.isChecked, + treatment_wizard_notes.text.toString(), carbTime) + + wizard?.let { wizard -> + treatments_wizard_bg.text = String.format(resourceHelper.gs(R.string.format_bg_isf), BgReading().value(Profile.toMgdl(bg, profileFunction.getUnits())).valueToUnitsToString(profileFunction.getUnits()), wizard.sens) + treatments_wizard_bginsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromBG) + + treatments_wizard_carbs.text = String.format(resourceHelper.gs(R.string.format_carbs_ic), carbs.toDouble(), wizard.ic) + treatments_wizard_carbsinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromCarbs) + + treatments_wizard_bolusiobinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromBolusIOB) + treatments_wizard_basaliobinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromBasalsIOB) + + treatments_wizard_correctioninsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromCorrection) + + // Superbolus + treatments_wizard_sb.text = if (treatments_wizard_sbcheckbox.isChecked) resourceHelper.gs(R.string.twohours) else "" + treatments_wizard_sbinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromSuperBolus) + + // Trend + if (treatments_wizard_bgtrendcheckbox.isChecked && wizard.glucoseStatus != null) { + treatments_wizard_bgtrend.text = ((if (wizard.trend > 0) "+" else "") + + Profile.toUnitsString(wizard.trend * 3, wizard.trend * 3 / Constants.MMOLL_TO_MGDL, profileFunction.getUnits()) + + " " + profileFunction.getUnits()) + } else { + treatments_wizard_bgtrend.text = "" + } + treatments_wizard_bgtrendinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromTrend) + + // COB + if (treatments_wizard_cobcheckbox.isChecked) { + treatments_wizard_cob.text = String.format(resourceHelper.gs(R.string.format_cob_ic), cob, wizard.ic) + treatments_wizard_cobinsulin.text = resourceHelper.gs(R.string.formatinsulinunits, wizard.insulinFromCOB) + } else { + treatments_wizard_cob.text = "" + treatments_wizard_cobinsulin.text = "" + } + + if (wizard.calculatedTotalInsulin > 0.0 || carbsAfterConstraint > 0.0) { + val insulinText = if (wizard.calculatedTotalInsulin > 0.0) resourceHelper.gs(R.string.formatinsulinunits, wizard.calculatedTotalInsulin) else "" + val carbsText = if (carbsAfterConstraint > 0.0) resourceHelper.gs(R.string.format_carbs, carbsAfterConstraint) else "" + treatments_wizard_total.text = resourceHelper.gs(R.string.result_insulin_carbs, insulinText, carbsText) + ok.visibility = View.VISIBLE + } else { + treatments_wizard_total.text = resourceHelper.gs(R.string.missing_carbs, wizard.carbsEquivalent.toInt()) + ok.visibility = View.INVISIBLE + } + } + + } + + override fun show(manager: FragmentManager, tag: String?) { + try { + manager.beginTransaction().let { + it.add(this, tag) + it.commitAllowingStateLoss() + } + } catch (e: IllegalStateException) { + aapsLogger.debug(e.localizedMessage) + } + } +} diff --git a/app/src/main/res/layout/dialog_loop.xml b/app/src/main/res/layout/dialog_loop.xml new file mode 100644 index 0000000000..8fd4028ccd --- /dev/null +++ b/app/src/main/res/layout/dialog_loop.xml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file