1 /* 2 * Copyright (C) 2024 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.appsearch.appsindexer; 18 19 import android.annotation.NonNull; 20 import android.annotation.WorkerThread; 21 import android.app.appsearch.AppSearchBatchResult; 22 import android.app.appsearch.AppSearchResult; 23 import android.app.appsearch.AppSearchSchema; 24 import android.app.appsearch.GenericDocument; 25 import android.app.appsearch.PackageIdentifier; 26 import android.app.appsearch.exceptions.AppSearchException; 27 import android.content.Context; 28 import android.content.pm.PackageInfo; 29 import android.content.pm.PackageManager; 30 import android.os.SystemClock; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 import android.util.Log; 34 import android.util.Pair; 35 36 import com.android.appsearch.flags.Flags; 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionDocument; 39 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata; 40 import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication; 41 42 import java.io.Closeable; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.Set; 49 50 /** 51 * Interactions with PackageManager and AppSearch. 52 * 53 * <p>This class is NOT thread-safe. 54 * 55 * @hide 56 */ 57 public final class AppsIndexerImpl implements Closeable { 58 static final String TAG = "AppSearchAppsIndexerImpl"; 59 60 private final Context mContext; 61 private final AppSearchHelper mAppSearchHelper; 62 private final AppsIndexerConfig mAppsIndexerConfig; 63 AppsIndexerImpl(@onNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig)64 public AppsIndexerImpl(@NonNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig) 65 throws AppSearchException { 66 mContext = Objects.requireNonNull(context); 67 mAppSearchHelper = new AppSearchHelper(context); 68 mAppsIndexerConfig = Objects.requireNonNull(appsIndexerConfig); 69 } 70 71 /** 72 * Checks PackageManager and AppSearch to sync the Apps Index in AppSearch. 73 * 74 * <p>It deletes removed apps, inserts newly-added ones, and updates existing ones in the App 75 * corpus in AppSearch. 76 * 77 * @param settings contains update timestamps that help the indexer determine which apps were 78 * updated. 79 * @param appsUpdateStats contains stats about the apps indexer update. This method will 80 * populate the fields of this {@link AppsUpdateStats} structure. 81 * @param isFullUpdateRequired whether to re-index all apps irrespective of their last update 82 * timestamp. 83 */ 84 @VisibleForTesting 85 @WorkerThread doUpdateIncrementalPut( @onNull AppsIndexerSettings settings, @NonNull AppsUpdateStats appsUpdateStats, boolean isFullUpdateRequired)86 public void doUpdateIncrementalPut( 87 @NonNull AppsIndexerSettings settings, 88 @NonNull AppsUpdateStats appsUpdateStats, 89 boolean isFullUpdateRequired) 90 throws AppSearchException { 91 // TODO(b/357551503): Add metrics for app function indexing 92 Objects.requireNonNull(settings); 93 Objects.requireNonNull(appsUpdateStats); 94 long currentTimeMillis = System.currentTimeMillis(); 95 96 // Search AppSearch for MobileApplication objects to get a "current" list of indexed apps. 97 long beforeGetTimestamp = SystemClock.elapsedRealtime(); 98 Map<String, Long> appUpdatedTimestamps = mAppSearchHelper.getAppsFromAppSearch(); 99 100 appsUpdateStats.mAppSearchGetLatencyMillis = 101 SystemClock.elapsedRealtime() - beforeGetTimestamp; 102 103 long beforePackageManagerTimestamp = SystemClock.elapsedRealtime(); 104 PackageManager packageManager = mContext.getPackageManager(); 105 Map<PackageInfo, ResolveInfos> packagesToIndex = 106 AppsUtil.getPackagesToIndex(packageManager); 107 appsUpdateStats.mPackageManagerLatencyMillis = 108 SystemClock.elapsedRealtime() - beforePackageManagerTimestamp; 109 110 List<AppFunctionDocument> functionDocumentsToAddOrUpdate = new ArrayList<>(); 111 // To remove, we only need the id 112 Set<String> functionIdsToRemove = new ArraySet<>(); 113 114 long mostRecentAppUpdatedTimestampMillis = settings.getLastAppUpdateTimestampMillis(); 115 116 // This boolean will be turned on if an app was added, an app was removed, all app 117 // functions were removed from an app, or app functions were added to an app that 118 // didn't have them previously. In all cases, we need to call setSchema to keep 119 // AppSearch in sync with PackageManager. 120 boolean addedOrRemovedFlag = false; 121 122 Set<String> packagesToIndexIdSet = new ArraySet<>(); 123 124 // Prepare a list of newly added and updated packages. Added packages will have all 125 // their app functions added to AppSearch, without checking what's in AppSearch. Updated 126 // packages will require an additional call to AppSearch to see if we need to 127 // add/update/remove individual app function documents. We don't do this for added apps as 128 // we can just assume we need to add all of them. This saves a call to AppSearch. For both 129 // added and updated packages, we parse xml. We don't check what functions are in AppSearch 130 // for removed packages, as we can just remove the entire MobileApplication + 131 // AppFunctionStaticMetadata schemas, which will in turn remove the documents. 132 Map<PackageInfo, ResolveInfos> packagesToBeAddedOrUpdated = new ArrayMap<>(); 133 Set<String> updatedPackageIds = new ArraySet<>(); 134 135 // First loop, determine the status of apps 136 for (Map.Entry<PackageInfo, ResolveInfos> packageEntry : packagesToIndex.entrySet()) { 137 PackageInfo packageInfo = packageEntry.getKey(); 138 packagesToIndexIdSet.add(packageInfo.packageName); 139 140 // Update the most recent timestamp as we iterate 141 if (packageInfo.lastUpdateTime > mostRecentAppUpdatedTimestampMillis) { 142 mostRecentAppUpdatedTimestampMillis = packageInfo.lastUpdateTime; 143 } 144 145 Long storedAppUpdateTime = appUpdatedTimestamps.get(packageInfo.packageName); 146 147 if (storedAppUpdateTime == null) { 148 // New app. 149 addedOrRemovedFlag = true; 150 appsUpdateStats.mNumberOfAppsAdded++; 151 packagesToBeAddedOrUpdated.put(packageInfo, packageEntry.getValue()); 152 } else if (packageInfo.lastUpdateTime != storedAppUpdateTime || isFullUpdateRequired) { 153 // Package last update timestamp discrepancy between AppSearch and PackageManager 154 // or app indexer code was updated. Add this to the list of updated 155 // apps so we can check what functions are indexed in AppSearch 156 appsUpdateStats.mNumberOfAppsUpdated++; 157 updatedPackageIds.add(packageInfo.packageName); 158 packagesToBeAddedOrUpdated.put(packageInfo, packagesToIndex.get(packageInfo)); 159 } else { 160 // Not updated. 161 appsUpdateStats.mNumberOfAppsUnchanged++; 162 } 163 } 164 165 // Now check for removed apps 166 for (String appPackageId : appUpdatedTimestamps.keySet()) { 167 if (!packagesToIndexIdSet.contains(appPackageId)) { 168 // App was removed, remove all it's functions. This is simple because removing the 169 // schema will remove all the functions. Do not add the app to the list of schemas 170 // to set. 171 appsUpdateStats.mNumberOfAppsRemoved++; 172 addedOrRemovedFlag = true; 173 } 174 } 175 176 Map<String, Map<String, AppSearchSchema>> dynamicAppFunctionSchemasForPackages = null; 177 if (Flags.enableAppFunctionsSchemaParser()) { 178 // TODO(b/382254638): Skip XML parsing for packages that were not updated by using 179 // AppSearchSessio#getSchema. 180 dynamicAppFunctionSchemasForPackages = 181 AppsUtil.getDynamicAppFunctionSchemasForPackages( 182 packageManager, 183 packagesToIndex, 184 mAppsIndexerConfig.getMaxAllowedAppFunctionSchemasPerPackage()); 185 } 186 187 // Parse and build all necessary AppFunctionStaticMetadata from PackageManager. 188 Map<String, Map<String, ? extends AppFunctionDocument>> 189 currentAppFunctionsForAddedUpdatedPackages = 190 AppsUtil.buildAppFunctionDocumentsIntoMap( 191 packageManager, 192 packagesToBeAddedOrUpdated, 193 /* indexerPackageName= */ mContext.getPackageName(), 194 mAppsIndexerConfig, 195 dynamicAppFunctionSchemasForPackages); 196 197 // Get all currently indexed AppFunctionStaticMetadata docs for the necessary packages. 198 Map<String, Map<String, AppFunctionDocument>> appFunctionsFromAppSearch = 199 mAppSearchHelper.getAppFunctionDocumentsFromAppSearch(updatedPackageIds); 200 201 for (Map.Entry<String, Map<String, ? extends AppFunctionDocument>> packageEntry : 202 currentAppFunctionsForAddedUpdatedPackages.entrySet()) { 203 String packageName = packageEntry.getKey(); 204 Map<String, ? extends AppFunctionDocument> currentAppFunctionsPerApp = 205 packageEntry.getValue(); 206 207 // This might be null, in the case of functions newly added to a package 208 Map<String, AppFunctionDocument> appSearchAppFunctionsPerApp = 209 appFunctionsFromAppSearch.get(packageName); 210 211 if (appSearchAppFunctionsPerApp == null && !currentAppFunctionsPerApp.isEmpty()) { 212 // Functions added to an app that didn't have them 213 functionDocumentsToAddOrUpdate.addAll(currentAppFunctionsPerApp.values()); 214 addedOrRemovedFlag = true; 215 } 216 217 if (appSearchAppFunctionsPerApp != null) { 218 if (currentAppFunctionsPerApp.isEmpty()) { 219 // All functions removed from an app that had them 220 addedOrRemovedFlag = true; 221 } else { 222 // App updated that had packages, we should check 223 comparePackageFunctionDocuments( 224 currentAppFunctionsPerApp, 225 appSearchAppFunctionsPerApp, 226 functionDocumentsToAddOrUpdate, 227 functionIdsToRemove); 228 } 229 } 230 } 231 232 try { 233 // TODO(b/382254638): Skip set schema calls if no packages have an updated schema. 234 if (dynamicAppFunctionSchemasForPackages != null) { 235 // Since dynamic schemas are enabled, we need to account for schema changes in 236 // both updated packages and newly added packages. 237 Pair<List<PackageIdentifier>, List<PackageIdentifier>> 238 mobileAppAndAppFunctionIdentifiers = 239 getPackageIdentifiers( 240 packagesToIndex, 241 currentAppFunctionsForAddedUpdatedPackages); 242 243 long beforeSetSchemaTimestamp = SystemClock.elapsedRealtime(); 244 mAppSearchHelper.setSchemasForPackages( 245 /* mobileAppPkgs= */ mobileAppAndAppFunctionIdentifiers.first, 246 /* appFunctionPkgs= */ mobileAppAndAppFunctionIdentifiers.second, 247 dynamicAppFunctionSchemasForPackages); 248 appsUpdateStats.mAppSearchSetSchemaLatencyMillis = 249 SystemClock.elapsedRealtime() - beforeSetSchemaTimestamp; 250 } else if (addedOrRemovedFlag) { 251 // This branch is executed when dynamic schemas are disabled and new packages are 252 // added or removed to keep the AppSearch schema in sync with 253 // PackageManager. 254 Pair<List<PackageIdentifier>, List<PackageIdentifier>> 255 mobileAppAndAppFunctionIdentifiers = 256 getPackageIdentifiers( 257 packagesToIndex, 258 currentAppFunctionsForAddedUpdatedPackages); 259 260 // The certificate is necessary along with the package name as it is used in 261 // visibility settings. 262 long beforeSetSchemaTimestamp = SystemClock.elapsedRealtime(); 263 mAppSearchHelper.setSchemasForPackages( 264 /* mobileAppPkgs= */ mobileAppAndAppFunctionIdentifiers.first, 265 /* appFunctionPkgs= */ mobileAppAndAppFunctionIdentifiers.second); 266 appsUpdateStats.mAppSearchSetSchemaLatencyMillis = 267 SystemClock.elapsedRealtime() - beforeSetSchemaTimestamp; 268 } 269 270 if (!packagesToBeAddedOrUpdated.isEmpty() 271 || !functionDocumentsToAddOrUpdate.isEmpty()) { 272 long beforePutTimestamp = SystemClock.elapsedRealtime(); 273 List<MobileApplication> mobileApplications = 274 AppsUtil.buildAppsFromPackageInfos( 275 packageManager, packagesToBeAddedOrUpdated); 276 277 AppSearchBatchResult<String, Void> result = 278 mAppSearchHelper.indexApps( 279 mobileApplications, functionDocumentsToAddOrUpdate); 280 if (result.isSuccess()) { 281 appsUpdateStats.mUpdateStatusCodes.add(AppSearchResult.RESULT_OK); 282 } else { 283 Collection<AppSearchResult<Void>> values = result.getAll().values(); 284 285 for (AppSearchResult<Void> putResult : values) { 286 appsUpdateStats.mUpdateStatusCodes.add(putResult.getResultCode()); 287 } 288 } 289 290 appsUpdateStats.mAppSearchPutLatencyMillis = 291 SystemClock.elapsedRealtime() - beforePutTimestamp; 292 } 293 294 if (!functionIdsToRemove.isEmpty()) { 295 AppSearchBatchResult<String, Void> result = 296 mAppSearchHelper.removeAppFunctionsById(functionIdsToRemove); 297 if (result.isSuccess()) { 298 appsUpdateStats.mUpdateStatusCodes.add(AppSearchResult.RESULT_OK); 299 } 300 } 301 302 settings.setLastAppUpdateTimestampMillis(mostRecentAppUpdatedTimestampMillis); 303 settings.setLastUpdateTimestampMillis(currentTimeMillis); 304 305 appsUpdateStats.mLastAppUpdateTimestampMillis = mostRecentAppUpdatedTimestampMillis; 306 } catch (AppSearchException e) { 307 // Reset the last update time stamp and app update timestamp so we can try again later. 308 settings.reset(); 309 appsUpdateStats.mUpdateStatusCodes.clear(); 310 appsUpdateStats.mUpdateStatusCodes.add(e.getResultCode()); 311 throw e; 312 } 313 } 314 315 /** 316 * Return a pair of lists of {@link PackageIdentifier}s, the first list representing all 317 * packages, and the second list representing packages with app functions. 318 * 319 * <p>The second list is always a subset of the first list. 320 * 321 * @param packagesToIndex a mapping of {@link PackageInfo}s with their corresponding {@link 322 * ResolveInfos} for the packages launch activity and maybe app function resolve info. 323 * @param currentAppFunctionsForAddedUpdatedPackages a mapping of package name to a map of all 324 * app functions for the packages that were either updated or added. 325 * @return a pair of lists of {@link PackageIdentifier}s, the first list representing all 326 * packages, and the second list representing packages with app functions. 327 */ getPackageIdentifiers( @onNull Map<PackageInfo, ResolveInfos> packagesToIndex, Map<String, Map<String, ? extends AppFunctionDocument>> currentAppFunctionsForAddedUpdatedPackages)328 private Pair<List<PackageIdentifier>, List<PackageIdentifier>> getPackageIdentifiers( 329 @NonNull Map<PackageInfo, ResolveInfos> packagesToIndex, 330 Map<String, Map<String, ? extends AppFunctionDocument>> 331 currentAppFunctionsForAddedUpdatedPackages) { 332 List<PackageIdentifier> packageIdentifiers = new ArrayList<>(); 333 List<PackageIdentifier> packageIdentifiersWithAppFunctions = new ArrayList<>(); 334 for (Map.Entry<PackageInfo, ResolveInfos> entry : packagesToIndex.entrySet()) { 335 // We get certificates here as getting the certificates during the previous for 336 // loop would be wasteful if we end up not needing to call set schema 337 PackageInfo packageInfo = entry.getKey(); 338 byte[] certificate = AppsUtil.getCertificate(packageInfo); 339 if (certificate == null) { 340 Log.e(TAG, "Certificate not found for package: " + packageInfo.packageName); 341 continue; 342 } 343 PackageIdentifier packageIdentifier = 344 new PackageIdentifier(packageInfo.packageName, certificate); 345 packageIdentifiers.add(packageIdentifier); 346 // Check if the package was updated and all app functions were removed. The map only 347 // contains entries for packages that updated or newly added, for packages with no 348 // change we would rely solely on presence of AppFunctionServiceInfo to decide if it's 349 // an app function package. 350 boolean appFunctionsRemoved = 351 currentAppFunctionsForAddedUpdatedPackages.containsKey(packageInfo.packageName) 352 && currentAppFunctionsForAddedUpdatedPackages 353 .get(packageInfo.packageName) 354 .isEmpty(); 355 if (entry.getValue().getAppFunctionServiceInfo() != null && !appFunctionsRemoved) { 356 packageIdentifiersWithAppFunctions.add(packageIdentifier); 357 } 358 } 359 return new Pair<>(packageIdentifiers, packageIdentifiersWithAppFunctions); 360 } 361 362 /** 363 * Compares the app function documents in PackageManager vs those in AppSearch, and updates 364 * functionDocumentsToAddOrUpdate and functionDocumentIdsToRemove accordingly. 365 * 366 * @param currentAppFunctionDocumentsPerApp the mapping of function ids to documents 367 * corresponding to what is in the apps metadata. 368 * @param appSearchAppFunctionDocumentsPerApp the mapping of function ids to documents 369 * corresponding to what is in AppSearch 370 * @param functionDocumentsToAddOrUpdate the List of {@link AppFunctionDocument} that will be 371 * sent to a put call to AppSearch 372 * @param functionDocumentIdsToRemove the set of ids that will be sent to a remove call in 373 * AppSearch 374 */ comparePackageFunctionDocuments( @onNull Map<String, ? extends AppFunctionDocument> currentAppFunctionDocumentsPerApp, @NonNull Map<String, AppFunctionDocument> appSearchAppFunctionDocumentsPerApp, @NonNull List<AppFunctionDocument> functionDocumentsToAddOrUpdate, @NonNull Set<String> functionDocumentIdsToRemove)375 private void comparePackageFunctionDocuments( 376 @NonNull Map<String, ? extends AppFunctionDocument> currentAppFunctionDocumentsPerApp, 377 @NonNull Map<String, AppFunctionDocument> appSearchAppFunctionDocumentsPerApp, 378 @NonNull List<AppFunctionDocument> functionDocumentsToAddOrUpdate, 379 @NonNull Set<String> functionDocumentIdsToRemove) { 380 Objects.requireNonNull(currentAppFunctionDocumentsPerApp); 381 Objects.requireNonNull(appSearchAppFunctionDocumentsPerApp); 382 Objects.requireNonNull(functionDocumentsToAddOrUpdate); 383 Objects.requireNonNull(functionDocumentIdsToRemove); 384 385 for (Map.Entry<String, ? extends AppFunctionDocument> currentFunctionEntry : 386 currentAppFunctionDocumentsPerApp.entrySet()) { 387 String functionId = currentFunctionEntry.getKey(); 388 AppFunctionDocument currentFunctionDocument = currentFunctionEntry.getValue(); 389 AppFunctionDocument appSearchFunctionDocument = 390 appSearchAppFunctionDocumentsPerApp.get(functionId); 391 // appSearchFunctionDocument == null means it's a new document, document inequality 392 // means 393 // updated document. Both mean we need to call put with this document. 394 if (appSearchFunctionDocument == null 395 || !areFunctionDocumentsEqual( 396 appSearchFunctionDocument, currentFunctionDocument)) { 397 functionDocumentsToAddOrUpdate.add(currentFunctionDocument); 398 } 399 } 400 401 for (Map.Entry<String, AppFunctionDocument> appSearchFunctionEntry : 402 appSearchAppFunctionDocumentsPerApp.entrySet()) { 403 if (!currentAppFunctionDocumentsPerApp.containsKey(appSearchFunctionEntry.getKey())) { 404 functionDocumentIdsToRemove.add(appSearchFunctionEntry.getValue().getId()); 405 } 406 } 407 } 408 409 /** 410 * Checks if two AppFunction documents are equal. It isn't enough to call equals. We also need 411 * to ignore creation timestamp and parent types. These are set in AppSearch, but aren't set for 412 * the "about to be indexed" docs 413 * 414 * @return true if the documents are equal, false otherwise. 415 */ areFunctionDocumentsEqual( @onNull GenericDocument document1, @NonNull GenericDocument document2)416 private boolean areFunctionDocumentsEqual( 417 @NonNull GenericDocument document1, @NonNull GenericDocument document2) { 418 Objects.requireNonNull(document1); 419 Objects.requireNonNull(document2); 420 421 document1 = clearTimestampsAndParentTypesInDocument(document1); 422 document2 = clearTimestampsAndParentTypesInDocument(document2); 423 424 return document1.equals(document2); 425 } 426 clearTimestampsAndParentTypesInDocument( @onNull GenericDocument document)427 private GenericDocument clearTimestampsAndParentTypesInDocument( 428 @NonNull GenericDocument document) { 429 GenericDocument.Builder<?> builder = 430 new GenericDocument.Builder<>(document) 431 .setCreationTimestampMillis(0) 432 // GenericDocument#PARENT_TYPES_SYNTHETIC_PROPERTY is hidden 433 .clearProperty("$$__AppSearch__parentTypes"); 434 435 for (String propertyName : document.getPropertyNames()) { 436 Object property = document.getProperty(propertyName); 437 if (property instanceof GenericDocument[] nestedDocuments) { 438 GenericDocument[] clearedNestedDocuments = 439 new GenericDocument[nestedDocuments.length]; 440 441 for (int i = 0; i < nestedDocuments.length; i++) { 442 clearedNestedDocuments[i] = 443 clearTimestampsAndParentTypesInDocument(nestedDocuments[i]); 444 } 445 446 builder.setPropertyDocument(propertyName, clearedNestedDocuments); 447 } 448 } 449 450 return builder.build(); 451 } 452 453 /** 454 * Checks PackageManager and AppSearch to sync the Apps Index in AppSearch. 455 * 456 * <p>It deletes removed apps, inserts newly-added ones, and updates existing ones in the App 457 * corpus in AppSearch. 458 * 459 * @param settings contains update timestamps that help the indexer determine which apps were 460 * updated. 461 * @param appsUpdateStats contains stats about the apps indexer update. This method will 462 * populate the fields of this {@link AppsUpdateStats} structure. 463 */ 464 @VisibleForTesting 465 @WorkerThread doUpdate( @onNull AppsIndexerSettings settings, @NonNull AppsUpdateStats appsUpdateStats)466 public void doUpdate( 467 @NonNull AppsIndexerSettings settings, @NonNull AppsUpdateStats appsUpdateStats) 468 throws AppSearchException { 469 Objects.requireNonNull(settings); 470 Objects.requireNonNull(appsUpdateStats); 471 long currentTimeMillis = System.currentTimeMillis(); 472 473 // Search AppSearch for MobileApplication objects to get a "current" list of indexed apps. 474 long beforeGetTimestamp = SystemClock.elapsedRealtime(); 475 Map<String, Long> appUpdatedTimestamps = mAppSearchHelper.getAppsFromAppSearch(); 476 appsUpdateStats.mAppSearchGetLatencyMillis = 477 SystemClock.elapsedRealtime() - beforeGetTimestamp; 478 479 long beforePackageManagerTimestamp = SystemClock.elapsedRealtime(); 480 PackageManager packageManager = mContext.getPackageManager(); 481 Map<PackageInfo, ResolveInfos> packagesToIndex = 482 AppsUtil.getPackagesToIndex(packageManager); 483 appsUpdateStats.mPackageManagerLatencyMillis = 484 SystemClock.elapsedRealtime() - beforePackageManagerTimestamp; 485 Set<PackageInfo> packageInfos = packagesToIndex.keySet(); 486 487 Map<PackageInfo, ResolveInfos> packagesToBeAddedOrUpdated = new ArrayMap<>(); 488 long mostRecentAppUpdatedTimestampMillis = settings.getLastAppUpdateTimestampMillis(); 489 490 // Prepare a set of current app IDs for efficient lookup 491 Set<String> currentAppIds = new ArraySet<>(); 492 for (PackageInfo packageInfo : packageInfos) { 493 currentAppIds.add(packageInfo.packageName); 494 495 // Update the most recent timestamp as we iterate 496 if (packageInfo.lastUpdateTime > mostRecentAppUpdatedTimestampMillis) { 497 mostRecentAppUpdatedTimestampMillis = packageInfo.lastUpdateTime; 498 } 499 500 Long storedUpdateTime = appUpdatedTimestamps.get(packageInfo.packageName); 501 502 boolean added = storedUpdateTime == null; 503 boolean updated = 504 storedUpdateTime != null && packageInfo.lastUpdateTime != storedUpdateTime; 505 506 if (added) { 507 appsUpdateStats.mNumberOfAppsAdded++; 508 } 509 if (updated) { 510 appsUpdateStats.mNumberOfAppsUpdated++; 511 } 512 if (added || updated) { 513 packagesToBeAddedOrUpdated.put(packageInfo, packagesToIndex.get(packageInfo)); 514 } else { 515 appsUpdateStats.mNumberOfAppsUnchanged++; 516 } 517 } 518 519 List<GenericDocument> appSearchAppFunctions = 520 mAppSearchHelper.getAppFunctionsFromAppSearch(); 521 522 try { 523 if (!currentAppIds.equals(appUpdatedTimestamps.keySet()) 524 || requiresInsertSchemaForAppFunction(packagesToIndex, appSearchAppFunctions)) { 525 // The current list of apps/app functions in AppSearch does not match what is in 526 // PackageManager. This means this is the first sync, an app/app function was 527 // removed, or an app/app function was added. In all cases, we need to call 528 // setSchema to keep AppSearch in sync with PackageManager. 529 530 // currentAppIds comes from PackageManager, appUpdatedTimestamps comes from 531 // AppSearch. Deleted apps are those in appUpdateTimestamps and NOT in currentAppIds 532 appsUpdateStats.mNumberOfAppsRemoved = 0; 533 for (String appSearchApp : appUpdatedTimestamps.keySet()) { 534 if (!currentAppIds.contains(appSearchApp)) { 535 appsUpdateStats.mNumberOfAppsRemoved++; 536 } 537 } 538 539 List<PackageIdentifier> packageIdentifiers = new ArrayList<>(); 540 List<PackageIdentifier> packageIdentifiersWithAppFunctions = new ArrayList<>(); 541 for (Map.Entry<PackageInfo, ResolveInfos> entry : packagesToIndex.entrySet()) { 542 // We get certificates here as getting the certificates during the previous for 543 // loop would be wasteful if we end up not needing to call set schema 544 PackageInfo packageInfo = entry.getKey(); 545 byte[] certificate = AppsUtil.getCertificate(packageInfo); 546 if (certificate == null) { 547 Log.e(TAG, "Certificate not found for package: " + packageInfo.packageName); 548 continue; 549 } 550 PackageIdentifier packageIdentifier = 551 new PackageIdentifier(packageInfo.packageName, certificate); 552 packageIdentifiers.add(packageIdentifier); 553 if (entry.getValue().getAppFunctionServiceInfo() != null) { 554 packageIdentifiersWithAppFunctions.add(packageIdentifier); 555 } 556 } 557 // The certificate is necessary along with the package name as it is used in 558 // visibility settings. 559 long beforeSetSchemaTimestamp = SystemClock.elapsedRealtime(); 560 mAppSearchHelper.setSchemasForPackages( 561 packageIdentifiers, packageIdentifiersWithAppFunctions); 562 appsUpdateStats.mAppSearchSetSchemaLatencyMillis = 563 SystemClock.elapsedRealtime() - beforeSetSchemaTimestamp; 564 } 565 566 if (!packagesToBeAddedOrUpdated.isEmpty()) { 567 long beforePutTimestamp = SystemClock.elapsedRealtime(); 568 List<MobileApplication> mobileApplications = 569 AppsUtil.buildAppsFromPackageInfos( 570 packageManager, packagesToBeAddedOrUpdated); 571 List<AppFunctionStaticMetadata> appFunctions = 572 AppsUtil.buildAppFunctionStaticMetadata( 573 packageManager, 574 packagesToBeAddedOrUpdated, 575 /* indexerPackageName= */ mContext.getPackageName(), 576 mAppsIndexerConfig); 577 578 AppSearchBatchResult<String, Void> result = 579 mAppSearchHelper.indexApps( 580 mobileApplications, 581 appFunctions, 582 appSearchAppFunctions, 583 appsUpdateStats); 584 // Here we log all of these functions as "updated". However, some of these may be 585 // added or unchanged. 586 // TODO(b/357551503): Log function counts more accurately 587 appsUpdateStats.mNumberOfFunctionsUpdated = appFunctions.size(); 588 if (result.isSuccess()) { 589 appsUpdateStats.mUpdateStatusCodes.add(AppSearchResult.RESULT_OK); 590 } else { 591 Collection<AppSearchResult<Void>> values = result.getAll().values(); 592 593 for (AppSearchResult<Void> putResult : values) { 594 appsUpdateStats.mUpdateStatusCodes.add(putResult.getResultCode()); 595 } 596 } 597 appsUpdateStats.mAppSearchPutLatencyMillis = 598 SystemClock.elapsedRealtime() - beforePutTimestamp; 599 } 600 601 settings.setLastAppUpdateTimestampMillis(mostRecentAppUpdatedTimestampMillis); 602 settings.setLastUpdateTimestampMillis(currentTimeMillis); 603 604 appsUpdateStats.mLastAppUpdateTimestampMillis = mostRecentAppUpdatedTimestampMillis; 605 } catch (AppSearchException e) { 606 // Reset the last update time stamp and app update timestamp so we can try again later. 607 settings.reset(); 608 appsUpdateStats.mUpdateStatusCodes.clear(); 609 appsUpdateStats.mUpdateStatusCodes.add(e.getResultCode()); 610 throw e; 611 } 612 } 613 614 /** Returns whether the indexer should insert schema for app functions. */ requiresInsertSchemaForAppFunction( @onNull Map<PackageInfo, ResolveInfos> targetedPackages, List<GenericDocument> appSearchAppFunctions)615 private boolean requiresInsertSchemaForAppFunction( 616 @NonNull Map<PackageInfo, ResolveInfos> targetedPackages, 617 List<GenericDocument> appSearchAppFunctions) 618 throws AppSearchException { 619 // Should re-insert the schema as long as the indexed packages does not match the current 620 // set of packages. 621 Set<String> indexedAppFunctionPackages = new ArraySet<>(); 622 for (int i = 0; i < appSearchAppFunctions.size(); i++) { 623 indexedAppFunctionPackages.add( 624 appSearchAppFunctions 625 .get(i) 626 .getPropertyString(AppFunctionDocument.PROPERTY_PACKAGE_NAME)); 627 } 628 Set<String> currentAppFunctionPackages = getCurrentAppFunctionPackages(targetedPackages); 629 return !indexedAppFunctionPackages.equals(currentAppFunctionPackages); 630 } 631 632 /** Returns a set of currently installed packages that have app functions. */ getCurrentAppFunctionPackages( @onNull Map<PackageInfo, ResolveInfos> targetedPackages)633 private Set<String> getCurrentAppFunctionPackages( 634 @NonNull Map<PackageInfo, ResolveInfos> targetedPackages) { 635 Set<String> currentAppFunctionPackages = new ArraySet<>(); 636 for (Map.Entry<PackageInfo, ResolveInfos> entry : targetedPackages.entrySet()) { 637 PackageInfo packageInfo = entry.getKey(); 638 ResolveInfos resolveInfos = entry.getValue(); 639 if (resolveInfos.getAppFunctionServiceInfo() != null) { 640 currentAppFunctionPackages.add(packageInfo.packageName); 641 } 642 } 643 return currentAppFunctionPackages; 644 } 645 646 /** Shuts down the {@link AppsIndexerImpl} and its {@link AppSearchHelper}. */ 647 @Override close()648 public void close() { 649 mAppSearchHelper.close(); 650 } 651 } 652