Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fba18bc7e | ||
|
|
fda104041a |
@@ -0,0 +1,20 @@
|
||||
package com.placeholder.ringhealth
|
||||
|
||||
import com.placeholder.ringhealth.data.mock.MockHealthDataRepository
|
||||
import com.placeholder.ringhealth.data.mock.MockRingDeviceRepository
|
||||
import com.placeholder.ringhealth.domain.repository.HealthDataRepository
|
||||
import com.placeholder.ringhealth.domain.repository.RingDeviceRepository
|
||||
|
||||
/**
|
||||
* 手动依赖注入容器。M2 接入 SDK 时只需在这里把 Mock 实现换成真实实现。
|
||||
*/
|
||||
interface AppContainer {
|
||||
val healthDataRepository: HealthDataRepository
|
||||
val ringDeviceRepository: RingDeviceRepository
|
||||
}
|
||||
|
||||
/** 默认容器:阶段一全部使用 Mock 数据。 */
|
||||
class DefaultAppContainer : AppContainer {
|
||||
override val healthDataRepository: HealthDataRepository = MockHealthDataRepository()
|
||||
override val ringDeviceRepository: RingDeviceRepository = MockRingDeviceRepository()
|
||||
}
|
||||
@@ -3,6 +3,16 @@ package com.placeholder.ringhealth
|
||||
import android.app.Application
|
||||
|
||||
/**
|
||||
* 应用入口。同时作为手动依赖注入的组合根(M1 阶段只持有 Mock 容器)。
|
||||
* 应用入口,同时作为手动依赖注入的组合根。
|
||||
*/
|
||||
class RingHealthApplication : Application()
|
||||
class RingHealthApplication : Application() {
|
||||
|
||||
/** 全局依赖容器。 */
|
||||
lateinit var container: AppContainer
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
container = DefaultAppContainer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.placeholder.ringhealth.data.mock
|
||||
|
||||
import com.placeholder.ringhealth.domain.model.DateRange
|
||||
import com.placeholder.ringhealth.domain.model.DeviceFunctionSetting
|
||||
import com.placeholder.ringhealth.domain.model.HealthSummary
|
||||
import com.placeholder.ringhealth.domain.model.MetricPoint
|
||||
import com.placeholder.ringhealth.domain.model.MetricType
|
||||
import com.placeholder.ringhealth.domain.model.RingDeviceState
|
||||
import com.placeholder.ringhealth.domain.model.WorkoutRecord
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* 演示数据生成器。所有方法都是确定性的(同参数、同时刻返回同结果),
|
||||
* 保证页面展示与截图稳定。M2 接入真机后本类整体弃用。
|
||||
*
|
||||
* @param seed 随机种子,决定历史数据的取值序列。
|
||||
* @param now 时间源,默认取系统时钟;测试注入固定值后可得到完全可复现的时间戳
|
||||
* (ledger Ruling R17)。
|
||||
*/
|
||||
class MockDataProvider(
|
||||
private val seed: Long = DEFAULT_SEED,
|
||||
private val now: () -> Long = System::currentTimeMillis,
|
||||
) {
|
||||
|
||||
/** 今日概览:固定演示值。血压为 null 以演示“未开启”占位态。 */
|
||||
fun todaySummary(): HealthSummary = HealthSummary(
|
||||
steps = 8426,
|
||||
stepGoal = 10000,
|
||||
calories = 327,
|
||||
distanceKm = 6.21,
|
||||
heartRate = 72,
|
||||
sleepMinutes = 452,
|
||||
bloodOxygen = 98,
|
||||
stress = 34,
|
||||
temperature = 36.5,
|
||||
bloodPressure = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* 指定指标的历史数据:WEEK 返回 7 个点,MONTH 返回 30 个点。
|
||||
* 时间戳按“每点间隔一天”倒推,值在指标合理区间内波动。
|
||||
*/
|
||||
fun metricHistory(metric: MetricType, range: DateRange): List<MetricPoint> {
|
||||
val count = if (range == DateRange.WEEK) 7 else 30
|
||||
val random = Random(seed + metric.ordinal * 100L + range.ordinal)
|
||||
val (min, max) = valueRange(metric)
|
||||
val current = now()
|
||||
val dayMillis = 24L * 60 * 60 * 1000
|
||||
return (0 until count).map { index ->
|
||||
val progress = index.toFloat() / (count - 1).coerceAtLeast(1)
|
||||
val value = min + (max - min) * progress + random.nextDouble(-1.0, 1.0) * (max - min) * 0.15
|
||||
MetricPoint(
|
||||
timestamp = current - (count - 1 - index) * dayMillis,
|
||||
value = value.coerceIn(min, max),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 运动记录:固定 5 条演示数据,倒序(最近的在最前)。 */
|
||||
fun workouts(): List<WorkoutRecord> {
|
||||
val random = Random(seed + 7)
|
||||
val current = now()
|
||||
val dayMillis = 24L * 60 * 60 * 1000
|
||||
val types = listOf("户外跑步", "健走", "骑行", "自由训练", "户外跑步")
|
||||
return types.mapIndexed { index, type ->
|
||||
WorkoutRecord(
|
||||
id = "workout-$index",
|
||||
type = type,
|
||||
startTime = current - (index + 1) * dayMillis,
|
||||
durationMinutes = 25 + random.nextInt(0, 45),
|
||||
distanceKm = 2.0 + random.nextDouble(0.0, 5.0),
|
||||
calories = 120 + random.nextInt(0, 260),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 戒指设备状态:固定演示值。 */
|
||||
fun deviceState(): RingDeviceState = RingDeviceState(
|
||||
connected = true,
|
||||
name = "RingHealth 智能戒指",
|
||||
batteryPercent = 78,
|
||||
firmwareVersion = "1.0.5",
|
||||
charging = false,
|
||||
)
|
||||
|
||||
/** 五个功能开关,对应协议中的自动监测能力。 */
|
||||
fun defaultFunctionSettings(): List<DeviceFunctionSetting> = listOf(
|
||||
DeviceFunctionSetting("heart_rate", "自动心率监测", "每 10 分钟测量一次", true),
|
||||
DeviceFunctionSetting("blood_oxygen", "自动血氧监测", "夜间持续监测", true),
|
||||
DeviceFunctionSetting("sleep", "睡眠监测", "自动识别睡眠时段", true),
|
||||
DeviceFunctionSetting("stress", "压力监测", "全天候压力评估", false),
|
||||
DeviceFunctionSetting("blood_pressure", "血压监测", "需在 App 内手动校准", false),
|
||||
)
|
||||
|
||||
/** 各指标的取值区间,用于生成合理的历史数据。 */
|
||||
private fun valueRange(metric: MetricType): Pair<Double, Double> = when (metric) {
|
||||
MetricType.STEP -> 3000.0 to 12000.0
|
||||
MetricType.HEART_RATE -> 58.0 to 96.0
|
||||
MetricType.SLEEP -> 330.0 to 510.0
|
||||
MetricType.BLOOD_OXYGEN -> 95.0 to 99.0
|
||||
MetricType.STRESS -> 20.0 to 70.0
|
||||
MetricType.TEMPERATURE -> 36.1 to 36.9
|
||||
MetricType.BLOOD_PRESSURE -> 110.0 to 130.0
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** 默认随机种子,保证演示数据稳定。 */
|
||||
const val DEFAULT_SEED: Long = 20260910L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.placeholder.ringhealth.data.mock
|
||||
|
||||
import com.placeholder.ringhealth.domain.model.DateRange
|
||||
import com.placeholder.ringhealth.domain.model.HealthSummary
|
||||
import com.placeholder.ringhealth.domain.model.MetricPoint
|
||||
import com.placeholder.ringhealth.domain.model.MetricType
|
||||
import com.placeholder.ringhealth.domain.model.WorkoutRecord
|
||||
import com.placeholder.ringhealth.domain.repository.HealthDataRepository
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/**
|
||||
* Mock 健康数据仓库。模拟 200ms 网络/设备延迟,便于阶段一验证加载态。
|
||||
*/
|
||||
class MockHealthDataRepository(
|
||||
private val provider: MockDataProvider = MockDataProvider(),
|
||||
) : HealthDataRepository {
|
||||
|
||||
override fun observeTodaySummary(): Flow<HealthSummary> = flow {
|
||||
delay(MOCK_DELAY_MILLIS)
|
||||
emit(provider.todaySummary())
|
||||
}
|
||||
|
||||
override fun observeMetricHistory(metric: MetricType, range: DateRange): Flow<List<MetricPoint>> = flow {
|
||||
delay(MOCK_DELAY_MILLIS)
|
||||
emit(provider.metricHistory(metric, range))
|
||||
}
|
||||
|
||||
override fun observeWorkouts(): Flow<List<WorkoutRecord>> = flow {
|
||||
delay(MOCK_DELAY_MILLIS)
|
||||
emit(provider.workouts())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MOCK_DELAY_MILLIS = 200L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.placeholder.ringhealth.data.mock
|
||||
|
||||
import com.placeholder.ringhealth.domain.model.DeviceFunctionSetting
|
||||
import com.placeholder.ringhealth.domain.model.RingDeviceState
|
||||
import com.placeholder.ringhealth.domain.repository.RingDeviceRepository
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Mock 设备仓库。开关状态保存在内存中,进程重启后重置(阶段一不做持久化)。
|
||||
*
|
||||
* 注意 [refreshDeviceState] 的语义:它以 **suspend 函数正常返回**表示刷新完成,
|
||||
* 而**不是**靠发射新状态。`MutableStateFlow` 会抑制相等值,M1 的设备状态是常量,
|
||||
* 因此刷新不会产生任何可观察的发射。调用方要展示刷新反馈,应当
|
||||
* `launch { repo.refreshDeviceState(); isRefreshing = false }`,
|
||||
* **不要**写成「等待下一个不同的 deviceState 发射」(那样在 M1 会永远等不到)。
|
||||
*/
|
||||
class MockRingDeviceRepository(
|
||||
private val provider: MockDataProvider = MockDataProvider(),
|
||||
) : RingDeviceRepository {
|
||||
|
||||
private val _deviceState = MutableStateFlow(provider.deviceState())
|
||||
override val deviceState: StateFlow<RingDeviceState> = _deviceState.asStateFlow()
|
||||
|
||||
private val _functionSettings = MutableStateFlow(provider.defaultFunctionSettings())
|
||||
override val functionSettings: StateFlow<List<DeviceFunctionSetting>> = _functionSettings.asStateFlow()
|
||||
|
||||
override suspend fun refreshDeviceState() {
|
||||
delay(REFRESH_DELAY_MILLIS)
|
||||
_deviceState.value = provider.deviceState()
|
||||
}
|
||||
|
||||
override fun setFunctionEnabled(id: String, enabled: Boolean) {
|
||||
_functionSettings.update { settings ->
|
||||
settings.map { if (it.id == id) it.copy(enabled = enabled) else it }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val REFRESH_DELAY_MILLIS = 300L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.placeholder.ringhealth.data.mock
|
||||
|
||||
import com.placeholder.ringhealth.domain.model.DateRange
|
||||
import com.placeholder.ringhealth.domain.model.MetricType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class MockDataProviderTest {
|
||||
|
||||
/**
|
||||
* 固定时钟。Mock 的「确定性」承诺依赖时间源固定,否则 `timestamp` 会随毫秒跳动
|
||||
* (ledger Ruling R17)。
|
||||
*/
|
||||
private val provider = MockDataProvider(now = { FIXED_NOW })
|
||||
|
||||
private companion object {
|
||||
/** 固定的“当前时间”,让历史数据的时间戳可复现。 */
|
||||
const val FIXED_NOW = 1_760_000_000_000L
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `今日概览返回固定演示数据`() {
|
||||
val summary = provider.todaySummary()
|
||||
assertEquals(8426, summary.steps)
|
||||
assertEquals(10000, summary.stepGoal)
|
||||
assertEquals(327, summary.calories)
|
||||
assertEquals(72, summary.heartRate)
|
||||
assertEquals(452, summary.sleepMinutes)
|
||||
// 血压未开启,应为 null
|
||||
assertEquals(null, summary.bloodPressure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `历史数据长度与范围一致`() {
|
||||
assertEquals(7, provider.metricHistory(MetricType.STEP, DateRange.WEEK).size)
|
||||
assertEquals(30, provider.metricHistory(MetricType.STEP, DateRange.MONTH).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `历史数据可重复生成且值合理`() {
|
||||
val first = provider.metricHistory(MetricType.HEART_RATE, DateRange.WEEK)
|
||||
val second = provider.metricHistory(MetricType.HEART_RATE, DateRange.WEEK)
|
||||
assertEquals(first, second)
|
||||
assertTrue(first.all { it.value > 40.0 && it.value < 180.0 })
|
||||
// 时间戳须严格递增(“每点间隔一天”倒推,最早的在最前)
|
||||
assertTrue(first.zipWithNext().all { (a, b) -> b.timestamp > a.timestamp })
|
||||
// 断言绝对时间戳,让注入的时钟真正成为被测对象:若退回 System.currentTimeMillis(),
|
||||
// 这两条会立刻失败(ledger R17 / R20)。只比较 first == second 是不够的 ——
|
||||
// 两次相邻调用几乎总落在同一毫秒,那时去掉时钟注入测试照样通过。
|
||||
assertEquals(FIXED_NOW, first.last().timestamp)
|
||||
assertEquals(FIXED_NOW - 6L * 24 * 60 * 60 * 1000, first.first().timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `设备状态与开关数据合法`() {
|
||||
val device = provider.deviceState()
|
||||
assertTrue(device.batteryPercent in 0..100)
|
||||
assertEquals(true, device.connected)
|
||||
assertEquals(5, provider.defaultFunctionSettings().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `运动记录非空且字段合理`() {
|
||||
val records = provider.workouts()
|
||||
assertTrue(records.isNotEmpty())
|
||||
assertTrue(records.all { it.durationMinutes > 0 && it.calories > 0 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user