• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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