• 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 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