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