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.appfunctions; 18 19 import static android.app.appfunctions.AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.WorkerThread; 24 import android.app.appfunctions.AppFunctionRuntimeMetadata; 25 import android.app.appfunctions.AppFunctionStaticMetadataHelper; 26 import android.app.appsearch.AppSearchBatchResult; 27 import android.app.appsearch.AppSearchManager; 28 import android.app.appsearch.AppSearchManager.SearchContext; 29 import android.app.appsearch.AppSearchResult; 30 import android.app.appsearch.AppSearchSchema; 31 import android.app.appsearch.PackageIdentifier; 32 import android.app.appsearch.PropertyPath; 33 import android.app.appsearch.PutDocumentsRequest; 34 import android.app.appsearch.RemoveByDocumentIdRequest; 35 import android.app.appsearch.SearchResult; 36 import android.app.appsearch.SearchSpec; 37 import android.app.appsearch.SetSchemaRequest; 38 import android.content.pm.PackageInfo; 39 import android.content.pm.PackageManager; 40 import android.content.pm.Signature; 41 import android.util.ArrayMap; 42 import android.util.ArraySet; 43 import android.util.Slog; 44 45 import com.android.internal.annotations.GuardedBy; 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.internal.infra.AndroidFuture; 48 49 import java.security.MessageDigest; 50 import java.security.NoSuchAlgorithmException; 51 import java.util.Collection; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.Set; 55 import java.util.concurrent.ExecutionException; 56 import java.util.concurrent.ExecutorService; 57 import java.util.concurrent.Executors; 58 import java.util.concurrent.Future; 59 60 /** 61 * This class implements helper methods for synchronously interacting with AppSearch while 62 * synchronizing AppFunction runtime and static metadata. 63 * 64 * <p>This class is not thread safe. 65 */ 66 public class MetadataSyncAdapter { 67 private static final String TAG = MetadataSyncAdapter.class.getSimpleName(); 68 69 private final ExecutorService mExecutor; 70 71 private final AppSearchManager mAppSearchManager; 72 private final PackageManager mPackageManager; 73 private final Object mLock = new Object(); 74 75 @GuardedBy("mLock") 76 private Future<?> mCurrentSyncTask; 77 78 // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility 79 // by permissions. 80 public static final int EXECUTE_APP_FUNCTIONS = 9; 81 MetadataSyncAdapter( @onNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager)82 public MetadataSyncAdapter( 83 @NonNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager) { 84 mPackageManager = Objects.requireNonNull(packageManager); 85 mAppSearchManager = Objects.requireNonNull(appSearchManager); 86 mExecutor = 87 Executors.newSingleThreadExecutor( 88 new NamedThreadFactory("AppFunctionSyncExecutors")); 89 } 90 91 /** 92 * This method submits a request to synchronize the AppFunction runtime and static metadata. 93 * 94 * @return A {@link AndroidFuture} that completes with a boolean value indicating whether the 95 * synchronization was successful. 96 */ submitSyncRequest()97 public AndroidFuture<Boolean> submitSyncRequest() { 98 SearchContext staticMetadataSearchContext = 99 new SearchContext.Builder( 100 AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB) 101 .build(); 102 SearchContext runtimeMetadataSearchContext = 103 new SearchContext.Builder( 104 AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB) 105 .build(); 106 AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>(); 107 Runnable runnable = 108 () -> { 109 try (FutureAppSearchSession staticMetadataSearchSession = 110 new FutureAppSearchSessionImpl( 111 mAppSearchManager, 112 AppFunctionExecutors.THREAD_POOL_EXECUTOR, 113 staticMetadataSearchContext); 114 FutureAppSearchSession runtimeMetadataSearchSession = 115 new FutureAppSearchSessionImpl( 116 mAppSearchManager, 117 AppFunctionExecutors.THREAD_POOL_EXECUTOR, 118 runtimeMetadataSearchContext)) { 119 120 trySyncAppFunctionMetadataBlocking( 121 staticMetadataSearchSession, runtimeMetadataSearchSession); 122 settableSyncStatus.complete(true); 123 124 } catch (Exception ex) { 125 settableSyncStatus.completeExceptionally(ex); 126 } 127 }; 128 129 synchronized (mLock) { 130 if (mCurrentSyncTask != null && !mCurrentSyncTask.isDone()) { 131 var unused = mCurrentSyncTask.cancel(false); 132 } 133 mCurrentSyncTask = mExecutor.submit(runnable); 134 } 135 136 return settableSyncStatus; 137 } 138 139 /** This method shuts down the {@link MetadataSyncAdapter} scheduler. */ shutDown()140 public void shutDown() { 141 mExecutor.shutdown(); 142 } 143 144 @WorkerThread 145 @VisibleForTesting trySyncAppFunctionMetadataBlocking( @onNull FutureAppSearchSession staticMetadataSearchSession, @NonNull FutureAppSearchSession runtimeMetadataSearchSession)146 void trySyncAppFunctionMetadataBlocking( 147 @NonNull FutureAppSearchSession staticMetadataSearchSession, 148 @NonNull FutureAppSearchSession runtimeMetadataSearchSession) 149 throws ExecutionException, InterruptedException { 150 ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap = 151 getPackageToFunctionIdMap( 152 staticMetadataSearchSession, 153 AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE, 154 AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, 155 AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME); 156 ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap = 157 getPackageToFunctionIdMap( 158 runtimeMetadataSearchSession, 159 RUNTIME_SCHEMA_TYPE, 160 AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID, 161 AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME); 162 163 ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap = 164 getAddedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); 165 ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap = 166 getRemovedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); 167 168 if (!staticPackageToFunctionMap.keySet().equals(runtimePackageToFunctionMap.keySet())) { 169 // Drop removed packages from removedFunctionsDiffMap, as setSchema() deletes them 170 ArraySet<String> removedPackages = 171 getRemovedPackages( 172 staticPackageToFunctionMap.keySet(), removedFunctionsDiffMap.keySet()); 173 for (String packageName : removedPackages) { 174 removedFunctionsDiffMap.remove(packageName); 175 } 176 Set<AppSearchSchema> appRuntimeMetadataSchemas = 177 getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet()); 178 appRuntimeMetadataSchemas.add( 179 AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()); 180 SetSchemaRequest addSetSchemaRequest = 181 buildSetSchemaRequestForRuntimeMetadataSchemas( 182 mPackageManager, appRuntimeMetadataSchemas); 183 Objects.requireNonNull( 184 runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get()); 185 } 186 187 if (!removedFunctionsDiffMap.isEmpty()) { 188 RemoveByDocumentIdRequest removeByDocumentIdRequest = 189 buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap); 190 AppSearchBatchResult<String, Void> removeDocumentBatchResult = 191 runtimeMetadataSearchSession.remove(removeByDocumentIdRequest).get(); 192 if (!removeDocumentBatchResult.isSuccess()) { 193 throw convertFailedAppSearchResultToException( 194 removeDocumentBatchResult.getFailures().values()); 195 } 196 } 197 198 if (!addedFunctionsDiffMap.isEmpty()) { 199 PutDocumentsRequest putDocumentsRequest = 200 buildPutRuntimeMetadataRequest(addedFunctionsDiffMap); 201 AppSearchBatchResult<String, Void> putDocumentBatchResult = 202 runtimeMetadataSearchSession.put(putDocumentsRequest).get(); 203 if (!putDocumentBatchResult.isSuccess()) { 204 throw convertFailedAppSearchResultToException( 205 putDocumentBatchResult.getFailures().values()); 206 } 207 } 208 } 209 210 @NonNull convertFailedAppSearchResultToException( @onNull Collection<AppSearchResult<Void>> appSearchResult)211 private static IllegalStateException convertFailedAppSearchResultToException( 212 @NonNull Collection<AppSearchResult<Void>> appSearchResult) { 213 Objects.requireNonNull(appSearchResult); 214 StringBuilder errorMessages = new StringBuilder(); 215 for (AppSearchResult<Void> result : appSearchResult) { 216 errorMessages.append(result.getErrorMessage()); 217 } 218 return new IllegalStateException(errorMessages.toString()); 219 } 220 221 @NonNull buildPutRuntimeMetadataRequest( @onNull ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap)222 private PutDocumentsRequest buildPutRuntimeMetadataRequest( 223 @NonNull ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap) { 224 Objects.requireNonNull(addedFunctionsDiffMap); 225 PutDocumentsRequest.Builder putDocumentRequestBuilder = new PutDocumentsRequest.Builder(); 226 227 for (int i = 0; i < addedFunctionsDiffMap.size(); i++) { 228 String packageName = addedFunctionsDiffMap.keyAt(i); 229 ArraySet<String> addedFunctionIds = addedFunctionsDiffMap.valueAt(i); 230 for (String addedFunctionId : addedFunctionIds) { 231 putDocumentRequestBuilder.addGenericDocuments( 232 new AppFunctionRuntimeMetadata.Builder(packageName, addedFunctionId) 233 .build()); 234 } 235 } 236 return putDocumentRequestBuilder.build(); 237 } 238 239 @NonNull buildRemoveRuntimeMetadataRequest( @onNull ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap)240 private RemoveByDocumentIdRequest buildRemoveRuntimeMetadataRequest( 241 @NonNull ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap) { 242 Objects.requireNonNull(AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE); 243 Objects.requireNonNull(removedFunctionsDiffMap); 244 RemoveByDocumentIdRequest.Builder removeDocumentRequestBuilder = 245 new RemoveByDocumentIdRequest.Builder( 246 AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE); 247 248 for (int i = 0; i < removedFunctionsDiffMap.size(); i++) { 249 String packageName = removedFunctionsDiffMap.keyAt(i); 250 ArraySet<String> removedFunctionIds = removedFunctionsDiffMap.valueAt(i); 251 for (String functionId : removedFunctionIds) { 252 String documentId = 253 AppFunctionRuntimeMetadata.getDocumentIdForAppFunction( 254 packageName, functionId); 255 removeDocumentRequestBuilder.addIds(documentId); 256 } 257 } 258 return removeDocumentRequestBuilder.build(); 259 } 260 261 @NonNull buildSetSchemaRequestForRuntimeMetadataSchemas( @onNull PackageManager packageManager, @NonNull Set<AppSearchSchema> metadataSchemaSet)262 private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas( 263 @NonNull PackageManager packageManager, 264 @NonNull Set<AppSearchSchema> metadataSchemaSet) { 265 Objects.requireNonNull(metadataSchemaSet); 266 SetSchemaRequest.Builder setSchemaRequestBuilder = 267 new SetSchemaRequest.Builder().setForceOverride(true).addSchemas(metadataSchemaSet); 268 269 for (AppSearchSchema runtimeMetadataSchema : metadataSchemaSet) { 270 String packageName = 271 AppFunctionRuntimeMetadata.getPackageNameFromSchema( 272 runtimeMetadataSchema.getSchemaType()); 273 byte[] packageCert = getCertificate(packageManager, packageName); 274 if (packageCert == null) { 275 continue; 276 } 277 setSchemaRequestBuilder.setSchemaTypeVisibilityForPackage( 278 runtimeMetadataSchema.getSchemaType(), 279 true, 280 new PackageIdentifier(packageName, packageCert)); 281 setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility( 282 runtimeMetadataSchema.getSchemaType(), Set.of(EXECUTE_APP_FUNCTIONS)); 283 } 284 return setSchemaRequestBuilder.build(); 285 } 286 287 @NonNull 288 @WorkerThread getAllRuntimeMetadataSchemas( @onNull Set<String> staticMetadataPackages)289 private Set<AppSearchSchema> getAllRuntimeMetadataSchemas( 290 @NonNull Set<String> staticMetadataPackages) { 291 Objects.requireNonNull(staticMetadataPackages); 292 293 Set<AppSearchSchema> appRuntimeMetadataSchemas = new ArraySet<>(); 294 for (String packageName : staticMetadataPackages) { 295 appRuntimeMetadataSchemas.add( 296 AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(packageName)); 297 } 298 299 return appRuntimeMetadataSchemas; 300 } 301 302 /** 303 * This method returns a set of packages that are in the removed function packages but not in 304 * the all existing static packages. 305 * 306 * @param allExistingStaticPackages A set of all existing static metadata packages. 307 * @param removedFunctionPackages A set of all removed function packages. 308 * @return A set of packages that are in the removed function packages but not in the all 309 * existing static packages. 310 */ 311 @NonNull getRemovedPackages( @onNull Set<String> allExistingStaticPackages, @NonNull Set<String> removedFunctionPackages)312 private static ArraySet<String> getRemovedPackages( 313 @NonNull Set<String> allExistingStaticPackages, 314 @NonNull Set<String> removedFunctionPackages) { 315 ArraySet<String> removedPackages = new ArraySet<>(); 316 317 for (String packageName : removedFunctionPackages) { 318 if (!allExistingStaticPackages.contains(packageName)) { 319 removedPackages.add(packageName); 320 } 321 } 322 323 return removedPackages; 324 } 325 326 /** 327 * This method returns a map of package names to a set of function ids that are in the static 328 * metadata but not in the runtime metadata. 329 * 330 * @param staticPackageToFunctionMap A map of package names to a set of function ids from the 331 * static metadata. 332 * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the 333 * runtime metadata. 334 * @return A map of package names to a set of function ids that are in the static metadata but 335 * not in the runtime metadata. 336 */ 337 @NonNull 338 @VisibleForTesting getAddedFunctionsDiffMap( @onNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap)339 static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap( 340 @NonNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, 341 @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { 342 Objects.requireNonNull(staticPackageToFunctionMap); 343 Objects.requireNonNull(runtimePackageToFunctionMap); 344 345 return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); 346 } 347 348 /** 349 * This method returns a map of package names to a set of function ids that are in the runtime 350 * metadata but not in the static metadata. 351 * 352 * @param staticPackageToFunctionMap A map of package names to a set of function ids from the 353 * static metadata. 354 * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the 355 * runtime metadata. 356 * @return A map of package names to a set of function ids that are in the runtime metadata but 357 * not in the static metadata. 358 */ 359 @NonNull 360 @VisibleForTesting getRemovedFunctionsDiffMap( @onNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap)361 static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap( 362 @NonNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, 363 @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { 364 Objects.requireNonNull(staticPackageToFunctionMap); 365 Objects.requireNonNull(runtimePackageToFunctionMap); 366 367 return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap); 368 } 369 370 @NonNull getFunctionsDiffMap( @onNull ArrayMap<String, ArraySet<String>> packageToFunctionMapA, @NonNull ArrayMap<String, ArraySet<String>> packageToFunctionMapB)371 private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap( 372 @NonNull ArrayMap<String, ArraySet<String>> packageToFunctionMapA, 373 @NonNull ArrayMap<String, ArraySet<String>> packageToFunctionMapB) { 374 Objects.requireNonNull(packageToFunctionMapA); 375 Objects.requireNonNull(packageToFunctionMapB); 376 377 ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>(); 378 for (String packageName : packageToFunctionMapA.keySet()) { 379 if (!packageToFunctionMapB.containsKey(packageName)) { 380 diffMap.put(packageName, packageToFunctionMapA.get(packageName)); 381 continue; 382 } 383 ArraySet<String> diffFunctions = new ArraySet<>(); 384 for (String functionId : 385 Objects.requireNonNull(packageToFunctionMapA.get(packageName))) { 386 if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName)) 387 .contains(functionId)) { 388 diffFunctions.add(functionId); 389 } 390 } 391 if (!diffFunctions.isEmpty()) { 392 diffMap.put(packageName, diffFunctions); 393 } 394 } 395 return diffMap; 396 } 397 398 /** 399 * This method returns a map of package names to a set of function ids from the AppFunction 400 * metadata. 401 * 402 * @param searchSession The {@link FutureAppSearchSession} to search the AppFunction metadata. 403 * @param schemaType The schema type of the AppFunction metadata. 404 * @param propertyFunctionId The property name of the function id in the AppFunction metadata. 405 * @param propertyPackageName The property name of the package name in the AppFunction metadata. 406 * @return A map of package names to a set of function ids from the AppFunction metadata. 407 */ 408 @NonNull 409 @VisibleForTesting 410 @WorkerThread getPackageToFunctionIdMap( @onNull FutureAppSearchSession searchSession, @NonNull String schemaType, @NonNull String propertyFunctionId, @NonNull String propertyPackageName)411 static ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap( 412 @NonNull FutureAppSearchSession searchSession, 413 @NonNull String schemaType, 414 @NonNull String propertyFunctionId, 415 @NonNull String propertyPackageName) 416 throws ExecutionException, InterruptedException { 417 Objects.requireNonNull(schemaType); 418 Objects.requireNonNull(propertyFunctionId); 419 Objects.requireNonNull(propertyPackageName); 420 ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>(); 421 422 try (FutureSearchResults futureSearchResults = 423 searchSession 424 .search( 425 "", 426 buildMetadataSearchSpec( 427 schemaType, propertyFunctionId, propertyPackageName)) 428 .get(); ) { 429 List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get(); 430 // TODO(b/357551503): This could be expensive if we have more functions 431 while (!searchResultsList.isEmpty()) { 432 for (SearchResult searchResult : searchResultsList) { 433 String packageName = 434 searchResult 435 .getGenericDocument() 436 .getPropertyString(propertyPackageName); 437 String functionId = 438 searchResult.getGenericDocument().getPropertyString(propertyFunctionId); 439 packageToFunctionIds 440 .computeIfAbsent(packageName, k -> new ArraySet<>()) 441 .add(functionId); 442 } 443 searchResultsList = futureSearchResults.getNextPage().get(); 444 } 445 } 446 return packageToFunctionIds; 447 } 448 449 /** 450 * This method returns a {@link SearchSpec} for searching the AppFunction metadata. 451 * 452 * @param schemaType The schema type of the AppFunction metadata. 453 * @param propertyFunctionId The property name of the function id in the AppFunction metadata. 454 * @param propertyPackageName The property name of the package name in the AppFunction metadata. 455 * @return A {@link SearchSpec} for searching the AppFunction metadata. 456 */ 457 @NonNull buildMetadataSearchSpec( @onNull String schemaType, @NonNull String propertyFunctionId, @NonNull String propertyPackageName)458 private static SearchSpec buildMetadataSearchSpec( 459 @NonNull String schemaType, 460 @NonNull String propertyFunctionId, 461 @NonNull String propertyPackageName) { 462 Objects.requireNonNull(schemaType); 463 Objects.requireNonNull(propertyFunctionId); 464 Objects.requireNonNull(propertyPackageName); 465 return new SearchSpec.Builder() 466 .addFilterSchemas(schemaType) 467 .addProjectionPaths( 468 schemaType, 469 List.of( 470 new PropertyPath(propertyFunctionId), 471 new PropertyPath(propertyPackageName))) 472 .build(); 473 } 474 475 /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */ 476 @Nullable getCertificate( @onNull PackageManager packageManager, @NonNull String packageName)477 private byte[] getCertificate( 478 @NonNull PackageManager packageManager, @NonNull String packageName) { 479 Objects.requireNonNull(packageManager); 480 Objects.requireNonNull(packageName); 481 PackageInfo packageInfo; 482 try { 483 packageInfo = 484 Objects.requireNonNull( 485 packageManager.getPackageInfo( 486 packageName, 487 PackageManager.GET_META_DATA 488 | PackageManager.GET_SIGNING_CERTIFICATES)); 489 } catch (Exception e) { 490 Slog.d(TAG, "Package name info not found for package: " + packageName); 491 return null; 492 } 493 if (packageInfo.signingInfo == null) { 494 Slog.d(TAG, "Signing info not found for package: " + packageInfo.packageName); 495 return null; 496 } 497 498 MessageDigest md; 499 try { 500 md = MessageDigest.getInstance("SHA256"); 501 } catch (NoSuchAlgorithmException e) { 502 return null; 503 } 504 Signature[] signatures = packageInfo.signingInfo.getSigningCertificateHistory(); 505 if (signatures == null || signatures.length == 0) { 506 return null; 507 } 508 md.update(signatures[0].toByteArray()); 509 return md.digest(); 510 } 511 } 512