diff options
2 files changed, 445 insertions, 0 deletions
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java new file mode 100644 index 000000000000..be5770b280dc --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appfunctions; + +import android.annotation.NonNull; +import android.annotation.WorkerThread; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchSpec; +import android.util.ArrayMap; +import android.util.ArraySet; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults; + +import java.util.List; +import java.util.Objects; +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. + */ +public class MetadataSyncAdapter { + private final FutureAppSearchSession mFutureAppSearchSession; + private final Executor mSyncExecutor; + + public MetadataSyncAdapter( + @NonNull Executor syncExecutor, + @NonNull FutureAppSearchSession futureAppSearchSession) { + mSyncExecutor = Objects.requireNonNull(syncExecutor); + mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession); + } + + /** + * This method returns a map of package names to a set of function ids that are in the static + * metadata but not in the runtime metadata. + * + * @param staticPackageToFunctionMap A map of package names to a set of function ids from the + * static metadata. + * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the + * runtime metadata. + * @return A map of package names to a set of function ids that are in the static metadata but + * not in the runtime metadata. + */ + @NonNull + @VisibleForTesting + static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { + return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); + } + + /** + * This method returns a map of package names to a set of function ids that are in the runtime + * metadata but not in the static metadata. + * + * @param staticPackageToFunctionMap A map of package names to a set of function ids from the + * static metadata. + * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the + * runtime metadata. + * @return A map of package names to a set of function ids that are in the runtime metadata but + * not in the static metadata. + */ + @NonNull + @VisibleForTesting + static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { + return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap); + } + + @NonNull + private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> packageToFunctionMapA, + ArrayMap<String, ArraySet<String>> packageToFunctionMapB) { + ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>(); + for (String packageName : packageToFunctionMapA.keySet()) { + if (!packageToFunctionMapB.containsKey(packageName)) { + diffMap.put(packageName, packageToFunctionMapA.get(packageName)); + continue; + } + ArraySet<String> diffFunctions = new ArraySet<>(); + for (String functionId : + Objects.requireNonNull(packageToFunctionMapA.get(packageName))) { + if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName)) + .contains(functionId)) { + diffFunctions.add(functionId); + } + } + if (!diffFunctions.isEmpty()) { + diffMap.put(packageName, diffFunctions); + } + } + return diffMap; + } + + /** + * This method returns a map of package names to a set of function ids. + * + * @param queryExpression The query expression to use when searching for AppFunction metadata. + * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata. + * @return A map of package names to a set of function ids. + * @throws ExecutionException If the future search results fail to execute. + * @throws InterruptedException If the future search results are interrupted. + */ + @NonNull + @VisibleForTesting + @WorkerThread + ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap( + @NonNull String queryExpression, + @NonNull SearchSpec metadataSearchSpec, + @NonNull String propertyFunctionId, + @NonNull String propertyPackageName) + throws ExecutionException, InterruptedException { + ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>(); + FutureSearchResults futureSearchResults = + mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get(); + List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get(); + // TODO(b/357551503): This could be expensive if we have more functions + while (!searchResultsList.isEmpty()) { + for (SearchResult searchResult : searchResultsList) { + String packageName = + searchResult.getGenericDocument().getPropertyString(propertyPackageName); + String functionId = + searchResult.getGenericDocument().getPropertyString(propertyFunctionId); + packageToFunctionIds + .computeIfAbsent(packageName, k -> new ArraySet<>()) + .add(functionId); + } + searchResultsList = futureSearchResults.getNextPage().get(); + } + return packageToFunctionIds; + } +} diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt new file mode 100644 index 000000000000..1061da28f799 --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.appfunctions + +import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID +import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME +import android.app.appsearch.AppSearchManager +import android.app.appsearch.AppSearchManager.SearchContext +import android.app.appsearch.PutDocumentsRequest +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.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class MetadataSyncAdapterTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val appSearchManager = context.getSystemService(AppSearchManager::class.java) + private val testExecutor = MoreExecutors.directExecutor() + + @Before + @After + fun clearData() { + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() + it.setSchema(setSchemaRequest) + } + } + + @Test + fun getPackageToFunctionIdMap() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val functionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build() + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()) + .addSchemas( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + ) + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaResponse = it.setSchema(setSchemaRequest).get() + assertThat(setSchemaResponse).isNotNull() + val appSearchBatchResult = it.put(putDocumentsRequest).get() + assertThat(appSearchBatchResult.isSuccess).isTrue() + } + + val metadataSyncAdapter = + MetadataSyncAdapter( + testExecutor, + FutureAppSearchSession(appSearchManager, testExecutor, searchContext), + ) + val searchSpec: SearchSpec = + SearchSpec.Builder() + .addFilterSchemas( + AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + .schemaType, + ) + .build() + val packageToFunctionIdMap = + metadataSyncAdapter.getPackageToFunctionIdMap( + "", + searchSpec, + PROPERTY_FUNCTION_ID, + PROPERTY_PACKAGE_NAME, + ) + + assertThat(packageToFunctionIdMap).isNotNull() + assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId") + } + + @Test + fun getPackageToFunctionIdMap_multipleDocuments() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val functionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build() + val functionRuntimeMetadata1 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build() + val functionRuntimeMetadata2 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build() + val functionRuntimeMetadata3 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build() + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()) + .addSchemas( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + ) + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments( + functionRuntimeMetadata, + functionRuntimeMetadata1, + functionRuntimeMetadata2, + functionRuntimeMetadata3, + ) + .build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaResponse = it.setSchema(setSchemaRequest).get() + assertThat(setSchemaResponse).isNotNull() + val appSearchBatchResult = it.put(putDocumentsRequest).get() + assertThat(appSearchBatchResult.isSuccess).isTrue() + } + + val metadataSyncAdapter = + MetadataSyncAdapter( + testExecutor, + FutureAppSearchSession(appSearchManager, testExecutor, searchContext), + ) + val searchSpec: SearchSpec = + SearchSpec.Builder() + .setResultCountPerPage(1) + .addFilterSchemas( + AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + .schemaType, + ) + .build() + val packageToFunctionIdMap = + metadataSyncAdapter.getPackageToFunctionIdMap( + "", + searchSpec, + PROPERTY_FUNCTION_ID, + PROPERTY_PACKAGE_NAME, + ) + + assertThat(packageToFunctionIdMap).isNotNull() + assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]) + .containsExactly( + "testFunctionId", + "testFunctionId1", + "testFunctionId2", + "testFunctionId3", + ) + } + + @Test + fun getAddedFunctionsDiffMap_noDiff() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = + ArrayMap(staticPackageToFunctionMap) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getAddedFunctionsDiffMap_addedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.size).isEqualTo(1) + assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2") + } + + @Test + fun getAddedFunctionsDiffMap_addedFunctionNewPackage() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.size).isEqualTo(1) + assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1") + } + + @Test + fun getAddedFunctionsDiffMap_removedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getRemovedFunctionsDiffMap_noDiff() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = + ArrayMap(staticPackageToFunctionMap) + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getRemovedFunctionsDiffMap_removedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.size).isEqualTo(1) + assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1") + } + + @Test + fun getRemovedFunctionsDiffMap_addedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + private companion object { + const val TEST_DB: String = "test_db" + const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests" + } +} |