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.HealthPermission.FitnessPermission
26 import com.android.healthconnect.controller.permissions.data.PermissionsAccessType
27 import com.android.healthconnect.controller.permissiontypes.api.LoadPriorityListUseCase
28 import com.android.healthconnect.controller.service.IoDispatcher
29 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.healthPermissionTypes
30 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
31 import com.android.healthconnect.controller.shared.HealthPermissionReader
32 import com.android.healthconnect.controller.shared.app.AppInfoReader
33 import com.android.healthconnect.controller.shared.app.AppMetadata
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 LoadPotentialPriorityListUseCase
43 @Inject
44 constructor(
45     private val appInfoReader: AppInfoReader,
46     private val healthConnectManager: HealthConnectManager,
47     private val healthPermissionReader: HealthPermissionReader,
48     private val loadGrantedHealthPermissionsUseCase: GetGrantedHealthPermissionsUseCase,
49     private val loadPriorityListUseCase: LoadPriorityListUseCase,
50     @IoDispatcher private val dispatcher: CoroutineDispatcher
51 ) : ILoadPotentialPriorityListUseCase {
52 
53     private val TAG = "LoadAppSourcesUseCase"
54 
55     /** Returns a list of unique [AppMetadata]s that are potential priority list candidates. */
56     override suspend operator fun invoke(
57         category: @HealthDataCategoryInt Int
58     ): UseCaseResults<List<AppMetadata>> =
59         withContext(dispatcher) {
60             val appsWithDataResult = getAppsWithData(category)
61             val appsWithWritePermissionResult = getAppsWithWritePermission(category)
62             val appsOnPriorityListResult = loadPriorityListUseCase.invoke(category)
63 
64             // Propagate error if any calls fail
65             if (appsWithDataResult is UseCaseResults.Failed) {
66                 UseCaseResults.Failed(appsWithDataResult.exception)
67             } else if (appsWithWritePermissionResult is UseCaseResults.Failed) {
68                 UseCaseResults.Failed(appsWithWritePermissionResult.exception)
69             } else if (appsOnPriorityListResult is UseCaseResults.Failed) {
70                 UseCaseResults.Failed(appsOnPriorityListResult.exception)
71             } else {
72                 val appsWithData = (appsWithDataResult as UseCaseResults.Success).data
73                 val appsWithWritePermission =
74                     (appsWithWritePermissionResult as UseCaseResults.Success).data
75                 val appsOnPriorityList =
76                     (appsOnPriorityListResult as UseCaseResults.Success)
77                         .data
78                         .map { it.packageName }
79                         .toSet()
80 
81                 val potentialPriorityListApps =
82                     appsWithData
83                         .union(appsWithWritePermission)
84                         .minus(appsOnPriorityList)
85                         .toList()
86                         .map { appInfoReader.getAppMetadata(it) }
87 
88                 UseCaseResults.Success(potentialPriorityListApps)
89             }
90         }
91 
92     /** Returns a list of unique packageNames that have data in this [HealthDataCategory]. */
93     @VisibleForTesting
94     suspend fun getAppsWithData(category: @HealthDataCategoryInt Int): UseCaseResults<Set<String>> =
95         withContext(dispatcher) {
96             try {
97                 val recordTypeInfoMap: Map<Class<out Record>, RecordTypeInfoResponse> =
98                     suspendCancellableCoroutine { continuation ->
99                         healthConnectManager.queryAllRecordTypesInfo(
100                             Runnable::run, continuation.asOutcomeReceiver())
101                     }
102                 val packages =
103                     recordTypeInfoMap.values
104                         .filter {
105                             it.contributingPackages.isNotEmpty() && it.dataCategory == category
106                         }
107                         .map { it.contributingPackages }
108                         .flatten()
109                 UseCaseResults.Success(packages.map { it.packageName }.toSet())
110             } catch (e: Exception) {
111                 Log.e(TAG, "Failed to get apps with data ", e)
112                 UseCaseResults.Failed(e)
113             }
114         }
115 
116     /**
117      * Returns a set of packageNames which have at least one WRITE permission in this
118      * [HealthDataCategory] *
119      */
120     @VisibleForTesting
121     suspend fun getAppsWithWritePermission(
122         category: @HealthDataCategoryInt Int
123     ): UseCaseResults<Set<String>> =
124         withContext(dispatcher) {
125             try {
126                 val writeAppPackageNameSet: MutableSet<String> = mutableSetOf()
127                 val appsWithFitnessPermissions: List<String> =
128                     healthPermissionReader.getAppsWithFitnessPermissions()
129                 val fitnessPermissionsInCategory: List<String> =
130                     category.healthPermissionTypes().map { healthPermissionType ->
131                         FitnessPermission(healthPermissionType, PermissionsAccessType.WRITE)
132                             .toString()
133                     }
134 
135                 appsWithFitnessPermissions.forEach { packageName ->
136                     val permissionsPerPackage: List<String> =
137                         loadGrantedHealthPermissionsUseCase(packageName)
138 
139                     // Apps that can WRITE the given HealthDataCategory
140                     if (fitnessPermissionsInCategory.any { permissionsPerPackage.contains(it) }) {
141                         writeAppPackageNameSet.add(packageName)
142                     }
143                 }
144 
145                 UseCaseResults.Success(writeAppPackageNameSet)
146             } catch (e: Exception) {
147                 Log.e(TAG, "Failed to get apps with write permission ", e)
148                 UseCaseResults.Failed(e)
149             }
150         }
151 }
152 
153 interface ILoadPotentialPriorityListUseCase {
invokenull154     suspend fun invoke(category: @HealthDataCategoryInt Int): UseCaseResults<List<AppMetadata>>
155 }
156