diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 8e8f21863ca73..ad51a5c4158ef 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -25,7 +25,6 @@ import com.android.internal.infra.AndroidFuture; import com.google.android.icing.proto.DocumentProto; import com.google.android.icing.proto.SearchResultProto; -import com.google.android.icing.proto.SearchSpecProto; import com.google.android.icing.proto.StatusProto; import com.google.protobuf.InvalidProtocolBufferException; @@ -294,13 +293,7 @@ public class AppSearchManager { // them in one big list. AndroidFuture 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); + mService.query(queryExpression, searchSpec.getBundle(), searchResultFuture); } catch (RemoteException e) { searchResultFuture.completeExceptionally(e); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index b95fe314120f3..c710a29919e3e 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -67,14 +67,14 @@ interface IAppSearchManager { /** * Searches a document based on a given specifications. * - * @param searchSpecBytes Serialized SearchSpecProto. - * @param resultSpecBytes Serialized SearchResultsProto. - * @param scoringSpecBytes Serialized ScoringSpecProto. + * @param queryExpression String to search for + * @param searchSpecBundle SearchSpec bundle * @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link byte[]}>> * Will be completed with a serialized {@link SearchResultsProto}. */ void query( - in byte[] searchSpecBytes, in byte[] resultSpecBytes, in byte[] scoringSpecBytes, + in String queryExpression, + in Bundle searchSpecBundle, in AndroidFuture callback); /** diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java index 970808cedd6e9..817c3ef5e028c 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2020 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. @@ -16,19 +16,17 @@ package android.app.appsearch; +import android.os.Bundle; + import android.annotation.IntDef; import android.annotation.NonNull; -import android.app.appsearch.exceptions.IllegalSearchSpecException; -import com.google.android.icing.proto.ResultSpecProto; -import com.google.android.icing.proto.ScoringSpecProto; -import com.google.android.icing.proto.SearchSpecProto; -import com.google.android.icing.proto.TermMatchType; +import android.app.appsearch.exceptions.IllegalSearchSpecException; +import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; - /** * This class represents the specification logic for AppSearch. It can be used to set the type of * search, like prefix or exact only or apply filters to search for a specific schema type only etc. @@ -36,67 +34,100 @@ import java.lang.annotation.RetentionPolicy; */ // TODO(sidchhabra) : AddResultSpec fields for Snippets etc. public final class SearchSpec { - - private final SearchSpecProto mSearchSpecProto; - private final ResultSpecProto mResultSpecProto; - private final ScoringSpecProto mScoringSpecProto; - - private SearchSpec(@NonNull SearchSpecProto searchSpecProto, - @NonNull ResultSpecProto resultSpecProto, @NonNull ScoringSpecProto scoringSpecProto) { - mSearchSpecProto = searchSpecProto; - mResultSpecProto = resultSpecProto; - mScoringSpecProto = scoringSpecProto; - } - - /** Creates a new {@link SearchSpec.Builder}. */ - @NonNull - public static SearchSpec.Builder newBuilder() { - return new SearchSpec.Builder(); - } + /** @hide */ + + public static final String TERM_MATCH_TYPE_FIELD = "termMatchType"; /** @hide */ - @NonNull - SearchSpecProto getSearchSpecProto() { - return mSearchSpecProto; - } + + public static final String SCHEMA_TYPES_FIELD = "schemaType"; /** @hide */ - @NonNull - ResultSpecProto getResultSpecProto() { - return mResultSpecProto; - } + + public static final String NAMESPACE_FIELD = "namespace"; /** @hide */ - @NonNull - ScoringSpecProto getScoringSpecProto() { - return mScoringSpecProto; + + public static final String NUM_PER_PAGE_FIELD = "numPerPage"; + + /** @hide */ + + public static final String RANKING_STRATEGY_FIELD = "rankingStrategy"; + + /** @hide */ + + public static final String ORDER_FIELD = "order"; + + /** @hide */ + + public static final String SNIPPET_COUNT_FIELD = "snippetCount"; + + /** @hide */ + + public static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty"; + + /** @hide */ + + public static final String MAX_SNIPPET_FIELD = "maxSnippet"; + + /** @hide */ + + public static final int DEFAULT_NUM_PER_PAGE = 10; + + private static final int MAX_NUM_PER_PAGE = 10_000; + private static final int MAX_SNIPPET_COUNT = 10_000; + private static final int MAX_SNIPPET_PER_PROPERTY_COUNT = 10_000; + private static final int MAX_SNIPPET_SIZE_LIMIT = 10_000; + + private final Bundle mBundle; + + /** @hide */ + + public SearchSpec(@NonNull Bundle bundle) { + Preconditions.checkNotNull(bundle); + mBundle = bundle; } - /** Term Match Type for the query. */ + /** + * Returns the {@link Bundle} populated by this builder. + * @hide + */ + @NonNull + public Bundle getBundle() { + return mBundle; + } + + /** + * Term Match Type for the query. + * @hide + */ // NOTE: The integer values of these constants must match the proto enum constants in // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType} - @IntDef(prefix = {"TERM_MATCH_TYPE_"}, value = { - TERM_MATCH_TYPE_EXACT_ONLY, - TERM_MATCH_TYPE_PREFIX + @IntDef(value = { + TERM_MATCH_EXACT_ONLY, + TERM_MATCH_PREFIX }) @Retention(RetentionPolicy.SOURCE) - public @interface TermMatchTypeCode {} + public @interface TermMatchCode {} /** * Query terms will only match exact tokens in the index. *

Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football". */ - public static final int TERM_MATCH_TYPE_EXACT_ONLY = 1; + public static final int TERM_MATCH_EXACT_ONLY = 1; /** * Query terms will match indexed tokens when the query term is a prefix of the token. *

Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football". */ - public static final int TERM_MATCH_TYPE_PREFIX = 2; + public static final int TERM_MATCH_PREFIX = 2; - /** Ranking Strategy for query result.*/ + /** + * Ranking Strategy for query result. + * @hide + */ // NOTE: The integer values of these constants must match the proto enum constants in // {@link ScoringSpecProto.RankingStrategy.Code } - @IntDef(prefix = {"RANKING_STRATEGY_"}, value = { + @IntDef(value = { RANKING_STRATEGY_NONE, RANKING_STRATEGY_DOCUMENT_SCORE, RANKING_STRATEGY_CREATION_TIMESTAMP @@ -111,10 +142,13 @@ public final class SearchSpec { /** Ranked by document creation timestamps. */ public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; - /** Order for query result.*/ + /** + * Order for query result. + * @hide + */ // NOTE: The integer values of these constants must match the proto enum constants in // {@link ScoringSpecProto.Order.Code } - @IntDef(prefix = {"ORDER_"}, value = { + @IntDef(value = { ORDER_DESCENDING, ORDER_ASCENDING }) @@ -129,27 +163,24 @@ public final class SearchSpec { /** Builder for {@link SearchSpec objects}. */ public static final class Builder { - private final SearchSpecProto.Builder mSearchSpecBuilder = SearchSpecProto.newBuilder(); - private final ResultSpecProto.Builder mResultSpecBuilder = ResultSpecProto.newBuilder(); - private final ScoringSpecProto.Builder mScoringSpecBuilder = ScoringSpecProto.newBuilder(); - private final ResultSpecProto.SnippetSpecProto.Builder mSnippetSpecBuilder = - ResultSpecProto.SnippetSpecProto.newBuilder(); + private final Bundle mBundle; + private boolean mBuilt = false; - private Builder() { + /** Creates a new {@link SearchSpec.Builder}. */ + public Builder() { + mBundle = new Bundle(); + mBundle.putInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE); } /** - * Indicates how the query terms should match {@link TermMatchTypeCode} in the index. + * Indicates how the query terms should match {@code TermMatchCode} in the index. */ @NonNull - public Builder setTermMatchType(@TermMatchTypeCode int termMatchTypeCode) { - TermMatchType.Code termMatchTypeCodeProto = - TermMatchType.Code.forNumber(termMatchTypeCode); - if (termMatchTypeCodeProto == null) { - throw new IllegalArgumentException("Invalid term match type: " - + termMatchTypeCode); - } - mSearchSpecBuilder.setTermMatchType(termMatchTypeCodeProto); + public Builder setTermMatch(@TermMatchCode int termMatchTypeCode) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange(termMatchTypeCode, TERM_MATCH_EXACT_ONLY, + TERM_MATCH_PREFIX, "Term match type"); + mBundle.putInt(TERM_MATCH_TYPE_FIELD, termMatchTypeCode); return this; } @@ -160,31 +191,44 @@ public final class SearchSpec { */ @NonNull public Builder setSchemaTypes(@NonNull String... schemaTypes) { - for (String schemaType : schemaTypes) { - mSearchSpecBuilder.addSchemaTypeFilters(schemaType); - } + Preconditions.checkNotNull(schemaTypes); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + mBundle.putStringArray(SCHEMA_TYPES_FIELD, schemaTypes); return this; } - /** Sets the maximum number of results to retrieve from the query */ + /** + * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that + * have the specified namespaces. + *

If unset, the query will search over all namespaces. + */ @NonNull - public SearchSpec.Builder setNumToRetrieve(int numToRetrieve) { - // Just retrieve everything in one page. - // TODO(b/152359656): Realign these two apis properly. - mResultSpecBuilder.setNumPerPage(numToRetrieve); + public Builder setNamespaces(@NonNull String... namespaces) { + Preconditions.checkNotNull(namespaces); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + mBundle.putStringArray(NAMESPACE_FIELD, namespaces); + return this; + } + + /** + * Sets the number of results per page in the returned object. + *

The default number of results per page is 10. And should be set in range [0, 10k]. + */ + @NonNull + public SearchSpec.Builder setNumPerPage(int numPerPage) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange(numPerPage, 0, MAX_NUM_PER_PAGE, "NumPerPage"); + mBundle.putInt(NUM_PER_PAGE_FIELD, numPerPage); return this; } /** Sets ranking strategy for AppSearch results.*/ @NonNull public Builder setRankingStrategy(@RankingStrategyCode int rankingStrategy) { - ScoringSpecProto.RankingStrategy.Code rankingStrategyCodeProto = - ScoringSpecProto.RankingStrategy.Code.forNumber(rankingStrategy); - if (rankingStrategyCodeProto == null) { - throw new IllegalArgumentException("Invalid result ranking strategy: " - + rankingStrategyCodeProto); - } - mScoringSpecBuilder.setRankBy(rankingStrategyCodeProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE, + RANKING_STRATEGY_CREATION_TIMESTAMP, "Result ranking strategy"); + mBundle.putInt(RANKING_STRATEGY_FIELD, rankingStrategy); return this; } @@ -195,37 +239,41 @@ public final class SearchSpec { */ @NonNull public Builder setOrder(@OrderCode int order) { - ScoringSpecProto.Order.Code orderCodeProto = - ScoringSpecProto.Order.Code.forNumber(order); - if (orderCodeProto == null) { - throw new IllegalArgumentException("Invalid result ranking order: " - + orderCodeProto); - } - mScoringSpecBuilder.setOrderBy(orderCodeProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange(order, ORDER_DESCENDING, ORDER_ASCENDING, + "Result ranking order"); + mBundle.putInt(ORDER_FIELD, order); return this; } /** - * Only the first {@code numToSnippet} documents based on the ranking strategy + * Only the first {@code snippetCount} documents based on the ranking strategy * will have snippet information provided. *

If set to 0 (default), snippeting is disabled and - * {@link SearchResults.Result#getMatchInfo} will return {@code null} for that result. + * {@link SearchResults.Result#getMatches} will return {@code null} for that result. + *

The value should be set in range[0, 10k]. */ @NonNull - public SearchSpec.Builder setNumToSnippet(int numToSnippet) { - mSnippetSpecBuilder.setNumToSnippet(numToSnippet); + public SearchSpec.Builder setSnippetCount(int snippetCount) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount"); + mBundle.putInt(SNIPPET_COUNT_FIELD, snippetCount); return this; } /** - * Only the first {@code numMatchesPerProperty} matches for a every property of - * {@link AppSearchDocument} will contain snippet information. - *

If set to 0, snippeting is disabled and {@link SearchResults.Result#getMatchInfo} + * Only the first {@code matchesCountPerProperty} matches for a every property of + * {@link GenericDocument} will contain snippet information. + *

If set to 0, snippeting is disabled and {@link SearchResults.Result#getMatches} * will return {@code null} for that result. + *

The value should be set in range[0, 10k]. */ @NonNull - public SearchSpec.Builder setNumMatchesPerProperty(int numMatchesPerProperty) { - mSnippetSpecBuilder.setNumMatchesPerProperty(numMatchesPerProperty); + public SearchSpec.Builder setSnippetCountPerProperty(int snippetCountPerProperty) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange(snippetCountPerProperty, + 0, MAX_SNIPPET_PER_PROPERTY_COUNT, "snippetCountPerProperty"); + mBundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, snippetCountPerProperty); return this; } @@ -238,10 +286,14 @@ public final class SearchSpec { * be returned. If matches enabled is also set to false, then snippeting is disabled. *

Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will * return a window of "bar baz bat" which is only 11 bytes long. + *

The value should be in range[0, 10k]. */ @NonNull public SearchSpec.Builder setMaxSnippetSize(int maxSnippetSize) { - mSnippetSpecBuilder.setMaxWindowBytes(maxSnippetSize); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange( + maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize"); + mBundle.putInt(MAX_SNIPPET_FIELD, maxSnippetSize); return this; } @@ -252,12 +304,12 @@ public final class SearchSpec { */ @NonNull public SearchSpec build() { - if (mSearchSpecBuilder.getTermMatchType() == TermMatchType.Code.UNKNOWN) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + if (!mBundle.containsKey(TERM_MATCH_TYPE_FIELD)) { throw new IllegalSearchSpecException("Missing termMatchType field."); } - mResultSpecBuilder.setSnippetSpec(mSnippetSpecBuilder); - return new SearchSpec(mSearchSpecBuilder.build(), mResultSpecBuilder.build(), - mScoringSpecBuilder.build()); + mBuilt = true; + return new SearchSpec(mBundle); } } } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index c3034873a7921..4bc0c39bb6c91 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -21,6 +21,7 @@ import android.app.appsearch.AppSearchDocument; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.IAppSearchManager; +import android.app.appsearch.SearchSpec; import android.app.appsearch.exceptions.AppSearchException; import android.content.Context; import android.os.Binder; @@ -32,15 +33,13 @@ import com.android.internal.util.Preconditions; import com.android.server.SystemService; import com.android.server.appsearch.external.localbackend.AppSearchImpl; import com.android.server.appsearch.external.localbackend.converter.SchemaToProtoConverter; +import com.android.server.appsearch.external.localbackend.converter.SearchSpecToProtoConverter; 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.SchemaTypeConfigProto; -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.proto.StatusProto; import java.io.IOException; import java.util.List; @@ -160,36 +159,31 @@ public class AppSearchManagerService extends SystemService { // TODO(sidchhabra): Do this in a threadpool. @Override public void query( - @NonNull byte[] searchSpecBytes, - @NonNull byte[] resultSpecBytes, - @NonNull byte[] scoringSpecBytes, + @NonNull String queryExpression, + @NonNull Bundle searchSpecBundle, @NonNull AndroidFuture callback) { - Preconditions.checkNotNull(searchSpecBytes); - Preconditions.checkNotNull(resultSpecBytes); - Preconditions.checkNotNull(scoringSpecBytes); + Preconditions.checkNotNull(queryExpression); + Preconditions.checkNotNull(searchSpecBundle); Preconditions.checkNotNull(callback); int callingUid = Binder.getCallingUidOrThrow(); int callingUserId = UserHandle.getUserId(callingUid); final long callingIdentity = Binder.clearCallingIdentity(); try { - SearchSpecProto searchSpecProto = SearchSpecProto.parseFrom(searchSpecBytes); - ResultSpecProto resultSpecProto = ResultSpecProto.parseFrom(resultSpecBytes); - ScoringSpecProto scoringSpecProto = ScoringSpecProto.parseFrom(scoringSpecBytes); + SearchSpec searchSpec = new SearchSpec(searchSpecBundle); + SearchSpecProto searchSpecProto = + SearchSpecToProtoConverter.toSearchSpecProto(searchSpec); + searchSpecProto = searchSpecProto.toBuilder() + .setQuery(queryExpression).build(); AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); String databaseName = makeDatabaseName(callingUid); + // TODO(adorokhine): handle pagination SearchResultProto searchResultProto = impl.query( - databaseName, 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())); - } + databaseName, + searchSpecProto, + SearchSpecToProtoConverter.toResultSpecProto(searchSpec), + SearchSpecToProtoConverter.toScoringSpecProto(searchSpec)); + callback.complete( + AppSearchResult.newSuccessfulResult(searchResultProto.toByteArray())); } catch (Throwable t) { callback.complete(throwableToFailedResult(t)); } finally { diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchSpecToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchSpecToProtoConverter.java new file mode 100644 index 0000000000000..a5d913a205901 --- /dev/null +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchSpecToProtoConverter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020 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.appsearch.external.localbackend.converter; + +import android.os.Bundle; + +import android.annotation.NonNull; + +import android.app.appsearch.SearchSpec; +import com.android.internal.util.Preconditions; + +import com.google.android.icing.proto.ResultSpecProto; +import com.google.android.icing.proto.ScoringSpecProto; +import com.google.android.icing.proto.SearchSpecProto; +import com.google.android.icing.proto.TermMatchType; + +import java.util.Arrays; + +/** + * Translates a {@link SearchSpec} into icing search protos. + * @hide + */ + +public final class SearchSpecToProtoConverter { + private SearchSpecToProtoConverter() {} + + /** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */ + @NonNull + public static SearchSpecProto toSearchSpecProto(@NonNull SearchSpec spec) { + Preconditions.checkNotNull(spec); + Bundle bundle = spec.getBundle(); + SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder(); + + @SearchSpec.TermMatchCode int termMatchCode = + bundle.getInt(SearchSpec.TERM_MATCH_TYPE_FIELD); + TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode); + if (termMatchCodeProto == null || termMatchCodeProto.equals(TermMatchType.Code.UNKNOWN)) { + throw new IllegalArgumentException("Invalid term match type: " + termMatchCode); + } + protoBuilder.setTermMatchType(termMatchCodeProto); + + String[] schemaTypes = bundle.getStringArray(SearchSpec.SCHEMA_TYPES_FIELD); + if (schemaTypes != null) { + protoBuilder.addAllSchemaTypeFilters(Arrays.asList(schemaTypes)); + } + String[] namespaces = bundle.getStringArray(SearchSpec.NAMESPACE_FIELD); + if (namespaces != null) { + protoBuilder.addAllNamespaceFilters(Arrays.asList(namespaces)); + } + return protoBuilder.build(); + } + + /** Extracts {@link ResultSpecProto} information from a {@link SearchSpec}. */ + @NonNull + public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) { + Preconditions.checkNotNull(spec); + Bundle bundle = spec.getBundle(); + return ResultSpecProto.newBuilder() + .setNumPerPage(bundle.getInt( + SearchSpec.NUM_PER_PAGE_FIELD, SearchSpec.DEFAULT_NUM_PER_PAGE)) + .setSnippetSpec(ResultSpecProto.SnippetSpecProto.newBuilder() + .setNumToSnippet(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)) + .setNumMatchesPerProperty( + bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)) + .setMaxWindowBytes(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD))) + .build(); + + } + + /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */ + @NonNull + public static ScoringSpecProto toScoringSpecProto(@NonNull SearchSpec spec) { + Preconditions.checkNotNull(spec); + Bundle bundle = spec.getBundle(); + ScoringSpecProto.Builder protoBuilder = ScoringSpecProto.newBuilder(); + + @SearchSpec.OrderCode int orderCode = bundle.getInt(SearchSpec.ORDER_FIELD); + ScoringSpecProto.Order.Code orderCodeProto = + ScoringSpecProto.Order.Code.forNumber(orderCode); + if (orderCodeProto == null) { + throw new IllegalArgumentException("Invalid result ranking order: " + orderCode); + } + protoBuilder.setOrderBy(orderCodeProto); + + @SearchSpec.RankingStrategyCode int rankingStrategyCode = + bundle.getInt(SearchSpec.RANKING_STRATEGY_FIELD); + ScoringSpecProto.RankingStrategy.Code rankingStrategyCodeProto = + ScoringSpecProto.RankingStrategy.Code.forNumber(rankingStrategyCode); + if (rankingStrategyCodeProto == null) { + throw new IllegalArgumentException("Invalid result ranking strategy: " + + rankingStrategyCode); + } + protoBuilder.setRankBy(rankingStrategyCodeProto); + + return protoBuilder.build(); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java b/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java index 1d71ec4520c30..ac0f44bb17e48 100644 --- a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java +++ b/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java @@ -59,7 +59,7 @@ public class SearchResultsTest { @Test public void buildSearchSpecWithoutTermMatchType() { - assertThrows(RuntimeException.class, () -> SearchSpec.newBuilder() + assertThrows(RuntimeException.class, () -> new SearchSpec.Builder() .setSchemaTypes("testSchemaType") .build()); }