Merge pull request #12 from 0pen-dash/adrian/history-persistance

history records / database
This commit is contained in:
bartsopers 2021-02-28 13:37:53 +01:00 committed by GitHub
commit 42279e1339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 415 additions and 4 deletions

View file

@ -13,9 +13,16 @@ dependencies {
testImplementation "org.skyscreamer:jsonassert:1.5.0"
testImplementation "org.hamcrest:hamcrest-all:1.3"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'
androidTestImplementation "androidx.test.ext:junit:$androidx_junit"
androidTestImplementation "androidx.test:rules:$androidx_rules"
// newer integration test libraries might not work
// https://stackoverflow.com/questions/64700104/attribute-androidforcequeryable-not-found-in-android-studio-when-running-espres
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}

View file

@ -17,4 +17,11 @@ android {
dependencies {
implementation project(':core')
implementation project(':omnipod-common')
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'com.github.guepardoapps:kulid:1.1.2.0'
}

View file

@ -0,0 +1,72 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.guepardoapps.kulid.ULID
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.DashHistoryDatabase
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper.HistoryMapper
import io.reactivex.schedulers.Schedulers
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DashHistoryTest {
private lateinit var dao: HistoryRecordDao
private lateinit var database: DashHistoryDatabase
private lateinit var dashHistory: DashHistory
@get:Rule
val schedulerRule = RxSchedulerRule(Schedulers.trampoline())
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context, DashHistoryDatabase::class.java).build()
dao = database.historyRecordDao()
dashHistory = DashHistory(dao, HistoryMapper())
}
@Test
fun testInsertionAndConverters() {
dashHistory.getRecords().test().apply {
assertValue { it.isEmpty() }
}
dashHistory.createRecord(commandType = OmnipodCommandType.CANCEL_BOLUS, 0L).test().apply {
assertValue { ULID.isValid(it) }
}
dashHistory.getRecords().test().apply {
assertValue { it.size == 1 }
}
}
@Test
fun testExceptionOnBolusWithoutRecord() {
dashHistory.getRecords().test().apply {
assertValue { it.isEmpty() }
}
dashHistory.createRecord(commandType = OmnipodCommandType.SET_BOLUS, 0L).test().apply {
assertError(IllegalArgumentException::class.java)
}
dashHistory.getRecords().test().apply {
assertValue { it.isEmpty() }
}
}
@After
fun tearDown() {
database.close()
}
}

View file

@ -0,0 +1,32 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history
import io.reactivex.Scheduler
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.plugins.RxJavaPlugins
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
// TODO: move to core before the big merge
class RxSchedulerRule(val scheduler: Scheduler) : TestRule {
override fun apply(base: Statement, description: Description) =
object : Statement() {
override fun evaluate() {
RxAndroidPlugins.reset()
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
RxJavaPlugins.reset()
RxJavaPlugins.setIoSchedulerHandler { scheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
try {
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}

View file

@ -0,0 +1,33 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.dagger
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.Reusable
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.DashHistory
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.DashHistoryDatabase
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper.HistoryMapper
import javax.inject.Singleton
@Module
class OmnipodDashHistoryModule {
@Provides
@Singleton
internal fun provideDatabase(context: Context): DashHistoryDatabase = DashHistoryDatabase.build(context)
@Provides
@Singleton
internal fun provideHistoryRecordDao(dashHistoryDatabase: DashHistoryDatabase): HistoryRecordDao = dashHistoryDatabase.historyRecordDao()
@Provides
@Reusable // no state, let system decide when to reuse or create new.
internal fun provideHistoryMapper() = HistoryMapper()
@Provides
@Singleton
internal fun provideDashHistory(dao: HistoryRecordDao, historyMapper: HistoryMapper) =
DashHistory(dao, historyMapper)
}

View file

@ -16,7 +16,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOvervi
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.DashPodActivationWizardActivity
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.deactivation.DashPodDeactivationWizardActivity
@Module
@Module(includes = [OmnipodDashHistoryModule::class])
@Suppress("unused")
abstract class OmnipodDashModule {
// ACTIVITIES

View file

@ -0,0 +1,69 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history
import com.github.guepardoapps.kulid.ULID
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType.SET_BOLUS
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType.SET_TEMPORARY_BASAL
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.HistoryRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordEntity
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper.HistoryMapper
import io.reactivex.Completable
import io.reactivex.Single
import java.lang.System.currentTimeMillis
import javax.inject.Inject
class DashHistory @Inject constructor(
private val dao: HistoryRecordDao,
private val historyMapper: HistoryMapper
) {
fun markSuccess(id: String, date: Long): Completable = dao.markResolved(id, ResolvedResult.SUCCESS, currentTimeMillis())
fun markFailure(id: String, date: Long): Completable = dao.markResolved(id, ResolvedResult.FAILURE, currentTimeMillis())
fun createRecord(
commandType: OmnipodCommandType,
date: Long,
initialResult: InitialResult = InitialResult.UNCONFIRMED,
tempBasalRecord: TempBasalRecord? = null,
bolusRecord: BolusRecord? = null,
resolveResult: ResolvedResult? = null,
resolvedAt: Long? = null
): Single<String> {
val id = ULID.random()
when {
commandType == SET_BOLUS && bolusRecord == null ->
Single.error(IllegalArgumentException("bolusRecord missing on SET_BOLUS"))
commandType == SET_TEMPORARY_BASAL && tempBasalRecord == null ->
Single.error<String>(IllegalArgumentException("tempBasalRecord missing on SET_TEMPORARY_BASAL"))
else -> null
}?.let { return it }
return dao.save(
HistoryRecordEntity(
id = id,
date = date,
createdAt = currentTimeMillis(),
commandType = commandType,
tempBasalRecord = tempBasalRecord,
bolusRecord = bolusRecord,
initialResult = initialResult,
resolvedResult = resolveResult,
resolvedAt = resolvedAt,
)
).toSingle { id }
}
fun getRecords(): Single<List<HistoryRecord>> =
dao.all().map { list -> list.map(historyMapper::entityToDomain) }
fun getRecordsAfter(time: Long): Single<List<HistoryRecordEntity>> = dao.allSince(time)
}

View file

@ -0,0 +1,14 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
data class HistoryRecord(
val id: String, // ULID
val createdAt: Long, // creation date of the record
val date: Long, // when event actually happened
val commandType: OmnipodCommandType,
val initialResult: InitialResult,
val record: Record?,
val resolvedResult: ResolvedResult?,
val resolvedAt: Long?
)

View file

@ -0,0 +1,11 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data
sealed class Record
data class BolusRecord(val amout: Double, val bolusType: BolusType): Record()
data class TempBasalRecord(val duration: Long, val rate: Double): Record()
enum class BolusType {
DEFAULT, SMB
}

View file

@ -0,0 +1,9 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data
enum class InitialResult {
SUCCESS, FAILURE, UNCONFIRMED
}
enum class ResolvedResult {
SUCCESS, FAILURE
}

View file

@ -0,0 +1,34 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
import androidx.room.TypeConverter
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
class Converters {
@TypeConverter
fun toBolusType(s: String) = enumValueOf<BolusType>(s)
@TypeConverter
fun fromBolusType(bolusType: BolusType) = bolusType.name
@TypeConverter
fun toInitialResult(s: String) = enumValueOf<InitialResult>(s)
@TypeConverter
fun fromInitialResult(initialResult: InitialResult) = initialResult.name
@TypeConverter
fun toResolvedResult(s: String?) = s?.let { enumValueOf<ResolvedResult>(it) }
@TypeConverter
fun fromResolvedResult(resolvedResult: ResolvedResult?) = resolvedResult?.name
@TypeConverter
fun toOmnipodCommandType(s: String) = enumValueOf<OmnipodCommandType>(s)
@TypeConverter
fun fromOmnipodCommandType(omnipodCommandType: OmnipodCommandType) = omnipodCommandType.name
}

View file

@ -0,0 +1,30 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
@Database(
entities = [HistoryRecordEntity::class],
exportSchema = false,
version = DashHistoryDatabase.VERSION
)
@TypeConverters(Converters::class)
abstract class DashHistoryDatabase : RoomDatabase() {
abstract fun historyRecordDao() : HistoryRecordDao
companion object {
const val VERSION = 1
fun build(context: Context) =
Room.databaseBuilder(context.applicationContext, DashHistoryDatabase::class.java, "omnipod_dash_history_database.db")
.fallbackToDestructiveMigration()
.build()
}
}

View file

@ -0,0 +1,36 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
import io.reactivex.Completable
import io.reactivex.Single
@Dao
abstract class HistoryRecordDao {
@Query("SELECT * from historyrecords")
abstract fun all(): Single<List<HistoryRecordEntity>>
@Query("SELECT * from historyrecords")
abstract fun allBlocking(): List<HistoryRecordEntity>
@Query("SELECT * from historyrecords WHERE createdAt <= :since")
abstract fun allSince(since: Long): Single<List<HistoryRecordEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun saveBlocking(historyRecordEntity: HistoryRecordEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun save(historyRecordEntity: HistoryRecordEntity): Completable
@Delete
abstract fun delete(historyRecordEntity: HistoryRecordEntity): Completable
@Query("UPDATE historyrecords SET resolvedResult = :resolvedResult, resolvedAt = :resolvedAt WHERE id = :id ")
abstract fun markResolved(id: String, resolvedResult: ResolvedResult, resolvedAt: Long): Completable
}

View file

@ -0,0 +1,23 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord
@Entity(tableName = "historyrecords")
data class HistoryRecordEntity(
@PrimaryKey val id: String, // ULID
val createdAt: Long, // creation date of the record
val date: Long, // when event actually happened
val commandType: OmnipodCommandType,
val initialResult: InitialResult,
@Embedded(prefix = "tempBasalRecord_") val tempBasalRecord: TempBasalRecord?,
@Embedded(prefix = "bolusRecord_") val bolusRecord: BolusRecord?,
val resolvedResult: ResolvedResult?,
val resolvedAt: Long?)

View file

@ -0,0 +1,34 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.HistoryRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordEntity
class HistoryMapper {
fun domainToEntity(historyRecord: HistoryRecord): HistoryRecordEntity =
HistoryRecordEntity(
id = historyRecord.id,
createdAt = historyRecord.createdAt,
date = historyRecord.date,
commandType = historyRecord.commandType,
initialResult = historyRecord.initialResult,
tempBasalRecord = historyRecord.record as? TempBasalRecord,
bolusRecord = historyRecord.record as? BolusRecord,
resolvedResult = historyRecord.resolvedResult,
resolvedAt = historyRecord.resolvedAt,
)
fun entityToDomain(entity: HistoryRecordEntity): HistoryRecord =
HistoryRecord(id = entity.id,
createdAt = entity.createdAt,
date = entity.date,
initialResult = entity.initialResult,
commandType = entity.commandType,
record = entity.bolusRecord ?: entity.tempBasalRecord,
resolvedResult = entity.resolvedResult,
resolvedAt = entity.resolvedAt
)
}