1 /**
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * ```
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * ```
10  *
11  * Unless required by applicable law or agreed to in writing, software distributed under the License
12  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.android.healthconnect.testapps.toolbox.seed
17 
18 import android.content.Context
19 import android.health.connect.HealthConnectManager
20 import android.health.connect.datatypes.DataOrigin
21 import android.health.connect.datatypes.Device
22 import android.health.connect.datatypes.HeartRateRecord
23 import android.health.connect.datatypes.MenstruationFlowRecord
24 import android.health.connect.datatypes.MenstruationFlowRecord.MenstruationFlowType
25 import android.health.connect.datatypes.MenstruationPeriodRecord
26 import android.health.connect.datatypes.Metadata
27 import android.health.connect.datatypes.SkinTemperatureRecord
28 import android.health.connect.datatypes.StepsRecord
29 import android.health.connect.datatypes.units.Temperature
30 import android.health.connect.datatypes.units.TemperatureDelta
31 import android.os.Build.MANUFACTURER
32 import android.os.Build.MODEL
33 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.insertRecords
34 import java.time.Duration.ofDays
35 import java.time.Duration.ofMinutes
36 import java.time.Instant
37 import java.time.temporal.ChronoUnit
38 import java.util.Random
39 import kotlin.random.Random as ktRandom
40 import kotlinx.coroutines.runBlocking
41 
42 class SeedData(private val context: Context, private val manager: HealthConnectManager) {
43 
44     companion object {
45         const val NUMBER_OF_SERIES_RECORDS_TO_INSERT = 200L
46         val VALID_READING_LOCATIONS =
47             setOf(
48                 SkinTemperatureRecord.MEASUREMENT_LOCATION_FINGER,
49                 SkinTemperatureRecord.MEASUREMENT_LOCATION_TOE,
50                 SkinTemperatureRecord.MEASUREMENT_LOCATION_WRIST,
51                 SkinTemperatureRecord.MEASUREMENT_LOCATION_UNKNOWN)
52     }
53 
54     fun seedData() {
55         runBlocking {
56             try {
57                 seedMenstruationData()
58                 seedStepsData()
59                 seedHeartRateData(10)
60                 seedSkinTemperatureData(10)
61             } catch (ex: Exception) {
62                 throw ex
63             }
64         }
65     }
66 
67     private suspend fun seedStepsData() {
68         val start = Instant.now().truncatedTo(ChronoUnit.DAYS)
69         val records = (1L..50).map { count -> getStepsRecord(count, start.plus(ofMinutes(count))) }
70 
71         insertRecords(records, manager)
72     }
73 
74     private suspend fun seedMenstruationData() {
75         val today = Instant.now()
76         val periodRecord =
77             MenstruationPeriodRecord.Builder(getMetaData(), today.minus(ofDays(5L)), today).build()
78         val flowRecords =
79             (-5..0).map { days ->
80                 MenstruationFlowRecord.Builder(
81                         getMetaData(),
82                         today.plus(ofDays(days.toLong())),
83                         MenstruationFlowType.FLOW_MEDIUM)
84                     .build()
85             }
86         insertRecords(
87             buildList {
88                 add(periodRecord)
89                 addAll(flowRecords)
90             },
91             manager)
92     }
93 
94     suspend fun seedHeartRateData(numberOfRecordsPerBatch: Long) {
95         val start = Instant.now().truncatedTo(ChronoUnit.DAYS)
96         val random = Random()
97         val records =
98             (1L..numberOfRecordsPerBatch).map { timeOffset ->
99                 val hrSamples = ArrayList<Pair<Long, Instant>>()
100                 repeat(10) { i ->
101                     hrSamples.add(
102                         Pair(getValidHeartRate(random), start.plus(ofMinutes(timeOffset + i))))
103                 }
104                 getHeartRateRecord(
105                     hrSamples,
106                     start.plus(ofMinutes(timeOffset)),
107                     start.plus(ofMinutes(timeOffset + 100)))
108             }
109         insertRecords(records, manager)
110     }
111 
112     private suspend fun seedSkinTemperatureData(numberOfRecordsPerBatch: Long) {
113         val start = Instant.now().truncatedTo(ChronoUnit.DAYS)
114         val records =
115             (1L..numberOfRecordsPerBatch).map { timeOffset ->
116                 val skinTempSamples = ArrayList<SkinTemperatureRecord.Delta>()
117                 repeat(10) { i ->
118                     skinTempSamples.add(
119                         SkinTemperatureRecord.Delta(
120                             getValidTemperatureDelta(), start.plus(ofMinutes(timeOffset + i))))
121                 }
122                 getSkinTemperatureRecord(
123                     skinTempSamples,
124                     start.plus(ofMinutes(timeOffset)),
125                     start.plus(ofMinutes(timeOffset + 100)))
126             }
127         insertRecords(records, manager)
128     }
129 
130     private fun getSkinTemperatureRecord(
131         deltasList: List<SkinTemperatureRecord.Delta>,
132         startTime: Instant,
133         endTime: Instant
134     ): SkinTemperatureRecord {
135         return SkinTemperatureRecord.Builder(getMetaData(), startTime, endTime)
136             .setDeltas(deltasList)
137             .setBaseline(Temperature.fromCelsius(25.0))
138             .setMeasurementLocation(VALID_READING_LOCATIONS.random())
139             .build()
140     }
141 
142     private fun getValidTemperatureDelta(): TemperatureDelta {
143         return TemperatureDelta.fromCelsius((ktRandom.nextInt(-30, 30)).toDouble() / 10)
144     }
145 
146     private fun getHeartRateRecord(
147         heartRateValues: List<Pair<Long, Instant>>,
148         start: Instant,
149         end: Instant,
150     ): HeartRateRecord {
151         return HeartRateRecord.Builder(
152                 getMetaData(),
153                 start,
154                 end,
155                 heartRateValues.map { HeartRateRecord.HeartRateSample(it.first, it.second) })
156             .build()
157     }
158 
159     private fun getValidHeartRate(random: Random): Long {
160         return (random.nextInt(20) + 80).toLong()
161     }
162 
163     private fun getStepsRecord(count: Long, time: Instant): StepsRecord {
164         return StepsRecord.Builder(getMetaData(), time, time.plusSeconds(30), count).build()
165     }
166 
167     private fun getMetaData(): Metadata {
168         val device: Device =
169             Device.Builder().setManufacturer(MANUFACTURER).setModel(MODEL).setType(1).build()
170         val dataOrigin = DataOrigin.Builder().setPackageName(context.packageName).build()
171         return Metadata.Builder().setDevice(device).setDataOrigin(dataOrigin).build()
172     }
173 }
174