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.controller.tests.utils
17 
18 import android.health.connect.datatypes.BasalMetabolicRateRecord
19 import android.health.connect.datatypes.BodyTemperatureMeasurementLocation
20 import android.health.connect.datatypes.BodyTemperatureRecord
21 import android.health.connect.datatypes.BodyWaterMassRecord
22 import android.health.connect.datatypes.DataOrigin
23 import android.health.connect.datatypes.Device
24 import android.health.connect.datatypes.DistanceRecord
25 import android.health.connect.datatypes.ExerciseCompletionGoal
26 import android.health.connect.datatypes.ExercisePerformanceGoal
27 import android.health.connect.datatypes.ExerciseSegmentType
28 import android.health.connect.datatypes.ExerciseSessionType
29 import android.health.connect.datatypes.HeartRateRecord
30 import android.health.connect.datatypes.HydrationRecord
31 import android.health.connect.datatypes.IntermenstrualBleedingRecord
32 import android.health.connect.datatypes.Metadata
33 import android.health.connect.datatypes.OxygenSaturationRecord
34 import android.health.connect.datatypes.PlannedExerciseBlock
35 import android.health.connect.datatypes.PlannedExerciseSessionRecord
36 import android.health.connect.datatypes.PlannedExerciseStep
37 import android.health.connect.datatypes.Record
38 import android.health.connect.datatypes.SleepSessionRecord
39 import android.health.connect.datatypes.StepsRecord
40 import android.health.connect.datatypes.TotalCaloriesBurnedRecord
41 import android.health.connect.datatypes.WeightRecord
42 import android.health.connect.datatypes.units.Energy
43 import android.health.connect.datatypes.units.Length
44 import android.health.connect.datatypes.units.Mass
45 import android.health.connect.datatypes.units.Percentage
46 import android.health.connect.datatypes.units.Power
47 import android.health.connect.datatypes.units.Temperature
48 import android.health.connect.datatypes.units.Velocity
49 import android.health.connect.datatypes.units.Volume
50 import androidx.test.platform.app.InstrumentationRegistry
51 import androidx.test.uiautomator.UiDevice
52 import com.android.healthconnect.controller.dataentries.units.PowerConverter
53 import com.android.healthconnect.controller.permissions.data.HealthPermission
54 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
55 import com.android.healthconnect.controller.shared.app.AppMetadata
56 import com.android.healthconnect.controller.utils.TimeSource
57 import com.android.healthconnect.controller.utils.randomInstant
58 import com.android.healthconnect.controller.utils.toInstant
59 import com.android.healthconnect.controller.utils.toLocalDateTime
60 import com.google.common.truth.Truth.assertThat
61 import java.time.Instant
62 import java.time.LocalDate
63 import kotlin.random.Random
64 import org.mockito.Mockito
65 import java.time.ZoneOffset
66 
67 val NOW: Instant = Instant.parse("2022-10-20T07:06:05.432Z")
68 val MIDNIGHT: Instant = Instant.parse("2022-10-20T00:00:00.000Z")
69 
70 fun getHeartRateRecord(heartRateValues: List<Long>, startTime: Instant = NOW): HeartRateRecord {
71     return HeartRateRecord.Builder(
72             getMetaData(),
73             startTime,
74             startTime.plusSeconds(2),
75             heartRateValues.map { HeartRateRecord.HeartRateSample(it, NOW) })
76         .build()
77 }
78 
getStepsRecordnull79 fun getStepsRecord(steps: Long, time: Instant = NOW): StepsRecord {
80     return StepsRecord.Builder(getMetaData(), time, time.plusSeconds(2), steps).build()
81 }
82 
getBasalMetabolicRateRecordnull83 fun getBasalMetabolicRateRecord(calories: Long): BasalMetabolicRateRecord {
84     val watts = PowerConverter.convertWattsFromCalories(calories)
85     return BasalMetabolicRateRecord.Builder(getMetaData(), NOW, Power.fromWatts(watts)).build()
86 }
87 
getDistanceRecordnull88 fun getDistanceRecord(distance: Length, time: Instant = NOW): DistanceRecord {
89     return DistanceRecord.Builder(getMetaData(), time, time.plusSeconds(2), distance).build()
90 }
91 
getTotalCaloriesBurnedRecordnull92 fun getTotalCaloriesBurnedRecord(calories: Energy, time: Instant = NOW): TotalCaloriesBurnedRecord {
93     return TotalCaloriesBurnedRecord.Builder(getMetaData(), time, time.plusSeconds(2), calories)
94         .build()
95 }
96 
getSleepSessionRecordnull97 fun getSleepSessionRecord(startTime: Instant = NOW): SleepSessionRecord {
98     val endTime = startTime.toLocalDateTime().plusHours(8).toInstant()
99     return SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build()
100 }
101 
getSleepSessionRecordnull102 fun getSleepSessionRecord(startTime: Instant, endTime: Instant): SleepSessionRecord {
103     return SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build()
104 }
105 
getWeightRecordnull106 fun getWeightRecord(time: Instant = NOW, weight: Mass): WeightRecord {
107     return WeightRecord.Builder(getMetaData(), time, weight).build()
108 }
109 
getIntermenstrualBleedingRecordnull110 fun getIntermenstrualBleedingRecord(time: Instant): IntermenstrualBleedingRecord {
111     return IntermenstrualBleedingRecord.Builder(getMetaData(), time).build()
112 }
113 
getBodyTemperatureRecordnull114 fun getBodyTemperatureRecord(
115     time: Instant,
116     location: Int,
117     temperature: Temperature
118 ): BodyTemperatureRecord {
119     return BodyTemperatureRecord.Builder(getMetaData(), time, location, temperature).build()
120 }
121 
getOxygenSaturationRecordnull122 fun getOxygenSaturationRecord(time: Instant, percentage: Percentage): OxygenSaturationRecord {
123     return OxygenSaturationRecord.Builder(getMetaData(), time, percentage).build()
124 }
125 
getHydrationRecordnull126 fun getHydrationRecord(startTime: Instant, endTime: Instant, volume: Volume): HydrationRecord {
127     return HydrationRecord.Builder(getMetaData(), startTime, endTime, volume).build()
128 }
129 
getBodyWaterMassRecordnull130 fun getBodyWaterMassRecord(time: Instant, bodyWaterMass: Mass): BodyWaterMassRecord {
131     return BodyWaterMassRecord.Builder(getMetaData(), time, bodyWaterMass).build()
132 }
133 
getRandomRecordnull134 fun getRandomRecord(healthPermissionType: HealthPermissionType, date: LocalDate): Record {
135     return when (healthPermissionType) {
136         HealthPermissionType.STEPS -> getStepsRecord(Random.nextLong(0, 5000), date.randomInstant())
137         HealthPermissionType.DISTANCE ->
138             getDistanceRecord(
139                 Length.fromMeters(Random.nextDouble(0.0, 5000.0)), date.randomInstant())
140         HealthPermissionType.TOTAL_CALORIES_BURNED ->
141             getTotalCaloriesBurnedRecord(
142                 Energy.fromCalories(Random.nextDouble(1500.0, 5000.0)), date.randomInstant())
143         HealthPermissionType.SLEEP -> getSleepSessionRecord(date.randomInstant())
144         else ->
145             throw IllegalArgumentException(
146                 "HealthPermissionType $healthPermissionType not supported")
147     }
148 }
149 
getSamplePlannedExerciseSessionRecordnull150 fun getSamplePlannedExerciseSessionRecord(): PlannedExerciseSessionRecord {
151     val exerciseBlock1 =
152         getPlannedExerciseBlock(
153             repetitions = 1,
154             description = "Warm up",
155             exerciseSteps =
156                 listOf(
157                     getPlannedExerciseStep(
158                         exerciseSegmentType = ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_RUNNING,
159                         completionGoal =
160                             ExerciseCompletionGoal.DistanceGoal(Length.fromMeters(1000.0)),
161                         performanceGoals =
162                             listOf(
163                                 ExercisePerformanceGoal.HeartRateGoal(100, 150),
164                                 ExercisePerformanceGoal.SpeedGoal(
165                                     Velocity.fromMetersPerSecond(25.0),
166                                     Velocity.fromMetersPerSecond(15.0))))))
167     val exerciseBlock2 =
168         getPlannedExerciseBlock(
169             repetitions = 1,
170             description = "Main set",
171             exerciseSteps =
172                 listOf(
173                     getPlannedExerciseStep(
174                         exerciseSegmentType = ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_RUNNING,
175                         completionGoal =
176                             ExerciseCompletionGoal.DistanceGoal(Length.fromMeters(4000.0)),
177                         performanceGoals =
178                             listOf(
179                                 ExercisePerformanceGoal.HeartRateGoal(150, 180),
180                                 ExercisePerformanceGoal.SpeedGoal(
181                                     Velocity.fromMetersPerSecond(50.0),
182                                     Velocity.fromMetersPerSecond(25.0))))))
183     val exerciseBlocks = listOf(exerciseBlock1, exerciseBlock2)
184 
185     return getPlannedExerciseSessionRecord(
186         title = "Morning Run",
187         note = "Morning quick run by the park",
188         exerciseBlocks = exerciseBlocks)
189 }
190 
getPlannedExerciseSessionRecordnull191 fun getPlannedExerciseSessionRecord(
192     title: String,
193     note: String,
194     exerciseBlocks: List<PlannedExerciseBlock>
195 ): PlannedExerciseSessionRecord {
196     return basePlannedExerciseSession(ExerciseSessionType.EXERCISE_SESSION_TYPE_RUNNING)
197         .setTitle(title)
198         .setNotes(note)
199         .setBlocks(exerciseBlocks)
200         .build()
201 }
202 
basePlannedExerciseSessionnull203 private fun basePlannedExerciseSession(exerciseType: Int): PlannedExerciseSessionRecord.Builder {
204     val builder: PlannedExerciseSessionRecord.Builder =
205         PlannedExerciseSessionRecord.Builder(
206             getMetaData(), exerciseType, NOW, NOW.plusSeconds(3600))
207     builder.setNotes("Sample training plan notes")
208     builder.setTitle("Training plan title")
209     builder.setStartZoneOffset(ZoneOffset.UTC)
210     builder.setEndZoneOffset(ZoneOffset.UTC)
211     return builder
212 }
213 
getPlannedExerciseBlocknull214 fun getPlannedExerciseBlock(
215     repetitions: Int,
216     description: String,
217     exerciseSteps: List<PlannedExerciseStep>
218 ): PlannedExerciseBlock {
219     return PlannedExerciseBlock.Builder(repetitions)
220         .setDescription(description)
221         .setSteps(exerciseSteps)
222         .build()
223 }
224 
getPlannedExerciseStepnull225 fun getPlannedExerciseStep(
226     exerciseSegmentType: Int,
227     completionGoal: ExerciseCompletionGoal,
228     performanceGoals: List<ExercisePerformanceGoal>
229 ): PlannedExerciseStep {
230     return PlannedExerciseStep.Builder(
231             exerciseSegmentType, PlannedExerciseStep.EXERCISE_CATEGORY_ACTIVE, completionGoal)
232         .setPerformanceGoals(performanceGoals)
233         .build()
234 }
235 
getMetaDatanull236 fun getMetaData(): Metadata {
237     return getMetaData(TEST_APP_PACKAGE_NAME)
238 }
239 
getMetaDatanull240 fun getMetaData(packageName: String): Metadata {
241     val device: Device =
242         Device.Builder().setManufacturer("google").setModel("Pixel4a").setType(2).build()
243     val dataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
244     return Metadata.Builder()
245         .setId("test_id")
246         .setDevice(device)
247         .setDataOrigin(dataOrigin)
248         .setClientRecordId("BMR" + Math.random().toString())
249         .build()
250 }
251 
getDataOriginnull252 fun getDataOrigin(packageName: String): DataOrigin =
253     DataOrigin.Builder().setPackageName(packageName).build()
254 
255 fun getSleepSessionRecords(inputDates: List<Pair<Instant, Instant>>): List<SleepSessionRecord> {
256     val result = arrayListOf<SleepSessionRecord>()
257     inputDates.forEach { (startTime, endTime) ->
258         result.add(SleepSessionRecord.Builder(getMetaData(), startTime, endTime).build())
259     }
260 
261     return result
262 }
263 
verifySleepSessionListsEqualnull264 fun verifySleepSessionListsEqual(actual: List<Record>, expected: List<SleepSessionRecord>) {
265     assertThat(actual.size).isEqualTo(expected.size)
266     for ((index, element) in actual.withIndex()) {
267         assertThat(element is SleepSessionRecord).isTrue()
268         val expectedElement = expected[index]
269         val actualElement = element as SleepSessionRecord
270 
271         assertThat(actualElement.startTime).isEqualTo(expectedElement.startTime)
272         assertThat(actualElement.endTime).isEqualTo(expectedElement.endTime)
273         assertThat(actualElement.notes).isEqualTo(expectedElement.notes)
274         assertThat(actualElement.title).isEqualTo(expectedElement.title)
275         assertThat(actualElement.stages).isEqualTo(expectedElement.stages)
276     }
277 }
278 
verifyOxygenSaturationListsEqualnull279 fun verifyOxygenSaturationListsEqual(actual: List<Record>, expected: List<OxygenSaturationRecord>) {
280     assertThat(actual.size).isEqualTo(expected.size)
281     for ((index, element) in actual.withIndex()) {
282         assertThat(element is OxygenSaturationRecord).isTrue()
283         val expectedElement = expected[index]
284         val actualElement = element as OxygenSaturationRecord
285 
286         assertThat(actualElement.time).isEqualTo(expectedElement.time)
287         assertThat(actualElement.percentage).isEqualTo(expectedElement.percentage)
288     }
289 }
290 
verifyHydrationListsEqualnull291 fun verifyHydrationListsEqual(actual: List<Record>, expected: List<HydrationRecord>) {
292     assertThat(actual.size).isEqualTo(expected.size)
293     for ((index, element) in actual.withIndex()) {
294         assertThat(element is HydrationRecord).isTrue()
295         val expectedElement = expected[index]
296         val actualElement = element as HydrationRecord
297 
298         assertThat(actualElement.startTime).isEqualTo(expectedElement.startTime)
299         assertThat(actualElement.endTime).isEqualTo(expectedElement.endTime)
300         assertThat(actualElement.volume).isEqualTo(expectedElement.volume)
301     }
302 }
303 
verifyBodyWaterMassListsEqualnull304 fun verifyBodyWaterMassListsEqual(actual: List<Record>, expected: List<Record>) {
305     assertThat(actual.size).isEqualTo(expected.size)
306     for ((index, element) in actual.withIndex()) {
307         assertThat(element is BodyWaterMassRecord).isTrue()
308         val expectedElement = expected[index] as BodyWaterMassRecord
309         val actualElement = element as BodyWaterMassRecord
310 
311         assertThat(actualElement.time).isEqualTo(expectedElement.time)
312         assertThat(actualElement.bodyWaterMass).isEqualTo(expectedElement.bodyWaterMass)
313     }
314 }
315 
316 // test data constants - start
317 
318 val START_TIME = Instant.parse("2023-06-12T22:30:00Z")
319 
320 // pre-defined Instants within a day, week, and month of the START_TIME Instant
321 val INSTANT_DAY: Instant = Instant.parse("2023-06-11T23:30:00Z")
322 val INSTANT_DAY2: Instant = Instant.parse("2023-06-12T02:00:00Z")
323 val INSTANT_WEEK: Instant = Instant.parse("2023-06-14T11:15:00Z")
324 val INSTANT_MONTH1: Instant = Instant.parse("2023-06-26T23:10:00Z")
325 val INSTANT_MONTH2: Instant = Instant.parse("2023-06-30T11:30:00Z")
326 val INSTANT_MONTH3: Instant = Instant.parse("2023-07-01T07:45:00Z")
327 val INSTANT_MONTH4: Instant = Instant.parse("2023-07-01T19:15:00Z")
328 val INSTANT_MONTH5: Instant = Instant.parse("2023-07-05T03:45:00Z")
329 val INSTANT_MONTH6: Instant = Instant.parse("2023-07-07T07:05:00Z")
330 
331 val SLEEP_DAY_0H20 =
332     getSleepSessionRecord(
333         Instant.parse("2023-06-12T21:00:00Z"), Instant.parse("2023-06-12T21:20:00Z"))
334 val SLEEP_DAY_1H45 =
335     getSleepSessionRecord(
336         Instant.parse("2023-06-12T16:00:00Z"), Instant.parse("2023-06-12T17:45:00Z"))
337 val SLEEP_DAY_9H15 =
338     getSleepSessionRecord(
339         Instant.parse("2023-06-12T22:30:00Z"), Instant.parse("2023-06-13T07:45:00Z"))
340 val SLEEP_WEEK_9H15 =
341     getSleepSessionRecord(
342         Instant.parse("2023-06-14T22:30:00Z"), Instant.parse("2023-06-15T07:45:00Z"))
343 val SLEEP_WEEK_33H15 =
344     getSleepSessionRecord(
345         Instant.parse("2023-06-11T22:30:00Z"), Instant.parse("2023-06-13T07:45:00Z"))
346 val SLEEP_MONTH_81H15 =
347     getSleepSessionRecord(
348         Instant.parse("2023-07-09T22:30:00Z"), Instant.parse("2023-07-13T07:45:00Z"))
349 
350 val HYDRATION_MONTH: HydrationRecord =
351     getHydrationRecord(INSTANT_MONTH1, INSTANT_MONTH2, Volume.fromLiters(2.0))
352 val HYDRATION_MONTH2: HydrationRecord =
353     getHydrationRecord(INSTANT_MONTH3, INSTANT_MONTH4, Volume.fromLiters(0.3))
354 val HYDRATION_MONTH3: HydrationRecord =
355     getHydrationRecord(INSTANT_MONTH5, INSTANT_MONTH6, Volume.fromLiters(1.5))
356 
357 val OXYGENSATURATION_DAY: OxygenSaturationRecord =
358     getOxygenSaturationRecord(INSTANT_DAY, Percentage.fromValue(98.0))
359 val OXYGENSATURATION_DAY2: OxygenSaturationRecord =
360     getOxygenSaturationRecord(INSTANT_DAY2, Percentage.fromValue(95.0))
361 
362 val DISTANCE_STARTDATE_1500: DistanceRecord =
363     getDistanceRecord(Length.fromMeters(1500.0), START_TIME)
364 
365 val WEIGHT_DAY_100: WeightRecord = getWeightRecord(INSTANT_DAY, Mass.fromGrams(100000.0))
366 val WEIGHT_WEEK_100: WeightRecord = getWeightRecord(INSTANT_WEEK, Mass.fromGrams(100000.0))
367 val WEIGHT_MONTH_100: WeightRecord = getWeightRecord(INSTANT_MONTH3, Mass.fromGrams(100000.0))
368 val WEIGHT_STARTDATE_100: WeightRecord = getWeightRecord(START_TIME, Mass.fromGrams(100000.0))
369 
370 val INTERMENSTRUAL_BLEEDING_DAY: IntermenstrualBleedingRecord =
371     getIntermenstrualBleedingRecord(INSTANT_DAY)
372 
373 val BODYTEMPERATURE_MONTH: BodyTemperatureRecord =
374     getBodyTemperatureRecord(
375         INSTANT_MONTH3,
376         BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_MOUTH,
377         Temperature.fromCelsius(100.0))
378 
379 val BODYWATERMASS_WEEK: BodyWaterMassRecord =
380     getBodyWaterMassRecord(INSTANT_WEEK, Mass.fromGrams(1000.0))
381 
382 // records using today's date, yesterday's date, and the date two days ago - for header testing
getMixedRecordsAcrossTwoDaysnull383 fun getMixedRecordsAcrossTwoDays(timeSource: TimeSource): List<Record> {
384     val instantToday: Instant = timeSource.currentLocalDateTime().toInstant()
385     val instantYesterday: Instant = timeSource.currentLocalDateTime().minusDays(1).toInstant()
386     return listOf(
387         getHydrationRecord(instantToday, instantToday.plusSeconds(900), Volume.fromLiters(2.0)),
388         getSleepSessionRecord(instantToday, instantToday.plusSeconds(1800)),
389         getDistanceRecord(Length.fromMeters(2500.0), instantYesterday),
390         getOxygenSaturationRecord(instantYesterday, Percentage.fromValue(99.0)))
391 }
392 
getMixedRecordsAcrossThreeDaysnull393 fun getMixedRecordsAcrossThreeDays(timeSource: TimeSource): List<Record> {
394     val instantTwoDaysAgo: Instant = timeSource.currentLocalDateTime().minusDays(2).toInstant()
395     return getMixedRecordsAcrossTwoDays(timeSource)
396         .plus(
397             listOf(
398                 getWeightRecord(instantTwoDaysAgo, Mass.fromGrams(95000.0)),
399                 getDistanceRecord(Length.fromMeters(2000.0), instantTwoDaysAgo)))
400 }
401 
402 // test data constants - end
403 
404 // Enables or disables animations in a test
toggleAnimationnull405 fun toggleAnimation(isEnabled: Boolean) {
406     with(UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())) {
407         executeShellCommand(
408             "settings put global transition_animation_scale ${if (isEnabled) 1 else 0}")
409         executeShellCommand("settings put global window_animation_scale ${if (isEnabled) 1 else 0}")
410         executeShellCommand(
411             "settings put global animator_duration_scale ${if (isEnabled) 1 else 0}")
412     }
413 }
414 
415 // Used for matching arguments for [RequestPermissionViewModel]
anynull416 fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
417 
418 /** Utility function to turn an array of permission strings to a list of [HealthPermission]s */
419 fun Array<String>.toPermissionsList(): List<HealthPermission> {
420     return this.map { HealthPermission.fromPermissionString(it) }.toList()
421 }
422 
423 // region apps
424 
425 const val TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app"
426 const val TEST_APP_PACKAGE_NAME_2 = "android.healthconnect.controller.test.app2"
427 const val TEST_APP_PACKAGE_NAME_3 = "package.name.3"
428 const val UNSUPPORTED_TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app3"
429 const val OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME = "android.healthconnect.controller.test.app4"
430 const val TEST_APP_NAME = "Health Connect test app"
431 const val TEST_APP_NAME_2 = "Health Connect test app 2"
432 const val TEST_APP_NAME_3 = "Health Connect test app 3"
433 const val OLD_APP_NAME = "Old permissions test app"
434 
435 val TEST_APP =
436     AppMetadata(packageName = TEST_APP_PACKAGE_NAME, appName = TEST_APP_NAME, icon = null)
437 val TEST_APP_2 =
438     AppMetadata(packageName = TEST_APP_PACKAGE_NAME_2, appName = TEST_APP_NAME_2, icon = null)
439 val TEST_APP_3 =
440     AppMetadata(packageName = TEST_APP_PACKAGE_NAME_3, appName = TEST_APP_NAME_3, icon = null)
441 val OLD_TEST_APP =
442     AppMetadata(
443         packageName = OLD_PERMISSIONS_TEST_APP_PACKAGE_NAME, appName = OLD_APP_NAME, icon = null)
444 // endregion
445