• 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");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.healthconnect.controller.datasources.api
17 
18 import android.health.connect.HealthConnectManager
19 import android.health.connect.RecordTypeInfoResponse
20 import android.health.connect.datatypes.Record
21 import android.util.Log
22 import androidx.annotation.VisibleForTesting
23 import androidx.core.os.asOutcomeReceiver
24 import com.android.healthconnect.controller.permissions.api.GetGrantedHealthPermissionsUseCase
25 import com.android.healthconnect.controller.permissions.data.FitnessPermissionType
26 import com.android.healthconnect.controller.permissions.data.HealthPermission.FitnessPermission
27 import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
28 import com.android.healthconnect.controller.permissiontypes.api.LoadPriorityListUseCase
29 import com.android.healthconnect.controller.service.IoDispatcher
30 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes
31 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
32 import com.android.healthconnect.controller.shared.HealthPermissionReader
33 import com.android.healthconnect.controller.shared.app.AppInfoReader
34 import com.android.healthconnect.controller.shared.app.AppMetadata
35 import com.android.healthconnect.controller.shared.usecase.UseCaseResults
36 import javax.inject.Inject
37 import javax.inject.Singleton
38 import kotlinx.coroutines.CoroutineDispatcher
39 import kotlinx.coroutines.suspendCancellableCoroutine
40 import kotlinx.coroutines.withContext
41 
42 @Singleton
43 class LoadPotentialPriorityListUseCase
44 @Inject
45 constructor(
46     private val appInfoReader: AppInfoReader,
47     private val healthConnectManager: HealthConnectManager,
48     private val healthPermissionReader: HealthPermissionReader,
49     private val loadGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
50     private val loadPriorityListUseCase: LoadPriorityListUseCase,
51     @IoDispatcher private val dispatcher: CoroutineDispatcher
52 ) : ILoadPotentialPriorityListUseCase {
53 
54     private val TAG = "LoadAppSourcesUseCase"
55 
56     /** Returns a list of unique [AppMetadata]s that are potential priority list candidates. */
57     override suspend operator fun invoke(
58         category: @HealthDataCategoryInt Int
59     ): UseCaseResults<List<AppMetadata>> =
60         withContext(dispatcher) {
61             val appsWithDataResult = getAppsWithData(category)
62             val appsWithWritePermissionResult = getAppsWithWritePermission(category)
63             val appsOnPriorityListResult = loadPriorityListUseCase.invoke(category)
64 
65             // Propagate error if any calls fail
66             if (appsWithDataResult is UseCaseResults.Failed) {
67                 UseCaseResults.Failed(appsWithDataResult.exception)
68             } else if (appsWithWritePermissionResult is UseCaseResults.Failed) {
69                 UseCaseResults.Failed(appsWithWritePermissionResult.exception)
70             } else if (appsOnPriorityListResult is UseCaseResults.Failed) {
71                 UseCaseResults.Failed(appsOnPriorityListResult.exception)
72             } else {
73                 val appsWithData = (appsWithDataResult as UseCaseResults.Success).data
74                 val appsWithWritePermission =
75                     (appsWithWritePermissionResult as UseCaseResults.Success).data
76                 val appsOnPriorityList =
77                     (appsOnPriorityListResult as UseCaseResults.Success)
78                         .data
79                         .map { it.packageName }
80                         .toSet()
81 
82                 val potentialPriorityListApps =
83                     appsWithData
84                         .union(appsWithWritePermission)
85                         .minus(appsOnPriorityList)
86                         .toList()
87                         .map { appInfoReader.getAppMetadata(it) }
88 
89                 UseCaseResults.Success(potentialPriorityListApps)
90             }
91         }
92 
93     /** Returns a list of unique packageNames that have data in this [HealthDataCategory]. */
94     @VisibleForTesting
95     suspend fun getAppsWithData(category: @HealthDataCategoryInt Int): UseCaseResults<Set<String>> =
96         withContext(dispatcher) {
97             try {
98                 val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
99                     suspendCancellableCoroutine { continuation ->
100                         healthConnectManager.queryAllRecordTypesInfo(
101                             Runnable::run, continuation.asOutcomeReceiver())
102                     }
103                 val packages =
104                     recordTypeInfoMap.values
105                         .filter {
106                             it.contributingPackages.isNotEmpty() && it.dataCategory == category
107                         }
108                         .map { it.contributingPackages }
109                         .flatten()
110                 UseCaseResults.Success(packages.map { it.packageName }.toSet())
111             } catch (e: Exception) {
112                 Log.e(TAG, "Failed to get apps with data ", e)
113                 UseCaseResults.Failed(e)
114             }
115         }
116 
117     /**
118      * Returns a set of packageNames which have at least one WRITE permission in this
119      * [HealthDataCategory] *
120      */
121     @VisibleForTesting
122     suspend fun getAppsWithWritePermission(
123         category: @HealthDataCategoryInt Int
124     ): UseCaseResults<Set<String>> =
125         withContext(dispatcher) {
126             try {
127                 val writeAppPackageNameSet: MutableSet<String> = mutableSetOf()
128                 val appsWithFitnessPermissions: List<String> =
129                     healthPermissionReader.getAppsWithFitnessPermissions()
130                 val fitnessPermissionsInCategory: List<String> =
131                     category
132                         .healthPermissionTypes()
133                         .filterIsInstance<FitnessPermissionType>()
134                         .map { healthPermissionType ->
135                             FitnessPermission(healthPermissionType, PermissionsAccessType.WRITE)
136                                 .toString()
137                         }
138 
139                 appsWithFitnessPermissions.forEach { packageName ->
140                     val permissionsPerPackage: List<String> =
141                         loadGrantedHealthPermissionsUseCase(packageName)
142 
143                     // Apps that can WRITE the given HealthDataCategory
144                     if (fitnessPermissionsInCategory.any { permissionsPerPackage.contains(it) }) {
145                         writeAppPackageNameSet.add(packageName)
146                     }
147                 }
148 
149                 UseCaseResults.Success(writeAppPackageNameSet)
150             } catch (e: Exception) {
151                 Log.e(TAG, "Failed to get apps with write permission ", e)
152                 UseCaseResults.Failed(e)
153             }
154         }
155 }
156 
157 interface ILoadPotentialPriorityListUseCase {
invokenull158     suspend fun invoke(category: @HealthDataCategoryInt Int): UseCaseResults<List<AppMetadata>>
159 }
160