• 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.Constants.DEBUG;
20 import static android.health.connect.Constants.DEFAULT_LONG;
21 
22 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.APP_INFO_ID_COLUMN_NAME;
23 import static com.android.server.healthconnect.storage.request.UpsertTableRequest.TYPE_STRING;
24 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB;
25 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY;
26 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL_UNIQUE;
27 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL;
28 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorBlob;
29 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
30 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
31 import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
32 
33 import static java.util.Objects.requireNonNull;
34 
35 import android.annotation.Nullable;
36 import android.annotation.SuppressLint;
37 import android.content.ContentValues;
38 import android.content.pm.ApplicationInfo;
39 import android.content.pm.PackageManager;
40 import android.content.pm.PackageManager.ApplicationInfoFlags;
41 import android.content.pm.PackageManager.NameNotFoundException;
42 import android.database.Cursor;
43 import android.database.sqlite.SQLiteDatabase;
44 import android.database.sqlite.SQLiteException;
45 import android.graphics.Bitmap;
46 import android.graphics.BitmapFactory;
47 import android.graphics.Canvas;
48 import android.graphics.drawable.Drawable;
49 import android.health.connect.Constants;
50 import android.health.connect.datatypes.AppInfo;
51 import android.health.connect.internal.datatypes.AppInfoInternal;
52 import android.health.connect.internal.datatypes.RecordInternal;
53 import android.health.connect.internal.datatypes.utils.HealthConnectMappings;
54 import android.util.Log;
55 import android.util.Pair;
56 import android.util.Slog;
57 
58 import com.android.server.healthconnect.storage.HealthConnectContext;
59 import com.android.server.healthconnect.storage.TransactionManager;
60 import com.android.server.healthconnect.storage.request.CreateTableRequest;
61 import com.android.server.healthconnect.storage.request.ReadTableRequest;
62 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
63 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings;
64 import com.android.server.healthconnect.storage.utils.WhereClauses;
65 
66 import java.io.ByteArrayOutputStream;
67 import java.io.IOException;
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.Collections;
71 import java.util.HashMap;
72 import java.util.HashSet;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Objects;
76 import java.util.Optional;
77 import java.util.Set;
78 import java.util.concurrent.ConcurrentHashMap;
79 import java.util.stream.Collectors;
80 
81 /**
82  * A class to help with the DB transaction for storing Application Info. {@link AppInfoHelper} acts
83  * as a layer b/w the application_igenfo_table stored in the DB and helps perform insert and read
84  * operations on the table
85  *
86  * @hide
87  */
88 public final class AppInfoHelper extends DatabaseHelper {
89     public static final String TABLE_NAME = "application_info_table";
90     public static final String APPLICATION_COLUMN_NAME = "app_name";
91     public static final String PACKAGE_COLUMN_NAME = "package_name";
92     public static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO =
93             Collections.singletonList(new Pair<>(PACKAGE_COLUMN_NAME, TYPE_STRING));
94     public static final String APP_ICON_COLUMN_NAME = "app_icon";
95     private static final String TAG = "HealthConnectAppInfoHelper";
96     private static final String RECORD_TYPES_USED_COLUMN_NAME = "record_types_used";
97     private static final int COMPRESS_FACTOR = 100;
98 
99     /**
100      * Map to store appInfoId -> packageName mapping for populating record for read
101      *
102      * <p>TO HAVE THREAD SAFETY DON'T USE THESE VARIABLES DIRECTLY, INSTEAD USE ITS GETTER
103      */
104     @Nullable private volatile ConcurrentHashMap<Long, String> mIdPackageNameMap;
105 
106     /**
107      * Map to store application package-name -> AppInfo mapping (such as packageName -> appName,
108      * icon, rowId in the DB etc.)
109      *
110      * <p>TO HAVE THREAD SAFETY DON'T USE THESE VARIABLES DIRECTLY, INSTEAD USE ITS GETTER
111      */
112     @Nullable private volatile ConcurrentHashMap<String, AppInfoInternal> mAppInfoMap;
113 
114     private HealthConnectContext mUserContext;
115     private final TransactionManager mTransactionManager;
116     private final InternalHealthConnectMappings mInternalHealthConnectMappings;
117     private final HealthConnectMappings mHealthConnectMappings;
118 
AppInfoHelper( HealthConnectContext userContext, TransactionManager transactionManager, InternalHealthConnectMappings internalHealthConnectMappings, DatabaseHelpers databaseHelpers)119     public AppInfoHelper(
120             HealthConnectContext userContext,
121             TransactionManager transactionManager,
122             InternalHealthConnectMappings internalHealthConnectMappings,
123             DatabaseHelpers databaseHelpers) {
124         super(databaseHelpers);
125         mUserContext = userContext;
126         mTransactionManager = transactionManager;
127         mInternalHealthConnectMappings = internalHealthConnectMappings;
128         mHealthConnectMappings = internalHealthConnectMappings.getExternalMappings();
129     }
130 
131     @Override
clearCache()132     public synchronized void clearCache() {
133         mAppInfoMap = null;
134         mIdPackageNameMap = null;
135     }
136 
137     /** Setup AppInfoHelper for the given user. */
setupForUser(HealthConnectContext userContext)138     public synchronized void setupForUser(HealthConnectContext userContext) {
139         mUserContext = userContext;
140         // While we already call clearCache() in HCManager.onUserSwitching(), calling this again
141         // here in case any of the methods below was called in between that initialized the cache
142         // with the wrong context.
143         clearCache();
144     }
145 
146     @Override
getMainTableName()147     protected String getMainTableName() {
148         return TABLE_NAME;
149     }
150 
151     /**
152      * Returns a requests representing the tables that should be created corresponding to this
153      * helper
154      */
getCreateTableRequest()155     public static CreateTableRequest getCreateTableRequest() {
156         return new CreateTableRequest(TABLE_NAME, getColumnInfo());
157     }
158 
159     /** Populates record with appInfoId */
populateAppInfoId(RecordInternal<?> record, boolean requireAllFields)160     public void populateAppInfoId(RecordInternal<?> record, boolean requireAllFields) {
161         final String packageName = requireNonNull(record.getPackageName());
162         AppInfoInternal appInfo = getAppInfoMap().get(packageName);
163 
164         if (appInfo == null) {
165             try {
166                 appInfo = getAppInfo(packageName);
167             } catch (NameNotFoundException e) {
168                 if (requireAllFields) {
169                     throw new IllegalStateException("Could not find package info", e);
170                 }
171 
172                 appInfo =
173                         new AppInfoInternal(
174                                 DEFAULT_LONG, packageName, record.getAppName(), null, null);
175             }
176 
177             insertIfNotPresent(packageName, appInfo);
178         }
179 
180         record.setAppInfoId(appInfo.getId());
181         record.setPackageName(appInfo.getPackageName());
182     }
183 
184     /**
185      * Replaces the application info of the specified {@code packageName} with the specified {@code
186      * name} and {@code icon}, only if the corresponding application is not currently installed.
187      *
188      * <p>Only replaces the exiting AppInfo; no new insertion.
189      */
updateAppInfoIfNotInstalled( String packageName, @Nullable String name, @Nullable byte[] maybeIcon)190     public void updateAppInfoIfNotInstalled(
191             String packageName, @Nullable String name, @Nullable byte[] maybeIcon) {
192         if (isAppInstalled(packageName)) {
193             return;
194         }
195 
196         byte[] icon = maybeIcon == null ? getIconFromPackageName(packageName) : maybeIcon;
197         var appInfo = getAppInfoMap().get(packageName);
198         // using pre-existing value of recordTypesUsed.
199         var recordTypesUsed = appInfo == null ? null : appInfo.getRecordTypesUsed();
200         AppInfoInternal appInfoInternal =
201                 new AppInfoInternal(
202                         getAppInfoId(packageName),
203                         packageName,
204                         name,
205                         decodeBitmap(icon),
206                         recordTypesUsed);
207         updateIfPresent(packageName, appInfoInternal);
208     }
209 
210     /**
211      * Inserts the application info of the specified {@code packageName} if it is missing, or
212      * updates it with the specified {@code name}, only if the corresponding application is not
213      * currently installed.
214      */
restoreAppInfo(String packageName, @Nullable String name)215     public void restoreAppInfo(String packageName, @Nullable String name) {
216         var currentAppInfo = getAppInfoMap().get(packageName);
217         if (currentAppInfo == null) {
218             addAppInfoIfNoAppInfoEntryExists(packageName, name);
219         } else if (!isAppInstalled(packageName)) {
220             AppInfoInternal updatedAppInfo =
221                     new AppInfoInternal(
222                             currentAppInfo.getId(),
223                             currentAppInfo.getPackageName(),
224                             name,
225                             currentAppInfo.getIcon(),
226                             currentAppInfo.getRecordTypesUsed());
227             updateIfPresent(packageName, updatedAppInfo);
228         }
229     }
230 
231     /**
232      * Inserts the application info of the specified {@code packageName} with the specified {@code
233      * name} and {@code icon}, only if no AppInfo entry already exists.
234      */
addAppInfoIfNoAppInfoEntryExists(String packageName, @Nullable String name)235     public void addAppInfoIfNoAppInfoEntryExists(String packageName, @Nullable String name) {
236         if (!containsAppInfo(packageName)) {
237             byte[] icon = getIconFromPackageName(packageName);
238             AppInfoInternal appInfoInternal =
239                     new AppInfoInternal(DEFAULT_LONG, packageName, name, decodeBitmap(icon), null);
240             insertIfNotPresent(packageName, appInfoInternal);
241         }
242     }
243 
isAppInstalled(String packageName)244     private boolean isAppInstalled(String packageName) {
245         try {
246             mUserContext
247                     .getPackageManager()
248                     .getApplicationInfo(packageName, ApplicationInfoFlags.of(0));
249             return true;
250         } catch (NameNotFoundException e) {
251             return false;
252         }
253     }
254 
255     /**
256      * @return id of {@code packageName} or {@link Constants#DEFAULT_LONG} if the id is not found
257      */
getAppInfoId(String packageName)258     public long getAppInfoId(String packageName) {
259         if (packageName == null) {
260             return DEFAULT_LONG;
261         }
262 
263         AppInfoInternal appInfo = getAppInfoMap().getOrDefault(packageName, null);
264 
265         if (appInfo == null) {
266             return DEFAULT_LONG;
267         }
268         return appInfo.getId();
269     }
270 
271     /**
272      * @param packageName Name of package being checked.
273      * @return Boolean stating whether a record for the package being queried exists already.
274      */
containsAppInfo(String packageName)275     private boolean containsAppInfo(String packageName) {
276         return getAppInfoMap().containsKey(packageName);
277     }
278 
279     /**
280      * @param packageNames List of package names
281      * @return A list of appinfo ids from the application_info_table.
282      */
getAppInfoIds(List<String> packageNames)283     public List<Long> getAppInfoIds(List<String> packageNames) {
284         if (DEBUG) {
285             Slog.d(TAG, "App info map: " + getAppInfoMap());
286         }
287         if (packageNames == null || packageNames.isEmpty()) {
288             return Collections.emptyList();
289         }
290 
291         List<Long> result = new ArrayList<>(packageNames.size());
292         packageNames.forEach(packageName -> result.add(getAppInfoId(packageName)));
293 
294         return result;
295     }
296 
297     /** Gets the package name corresponding to the {@code packageId}. */
getPackageName(long packageId)298     public String getPackageName(long packageId) throws NameNotFoundException {
299         String packageName = getIdPackageNameMap().get(packageId);
300         if (packageName == null) {
301             throw new NameNotFoundException("No package name found for id " + packageId);
302         }
303         return packageName;
304     }
305 
306     // TODO(sameerj): Remove identical method convertPackageIdsToPackageName.
getPackageNames(List<Long> packageIds)307     public List<String> getPackageNames(List<Long> packageIds) {
308         if (packageIds == null || packageIds.isEmpty()) {
309             return Collections.emptyList();
310         }
311 
312         List<String> packageNames = new ArrayList<>();
313         packageIds.forEach(
314                 (packageId) -> {
315                     try {
316                         String packageName = getPackageName(packageId);
317                         packageNames.add(packageName);
318                     } catch (PackageManager.NameNotFoundException e) {
319                         throw new NullPointerException("Package name was null for the given id");
320                     }
321                 });
322 
323         return packageNames;
324     }
325 
326     /**
327      * Returns a list of AppInfo objects which are contributing data to some recordType, or belongs
328      * to the provided {@code appInfoIds}.
329      */
getApplicationInfosWithRecordTypesOrInIdsList(Set<Long> appInfoIds)330     public List<AppInfo> getApplicationInfosWithRecordTypesOrInIdsList(Set<Long> appInfoIds) {
331         return getAppInfoMap().values().stream()
332                 .filter(
333                         (appInfo) ->
334                                 (appInfo.getRecordTypesUsed() != null
335                                                 && !appInfo.getRecordTypesUsed().isEmpty())
336                                         || appInfoIds.contains(appInfo.getId()))
337                 .map(AppInfoInternal::toExternal)
338                 .collect(Collectors.toList());
339     }
340 
341     /**
342      * Returns AppInfo id for the provided {@code packageName}, creating it if needed using the
343      * given {@link SQLiteDatabase}.
344      */
getOrInsertAppInfoId(SQLiteDatabase db, String packageName)345     public long getOrInsertAppInfoId(SQLiteDatabase db, String packageName) {
346         return getOrInsertAppInfoId(Optional.of(db), packageName);
347     }
348 
349     /** Returns AppInfo id for the provided {@code packageName}, creating it if needed. */
getOrInsertAppInfoId(String packageName)350     public long getOrInsertAppInfoId(String packageName) {
351         return getOrInsertAppInfoId(Optional.empty(), packageName);
352     }
353 
354     /**
355      * Returns AppInfo id for the provided {@code packageName}, creating it if needed. If given db
356      * is null, the default will be {@link TransactionManager#getReadableDb()} for reads and {@link
357      * TransactionManager#getWritableDb()} for writes.
358      */
getOrInsertAppInfoId(Optional<SQLiteDatabase> db, String packageName)359     private long getOrInsertAppInfoId(Optional<SQLiteDatabase> db, String packageName) {
360         AppInfoInternal appInfoInternal = getAppInfoMap(db).get(packageName);
361 
362         if (appInfoInternal == null) {
363             try {
364                 appInfoInternal = getAppInfo(packageName);
365             } catch (NameNotFoundException e) {
366                 throw new IllegalStateException("Could not find package info for package", e);
367             }
368 
369             insertIfNotPresent(db, packageName, appInfoInternal);
370         }
371 
372         return appInfoInternal.getId();
373     }
374 
populateAppInfoMap(Optional<SQLiteDatabase> db)375     private synchronized void populateAppInfoMap(Optional<SQLiteDatabase> db) {
376         if (mAppInfoMap != null) {
377             return;
378         }
379         ConcurrentHashMap<String, AppInfoInternal> appInfoMap = new ConcurrentHashMap<>();
380         ConcurrentHashMap<Long, String> idPackageNameMap = new ConcurrentHashMap<>();
381         try (Cursor cursor = readAppInfo(db)) {
382             while (cursor.moveToNext()) {
383                 long rowId = getCursorLong(cursor, RecordHelper.PRIMARY_COLUMN_NAME);
384                 String packageName = getCursorString(cursor, PACKAGE_COLUMN_NAME);
385                 String appName = getCursorString(cursor, APPLICATION_COLUMN_NAME);
386                 byte[] icon = getCursorBlob(cursor, APP_ICON_COLUMN_NAME);
387                 Bitmap bitmap = decodeBitmap(icon);
388                 String recordTypesUsed = getCursorString(cursor, RECORD_TYPES_USED_COLUMN_NAME);
389 
390                 Set<Integer> recordTypesListAsSet = getRecordTypesAsSet(recordTypesUsed);
391 
392                 appInfoMap.put(
393                         packageName,
394                         new AppInfoInternal(
395                                 rowId, packageName, appName, bitmap, recordTypesListAsSet));
396                 idPackageNameMap.put(rowId, packageName);
397             }
398         }
399         mAppInfoMap = appInfoMap;
400         mIdPackageNameMap = idPackageNameMap;
401     }
402 
readAppInfo(Optional<SQLiteDatabase> db)403     private Cursor readAppInfo(Optional<SQLiteDatabase> db) {
404         ReadTableRequest request = new ReadTableRequest(TABLE_NAME);
405         return db.map(sqLiteDatabase -> mTransactionManager.read(sqLiteDatabase, request))
406                 .orElseGet(() -> mTransactionManager.read(request));
407     }
408 
409     @Nullable
getRecordTypesAsSet(String recordTypesUsed)410     private Set<Integer> getRecordTypesAsSet(String recordTypesUsed) {
411         if (recordTypesUsed != null && !recordTypesUsed.isEmpty()) {
412             return Arrays.stream(recordTypesUsed.split(","))
413                     .map(Integer::parseInt)
414                     .collect(Collectors.toSet());
415         }
416         return null;
417     }
418 
419     /**
420      * Updates recordTypesUsed for the {@code packageName} in app info table.
421      *
422      * <p><b>NOTE:</b> This method should only be used for insert operation on recordType tables.
423      * Should not be called elsewhere.
424      *
425      * <p>see {@link AppInfoHelper#syncAppInfoMapRecordTypesUsed(Map)}} for updating this table
426      * during delete operations on recordTypes.
427      *
428      * @param recordTypes The record types that needs to be inserted.
429      * @param packageName The package for which the records need to be inserted.
430      */
431     @SuppressLint("LongLogTag")
updateAppInfoRecordTypesUsedOnInsert( Set<Integer> recordTypes, String packageName)432     public synchronized void updateAppInfoRecordTypesUsedOnInsert(
433             Set<Integer> recordTypes, String packageName) {
434         AppInfoInternal appInfo = getAppInfoMap().get(packageName);
435         if (appInfo == null) {
436             Log.e(
437                     TAG,
438                     "AppInfo for the current package: "
439                             + packageName
440                             + " does not exist. "
441                             + "Hence recordTypesUsed is not getting updated.");
442 
443             return;
444         }
445 
446         if (recordTypes == null || recordTypes.isEmpty()) {
447             return;
448         }
449         Set<Integer> updatedRecordTypes = new HashSet<>(recordTypes);
450         if (appInfo.getRecordTypesUsed() != null) {
451             updatedRecordTypes.addAll(appInfo.getRecordTypesUsed());
452         }
453         if (!updatedRecordTypes.equals(appInfo.getRecordTypesUsed())) {
454             updateAppInfoRecordTypesUsedSync(packageName, appInfo, updatedRecordTypes);
455         }
456     }
457 
458     /**
459      * Updates recordTypesUsed by for all packages in app info table.
460      *
461      * <p><b>NOTE:</b> This method should only be used for delete operation on recordType tables.
462      * Should not be called elsewhere.
463      *
464      * <p>Use this method to update the table for passed recordTypes, not passing any record will
465      * update all recordTypes.
466      *
467      * <p>see {@link AppInfoHelper#updateAppInfoRecordTypesUsedOnInsert(Set, String)} for updating
468      * this table during insert operations on recordTypes.
469      */
syncAppInfoRecordTypesUsed()470     public synchronized void syncAppInfoRecordTypesUsed() {
471         syncAppInfoRecordTypesUsed(null);
472     }
473 
474     /**
475      * Updates recordTypesUsed by for all packages in app info table.
476      *
477      * <p><b>NOTE:</b> This method should only be used for delete operation on recordType tables.
478      * Should not be called elsewhere.
479      *
480      * <p>Use this method to update the table for passed {@code recordTypesToBeSynced}, not passing
481      * any record will update all recordTypes.
482      *
483      * <p>see {@link AppInfoHelper#updateAppInfoRecordTypesUsedOnInsert(Set, String)} for updating
484      * this table during insert operations on recordTypes.
485      */
syncAppInfoRecordTypesUsed( @ullable Set<Integer> recordTypesToBeSynced)486     public synchronized void syncAppInfoRecordTypesUsed(
487             @Nullable Set<Integer> recordTypesToBeSynced) {
488         Set<Integer> recordTypesToBeUpdated =
489                 Objects.requireNonNullElseGet(
490                         recordTypesToBeSynced,
491                         () ->
492                                 mHealthConnectMappings
493                                         .getRecordIdToExternalRecordClassMap()
494                                         .keySet());
495 
496         Map<Integer, Set<Long>> recordTypeToContributingPackageIdsMap =
497                 getDistinctPackageIdsForRecordsTable(recordTypesToBeUpdated);
498 
499         Map<Integer, Set<String>> recordTypeToContributingPackageNamesMap = new HashMap<>();
500         recordTypeToContributingPackageIdsMap.forEach(
501                 (recordType, packageIds) ->
502                         recordTypeToContributingPackageNamesMap.put(
503                                 recordType, convertPackageIdsToPackageName(packageIds)));
504 
505         if (recordTypesToBeSynced == null) {
506             syncAppInfoMapRecordTypesUsed(recordTypeToContributingPackageNamesMap);
507         } else {
508             getAppInfoMap()
509                     .keySet()
510                     .forEach(
511                             (packageName) -> {
512                                 deleteRecordTypesForPackagesIfRequiredInternal(
513                                         recordTypesToBeUpdated,
514                                         recordTypeToContributingPackageNamesMap,
515                                         packageName);
516                             });
517         }
518     }
519 
520     /**
521      * This method updates recordTypesUsed for all packages and hence is a heavy operation. This
522      * method is used during AutoDeleteService and is run once per day.
523      */
524     @SuppressLint("LongLogTag")
syncAppInfoMapRecordTypesUsed( Map<Integer, Set<String>> recordTypeToContributingPackagesMap)525     private synchronized void syncAppInfoMapRecordTypesUsed(
526             Map<Integer, Set<String>> recordTypeToContributingPackagesMap) {
527         HashMap<String, List<Integer>> packageToRecordTypesMap =
528                 getPackageToRecordTypesMap(recordTypeToContributingPackagesMap);
529         getAppInfoMap()
530                 .forEach(
531                         (packageName, appInfo) -> {
532                             if (packageToRecordTypesMap.containsKey(packageName)) {
533                                 updateAppInfoRecordTypesUsedSync(
534                                         packageName,
535                                         appInfo,
536                                         new HashSet<>(packageToRecordTypesMap.get(packageName)));
537                             } else {
538                                 updateAppInfoRecordTypesUsedSync(
539                                         packageName, appInfo, /* recordTypesUsed */ null);
540                             }
541                             if (DEBUG) {
542                                 Log.d(
543                                         TAG,
544                                         "Syncing packages and corresponding recordTypesUsed for"
545                                                 + " package : "
546                                                 + packageName
547                                                 + ", recordTypesUsed : "
548                                                 + appInfo.getRecordTypesUsed());
549                             }
550                         });
551     }
552 
getPackageToRecordTypesMap( Map<Integer, Set<String>> recordTypeToContributingPackagesMap)553     private HashMap<String, List<Integer>> getPackageToRecordTypesMap(
554             Map<Integer, Set<String>> recordTypeToContributingPackagesMap) {
555         HashMap<String, List<Integer>> packageToRecordTypesMap = new HashMap<>();
556         recordTypeToContributingPackagesMap.forEach(
557                 (recordType, packageList) -> {
558                     packageList.forEach(
559                             (packageName) -> {
560                                 if (packageToRecordTypesMap.containsKey(packageName)) {
561                                     packageToRecordTypesMap.get(packageName).add(recordType);
562                                 } else {
563                                     ArrayList<Integer> types = new ArrayList<>();
564                                     types.add(recordType);
565                                     packageToRecordTypesMap.put(packageName, types);
566                                 }
567                             });
568                 });
569         return packageToRecordTypesMap;
570     }
571 
572     /**
573      * Checks and deletes record types in app info table for which the package is no longer
574      * contributing data. This is done after delete records operation has been performed.
575      */
576     @SuppressLint("LongLogTag")
deleteRecordTypesForPackagesIfRequiredInternal( Set<Integer> recordTypesToBeDeleted, Map<Integer, Set<String>> currentRecordTypePackageMap, String packageName)577     private synchronized void deleteRecordTypesForPackagesIfRequiredInternal(
578             Set<Integer> recordTypesToBeDeleted,
579             Map<Integer, Set<String>> currentRecordTypePackageMap,
580             String packageName) {
581         AppInfoInternal appInfo = getAppInfoMap().get(packageName);
582         if (appInfo == null) {
583             Log.e(
584                     TAG,
585                     "AppInfo for the current package: "
586                             + packageName
587                             + " does not exist. "
588                             + "Hence recordTypesUsed is not getting updated.");
589 
590             return;
591         }
592         if (appInfo.getRecordTypesUsed() == null || appInfo.getRecordTypesUsed().isEmpty()) {
593             // return since this package is not contributing to any recordType and hence there
594             // is nothing to delete.
595             return;
596         }
597         Set<Integer> updatedRecordTypesUsed = new HashSet<>(appInfo.getRecordTypesUsed());
598         for (Integer recordType : recordTypesToBeDeleted) {
599             // get the distinct packages used by the record after the deletion process, check if
600             // the recordType does not have the current package then remove record type from
601             // the package's app info record.
602             if (!currentRecordTypePackageMap
603                     .getOrDefault(recordType, new HashSet<>())
604                     .contains(packageName)) {
605                 updatedRecordTypesUsed.remove(recordType);
606             }
607         }
608         if (updatedRecordTypesUsed.equals(appInfo.getRecordTypesUsed())) {
609             return;
610         }
611         if (updatedRecordTypesUsed.isEmpty()) {
612             updatedRecordTypesUsed = null;
613         }
614         updateAppInfoRecordTypesUsedSync(packageName, appInfo, updatedRecordTypesUsed);
615     }
616 
617     @SuppressLint("LongLogTag")
updateAppInfoRecordTypesUsedSync( String packageName, AppInfoInternal appInfo, @Nullable Set<Integer> recordTypesUsed)618     private synchronized void updateAppInfoRecordTypesUsedSync(
619             String packageName, AppInfoInternal appInfo, @Nullable Set<Integer> recordTypesUsed) {
620         appInfo.setRecordTypesUsed(recordTypesUsed);
621         // create upsert table request to modify app info table, keyed by packages name.
622         WhereClauses whereClauseForAppInfoTableUpdate = new WhereClauses(AND);
623         whereClauseForAppInfoTableUpdate.addWhereEqualsClause(
624                 PACKAGE_COLUMN_NAME, appInfo.getPackageName());
625         UpsertTableRequest upsertRequestForAppInfoUpdate =
626                 new UpsertTableRequest(
627                         TABLE_NAME, getContentValues(packageName, appInfo), UNIQUE_COLUMN_INFO);
628         mTransactionManager.update(upsertRequestForAppInfoUpdate);
629 
630         // update locally stored maps to keep data in sync.
631         getAppInfoMap().put(packageName, appInfo);
632         getIdPackageNameMap().put(appInfo.getId(), packageName);
633         if (DEBUG) {
634             Log.d(
635                     TAG,
636                     "Updated app info table. PackageName : "
637                             + packageName
638                             + " , RecordTypesUsed : "
639                             + appInfo.getRecordTypesUsed()
640                             + ".");
641         }
642     }
643 
644     /** Returns a map for recordTypes and their contributing packages. */
getRecordTypesToContributingPackagesMap()645     public Map<Integer, Set<String>> getRecordTypesToContributingPackagesMap() {
646         Map<Integer, Set<String>> recordTypeContributingPackagesMap = new HashMap<>();
647         Map<String, AppInfoInternal> appInfoMap = getAppInfoMap();
648         appInfoMap.forEach(
649                 (packageName, appInfo) -> {
650                     Set<Integer> recordTypesUsed = appInfo.getRecordTypesUsed();
651                     if (recordTypesUsed != null) {
652                         recordTypesUsed.forEach(
653                                 (recordType) -> {
654                                     if (recordTypeContributingPackagesMap.containsKey(recordType)) {
655                                         recordTypeContributingPackagesMap
656                                                 .get(recordType)
657                                                 .add(packageName);
658                                     } else {
659                                         recordTypeContributingPackagesMap.put(
660                                                 recordType,
661                                                 new HashSet<>(Collections.singleton(packageName)));
662                                     }
663                                 });
664                     }
665                 });
666         return recordTypeContributingPackagesMap;
667     }
668 
getAppInfoMap()669     public Map<String, AppInfoInternal> getAppInfoMap() {
670         return getAppInfoMap(Optional.empty());
671     }
672 
673     /**
674      * Populates and gets the {@code mAppInfoMap} using the given {@link SQLiteDatabase} to read the
675      * table. If given db is null, the default will be {@link TransactionManager#getReadableDb()}.
676      */
getAppInfoMap(Optional<SQLiteDatabase> db)677     private Map<String, AppInfoInternal> getAppInfoMap(Optional<SQLiteDatabase> db) {
678         if (Objects.isNull(mAppInfoMap)) {
679             populateAppInfoMap(db);
680         }
681 
682         return Objects.requireNonNull(mAppInfoMap);
683     }
684 
685     /**
686      * Populates and gets the {@code mIdPackageNameMap} using the given {@link SQLiteDatabase} to
687      * read the table. If given db is null, the default will be {@link
688      * TransactionManager#getReadableDb()}.
689      */
getIdPackageNameMap(Optional<SQLiteDatabase> db)690     private Map<Long, String> getIdPackageNameMap(Optional<SQLiteDatabase> db) {
691         if (mIdPackageNameMap == null) {
692             populateAppInfoMap(db);
693         }
694 
695         return Objects.requireNonNull(mIdPackageNameMap);
696     }
697 
getIdPackageNameMap()698     private Map<Long, String> getIdPackageNameMap() {
699         return getIdPackageNameMap(Optional.empty());
700     }
701 
getAppInfo(String packageName)702     private AppInfoInternal getAppInfo(String packageName) throws NameNotFoundException {
703         PackageManager packageManager = mUserContext.getPackageManager();
704         ApplicationInfo info =
705                 packageManager.getApplicationInfo(
706                         packageName, PackageManager.ApplicationInfoFlags.of(0));
707         String appName = packageManager.getApplicationLabel(info).toString();
708         Drawable icon = packageManager.getApplicationIcon(info);
709         Bitmap bitmap = getBitmapFromDrawable(icon);
710         return new AppInfoInternal(DEFAULT_LONG, packageName, appName, bitmap, null);
711     }
712 
713     @Nullable
getIconFromPackageName(String packageName)714     private byte[] getIconFromPackageName(String packageName) {
715         PackageManager packageManager = mUserContext.getPackageManager();
716         try {
717             Drawable drawable = packageManager.getApplicationIcon(packageName);
718             Bitmap bitmap = getBitmapFromDrawable(drawable);
719             return encodeBitmap(bitmap);
720         } catch (PackageManager.NameNotFoundException e) {
721             Drawable drawable = packageManager.getDefaultActivityIcon();
722             Bitmap bitmap = getBitmapFromDrawable(drawable);
723             return encodeBitmap(bitmap);
724         }
725     }
726 
insertIfNotPresent(String packageName, AppInfoInternal appInfo)727     private synchronized void insertIfNotPresent(String packageName, AppInfoInternal appInfo) {
728         insertIfNotPresent(Optional.empty(), packageName, appInfo);
729     }
730 
731     /**
732      * Inserts appInfo if not present in the db, using the given {@link SQLiteDatabase}. If given db
733      * is null, the default will be {@link TransactionManager#getReadableDb()} for reads and {@link
734      * TransactionManager#getWritableDb()} for writes.
735      */
insertIfNotPresent( Optional<SQLiteDatabase> db, String packageName, AppInfoInternal appInfo)736     private synchronized void insertIfNotPresent(
737             Optional<SQLiteDatabase> db, String packageName, AppInfoInternal appInfo) {
738         if (getAppInfoMap(db).containsKey(packageName)) {
739             return;
740         }
741 
742         long rowId = insertAppInfo(db, packageName, appInfo);
743         appInfo.setId(rowId);
744         getAppInfoMap(db).put(packageName, appInfo);
745         getIdPackageNameMap(db).put(appInfo.getId(), packageName);
746     }
747 
insertAppInfo( Optional<SQLiteDatabase> db, String packageName, AppInfoInternal appInfo)748     private long insertAppInfo(
749             Optional<SQLiteDatabase> db, String packageName, AppInfoInternal appInfo) {
750         UpsertTableRequest upsertRequest =
751                 new UpsertTableRequest(
752                         TABLE_NAME, getContentValues(packageName, appInfo), UNIQUE_COLUMN_INFO);
753         return db.map(sqLiteDatabase -> mTransactionManager.insert(sqLiteDatabase, upsertRequest))
754                 .orElseGet(() -> mTransactionManager.insert(upsertRequest));
755     }
756 
updateIfPresent(String packageName, AppInfoInternal appInfoInternal)757     private synchronized void updateIfPresent(String packageName, AppInfoInternal appInfoInternal) {
758         if (!getAppInfoMap().containsKey(packageName)) {
759             return;
760         }
761 
762         UpsertTableRequest upsertTableRequest =
763                 new UpsertTableRequest(
764                         TABLE_NAME,
765                         getContentValues(packageName, appInfoInternal),
766                         UNIQUE_COLUMN_INFO);
767 
768         mTransactionManager.update(upsertTableRequest);
769         getAppInfoMap().put(packageName, appInfoInternal);
770     }
771 
getContentValues(String packageName, AppInfoInternal appInfo)772     private ContentValues getContentValues(String packageName, AppInfoInternal appInfo) {
773         ContentValues contentValues = new ContentValues();
774         contentValues.put(PACKAGE_COLUMN_NAME, packageName);
775         contentValues.put(APPLICATION_COLUMN_NAME, appInfo.getName());
776         contentValues.put(APP_ICON_COLUMN_NAME, encodeBitmap(appInfo.getIcon()));
777         String recordTypesUsedAsString = null;
778         // Since a list of recordTypeIds cannot be saved directly in the database, record types IDs
779         // are concatenated using ',' and are saved as a string.
780         if (appInfo.getRecordTypesUsed() != null) {
781             recordTypesUsedAsString =
782                     appInfo.getRecordTypesUsed().stream()
783                             .map(String::valueOf)
784                             .collect(Collectors.joining(","));
785         }
786         contentValues.put(RECORD_TYPES_USED_COLUMN_NAME, recordTypesUsedAsString);
787 
788         return contentValues;
789     }
790 
791     /**
792      * This implementation should return the column names with which the table should be created.
793      *
794      * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table
795      * already exists on the device
796      *
797      * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS
798      */
getColumnInfo()799     private static List<Pair<String, String>> getColumnInfo() {
800         ArrayList<Pair<String, String>> columnInfo = new ArrayList<>();
801         columnInfo.add(new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY));
802         columnInfo.add(new Pair<>(PACKAGE_COLUMN_NAME, TEXT_NOT_NULL_UNIQUE));
803         columnInfo.add(new Pair<>(APPLICATION_COLUMN_NAME, TEXT_NULL));
804         columnInfo.add(new Pair<>(APP_ICON_COLUMN_NAME, BLOB));
805         columnInfo.add(new Pair<>(RECORD_TYPES_USED_COLUMN_NAME, TEXT_NULL));
806 
807         return columnInfo;
808     }
809 
810     @Nullable
encodeBitmap(@ullable Bitmap bitmap)811     private static byte[] encodeBitmap(@Nullable Bitmap bitmap) {
812         if (bitmap == null) {
813             return null;
814         }
815 
816         try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
817             bitmap.compress(Bitmap.CompressFormat.PNG, COMPRESS_FACTOR, stream);
818             return stream.toByteArray();
819         } catch (IOException exception) {
820             throw new IllegalArgumentException(exception);
821         }
822     }
823 
824     @Nullable
decodeBitmap(@ullable byte[] bytes)825     private static Bitmap decodeBitmap(@Nullable byte[] bytes) {
826         return bytes != null ? BitmapFactory.decodeByteArray(bytes, 0, bytes.length) : null;
827     }
828 
getBitmapFromDrawable(Drawable drawable)829     private static Bitmap getBitmapFromDrawable(Drawable drawable) {
830         final Bitmap bmp =
831                 Bitmap.createBitmap(
832                         drawable.getIntrinsicWidth(),
833                         drawable.getIntrinsicHeight(),
834                         Bitmap.Config.ARGB_8888);
835         final Canvas canvas = new Canvas(bmp);
836         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
837         drawable.draw(canvas);
838         return bmp;
839     }
840 
convertPackageIdsToPackageName(Set<Long> packageIds)841     private Set<String> convertPackageIdsToPackageName(Set<Long> packageIds) {
842         Set<String> packageNames = new HashSet<>();
843         for (Long packageId : packageIds) {
844             try {
845                 String packageName = getPackageName(packageId);
846                 if (!packageName.isEmpty()) {
847                     packageNames.add(packageName);
848                 }
849             } catch (PackageManager.NameNotFoundException e) {
850                 Slog.e(TAG, "Package name not found for the given id", e);
851             }
852         }
853         return packageNames;
854     }
855 
856     /**
857      * @return map of distinct packageNames corresponding to the input table name after querying the
858      *     table.
859      */
getDistinctPackageIdsForRecordsTable(Set<Integer> recordTypes)860     private Map<Integer, Set<Long>> getDistinctPackageIdsForRecordsTable(Set<Integer> recordTypes)
861             throws SQLiteException {
862         return mTransactionManager.runWithoutTransaction(
863                 db -> {
864                     HashMap<Integer, Set<Long>> recordTypeToPackageIdsMap = new HashMap<>();
865                     for (Integer recordType : recordTypes) {
866                         RecordHelper<?> recordHelper =
867                                 mInternalHealthConnectMappings.getRecordHelper(recordType);
868                         HashSet<Long> packageIds = new HashSet<>();
869                         try (Cursor cursorForDistinctPackageNames =
870                                 db.rawQuery(
871                                         /* sql query */
872                                         recordHelper
873                                                 .getReadTableRequestWithDistinctAppInfoIds()
874                                                 .getReadCommand(),
875                                         /* selectionArgs */ null)) {
876                             if (cursorForDistinctPackageNames.getCount() > 0) {
877                                 while (cursorForDistinctPackageNames.moveToNext()) {
878                                     packageIds.add(
879                                             cursorForDistinctPackageNames.getLong(
880                                                     cursorForDistinctPackageNames.getColumnIndex(
881                                                             APP_INFO_ID_COLUMN_NAME)));
882                                 }
883                             }
884                         }
885                         recordTypeToPackageIdsMap.put(recordType, packageIds);
886                     }
887                     return recordTypeToPackageIdsMap;
888                 });
889     }
890 }
891