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

Commit 72374681 authored by Aldi Fahrezi's avatar Aldi Fahrezi Committed by Oluwarotimi Adesina
Browse files

Add AppFunctionRuntimeMetadata and helpers related to AppSearch for AppFunctions.

Flag: android.app.appfunctions.flags.enable_app_function_manager
Test: none
Change-Id: I1aa8b87c476644aca80a28594457f4cc6e1d49a9
parent 88f1c7e6
Loading
Loading
Loading
Loading
+210 −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 android.app.appfunctions;

import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID;
import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_ENABLED;
import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_INDEXER_PACKAGE;
import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_ENABLED_BY_DEFAULT;
import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;

import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.GlobalSearchSession;
import android.app.appsearch.JoinSpec;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResults;
import android.app.appsearch.SearchSpec;
import android.os.OutcomeReceiver;

import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Helper class containing utilities for {@link AppFunctionManager}.
 *
 * @hide
 */
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
public class AppFunctionManagerHelper {

    /**
     * Returns (through a callback) a boolean indicating whether the app function is enabled.
     * <p>
     * This method can only check app functions that are owned by the caller owned by packages
     * visible to the caller.
     * <p>
     * If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
     * <ul>
     *     <li>{@link IllegalArgumentException}, if the function is not found</li>
     *     <li>{@link SecurityException}, if the caller does not have permission to query the
     *         target package
     *     </li>
     *  </ul>
     *
     * @param functionIdentifier the identifier of the app function to check (unique within the
     *                           target package) and in most cases, these are automatically
     *                           generated by the AppFunctions SDK
     * @param targetPackage      the package name of the app function's owner
     * @param appSearchExecutor  the executor to run the metadata search mechanism through AppSearch
     * @param callbackExecutor   the executor to run the callback
     * @param callback           the callback to receive the function enabled check result
     * @hide
     */
    public static void isAppFunctionEnabled(
            @NonNull String functionIdentifier,
            @NonNull String targetPackage,
            @NonNull AppSearchManager appSearchManager,
            @NonNull Executor appSearchExecutor,
            @NonNull @CallbackExecutor Executor callbackExecutor,
            @NonNull OutcomeReceiver<Boolean, Exception> callback
    ) {
        Objects.requireNonNull(functionIdentifier);
        Objects.requireNonNull(targetPackage);
        Objects.requireNonNull(appSearchManager);
        Objects.requireNonNull(appSearchExecutor);
        Objects.requireNonNull(callbackExecutor);
        Objects.requireNonNull(callback);

        appSearchManager.createGlobalSearchSession(appSearchExecutor,
                (searchSessionResult) -> {
                    if (!searchSessionResult.isSuccess()) {
                        callbackExecutor.execute(() ->
                                callback.onError(failedResultToException(searchSessionResult)));
                        return;
                    }
                    try (GlobalSearchSession searchSession = searchSessionResult.getResultValue()) {
                        SearchResults results = searchJoinedStaticWithRuntimeAppFunctions(
                                searchSession, targetPackage, functionIdentifier);
                        results.getNextPage(appSearchExecutor,
                                listAppSearchResult -> callbackExecutor.execute(() -> {
                                    if (listAppSearchResult.isSuccess()) {
                                        callback.onResult(getEnabledStateFromSearchResults(
                                                Objects.requireNonNull(
                                                        listAppSearchResult.getResultValue())));
                                    } else {
                                        callback.onError(
                                                failedResultToException(listAppSearchResult));
                                    }
                                }));
                    } catch (Exception e) {
                        callbackExecutor.execute(() -> callback.onError(e));
                    }
                });
    }

    /**
     * Searches joined app function static and runtime metadata using the function Id and the
     * package.
     *
     * @hide
     */
    private static @NonNull SearchResults searchJoinedStaticWithRuntimeAppFunctions(
            @NonNull GlobalSearchSession session,
            @NonNull String targetPackage,
            @NonNull String functionIdentifier) {
        SearchSpec runtimeSearchSpec = getAppFunctionRuntimeMetadataSearchSpecByFunctionId(
                targetPackage);
        JoinSpec joinSpec = new JoinSpec.Builder(
                PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID)
                .setNestedSearch(
                        functionIdentifier,
                        runtimeSearchSpec).build();
        SearchSpec joinedStaticWithRuntimeSearchSpec = new SearchSpec.Builder()
                .setJoinSpec(joinSpec)
                .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
                .addFilterSchemas(
                        AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage(
                                targetPackage))
                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                .build();
        return session.search(functionIdentifier, joinedStaticWithRuntimeSearchSpec);
    }

    /**
     * Finds whether the function is enabled or not from the search results returned by
     * {@link #searchJoinedStaticWithRuntimeAppFunctions}.
     *
     * @throws IllegalArgumentException if the function is not found in the results
     * @hide
     */
    private static boolean getEnabledStateFromSearchResults(
            @NonNull List<SearchResult> joinedStaticRuntimeResults) {
        if (joinedStaticRuntimeResults.isEmpty()) {
            // Function not found.
            throw new IllegalArgumentException("App function not found.");
        } else {
            List<SearchResult> runtimeMetadataResults =
                    joinedStaticRuntimeResults.getFirst().getJoinedResults();
            if (!runtimeMetadataResults.isEmpty()) {
                Boolean result = (Boolean) runtimeMetadataResults
                        .getFirst().getGenericDocument()
                        .getProperty(PROPERTY_ENABLED);
                if (result != null) {
                    return result;
                }
            }
            // Runtime metadata not found. Using the default value in the static metadata.
            return joinedStaticRuntimeResults.getFirst().getGenericDocument()
                    .getPropertyBoolean(STATIC_PROPERTY_ENABLED_BY_DEFAULT);
        }
    }

    /**
     * Returns search spec that queries app function metadata for a specific package name by its
     * function identifier.
     *
     * @hide
     */
    public static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByFunctionId(
            @NonNull String targetPackage) {
        return new SearchSpec.Builder()
                .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
                .addFilterSchemas(
                        AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(
                                targetPackage))
                .addFilterProperties(
                        AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(
                                targetPackage),
                        List.of(AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID))
                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                .build();
    }

    /**
     * Converts a failed app search result codes into an exception.
     *
     * @hide
     */
    public static @NonNull Exception failedResultToException(
            @NonNull AppSearchResult appSearchResult) {
        return switch (appSearchResult.getResultCode()) {
            case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
                    appSearchResult.getErrorMessage());
            case AppSearchResult.RESULT_IO_ERROR -> new IOException(
                    appSearchResult.getErrorMessage());
            case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException(
                    appSearchResult.getErrorMessage());
            default -> new IllegalStateException(appSearchResult.getErrorMessage());
        };
    }
}
+204 −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 android.app.appfunctions;

import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;

import com.android.internal.annotations.VisibleForTesting;

import java.util.Objects;

/**
 * Represents runtime function metadata of an app function.
 *
 * <p>This is a temporary solution for app function indexing, as later we would like to index the
 * actual function signature entity class shape instead of just the schema info.
 *
 * @hide
 */
// TODO(b/357551503): Link to canonical docs rather than duplicating once they
// are available.
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
public class AppFunctionRuntimeMetadata extends GenericDocument {
    public static final String RUNTIME_SCHEMA_TYPE = "AppFunctionRuntimeMetadata";
    public static final String APP_FUNCTION_INDEXER_PACKAGE = "android";
    public static final String APP_FUNCTION_RUNTIME_METADATA_DB = "appfunctions-db";
    public static final String APP_FUNCTION_RUNTIME_NAMESPACE = "app_functions_runtime";
    public static final String PROPERTY_FUNCTION_ID = "functionId";
    public static final String PROPERTY_PACKAGE_NAME = "packageName";
    public static final String PROPERTY_ENABLED = "enabled";
    public static final String PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID =
            "appFunctionStaticMetadataQualifiedId";
    private static final String TAG = "AppSearchAppFunction";
    private static final String RUNTIME_SCHEMA_TYPE_SEPARATOR = "-";

    public AppFunctionRuntimeMetadata(@NonNull GenericDocument genericDocument) {
        super(genericDocument);
    }

    /**
     * Returns a per-app runtime metadata schema name, to store all functions for that package.
     */
    public static String getRuntimeSchemaNameForPackage(@NonNull String pkg) {
        return RUNTIME_SCHEMA_TYPE + RUNTIME_SCHEMA_TYPE_SEPARATOR + Objects.requireNonNull(pkg);
    }

    /**
     * Returns the document id for an app function's runtime metadata.
     */
    public static String getDocumentIdForAppFunction(
            @NonNull String pkg, @NonNull String functionId) {
        return pkg + "/" + functionId;
    }

    /**
     * Different packages have different visibility requirements. To allow for different visibility,
     * we need to have per-package app function schemas.
     * <p>This schema should be set visible to callers from the package owner itself and for callers
     * with {@link android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@link
     * android.permission.EXECUTE_APP_FUNCTIONS} permissions.
     *
     * @param packageName The package name to create a schema for.
     */
    @NonNull
    public static AppSearchSchema createAppFunctionRuntimeSchema(@NonNull String packageName) {
        return new AppSearchSchema.Builder(getRuntimeSchemaNameForPackage(packageName))
                .addProperty(
                        new AppSearchSchema.StringPropertyConfig.Builder(PROPERTY_FUNCTION_ID)
                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                .setIndexingType(
                                        AppSearchSchema.StringPropertyConfig
                                                .INDEXING_TYPE_EXACT_TERMS)
                                .setTokenizerType(
                                        AppSearchSchema.StringPropertyConfig
                                                .TOKENIZER_TYPE_VERBATIM)
                                .build())
                .addProperty(
                        new AppSearchSchema.StringPropertyConfig.Builder(PROPERTY_PACKAGE_NAME)
                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                .setIndexingType(
                                        AppSearchSchema.StringPropertyConfig
                                                .INDEXING_TYPE_EXACT_TERMS)
                                .setTokenizerType(
                                        AppSearchSchema.StringPropertyConfig
                                                .TOKENIZER_TYPE_VERBATIM)
                                .build())
                .addProperty(
                        new AppSearchSchema.BooleanPropertyConfig.Builder(PROPERTY_ENABLED)
                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                .build())
                .addProperty(
                        new AppSearchSchema.StringPropertyConfig.Builder(
                                PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID)
                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                .setJoinableValueType(
                                        AppSearchSchema.StringPropertyConfig
                                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
                                .build())
                .addParentType(RUNTIME_SCHEMA_TYPE)
                .build();
    }

    /**
     * Returns the function id. This might look like "com.example.message#send_message".
     */
    @NonNull
    public String getFunctionId() {
        return Objects.requireNonNull(getPropertyString(PROPERTY_FUNCTION_ID));
    }

    /**
     * Returns the package name of the package that owns this function.
     */
    @NonNull
    public String getPackageName() {
        return Objects.requireNonNull(getPropertyString(PROPERTY_PACKAGE_NAME));
    }

    /**
     * Returns if the function is set to be enabled or not. If not set, the {@link
     * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT} value would be used.
     */
    @Nullable
    public Boolean getEnabled() {
        return (Boolean) getProperty(PROPERTY_ENABLED);
    }

    /**
     * Returns the qualified id linking to the static metadata of the app function.
     */
    @Nullable
    @VisibleForTesting
    public String getAppFunctionStaticMetadataQualifiedId() {
        return getPropertyString(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID);
    }

    public static final class Builder extends GenericDocument.Builder<Builder> {
        /**
         * Creates a Builder for a {@link AppFunctionRuntimeMetadata}.
         *
         * @param packageName               the name of the package that owns the function.
         * @param functionId                the id of the function.
         * @param staticMetadataQualifiedId the qualified static metadata id that this runtime
         *                                  metadata refers to.
         */
        public Builder(
                @NonNull String packageName,
                @NonNull String functionId,
                @NonNull String staticMetadataQualifiedId) {
            super(
                    APP_FUNCTION_RUNTIME_NAMESPACE,
                    getDocumentIdForAppFunction(
                            Objects.requireNonNull(packageName),
                            Objects.requireNonNull(functionId)),
                    getRuntimeSchemaNameForPackage(packageName));
            setPropertyString(PROPERTY_PACKAGE_NAME, packageName);
            setPropertyString(PROPERTY_FUNCTION_ID, functionId);

            // Set qualified id automatically
            setPropertyString(
                    PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID,
                    staticMetadataQualifiedId);
        }


        /**
         * Sets an indicator specifying if the function is enabled or not. This would override the
         * default enabled state in the static metadata ({@link
         * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}).
         */
        @NonNull
        public Builder setEnabled(boolean enabled) {
            setPropertyBoolean(PROPERTY_ENABLED, enabled);
            return this;
        }

        /**
         * Creates the {@link AppFunctionRuntimeMetadata} GenericDocument.
         */
        @NonNull
        public AppFunctionRuntimeMetadata build() {
            return new AppFunctionRuntimeMetadata(super.build());
        }
    }
}
+72 −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 android.app.appfunctions;

import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.appsearch.util.DocumentIdUtil;

import java.util.Objects;

/**
 * Contains constants and helper related to static metadata represented with {@code
 * com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata}.
 * <p>
 * The constants listed here **must not change** and be kept consistent with the canonical
 * static metadata class.
 *
 * @hide
 */
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
public class AppFunctionStaticMetadataHelper {
    public static final String STATIC_SCHEMA_TYPE = "AppFunctionStaticMetadata";
    public static final String STATIC_PROPERTY_ENABLED_BY_DEFAULT = "enabledByDefault";

    public static final String APP_FUNCTION_STATIC_NAMESPACE = "app_functions";

    // These are constants that has to be kept the same with {@code
    // com.android.server.appsearch.appsindexer.appsearchtypes.AppSearchHelper}.
    public static final String APP_FUNCTION_STATIC_METADATA_DB = "apps-db";
    public static final String APP_FUNCTION_INDEXER_PACKAGE = "android";

    /**
     * Returns a per-app static metadata schema name, to store all functions for that package.
     */
    public static String getStaticSchemaNameForPackage(@NonNull String pkg) {
        return STATIC_SCHEMA_TYPE + "-" + Objects.requireNonNull(pkg);
    }

    /** Returns the document id for an app function's static metadata. */
    public static String getDocumentIdForAppFunction(
            @NonNull String pkg, @NonNull String functionId) {
        return pkg + "/" + functionId;
    }

    /**
     * Returns the fully qualified Id used in AppSearch for the given package and function id
     * app function static metadata.
     */
    public static String getStaticMetadataQualifiedId(String packageName, String functionId) {
        return DocumentIdUtil.createQualifiedId(
                APP_FUNCTION_INDEXER_PACKAGE,
                APP_FUNCTION_STATIC_METADATA_DB,
                APP_FUNCTION_STATIC_NAMESPACE,
                getDocumentIdForAppFunction(packageName, functionId));
    }
}
+130 −0

File added.

Preview size limit exceeded, changes collapsed.