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