• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.data.appdata
17 
18 import android.health.connect.HealthConnectManager
19 import android.health.connect.MedicalResourceTypeInfo
20 import android.health.connect.RecordTypeInfoResponse
21 import android.health.connect.datatypes.Record
22 import android.util.Log
23 import androidx.core.os.asOutcomeReceiver
24 import com.android.healthconnect.controller.permissions.data.FitnessPermissionType
25 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
26 import com.android.healthconnect.controller.permissions.data.MedicalPermissionType
27 import com.android.healthconnect.controller.permissions.data.fromHealthPermissionCategory
28 import com.android.healthconnect.controller.permissions.data.fromMedicalResourceType
29 import com.android.healthconnect.controller.service.IoDispatcher
30 import com.android.healthconnect.controller.shared.FITNESS_DATA_CATEGORIES
31 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.MEDICAL
32 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes
33 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
34 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
35 import javax.inject.Inject
36 import javax.inject.Singleton
37 import kotlinx.coroutines.CoroutineDispatcher
38 import kotlinx.coroutines.suspendCancellableCoroutine
39 import kotlinx.coroutines.withContext
40 
41 @Singleton
42 class AllDataUseCase
43 @Inject
44 constructor(
45     private val healthConnectManager: HealthConnectManager,
46     @IoDispatcher private val dispatcher: CoroutineDispatcher,
47 ) {
48 
49     /** Returns list of all fitness categories and permission types to be shown on the HC UI. */
50     suspend fun loadAllFitnessData(): UseCaseResults<List<PermissionTypesPerCategory>> =
51         withContext(dispatcher) {
52             try {
53                 val recordTypeInfoMap = getRecordTypeInfoMap()
54                 val categories =
55                     FITNESS_DATA_CATEGORIES.map {
56                         PermissionTypesPerCategory(
57                             it,
58                             getPermissionTypesPerCategory(it, recordTypeInfoMap, packageName = null),
59                         )
60                     }
61                 UseCaseResults.Success(categories)
62             } catch (e: Exception) {
63                 Log.e("TAG_ERROR", "Loading error ", e)
64                 UseCaseResults.Failed(e)
65             }
66         }
67 
68     /** Returns list of all medical permission types to be shown on the HC UI. */
69     suspend fun loadAllMedicalData(): UseCaseResults<List<PermissionTypesPerCategory>> =
70         withContext(dispatcher) {
71             try {
72                 val medicalResourceTypeInfos = getMedicalResourceTypeInfos()
73                 val medicalPermissionTypes =
74                     medicalResourceTypeInfos
75                         .filter { it.contributingDataSources.isNotEmpty() }
76                         .map { fromMedicalResourceType(it.medicalResourceType) }
77                 if (medicalPermissionTypes.isEmpty()) {
78                     UseCaseResults.Success(listOf())
79                 } else {
80                     UseCaseResults.Success(
81                         listOf(PermissionTypesPerCategory(MEDICAL, medicalPermissionTypes))
82                     )
83                 }
84             } catch (e: Exception) {
85                 Log.e("TAG_ERROR", "Loading error ", e)
86                 UseCaseResults.Failed(e)
87             }
88         }
89 
90     /** Returns whether there is any fitness data in HC. */
91     suspend fun loadHasAnyFitnessData(): UseCaseResults<Boolean> =
92         withContext(dispatcher) {
93             try {
94                 val recordTypeInfoMap = getRecordTypeInfoMap()
95                 val anyFitnessData =
96                     recordTypeInfoMap.any { it.value.contributingPackages.isNotEmpty() }
97                 UseCaseResults.Success(anyFitnessData)
98             } catch (e: Exception) {
99                 Log.e("TAG_ERROR", "Loading error ", e)
100                 UseCaseResults.Failed(e)
101             }
102         }
103 
104     /** Returns whether there is any medical data in HC. */
105     suspend fun loadHasAnyMedicalData(): UseCaseResults<Boolean> =
106         withContext(dispatcher) {
107             try {
108                 val medicalResourceTypeInfos = getMedicalResourceTypeInfos()
109                 val anyMedicalData =
110                     medicalResourceTypeInfos.any { it.contributingDataSources.isNotEmpty() }
111                 UseCaseResults.Success(anyMedicalData)
112             } catch (e: Exception) {
113                 Log.e("TAG_ERROR", "Loading error ", e)
114                 UseCaseResults.Failed(e)
115             }
116         }
117 
118     /**
119      * Returns list of fitness categories and permission types written by the given app to be shown
120      * on the HC UI.
121      */
122     suspend fun loadFitnessAppData(
123         packageName: String
124     ): UseCaseResults<List<PermissionTypesPerCategory>> =
125         withContext(dispatcher) {
126             try {
127                 val recordTypeInfoMap = getRecordTypeInfoMap()
128                 val categories =
129                     FITNESS_DATA_CATEGORIES.map {
130                         PermissionTypesPerCategory(
131                             it,
132                             getPermissionTypesPerCategory(it, recordTypeInfoMap, packageName),
133                         )
134                     }
135                 UseCaseResults.Success(categories)
136             } catch (e: Exception) {
137                 UseCaseResults.Failed(e)
138             }
139         }
140 
141     /**
142      * Returns list of medical categories and permission types written by the given app to be shown
143      * on the HC UI.
144      */
145     suspend fun loadMedicalAppData(
146         packageName: String
147     ): UseCaseResults<List<PermissionTypesPerCategory>> =
148         withContext(dispatcher) {
149             try {
150                 val medicalResourceTypeInfos = getMedicalResourceTypeInfos()
151                 val medicalPermissionTypes =
152                     filterMedicalPermissionTypes(medicalResourceTypeInfos, packageName)
153                 if (medicalPermissionTypes.isEmpty()) {
154                     UseCaseResults.Success(listOf())
155                 } else {
156                     UseCaseResults.Success(
157                         listOf(PermissionTypesPerCategory(MEDICAL, medicalPermissionTypes))
158                     )
159                 }
160             } catch (e: Exception) {
161                 UseCaseResults.Failed(e)
162             }
163         }
164 
165     private suspend fun getRecordTypeInfoMap(): Map<Class<out Record>, RecordTypeInfoResponse> {
166         val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
167             suspendCancellableCoroutine { continuation ->
168                 healthConnectManager.queryAllRecordTypesInfo(
169                     Runnable::run,
170                     continuation.asOutcomeReceiver(),
171                 )
172             }
173         return recordTypeInfoMap
174     }
175 
176     private suspend fun getMedicalResourceTypeInfos() =
177         suspendCancellableCoroutine { continuation ->
178             healthConnectManager.queryAllMedicalResourceTypeInfos(
179                 Runnable::run,
180                 continuation.asOutcomeReceiver(),
181             )
182         }
183 
184     private fun filterMedicalPermissionTypes(
185         medicalResourceTypeInfos: List<MedicalResourceTypeInfo>,
186         packageName: String,
187     ): List<MedicalPermissionType> =
188         medicalResourceTypeInfos
189             .filter { medicalResourceTypeInfo ->
190                 val contributingPackageNames =
191                     medicalResourceTypeInfo.contributingDataSources.map { it.packageName }.toSet()
192                 contributingPackageNames.contains(packageName)
193             }
194             .map { fromMedicalResourceType(it.medicalResourceType) }
195 
196     /**
197      * Returns those [FitnessPermissionType]s that have some data written by the given [packageName]
198      * app. If the is no app provided then return all data.
199      */
200     private fun getPermissionTypesPerCategory(
201         category: @HealthDataCategoryInt Int,
202         recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>,
203         packageName: String?,
204     ): List<HealthPermissionType> {
205         if (packageName == null) {
206             return category.healthPermissionTypes().filter { hasData(it, recordTypeInfoMap) }
207         }
208         return category.healthPermissionTypes().filter {
209             hasDataByApp(it, recordTypeInfoMap, packageName)
210         }
211     }
212 
213     private fun hasData(
214         permissionType: HealthPermissionType,
215         recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>,
216     ): Boolean =
217         recordTypeInfoMap.values.firstOrNull {
218             fromHealthPermissionCategory(it.permissionCategory) == permissionType &&
219                 it.contributingPackages.isNotEmpty()
220         } != null
221 
222     private fun hasDataByApp(
223         permissionType: HealthPermissionType,
224         recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse>,
225         packageName: String,
226     ): Boolean =
227         recordTypeInfoMap.values.firstOrNull {
228             fromHealthPermissionCategory(it.permissionCategory) == permissionType &&
229                 it.contributingPackages.isNotEmpty() &&
230                 it.contributingPackages
231                     .map { contributingApp -> contributingApp.packageName }
232                     .contains(packageName)
233         } != null
234 }
235 
236 /**
237  * Represents Health Category group to be shown in health connect screens.
238  *
239  * @param category Category id
240  * @param data [HealthPermissionType]s within the category that have data written by given app.
241  */
242 data class PermissionTypesPerCategory(
243     val category: @HealthDataCategoryInt Int,
244     val data: List<HealthPermissionType>,
245 )
246