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

Commit 4c328661 authored by Alexander Dorokhine's avatar Alexander Dorokhine Committed by Android (Google) Code Review
Browse files

Merge "Support queries in AppSearchImpl and FakeIcing."

parents 1a7f6ce7 4604f23c
Loading
Loading
Loading
Loading
+44 −54
Original line number Diff line number Diff line
@@ -15,7 +15,6 @@
 */
package android.app.appsearch;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.SystemService;
import android.content.Context;
@@ -35,8 +34,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;

/**
 * This class provides access to the centralized AppSearch index maintained by the system.
@@ -82,8 +79,8 @@ public class AppSearchManager {
     *     <li>Removal of an existing type
     *     <li>Removal of a property from a type
     *     <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property
     *     <li>For properties of {@code Document} type, changing the schema type of
     *         {@code Document Documents} of that property
     *     <li>For properties of {@code AppSearchDocument} type, changing the schema type of
     *         {@code AppSearchDocument}s of that property
     *     <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
     *         {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
     *             OPTIONAL} property into a
@@ -156,15 +153,15 @@ public class AppSearchManager {
    }

    /**
     * Index {@link AppSearchDocument Documents} into AppSearch.
     * Index {@link AppSearchDocument}s into AppSearch.
     *
     * <p>You should not call this method directly; instead, use the
     * {@code AppSearch#putDocuments()} API provided by JetPack.
     *
     * <p>Each {@link AppSearchDocument Document's} {@code schemaType} field must be set to the
     * name of a schema type previously registered via the {@link #setSchema} method.
     * <p>Each {@link AppSearchDocument}'s {@code schemaType} field must be set to the name of a
     * schema type previously registered via the {@link #setSchema} method.
     *
     * @param documents {@link AppSearchDocument Documents} that need to be indexed.
     * @param documents {@link AppSearchDocument}s that need to be indexed.
     * @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they
     *     were successfully indexed, or a {@link Throwable} describing the failure if they could
     *     not be indexed.
@@ -253,8 +250,10 @@ public class AppSearchManager {
    }

    /**
     * This method searches for documents based on a given query string. It also accepts
     * specifications regarding how to search and format the results.
     * Searches a document based on a given query string.
     *
     * <p>You should not call this method directly; instead, use the {@code AppSearch#query()} API
     * provided by JetPack.
     *
     * <p>Currently we support following features in the raw query format:
     * <ul>
@@ -288,59 +287,50 @@ public class AppSearchManager {
     *     ‘Video’ schema type.
     * </ul>
     *
     * <p> It is strongly recommended to use Jetpack APIs.
     *
     * @param queryExpression Query String to search.
     * @param searchSpec Spec for setting filters, raw query etc.
     * @param executor Executor on which to invoke the callback.
     * @param callback  Callback to receive errors resulting from the query operation. If the
     *                 operation succeeds, the callback will be invoked with {@code null}.
     * @hide
     */
    @NonNull
    public void query(
            @NonNull String queryExpression,
            @NonNull SearchSpec searchSpec,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull BiConsumer<? super SearchResults, ? super Throwable> callback) {
        AndroidFuture<byte[]> future = new AndroidFuture<>();
        future.whenCompleteAsync((searchResultBytes, err) -> {
            if (err != null) {
                callback.accept(null, err);
                return;
    public AppSearchResult<SearchResults> query(
            @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
        // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
        //     them in one big list.
        AndroidFuture<AppSearchResult> searchResultFuture = new AndroidFuture<>();
        try {
            SearchSpecProto searchSpecProto = searchSpec.getSearchSpecProto();
            searchSpecProto = searchSpecProto.toBuilder().setQuery(queryExpression).build();
            mService.query(
                    searchSpecProto.toByteArray(),
                    searchSpec.getResultSpecProto().toByteArray(),
                    searchSpec.getScoringSpecProto().toByteArray(),
                    searchResultFuture);
        } catch (RemoteException e) {
            searchResultFuture.completeExceptionally(e);
        }

        // Deserialize the protos into Document objects
        AppSearchResult<byte[]> searchResultBytes = getFutureOrThrow(searchResultFuture);
        if (!searchResultBytes.isSuccess()) {
            return AppSearchResult.newFailedResult(
                    searchResultBytes.getResultCode(), searchResultBytes.getErrorMessage());
        }
            if (searchResultBytes != null) {
        SearchResultProto searchResultProto;
        try {
                    searchResultProto = SearchResultProto.parseFrom(searchResultBytes);
            searchResultProto = SearchResultProto.parseFrom(searchResultBytes.getResultValue());
        } catch (InvalidProtocolBufferException e) {
                    callback.accept(null, e);
                    return;
            return AppSearchResult.newFailedResult(
                    AppSearchResult.RESULT_INTERNAL_ERROR, e.getMessage());
        }
        if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
                    // TODO(sidchhabra): Add better exception handling.
                    callback.accept(
                            null,
                            new RuntimeException(searchResultProto.getStatus().getMessage()));
                    return;
                }
                SearchResults searchResults = new SearchResults(searchResultProto);
                callback.accept(searchResults, null);
                return;
            }
            // Nothing was supplied in the future at all
            callback.accept(
                    null, new IllegalStateException("Unknown failure occurred while querying"));
        }, executor);
        try {
            SearchSpecProto searchSpecProto = searchSpec.getSearchSpecProto();
            searchSpecProto = searchSpecProto.toBuilder().setQuery(queryExpression).build();
            mService.query(searchSpecProto.toByteArray(),
                    searchSpec.getResultSpecProto().toByteArray(),
                    searchSpec.getScoringSpecProto().toByteArray(), future);
        } catch (RemoteException e) {
            future.completeExceptionally(e);
            // This should never happen; AppSearchManagerService should catch failed searchResults
            // entries and transmit them as a failed AppSearchResult.
            return AppSearchResult.newFailedResult(
                    AppSearchResult.RESULT_INTERNAL_ERROR,
                    searchResultProto.getStatus().getMessage());
        }

        return AppSearchResult.newSuccessfulResult(new SearchResults(searchResultProto));
    }

    private static <T> T getFutureOrThrow(@NonNull AndroidFuture<T> future) {
+5 −4
Original line number Diff line number Diff line
@@ -66,9 +66,10 @@ interface IAppSearchManager {
     * @param searchSpecBytes Serialized SearchSpecProto.
     * @param resultSpecBytes Serialized SearchResultsProto.
     * @param scoringSpecBytes Serialized ScoringSpecProto.
     * @param callback {@link AndroidFuture}. Will be completed with a serialized
     *     {@link SearchResultsProto}, or completed exceptionally if query fails.
     * @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link byte[]}&gt;&gt;
     *     Will be completed with a serialized {@link SearchResultsProto}.
     */
    void query(in byte[] searchSpecBytes, in byte[] resultSpecBytes,
            in byte[] scoringSpecBytes, in AndroidFuture callback);
    void query(
        in byte[] searchSpecBytes, in byte[] resultSpecBytes, in byte[] scoringSpecBytes,
        in AndroidFuture<AppSearchResult> callback);
}
+36 −18
Original line number Diff line number Diff line
@@ -27,14 +27,15 @@ import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import com.android.server.appsearch.impl.AppSearchImpl;
import com.android.server.appsearch.impl.FakeIcing;
import com.android.server.appsearch.impl.ImplInstanceManager;

import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.ResultSpecProto;
import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.ScoringSpecProto;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.proto.SearchSpecProto;
import com.google.android.icing.protobuf.InvalidProtocolBufferException;
import com.google.android.icing.proto.StatusProto;

import java.io.IOException;
import java.util.List;
@@ -46,11 +47,8 @@ public class AppSearchManagerService extends SystemService {

    public AppSearchManagerService(Context context) {
        super(context);
        mFakeIcing = new FakeIcing();
    }

    private final FakeIcing mFakeIcing;

    @Override
    public void onStart() {
        publishBinderService(Context.APP_SEARCH_SERVICE, new Stub());
@@ -144,23 +142,43 @@ public class AppSearchManagerService extends SystemService {
            }
        }

        // TODO(sidchhabra):Init FakeIcing properly.
        // TODO(sidchhabra): Do this in a threadpool.
        @Override
        public void query(@NonNull byte[] searchSpec, @NonNull byte[] resultSpec,
                @NonNull byte[] scoringSpec, AndroidFuture callback) {
            Preconditions.checkNotNull(searchSpec);
            Preconditions.checkNotNull(resultSpec);
            Preconditions.checkNotNull(scoringSpec);
            SearchSpecProto searchSpecProto = null;
        public void query(
                @NonNull byte[] searchSpecBytes,
                @NonNull byte[] resultSpecBytes,
                @NonNull byte[] scoringSpecBytes,
                @NonNull AndroidFuture<AppSearchResult> callback) {
            Preconditions.checkNotNull(searchSpecBytes);
            Preconditions.checkNotNull(resultSpecBytes);
            Preconditions.checkNotNull(scoringSpecBytes);
            Preconditions.checkNotNull(callback);
            int callingUid = Binder.getCallingUidOrThrow();
            int callingUserId = UserHandle.getUserId(callingUid);
            long callingIdentity = Binder.clearCallingIdentity();
            try {
                searchSpecProto = SearchSpecProto.parseFrom(searchSpec);
            } catch (InvalidProtocolBufferException e) {
                throw new RuntimeException(e);
                SearchSpecProto searchSpecProto = SearchSpecProto.parseFrom(searchSpecBytes);
                ResultSpecProto resultSpecProto = ResultSpecProto.parseFrom(resultSpecBytes);
                ScoringSpecProto scoringSpecProto = ScoringSpecProto.parseFrom(scoringSpecBytes);
                AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
                SearchResultProto searchResultProto =
                        impl.query(callingUid, searchSpecProto, resultSpecProto, scoringSpecProto);
                // TODO(sidchhabra): Translate SearchResultProto errors into error codes. This might
                //     better be done in AppSearchImpl by throwing an AppSearchException.
                if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
                    callback.complete(
                            AppSearchResult.newFailedResult(
                                    AppSearchResult.RESULT_INTERNAL_ERROR,
                                    searchResultProto.getStatus().getMessage()));
                } else {
                    callback.complete(
                            AppSearchResult.newSuccessfulResult(searchResultProto.toByteArray()));
                }
            } catch (Throwable t) {
                callback.complete(throwableToFailedResult(t));
            } finally {
                Binder.restoreCallingIdentity(callingIdentity);
            }
            SearchResultProto searchResults =
                    mFakeIcing.query(searchSpecProto.getQuery());
            callback.complete(searchResults.toByteArray());
        }

        private <ValueType> AppSearchResult<ValueType> throwableToFailedResult(
+80 −0
Original line number Diff line number Diff line
@@ -20,14 +20,21 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.Context;
import android.util.ArraySet;

import com.android.internal.annotations.VisibleForTesting;

import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyConfigProto;
import com.google.android.icing.proto.PropertyProto;
import com.google.android.icing.proto.ResultSpecProto;
import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.proto.ScoringSpecProto;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.proto.SearchSpecProto;

import java.util.Set;

/**
 * Manages interaction with {@link FakeIcing} and other components to implement AppSearch
@@ -122,12 +129,85 @@ public final class AppSearchImpl {
    public DocumentProto getDocument(int callingUid, @NonNull String uri) {
        String typePrefix = getTypePrefix(callingUid);
        DocumentProto document = mFakeIcing.get(uri);

        // TODO(b/146526096): Since FakeIcing doesn't currently handle namespaces, we perform a
        //  post-filter to make sure we don't return documents we shouldn't. This should be removed
        //  once the real Icing Lib is implemented.
        if (!document.getNamespace().equals(typePrefix)) {
            return null;
        }

        // Rewrite the type names to remove the app's prefix
        DocumentProto.Builder documentBuilder = document.toBuilder();
        rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ false);
        return documentBuilder.build();
    }

    /**
     * Executes a query against the AppSearch index and returns results.
     *
     * @param callingUid The uid of the app calling AppSearch.
     * @param searchSpec Defines what and how to search
     * @param resultSpec Defines what results to show
     * @param scoringSpec Defines how to order results
     * @return The results of performing this search  The proto might have no {@code results} if no
     *     documents matched the query.
     */
    @NonNull
    public SearchResultProto query(
            int callingUid,
            @NonNull SearchSpecProto searchSpec,
            @NonNull ResultSpecProto resultSpec,
            @NonNull ScoringSpecProto scoringSpec) {
        String typePrefix = getTypePrefix(callingUid);
        SearchResultProto searchResults = mFakeIcing.query(searchSpec.getQuery());
        if (searchResults.getResultsCount() == 0) {
            return searchResults;
        }
        Set<String> qualifiedSearchFilters = null;
        if (searchSpec.getSchemaTypeFiltersCount() > 0) {
            qualifiedSearchFilters = new ArraySet<>(searchSpec.getSchemaTypeFiltersCount());
            for (String schema : searchSpec.getSchemaTypeFiltersList()) {
                String qualifiedSchema = typePrefix + schema;
                qualifiedSearchFilters.add(qualifiedSchema);
            }
        }
        // Rewrite the type names to remove the app's prefix
        SearchResultProto.Builder searchResultsBuilder = searchResults.toBuilder();
        for (int i = 0; i < searchResultsBuilder.getResultsCount(); i++) {
            if (searchResults.getResults(i).hasDocument()) {
                SearchResultProto.ResultProto.Builder resultBuilder =
                        searchResultsBuilder.getResults(i).toBuilder();

                // TODO(b/145631811): Since FakeIcing doesn't currently handle namespaces, we
                //  perform a post-filter to make sure we don't return documents we shouldn't. This
                //  should be removed once the real Icing Lib is implemented.
                if (!resultBuilder.getDocument().getNamespace().equals(typePrefix)) {
                    searchResultsBuilder.removeResults(i);
                    i--;
                    continue;
                }

                // TODO(b/145631811): Since FakeIcing doesn't currently handle type names, we
                //  perform a post-filter to make sure we don't return documents we shouldn't. This
                //  should be removed once the real Icing Lib is implemented.
                if (qualifiedSearchFilters != null
                        && !qualifiedSearchFilters.contains(
                                resultBuilder.getDocument().getSchema())) {
                    searchResultsBuilder.removeResults(i);
                    i--;
                    continue;
                }

                DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
                rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/false);
                resultBuilder.setDocument(documentBuilder);
                searchResultsBuilder.setResults(i, resultBuilder);
            }
        }
        return searchResultsBuilder.build();
    }

    /**
     * Rewrites all types mentioned anywhere in {@code documentBuilder} to prepend or remove
     * {@code typePrefix}.
+19 −6
Original line number Diff line number Diff line
@@ -88,22 +88,35 @@ public class FakeIcing {
    }

    /**
     * Returns documents containing the given term.
     * Returns documents containing all words in the given query string.
     *
     * @param term A single exact term to look up in the index.
     * @param queryExpression A set of words to search for. They will be implicitly AND-ed together.
     *     No operators are supported.
     * @return A {@link SearchResultProto} containing the matching documents, which may have no
     *   results if no documents match.
     */
    @NonNull
    public SearchResultProto query(@NonNull String term) {
        String normTerm = normalizeString(term);
        Set<Integer> docIds = mIndex.get(normTerm);
    public SearchResultProto query(@NonNull String queryExpression) {
        String[] terms = normalizeString(queryExpression).split("\\s+");
        SearchResultProto.Builder results = SearchResultProto.newBuilder()
                .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK));
        if (terms.length == 0) {
            return results.build();
        }
        Set<Integer> docIds = mIndex.get(terms[0]);
        if (docIds == null || docIds.isEmpty()) {
            return results.build();
        }

        for (int i = 1; i < terms.length; i++) {
            Set<Integer> termDocIds = mIndex.get(terms[i]);
            if (termDocIds == null) {
                return results.build();
            }
            docIds.retainAll(termDocIds);
            if (docIds.isEmpty()) {
                return results.build();
            }
        }
        for (int docId : docIds) {
            DocumentProto document = mDocStore.get(docId);
            if (document != null) {
+1 −1

File changed.

Contains only whitespace changes.

Loading