• 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.Nullable;
21 import android.app.appsearch.AppSearchSchema;
22 import android.app.appsearch.GenericDocument;
23 import android.app.appsearch.util.LogUtil;
24 import android.app.usage.UsageEvents;
25 import android.app.usage.UsageStatsManager;
26 import android.content.ComponentName;
27 import android.content.ContentResolver;
28 import android.content.Intent;
29 import android.content.pm.ActivityInfo;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ResolveInfo;
34 import android.content.pm.Signature;
35 import android.content.res.Resources;
36 import android.net.Uri;
37 import android.text.TextUtils;
38 import android.util.ArrayMap;
39 import android.util.Log;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionDocument;
43 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata;
44 import com.android.server.appsearch.appsindexer.appsearchtypes.AppOpenEvent;
45 import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
46 
47 import java.security.MessageDigest;
48 import java.security.NoSuchAlgorithmException;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Objects;
54 
55 /** Utility class for pulling apps details from package manager. */
56 public final class AppsUtil {
57     public static final String TAG = "AppSearchAppsUtil";
58 
AppsUtil()59     private AppsUtil() {}
60 
61     /** Gets the resource Uri given a resource id. */
62     @NonNull
getResourceUri( @onNull PackageManager packageManager, @NonNull ApplicationInfo appInfo, int resourceId)63     private static Uri getResourceUri(
64             @NonNull PackageManager packageManager,
65             @NonNull ApplicationInfo appInfo,
66             int resourceId)
67             throws PackageManager.NameNotFoundException {
68         Objects.requireNonNull(packageManager);
69         Objects.requireNonNull(appInfo);
70         Resources resources = packageManager.getResourcesForApplication(appInfo);
71         String resPkg = resources.getResourcePackageName(resourceId);
72         String type = resources.getResourceTypeName(resourceId);
73         return makeResourceUri(appInfo.packageName, resPkg, type, resourceId);
74     }
75 
76     /**
77      * Appends the resource id instead of name to make the resource uri due to b/161564466. The
78      * resource names for some apps (e.g. Chrome) are obfuscated due to resource name collapsing, so
79      * we need to use resource id instead.
80      *
81      * @see Uri
82      */
83     @NonNull
makeResourceUri( @onNull String appPkg, @NonNull String resPkg, @NonNull String type, int resourceId)84     private static Uri makeResourceUri(
85             @NonNull String appPkg, @NonNull String resPkg, @NonNull String type, int resourceId) {
86         Objects.requireNonNull(appPkg);
87         Objects.requireNonNull(resPkg);
88         Objects.requireNonNull(type);
89 
90         // For more details on Android URIs, see the official Android documentation:
91         // https://developer.android.com/guide/topics/providers/content-provider-basics#ContentURIs
92         Uri.Builder uriBuilder = new Uri.Builder();
93         uriBuilder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE);
94         uriBuilder.encodedAuthority(appPkg);
95         uriBuilder.appendEncodedPath(type);
96         if (!appPkg.equals(resPkg)) {
97             uriBuilder.appendEncodedPath(resPkg + ":" + resourceId);
98         } else {
99             uriBuilder.appendEncodedPath(String.valueOf(resourceId));
100         }
101         return uriBuilder.build();
102     }
103 
104     /**
105      * Gets the icon uri for the activity.
106      *
107      * @return the icon Uri string, or null if there is no icon resource.
108      */
109     @Nullable
getActivityIconUriString( @onNull PackageManager packageManager, @NonNull ActivityInfo activityInfo)110     private static String getActivityIconUriString(
111             @NonNull PackageManager packageManager, @NonNull ActivityInfo activityInfo) {
112         Objects.requireNonNull(packageManager);
113         Objects.requireNonNull(activityInfo);
114         int iconResourceId = activityInfo.getIconResource();
115         if (iconResourceId == 0) {
116             return null;
117         }
118 
119         try {
120             return getResourceUri(packageManager, activityInfo.applicationInfo, iconResourceId)
121                     .toString();
122         } catch (PackageManager.NameNotFoundException e) {
123             // If resources aren't found for the application, that is fine. We return null and
124             // handle it with getActivityIconUriString
125             return null;
126         }
127     }
128 
129     /**
130      * Gets {@link PackageInfo}s for packages that have a launch activity or has app functions,
131      * along with their corresponding {@link ResolveInfo}. This is useful for building schemas as
132      * well as determining which packages to set schemas for.
133      *
134      * @return a mapping of {@link PackageInfo}s with their corresponding {@link ResolveInfos} for
135      *     the packages launch activity and maybe app function resolve info.
136      * @see PackageManager#getInstalledPackages
137      * @see PackageManager#queryIntentActivities
138      * @see PackageManager#queryIntentServices
139      */
140     @NonNull
getPackagesToIndex( @onNull PackageManager packageManager)141     public static Map<PackageInfo, ResolveInfos> getPackagesToIndex(
142             @NonNull PackageManager packageManager) {
143         Objects.requireNonNull(packageManager);
144         List<PackageInfo> packageInfos =
145                 packageManager.getInstalledPackages(
146                         PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES);
147 
148         Intent launchIntent = new Intent(Intent.ACTION_MAIN, null);
149         launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
150         launchIntent.setPackage(null);
151         List<ResolveInfo> activities = packageManager.queryIntentActivities(launchIntent, 0);
152         Map<String, ResolveInfo> packageNameToLauncher = new ArrayMap<>();
153         for (int i = 0; i < activities.size(); i++) {
154             ResolveInfo resolveInfo = activities.get(i);
155             packageNameToLauncher.put(resolveInfo.activityInfo.packageName, resolveInfo);
156         }
157 
158         // This is to workaround the android lint check.
159         // AppFunctionService.SERVICE_INTERFACE is defined in API 36 but also it is just a string
160         // literal.
161         Intent appFunctionServiceIntent = new Intent("android.app.appfunctions.AppFunctionService");
162         Map<String, ResolveInfo> packageNameToAppFunctionServiceInfo = new ArrayMap<>();
163         List<ResolveInfo> services =
164                 packageManager.queryIntentServices(appFunctionServiceIntent, 0);
165         for (int i = 0; i < services.size(); i++) {
166             ResolveInfo resolveInfo = services.get(i);
167             packageNameToAppFunctionServiceInfo.put(
168                     resolveInfo.serviceInfo.packageName, resolveInfo);
169         }
170 
171         Map<PackageInfo, ResolveInfos> packagesToIndex = new ArrayMap<>();
172         for (int i = 0; i < packageInfos.size(); i++) {
173             PackageInfo packageInfo = packageInfos.get(i);
174             ResolveInfos.Builder builder = new ResolveInfos.Builder();
175 
176             ResolveInfo launchActivityResolveInfo =
177                     packageNameToLauncher.get(packageInfo.packageName);
178             if (launchActivityResolveInfo != null) {
179                 builder.setLaunchActivityResolveInfo(launchActivityResolveInfo);
180             }
181 
182             ResolveInfo appFunctionServiceInfo =
183                     packageNameToAppFunctionServiceInfo.get(packageInfo.packageName);
184             if (appFunctionServiceInfo != null) {
185                 builder.setAppFunctionServiceResolveInfo(appFunctionServiceInfo);
186             }
187 
188             if (launchActivityResolveInfo != null || appFunctionServiceInfo != null) {
189                 packagesToIndex.put(packageInfo, builder.build());
190             }
191         }
192         return packagesToIndex;
193     }
194 
195     /**
196      * Uses {@link PackageManager} and a Map of {@link PackageInfo}s to {@link ResolveInfos}s to
197      * build AppSearch {@link MobileApplication} documents. Info from both are required to build app
198      * documents.
199      *
200      * @param packageInfos a mapping of {@link PackageInfo}s and their corresponding {@link
201      *     ResolveInfos} for the packages launch activity.
202      */
203     @NonNull
buildAppsFromPackageInfos( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfos> packageInfos)204     public static List<MobileApplication> buildAppsFromPackageInfos(
205             @NonNull PackageManager packageManager,
206             @NonNull Map<PackageInfo, ResolveInfos> packageInfos) {
207         Objects.requireNonNull(packageManager);
208         Objects.requireNonNull(packageInfos);
209 
210         List<MobileApplication> mobileApplications = new ArrayList<>();
211         for (Map.Entry<PackageInfo, ResolveInfos> entry : packageInfos.entrySet()) {
212             ResolveInfo resolveInfo = entry.getValue().getLaunchActivityResolveInfo();
213 
214             MobileApplication mobileApplication =
215                     createMobileApplication(packageManager, entry.getKey(), resolveInfo);
216             if (mobileApplication != null) {
217                 mobileApplications.add(mobileApplication);
218             }
219         }
220         return mobileApplications;
221     }
222 
223     // TODO(b/367410454): Remove this method once enable_apps_indexer_incremental_put flag is
224     //  rolled out
225     /**
226      * Uses {@link PackageManager} and a Map of {@link PackageInfo}s to {@link ResolveInfos}s to
227      * build AppSearch {@link AppFunctionStaticMetadata} documents. Info from both are required to
228      * build app documents.
229      *
230      * @param packageInfos a mapping of {@link PackageInfo}s and their corresponding {@link
231      *     ResolveInfo} for the packages launch activity.
232      * @param indexerPackageName the name of the package performing the indexing. This should be the
233      *     same as the package running the apps indexer so that qualified ids are correctly created.
234      * @param config the app indexer config used to enforce various limits during parsing.
235      */
buildAppFunctionStaticMetadata( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfos> packageInfos, @NonNull String indexerPackageName, AppsIndexerConfig config)236     public static List<AppFunctionStaticMetadata> buildAppFunctionStaticMetadata(
237             @NonNull PackageManager packageManager,
238             @NonNull Map<PackageInfo, ResolveInfos> packageInfos,
239             @NonNull String indexerPackageName,
240             AppsIndexerConfig config) {
241         AppFunctionDocumentParser parser =
242                 new AppFunctionDocumentParserImpl(indexerPackageName, config);
243         return buildAppFunctionStaticMetadata(packageManager, packageInfos, parser);
244     }
245 
246     // TODO(b/367410454): Remove this method once enable_apps_indexer_incremental_put flag is
247     //  rolled out
248     /**
249      * Similar to the above {@link #buildAppFunctionStaticMetadata}, but allows the caller to
250      * provide a custom parser. This is for testing purposes.
251      */
252     @VisibleForTesting
buildAppFunctionStaticMetadata( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfos> packageInfos, @NonNull AppFunctionDocumentParser parser)253     static List<AppFunctionStaticMetadata> buildAppFunctionStaticMetadata(
254             @NonNull PackageManager packageManager,
255             @NonNull Map<PackageInfo, ResolveInfos> packageInfos,
256             @NonNull AppFunctionDocumentParser parser) {
257         Objects.requireNonNull(packageManager);
258         Objects.requireNonNull(packageInfos);
259         Objects.requireNonNull(parser);
260 
261         List<AppFunctionStaticMetadata> appFunctions = new ArrayList<>();
262         for (Map.Entry<PackageInfo, ResolveInfos> entry : packageInfos.entrySet()) {
263             PackageInfo packageInfo = entry.getKey();
264             ResolveInfo resolveInfo = entry.getValue().getAppFunctionServiceInfo();
265             if (resolveInfo == null) {
266                 continue;
267             }
268 
269             String assetFilePath;
270             try {
271                 PackageManager.Property property =
272                         packageManager.getProperty(
273                                 "android.app.appfunctions",
274                                 new ComponentName(
275                                         resolveInfo.serviceInfo.packageName,
276                                         resolveInfo.serviceInfo.name));
277                 assetFilePath = property.getString();
278             } catch (PackageManager.NameNotFoundException e) {
279                 Log.w(TAG, "buildAppFunctionMetadataFromPackageInfo: Failed to get property", e);
280                 continue;
281             }
282             if (assetFilePath != null) {
283                 appFunctions.addAll(
284                         parser.parse(packageManager, packageInfo.packageName, assetFilePath));
285             }
286         }
287         return appFunctions;
288     }
289 
290     /**
291      * Uses {@link PackageManager} and a Map of {@link PackageInfo}s to {@link ResolveInfos}s to
292      * build AppSearch {@link GenericDocument} objects. Info from both are required to build app
293      * documents.
294      *
295      * <p>App documents will be returned as a mapping of packages to a mapping of document ids to
296      * documents. This is useful for determining what has changed during an update.
297      *
298      * <p>The parser will parse app function documents based on schemas if schemasPerPackage is not
299      * null or the map of schemas for a package is not empty, else it will default to predefined
300      * schema properties created by {@link
301      * AppFunctionStaticMetadata#createAppFunctionSchemaForPackage} to create the {@link
302      * AppFunctionStaticMetadata} documents.
303      *
304      * @param packageInfos a mapping of {@link PackageInfo}s and their corresponding {@link
305      *     ResolveInfo} for the packages launch activity.
306      * @param indexerPackageName the name of the package performing the indexing. This should be the
307      *     same as the package running the apps indexer so that qualified ids are correctly created.
308      * @param config the app indexer config used to enforce various limits during parsing.
309      * @param schemasPerPackage a mapping of packages to a mapping of schema types to their
310      *     corresponding {@link AppSearchSchema} objects, or null if there are no schemas to
311      *     consider.
312      * @return A mapping of packages to a mapping of document ids to AppFunction GenericDocuments
313      *     conforming the schemas for the corresponding package.
314      */
315     public static Map<String, Map<String, ? extends AppFunctionDocument>>
buildAppFunctionDocumentsIntoMap( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfos> packageInfos, @NonNull String indexerPackageName, AppsIndexerConfig config, @Nullable Map<String, Map<String, AppSearchSchema>> schemasPerPackage)316             buildAppFunctionDocumentsIntoMap(
317                     @NonNull PackageManager packageManager,
318                     @NonNull Map<PackageInfo, ResolveInfos> packageInfos,
319                     @NonNull String indexerPackageName,
320                     AppsIndexerConfig config,
321                     @Nullable Map<String, Map<String, AppSearchSchema>> schemasPerPackage) {
322         AppFunctionDocumentParser parser =
323                 new AppFunctionDocumentParserImpl(indexerPackageName, config);
324         return buildAppFunctionDocumentsIntoMap(
325                 packageManager, packageInfos, parser, schemasPerPackage);
326     }
327 
328     /**
329      * Similar to the above {@link #buildAppFunctionStaticMetadata}, but allows the caller to
330      * provide a custom parser. This is for testing purposes.
331      *
332      * @see #buildAppFunctionDocumentsIntoMap(PackageManager, Map, String, AppsIndexerConfig, Map)
333      */
334     @VisibleForTesting
buildAppFunctionDocumentsIntoMap( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfos> packageInfos, @NonNull AppFunctionDocumentParser parser, @Nullable Map<String, Map<String, AppSearchSchema>> schemasPerPackage)335     static Map<String, Map<String, ? extends AppFunctionDocument>> buildAppFunctionDocumentsIntoMap(
336             @NonNull PackageManager packageManager,
337             @NonNull Map<PackageInfo, ResolveInfos> packageInfos,
338             @NonNull AppFunctionDocumentParser parser,
339             @Nullable Map<String, Map<String, AppSearchSchema>> schemasPerPackage) {
340         Objects.requireNonNull(packageManager);
341         Objects.requireNonNull(packageInfos);
342         Objects.requireNonNull(parser);
343         Map<String, Map<String, ? extends AppFunctionDocument>> appFunctions = new ArrayMap<>();
344         for (Map.Entry<PackageInfo, ResolveInfos> entry : packageInfos.entrySet()) {
345             PackageInfo packageInfo = entry.getKey();
346             ResolveInfo resolveInfo = entry.getValue().getAppFunctionServiceInfo();
347             if (resolveInfo == null) {
348                 continue;
349             }
350 
351             String assetFilePath;
352             boolean isDynamicSchemaDefined =
353                     schemasPerPackage != null
354                             && !schemasPerPackage
355                                     .getOrDefault(packageInfo.packageName, Collections.emptyMap())
356                                     .isEmpty();
357 
358             // Currently SDK will generate two files for hardcoded and dynamic schemas respectively
359             // so that devices running older AppSearch versions that are incompatible with new
360             // format can continue to parse app function documents while newer versions can use v2
361             // file for constructing app function documents with dynamic schema and more properties.
362             // TODO(b/386676297) - Merge these two when enough devices have changes to support
363             // dynamic schema.
364             String appFunctionXmlPropertyName =
365                     isDynamicSchemaDefined
366                             ? "android.app.appfunctions.v2"
367                             : "android.app.appfunctions";
368             try {
369                 PackageManager.Property property =
370                         packageManager.getProperty(
371                                 appFunctionXmlPropertyName,
372                                 new ComponentName(
373                                         resolveInfo.serviceInfo.packageName,
374                                         resolveInfo.serviceInfo.name));
375                 assetFilePath = property.getString();
376             } catch (PackageManager.NameNotFoundException e) {
377                 Log.w(TAG, "buildAppFunctionMetadataFromPackageInfo: Failed to get property", e);
378                 continue;
379             }
380 
381             if (assetFilePath != null) {
382                 if (isDynamicSchemaDefined) {
383                     appFunctions.put(
384                             packageInfo.packageName,
385                             parser.parseIntoMapForGivenSchemas(
386                                     packageManager,
387                                     packageInfo.packageName,
388                                     assetFilePath,
389                                     schemasPerPackage.get(packageInfo.packageName)));
390                 } else {
391                     appFunctions.put(
392                             packageInfo.packageName,
393                             parser.parseIntoMap(
394                                     packageManager, packageInfo.packageName, assetFilePath));
395                 }
396             }
397         }
398         return appFunctions;
399     }
400 
401     /**
402      * Gets a list of app open events (package name and timestamp) within a specific time range.
403      *
404      * @param usageStatsManager the {@link UsageStatsManager} to query for app open events.
405      * @param startTime the start time in milliseconds since the epoch.
406      * @param endTime the end time in milliseconds since the epoch.
407      * @return a list of {@link AppOpenEvent} representing the app open events.
408      */
409     @NonNull
getAppOpenEvents( @onNull UsageStatsManager usageStatsManager, long startTime, long endTime)410     public static List<AppOpenEvent> getAppOpenEvents(
411             @NonNull UsageStatsManager usageStatsManager, long startTime, long endTime) {
412 
413         List<AppOpenEvent> appOpenEvents = new ArrayList<>();
414 
415         UsageEvents usageEvents = usageStatsManager.queryEvents(startTime, endTime);
416         while (usageEvents.hasNextEvent()) {
417             UsageEvents.Event event = new UsageEvents.Event();
418             usageEvents.getNextEvent(event);
419 
420             if (event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND
421                     || event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED) {
422                 String packageName = event.getPackageName();
423                 long timestamp = event.getTimeStamp();
424 
425                 AppOpenEvent appOpenEvent = AppOpenEvent.create(packageName, timestamp);
426                 appOpenEvents.add(appOpenEvent);
427             }
428         }
429 
430         return appOpenEvents;
431     }
432 
433     /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found */
434     @Nullable
getCertificate(@onNull PackageInfo packageInfo)435     public static byte[] getCertificate(@NonNull PackageInfo packageInfo) {
436         Objects.requireNonNull(packageInfo);
437         if (packageInfo.signingInfo == null) {
438             if (LogUtil.DEBUG) {
439                 Log.d(TAG, "Signing info not found for package: " + packageInfo.packageName);
440             }
441             return null;
442         }
443         MessageDigest md;
444         try {
445             md = MessageDigest.getInstance("SHA256");
446         } catch (NoSuchAlgorithmException e) {
447             return null;
448         }
449         Signature[] signatures = packageInfo.signingInfo.getSigningCertificateHistory();
450         if (signatures == null || signatures.length == 0) {
451             return null;
452         }
453         md.update(signatures[0].toByteArray());
454         return md.digest();
455     }
456 
457     /**
458      * Uses PackageManager to supplement packageInfos with an application display name and icon uri,
459      * if any.
460      *
461      * @return a MobileApplication representing the packageInfo, null if finding the signing
462      *     certificate fails.
463      */
464     @Nullable
createMobileApplication( @onNull PackageManager packageManager, @NonNull PackageInfo packageInfo, @Nullable ResolveInfo resolveInfo)465     private static MobileApplication createMobileApplication(
466             @NonNull PackageManager packageManager,
467             @NonNull PackageInfo packageInfo,
468             @Nullable ResolveInfo resolveInfo) {
469         Objects.requireNonNull(packageManager);
470         Objects.requireNonNull(packageInfo);
471 
472         byte[] certificate = getCertificate(packageInfo);
473         if (certificate == null) {
474             return null;
475         }
476 
477         MobileApplication.Builder builder =
478                 new MobileApplication.Builder(packageInfo.packageName, certificate)
479                         // TODO(b/275592563): Populate with nicknames from various sources
480                         .setCreationTimestampMillis(packageInfo.firstInstallTime)
481                         .setUpdatedTimestampMs(packageInfo.lastUpdateTime);
482 
483         if (resolveInfo == null) {
484             return builder.build();
485         }
486         String applicationDisplayName = resolveInfo.loadLabel(packageManager).toString();
487         if (TextUtils.isEmpty(applicationDisplayName)) {
488             applicationDisplayName = packageInfo.applicationInfo.className;
489         }
490         builder.setDisplayName(applicationDisplayName);
491         String iconUri = getActivityIconUriString(packageManager, resolveInfo.activityInfo);
492         if (iconUri != null) {
493             builder.setIconUri(iconUri);
494         }
495         String applicationLabel =
496                 packageManager.getApplicationLabel(packageInfo.applicationInfo).toString();
497         if (!applicationDisplayName.equals(applicationLabel)) {
498             // This can be different from applicationDisplayName, and should be indexed
499             builder.setAlternateNames(applicationLabel);
500         }
501         if (resolveInfo.activityInfo.name != null) {
502             builder.setClassName(resolveInfo.activityInfo.name);
503         }
504         return builder.build();
505     }
506 
507     /**
508      * Creates dynamic app function schemas defined by the app per package.
509      *
510      * <p>Packages which don't have a AppFunctionService will not have an entry in the returned map.
511      *
512      * @param packageManager the {@link PackageManager} to use to get the schema file path.
513      * @param packageInfos a mapping of {@link PackageInfo}s and their corresponding {@link
514      *     ResolveInfo} for the packages launch activity.
515      * @param maxAllowedAppFunctionSchemasPerPackage the max number of schema definitions allowed
516      *     per package.
517      * @return A mapping of packages to a mapping of schema types to their corresponding {@link
518      *     AppSearchSchema} objects or an empty map for a package if there's an error during parsing
519      *     or no schema file is found.
520      */
521     @NonNull
getDynamicAppFunctionSchemasForPackages( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfos> packageInfos, int maxAllowedAppFunctionSchemasPerPackage)522     public static Map<String, Map<String, AppSearchSchema>> getDynamicAppFunctionSchemasForPackages(
523             @NonNull PackageManager packageManager,
524             @NonNull Map<PackageInfo, ResolveInfos> packageInfos,
525             int maxAllowedAppFunctionSchemasPerPackage) {
526         Objects.requireNonNull(packageInfos);
527 
528         Map<String, Map<String, AppSearchSchema>> schemasPerPackage = new ArrayMap<>();
529         AppFunctionSchemaParser parser =
530                 new AppFunctionSchemaParser(maxAllowedAppFunctionSchemasPerPackage);
531         for (Map.Entry<PackageInfo, ResolveInfos> entry : packageInfos.entrySet()) {
532             PackageInfo packageInfo = entry.getKey();
533             ResolveInfo resolveInfo = entry.getValue().getAppFunctionServiceInfo();
534             if (resolveInfo == null) {
535                 continue;
536             }
537 
538             String assetFilePath = null;
539             try {
540                 PackageManager.Property property =
541                         packageManager.getProperty(
542                                 /* propertyName= */ "android.app.appfunctions.schema",
543                                 new ComponentName(
544                                         resolveInfo.serviceInfo.packageName,
545                                         resolveInfo.serviceInfo.name));
546                 assetFilePath = property.getString();
547             } catch (PackageManager.NameNotFoundException e) {
548                 Log.w(
549                         TAG,
550                         "getDynamicAppFunctionSchemasForPackages: Failed to get schema "
551                                 + "property for package: "
552                                 + resolveInfo.serviceInfo.packageName,
553                         e);
554             }
555 
556             if (assetFilePath != null) {
557                 schemasPerPackage.put(
558                         packageInfo.packageName,
559                         parser.parseAndCreateSchemas(
560                                 packageManager, packageInfo.packageName, assetFilePath));
561             } else {
562                 schemasPerPackage.put(packageInfo.packageName, Collections.emptyMap());
563             }
564         }
565 
566         return schemasPerPackage;
567     }
568 }
569