Merge pull request #12 from 0pen-dash/adrian/history-persistance
history records / database
This commit is contained in:
commit
42279e1339
15 changed files with 415 additions and 4 deletions
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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?
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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?)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
Loading…
Reference in a new issue