diff options
author | 2024-09-10 15:45:11 +0000 | |
---|---|---|
committer | 2024-09-12 13:51:42 +0000 | |
commit | 881c12ec5bcf3a5e7463e16dae1d0d5baefd2d28 (patch) | |
tree | 6344e7b174feef4ed73972f92a798aabd7d2147c /services/appfunctions/java | |
parent | a61891a008b948f17298986104ff9461784523a8 (diff) |
[MetadataSyncAdapter Sync api]
- Add api for submitting a sync request to the sync executor.
Flag: android.app.appfunctions.flags.enable_app_function_manager
Test: atest FrameworksAppFunctionsTests -c
Bug: 357551503
Change-Id: Idcd1715fb08e1481a28cdcb9c0b1ff07fe7689a7
Diffstat (limited to 'services/appfunctions/java')
3 files changed, 280 insertions, 29 deletions
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java index b1c25c4f3c61..56f373d22f75 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java @@ -26,7 +26,6 @@ import android.app.appsearch.GetSchemaResponse; import android.app.appsearch.PutDocumentsRequest; import android.app.appsearch.RemoveByDocumentIdRequest; import android.app.appsearch.SearchResult; -import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.app.appsearch.SetSchemaRequest; import android.app.appsearch.SetSchemaResponse; @@ -36,8 +35,6 @@ import com.android.internal.infra.AndroidFuture; import java.io.Closeable; import java.io.IOException; import java.util.List; -import java.util.Objects; -import java.util.concurrent.Executor; /** A future API wrapper of {@link AppSearchSession} APIs. */ public interface FutureAppSearchSession extends Closeable { @@ -88,29 +85,15 @@ public interface FutureAppSearchSession extends Closeable { @NonNull String queryExpression, @NonNull SearchSpec searchSpec); /** A future API wrapper of {@link android.app.appsearch.SearchResults}. */ - class FutureSearchResults { - private final SearchResults mSearchResults; - private final Executor mExecutor; - - public FutureSearchResults( - @NonNull SearchResults searchResults, @NonNull Executor executor) { - mSearchResults = Objects.requireNonNull(searchResults); - mExecutor = Objects.requireNonNull(executor); - } - - public AndroidFuture<List<SearchResult>> getNextPage() { - AndroidFuture<AppSearchResult<List<SearchResult>>> nextPageFuture = - new AndroidFuture<>(); - - mSearchResults.getNextPage(mExecutor, nextPageFuture::complete); - return nextPageFuture.thenApply( - result -> { - if (result.isSuccess()) { - return result.getResultValue(); - } else { - throw new RuntimeException(failedResultToException(result)); - } - }); - } + interface FutureSearchResults { + + /** + * Retrieves the next page of {@link SearchResult} objects from the {@link AppSearchSession} + * database. + * + * <p>Continue calling this method to access results until it returns an empty list, + * signifying there are no more results. + */ + AndroidFuture<List<SearchResult>> getNextPage(); } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java index e78f390a4825..3079d9f51bf3 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java @@ -30,6 +30,8 @@ import android.app.appsearch.GetByDocumentIdRequest; import android.app.appsearch.GetSchemaResponse; import android.app.appsearch.PutDocumentsRequest; import android.app.appsearch.RemoveByDocumentIdRequest; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.app.appsearch.SetSchemaRequest; import android.app.appsearch.SetSchemaResponse; @@ -37,6 +39,7 @@ import android.app.appsearch.SetSchemaResponse; import com.android.internal.infra.AndroidFuture; import java.io.IOException; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -176,12 +179,39 @@ public class FutureAppSearchSessionImpl implements FutureAppSearchSession { @NonNull String queryExpression, @NonNull SearchSpec searchSpec) { return getSessionAsync() .thenApply(session -> session.search(queryExpression, searchSpec)) - .thenApply(result -> new FutureSearchResults(result, mExecutor)); + .thenApply(result -> new FutureSearchResultsImpl(result, mExecutor)); } @Override public void close() throws IOException {} + private static final class FutureSearchResultsImpl implements FutureSearchResults { + private final SearchResults mSearchResults; + private final Executor mExecutor; + + private FutureSearchResultsImpl( + @NonNull SearchResults searchResults, @NonNull Executor executor) { + this.mSearchResults = searchResults; + this.mExecutor = executor; + } + + @Override + public AndroidFuture<List<SearchResult>> getNextPage() { + AndroidFuture<AppSearchResult<List<SearchResult>>> nextPageFuture = + new AndroidFuture<>(); + + mSearchResults.getNextPage(mExecutor, nextPageFuture::complete); + return nextPageFuture.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException(failedResultToException(result)); + } + }); + } + } + private static final class BatchResultCallbackAdapter<K, V> implements BatchResultCallback<K, V> { private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture; diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java index 01e10086a03a..5f608043927b 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -16,35 +16,238 @@ package com.android.server.appfunctions; +import static android.app.appfunctions.AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE; + import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.WorkerThread; +import android.app.appfunctions.AppFunctionRuntimeMetadata; +import android.app.appfunctions.AppFunctionStaticMetadataHelper; +import android.app.appsearch.AppSearchBatchResult; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.AppSearchSchema; +import android.app.appsearch.PackageIdentifier; import android.app.appsearch.PropertyPath; +import android.app.appsearch.PutDocumentsRequest; +import android.app.appsearch.RemoveByDocumentIdRequest; import android.app.appsearch.SearchResult; import android.app.appsearch.SearchSpec; +import android.app.appsearch.SetSchemaRequest; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.infra.AndroidFuture; import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; /** * This class implements helper methods for synchronously interacting with AppSearch while * synchronizing AppFunction runtime and static metadata. + * + * <p>This class is not thread safe. */ public class MetadataSyncAdapter { + private static final String TAG = MetadataSyncAdapter.class.getSimpleName(); private final FutureAppSearchSession mFutureAppSearchSession; private final Executor mSyncExecutor; + private final PackageManager mPackageManager; + + // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility + // by permissions. + public static final int EXECUTE_APP_FUNCTIONS = 9; + public static final int EXECUTE_APP_FUNCTIONS_TRUSTED = 10; public MetadataSyncAdapter( @NonNull Executor syncExecutor, - @NonNull FutureAppSearchSession futureAppSearchSession) { + @NonNull FutureAppSearchSession futureAppSearchSession, + @NonNull PackageManager packageManager) { mSyncExecutor = Objects.requireNonNull(syncExecutor); mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession); + mPackageManager = Objects.requireNonNull(packageManager); + } + + /** + * This method submits a request to synchronize the AppFunction runtime and static metadata. + * + * @return A {@link AndroidFuture} that completes with a boolean value indicating whether the + * synchronization was successful. + */ + public AndroidFuture<Boolean> submitSyncRequest() { + AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>(); + mSyncExecutor.execute( + () -> { + try { + trySyncAppFunctionMetadataBlocking(); + settableSyncStatus.complete(true); + } catch (Exception e) { + settableSyncStatus.completeExceptionally(e); + } + }); + return settableSyncStatus; + } + + @WorkerThread + private void trySyncAppFunctionMetadataBlocking() + throws ExecutionException, InterruptedException { + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap = + getPackageToFunctionIdMap( + AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE, + AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, + AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME); + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap = + getPackageToFunctionIdMap( + RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID, + AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME); + + ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap = + getAddedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); + ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap = + getRemovedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); + + Set<AppSearchSchema> appRuntimeMetadataSchemas = + getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet()); + appRuntimeMetadataSchemas.add( + AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()); + + // Operation order matters here. i.e. remove -> setSchema -> add. Otherwise we would + // encounter an error trying to delete a document with no existing schema. + if (!removedFunctionsDiffMap.isEmpty()) { + RemoveByDocumentIdRequest removeByDocumentIdRequest = + buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap); + AppSearchBatchResult<String, Void> removeDocumentBatchResult = + mFutureAppSearchSession.remove(removeByDocumentIdRequest).get(); + if (!removeDocumentBatchResult.isSuccess()) { + throw convertFailedAppSearchResultToException( + removeDocumentBatchResult.getFailures().values()); + } + } + + if (!addedFunctionsDiffMap.isEmpty()) { + // TODO(b/357551503): only set schema on package diff + SetSchemaRequest addSetSchemaRequest = + buildSetSchemaRequestForRuntimeMetadataSchemas(appRuntimeMetadataSchemas); + Objects.requireNonNull(mFutureAppSearchSession.setSchema(addSetSchemaRequest).get()); + PutDocumentsRequest putDocumentsRequest = + buildPutRuntimeMetadataRequest(addedFunctionsDiffMap); + AppSearchBatchResult<String, Void> putDocumentBatchResult = + mFutureAppSearchSession.put(putDocumentsRequest).get(); + if (!putDocumentBatchResult.isSuccess()) { + throw convertFailedAppSearchResultToException( + putDocumentBatchResult.getFailures().values()); + } + } + } + + @NonNull + private static IllegalStateException convertFailedAppSearchResultToException( + @NonNull Collection<AppSearchResult<Void>> appSearchResult) { + Objects.requireNonNull(appSearchResult); + StringBuilder errorMessages = new StringBuilder(); + for (AppSearchResult<Void> result : appSearchResult) { + errorMessages.append(result.getErrorMessage()); + } + return new IllegalStateException(errorMessages.toString()); + } + + @NonNull + private PutDocumentsRequest buildPutRuntimeMetadataRequest( + @NonNull ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap) { + Objects.requireNonNull(addedFunctionsDiffMap); + PutDocumentsRequest.Builder putDocumentRequestBuilder = new PutDocumentsRequest.Builder(); + + for (int i = 0; i < addedFunctionsDiffMap.size(); i++) { + String packageName = addedFunctionsDiffMap.keyAt(i); + ArraySet<String> addedFunctionIds = addedFunctionsDiffMap.valueAt(i); + for (String addedFunctionId : addedFunctionIds) { + putDocumentRequestBuilder.addGenericDocuments( + new AppFunctionRuntimeMetadata.Builder( + packageName, + addedFunctionId, + AppFunctionRuntimeMetadata + .PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID) + .build()); + } + } + return putDocumentRequestBuilder.build(); + } + + @NonNull + private RemoveByDocumentIdRequest buildRemoveRuntimeMetadataRequest( + @NonNull ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap) { + Objects.requireNonNull(AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE); + Objects.requireNonNull(removedFunctionsDiffMap); + RemoveByDocumentIdRequest.Builder removeDocumentRequestBuilder = + new RemoveByDocumentIdRequest.Builder( + AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE); + + for (int i = 0; i < removedFunctionsDiffMap.size(); i++) { + String packageName = removedFunctionsDiffMap.keyAt(i); + ArraySet<String> removedFunctionIds = removedFunctionsDiffMap.valueAt(i); + for (String functionId : removedFunctionIds) { + String documentId = + AppFunctionRuntimeMetadata.getDocumentIdForAppFunction( + packageName, functionId); + removeDocumentRequestBuilder.addIds(documentId); + } + } + return removeDocumentRequestBuilder.build(); + } + + @NonNull + private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas( + @NonNull Set<AppSearchSchema> metadataSchemaSet) { + Objects.requireNonNull(metadataSchemaSet); + SetSchemaRequest.Builder setSchemaRequestBuilder = + new SetSchemaRequest.Builder().setForceOverride(true).addSchemas(metadataSchemaSet); + + for (AppSearchSchema runtimeMetadataSchema : metadataSchemaSet) { + String packageName = + AppFunctionRuntimeMetadata.getPackageNameFromSchema( + runtimeMetadataSchema.getSchemaType()); + byte[] packageCert = getCertificate(packageName); + if (packageCert == null) { + continue; + } + setSchemaRequestBuilder.setSchemaTypeVisibilityForPackage( + runtimeMetadataSchema.getSchemaType(), + true, + new PackageIdentifier(packageName, packageCert)); + } + + setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility( + RUNTIME_SCHEMA_TYPE, Set.of(EXECUTE_APP_FUNCTIONS)); + setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility( + RUNTIME_SCHEMA_TYPE, Set.of(EXECUTE_APP_FUNCTIONS_TRUSTED)); + return setSchemaRequestBuilder.build(); + } + + @NonNull + @WorkerThread + private Set<AppSearchSchema> getAllRuntimeMetadataSchemas( + @NonNull Set<String> staticMetadataPackages) { + Objects.requireNonNull(staticMetadataPackages); + + Set<AppSearchSchema> appRuntimeMetadataSchemas = new ArraySet<>(); + for (String packageName : staticMetadataPackages) { + appRuntimeMetadataSchemas.add( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(packageName)); + } + + return appRuntimeMetadataSchemas; } /** @@ -188,4 +391,39 @@ public class MetadataSyncAdapter { new PropertyPath(propertyPackageName))) .build(); } + + /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */ + @Nullable + private byte[] getCertificate(@NonNull String packageName) { + Objects.requireNonNull(packageName); + PackageInfo packageInfo; + try { + packageInfo = + Objects.requireNonNull( + mPackageManager.getPackageInfo( + packageName, + PackageManager.GET_META_DATA + | PackageManager.GET_SIGNING_CERTIFICATES)); + } catch (Exception e) { + Slog.d(TAG, "Package name info not found for package: " + packageName); + return null; + } + if (packageInfo.signingInfo == null) { + Slog.d(TAG, "Signing info not found for package: " + packageInfo.packageName); + return null; + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA256"); + } catch (NoSuchAlgorithmException e) { + return null; + } + Signature[] signatures = packageInfo.signingInfo.getSigningCertificateHistory(); + if (signatures == null || signatures.length == 0) { + return null; + } + md.update(signatures[0].toByteArray()); + return md.digest(); + } } |