• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 
17 package com.android.server.healthconnect.storage.datatypehelpers;
18 
19 import static com.android.server.healthconnect.storage.request.UpsertTableRequest.TYPE_STRING;
20 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
21 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_UNIQUE;
22 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY;
23 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL;
24 
25 import android.annotation.NonNull;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.pm.PackageInfo;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.database.sqlite.SQLiteDatabase;
32 import android.health.connect.HealthConnectManager;
33 import android.health.connect.HealthDataCategory;
34 import android.health.connect.HealthPermissions;
35 import android.os.UserHandle;
36 import android.util.ArraySet;
37 import android.util.Pair;
38 import android.util.Slog;
39 
40 import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
41 import com.android.server.healthconnect.storage.TransactionManager;
42 import com.android.server.healthconnect.storage.request.CreateTableRequest;
43 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
44 import com.android.server.healthconnect.storage.request.ReadTableRequest;
45 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
46 import com.android.server.healthconnect.storage.utils.StorageUtils;
47 
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Set;
54 import java.util.concurrent.ConcurrentHashMap;
55 import java.util.stream.Collectors;
56 
57 /**
58  * Helper class to get priority of the apps for each {@link HealthDataCategory}
59  *
60  * @hide
61  */
62 public class HealthDataCategoryPriorityHelper {
63     private static final String TABLE_NAME = "health_data_category_priority_table";
64     private static final String HEALTH_DATA_CATEGORY_COLUMN_NAME = "health_data_category";
65     public static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO =
66             Collections.singletonList(new Pair<>(HEALTH_DATA_CATEGORY_COLUMN_NAME, TYPE_STRING));
67     private static final String APP_ID_PRIORITY_ORDER_COLUMN_NAME = "app_id_priority_order";
68     private static final String TAG = "HealthConnectPrioHelper";
69     private static final String DEFAULT_APP_RESOURCE_NAME =
70             "android:string/config_defaultHealthConnectApp";
71     private static volatile HealthDataCategoryPriorityHelper sHealthDataCategoryPriorityHelper;
72 
73     /**
74      * map of {@link HealthDataCategory} to list of app ids from {@link AppInfoHelper}, in the order
75      * of their priority
76      */
77     private volatile ConcurrentHashMap<Integer, List<Long>> mHealthDataCategoryToAppIdPriorityMap;
78 
HealthDataCategoryPriorityHelper()79     private HealthDataCategoryPriorityHelper() {}
80 
81     // Called on DB update.
onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db)82     public void onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db) {
83         // empty by default
84     }
85 
86     /**
87      * Returns a requests representing the tables that should be created corresponding to this
88      * helper
89      */
90     @NonNull
getCreateTableRequest()91     public final CreateTableRequest getCreateTableRequest() {
92         return new CreateTableRequest(TABLE_NAME, getColumnInfo());
93     }
94 
appendToPriorityList( @onNull String packageName, @HealthDataCategory.Type int dataCategory, Context context)95     public synchronized void appendToPriorityList(
96             @NonNull String packageName,
97             @HealthDataCategory.Type int dataCategory,
98             Context context) {
99         List<Long> newPriorityOrder;
100         getHealthDataCategoryToAppIdPriorityMap().putIfAbsent(dataCategory, new ArrayList<>());
101         long appInfoId = AppInfoHelper.getInstance().getOrInsertAppInfoId(packageName, context);
102         if (getHealthDataCategoryToAppIdPriorityMap().get(dataCategory).contains(appInfoId)) {
103             return;
104         }
105         newPriorityOrder =
106                 new ArrayList<>(getHealthDataCategoryToAppIdPriorityMap().get(dataCategory));
107 
108         String defaultApp =
109                 context.getResources()
110                         .getString(
111                                 Resources.getSystem()
112                                         .getIdentifier(DEFAULT_APP_RESOURCE_NAME, null, null));
113         if (Objects.equals(packageName, defaultApp)) {
114             newPriorityOrder.add(0, appInfoId);
115         } else {
116             newPriorityOrder.add(appInfoId);
117         }
118         safelyUpdateDBAndUpdateCache(
119                 new UpsertTableRequest(
120                         TABLE_NAME,
121                         getContentValuesFor(dataCategory, newPriorityOrder),
122                         UNIQUE_COLUMN_INFO),
123                 dataCategory,
124                 newPriorityOrder);
125     }
126 
removeFromPriorityList( @onNull String packageName, @HealthDataCategory.Type int dataCategory, HealthConnectPermissionHelper permissionHelper, UserHandle userHandle)127     public synchronized void removeFromPriorityList(
128             @NonNull String packageName,
129             @HealthDataCategory.Type int dataCategory,
130             HealthConnectPermissionHelper permissionHelper,
131             UserHandle userHandle) {
132 
133         final List<String> grantedPermissions =
134                 permissionHelper.getGrantedHealthPermissions(packageName, userHandle);
135         for (String permission : HealthPermissions.getWriteHealthPermissionsFor(dataCategory)) {
136             if (grantedPermissions.contains(permission)) {
137                 return;
138             }
139         }
140         removeFromPriorityListInternal(dataCategory, packageName);
141     }
142 
removeFromPriorityListIfNeeded( @onNull PackageInfo packageInfo, @NonNull Context context)143     public synchronized void removeFromPriorityListIfNeeded(
144             @NonNull PackageInfo packageInfo, @NonNull Context context) {
145         Set<Integer> dataCategoryWithPermission = new ArraySet<>();
146         for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
147             String currPerm = packageInfo.requestedPermissions[i];
148             if (HealthConnectManager.isHealthPermission(context, currPerm)
149                     && ((packageInfo.requestedPermissionsFlags[i]
150                                     & PackageInfo.REQUESTED_PERMISSION_GRANTED)
151                             != 0)) {
152                 int dataCategory = HealthPermissions.getHealthDataCategory(currPerm);
153                 if (dataCategory != -1) {
154                     dataCategoryWithPermission.add(dataCategory);
155                 }
156             }
157         }
158         for (int category : getHealthDataCategoryToAppIdPriorityMap().keySet()) {
159             if (!dataCategoryWithPermission.contains(category)) {
160                 removeFromPriorityListInternal(category, packageInfo.packageName);
161             }
162         }
163     }
164 
165     /** Removes app from priorityList for all HealthData Categories if the package is uninstalled */
removeAppFromPriorityList(@onNull String packageName)166     public synchronized void removeAppFromPriorityList(@NonNull String packageName) {
167         Objects.requireNonNull(packageName);
168         for (Integer dataCategory : getHealthDataCategoryToAppIdPriorityMap().keySet()) {
169             removeFromPriorityListInternal(dataCategory, packageName);
170         }
171     }
172 
173     /** Returns list of package names based on priority for the input {@link HealthDataCategory} */
174     @NonNull
getPriorityOrder(@ealthDataCategory.Type int type)175     public List<String> getPriorityOrder(@HealthDataCategory.Type int type) {
176         return AppInfoHelper.getInstance().getPackageNames(getAppIdPriorityOrder(type));
177     }
178 
179     /** Returns list of App ids based on priority for the input {@link HealthDataCategory} */
180     @NonNull
getAppIdPriorityOrder(@ealthDataCategory.Type int type)181     public List<Long> getAppIdPriorityOrder(@HealthDataCategory.Type int type) {
182         List<Long> packageIds = getHealthDataCategoryToAppIdPriorityMap().get(type);
183         if (packageIds == null) {
184             return Collections.emptyList();
185         }
186 
187         return packageIds;
188     }
189 
setPriorityOrder(int dataCategory, @NonNull List<String> packagePriorityOrder)190     public void setPriorityOrder(int dataCategory, @NonNull List<String> packagePriorityOrder) {
191         List<Long> currentPriorityOrder =
192                 getHealthDataCategoryToAppIdPriorityMap()
193                         .getOrDefault(dataCategory, Collections.emptyList());
194         List<Long> newPriorityOrder =
195                 AppInfoHelper.getInstance().getAppInfoIds(packagePriorityOrder);
196 
197         // Remove appId from the priority order if it is not part of the current priority order,
198         // this is because in the time app tried to update the order an app permission might
199         // have been removed, and we only store priority order of apps with permission.
200         newPriorityOrder.removeIf(priorityOrder -> !currentPriorityOrder.contains(priorityOrder));
201         newPriorityOrder.addAll(currentPriorityOrder);
202         // Make sure we don't remove any new entries. So append old priority in new priority and
203         // remove duplicates
204         newPriorityOrder = newPriorityOrder.stream().distinct().collect(Collectors.toList());
205 
206         safelyUpdateDBAndUpdateCache(
207                 new UpsertTableRequest(
208                         TABLE_NAME,
209                         getContentValuesFor(dataCategory, newPriorityOrder),
210                         UNIQUE_COLUMN_INFO),
211                 dataCategory,
212                 newPriorityOrder);
213     }
214 
215     /** Deletes all entries from the database and clears the cache. */
clearData(@onNull TransactionManager transactionManager)216     public synchronized void clearData(@NonNull TransactionManager transactionManager) {
217         transactionManager.delete(new DeleteTableRequest(TABLE_NAME));
218         clearCache();
219     }
220 
clearCache()221     public synchronized void clearCache() {
222         mHealthDataCategoryToAppIdPriorityMap = null;
223     }
224 
getHealthDataCategoryToAppIdPriorityMap()225     private Map<Integer, List<Long>> getHealthDataCategoryToAppIdPriorityMap() {
226         if (mHealthDataCategoryToAppIdPriorityMap == null) {
227             populateDataCategoryToAppIdPriorityMap();
228         }
229 
230         return mHealthDataCategoryToAppIdPriorityMap;
231     }
232 
233     /** Returns an immutable map of data categories along with their priority order. */
getHealthDataCategoryToAppIdPriorityMapImmutable()234     public Map<Integer, List<Long>> getHealthDataCategoryToAppIdPriorityMapImmutable() {
235         return Collections.unmodifiableMap(getHealthDataCategoryToAppIdPriorityMap());
236     }
237 
populateDataCategoryToAppIdPriorityMap()238     private synchronized void populateDataCategoryToAppIdPriorityMap() {
239         if (mHealthDataCategoryToAppIdPriorityMap != null) {
240             return;
241         }
242 
243         ConcurrentHashMap<Integer, List<Long>> healthDataCategoryToAppIdPriorityMap =
244                 new ConcurrentHashMap<>();
245         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
246         try (Cursor cursor = transactionManager.read(new ReadTableRequest(TABLE_NAME))) {
247             while (cursor.moveToNext()) {
248                 int dataCategory =
249                         cursor.getInt(cursor.getColumnIndex(HEALTH_DATA_CATEGORY_COLUMN_NAME));
250                 List<Long> appIdsInOrder =
251                         StorageUtils.getCursorLongList(
252                                 cursor, APP_ID_PRIORITY_ORDER_COLUMN_NAME, DELIMITER);
253 
254                 healthDataCategoryToAppIdPriorityMap.put(dataCategory, appIdsInOrder);
255             }
256         }
257 
258         mHealthDataCategoryToAppIdPriorityMap = healthDataCategoryToAppIdPriorityMap;
259     }
260 
safelyUpdateDBAndUpdateCache( UpsertTableRequest request, @HealthDataCategory.Type int dataCategory, List<Long> newList)261     private synchronized void safelyUpdateDBAndUpdateCache(
262             UpsertTableRequest request,
263             @HealthDataCategory.Type int dataCategory,
264             List<Long> newList) {
265         try {
266             TransactionManager.getInitialisedInstance().insertOrReplace(request);
267             getHealthDataCategoryToAppIdPriorityMap().put(dataCategory, newList);
268         } catch (Exception e) {
269             Slog.e(TAG, "Priority update failed", e);
270             throw e;
271         }
272     }
273 
safelyUpdateDBAndUpdateCache( DeleteTableRequest request, @HealthDataCategory.Type int dataCategory)274     private synchronized void safelyUpdateDBAndUpdateCache(
275             DeleteTableRequest request, @HealthDataCategory.Type int dataCategory) {
276         try {
277             TransactionManager.getInitialisedInstance().delete(request);
278             getHealthDataCategoryToAppIdPriorityMap().remove(dataCategory);
279         } catch (Exception e) {
280             Slog.e(TAG, "Delete from priority DB failed: ", e);
281             throw e;
282         }
283     }
284 
getContentValuesFor( @ealthDataCategory.Type int dataCategory, List<Long> priorityList)285     private ContentValues getContentValuesFor(
286             @HealthDataCategory.Type int dataCategory, List<Long> priorityList) {
287         ContentValues contentValues = new ContentValues();
288         contentValues.put(HEALTH_DATA_CATEGORY_COLUMN_NAME, dataCategory);
289         contentValues.put(
290                 APP_ID_PRIORITY_ORDER_COLUMN_NAME, StorageUtils.flattenLongList(priorityList));
291 
292         return contentValues;
293     }
294 
295     /**
296      * This implementation should return the column names with which the table should be created.
297      *
298      * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table
299      * already exists on the device
300      *
301      * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS
302      */
303     @NonNull
getColumnInfo()304     private List<Pair<String, String>> getColumnInfo() {
305         ArrayList<Pair<String, String>> columnInfo = new ArrayList<>();
306         columnInfo.add(new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY));
307         columnInfo.add(new Pair<>(HEALTH_DATA_CATEGORY_COLUMN_NAME, INTEGER_UNIQUE));
308         columnInfo.add(new Pair<>(APP_ID_PRIORITY_ORDER_COLUMN_NAME, TEXT_NOT_NULL));
309 
310         return columnInfo;
311     }
312 
313     @NonNull
getInstance()314     public static synchronized HealthDataCategoryPriorityHelper getInstance() {
315         if (sHealthDataCategoryPriorityHelper == null) {
316             sHealthDataCategoryPriorityHelper = new HealthDataCategoryPriorityHelper();
317         }
318 
319         return sHealthDataCategoryPriorityHelper;
320     }
321 
removeFromPriorityListInternal( int dataCategory, @NonNull String packageName)322     private synchronized void removeFromPriorityListInternal(
323             int dataCategory, @NonNull String packageName) {
324         List<Long> newPriorityList =
325                 new ArrayList<>(
326                         getHealthDataCategoryToAppIdPriorityMap()
327                                 .getOrDefault(dataCategory, Collections.emptyList()));
328         if (newPriorityList.isEmpty()) {
329             return;
330         }
331 
332         newPriorityList.remove(AppInfoHelper.getInstance().getAppInfoId(packageName));
333         if (newPriorityList.isEmpty()) {
334             safelyUpdateDBAndUpdateCache(
335                     new DeleteTableRequest(TABLE_NAME)
336                             .setId(HEALTH_DATA_CATEGORY_COLUMN_NAME, String.valueOf(dataCategory)),
337                     dataCategory);
338             return;
339         }
340 
341         safelyUpdateDBAndUpdateCache(
342                 new UpsertTableRequest(
343                         TABLE_NAME,
344                         getContentValuesFor(dataCategory, newPriorityList),
345                         UNIQUE_COLUMN_INFO),
346                 dataCategory,
347                 newPriorityList);
348     }
349 }
350