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