1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appsearch.localstorage.converter;
18 
19 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
20 import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
21 import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
22 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
23 
24 import android.util.Log;
25 
26 import androidx.annotation.OptIn;
27 import androidx.annotation.RestrictTo;
28 import androidx.appsearch.app.EmbeddingVector;
29 import androidx.appsearch.app.ExperimentalAppSearchApi;
30 import androidx.appsearch.app.FeatureConstants;
31 import androidx.appsearch.app.JoinSpec;
32 import androidx.appsearch.app.SearchResult;
33 import androidx.appsearch.app.SearchSpec;
34 import androidx.appsearch.exceptions.AppSearchException;
35 import androidx.appsearch.localstorage.IcingOptionsConfig;
36 import androidx.appsearch.localstorage.NamespaceCache;
37 import androidx.appsearch.localstorage.SchemaCache;
38 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
39 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
40 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
41 import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
42 import androidx.collection.ArrayMap;
43 import androidx.collection.ArraySet;
44 import androidx.core.util.Preconditions;
45 
46 import com.google.android.icing.proto.JoinSpecProto;
47 import com.google.android.icing.proto.NamespaceDocumentUriGroup;
48 import com.google.android.icing.proto.PropertyWeight;
49 import com.google.android.icing.proto.ResultSpecProto;
50 import com.google.android.icing.proto.SchemaTypeAliasMapProto;
51 import com.google.android.icing.proto.SchemaTypeConfigProto;
52 import com.google.android.icing.proto.ScoringFeatureType;
53 import com.google.android.icing.proto.ScoringSpecProto;
54 import com.google.android.icing.proto.SearchSpecProto;
55 import com.google.android.icing.proto.TermMatchType;
56 import com.google.android.icing.proto.TypePropertyMask;
57 import com.google.android.icing.proto.TypePropertyWeights;
58 
59 import org.jspecify.annotations.NonNull;
60 import org.jspecify.annotations.Nullable;
61 
62 import java.util.ArrayList;
63 import java.util.Iterator;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Set;
67 
68 /**
69  * Translates a {@link SearchSpec} into icing search protos.
70  *
71  * @exportToFramework:hide
72  */
73 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
74 public final class SearchSpecToProtoConverter {
75     private static final String TAG = "AppSearchSearchSpecConv";
76     private final String mQueryExpression;
77     private final SearchSpec mSearchSpec;
78     /**
79      * The union of allowed prefixes for the top-level SearchSpec and any nested SearchSpecs.
80      */
81     private final Set<String> mAllAllowedPrefixes;
82     /**
83      * The intersection of mAllAllowedPrefixes and prefixes requested in the SearchSpec currently
84      * being handled.
85      */
86     private final Set<String> mCurrentSearchSpecPrefixFilters;
87     /**
88      * The intersected prefixed namespaces that are existing in AppSearch and also accessible to the
89      * client.
90      */
91     private final Set<String> mTargetPrefixedNamespaceFilters;
92     /**
93      * The intersected prefixed schema types that are existing in AppSearch and also accessible to
94      * the client.
95      */
96     private final Set<String> mTargetPrefixedSchemaFilters;
97 
98     /**
99      * The NamespaceCache instance held in AppSearch.
100      */
101     private final NamespaceCache mNamespaceCache;
102 
103     /**
104      * The SchemaCache instance held in AppSearch.
105      */
106     private final SchemaCache mSchemaCache;
107 
108     /**
109      * Optional config flags in {@link SearchSpecProto}.
110      */
111     private final IcingOptionsConfig mIcingOptionsConfig;
112 
113     /**
114      * The nested converter, which contains SearchSpec, ResultSpec, and ScoringSpec information
115      * about the nested query. This will remain null if there is no nested {@link JoinSpec}.
116      */
117     private @Nullable SearchSpecToProtoConverter mNestedConverter = null;
118 
119     /**
120      * Creates a {@link SearchSpecToProtoConverter} for given {@link SearchSpec}.
121      *
122      * @param queryExpression                Query String to search.
123      * @param searchSpec    The spec we need to convert from.
124      * @param allAllowedPrefixes Superset of database prefixes which the {@link SearchSpec} and all
125      *                           nested SearchSpecs are allowed to access. An empty set means no
126      *                           database prefixes are allowed, so nothing will be searched.
127      * @param namespaceCache  The NamespaceCache instance held in AppSearch.
128      * @param schemaCache     The SchemaCache instance held in AppSearch.
129      */
SearchSpecToProtoConverter( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull Set<String> allAllowedPrefixes, @NonNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache, @NonNull IcingOptionsConfig icingOptionsConfig)130     public SearchSpecToProtoConverter(
131             @NonNull String queryExpression,
132             @NonNull SearchSpec searchSpec,
133             @NonNull Set<String> allAllowedPrefixes,
134             @NonNull NamespaceCache namespaceCache,
135             @NonNull SchemaCache schemaCache,
136             @NonNull IcingOptionsConfig icingOptionsConfig) {
137         mQueryExpression = Preconditions.checkNotNull(queryExpression);
138         mSearchSpec = Preconditions.checkNotNull(searchSpec);
139         mAllAllowedPrefixes = Preconditions.checkNotNull(allAllowedPrefixes);
140         mNamespaceCache = Preconditions.checkNotNull(namespaceCache);
141         mSchemaCache = Preconditions.checkNotNull(schemaCache);
142         mIcingOptionsConfig = Preconditions.checkNotNull(icingOptionsConfig);
143 
144         // This field holds the prefix filters for the SearchSpec currently being handled, which
145         // could be an outer or inner SearchSpec. If this constructor is called from outside of
146         // SearchSpecToProtoConverter, it will be handling an outer SearchSpec. If this SearchSpec
147         // contains a JoinSpec, the nested SearchSpec will be handled in the creation of
148         // mNestedConverter. This is useful as the two SearchSpecs could have different package
149         // filters.
150         List<String> packageFilters = searchSpec.getFilterPackageNames();
151         if (packageFilters.isEmpty()) {
152             mCurrentSearchSpecPrefixFilters = mAllAllowedPrefixes;
153         } else {
154             mCurrentSearchSpecPrefixFilters = new ArraySet<>();
155             for (String prefix : mAllAllowedPrefixes) {
156                 String packageName = getPackageName(prefix);
157                 if (packageFilters.contains(packageName)) {
158                     // This performs an intersection of allowedPrefixes with prefixes requested
159                     // in the SearchSpec currently being handled. The same operation is done
160                     // on the nested SearchSpecs when mNestedConverter is created.
161                     mCurrentSearchSpecPrefixFilters.add(prefix);
162                 }
163             }
164         }
165 
166         mTargetPrefixedNamespaceFilters =
167                 SearchSpecToProtoConverterUtil.generateTargetNamespaceFilters(
168                         mCurrentSearchSpecPrefixFilters, namespaceCache,
169                         searchSpec.getFilterNamespaces());
170 
171         // If the target namespace filter is empty, the user has nothing to search for. We can skip
172         // generate the target schema filter.
173         if (!mTargetPrefixedNamespaceFilters.isEmpty()) {
174             mTargetPrefixedSchemaFilters =
175                     SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
176                             mCurrentSearchSpecPrefixFilters, schemaCache,
177                             searchSpec.getFilterSchemas());
178         } else {
179             mTargetPrefixedSchemaFilters = new ArraySet<>();
180         }
181 
182         JoinSpec joinSpec = searchSpec.getJoinSpec();
183         if (joinSpec == null) {
184             return;
185         }
186 
187         mNestedConverter = new SearchSpecToProtoConverter(
188                 joinSpec.getNestedQuery(),
189                 joinSpec.getNestedSearchSpec(),
190                 mAllAllowedPrefixes,
191                 namespaceCache,
192                 schemaCache,
193                 mIcingOptionsConfig);
194     }
195 
196     /**
197      * Returns whether this search's target filters are empty. If any target filter is empty, we
198      * should skip send request to Icing.
199      *
200      * <p>The nestedConverter is not checked as {@link SearchResult}s from the nested query have to
201      * be joined to a {@link SearchResult} from the parent query. If the parent query has nothing to
202      * search, then so does the child query.
203      */
hasNothingToSearch()204     public boolean hasNothingToSearch() {
205         return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
206     }
207 
208     /**
209      * For each target schema, we will check visibility store is that accessible to the caller. And
210      * remove this schemas if it is not allowed for caller to query.
211      *
212      * @param callerAccess      Visibility access info of the calling app
213      * @param visibilityStore   The {@link VisibilityStore} that store all visibility
214      *                          information.
215      * @param visibilityChecker Optional visibility checker to check whether the caller
216      *                          could access target schemas. Pass {@code null} will
217      *                          reject access for all documents which doesn't belong
218      *                          to the calling package.
219      */
removeInaccessibleSchemaFilter( @onNull CallerAccess callerAccess, @Nullable VisibilityStore visibilityStore, @Nullable VisibilityChecker visibilityChecker)220     public void removeInaccessibleSchemaFilter(
221             @NonNull CallerAccess callerAccess,
222             @Nullable VisibilityStore visibilityStore,
223             @Nullable VisibilityChecker visibilityChecker) {
224         removeInaccessibleSchemaFilterCached(callerAccess, visibilityStore,
225                 /*inaccessibleSchemaPrefixes=*/new ArraySet<>(),
226                 /*accessibleSchemaPrefixes=*/new ArraySet<>(), visibilityChecker);
227     }
228 
229     /**
230      * For each target schema, we will check visibility store is that accessible to the caller. And
231      * remove this schemas if it is not allowed for caller to query. This private version accepts
232      * two additional parameters to minimize the amount of calls to
233      * {@link VisibilityUtil#isSchemaSearchableByCaller}.
234      *
235      * @param callerAccess      Visibility access info of the calling app
236      * @param visibilityStore   The {@link VisibilityStore} that store all visibility
237      *                          information.
238      * @param visibilityChecker Optional visibility checker to check whether the caller
239      *                          could access target schemas. Pass {@code null} will
240      *                          reject access for all documents which doesn't belong
241      *                          to the calling package.
242      * @param inaccessibleSchemaPrefixes A set of schemas that are known to be inaccessible. This
243      *                                  is helpful for reducing duplicate calls to
244      *                                  {@link VisibilityUtil}.
245      * @param accessibleSchemaPrefixes A set of schemas that are known to be accessible. This is
246      *                                 helpful for reducing duplicate calls to
247      *                                 {@link VisibilityUtil}.
248      */
removeInaccessibleSchemaFilterCached( @onNull CallerAccess callerAccess, @Nullable VisibilityStore visibilityStore, @NonNull Set<String> inaccessibleSchemaPrefixes, @NonNull Set<String> accessibleSchemaPrefixes, @Nullable VisibilityChecker visibilityChecker)249     private void removeInaccessibleSchemaFilterCached(
250             @NonNull CallerAccess callerAccess,
251             @Nullable VisibilityStore visibilityStore,
252             @NonNull Set<String> inaccessibleSchemaPrefixes,
253             @NonNull Set<String> accessibleSchemaPrefixes,
254             @Nullable VisibilityChecker visibilityChecker) {
255         Iterator<String> targetPrefixedSchemaFilterIterator =
256                 mTargetPrefixedSchemaFilters.iterator();
257         while (targetPrefixedSchemaFilterIterator.hasNext()) {
258             String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next();
259             String packageName = getPackageName(targetPrefixedSchemaFilter);
260 
261             if (accessibleSchemaPrefixes.contains(targetPrefixedSchemaFilter)) {
262                 continue;
263             } else if (inaccessibleSchemaPrefixes.contains(targetPrefixedSchemaFilter)) {
264                 targetPrefixedSchemaFilterIterator.remove();
265             } else if (!VisibilityUtil.isSchemaSearchableByCaller(
266                     callerAccess,
267                     packageName,
268                     targetPrefixedSchemaFilter,
269                     visibilityStore,
270                     visibilityChecker)) {
271                 targetPrefixedSchemaFilterIterator.remove();
272                 inaccessibleSchemaPrefixes.add(targetPrefixedSchemaFilter);
273             } else {
274                 accessibleSchemaPrefixes.add(targetPrefixedSchemaFilter);
275             }
276         }
277 
278         if (mNestedConverter != null) {
279             mNestedConverter.removeInaccessibleSchemaFilterCached(
280                     callerAccess, visibilityStore, inaccessibleSchemaPrefixes,
281                     accessibleSchemaPrefixes, visibilityChecker);
282         }
283     }
284 
285 
286     /** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */
287     @OptIn(markerClass = ExperimentalAppSearchApi.class)
toSearchSpecProto()288     public @NonNull SearchSpecProto toSearchSpecProto() {
289         // set query to SearchSpecProto and override schema and namespace filter by
290         // targetPrefixedFilters which contains all existing and also accessible to the caller
291         // filters.
292         SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
293                 .setQuery(mQueryExpression)
294                 .addAllNamespaceFilters(mTargetPrefixedNamespaceFilters)
295                 .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters)
296                 .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch())
297                 .addAllQueryParameterStrings(mSearchSpec.getSearchStringParameters());
298 
299         List<EmbeddingVector> searchEmbeddings = mSearchSpec.getEmbeddingParameters();
300         for (int i = 0; i < searchEmbeddings.size(); i++) {
301             protoBuilder.addEmbeddingQueryVectors(
302                     GenericDocumentToProtoConverter.embeddingVectorToVectorProto(
303                             searchEmbeddings.get(i)));
304         }
305 
306         // Convert type property filter map into type property mask proto.
307         for (Map.Entry<String, List<String>> entry :
308                 mSearchSpec.getFilterProperties().entrySet()) {
309             if (entry.getKey().equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) {
310                 protoBuilder.addTypePropertyFilters(TypePropertyMask.newBuilder()
311                         .setSchemaType(SearchSpec.SCHEMA_TYPE_WILDCARD)
312                         .addAllPaths(entry.getValue())
313                         .build());
314             } else {
315                 for (String prefix : mCurrentSearchSpecPrefixFilters) {
316                     String prefixedSchemaType = prefix + entry.getKey();
317                     if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) {
318                         protoBuilder.addTypePropertyFilters(TypePropertyMask.newBuilder()
319                                 .setSchemaType(prefixedSchemaType)
320                                 .addAllPaths(entry.getValue())
321                                 .build());
322                     }
323                 }
324             }
325         }
326 
327         // Convert document id filters.
328         List<String> filterDocumentIds = mSearchSpec.getFilterDocumentIds();
329         if (!filterDocumentIds.isEmpty()) {
330             for (String targetPrefixedNamespaceFilter : mTargetPrefixedNamespaceFilters) {
331                 protoBuilder.addDocumentUriFilters(NamespaceDocumentUriGroup.newBuilder()
332                         .setNamespace(targetPrefixedNamespaceFilter)
333                         .addAllDocumentUris(filterDocumentIds)
334                         .build());
335             }
336         }
337 
338         @SearchSpec.TermMatch int termMatchCode = mSearchSpec.getTermMatch();
339         TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode);
340         if (termMatchCodeProto == null || termMatchCodeProto.equals(TermMatchType.Code.UNKNOWN)) {
341             throw new IllegalArgumentException("Invalid term match type: " + termMatchCode);
342         }
343         protoBuilder.setTermMatchType(termMatchCodeProto);
344 
345         @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType =
346                 mSearchSpec.getDefaultEmbeddingSearchMetricType();
347         SearchSpecProto.EmbeddingQueryMetricType.Code embeddingSearchMetricTypeProto =
348                 SearchSpecProto.EmbeddingQueryMetricType.Code.forNumber(embeddingSearchMetricType);
349         if (embeddingSearchMetricTypeProto == null || embeddingSearchMetricTypeProto.equals(
350                 SearchSpecProto.EmbeddingQueryMetricType.Code.UNKNOWN)) {
351             throw new IllegalArgumentException(
352                     "Invalid embedding search metric type: " + embeddingSearchMetricType);
353         }
354         protoBuilder.setEmbeddingQueryMetricType(embeddingSearchMetricTypeProto);
355 
356         if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) {
357             JoinSpecProto.NestedSpecProto nestedSpec =
358                     JoinSpecProto.NestedSpecProto.newBuilder()
359                             .setResultSpec(mNestedConverter.toResultSpecProto(
360                                     mNamespaceCache, mSchemaCache))
361                             .setScoringSpec(mNestedConverter.toScoringSpecProto())
362                             .setSearchSpec(mNestedConverter.toSearchSpecProto())
363                             .build();
364 
365             // This cannot be null, otherwise mNestedConverter would be null as well.
366             JoinSpec joinSpec = mSearchSpec.getJoinSpec();
367             JoinSpecProto.Builder joinSpecProtoBuilder =
368                     JoinSpecProto.newBuilder()
369                             .setNestedSpec(nestedSpec)
370                             .setParentPropertyExpression(JoinSpec.QUALIFIED_ID)
371                             .setChildPropertyExpression(joinSpec.getChildPropertyExpression())
372                             .setAggregationScoringStrategy(
373                                     toAggregationScoringStrategy(
374                                             joinSpec.getAggregationScoringStrategy()));
375 
376             protoBuilder.setJoinSpec(joinSpecProtoBuilder);
377         }
378 
379         if (mSearchSpec.isListFilterHasPropertyFunctionEnabled()
380                 && !mIcingOptionsConfig.getBuildPropertyExistenceMetadataHits()) {
381             // This condition should never be reached as long as Features.isFeatureSupported() is
382             // consistent with IcingOptionsConfig.
383             throw new UnsupportedOperationException(
384                     FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION
385                             + " is currently not operational because the building process for the "
386                             + "associated metadata has not yet been turned on.");
387         }
388 
389         // Set enabled search features.
390         protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures(
391                 extractEnabledSearchFeatures(mSearchSpec.getEnabledFeatures())));
392 
393         return protoBuilder.build();
394     }
395 
396     /**
397      * Helper to convert to JoinSpecProto.AggregationScore.
398      *
399      * <p> {@link JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL} will be treated as
400      * undefined, which is the default behavior.
401      *
402      * @param aggregationScoringStrategy the scoring strategy to convert.
403      */
404     public static JoinSpecProto.AggregationScoringStrategy.@NonNull Code
toAggregationScoringStrategy( @oinSpec.AggregationScoringStrategy int aggregationScoringStrategy)405             toAggregationScoringStrategy(
406                     @JoinSpec.AggregationScoringStrategy int aggregationScoringStrategy) {
407         switch (aggregationScoringStrategy) {
408             case JoinSpec.AGGREGATION_SCORING_AVG_RANKING_SIGNAL:
409                 return JoinSpecProto.AggregationScoringStrategy.Code.AVG;
410             case JoinSpec.AGGREGATION_SCORING_MIN_RANKING_SIGNAL:
411                 return JoinSpecProto.AggregationScoringStrategy.Code.MIN;
412             case JoinSpec.AGGREGATION_SCORING_MAX_RANKING_SIGNAL:
413                 return JoinSpecProto.AggregationScoringStrategy.Code.MAX;
414             case JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL:
415                 return JoinSpecProto.AggregationScoringStrategy.Code.SUM;
416             case JoinSpec.AGGREGATION_SCORING_RESULT_COUNT:
417                 return JoinSpecProto.AggregationScoringStrategy.Code.COUNT;
418             default:
419                 return JoinSpecProto.AggregationScoringStrategy.Code.NONE;
420         }
421     }
422 
423     /**
424      * Extracts {@link ResultSpecProto} information from a {@link SearchSpec}.
425      *
426      * @param namespaceCache  The NamespaceCache instance held in AppSearch.
427      * @param schemaCache     The SchemaCache instance held in AppSearch.
428      */
429     @OptIn(markerClass = ExperimentalAppSearchApi.class)
toResultSpecProto( @onNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache)430     public @NonNull ResultSpecProto toResultSpecProto(
431             @NonNull NamespaceCache namespaceCache,
432             @NonNull SchemaCache schemaCache) {
433         ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder()
434                 .setNumPerPage(mSearchSpec.getResultCountPerPage())
435                 .setSnippetSpec(
436                         ResultSpecProto.SnippetSpecProto.newBuilder()
437                                 .setNumToSnippet(mSearchSpec.getSnippetCount())
438                                 .setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty())
439                                 .setMaxWindowUtf32Length(mSearchSpec.getMaxSnippetSize())
440                                 .setGetEmbeddingMatchInfo(
441                                         mSearchSpec.shouldRetrieveEmbeddingMatchInfos()))
442                 .setNumTotalBytesPerPageThreshold(mIcingOptionsConfig.getMaxPageBytesLimit());
443         JoinSpec joinSpec = mSearchSpec.getJoinSpec();
444         if (joinSpec != null) {
445             resultSpecBuilder.setMaxJoinedChildrenPerParentToReturn(
446                     joinSpec.getMaxJoinedResultCount());
447         }
448 
449         // Add result groupings for the available prefixes
450         int groupingType = mSearchSpec.getResultGroupingTypeFlags();
451         ResultSpecProto.ResultGroupingType resultGroupingType =
452                 ResultSpecProto.ResultGroupingType.NONE;
453         switch (groupingType) {
454             case SearchSpec.GROUPING_TYPE_PER_PACKAGE :
455                 addPerPackageResultGroupings(mCurrentSearchSpecPrefixFilters,
456                         mSearchSpec.getResultGroupingLimit(), namespaceCache, resultSpecBuilder);
457                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE;
458                 break;
459             case SearchSpec.GROUPING_TYPE_PER_NAMESPACE:
460                 addPerNamespaceResultGroupings(mCurrentSearchSpecPrefixFilters,
461                         mSearchSpec.getResultGroupingLimit(), namespaceCache, resultSpecBuilder);
462                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE;
463                 break;
464             case SearchSpec.GROUPING_TYPE_PER_SCHEMA:
465                 addPerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
466                         mSearchSpec.getResultGroupingLimit(), schemaCache, resultSpecBuilder);
467                 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
468                 break;
469             case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE:
470                 addPerPackagePerNamespaceResultGroupings(mCurrentSearchSpecPrefixFilters,
471                         mSearchSpec.getResultGroupingLimit(),
472                         namespaceCache, resultSpecBuilder);
473                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE;
474                 break;
475             case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
476                 addPerPackagePerSchemaResultGroupings(mCurrentSearchSpecPrefixFilters,
477                         mSearchSpec.getResultGroupingLimit(),
478                         schemaCache, resultSpecBuilder);
479                 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
480                 break;
481             case SearchSpec.GROUPING_TYPE_PER_NAMESPACE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
482                 addPerNamespaceAndSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
483                         mSearchSpec.getResultGroupingLimit(),
484                         namespaceCache, schemaCache, resultSpecBuilder);
485                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
486                 break;
487             case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE
488                 | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
489                 addPerPackagePerNamespacePerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
490                         mSearchSpec.getResultGroupingLimit(),
491                         namespaceCache, schemaCache, resultSpecBuilder);
492                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
493                 break;
494             default:
495                 break;
496         }
497         resultSpecBuilder.setResultGroupType(resultGroupingType);
498 
499         List<TypePropertyMask.Builder> typePropertyMaskBuilders =
500                 TypePropertyPathToProtoConverter
501                         .toTypePropertyMaskBuilderList(mSearchSpec.getProjections());
502         // Rewrite filters to include a database prefix.
503         for (int i = 0; i < typePropertyMaskBuilders.size(); i++) {
504             String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType();
505             if (unprefixedType.equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) {
506                 resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i).build());
507             } else {
508                 // Qualify the given schema types
509                 for (String prefix : mCurrentSearchSpecPrefixFilters) {
510                     String prefixedType = prefix + unprefixedType;
511                     if (mTargetPrefixedSchemaFilters.contains(prefixedType)) {
512                         resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i)
513                                 .setSchemaType(prefixedType).build());
514                     }
515                 }
516             }
517         }
518 
519         return resultSpecBuilder.build();
520     }
521 
522     /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */
523     @OptIn(markerClass = ExperimentalAppSearchApi.class)
toScoringSpecProto()524     public @NonNull ScoringSpecProto toScoringSpecProto() {
525         ScoringSpecProto.Builder protoBuilder = ScoringSpecProto.newBuilder();
526 
527         @SearchSpec.Order int orderCode = mSearchSpec.getOrder();
528         ScoringSpecProto.Order.Code orderCodeProto =
529                 ScoringSpecProto.Order.Code.forNumber(orderCode);
530         if (orderCodeProto == null) {
531             throw new IllegalArgumentException("Invalid result ranking order: " + orderCode);
532         }
533         protoBuilder.setOrderBy(orderCodeProto).setRankBy(
534                 toProtoRankingStrategy(mSearchSpec.getRankingStrategy()));
535 
536         addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder);
537 
538         protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression());
539         protoBuilder.addAllAdditionalAdvancedScoringExpressions(
540                 mSearchSpec.getInformationalRankingExpressions());
541 
542         // TODO(b/380924970): create extractEnabledScoringFeatures() for populating scorable
543         // features
544         if (mSearchSpec.isScorablePropertyRankingEnabled()) {
545             protoBuilder.addScoringFeatureTypesEnabled(
546                     ScoringFeatureType.SCORABLE_PROPERTY_RANKING);
547             Map<String, Set<String>> schemaToPrefixedSchemasMap = createSchemaToPrefixedSchemasMap(
548                     mTargetPrefixedSchemaFilters);
549             for (Map.Entry<String, Set<String>> entry : schemaToPrefixedSchemasMap.entrySet()) {
550                 SchemaTypeAliasMapProto.Builder schemaTypeAliasMapProto =
551                         SchemaTypeAliasMapProto.newBuilder();
552                 schemaTypeAliasMapProto.setAliasSchemaType(entry.getKey());
553                 schemaTypeAliasMapProto.addAllSchemaTypes(entry.getValue());
554                 protoBuilder.addSchemaTypeAliasMapProtos(schemaTypeAliasMapProto);
555             }
556         }
557 
558         return protoBuilder.build();
559     }
560 
toProtoRankingStrategy( @earchSpec.RankingStrategy int rankingStrategyCode)561     private static ScoringSpecProto.RankingStrategy.Code toProtoRankingStrategy(
562             @SearchSpec.RankingStrategy int rankingStrategyCode) {
563         switch (rankingStrategyCode) {
564             case SearchSpec.RANKING_STRATEGY_NONE:
565                 return ScoringSpecProto.RankingStrategy.Code.NONE;
566             case SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE:
567                 return ScoringSpecProto.RankingStrategy.Code.DOCUMENT_SCORE;
568             case SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP:
569                 return ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP;
570             case SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE:
571                 return ScoringSpecProto.RankingStrategy.Code.RELEVANCE_SCORE;
572             case SearchSpec.RANKING_STRATEGY_USAGE_COUNT:
573                 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_COUNT;
574             case SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP:
575                 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_LAST_USED_TIMESTAMP;
576             case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT:
577                 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_COUNT;
578             case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP:
579                 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_LAST_USED_TIMESTAMP;
580             case SearchSpec.RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION:
581                 return ScoringSpecProto.RankingStrategy.Code.ADVANCED_SCORING_EXPRESSION;
582             case SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE:
583                 return ScoringSpecProto.RankingStrategy.Code.JOIN_AGGREGATE_SCORE;
584             default:
585                 throw new IllegalArgumentException("Invalid result ranking strategy: "
586                         + rankingStrategyCode);
587         }
588     }
589 
590     /**
591      * Maps a list of AppSearch search feature strings to the list of the corresponding Icing
592      * feature strings.
593      *
594      * @param appSearchFeatures The list of AppSearch search feature strings.
595      */
toIcingSearchFeatures( @onNull List<String> appSearchFeatures)596     private static @NonNull List<String> toIcingSearchFeatures(
597             @NonNull List<String> appSearchFeatures) {
598         List<String> result = new ArrayList<>();
599         for (int i = 0; i < appSearchFeatures.size(); i++) {
600             String appSearchFeature = appSearchFeatures.get(i);
601             if (appSearchFeature.equals(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION)) {
602                 result.add("HAS_PROPERTY_FUNCTION");
603             } else if (appSearchFeature.equals(
604                     FeatureConstants.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)) {
605                 result.add("MATCH_SCORE_EXPRESSION_FUNCTION");
606             } else {
607                 result.add(appSearchFeature);
608             }
609         }
610         return result;
611     }
612 
613     /**
614      * Returns a Map of namespace to prefixedNamespaces. This is NOT necessarily the
615      * same as the list of namespaces. If a namespace exists under different packages and/or
616      * different databases, they should still be grouped together.
617      *
618      * @param prefixes          Prefixes that we should prepend to all our filters.
619      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
620      */
getNamespaceToPrefixedNamespaces( @onNull Set<String> prefixes, @NonNull NamespaceCache namespaceCache)621     private static Map<String, List<String>> getNamespaceToPrefixedNamespaces(
622             @NonNull Set<String> prefixes,
623             @NonNull NamespaceCache namespaceCache) {
624         Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
625         for (String prefix : prefixes) {
626             Set<String> prefixedNamespaces = namespaceCache.getPrefixedDocumentNamespaces(prefix);
627             if (prefixedNamespaces == null) {
628                 continue;
629             }
630             for (String prefixedNamespace : prefixedNamespaces) {
631                 String namespace;
632                 try {
633                     namespace = removePrefix(prefixedNamespace);
634                 } catch (AppSearchException e) {
635                     // This should never happen. Skip this namespace if it does.
636                     Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
637                     continue;
638                 }
639                 List<String> groupedPrefixedNamespaces =
640                         namespaceToPrefixedNamespaces.get(namespace);
641                 if (groupedPrefixedNamespaces == null) {
642                     groupedPrefixedNamespaces = new ArrayList<>();
643                     namespaceToPrefixedNamespaces.put(namespace, groupedPrefixedNamespaces);
644                 }
645                 groupedPrefixedNamespaces.add(prefixedNamespace);
646             }
647         }
648         return namespaceToPrefixedNamespaces;
649     }
650 
651     /**
652      * Returns a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
653      * same as the list of namespaces. If one package has multiple databases, each with the same
654      * namespace, then those should be grouped together.
655      *
656      * @param prefixes          Prefixes that we should prepend to all our filters.
657      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
658      */
getPackageAndNamespaceToPrefixedNamespaces( @onNull Set<String> prefixes, @NonNull NamespaceCache namespaceCache)659     private static Map<String, List<String>> getPackageAndNamespaceToPrefixedNamespaces(
660             @NonNull Set<String> prefixes,
661             @NonNull NamespaceCache namespaceCache) {
662         Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
663         for (String prefix : prefixes) {
664             Set<String> prefixedNamespaces = namespaceCache.getPrefixedDocumentNamespaces(prefix);
665             if (prefixedNamespaces == null) {
666                 continue;
667             }
668             String packageName = getPackageName(prefix);
669             // Create a new prefix without the database name. This will allow us to group namespaces
670             // that have the same name and package but a different database name together.
671             String emptyDatabasePrefix = createPrefix(packageName, /* databaseName= */"");
672             for (String prefixedNamespace : prefixedNamespaces) {
673                 String namespace;
674                 try {
675                     namespace = removePrefix(prefixedNamespace);
676                 } catch (AppSearchException e) {
677                     // This should never happen. Skip this namespace if it does.
678                     Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
679                     continue;
680                 }
681                 String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
682                 List<String> namespaceList =
683                         packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
684                 if (namespaceList == null) {
685                     namespaceList = new ArrayList<>();
686                     packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
687                             namespaceList);
688                 }
689                 namespaceList.add(prefixedNamespace);
690             }
691         }
692         return packageAndNamespaceToNamespaces;
693     }
694 
695     /**
696      * Returns a map of schema to prefixedSchemas. This is NOT necessarily the
697      * same as the list of schemas. If a schema exists under different packages and/or
698      * different databases, they should still be grouped together.
699      *
700      * @param prefixes      Prefixes that we should prepend to all our filters.
701      * @param schemaCache   The SchemaCache instance held in AppSearch.
702      */
getSchemaToPrefixedSchemas( @onNull Set<String> prefixes, @NonNull SchemaCache schemaCache)703     private static Map<String, List<String>> getSchemaToPrefixedSchemas(
704             @NonNull Set<String> prefixes,
705             @NonNull SchemaCache schemaCache) {
706         Map<String, List<String>> schemaToPrefixedSchemas = new ArrayMap<>();
707         for (String prefix : prefixes) {
708             Map<String, SchemaTypeConfigProto> prefixedSchemas =
709                     schemaCache.getSchemaMapForPrefix(prefix);
710             for (String prefixedSchema : prefixedSchemas.keySet()) {
711                 String schema;
712                 try {
713                     schema = removePrefix(prefixedSchema);
714                 } catch (AppSearchException e) {
715                     // This should never happen. Skip this schema if it does.
716                     Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed.");
717                     continue;
718                 }
719                 List<String> groupedPrefixedSchemas =
720                         schemaToPrefixedSchemas.get(schema);
721                 if (groupedPrefixedSchemas == null) {
722                     groupedPrefixedSchemas = new ArrayList<>();
723                     schemaToPrefixedSchemas.put(schema, groupedPrefixedSchemas);
724                 }
725                 groupedPrefixedSchemas.add(prefixedSchema);
726             }
727         }
728         return schemaToPrefixedSchemas;
729     }
730 
731     /**
732      * Returns a map of schema to a set of prefixedSchemas, grouped by ending schema string.
733      *
734      * For example, an input of
735      *   {
736      *   "package1$database1/gmail", "package1$database2/gmail",
737      *   "package1$database1/person", "package1$database2/person"}
738      *   will return an output of:
739      *   {
740      *   "gmail": {"package1$database1/gmail", "package1$database2/gmail"},
741      *   "person": {"package1$database1/person", "package1$database2/person"},
742      *   }
743      */
createSchemaToPrefixedSchemasMap( Set<String> prefixedSchemas)744     private Map<String, Set<String>> createSchemaToPrefixedSchemasMap(
745             Set<String> prefixedSchemas) {
746         Map<String, Set<String>> schemasToPrefixedSchemas = new ArrayMap<>();
747         for (String prefixedSchema : prefixedSchemas) {
748             String schema;
749             try {
750                 schema = removePrefix(prefixedSchema);
751             } catch (AppSearchException e) {
752                 // This should never happen. Skip this schema if it does.
753                 Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed.");
754                 continue;
755             }
756             Set<String> prefixedSchemaSet =
757                     schemasToPrefixedSchemas.get(schema);
758             if (prefixedSchemaSet == null) {
759                 prefixedSchemaSet = new ArraySet<>();
760                 schemasToPrefixedSchemas.put(schema, prefixedSchemaSet);
761             }
762             prefixedSchemaSet.add(prefixedSchema);
763         }
764         return schemasToPrefixedSchemas;
765     }
766 
767     /**
768      * Returns a map for package+schema to prefixedSchemas. This is NOT necessarily the
769      * same as the list of schemas. If one package has multiple databases, each with the same
770      * schema, then those should be grouped together.
771      *
772      * @param prefixes      Prefixes that we should prepend to all our filters.
773      * @param schemaCache   The SchemaCache instance held in AppSearch.
774      */
getPackageAndSchemaToPrefixedSchemas( @onNull Set<String> prefixes, @NonNull SchemaCache schemaCache)775     private static Map<String, List<String>> getPackageAndSchemaToPrefixedSchemas(
776             @NonNull Set<String> prefixes,
777             @NonNull SchemaCache schemaCache) {
778         Map<String, List<String>> packageAndSchemaToSchemas = new ArrayMap<>();
779         for (String prefix : prefixes) {
780             Map<String, SchemaTypeConfigProto> prefixedSchemas =
781                     schemaCache.getSchemaMapForPrefix(prefix);
782             String packageName = getPackageName(prefix);
783             // Create a new prefix without the database name. This will allow us to group schemas
784             // that have the same name and package but a different database name together.
785             String emptyDatabasePrefix = createPrefix(packageName, /*database*/"");
786             for (String prefixedSchema : prefixedSchemas.keySet()) {
787                 String schema;
788                 try {
789                     schema = removePrefix(prefixedSchema);
790                 } catch (AppSearchException e) {
791                     // This should never happen. Skip this schema if it does.
792                     Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed.");
793                     continue;
794                 }
795                 String emptyDatabasePrefixedSchema = emptyDatabasePrefix + schema;
796                 List<String> schemaList =
797                         packageAndSchemaToSchemas.get(emptyDatabasePrefixedSchema);
798                 if (schemaList == null) {
799                     schemaList = new ArrayList<>();
800                     packageAndSchemaToSchemas.put(emptyDatabasePrefixedSchema, schemaList);
801                 }
802                 schemaList.add(prefixedSchema);
803             }
804         }
805         return packageAndSchemaToSchemas;
806     }
807 
808     /**
809      * Adds result groupings for each namespace in each package being queried for.
810      *
811      * @param prefixes          Prefixes that we should prepend to all our filters
812      * @param maxNumResults     The maximum number of results for each grouping to support.
813      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
814      * @param resultSpecBuilder ResultSpecs as specified by client
815      */
addPerPackagePerNamespaceResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)816     private static void addPerPackagePerNamespaceResultGroupings(
817             @NonNull Set<String> prefixes,
818             int maxNumResults,
819             @NonNull NamespaceCache namespaceCache,
820             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
821         Map<String, List<String>> packageAndNamespaceToNamespaces =
822                 getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceCache);
823 
824         for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
825             List<ResultSpecProto.ResultGrouping.Entry> entries =
826                     new ArrayList<>(prefixedNamespaces.size());
827             for (int i = 0; i < prefixedNamespaces.size(); i++) {
828                 entries.add(
829                         ResultSpecProto.ResultGrouping.Entry.newBuilder()
830                             .setNamespace(prefixedNamespaces.get(i)).build());
831             }
832             resultSpecBuilder.addResultGroupings(
833                     ResultSpecProto.ResultGrouping.newBuilder()
834                             .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
835         }
836     }
837 
838     /**
839      * Adds result groupings for each schema type in each package being queried for.
840      *
841      * @param prefixes          Prefixes that we should prepend to all our filters.
842      * @param maxNumResults     The maximum number of results for each grouping to support.
843      * @param schemaCache       The SchemaCache instance held in AppSearch.
844      * @param resultSpecBuilder ResultSpecs as a specified by client.
845      */
addPerPackagePerSchemaResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)846     private static void addPerPackagePerSchemaResultGroupings(
847             @NonNull Set<String> prefixes,
848             int maxNumResults,
849             @NonNull SchemaCache schemaCache,
850             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
851         Map<String, List<String>> packageAndSchemaToSchemas =
852                 getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
853 
854         for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
855             List<ResultSpecProto.ResultGrouping.Entry> entries =
856                     new ArrayList<>(prefixedSchemas.size());
857             for (int i = 0; i < prefixedSchemas.size(); i++) {
858                 entries.add(
859                         ResultSpecProto.ResultGrouping.Entry.newBuilder()
860                             .setSchema(prefixedSchemas.get(i)).build());
861             }
862             resultSpecBuilder.addResultGroupings(
863                     ResultSpecProto.ResultGrouping.newBuilder()
864                             .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
865         }
866     }
867 
868     /**
869      * Adds result groupings for each namespace and schema type being queried for.
870      *
871      * @param prefixes          Prefixes that we should prepend to all our filters.
872      * @param maxNumResults     The maximum number of results for each grouping to support.
873      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
874      * @param schemaCache       The SchemaCache instance held in AppSearch.
875      * @param resultSpecBuilder ResultSpec as specified by client.
876      */
addPerPackagePerNamespacePerSchemaResultGrouping( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)877     private static void addPerPackagePerNamespacePerSchemaResultGrouping(
878             @NonNull Set<String> prefixes,
879             int maxNumResults,
880             @NonNull NamespaceCache namespaceCache,
881             @NonNull SchemaCache schemaCache,
882             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
883         Map<String, List<String>> packageAndNamespaceToNamespaces =
884                 getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceCache);
885         Map<String, List<String>> packageAndSchemaToSchemas =
886                 getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
887 
888         for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
889             for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
890                 List<ResultSpecProto.ResultGrouping.Entry> entries =
891                         new ArrayList<>(prefixedNamespaces.size() * prefixedSchemas.size());
892                 // Iterate through all namespaces.
893                 for (int i = 0; i < prefixedNamespaces.size(); i++) {
894                     String namespacePackage = getPackageName(prefixedNamespaces.get(i));
895                     // Iterate through all schemas.
896                     for (int j = 0; j < prefixedSchemas.size(); j++) {
897                         String schemaPackage = getPackageName(prefixedSchemas.get(j));
898                         if (namespacePackage.equals(schemaPackage)) {
899                             entries.add(
900                                     ResultSpecProto.ResultGrouping.Entry.newBuilder()
901                                         .setNamespace(prefixedNamespaces.get(i))
902                                         .setSchema(prefixedSchemas.get(j))
903                                         .build());
904                         }
905                     }
906                 }
907                 if (entries.size() > 0) {
908                     resultSpecBuilder.addResultGroupings(
909                             ResultSpecProto.ResultGrouping.newBuilder()
910                                 .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
911                 }
912             }
913         }
914     }
915 
916     /**
917      * Adds result groupings for each package being queried for.
918      *
919      * @param prefixes          Prefixes that we should prepend to all our filters
920      * @param maxNumResults     The maximum number of results for each grouping to support.
921      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
922      * @param resultSpecBuilder ResultSpecs as specified by client
923      */
addPerPackageResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)924     private static void addPerPackageResultGroupings(
925             @NonNull Set<String> prefixes,
926             int maxNumResults,
927             @NonNull NamespaceCache namespaceCache,
928             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
929         // Build up a map of package to namespaces.
930         Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
931         for (String prefix : prefixes) {
932             Set<String> prefixedNamespaces = namespaceCache.getPrefixedDocumentNamespaces(prefix);
933             if (prefixedNamespaces == null) {
934                 continue;
935             }
936             String packageName = getPackageName(prefix);
937             List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
938             if (packageNamespaceList == null) {
939                 packageNamespaceList = new ArrayList<>();
940                 packageToNamespacesMap.put(packageName, packageNamespaceList);
941             }
942             packageNamespaceList.addAll(prefixedNamespaces);
943         }
944 
945         for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
946             List<ResultSpecProto.ResultGrouping.Entry> entries =
947                     new ArrayList<>(prefixedNamespaces.size());
948             for (String namespace : prefixedNamespaces) {
949                 entries.add(
950                         ResultSpecProto.ResultGrouping.Entry.newBuilder()
951                                 .setNamespace(namespace).build());
952             }
953             resultSpecBuilder.addResultGroupings(
954                     ResultSpecProto.ResultGrouping.newBuilder()
955                             .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
956         }
957     }
958 
959     /**
960      * Adds result groupings for each namespace being queried for.
961      *
962      * @param prefixes          Prefixes that we should prepend to all our filters
963      * @param maxNumResults     The maximum number of results for each grouping to support.
964      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
965      * @param resultSpecBuilder ResultSpecs as specified by client
966      */
addPerNamespaceResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)967     private static void addPerNamespaceResultGroupings(
968             @NonNull Set<String> prefixes,
969             int maxNumResults,
970             @NonNull NamespaceCache namespaceCache,
971             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
972         Map<String, List<String>> namespaceToPrefixedNamespaces =
973                 getNamespaceToPrefixedNamespaces(prefixes, namespaceCache);
974 
975         for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
976             List<ResultSpecProto.ResultGrouping.Entry> entries =
977                     new ArrayList<>(prefixedNamespaces.size());
978             for (int i = 0; i < prefixedNamespaces.size(); i++) {
979                 entries.add(
980                         ResultSpecProto.ResultGrouping.Entry.newBuilder()
981                             .setNamespace(prefixedNamespaces.get(i)).build());
982             }
983             resultSpecBuilder.addResultGroupings(
984                     ResultSpecProto.ResultGrouping.newBuilder()
985                             .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
986         }
987     }
988 
989     /**
990      * Adds result groupings for each schema type being queried for.
991      *
992      * @param prefixes          Prefixes that we should prepend to all our filters.
993      * @param maxNumResults     The maximum number of results for each grouping to support.
994      * @param schemaCache       The SchemaCache instance held in AppSearch.
995      * @param resultSpecBuilder ResultSpec as specified by client.
996      */
addPerSchemaResultGrouping( @onNull Set<String> prefixes, int maxNumResults, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)997     private static void addPerSchemaResultGrouping(
998             @NonNull Set<String> prefixes,
999             int maxNumResults,
1000             @NonNull SchemaCache schemaCache,
1001             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
1002         Map<String, List<String>> schemaToPrefixedSchemas =
1003                 getSchemaToPrefixedSchemas(prefixes, schemaCache);
1004 
1005         for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
1006             List<ResultSpecProto.ResultGrouping.Entry> entries =
1007                     new ArrayList<>(prefixedSchemas.size());
1008             for (int i = 0; i < prefixedSchemas.size(); i++) {
1009                 entries.add(
1010                         ResultSpecProto.ResultGrouping.Entry.newBuilder()
1011                             .setSchema(prefixedSchemas.get(i)).build());
1012             }
1013             resultSpecBuilder.addResultGroupings(
1014                     ResultSpecProto.ResultGrouping.newBuilder()
1015                             .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
1016         }
1017     }
1018 
1019     /**
1020      * Adds result groupings for each namespace and schema type being queried for.
1021      *
1022      * @param prefixes          Prefixes that we should prepend to all our filters.
1023      * @param maxNumResults     The maximum number of results for each grouping to support.
1024      * @param namespaceCache    The NamespaceCache instance held in AppSearch.
1025      * @param schemaCache       The SchemaCache instance held in AppSearch.
1026      * @param resultSpecBuilder ResultSpec as specified by client.
1027      */
addPerNamespaceAndSchemaResultGrouping( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)1028     private static void addPerNamespaceAndSchemaResultGrouping(
1029             @NonNull Set<String> prefixes,
1030             int maxNumResults,
1031             @NonNull NamespaceCache namespaceCache,
1032             @NonNull SchemaCache schemaCache,
1033             ResultSpecProto.@NonNull Builder resultSpecBuilder) {
1034         Map<String, List<String>> namespaceToPrefixedNamespaces =
1035                 getNamespaceToPrefixedNamespaces(prefixes, namespaceCache);
1036         Map<String, List<String>> schemaToPrefixedSchemas =
1037                 getSchemaToPrefixedSchemas(prefixes, schemaCache);
1038 
1039         for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
1040             for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
1041                 List<ResultSpecProto.ResultGrouping.Entry> entries =
1042                         new ArrayList<>(prefixedNamespaces.size() * prefixedSchemas.size());
1043                 // Iterate through all namespaces.
1044                 for (int i = 0; i < prefixedNamespaces.size(); i++) {
1045                     // Iterate through all schemas.
1046                     for (int j = 0; j < prefixedSchemas.size(); j++) {
1047                         try {
1048                             if (getPrefix(prefixedNamespaces.get(i))
1049                                     .equals(getPrefix(prefixedSchemas.get(j)))) {
1050                                 entries.add(
1051                                                 ResultSpecProto.ResultGrouping.Entry.newBuilder()
1052                                                 .setNamespace(prefixedNamespaces.get(i))
1053                                                 .setSchema(prefixedSchemas.get(j))
1054                                                 .build());
1055                             }
1056                         } catch (AppSearchException e) {
1057                             // This should never happen. Skip this schema if it does.
1058                             Log.e(TAG, "Prefixed string " + prefixedNamespaces.get(i) + " or "
1059                                     + prefixedSchemas.get(j) + " is malformed.");
1060                             continue;
1061                         }
1062                     }
1063                 }
1064                 if (entries.size() > 0) {
1065                     resultSpecBuilder.addResultGroupings(
1066                             ResultSpecProto.ResultGrouping.newBuilder()
1067                                 .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
1068                 }
1069             }
1070         }
1071     }
1072 
1073     /**
1074      * Adds {@link TypePropertyWeights} to {@link ScoringSpecProto}.
1075      *
1076      * <p>{@link TypePropertyWeights} are added to the {@link ScoringSpecProto} with database and
1077      * package prefixing added to the schema type.
1078      *
1079      * @param typePropertyWeightsMap a map from unprefixed schema type to an inner-map of property
1080      *                               paths to weight.
1081      * @param scoringSpecBuilder     scoring spec to add weights to.
1082      */
addTypePropertyWeights( @onNull Map<String, Map<String, Double>> typePropertyWeightsMap, ScoringSpecProto.@NonNull Builder scoringSpecBuilder)1083     private void addTypePropertyWeights(
1084             @NonNull Map<String, Map<String, Double>> typePropertyWeightsMap,
1085             ScoringSpecProto.@NonNull Builder scoringSpecBuilder) {
1086         Preconditions.checkNotNull(scoringSpecBuilder);
1087         Preconditions.checkNotNull(typePropertyWeightsMap);
1088 
1089         for (Map.Entry<String, Map<String, Double>> typePropertyWeight :
1090                 typePropertyWeightsMap.entrySet()) {
1091             for (String prefix : mCurrentSearchSpecPrefixFilters) {
1092                 String prefixedSchemaType = prefix + typePropertyWeight.getKey();
1093                 if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) {
1094                     TypePropertyWeights.Builder typePropertyWeightsBuilder =
1095                             TypePropertyWeights.newBuilder().setSchemaType(prefixedSchemaType);
1096 
1097                     for (Map.Entry<String, Double> propertyWeight :
1098                             typePropertyWeight.getValue().entrySet()) {
1099                         typePropertyWeightsBuilder.addPropertyWeights(
1100                                 PropertyWeight.newBuilder().setPath(
1101                                         propertyWeight.getKey()).setWeight(
1102                                         propertyWeight.getValue()));
1103                     }
1104 
1105                     scoringSpecBuilder.addTypePropertyWeights(typePropertyWeightsBuilder);
1106                 }
1107             }
1108         }
1109     }
1110 
extractEnabledSearchFeatures(List<String> allEnabledFeatures)1111     List<String> extractEnabledSearchFeatures(List<String> allEnabledFeatures) {
1112         List<String> searchFeatures = new ArrayList<>();
1113         for (int i = 0; i < allEnabledFeatures.size(); ++i) {
1114             String feature = allEnabledFeatures.get(i);
1115             if (FeatureConstants.SCORABLE_FEATURE_SET.contains(feature)) {
1116                 // The `allEnabledFeatures` set contains both scorable features and search features.
1117                 // Here, we extract the search related features and populate them to
1118                 // `SearchSpecProto`. The scoring related features are later populated to the
1119                 // `ScoringSpecProto` individually in `toScoringSpecProto()`.
1120                 // - This is because in Icing, the search expression and scoring expression are
1121                 //   parsed separately, and the enforcement of these enabled features are separate.
1122                 //   Icing needs the two different proto messages to distinguish between
1123                 //   features in the search expression and features in the scoring expression.
1124                 continue;
1125             }
1126             searchFeatures.add(feature);
1127         }
1128         return searchFeatures;
1129     }
1130 }
1131