Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 150f37a9 authored by Oluwarotimi Adesina's avatar Oluwarotimi Adesina Committed by Desh
Browse files

Introduce MetadataSyncAdapter.

This class is used to index runtime metadata in sync with the static
metadata for app functions.

Flag: android.app.appfunctions.flags.enable_app_function_manager
Test: atest FrameworksAppFunctionsTests -c
Bug: 357551503
Change-Id: Ifd031cced3bb974c5e7c183f402e6e3798078025
parent 5620e115
Loading
Loading
Loading
Loading
+149 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+296 −0
Original line number Diff line number Diff line
/*
 * 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"
    }
}