diff options
4 files changed, 479 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(); + } } diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt index 3ebf68937674..b938c3ccdd94 100644 --- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -16,15 +16,24 @@ package com.android.server.appfunctions import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema +import android.app.appfunctions.AppFunctionStaticMetadataHelper import android.app.appsearch.AppSearchManager import android.app.appsearch.AppSearchManager.SearchContext +import android.app.appsearch.GenericDocument import android.app.appsearch.PutDocumentsRequest +import android.app.appsearch.SearchResult +import android.app.appsearch.SearchSpec import android.app.appsearch.SetSchemaRequest import android.util.ArrayMap import android.util.ArraySet import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.infra.AndroidFuture +import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean import org.junit.After import org.junit.Before import org.junit.Test @@ -36,6 +45,7 @@ class MetadataSyncAdapterTest { private val context = InstrumentationRegistry.getInstrumentation().targetContext private val appSearchManager = context.getSystemService(AppSearchManager::class.java) private val testExecutor = MoreExecutors.directExecutor() + private val packageManager = context.packageManager @Before @After @@ -72,6 +82,7 @@ class MetadataSyncAdapterTest { MetadataSyncAdapter( testExecutor, FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext), + packageManager, ) val packageToFunctionIdMap = metadataSyncAdapter.getPackageToFunctionIdMap( @@ -122,6 +133,7 @@ class MetadataSyncAdapterTest { MetadataSyncAdapter( testExecutor, FutureAppSearchSessionImpl(appSearchManager, testExecutor, searchContext), + packageManager, ) val packageToFunctionIdMap = metadataSyncAdapter.getPackageToFunctionIdMap( @@ -159,6 +171,70 @@ class MetadataSyncAdapterTest { } @Test + fun syncMetadata_noDiff() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val appSearchSession = + PartialFakeFutureAppSearchSession(appSearchManager, testExecutor, searchContext) + val fakeFunctionId = "syncMetadata_noDiff" + val fakeStaticMetadata: GenericDocument = + GenericDocument.Builder<GenericDocument.Builder<*>>( + AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_NAMESPACE, + AppFunctionStaticMetadataHelper.getDocumentIdForAppFunction( + TEST_TARGET_PKG_NAME, + fakeFunctionId, + ), + AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE, + ) + .setPropertyString( + AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME, + TEST_TARGET_PKG_NAME, + ) + .setPropertyString( + AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, + fakeFunctionId, + ) + .build() + appSearchSession.overrideStaticMetadataSearchResult = mutableListOf(fakeStaticMetadata) + val putCorrespondingSchema = + appSearchSession + .setSchema( + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema( + TEST_TARGET_PKG_NAME + ), + ) + .setForceOverride(true) + .build() + ) + .get() + assertThat(putCorrespondingSchema).isNotNull() + val putCorrespondingRuntimeMetadata = + appSearchSession + .put( + PutDocumentsRequest.Builder() + .addGenericDocuments( + AppFunctionRuntimeMetadata.Builder( + TEST_TARGET_PKG_NAME, + fakeFunctionId, + "", + ) + .build() + ) + .build() + ) + .get() + assertThat(putCorrespondingRuntimeMetadata.isSuccess).isTrue() + val metadataSyncAdapter = + MetadataSyncAdapter(testExecutor, appSearchSession, context.packageManager) + + val submitSyncRequest = metadataSyncAdapter.submitSyncRequest() + + assertThat(submitSyncRequest.get()).isTrue() + } + + @Test fun getAddedFunctionsDiffMap_addedFunction() { val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() staticPackageToFunctionMap.putAll( @@ -180,6 +256,39 @@ class MetadataSyncAdapterTest { } @Test + fun syncMetadata_addedFunction() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val appSearchSession = + PartialFakeFutureAppSearchSession(appSearchManager, testExecutor, searchContext) + val fakeFunctionId = "addedFunction1" + val fakeStaticMetadata: GenericDocument = + GenericDocument.Builder<GenericDocument.Builder<*>>( + AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_NAMESPACE, + AppFunctionStaticMetadataHelper.getDocumentIdForAppFunction( + TEST_TARGET_PKG_NAME, + fakeFunctionId, + ), + AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE, + ) + .setPropertyString( + AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME, + TEST_TARGET_PKG_NAME, + ) + .setPropertyString( + AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, + fakeFunctionId, + ) + .build() + appSearchSession.overrideStaticMetadataSearchResult = mutableListOf(fakeStaticMetadata) + val metadataSyncAdapter = + MetadataSyncAdapter(testExecutor, appSearchSession, context.packageManager) + + val submitSyncRequest = metadataSyncAdapter.submitSyncRequest() + + assertThat(submitSyncRequest.get()).isTrue() + } + + @Test fun getAddedFunctionsDiffMap_addedFunctionNewPackage() { val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() staticPackageToFunctionMap.putAll( @@ -215,6 +324,51 @@ class MetadataSyncAdapterTest { } @Test + fun syncMetadata_removedFunction() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val appSearchSession = + PartialFakeFutureAppSearchSession(appSearchManager, testExecutor, searchContext) + val fakeFunctionId = "syncMetadata_removedFunction" + val putCorrespondingSchema = + appSearchSession + .setSchema( + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema( + TEST_TARGET_PKG_NAME + ), + ) + .setForceOverride(true) + .build() + ) + .get() + assertThat(putCorrespondingSchema).isNotNull() + val putStaleRuntimeMetadata = + appSearchSession + .put( + PutDocumentsRequest.Builder() + .addGenericDocuments( + AppFunctionRuntimeMetadata.Builder( + TEST_TARGET_PKG_NAME, + fakeFunctionId, + "", + ) + .build() + ) + .build() + ) + .get() + assertThat(putStaleRuntimeMetadata.isSuccess).isTrue() + val metadataSyncAdapter = + MetadataSyncAdapter(testExecutor, appSearchSession, context.packageManager) + + val submitSyncRequest = metadataSyncAdapter.submitSyncRequest() + + assertThat(submitSyncRequest.get()).isTrue() + } + + @Test fun getRemovedFunctionsDiffMap_noDiff() { val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() staticPackageToFunctionMap.putAll( @@ -271,4 +425,49 @@ class MetadataSyncAdapterTest { const val TEST_DB: String = "test_db" const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests" } + + class PartialFakeFutureAppSearchSession( + appSearchManager: AppSearchManager, + executor: Executor, + appSearchContext: SearchContext, + ) : FutureAppSearchSessionImpl(appSearchManager, executor, appSearchContext) { + var overrideStaticMetadataSearchResult: MutableList<GenericDocument> = mutableListOf() + private val overrideUsed = AtomicBoolean(false) + + // Overriding this method to fake searching for static metadata. + // Static metadata is the source of truth for the metadata sync behaviour since the sync is + // updating the runtime metadata to match the existing static metadata. + override fun search( + queryExpression: String, + searchSpec: SearchSpec, + ): AndroidFuture<FutureSearchResults> { + if ( + searchSpec.filterSchemas.contains( + AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE + ) + ) { + val futureSearchResults = + object : FutureSearchResults { + override fun getNextPage(): AndroidFuture<MutableList<SearchResult>> { + if (overrideUsed.get()) { + overrideStaticMetadataSearchResult.clear() + return AndroidFuture.completedFuture(mutableListOf()) + } + overrideUsed.set(true) + return AndroidFuture.completedFuture( + overrideStaticMetadataSearchResult + .map { + SearchResult.Builder(TEST_TARGET_PKG_NAME, TEST_DB) + .setGenericDocument(it) + .build() + } + .toMutableList() + ) + } + } + return AndroidFuture.completedFuture(futureSearchResults) + } + return super.search(queryExpression, searchSpec) + } + } } |