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 android.health.connect.HealthPermissions.getDataCategoriesWithWritePermissionsForPackage; 20 import static android.health.connect.HealthPermissions.getPackageHasWriteHealthPermissionsForCategory; 21 22 import static com.android.server.healthconnect.storage.request.UpsertTableRequest.TYPE_STRING; 23 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER; 24 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_UNIQUE; 25 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY; 26 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL; 27 28 import android.content.ContentValues; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager; 31 import android.content.res.Resources; 32 import android.database.Cursor; 33 import android.health.connect.HealthDataCategory; 34 import android.health.connect.internal.datatypes.utils.HealthConnectMappings; 35 import android.os.UserHandle; 36 import android.util.Pair; 37 import android.util.Slog; 38 39 import androidx.annotation.Nullable; 40 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.server.healthconnect.HealthConnectThreadScheduler; 43 import com.android.server.healthconnect.permission.PackageInfoUtils; 44 import com.android.server.healthconnect.storage.HealthConnectContext; 45 import com.android.server.healthconnect.storage.TransactionManager; 46 import com.android.server.healthconnect.storage.request.CreateTableRequest; 47 import com.android.server.healthconnect.storage.request.DeleteTableRequest; 48 import com.android.server.healthconnect.storage.request.ReadTableRequest; 49 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 50 import com.android.server.healthconnect.storage.utils.StorageUtils; 51 52 import java.util.ArrayList; 53 import java.util.Collections; 54 import java.util.HashMap; 55 import java.util.HashSet; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Objects; 59 import java.util.Set; 60 import java.util.concurrent.ConcurrentHashMap; 61 import java.util.stream.Collectors; 62 63 /** 64 * Helper class to get priority of the apps for each {@link HealthDataCategory} 65 * 66 * @hide 67 */ 68 public class HealthDataCategoryPriorityHelper extends DatabaseHelper { 69 public static final String PRIORITY_TABLE_NAME = "health_data_category_priority_table"; 70 public static final String HEALTH_DATA_CATEGORY_COLUMN_NAME = "health_data_category"; 71 public static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO = 72 Collections.singletonList(new Pair<>(HEALTH_DATA_CATEGORY_COLUMN_NAME, TYPE_STRING)); 73 public static final String APP_ID_PRIORITY_ORDER_COLUMN_NAME = "app_id_priority_order"; 74 private static final String TAG = "HealthConnectPrioHelper"; 75 private static final String DEFAULT_APP_RESOURCE_NAME = 76 "android:string/config_defaultHealthConnectApp"; 77 public static final String INACTIVE_APPS_ADDED = "inactive_apps_added"; 78 79 private HealthConnectContext mUserContext; 80 private final AppInfoHelper mAppInfoHelper; 81 private final PackageInfoUtils mPackageInfoUtils; 82 private final TransactionManager mTransactionManager; 83 private final PreferenceHelper mPreferenceHelper; 84 private final HealthConnectMappings mHealthConnectMappings; 85 private final HealthConnectThreadScheduler mThreadScheduler; 86 87 /** 88 * map of {@link HealthDataCategory} to list of app ids from {@link AppInfoHelper}, in the order 89 * of their priority 90 */ 91 @Nullable 92 private volatile ConcurrentHashMap<Integer, List<Long>> mHealthDataCategoryToAppIdPriorityMap; 93 HealthDataCategoryPriorityHelper( HealthConnectContext userContext, AppInfoHelper appInfoHelper, TransactionManager transactionManager, PreferenceHelper preferenceHelper, PackageInfoUtils packageInfoUtils, HealthConnectMappings healthConnectMappings, DatabaseHelpers databaseHelpers, HealthConnectThreadScheduler threadScheduler)94 public HealthDataCategoryPriorityHelper( 95 HealthConnectContext userContext, 96 AppInfoHelper appInfoHelper, 97 TransactionManager transactionManager, 98 PreferenceHelper preferenceHelper, 99 PackageInfoUtils packageInfoUtils, 100 HealthConnectMappings healthConnectMappings, 101 DatabaseHelpers databaseHelpers, 102 HealthConnectThreadScheduler threadScheduler) { 103 super(databaseHelpers); 104 mUserContext = userContext; 105 mAppInfoHelper = appInfoHelper; 106 mPackageInfoUtils = packageInfoUtils; 107 mTransactionManager = transactionManager; 108 mPreferenceHelper = preferenceHelper; 109 mHealthConnectMappings = healthConnectMappings; 110 mThreadScheduler = threadScheduler; 111 } 112 113 /** 114 * Returns a requests representing the tables that should be created corresponding to this 115 * helper 116 */ getCreateTableRequest()117 public static CreateTableRequest getCreateTableRequest() { 118 return new CreateTableRequest(PRIORITY_TABLE_NAME, getColumnInfo()); 119 } 120 121 @Override clearCache()122 public synchronized void clearCache() { 123 mHealthDataCategoryToAppIdPriorityMap = null; 124 } 125 126 /** Setup HealthDataCategoryPriorityHelper for the given user. */ setupForUser(HealthConnectContext userContext)127 public synchronized void setupForUser(HealthConnectContext userContext) { 128 mUserContext = userContext; 129 // While we already call clearCache() in HCManager.onUserSwitching(), calling this again 130 // here in case any of the methods below was called in between that initialized the cache 131 // with the wrong context. 132 clearCache(); 133 mThreadScheduler.scheduleInternalTask( 134 () -> { 135 reSyncHealthDataPriorityTable(); 136 addInactiveAppsWhenFirstMigratingToNewAggregationControl(); 137 }); 138 } 139 140 @Override getMainTableName()141 protected String getMainTableName() { 142 return PRIORITY_TABLE_NAME; 143 } 144 145 /** See appendToPriorityList below */ appendToPriorityList( String packageName, @HealthDataCategory.Type int dataCategory, UserHandle user)146 public synchronized void appendToPriorityList( 147 String packageName, @HealthDataCategory.Type int dataCategory, UserHandle user) { 148 if (!mUserContext.getUser().equals(user)) { 149 // We are currently limited to be able to update the priority list for the foreground 150 // user only. User will need to manually add the app to the priority list later. 151 return; 152 } 153 appendToPriorityList(packageName, dataCategory, /* isInactiveApp */ false); 154 } 155 156 /** See maybeRemoveAppFromPriorityList below */ maybeRemoveAppFromPriorityList( String packageName, @HealthDataCategory.Type int dataCategory, UserHandle user)157 public synchronized void maybeRemoveAppFromPriorityList( 158 String packageName, @HealthDataCategory.Type int dataCategory, UserHandle user) { 159 if (!mUserContext.getUser().equals(user)) { 160 // We are currently limited to be able to update the priority list for the foreground 161 // user only. Apps will be removed from the priority list when the device switches to 162 // this user if they no longer have permissions. 163 return; 164 } 165 maybeRemoveAppFromPriorityList(packageName, dataCategory); 166 } 167 168 /** See maybeRemoveAppFromPriorityList below */ maybeRemoveAppFromPriorityList(String packageName, UserHandle user)169 public synchronized void maybeRemoveAppFromPriorityList(String packageName, UserHandle user) { 170 if (!mUserContext.getUser().equals(user)) { 171 // We are currently limited to be able to update the priority list for the foreground 172 // user only. Apps will be removed from the priority list when the device switches to 173 // this user if they no longer have permissions. 174 return; 175 } 176 maybeRemoveAppFromPriorityList(packageName); 177 } 178 179 /** 180 * Appends a packageName to the priority list for this category when an app gets write 181 * permissions or during the one-time operation to add inactive apps. 182 * 183 * <p>Inactive apps are added at the bottom of the priority list even if they are the default 184 * app. 185 */ appendToPriorityList( String packageName, @HealthDataCategory.Type int dataCategory, boolean isInactiveApp)186 public synchronized void appendToPriorityList( 187 String packageName, @HealthDataCategory.Type int dataCategory, boolean isInactiveApp) { 188 List<Long> newPriorityOrder; 189 getHealthDataCategoryToAppIdPriorityMap().putIfAbsent(dataCategory, new ArrayList<>()); 190 long appInfoId = mAppInfoHelper.getOrInsertAppInfoId(packageName); 191 if (getHealthDataCategoryToAppIdPriorityMap().get(dataCategory).contains(appInfoId)) { 192 return; 193 } 194 newPriorityOrder = 195 new ArrayList<>(getHealthDataCategoryToAppIdPriorityMap().get(dataCategory)); 196 197 if (isDefaultApp(packageName) && !isInactiveApp) { 198 newPriorityOrder.add(0, appInfoId); 199 } else { 200 newPriorityOrder.add(appInfoId); 201 } 202 safelyUpdateDBAndUpdateCache( 203 new UpsertTableRequest( 204 PRIORITY_TABLE_NAME, 205 getContentValuesFor(dataCategory, newPriorityOrder), 206 UNIQUE_COLUMN_INFO), 207 dataCategory, 208 newPriorityOrder); 209 } 210 211 @VisibleForTesting isDefaultApp(String packageName)212 boolean isDefaultApp(String packageName) { 213 String defaultApp = 214 mUserContext 215 .getResources() 216 .getString( 217 Resources.getSystem() 218 .getIdentifier(DEFAULT_APP_RESOURCE_NAME, null, null)); 219 220 return Objects.equals(packageName, defaultApp); 221 } 222 223 /** 224 * Removes a packageName from the priority list of a particular category if the package name 225 * does not have any granted write permissions and has no data. 226 */ maybeRemoveAppFromPriorityList( String packageName, @HealthDataCategory.Type int dataCategory)227 public synchronized void maybeRemoveAppFromPriorityList( 228 String packageName, @HealthDataCategory.Type int dataCategory) { 229 PackageInfo packageInfo = 230 mPackageInfoUtils.getPackageInfoWithPermissionsAsUser( 231 packageName, mUserContext.getUser(), mUserContext); 232 233 // If package is not found, assume no permissions are granted. 234 if (packageInfo == null 235 || !getPackageHasWriteHealthPermissionsForCategory( 236 packageInfo, dataCategory, mUserContext)) { 237 removeAppFromPriorityListIfNoDataExists(dataCategory, packageName); 238 } 239 } 240 241 /** 242 * Removes a packageName from the priority list of all categories if the package name does not 243 * have any granted write permissions and has no data. 244 */ maybeRemoveAppFromPriorityList(String packageName)245 public synchronized void maybeRemoveAppFromPriorityList(String packageName) { 246 for (Integer dataCategory : getHealthDataCategoryToAppIdPriorityMap().keySet()) { 247 maybeRemoveAppFromPriorityList(packageName, dataCategory); 248 } 249 } 250 251 /** 252 * Removes a packageName from the priority list of a particular category if the package name has 253 * no data. 254 * 255 * <p>Assumes that the app has no write permission. 256 */ maybeRemoveAppWithoutWritePermissionsFromPriorityList( String packageName)257 public synchronized void maybeRemoveAppWithoutWritePermissionsFromPriorityList( 258 String packageName) { 259 for (Integer dataCategory : getHealthDataCategoryToAppIdPriorityMap().keySet()) { 260 removeAppFromPriorityListIfNoDataExists(dataCategory, packageName); 261 } 262 } 263 264 /** 265 * Refreshes the priority list and returns the list of package names based on priority for the 266 * input {@link HealthDataCategory} 267 */ syncAndGetPriorityOrder(@ealthDataCategory.Type int type)268 public List<String> syncAndGetPriorityOrder(@HealthDataCategory.Type int type) { 269 reSyncHealthDataPriorityTable(); 270 return mAppInfoHelper.getPackageNames(getAppIdPriorityOrder(type)); 271 } 272 273 /** Returns list of App ids based on priority for the input {@link HealthDataCategory} */ getAppIdPriorityOrder(@ealthDataCategory.Type int type)274 public List<Long> getAppIdPriorityOrder(@HealthDataCategory.Type int type) { 275 List<Long> packageIds = getHealthDataCategoryToAppIdPriorityMap().get(type); 276 if (packageIds == null) { 277 return Collections.emptyList(); 278 } 279 280 return packageIds; 281 } 282 283 /** 284 * Sets a new priority order for the given category, and allows adding and removing packages 285 * from the priority list. 286 * 287 * <p>In the old behaviour it is not allowed to add or remove packages so the new priority order 288 * needs to be sanitised before applying the operation. 289 */ setPriorityOrder(int dataCategory, List<String> packagePriorityOrder)290 public void setPriorityOrder(int dataCategory, List<String> packagePriorityOrder) { 291 List<Long> newPriorityOrder = mAppInfoHelper.getAppInfoIds(packagePriorityOrder); 292 safelyUpdateDBAndUpdateCache( 293 new UpsertTableRequest( 294 PRIORITY_TABLE_NAME, 295 getContentValuesFor(dataCategory, newPriorityOrder), 296 UNIQUE_COLUMN_INFO), 297 dataCategory, 298 newPriorityOrder); 299 } 300 getHealthDataCategoryToAppIdPriorityMap()301 private Map<Integer, List<Long>> getHealthDataCategoryToAppIdPriorityMap() { 302 if (mHealthDataCategoryToAppIdPriorityMap == null) { 303 populateDataCategoryToAppIdPriorityMap(); 304 } 305 306 return Objects.requireNonNull(mHealthDataCategoryToAppIdPriorityMap); 307 } 308 309 /** Returns an immutable map of data categories along with their priority order. */ getHealthDataCategoryToAppIdPriorityMapImmutable()310 public Map<Integer, List<Long>> getHealthDataCategoryToAppIdPriorityMapImmutable() { 311 return Collections.unmodifiableMap(getHealthDataCategoryToAppIdPriorityMap()); 312 } 313 populateDataCategoryToAppIdPriorityMap()314 private synchronized void populateDataCategoryToAppIdPriorityMap() { 315 if (mHealthDataCategoryToAppIdPriorityMap != null) { 316 return; 317 } 318 319 ConcurrentHashMap<Integer, List<Long>> healthDataCategoryToAppIdPriorityMap = 320 new ConcurrentHashMap<>(); 321 try (Cursor cursor = mTransactionManager.read(new ReadTableRequest(PRIORITY_TABLE_NAME))) { 322 while (cursor.moveToNext()) { 323 int dataCategory = 324 cursor.getInt(cursor.getColumnIndex(HEALTH_DATA_CATEGORY_COLUMN_NAME)); 325 List<Long> appIdsInOrder = 326 StorageUtils.getCursorLongList( 327 cursor, APP_ID_PRIORITY_ORDER_COLUMN_NAME, DELIMITER); 328 329 healthDataCategoryToAppIdPriorityMap.put(dataCategory, appIdsInOrder); 330 } 331 } 332 333 mHealthDataCategoryToAppIdPriorityMap = healthDataCategoryToAppIdPriorityMap; 334 } 335 safelyUpdateDBAndUpdateCache( UpsertTableRequest request, @HealthDataCategory.Type int dataCategory, List<Long> newList)336 private synchronized void safelyUpdateDBAndUpdateCache( 337 UpsertTableRequest request, 338 @HealthDataCategory.Type int dataCategory, 339 List<Long> newList) { 340 try { 341 mTransactionManager.insertOrReplaceOnConflict(request); 342 getHealthDataCategoryToAppIdPriorityMap().put(dataCategory, newList); 343 } catch (Exception e) { 344 Slog.e(TAG, "Priority update failed", e); 345 throw e; 346 } 347 } 348 safelyUpdateDBAndUpdateCache( DeleteTableRequest request, @HealthDataCategory.Type int dataCategory)349 private synchronized void safelyUpdateDBAndUpdateCache( 350 DeleteTableRequest request, @HealthDataCategory.Type int dataCategory) { 351 try { 352 mTransactionManager.delete(request); 353 getHealthDataCategoryToAppIdPriorityMap().remove(dataCategory); 354 } catch (Exception e) { 355 Slog.e(TAG, "Delete from priority DB failed: ", e); 356 throw e; 357 } 358 } 359 getContentValuesFor( @ealthDataCategory.Type int dataCategory, List<Long> priorityList)360 private ContentValues getContentValuesFor( 361 @HealthDataCategory.Type int dataCategory, List<Long> priorityList) { 362 ContentValues contentValues = new ContentValues(); 363 contentValues.put(HEALTH_DATA_CATEGORY_COLUMN_NAME, dataCategory); 364 contentValues.put( 365 APP_ID_PRIORITY_ORDER_COLUMN_NAME, StorageUtils.flattenLongList(priorityList)); 366 367 return contentValues; 368 } 369 370 /** 371 * This implementation should return the column names with which the table should be created. 372 * 373 * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table 374 * already exists on the device 375 * 376 * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS 377 */ getColumnInfo()378 private static List<Pair<String, String>> getColumnInfo() { 379 ArrayList<Pair<String, String>> columnInfo = new ArrayList<>(); 380 columnInfo.add(new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY)); 381 columnInfo.add(new Pair<>(HEALTH_DATA_CATEGORY_COLUMN_NAME, INTEGER_UNIQUE)); 382 columnInfo.add(new Pair<>(APP_ID_PRIORITY_ORDER_COLUMN_NAME, TEXT_NOT_NULL)); 383 384 return columnInfo; 385 } 386 387 /** Syncs priority table with the permissions and data. */ reSyncHealthDataPriorityTable()388 public synchronized void reSyncHealthDataPriorityTable() { 389 // Candidates to be removed from the priority list 390 Map<Integer, Set<Long>> dataCategoryToAppIdMapWithoutPermission = 391 getHealthDataCategoryToAppIdPriorityMap().entrySet().stream() 392 .collect( 393 Collectors.toMap( 394 Map.Entry::getKey, e -> new HashSet<>(e.getValue()))); 395 396 List<PackageInfo> validHealthApps = getValidHealthApps(); 397 for (PackageInfo packageInfo : validHealthApps) { 398 Set<Integer> dataCategoriesWithWritePermissionsForThisPackage = 399 getDataCategoriesWithWritePermissionsForPackage(packageInfo, mUserContext); 400 long appInfoId = mAppInfoHelper.getOrInsertAppInfoId(packageInfo.packageName); 401 402 for (int dataCategory : dataCategoriesWithWritePermissionsForThisPackage) { 403 Set<Long> appIdsWithoutPermission = 404 dataCategoryToAppIdMapWithoutPermission.getOrDefault( 405 dataCategory, new HashSet<>()); 406 if (appIdsWithoutPermission.remove(appInfoId)) { 407 dataCategoryToAppIdMapWithoutPermission.put( 408 dataCategory, appIdsWithoutPermission); 409 } 410 } 411 } 412 413 // Remove any apps without any permission for the category, if they have no data present. 414 for (Map.Entry<Integer, Set<Long>> entry : 415 dataCategoryToAppIdMapWithoutPermission.entrySet()) { 416 for (Long appInfoId : entry.getValue()) { 417 try { 418 removeAppFromPriorityListIfNoDataExists( 419 entry.getKey(), mAppInfoHelper.getPackageName(appInfoId)); 420 } catch (PackageManager.NameNotFoundException e) { 421 Slog.e(TAG, "Package name not found while syncing priority table", e); 422 } 423 } 424 } 425 addContributingAppsIfCategoryListIsEmpty(); 426 } 427 428 /** Returns a list of PackageInfos holding health permissions for this user. */ getValidHealthApps()429 private List<PackageInfo> getValidHealthApps() { 430 return mPackageInfoUtils.getPackagesHoldingHealthPermissions( 431 mUserContext.getUser(), mUserContext); 432 } 433 434 /** 435 * Removes a packageName from the priority list of a category. The package name is not removed 436 * if it has data in that category. 437 */ removeAppFromPriorityListIfNoDataExists( @ealthDataCategory.Type int dataCategory, String packageName)438 private synchronized void removeAppFromPriorityListIfNoDataExists( 439 @HealthDataCategory.Type int dataCategory, String packageName) { 440 boolean dataExistsForPackageName = appHasDataInCategory(packageName, dataCategory); 441 if (dataExistsForPackageName) { 442 return; 443 } 444 445 List<Long> newPriorityList = 446 new ArrayList<>( 447 getHealthDataCategoryToAppIdPriorityMap() 448 .getOrDefault(dataCategory, Collections.emptyList())); 449 if (newPriorityList.isEmpty()) { 450 return; 451 } 452 453 newPriorityList.remove(mAppInfoHelper.getAppInfoId(packageName)); 454 if (newPriorityList.isEmpty()) { 455 safelyUpdateDBAndUpdateCache( 456 new DeleteTableRequest(PRIORITY_TABLE_NAME) 457 .setId(HEALTH_DATA_CATEGORY_COLUMN_NAME, String.valueOf(dataCategory)), 458 dataCategory); 459 return; 460 } 461 462 safelyUpdateDBAndUpdateCache( 463 new UpsertTableRequest( 464 PRIORITY_TABLE_NAME, 465 getContentValuesFor(dataCategory, newPriorityList), 466 UNIQUE_COLUMN_INFO), 467 dataCategory, 468 newPriorityList); 469 } 470 471 /** 472 * If the priority list is empty for a {@link HealthDataCategory}, add the contributing apps. 473 * 474 * <p>This is necessary because the priority list should never be empty if there are 475 * contributing apps present. 476 */ addContributingAppsIfCategoryListIsEmpty()477 private synchronized void addContributingAppsIfCategoryListIsEmpty() { 478 mHealthConnectMappings 479 .getAllHealthDataCategories() 480 .forEach( 481 (category) -> 482 getHealthDataCategoryToAppIdPriorityMap() 483 .putIfAbsent(category, new ArrayList<>())); 484 Map<Integer, List<Long>> healthDataCategoryToAppIdPriorityMap = 485 getHealthDataCategoryToAppIdPriorityMap(); 486 for (int dataCategory : healthDataCategoryToAppIdPriorityMap.keySet()) { 487 List<Long> appIdsInPriorityOrder = 488 healthDataCategoryToAppIdPriorityMap.getOrDefault(dataCategory, List.of()); 489 if (appIdsInPriorityOrder.isEmpty()) { 490 getAllContributorApps().getOrDefault(dataCategory, new HashSet<>()).stream() 491 .sorted() 492 .forEach( 493 (contributingApp) -> 494 appendToPriorityList( 495 contributingApp, 496 dataCategory, 497 isInactiveApp(dataCategory, contributingApp))); 498 } 499 } 500 } 501 502 /** 503 * A one-time operation which adds contributing inactive apps to the priority list. 504 * 505 * <p>The contributing apps are added in ascending order of their package names. 506 */ addInactiveAppsWhenFirstMigratingToNewAggregationControl()507 public void addInactiveAppsWhenFirstMigratingToNewAggregationControl() { 508 if (!shouldAddInactiveApps()) { 509 return; 510 } 511 512 Map<Integer, Set<String>> inactiveApps = getAllInactiveApps(); 513 514 for (Map.Entry<Integer, Set<String>> entry : inactiveApps.entrySet()) { 515 int category = entry.getKey(); 516 entry.getValue().stream() 517 .sorted() 518 .forEach( 519 packageName -> 520 appendToPriorityList( 521 packageName, category, /* isInactiveApp= */ true)); 522 } 523 524 mPreferenceHelper.insertOrReplacePreference(INACTIVE_APPS_ADDED, String.valueOf(true)); 525 } 526 isInactiveApp(@ealthDataCategory.Type int dataCategory, String packageName)527 private boolean isInactiveApp(@HealthDataCategory.Type int dataCategory, String packageName) { 528 Map<Integer, Set<String>> inactiveApps = getAllInactiveApps(); 529 return inactiveApps.getOrDefault(dataCategory, new HashSet<>()).contains(packageName); 530 } 531 shouldAddInactiveApps()532 private boolean shouldAddInactiveApps() { 533 String haveInactiveAppsBeenAddedString = 534 mPreferenceHelper.getPreference(INACTIVE_APPS_ADDED); 535 536 return haveInactiveAppsBeenAddedString == null 537 || !Boolean.parseBoolean(haveInactiveAppsBeenAddedString); 538 } 539 540 @VisibleForTesting appHasDataInCategory(String packageName, int category)541 boolean appHasDataInCategory(String packageName, int category) { 542 return getDataCategoriesWithDataForPackage(packageName).contains(category); 543 } 544 545 @VisibleForTesting getDataCategoriesWithDataForPackage(String packageName)546 Set<Integer> getDataCategoriesWithDataForPackage(String packageName) { 547 Map<Integer, Set<String>> recordTypeToContributingPackages = 548 mAppInfoHelper.getRecordTypesToContributingPackagesMap(); 549 Set<Integer> dataCategoriesWithData = new HashSet<>(); 550 551 for (Map.Entry<Integer, Set<String>> entry : recordTypeToContributingPackages.entrySet()) { 552 Integer recordType = entry.getKey(); 553 Set<String> contributingPackages = entry.getValue(); 554 int recordCategory = mHealthConnectMappings.getRecordCategoryForRecordType(recordType); 555 boolean isPackageNameContributor = contributingPackages.contains(packageName); 556 if (isPackageNameContributor) { 557 dataCategoriesWithData.add(recordCategory); 558 } 559 } 560 return dataCategoriesWithData; 561 } 562 563 /** 564 * Returns a set of contributing apps for each dataCategory. If a dataCategory does not have any 565 * data it will not be present in the map. 566 */ 567 @VisibleForTesting getAllContributorApps()568 Map<Integer, Set<String>> getAllContributorApps() { 569 Map<Integer, Set<String>> recordTypeToContributingPackages = 570 mAppInfoHelper.getRecordTypesToContributingPackagesMap(); 571 572 Map<Integer, Set<String>> allContributorApps = new HashMap<>(); 573 574 for (Map.Entry<Integer, Set<String>> entry : recordTypeToContributingPackages.entrySet()) { 575 int recordCategory = 576 mHealthConnectMappings.getRecordCategoryForRecordType(entry.getKey()); 577 Set<String> contributingPackages = entry.getValue(); 578 579 Set<String> currentPackages = 580 allContributorApps.getOrDefault(recordCategory, new HashSet<>()); 581 currentPackages.addAll(contributingPackages); 582 allContributorApps.put(recordCategory, currentPackages); 583 } 584 585 return allContributorApps; 586 } 587 588 /** 589 * Returns a map of dataCategory to sets of packageNames that are inactive. 590 * 591 * <p>An inactive app is one that has data for the dataCategory but no write permissions. 592 */ 593 @VisibleForTesting getAllInactiveApps()594 Map<Integer, Set<String>> getAllInactiveApps() { 595 Map<Integer, Set<String>> allContributorApps = getAllContributorApps(); 596 Map<Integer, Set<String>> inactiveApps = new HashMap<>(); 597 598 for (Map.Entry<Integer, Set<String>> entry : allContributorApps.entrySet()) { 599 int category = entry.getKey(); 600 Set<String> contributorApps = entry.getValue(); 601 602 for (String packageName : contributorApps) { 603 PackageInfo packageInfo = 604 mPackageInfoUtils.getPackageInfoWithPermissionsAsUser( 605 packageName, mUserContext.getUser(), mUserContext); 606 if (packageInfo == null 607 || !getPackageHasWriteHealthPermissionsForCategory( 608 packageInfo, category, mUserContext)) { 609 Set<String> currentPackages = 610 inactiveApps.getOrDefault(category, new HashSet<>()); 611 if (currentPackages.add(packageName)) { 612 inactiveApps.put(category, currentPackages); 613 } 614 } 615 } 616 } 617 618 return inactiveApps; 619 } 620 } 621