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