Merge pull request #2259 from dv1/combov2-fixes-001
combov2: Fixes for UI related crashes, improvements to pump state store imports, and partial unit test integration
This commit is contained in:
commit
805ff505b7
|
@ -0,0 +1,49 @@
|
|||
package info.nightscout.core.ui.elements
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.SeekBarPreference
|
||||
|
||||
/**
|
||||
* Variant of SeekBarPreference with built-in string->int conversion.
|
||||
*
|
||||
* The normal SeekBarPreference crashes if the associated value in the
|
||||
* SharedPreferences is not an int. This is a problem, because AAPS
|
||||
* exports all settings as strings. When importing settings again,
|
||||
* the former int value becomes a string value as a consequence.
|
||||
*
|
||||
* For this reason, this variant exists. It tries to first read the
|
||||
* initial preference value from the preferences as an int. If it is
|
||||
* not an int, ClassCastException is thrown. This is caught, and the
|
||||
* value is re-read as a string and then converted to an int.
|
||||
*
|
||||
* To use this in fragment XMLs, replace "SeekBarPreference" in them
|
||||
* with "info.nightscout.core.ui.elements.IntSeekBarPreference".
|
||||
*/
|
||||
class IntSeekBarPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : SeekBarPreference(context, attrs) {
|
||||
override fun onSetInitialValue(defaultValue: Any?) {
|
||||
val actualDefaultValue = if (defaultValue == null)
|
||||
0
|
||||
else
|
||||
(defaultValue as Int?) ?: 0
|
||||
|
||||
val storedValue = try {
|
||||
getPersistedInt(actualDefaultValue)
|
||||
} catch (_: ClassCastException) {
|
||||
val keyToDelete = key
|
||||
// Remove the key manually. The setValue() function that is
|
||||
// used in the "value" property assignment below tries to look
|
||||
// up the existing stored value if it exists. If it does exist,
|
||||
// it tries to read the value - as an int. We then get another
|
||||
// ClassCastException. To avoid that, first delete the existing
|
||||
// value. This prevents setValue() from doing that int lookup.
|
||||
sharedPreferences?.edit {
|
||||
remove(keyToDelete)
|
||||
}
|
||||
getPersistedString(actualDefaultValue.toString()).toInt()
|
||||
}
|
||||
|
||||
value = storedValue
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ dependencies {
|
|||
implementation project(':core:utils')
|
||||
implementation project(':app-wear-shared:shared')
|
||||
implementation(project(":pump:combov2:comboctl"))
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinx_datetime_version")
|
||||
// This is necessary to avoid errors like these which otherwise come up often at runtime:
|
||||
// "WARNING: Failed to transform class kotlinx/datetime/TimeZone$Companion
|
||||
|
|
|
@ -2,6 +2,7 @@ apply plugin: 'com.android.library'
|
|||
apply plugin: 'kotlin-android'
|
||||
|
||||
apply from: "${project.rootDir}/core/main/android_dependencies.gradle"
|
||||
apply from: "${project.rootDir}/core/main/test_dependencies.gradle"
|
||||
|
||||
android {
|
||||
namespace 'info.nightscout.comboctl'
|
||||
|
@ -10,6 +11,9 @@ android {
|
|||
kotlin.srcDirs += ['src/commonMain/kotlin', 'src/androidMain/kotlin']
|
||||
manifest.srcFile 'src/androidMain/AndroidManifest.xml'
|
||||
}
|
||||
test {
|
||||
kotlin.srcDirs += ['src/jvmTest/kotlin']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,4 +22,8 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinx_datetime_version"
|
||||
implementation "androidx.core:core-ktx:$core_version"
|
||||
}
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-test'
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
|
||||
testImplementation "io.kotlintest:kotlintest-runner-junit5:3.4.2"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_jupiter_version"
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package info.nightscout.pump.combov2
|
||||
|
||||
import info.nightscout.comboctl.base.BluetoothAddress
|
||||
import info.nightscout.comboctl.base.CurrentTbrState
|
||||
import info.nightscout.comboctl.base.InvariantPumpData
|
||||
import info.nightscout.comboctl.base.Nonce
|
||||
import info.nightscout.comboctl.base.PumpStateStore
|
||||
import info.nightscout.comboctl.base.Tbr
|
||||
import info.nightscout.comboctl.base.toBluetoothAddress
|
||||
import info.nightscout.comboctl.base.toCipher
|
||||
import info.nightscout.comboctl.base.toNonce
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import info.nightscout.shared.sharedPreferences.SPDelegateInt
|
||||
import info.nightscout.shared.sharedPreferences.SPDelegateLong
|
||||
import info.nightscout.shared.sharedPreferences.SPDelegateString
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.UtcOffset
|
||||
import kotlin.reflect.KClassifier
|
||||
|
||||
/**
|
||||
* Special pump state store that mainly uses the internalSP, but also is able to sync up the AAPS main SP with that internal SP.
|
||||
*
|
||||
* This pump state store solves a problem: What if the user already paired AAPS with a Combo, and then imports and old AAPS
|
||||
* settings file, intending to just restore other settings like the basal profile, but does _not_ intend to import an old
|
||||
* pump state? After all, if we write a pump state to the AAPS main SP, the state values will also be written into a settings
|
||||
* file when exporting said settings. If we just relied on the main AAPS SP, the current pump state - including pairing info
|
||||
* like the PC and CP keys - would be overwritten with the old ones from the imported settings, and the user would have to
|
||||
* unnecessarily re-pair the Combo every time settings get imported.
|
||||
*
|
||||
* The solution is for this driver to _not_ primarily use the AAPS main SP. Instead, it uses its own internal SP. That SP
|
||||
* is exclusively used by this driver. But, a copy of the pump state values are stored in the AAPS main SP, and kept in
|
||||
* sync with the contents of the internal SP.
|
||||
*
|
||||
* When a new pump state is created, the invariant values (CP/PC keys etc.) are written in both the AAPS main SP and the
|
||||
* internal SP, along with the initial nonce. During operation, the driver will update the nonce value of the internal SP.
|
||||
* If TBR states are changed, then these changes are stored in the internal SP. Callers can use [copyVariantValuesToAAPSMainSP]
|
||||
* to update the corresponding values in the AAPS main SP to keep the two SPs in sync.
|
||||
*
|
||||
* There are a couple of special situations:
|
||||
*
|
||||
* 1. There is no pump state in the internal SP, and there is no pump state in the AAPS main SP. This is the unpaired state.
|
||||
* 2. There is no pump state in the internal SP, but there is one in the AAPS main SP. This typically happens when the
|
||||
* user (re)installed AAPS, and immediately after installing, imported AAPS settings. Callers are then supposed to call
|
||||
* [copyAllValuesFromAAPSMainSP] to import the pump state from the AAPS main SP over to the internal SP and continue to
|
||||
* work with that state.
|
||||
* 3. There is a pump state, and there is also one in the AAPS main SP. The latter one is then ignored. A pump state in the
|
||||
* AAPS main SP solely and only exists to be able to export/import pump states. It is not used for actual pump operations.
|
||||
* In particular, if - as mentioned above - a pump is already paired, and the user imports settings, this logic prevents
|
||||
* the current pump state to be overwritten.
|
||||
*/
|
||||
class AAPSPumpStateStore(
|
||||
private val aapsMainSP: SP,
|
||||
private val internalSP: InternalSP
|
||||
) : PumpStateStore {
|
||||
private var btAddress: String
|
||||
by SPDelegateString(internalSP, PreferenceKeys.BT_ADDRESS_KEY.str, "")
|
||||
|
||||
// The nonce is updated with commit instead of apply to make sure
|
||||
// is atomically written to storage synchronously, minimizing
|
||||
// the likelihood that it could be lost due to app crashes etc.
|
||||
// It is very important to not lose the nonce, hence that choice.
|
||||
private var nonceString: String
|
||||
by SPDelegateString(internalSP, PreferenceKeys.NONCE_KEY.str, Nonce.nullNonce().toString(), commit = true)
|
||||
|
||||
private var cpCipherString: String
|
||||
by SPDelegateString(internalSP, PreferenceKeys.CP_CIPHER_KEY.str, "")
|
||||
private var pcCipherString: String
|
||||
by SPDelegateString(internalSP, PreferenceKeys.PC_CIPHER_KEY.str, "")
|
||||
private var keyResponseAddressInt: Int
|
||||
by SPDelegateInt(internalSP, PreferenceKeys.KEY_RESPONSE_ADDRESS_KEY.str, 0)
|
||||
private var pumpID: String
|
||||
by SPDelegateString(internalSP, PreferenceKeys.PUMP_ID_KEY.str, "")
|
||||
private var tbrTimestamp: Long
|
||||
by SPDelegateLong(internalSP, PreferenceKeys.TBR_TIMESTAMP_KEY.str, 0)
|
||||
private var tbrPercentage: Int
|
||||
by SPDelegateInt(internalSP, PreferenceKeys.TBR_PERCENTAGE_KEY.str, 0)
|
||||
private var tbrDuration: Int
|
||||
by SPDelegateInt(internalSP, PreferenceKeys.TBR_DURATION_KEY.str, 0)
|
||||
private var tbrType: String
|
||||
by SPDelegateString(internalSP, PreferenceKeys.TBR_TYPE_KEY.str, "")
|
||||
private var utcOffsetSeconds: Int
|
||||
by SPDelegateInt(internalSP, PreferenceKeys.UTC_OFFSET_KEY.str, 0)
|
||||
|
||||
override fun createPumpState(
|
||||
pumpAddress: BluetoothAddress,
|
||||
invariantPumpData: InvariantPumpData,
|
||||
utcOffset: UtcOffset,
|
||||
tbrState: CurrentTbrState
|
||||
) {
|
||||
internalSP.edit(commit = true) {
|
||||
putString(PreferenceKeys.BT_ADDRESS_KEY.str, pumpAddress.toString().uppercase())
|
||||
putString(PreferenceKeys.CP_CIPHER_KEY.str, invariantPumpData.clientPumpCipher.toString())
|
||||
putString(PreferenceKeys.PC_CIPHER_KEY.str, invariantPumpData.pumpClientCipher.toString())
|
||||
putInt(PreferenceKeys.KEY_RESPONSE_ADDRESS_KEY.str, invariantPumpData.keyResponseAddress.toInt() and 0xFF)
|
||||
putString(PreferenceKeys.PUMP_ID_KEY.str, invariantPumpData.pumpID)
|
||||
putLong(PreferenceKeys.TBR_TIMESTAMP_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.timestamp.epochSeconds else -1)
|
||||
putInt(PreferenceKeys.TBR_PERCENTAGE_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.percentage else -1)
|
||||
putInt(PreferenceKeys.TBR_DURATION_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.durationInMinutes else -1)
|
||||
putString(PreferenceKeys.TBR_TYPE_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.type.stringId else "")
|
||||
putInt(PreferenceKeys.UTC_OFFSET_KEY.str, utcOffset.totalSeconds)
|
||||
}
|
||||
|
||||
copyAllValuesToAAPSMainSP(commit = true)
|
||||
}
|
||||
|
||||
override fun deletePumpState(pumpAddress: BluetoothAddress): Boolean {
|
||||
val hasState = internalSP.contains(PreferenceKeys.NONCE_KEY.str)
|
||||
|
||||
internalSP.edit(commit = true) {
|
||||
for (keys in PreferenceKeys.values())
|
||||
remove(keys.str)
|
||||
}
|
||||
|
||||
aapsMainSP.edit(commit = true) {
|
||||
for (keys in PreferenceKeys.values())
|
||||
remove(keys.str)
|
||||
}
|
||||
|
||||
return hasState
|
||||
}
|
||||
|
||||
override fun hasPumpState(pumpAddress: BluetoothAddress): Boolean =
|
||||
internalSP.contains(PreferenceKeys.NONCE_KEY.str)
|
||||
|
||||
override fun getAvailablePumpStateAddresses(): Set<BluetoothAddress> =
|
||||
if (btAddress.isBlank()) setOf() else setOf(btAddress.toBluetoothAddress())
|
||||
|
||||
override fun getInvariantPumpData(pumpAddress: BluetoothAddress) = InvariantPumpData(
|
||||
clientPumpCipher = cpCipherString.toCipher(),
|
||||
pumpClientCipher = pcCipherString.toCipher(),
|
||||
keyResponseAddress = keyResponseAddressInt.toByte(),
|
||||
pumpID = pumpID
|
||||
)
|
||||
|
||||
override fun getCurrentTxNonce(pumpAddress: BluetoothAddress) = nonceString.toNonce()
|
||||
|
||||
override fun setCurrentTxNonce(pumpAddress: BluetoothAddress, currentTxNonce: Nonce) {
|
||||
nonceString = currentTxNonce.toString()
|
||||
}
|
||||
|
||||
override fun getCurrentUtcOffset(pumpAddress: BluetoothAddress) =
|
||||
UtcOffset(seconds = utcOffsetSeconds)
|
||||
|
||||
override fun setCurrentUtcOffset(pumpAddress: BluetoothAddress, utcOffset: UtcOffset) {
|
||||
utcOffsetSeconds = utcOffset.totalSeconds
|
||||
}
|
||||
|
||||
override fun getCurrentTbrState(pumpAddress: BluetoothAddress) =
|
||||
if (tbrTimestamp >= 0)
|
||||
CurrentTbrState.TbrStarted(Tbr(
|
||||
timestamp = Instant.fromEpochSeconds(tbrTimestamp),
|
||||
percentage = tbrPercentage,
|
||||
durationInMinutes = tbrDuration,
|
||||
type = Tbr.Type.fromStringId(tbrType)!!
|
||||
))
|
||||
else
|
||||
CurrentTbrState.NoTbrOngoing
|
||||
|
||||
|
||||
override fun setCurrentTbrState(pumpAddress: BluetoothAddress, currentTbrState: CurrentTbrState) {
|
||||
when (currentTbrState) {
|
||||
is CurrentTbrState.TbrStarted -> {
|
||||
tbrTimestamp = currentTbrState.tbr.timestamp.epochSeconds
|
||||
tbrPercentage = currentTbrState.tbr.percentage
|
||||
tbrDuration = currentTbrState.tbr.durationInMinutes
|
||||
tbrType = currentTbrState.tbr.type.stringId
|
||||
}
|
||||
else -> {
|
||||
tbrTimestamp = -1
|
||||
tbrPercentage = -1
|
||||
tbrDuration = -1
|
||||
tbrType = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copies only those pump state values from the internal SP to the AAPS main SP which can vary during
|
||||
// pump operations. These are the TBR values, the UTC offset, and the nonce. Users are recommended to
|
||||
// call this after AAPS disconnects the pump.
|
||||
fun copyVariantValuesToAAPSMainSP(commit: Boolean) =
|
||||
copyValuesBetweenSPs(commit, from = internalSP, to = aapsMainSP, arrayOf(
|
||||
PreferenceKeys.NONCE_KEY,
|
||||
PreferenceKeys.TBR_TIMESTAMP_KEY,
|
||||
PreferenceKeys.TBR_PERCENTAGE_KEY,
|
||||
PreferenceKeys.TBR_DURATION_KEY,
|
||||
PreferenceKeys.TBR_TYPE_KEY,
|
||||
PreferenceKeys.UTC_OFFSET_KEY
|
||||
))
|
||||
|
||||
// Copies all pump state values from the AAPS main SP to the internal SP. This is supposed to be
|
||||
// called if the internal SP is empty. That way, a pump state can be imported from AAPS settings files.
|
||||
fun copyAllValuesFromAAPSMainSP(commit: Boolean) =
|
||||
copyValuesBetweenSPs(commit, from = aapsMainSP, to = internalSP, keys = PreferenceKeys.values())
|
||||
|
||||
// Copies all pump state values from the internal SP to the AAPS main SP to. The createPumpState()
|
||||
// function calls this after creating the pump state to ensure both SPs are in sync. Also, this
|
||||
// should be called when the driver starts in case AAPS settings are imported and there is already
|
||||
// a pump state present in the internal SP. Calling this then ensures that the pump state in the
|
||||
// main SP is fully synced up with the one from the internal SP, and does not contain some old
|
||||
// state that is not in use anymore.
|
||||
fun copyAllValuesToAAPSMainSP(commit: Boolean) =
|
||||
copyValuesBetweenSPs(commit, from = internalSP, to = aapsMainSP, keys = PreferenceKeys.values())
|
||||
|
||||
private fun copyValuesBetweenSPs(commit: Boolean, from: SP, to: SP, keys: Array<PreferenceKeys>) {
|
||||
to.edit(commit) {
|
||||
for (key in keys) {
|
||||
if (!from.contains(key.str))
|
||||
continue
|
||||
when (key.type) {
|
||||
Int::class -> putInt(key.str, from.getInt(key.str, 0))
|
||||
Long::class -> putLong(key.str, from.getLong(key.str, 0L))
|
||||
String::class -> putString(key.str, from.getString(key.str, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class PreferenceKeys(val str: String, val type: KClassifier) {
|
||||
BT_ADDRESS_KEY("combov2-bt-address-key", String::class),
|
||||
NONCE_KEY("combov2-nonce-key", String::class),
|
||||
CP_CIPHER_KEY("combov2-cp-cipher-key", String::class),
|
||||
PC_CIPHER_KEY("combov2-pc-cipher-key", String::class),
|
||||
KEY_RESPONSE_ADDRESS_KEY("combov2-key-response-address-key", Int::class),
|
||||
PUMP_ID_KEY("combov2-pump-id-key", String::class),
|
||||
TBR_TIMESTAMP_KEY("combov2-tbr-timestamp", Long::class),
|
||||
TBR_PERCENTAGE_KEY("combov2-tbr-percentage", Int::class),
|
||||
TBR_DURATION_KEY("combov2-tbr-duration", Int::class),
|
||||
TBR_TYPE_KEY("combov2-tbr-type", String::class),
|
||||
UTC_OFFSET_KEY("combov2-utc-offset", Int::class);
|
||||
|
||||
override fun toString(): String = str
|
||||
}
|
||||
}
|
|
@ -133,7 +133,17 @@ class ComboV2Plugin @Inject constructor (
|
|||
|
||||
private val _pumpDescription = PumpDescription()
|
||||
|
||||
private val pumpStateStore = SPPumpStateStore(sp)
|
||||
// The internal SP is the one that will be mainly used by the driver.
|
||||
// The AAPS main SP is updated when the pump state store is created
|
||||
// and when the driver disconnects (to update the nonce value).
|
||||
private val internalSP = InternalSP(
|
||||
context.getSharedPreferences(
|
||||
context.packageName + ".COMBO_PUMP_STATE_STORE",
|
||||
Context.MODE_PRIVATE
|
||||
),
|
||||
context
|
||||
)
|
||||
private val pumpStateStore = AAPSPumpStateStore(aapsMainSP = sp, internalSP = internalSP)
|
||||
|
||||
// These are initialized in onStart() and torn down in onStop().
|
||||
private var bluetoothInterface: AndroidBluetoothInterface? = null
|
||||
|
@ -236,6 +246,34 @@ class ComboV2Plugin @Inject constructor (
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// Check if there is a pump state in the internal SP. If not, try to
|
||||
// copy a pump state from the AAPS main SP. It is possible for example
|
||||
// that AAPS was reinstalled, and the previous settings were imported.
|
||||
// In that case, the internal SP is empty, but there is a pump state
|
||||
// that comes from the settings. We want to restore that pump state
|
||||
// then. If however, there _is_ a pump state in the internal SP, then
|
||||
// we just ignore any state in the main SP. For example, if the user
|
||||
// imports an older AAPS settings file with an old pump state, and a
|
||||
// Combo is already paired with AAPS, then it makes no sense to overwrite
|
||||
// the current pump state with the old one from the imported settings.
|
||||
if (pumpStateStore.getAvailablePumpStateAddresses().isEmpty()) {
|
||||
aapsLogger.info(LTag.PUMP, "There is no pump state in the internal SP; trying to copy a pump state from the main AAPS SP")
|
||||
pumpStateStore.copyAllValuesFromAAPSMainSP(commit = true)
|
||||
val btAddress = pumpStateStore.getAvailablePumpStateAddresses().firstOrNull()
|
||||
if (btAddress == null)
|
||||
aapsLogger.info(LTag.PUMP, "No pump state found in the main AAPS SP; continuing without a pump state (implying that no pump is paired)")
|
||||
else
|
||||
aapsLogger.info(LTag.PUMP, "Pump state found in the main AAPS SP (bluetooth address: $btAddress); continuing with that state")
|
||||
} else {
|
||||
// Copy over the internal SP pump state to the main AAPS SP. If the user
|
||||
// just imported AAPS settings, and said settings contained an old pump
|
||||
// state, then that old pump state is ignored if there is already a
|
||||
// current pump state in the internal SP - but we still need to make sure
|
||||
// the old pump state in the main AAPS SP is replaced by the current one.
|
||||
aapsLogger.debug(LTag.PUMP, "Copying internal SP pump state to main AAPS SP")
|
||||
pumpStateStore.copyAllValuesToAAPSMainSP(commit = false)
|
||||
}
|
||||
|
||||
aapsLogger.debug(LTag.PUMP, "Creating bluetooth interface")
|
||||
bluetoothInterface = AndroidBluetoothInterface(context)
|
||||
|
||||
|
@ -624,6 +662,11 @@ class ComboV2Plugin @Inject constructor (
|
|||
override fun disconnect(reason: String) {
|
||||
aapsLogger.debug(LTag.PUMP, "Disconnecting from Combo; reason: $reason")
|
||||
disconnectInternal(forceDisconnect = false)
|
||||
|
||||
// Sync up the TBR and nonce states in the main AAPS SP. We don't do this all the
|
||||
// time since this is unnecessary waste of resources. It is sufficient to update
|
||||
// those once AAPS is done with the connection.
|
||||
pumpStateStore.copyVariantValuesToAAPSMainSP(commit = false)
|
||||
}
|
||||
|
||||
// This is called when (a) the AAPS watchdog is about to toggle
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
package info.nightscout.pump.combov2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.StringRes
|
||||
import info.nightscout.shared.SafeParse
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
|
||||
// This is a copy of the AAPS SPImplementation. We keep this to be able
|
||||
// to set up a custom internal SP store for the Combo pump state.
|
||||
class InternalSP(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val context: Context
|
||||
) : SP {
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun edit(commit: Boolean, block: SP.Editor.() -> Unit) {
|
||||
val spEdit = sharedPreferences.edit()
|
||||
|
||||
val edit = object : SP.Editor {
|
||||
override fun clear() {
|
||||
spEdit.clear()
|
||||
}
|
||||
|
||||
override fun remove(@StringRes resourceID: Int) {
|
||||
spEdit.remove(context.getString(resourceID))
|
||||
}
|
||||
override fun remove(key: String) {
|
||||
spEdit.remove(key)
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) {
|
||||
spEdit.putBoolean(key, value)
|
||||
}
|
||||
override fun putBoolean(@StringRes resourceID: Int, value: Boolean) {
|
||||
spEdit.putBoolean(context.getString(resourceID), value)
|
||||
}
|
||||
override fun putDouble(key: String, value: Double) {
|
||||
spEdit.putString(key, value.toString())
|
||||
}
|
||||
override fun putDouble(@StringRes resourceID: Int, value: Double) {
|
||||
spEdit.putString(context.getString(resourceID), value.toString())
|
||||
}
|
||||
override fun putLong(key: String, value: Long) {
|
||||
spEdit.putLong(key, value)
|
||||
}
|
||||
override fun putLong(@StringRes resourceID: Int, value: Long) {
|
||||
spEdit.putLong(context.getString(resourceID), value)
|
||||
}
|
||||
override fun putInt(key: String, value: Int) {
|
||||
spEdit.putInt(key, value)
|
||||
}
|
||||
override fun putInt(@StringRes resourceID: Int, value: Int) {
|
||||
spEdit.putInt(context.getString(resourceID), value)
|
||||
}
|
||||
override fun putString(key: String, value: String) {
|
||||
spEdit.putString(key, value)
|
||||
}
|
||||
override fun putString(@StringRes resourceID: Int, value: String) {
|
||||
spEdit.putString(context.getString(resourceID), value)
|
||||
}
|
||||
}
|
||||
|
||||
block(edit)
|
||||
|
||||
if (commit)
|
||||
spEdit.commit()
|
||||
else
|
||||
spEdit.apply()
|
||||
}
|
||||
|
||||
override fun getAll(): Map<String, *> = sharedPreferences.all
|
||||
|
||||
override fun clear() = sharedPreferences.edit().clear().apply()
|
||||
|
||||
override fun contains(key: String): Boolean = sharedPreferences.contains(key)
|
||||
|
||||
override fun contains(resourceId: Int): Boolean = sharedPreferences.contains(context.getString(resourceId))
|
||||
|
||||
override fun remove(resourceID: Int) =
|
||||
sharedPreferences.edit().remove(context.getString(resourceID)).apply()
|
||||
|
||||
override fun remove(key: String) =
|
||||
sharedPreferences.edit().remove(key).apply()
|
||||
|
||||
override fun getString(resourceID: Int, defaultValue: String): String =
|
||||
sharedPreferences.getString(context.getString(resourceID), defaultValue) ?: defaultValue
|
||||
|
||||
override fun getStringOrNull(resourceID: Int, defaultValue: String?): String? =
|
||||
sharedPreferences.getString(context.getString(resourceID), defaultValue) ?: defaultValue
|
||||
|
||||
override fun getStringOrNull(key: String, defaultValue: String?): String? =
|
||||
sharedPreferences.getString(key, defaultValue)
|
||||
|
||||
override fun getString(key: String, defaultValue: String): String =
|
||||
sharedPreferences.getString(key, defaultValue) ?: defaultValue
|
||||
|
||||
override fun getBoolean(resourceID: Int, defaultValue: Boolean): Boolean {
|
||||
return try {
|
||||
sharedPreferences.getBoolean(context.getString(resourceID), defaultValue)
|
||||
} catch (e: Exception) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||
return try {
|
||||
sharedPreferences.getBoolean(key, defaultValue)
|
||||
} catch (e: Exception) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDouble(resourceID: Int, defaultValue: Double): Double =
|
||||
SafeParse.stringToDouble(sharedPreferences.getString(context.getString(resourceID), defaultValue.toString()))
|
||||
|
||||
override fun getDouble(key: String, defaultValue: Double): Double =
|
||||
SafeParse.stringToDouble(sharedPreferences.getString(key, defaultValue.toString()))
|
||||
|
||||
override fun getInt(resourceID: Int, defaultValue: Int): Int {
|
||||
return try {
|
||||
sharedPreferences.getInt(context.getString(resourceID), defaultValue)
|
||||
} catch (e: Exception) {
|
||||
SafeParse.stringToInt(sharedPreferences.getString(context.getString(resourceID), defaultValue.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String, defaultValue: Int): Int {
|
||||
return try {
|
||||
sharedPreferences.getInt(key, defaultValue)
|
||||
} catch (e: Exception) {
|
||||
SafeParse.stringToInt(sharedPreferences.getString(key, defaultValue.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(resourceID: Int, defaultValue: Long): Long {
|
||||
return try {
|
||||
sharedPreferences.getLong(context.getString(resourceID), defaultValue)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
SafeParse.stringToLong(sharedPreferences.getString(context.getString(resourceID), defaultValue.toString()))
|
||||
} catch (e: Exception) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(key: String, defaultValue: Long): Long {
|
||||
return try {
|
||||
sharedPreferences.getLong(key, defaultValue)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
SafeParse.stringToLong(sharedPreferences.getString(key, defaultValue.toString()))
|
||||
} catch (e: Exception) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun incLong(resourceID: Int) {
|
||||
val value = getLong(resourceID, 0) + 1L
|
||||
sharedPreferences.edit().putLong(context.getString(resourceID), value).apply()
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) = sharedPreferences.edit().putBoolean(key, value).apply()
|
||||
|
||||
override fun putBoolean(resourceID: Int, value: Boolean) =
|
||||
sharedPreferences.edit().putBoolean(context.getString(resourceID), value).apply()
|
||||
|
||||
override fun putDouble(key: String, value: Double) =
|
||||
sharedPreferences.edit().putString(key, value.toString()).apply()
|
||||
|
||||
override fun putDouble(resourceID: Int, value: Double) {
|
||||
sharedPreferences.edit().putString(context.getString(resourceID), value.toString()).apply()
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long) =
|
||||
sharedPreferences.edit().putLong(key, value).apply()
|
||||
|
||||
override fun putLong(resourceID: Int, value: Long) =
|
||||
sharedPreferences.edit().putLong(context.getString(resourceID), value).apply()
|
||||
|
||||
override fun putInt(key: String, value: Int) =
|
||||
sharedPreferences.edit().putInt(key, value).apply()
|
||||
|
||||
override fun putInt(resourceID: Int, value: Int) =
|
||||
sharedPreferences.edit().putInt(context.getString(resourceID), value).apply()
|
||||
|
||||
override fun incInt(resourceID: Int) {
|
||||
val value = getInt(resourceID, 0) + 1
|
||||
sharedPreferences.edit().putInt(context.getString(resourceID), value).apply()
|
||||
}
|
||||
|
||||
override fun putString(resourceID: Int, value: String) =
|
||||
sharedPreferences.edit().putString(context.getString(resourceID), value).apply()
|
||||
|
||||
override fun putString(key: String, value: String) =
|
||||
sharedPreferences.edit().putString(key, value).apply()
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
package info.nightscout.pump.combov2
|
||||
|
||||
import info.nightscout.comboctl.base.BluetoothAddress
|
||||
import info.nightscout.comboctl.base.CurrentTbrState
|
||||
import info.nightscout.comboctl.base.InvariantPumpData
|
||||
import info.nightscout.comboctl.base.Nonce
|
||||
import info.nightscout.comboctl.base.PumpStateStore
|
||||
import info.nightscout.comboctl.base.Tbr
|
||||
import info.nightscout.comboctl.base.toBluetoothAddress
|
||||
import info.nightscout.comboctl.base.toCipher
|
||||
import info.nightscout.comboctl.base.toNonce
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import info.nightscout.shared.sharedPreferences.SPDelegateInt
|
||||
import info.nightscout.shared.sharedPreferences.SPDelegateLong
|
||||
import info.nightscout.shared.sharedPreferences.SPDelegateString
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.UtcOffset
|
||||
|
||||
/**
|
||||
* AndroidAPS [SP] based pump state store.
|
||||
*
|
||||
* This store is set up to contain a single paired pump. AndroidAPS is not
|
||||
* designed to handle multiple pumps, so this simplification makes sense.
|
||||
* This affects all accessors, which
|
||||
*/
|
||||
class SPPumpStateStore(private val sp: SP) : PumpStateStore {
|
||||
private var btAddress: String
|
||||
by SPDelegateString(sp, BT_ADDRESS_KEY, "")
|
||||
|
||||
// The nonce is updated with commit instead of apply to make sure
|
||||
// is atomically written to storage synchronously, minimizing
|
||||
// the likelihood that it could be lost due to app crashes etc.
|
||||
// It is very important to not lose the nonce, hence that choice.
|
||||
private var nonceString: String
|
||||
by SPDelegateString(sp, NONCE_KEY, Nonce.nullNonce().toString(), commit = true)
|
||||
|
||||
private var cpCipherString: String
|
||||
by SPDelegateString(sp, CP_CIPHER_KEY, "")
|
||||
private var pcCipherString: String
|
||||
by SPDelegateString(sp, PC_CIPHER_KEY, "")
|
||||
private var keyResponseAddressInt: Int
|
||||
by SPDelegateInt(sp, KEY_RESPONSE_ADDRESS_KEY, 0)
|
||||
private var pumpID: String
|
||||
by SPDelegateString(sp, PUMP_ID_KEY, "")
|
||||
private var tbrTimestamp: Long
|
||||
by SPDelegateLong(sp, TBR_TIMESTAMP_KEY, 0)
|
||||
private var tbrPercentage: Int
|
||||
by SPDelegateInt(sp, TBR_PERCENTAGE_KEY, 0)
|
||||
private var tbrDuration: Int
|
||||
by SPDelegateInt(sp, TBR_DURATION_KEY, 0)
|
||||
private var tbrType: String
|
||||
by SPDelegateString(sp, TBR_TYPE_KEY, "")
|
||||
private var utcOffsetSeconds: Int
|
||||
by SPDelegateInt(sp, UTC_OFFSET_KEY, 0)
|
||||
|
||||
override fun createPumpState(
|
||||
pumpAddress: BluetoothAddress,
|
||||
invariantPumpData: InvariantPumpData,
|
||||
utcOffset: UtcOffset,
|
||||
tbrState: CurrentTbrState
|
||||
) {
|
||||
// Write these values via edit() instead of using the delegates
|
||||
// above to be able to write all of them with a single commit.
|
||||
sp.edit(commit = true) {
|
||||
putString(BT_ADDRESS_KEY, pumpAddress.toString().uppercase())
|
||||
putString(CP_CIPHER_KEY, invariantPumpData.clientPumpCipher.toString())
|
||||
putString(PC_CIPHER_KEY, invariantPumpData.pumpClientCipher.toString())
|
||||
putInt(KEY_RESPONSE_ADDRESS_KEY, invariantPumpData.keyResponseAddress.toInt() and 0xFF)
|
||||
putString(PUMP_ID_KEY, invariantPumpData.pumpID)
|
||||
putLong(TBR_TIMESTAMP_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.timestamp.epochSeconds else -1)
|
||||
putInt(TBR_PERCENTAGE_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.percentage else -1)
|
||||
putInt(TBR_DURATION_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.durationInMinutes else -1)
|
||||
putString(TBR_TYPE_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.type.stringId else "")
|
||||
putInt(UTC_OFFSET_KEY, utcOffset.totalSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deletePumpState(pumpAddress: BluetoothAddress): Boolean {
|
||||
val hasState = sp.contains(NONCE_KEY)
|
||||
|
||||
sp.edit(commit = true) {
|
||||
remove(BT_ADDRESS_KEY)
|
||||
remove(NONCE_KEY)
|
||||
remove(CP_CIPHER_KEY)
|
||||
remove(PC_CIPHER_KEY)
|
||||
remove(KEY_RESPONSE_ADDRESS_KEY)
|
||||
remove(TBR_TIMESTAMP_KEY)
|
||||
remove(TBR_PERCENTAGE_KEY)
|
||||
remove(TBR_DURATION_KEY)
|
||||
remove(TBR_TYPE_KEY)
|
||||
remove(UTC_OFFSET_KEY)
|
||||
}
|
||||
|
||||
return hasState
|
||||
}
|
||||
|
||||
override fun hasPumpState(pumpAddress: BluetoothAddress) =
|
||||
sp.contains(NONCE_KEY)
|
||||
|
||||
override fun getAvailablePumpStateAddresses() =
|
||||
if (btAddress.isBlank()) setOf() else setOf(btAddress.toBluetoothAddress())
|
||||
|
||||
override fun getInvariantPumpData(pumpAddress: BluetoothAddress) = InvariantPumpData(
|
||||
clientPumpCipher = cpCipherString.toCipher(),
|
||||
pumpClientCipher = pcCipherString.toCipher(),
|
||||
keyResponseAddress = keyResponseAddressInt.toByte(),
|
||||
pumpID = pumpID
|
||||
)
|
||||
|
||||
override fun getCurrentTxNonce(pumpAddress: BluetoothAddress) = nonceString.toNonce()
|
||||
|
||||
override fun setCurrentTxNonce(pumpAddress: BluetoothAddress, currentTxNonce: Nonce) {
|
||||
nonceString = currentTxNonce.toString()
|
||||
}
|
||||
|
||||
override fun getCurrentUtcOffset(pumpAddress: BluetoothAddress) =
|
||||
UtcOffset(seconds = utcOffsetSeconds)
|
||||
|
||||
override fun setCurrentUtcOffset(pumpAddress: BluetoothAddress, utcOffset: UtcOffset) {
|
||||
utcOffsetSeconds = utcOffset.totalSeconds
|
||||
}
|
||||
|
||||
override fun getCurrentTbrState(pumpAddress: BluetoothAddress) =
|
||||
if (tbrTimestamp >= 0)
|
||||
CurrentTbrState.TbrStarted(Tbr(
|
||||
timestamp = Instant.fromEpochSeconds(tbrTimestamp),
|
||||
percentage = tbrPercentage,
|
||||
durationInMinutes = tbrDuration,
|
||||
type = Tbr.Type.fromStringId(tbrType)!!
|
||||
))
|
||||
else
|
||||
CurrentTbrState.NoTbrOngoing
|
||||
|
||||
|
||||
override fun setCurrentTbrState(pumpAddress: BluetoothAddress, currentTbrState: CurrentTbrState) {
|
||||
when (currentTbrState) {
|
||||
is CurrentTbrState.TbrStarted -> {
|
||||
tbrTimestamp = currentTbrState.tbr.timestamp.epochSeconds
|
||||
tbrPercentage = currentTbrState.tbr.percentage
|
||||
tbrDuration = currentTbrState.tbr.durationInMinutes
|
||||
tbrType = currentTbrState.tbr.type.stringId
|
||||
}
|
||||
else -> {
|
||||
tbrTimestamp = -1
|
||||
tbrPercentage = -1
|
||||
tbrDuration = -1
|
||||
tbrType = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BT_ADDRESS_KEY = "combov2-bt-address-key"
|
||||
const val NONCE_KEY = "combov2-nonce-key"
|
||||
const val CP_CIPHER_KEY = "combov2-cp-cipher-key"
|
||||
const val PC_CIPHER_KEY = "combov2-pc-cipher-key"
|
||||
const val KEY_RESPONSE_ADDRESS_KEY = "combov2-key-response-address-key"
|
||||
const val PUMP_ID_KEY = "combov2-pump-id-key"
|
||||
const val TBR_TIMESTAMP_KEY = "combov2-tbr-timestamp"
|
||||
const val TBR_PERCENTAGE_KEY = "combov2-tbr-percentage"
|
||||
const val TBR_DURATION_KEY = "combov2-tbr-duration"
|
||||
const val TBR_TYPE_KEY = "combov2-tbr-type"
|
||||
const val UTC_OFFSET_KEY = "combov2-utc-offset"
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
android:key="@string/key_combov2_unpair_pump"
|
||||
android:shouldDisableView="true"/>
|
||||
|
||||
<SeekBarPreference
|
||||
<info.nightscout.core.ui.elements.IntSeekBarPreference
|
||||
android:key="@string/key_combov2_discovery_duration"
|
||||
android:title="@string/combov2_discovery_duration"
|
||||
android:min="30"
|
||||
|
|
Loading…
Reference in a new issue