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