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.request.UpsertTableRequest.TYPE_STRING; 23 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB; 24 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY; 25 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL_UNIQUE; 26 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL; 27 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorBlob; 28 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong; 29 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString; 30 31 import static java.util.Objects.requireNonNull; 32 33 import android.annotation.NonNull; 34 import android.annotation.Nullable; 35 import android.annotation.SuppressLint; 36 import android.content.ContentValues; 37 import android.content.Context; 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.graphics.Bitmap; 45 import android.graphics.BitmapFactory; 46 import android.graphics.Canvas; 47 import android.graphics.drawable.Drawable; 48 import android.health.connect.Constants; 49 import android.health.connect.datatypes.AppInfo; 50 import android.health.connect.internal.datatypes.AppInfoInternal; 51 import android.health.connect.internal.datatypes.RecordInternal; 52 import android.health.connect.internal.datatypes.utils.RecordMapper; 53 import android.util.Log; 54 import android.util.Pair; 55 import android.util.Slog; 56 57 import com.android.server.healthconnect.storage.TransactionManager; 58 import com.android.server.healthconnect.storage.request.CreateTableRequest; 59 import com.android.server.healthconnect.storage.request.DeleteTableRequest; 60 import com.android.server.healthconnect.storage.request.ReadTableRequest; 61 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 62 import com.android.server.healthconnect.storage.utils.WhereClauses; 63 64 import java.io.ByteArrayOutputStream; 65 import java.io.IOException; 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.Collections; 69 import java.util.HashMap; 70 import java.util.HashSet; 71 import java.util.List; 72 import java.util.Map; 73 import java.util.Objects; 74 import java.util.Set; 75 import java.util.concurrent.ConcurrentHashMap; 76 import java.util.stream.Collectors; 77 78 /** 79 * A class to help with the DB transaction for storing Application Info. {@link AppInfoHelper} acts 80 * as a layer b/w the application_igenfo_table stored in the DB and helps perform insert and read 81 * operations on the table 82 * 83 * @hide 84 */ 85 public final class AppInfoHelper { 86 public static final String TABLE_NAME = "application_info_table"; 87 public static final String APPLICATION_COLUMN_NAME = "app_name"; 88 public static final String PACKAGE_COLUMN_NAME = "package_name"; 89 public static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO = 90 Collections.singletonList(new Pair<>(PACKAGE_COLUMN_NAME, TYPE_STRING)); 91 public static final String APP_ICON_COLUMN_NAME = "app_icon"; 92 private static final String TAG = "HealthConnectAppInfoHelper"; 93 private static final String RECORD_TYPES_USED_COLUMN_NAME = "record_types_used"; 94 private static final int COMPRESS_FACTOR = 100; 95 private static volatile AppInfoHelper sAppInfoHelper; 96 97 /** 98 * Map to store appInfoId -> packageName mapping for populating record for read 99 * 100 * <p>TO HAVE THREAD SAFETY DON'T USE THESE VARIABLES DIRECTLY, INSTEAD USE ITS GETTER 101 */ 102 private volatile ConcurrentHashMap<Long, String> mIdPackageNameMap; 103 /** 104 * Map to store application package-name -> AppInfo mapping (such as packageName -> appName, 105 * icon, rowId in the DB etc.) 106 * 107 * <p>TO HAVE THREAD SAFETY DON'T USE THESE VARIABLES DIRECTLY, INSTEAD USE ITS GETTER 108 */ 109 private volatile ConcurrentHashMap<String, AppInfoInternal> mAppInfoMap; 110 AppInfoHelper()111 private AppInfoHelper() {} 112 113 /** Deletes all entries from the database and clears the cache. */ clearData(TransactionManager transactionManager)114 public synchronized void clearData(TransactionManager transactionManager) { 115 transactionManager.delete(new DeleteTableRequest(TABLE_NAME)); 116 clearCache(); 117 } 118 clearCache()119 public synchronized void clearCache() { 120 mAppInfoMap = null; 121 mIdPackageNameMap = null; 122 } 123 124 /** 125 * Returns a requests representing the tables that should be created corresponding to this 126 * helper 127 */ 128 @NonNull getCreateTableRequest()129 public CreateTableRequest getCreateTableRequest() { 130 return new CreateTableRequest(TABLE_NAME, getColumnInfo()); 131 } 132 133 /** Populates record with appInfoId */ populateAppInfoId( @onNull RecordInternal<?> record, @NonNull Context context, boolean requireAllFields)134 public void populateAppInfoId( 135 @NonNull RecordInternal<?> record, @NonNull Context context, boolean requireAllFields) { 136 final String packageName = requireNonNull(record.getPackageName()); 137 AppInfoInternal appInfo = getAppInfoMap().get(packageName); 138 139 if (appInfo == null) { 140 try { 141 appInfo = getAppInfo(packageName, context); 142 } catch (NameNotFoundException e) { 143 if (requireAllFields) { 144 throw new IllegalArgumentException("Could not find package info", e); 145 } 146 147 appInfo = 148 new AppInfoInternal( 149 DEFAULT_LONG, packageName, record.getAppName(), null, null); 150 } 151 152 insertIfNotPresent(packageName, appInfo); 153 } 154 155 record.setAppInfoId(appInfo.getId()); 156 record.setPackageName(appInfo.getPackageName()); 157 } 158 159 /** 160 * Inserts or replaces (based on the passed param onlyUpdate) the application info of the 161 * specified {@code packageName} with the specified {@code name} and {@code icon}, only if the 162 * corresponding application is not currently installed. 163 * 164 * <p>If onlyUpdate is true then only replace the exiting AppInfo; no new insertion. If 165 * onlyUpdate is false then only insert a new AppInfo entry; no replacement. 166 */ addOrUpdateAppInfoIfNotInstalled( @onNull Context context, @NonNull String packageName, @Nullable String name, @Nullable byte[] icon, boolean onlyUpdate)167 public void addOrUpdateAppInfoIfNotInstalled( 168 @NonNull Context context, 169 @NonNull String packageName, 170 @Nullable String name, 171 @Nullable byte[] icon, 172 boolean onlyUpdate) { 173 if (!isAppInstalled(context, packageName)) { 174 // using pre-existing value of recordTypesUsed. 175 var recordTypesUsed = 176 containsAppInfo(packageName) 177 ? mAppInfoMap.get(packageName).getRecordTypesUsed() 178 : null; 179 AppInfoInternal appInfoInternal = 180 new AppInfoInternal( 181 DEFAULT_LONG, packageName, name, decodeBitmap(icon), recordTypesUsed); 182 if (onlyUpdate) { 183 updateIfPresent(packageName, appInfoInternal); 184 } else { 185 insertIfNotPresent(packageName, appInfoInternal); 186 } 187 } 188 } 189 isAppInstalled(@onNull Context context, @NonNull String packageName)190 private boolean isAppInstalled(@NonNull Context context, @NonNull String packageName) { 191 try { 192 context.getPackageManager().getApplicationInfo(packageName, ApplicationInfoFlags.of(0)); 193 return true; 194 } catch (NameNotFoundException e) { 195 return false; 196 } 197 } 198 199 /** 200 * Populates record with package name 201 * 202 * @param appInfoId rowId from {@code application_info_table } 203 * @param record The record to be populated with package name 204 * @param idPackageNameMap the map from which to get the package name 205 */ populateRecordWithValue( long appInfoId, @NonNull RecordInternal<?> record, Map<Long, String> idPackageNameMap)206 public void populateRecordWithValue( 207 long appInfoId, @NonNull RecordInternal<?> record, Map<Long, String> idPackageNameMap) { 208 if (idPackageNameMap != null) { 209 record.setPackageName(idPackageNameMap.get(appInfoId)); 210 return; 211 } 212 record.setPackageName(getIdPackageNameMap().get(appInfoId)); 213 } 214 215 /** 216 * Called when a db update happens to make any required changes in appInfoHelper respecting 217 * version upgrade. 218 */ onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db)219 public void onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db) {} 220 221 /** 222 * @return id of {@code packageName} or {@link Constants#DEFAULT_LONG} if the id is not found 223 */ getAppInfoId(String packageName)224 public long getAppInfoId(String packageName) { 225 AppInfoInternal appInfo = getAppInfoMap().getOrDefault(packageName, null); 226 227 if (appInfo == null) { 228 return DEFAULT_LONG; 229 } 230 231 return appInfo.getId(); 232 } 233 containsAppInfo(String packageName)234 private boolean containsAppInfo(String packageName) { 235 return getAppInfoMap().containsKey(packageName); 236 } 237 238 /** 239 * @param packageNames List of package names 240 * @return A list of appinfo ids from the application_info_table. 241 */ getAppInfoIds(List<String> packageNames)242 public List<Long> getAppInfoIds(List<String> packageNames) { 243 if (DEBUG) { 244 Slog.d(TAG, "App info map: " + mAppInfoMap); 245 } 246 if (packageNames == null || packageNames.isEmpty()) { 247 return Collections.emptyList(); 248 } 249 250 List<Long> result = new ArrayList<>(packageNames.size()); 251 packageNames.forEach(packageName -> result.add(getAppInfoId(packageName))); 252 253 return result; 254 } 255 256 @NonNull getPackageName(long packageId)257 public String getPackageName(long packageId) { 258 return getIdPackageNameMap().get(packageId); 259 } 260 261 @NonNull getPackageNames(List<Long> packageIds)262 public List<String> getPackageNames(List<Long> packageIds) { 263 if (packageIds == null || packageIds.isEmpty()) { 264 return Collections.emptyList(); 265 } 266 267 List<String> packageNames = new ArrayList<>(); 268 packageIds.forEach( 269 (packageId) -> { 270 String packageName = getIdPackageNameMap().get(packageId); 271 requireNonNull(packageName); 272 273 packageNames.add(packageName); 274 }); 275 276 return packageNames; 277 } 278 279 /** Returns a list of AppInfo objects which are contributing data to some recordType. */ getApplicationInfosWithRecordTypes()280 public List<AppInfo> getApplicationInfosWithRecordTypes() { 281 return getAppInfoMap().values().stream() 282 .filter( 283 (appInfo) -> 284 (appInfo.getRecordTypesUsed() != null 285 && !appInfo.getRecordTypesUsed().isEmpty())) 286 .map(AppInfoInternal::toExternal) 287 .collect(Collectors.toList()); 288 } 289 290 /** Returns AppInfo id for the provided {@code packageName}, creating it if needed. */ getOrInsertAppInfoId(@onNull String packageName, @NonNull Context context)291 public long getOrInsertAppInfoId(@NonNull String packageName, @NonNull Context context) { 292 AppInfoInternal appInfoInternal = getAppInfoMap().get(packageName); 293 294 if (appInfoInternal == null) { 295 try { 296 appInfoInternal = getAppInfo(packageName, context); 297 } catch (NameNotFoundException e) { 298 throw new IllegalArgumentException("Could not find package info for package", e); 299 } 300 301 insertIfNotPresent(packageName, appInfoInternal); 302 } 303 304 return appInfoInternal.getId(); 305 } 306 populateAppInfoMap()307 private synchronized void populateAppInfoMap() { 308 if (mAppInfoMap != null) { 309 return; 310 } 311 ConcurrentHashMap<String, AppInfoInternal> appInfoMap = new ConcurrentHashMap<>(); 312 ConcurrentHashMap<Long, String> idPackageNameMap = new ConcurrentHashMap<>(); 313 final TransactionManager transactionManager = TransactionManager.getInitialisedInstance(); 314 try (Cursor cursor = transactionManager.read(new ReadTableRequest(TABLE_NAME))) { 315 while (cursor.moveToNext()) { 316 long rowId = getCursorLong(cursor, RecordHelper.PRIMARY_COLUMN_NAME); 317 String packageName = getCursorString(cursor, PACKAGE_COLUMN_NAME); 318 String appName = getCursorString(cursor, APPLICATION_COLUMN_NAME); 319 byte[] icon = getCursorBlob(cursor, APP_ICON_COLUMN_NAME); 320 Bitmap bitmap = decodeBitmap(icon); 321 String recordTypesUsed = getCursorString(cursor, RECORD_TYPES_USED_COLUMN_NAME); 322 323 Set<Integer> recordTypesListAsSet = getRecordTypesAsSet(recordTypesUsed); 324 325 appInfoMap.put( 326 packageName, 327 new AppInfoInternal( 328 rowId, packageName, appName, bitmap, recordTypesListAsSet)); 329 idPackageNameMap.put(rowId, packageName); 330 } 331 } 332 mAppInfoMap = appInfoMap; 333 mIdPackageNameMap = idPackageNameMap; 334 } 335 336 @Nullable getRecordTypesAsSet(String recordTypesUsed)337 private Set<Integer> getRecordTypesAsSet(String recordTypesUsed) { 338 if (recordTypesUsed != null && !recordTypesUsed.isEmpty()) { 339 return Arrays.stream(recordTypesUsed.split(",")) 340 .map(Integer::parseInt) 341 .collect(Collectors.toSet()); 342 } 343 return null; 344 } 345 346 /** 347 * Updates recordTypesUsed for the {@code packageName} in app info table. 348 * 349 * <p><b>NOTE:</b> This method should only be used for insert operation on recordType tables. 350 * Should not be called elsewhere. 351 * 352 * <p>see {@link AppInfoHelper#syncAppInfoMapRecordTypesUsed(Map)}} for updating this table 353 * during delete operations on recordTypes. 354 * 355 * @param recordTypes The record types that needs to be inserted. 356 * @param packageName The package for which the records need to be inserted. 357 */ 358 @SuppressLint("LongLogTag") updateAppInfoRecordTypesUsedOnInsert( Set<Integer> recordTypes, String packageName)359 public synchronized void updateAppInfoRecordTypesUsedOnInsert( 360 Set<Integer> recordTypes, String packageName) { 361 AppInfoInternal appInfo = getAppInfoMap().get(packageName); 362 if (appInfo == null) { 363 Log.e( 364 TAG, 365 "AppInfo for the current package: " 366 + packageName 367 + " does not exist. " 368 + "Hence recordTypesUsed is not getting updated."); 369 370 return; 371 } 372 373 if (recordTypes == null || recordTypes.isEmpty()) { 374 return; 375 } 376 Set<Integer> updatedRecordTypes = new HashSet<>(recordTypes); 377 if (appInfo.getRecordTypesUsed() != null) { 378 updatedRecordTypes.addAll(appInfo.getRecordTypesUsed()); 379 } 380 if (!updatedRecordTypes.equals(appInfo.getRecordTypesUsed())) { 381 updateAppInfoRecordTypesUsedSync(packageName, appInfo, updatedRecordTypes); 382 } 383 } 384 385 /** 386 * Updates recordTypesUsed by for all packages in app info table. 387 * 388 * <p><b>NOTE:</b> This method should only be used for delete operation on recordType tables. 389 * Should not be called elsewhere. 390 * 391 * <p>Use this method to update the table for passed recordTypes, not passing any record will 392 * update all recordTypes. 393 * 394 * <p>see {@link AppInfoHelper#updateAppInfoRecordTypesUsedOnInsert(Set, String)} for updating 395 * this table during insert operations on recordTypes. 396 */ syncAppInfoRecordTypesUsed()397 public synchronized void syncAppInfoRecordTypesUsed() { 398 syncAppInfoRecordTypesUsed(null); 399 } 400 401 /** 402 * Updates recordTypesUsed by for all packages in app info table. 403 * 404 * <p><b>NOTE:</b> This method should only be used for delete operation on recordType tables. 405 * Should not be called elsewhere. 406 * 407 * <p>Use this method to update the table for passed {@code recordTypesToBeSynced}, not passing 408 * any record will update all recordTypes. 409 * 410 * <p>see {@link AppInfoHelper#updateAppInfoRecordTypesUsedOnInsert(Set, String)} for updating 411 * this table during insert operations on recordTypes. 412 */ syncAppInfoRecordTypesUsed( @ullable Set<Integer> recordTypesToBeSynced)413 public synchronized void syncAppInfoRecordTypesUsed( 414 @Nullable Set<Integer> recordTypesToBeSynced) { 415 Set<Integer> recordTypesToBeUpdated = 416 Objects.requireNonNullElseGet( 417 recordTypesToBeSynced, 418 () -> 419 RecordMapper.getInstance() 420 .getRecordIdToExternalRecordClassMap() 421 .keySet()); 422 423 HashMap<Integer, HashSet<String>> recordTypeToContributingPackagesMap = 424 TransactionManager.getInitialisedInstance() 425 .getDistinctPackageNamesForRecordsTable(recordTypesToBeUpdated); 426 427 if (recordTypesToBeSynced == null) { 428 syncAppInfoMapRecordTypesUsed(recordTypeToContributingPackagesMap); 429 } else { 430 getAppInfoMap() 431 .keySet() 432 .forEach( 433 (packageName) -> { 434 deleteRecordTypesForPackagesIfRequiredInternal( 435 recordTypesToBeUpdated, 436 recordTypeToContributingPackagesMap, 437 packageName); 438 }); 439 } 440 } 441 442 /** 443 * This method updates recordTypesUsed for all packages and hence is a heavy operation. This 444 * method is used during AutoDeleteService and is run once per day. 445 */ 446 @SuppressLint("LongLogTag") syncAppInfoMapRecordTypesUsed( @onNull Map<Integer, HashSet<String>> recordTypeToContributingPackagesMap)447 private synchronized void syncAppInfoMapRecordTypesUsed( 448 @NonNull Map<Integer, HashSet<String>> recordTypeToContributingPackagesMap) { 449 HashMap<String, List<Integer>> packageToRecordTypesMap = 450 getPackageToRecordTypesMap(recordTypeToContributingPackagesMap); 451 getAppInfoMap() 452 .forEach( 453 (packageName, appInfo) -> { 454 if (packageToRecordTypesMap.containsKey(packageName)) { 455 updateAppInfoRecordTypesUsedSync( 456 packageName, 457 appInfo, 458 new HashSet<>(packageToRecordTypesMap.get(packageName))); 459 } else { 460 updateAppInfoRecordTypesUsedSync( 461 packageName, appInfo, /* recordTypesUsed */ null); 462 } 463 if (DEBUG) { 464 Log.d( 465 TAG, 466 "Syncing packages and corresponding recordTypesUsed for" 467 + " package : " 468 + packageName 469 + ", recordTypesUsed : " 470 + appInfo.getRecordTypesUsed()); 471 } 472 }); 473 } 474 getPackageToRecordTypesMap( @onNull Map<Integer, HashSet<String>> recordTypeToContributingPackagesMap)475 private HashMap<String, List<Integer>> getPackageToRecordTypesMap( 476 @NonNull Map<Integer, HashSet<String>> recordTypeToContributingPackagesMap) { 477 HashMap<String, List<Integer>> packageToRecordTypesMap = new HashMap<>(); 478 recordTypeToContributingPackagesMap.forEach( 479 (recordType, packageList) -> { 480 packageList.forEach( 481 (packageName) -> { 482 if (packageToRecordTypesMap.containsKey(packageName)) { 483 packageToRecordTypesMap.get(packageName).add(recordType); 484 } else { 485 packageToRecordTypesMap.put( 486 packageName, 487 new ArrayList<>() { 488 { 489 add(recordType); 490 } 491 }); 492 } 493 }); 494 }); 495 return packageToRecordTypesMap; 496 } 497 498 /** 499 * Checks and deletes record types in app info table for which the package is no longer 500 * contributing data. This is done after delete records operation has been performed. 501 */ 502 @SuppressLint("LongLogTag") deleteRecordTypesForPackagesIfRequiredInternal( Set<Integer> recordTypesToBeDeleted, HashMap<Integer, HashSet<String>> currentRecordTypePackageMap, String packageName)503 private synchronized void deleteRecordTypesForPackagesIfRequiredInternal( 504 Set<Integer> recordTypesToBeDeleted, 505 HashMap<Integer, HashSet<String>> currentRecordTypePackageMap, 506 String packageName) { 507 AppInfoInternal appInfo = getAppInfoMap().get(packageName); 508 if (appInfo == null) { 509 Log.e( 510 TAG, 511 "AppInfo for the current package: " 512 + packageName 513 + " does not exist. " 514 + "Hence recordTypesUsed is not getting updated."); 515 516 return; 517 } 518 if (appInfo.getRecordTypesUsed() == null || appInfo.getRecordTypesUsed().isEmpty()) { 519 // return since this package is not contributing to any recordType and hence there 520 // is nothing to delete. 521 return; 522 } 523 Set<Integer> updatedRecordTypesUsed = new HashSet<>(appInfo.getRecordTypesUsed()); 524 for (Integer recordType : recordTypesToBeDeleted) { 525 // get the distinct packages used by the record after the deletion process, check if 526 // the recordType does not have the current package then remove record type from 527 // the package's app info record. 528 if (!currentRecordTypePackageMap.get(recordType).contains(packageName)) { 529 updatedRecordTypesUsed.remove(recordType); 530 } 531 } 532 if (updatedRecordTypesUsed.equals(appInfo.getRecordTypesUsed())) { 533 return; 534 } 535 if (updatedRecordTypesUsed.isEmpty()) { 536 updatedRecordTypesUsed = null; 537 } 538 updateAppInfoRecordTypesUsedSync(packageName, appInfo, updatedRecordTypesUsed); 539 } 540 541 @SuppressLint("LongLogTag") updateAppInfoRecordTypesUsedSync( @onNull String packageName, @NonNull AppInfoInternal appInfo, Set<Integer> recordTypesUsed)542 private synchronized void updateAppInfoRecordTypesUsedSync( 543 @NonNull String packageName, 544 @NonNull AppInfoInternal appInfo, 545 Set<Integer> recordTypesUsed) { 546 appInfo.setRecordTypesUsed(recordTypesUsed); 547 // create upsert table request to modify app info table, keyed by packages name. 548 WhereClauses whereClauseForAppInfoTableUpdate = new WhereClauses(); 549 whereClauseForAppInfoTableUpdate.addWhereEqualsClause( 550 PACKAGE_COLUMN_NAME, appInfo.getPackageName()); 551 UpsertTableRequest upsertRequestForAppInfoUpdate = 552 new UpsertTableRequest( 553 TABLE_NAME, getContentValues(packageName, appInfo), UNIQUE_COLUMN_INFO); 554 TransactionManager.getInitialisedInstance().update(upsertRequestForAppInfoUpdate); 555 556 // update locally stored maps to keep data in sync. 557 getAppInfoMap().put(packageName, appInfo); 558 getIdPackageNameMap().put(appInfo.getId(), packageName); 559 if (DEBUG) { 560 Log.d( 561 TAG, 562 "Updated app info table. PackageName : " 563 + packageName 564 + " , RecordTypesUsed : " 565 + appInfo.getRecordTypesUsed() 566 + "."); 567 } 568 } 569 570 /** Returns a map for recordTypes and their contributing packages. */ getRecordTypesToContributingPackagesMap()571 public Map<Integer, Set<String>> getRecordTypesToContributingPackagesMap() { 572 Map<Integer, Set<String>> recordTypeContributingPackagesMap = new HashMap<>(); 573 Map<String, AppInfoInternal> appInfoMap = getAppInfoMap(); 574 appInfoMap.forEach( 575 (packageName, appInfo) -> { 576 Set<Integer> recordTypesUsed = appInfo.getRecordTypesUsed(); 577 if (recordTypesUsed != null) { 578 recordTypesUsed.forEach( 579 (recordType) -> { 580 if (recordTypeContributingPackagesMap.containsKey(recordType)) { 581 recordTypeContributingPackagesMap 582 .get(recordType) 583 .add(packageName); 584 } else { 585 recordTypeContributingPackagesMap.put( 586 recordType, 587 new HashSet<>(Collections.singleton(packageName))); 588 } 589 }); 590 } 591 }); 592 return recordTypeContributingPackagesMap; 593 } 594 getAppInfoMap()595 private Map<String, AppInfoInternal> getAppInfoMap() { 596 if (Objects.isNull(mAppInfoMap)) { 597 populateAppInfoMap(); 598 } 599 600 return mAppInfoMap; 601 } 602 getIdPackageNameMap()603 private Map<Long, String> getIdPackageNameMap() { 604 if (mIdPackageNameMap == null) { 605 populateAppInfoMap(); 606 } 607 608 return mIdPackageNameMap; 609 } 610 getAppInfo(@onNull String packageName, @NonNull Context context)611 private AppInfoInternal getAppInfo(@NonNull String packageName, @NonNull Context context) 612 throws NameNotFoundException { 613 PackageManager packageManager = context.getPackageManager(); 614 ApplicationInfo info = 615 packageManager.getApplicationInfo( 616 packageName, PackageManager.ApplicationInfoFlags.of(0)); 617 String appName = packageManager.getApplicationLabel(info).toString(); 618 Drawable icon = packageManager.getApplicationIcon(info); 619 Bitmap bitmap = getBitmapFromDrawable(icon); 620 return new AppInfoInternal(DEFAULT_LONG, packageName, appName, bitmap, null); 621 } 622 insertIfNotPresent( @onNull String packageName, @NonNull AppInfoInternal appInfo)623 private synchronized void insertIfNotPresent( 624 @NonNull String packageName, @NonNull AppInfoInternal appInfo) { 625 if (getAppInfoMap().containsKey(packageName)) { 626 return; 627 } 628 629 long rowId = 630 TransactionManager.getInitialisedInstance() 631 .insert( 632 new UpsertTableRequest( 633 TABLE_NAME, 634 getContentValues(packageName, appInfo), 635 UNIQUE_COLUMN_INFO)); 636 appInfo.setId(rowId); 637 getAppInfoMap().put(packageName, appInfo); 638 getIdPackageNameMap().put(appInfo.getId(), packageName); 639 } 640 updateIfPresent(String packageName, AppInfoInternal appInfoInternal)641 private synchronized void updateIfPresent(String packageName, AppInfoInternal appInfoInternal) { 642 if (!getAppInfoMap().containsKey(packageName)) { 643 return; 644 } 645 646 UpsertTableRequest upsertTableRequest = 647 new UpsertTableRequest( 648 TABLE_NAME, 649 getContentValues(packageName, appInfoInternal), 650 UNIQUE_COLUMN_INFO); 651 652 TransactionManager.getInitialisedInstance().updateTable(upsertTableRequest); 653 getAppInfoMap().put(packageName, appInfoInternal); 654 } 655 656 @NonNull getContentValues(String packageName, AppInfoInternal appInfo)657 private ContentValues getContentValues(String packageName, AppInfoInternal appInfo) { 658 ContentValues contentValues = new ContentValues(); 659 contentValues.put(PACKAGE_COLUMN_NAME, packageName); 660 contentValues.put(APPLICATION_COLUMN_NAME, appInfo.getName()); 661 contentValues.put(APP_ICON_COLUMN_NAME, encodeBitmap(appInfo.getIcon())); 662 String recordTypesUsedAsString = null; 663 // Since a list of recordTypeIds cannot be saved directly in the database, record types IDs 664 // are concatenated using ',' and are saved as a string. 665 if (appInfo.getRecordTypesUsed() != null) { 666 recordTypesUsedAsString = 667 appInfo.getRecordTypesUsed().stream() 668 .map(String::valueOf) 669 .collect(Collectors.joining(",")); 670 } 671 contentValues.put(RECORD_TYPES_USED_COLUMN_NAME, recordTypesUsedAsString); 672 673 return contentValues; 674 } 675 676 /** 677 * This implementation should return the column names with which the table should be created. 678 * 679 * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table 680 * already exists on the device 681 * 682 * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS 683 */ 684 @NonNull getColumnInfo()685 private List<Pair<String, String>> getColumnInfo() { 686 ArrayList<Pair<String, String>> columnInfo = new ArrayList<>(); 687 columnInfo.add(new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY)); 688 columnInfo.add(new Pair<>(PACKAGE_COLUMN_NAME, TEXT_NOT_NULL_UNIQUE)); 689 columnInfo.add(new Pair<>(APPLICATION_COLUMN_NAME, TEXT_NULL)); 690 columnInfo.add(new Pair<>(APP_ICON_COLUMN_NAME, BLOB)); 691 columnInfo.add(new Pair<>(RECORD_TYPES_USED_COLUMN_NAME, TEXT_NULL)); 692 693 return columnInfo; 694 } 695 getInstance()696 public static synchronized AppInfoHelper getInstance() { 697 if (sAppInfoHelper == null) { 698 sAppInfoHelper = new AppInfoHelper(); 699 } 700 701 return sAppInfoHelper; 702 } 703 704 @Nullable encodeBitmap(@ullable Bitmap bitmap)705 private static byte[] encodeBitmap(@Nullable Bitmap bitmap) { 706 if (bitmap == null) { 707 return null; 708 } 709 710 try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { 711 bitmap.compress(Bitmap.CompressFormat.PNG, COMPRESS_FACTOR, stream); 712 return stream.toByteArray(); 713 } catch (IOException exception) { 714 throw new IllegalArgumentException(exception); 715 } 716 } 717 718 @Nullable decodeBitmap(@ullable byte[] bytes)719 private static Bitmap decodeBitmap(@Nullable byte[] bytes) { 720 return bytes != null ? BitmapFactory.decodeByteArray(bytes, 0, bytes.length) : null; 721 } 722 723 @NonNull getBitmapFromDrawable(@onNull Drawable drawable)724 private static Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) { 725 final Bitmap bmp = 726 Bitmap.createBitmap( 727 drawable.getIntrinsicWidth(), 728 drawable.getIntrinsicHeight(), 729 Bitmap.Config.ARGB_8888); 730 final Canvas canvas = new Canvas(bmp); 731 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 732 drawable.draw(canvas); 733 return bmp; 734 } 735 } 736