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