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:
Milos Kozak 2022-12-06 12:51:12 +01:00 committed by GitHub
commit 805ff505b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 536 additions and 168 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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