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

Commit 536afa99 authored by Oluwarotimi Adesina's avatar Oluwarotimi Adesina Committed by Android (Google) Code Review
Browse files

Merge "Add AppFunctionRuntimeMetadata and helpers related to AppSearch for AppFunctions." into main

parents 86061f28 72374681
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.