• 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 android.app.appsearch;
18 
19 import android.annotation.IntDef;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.SuppressLint;
23 import android.app.appsearch.util.BundleUtil;
24 import android.os.Bundle;
25 import android.util.ArrayMap;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Objects;
38 import java.util.Set;
39 
40 /**
41  * This class represents the specification logic for AppSearch. It can be used to set the type of
42  * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
43  */
44 // TODO(sidchhabra) : AddResultSpec fields for Snippets etc.
45 public final class SearchSpec {
46     /**
47      * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply property paths to
48      * all results, excepting any types that have had their own, specific property paths set.
49      */
50     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
51 
52     static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
53     static final String SCHEMA_FIELD = "schema";
54     static final String NAMESPACE_FIELD = "namespace";
55     static final String PACKAGE_NAME_FIELD = "packageName";
56     static final String NUM_PER_PAGE_FIELD = "numPerPage";
57     static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
58     static final String ORDER_FIELD = "order";
59     static final String SNIPPET_COUNT_FIELD = "snippetCount";
60     static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
61     static final String MAX_SNIPPET_FIELD = "maxSnippet";
62     static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
63     static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
64     static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
65 
66     /** @hide */
67     public static final int DEFAULT_NUM_PER_PAGE = 10;
68 
69     // TODO(b/170371356): In framework, we may want these limits to be flag controlled.
70     //  If that happens, the @IntRange() directives in this class may have to change.
71     private static final int MAX_NUM_PER_PAGE = 10_000;
72     private static final int MAX_SNIPPET_COUNT = 10_000;
73     private static final int MAX_SNIPPET_PER_PROPERTY_COUNT = 10_000;
74     private static final int MAX_SNIPPET_SIZE_LIMIT = 10_000;
75 
76     /**
77      * Term Match Type for the query.
78      *
79      * @hide
80      */
81     // NOTE: The integer values of these constants must match the proto enum constants in
82     // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType}
83     @IntDef(value = {TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX})
84     @Retention(RetentionPolicy.SOURCE)
85     public @interface TermMatch {}
86 
87     /**
88      * Query terms will only match exact tokens in the index.
89      *
90      * <p>Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football".
91      */
92     public static final int TERM_MATCH_EXACT_ONLY = 1;
93     /**
94      * Query terms will match indexed tokens when the query term is a prefix of the token.
95      *
96      * <p>Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football".
97      */
98     public static final int TERM_MATCH_PREFIX = 2;
99 
100     /**
101      * Ranking Strategy for query result.
102      *
103      * @hide
104      */
105     // NOTE: The integer values of these constants must match the proto enum constants in
106     // {@link ScoringSpecProto.RankingStrategy.Code}
107     @IntDef(
108             value = {
109                 RANKING_STRATEGY_NONE,
110                 RANKING_STRATEGY_DOCUMENT_SCORE,
111                 RANKING_STRATEGY_CREATION_TIMESTAMP,
112                 RANKING_STRATEGY_RELEVANCE_SCORE,
113                 RANKING_STRATEGY_USAGE_COUNT,
114                 RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP,
115                 RANKING_STRATEGY_SYSTEM_USAGE_COUNT,
116                 RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP,
117             })
118     @Retention(RetentionPolicy.SOURCE)
119     public @interface RankingStrategy {}
120 
121     /** No Ranking, results are returned in arbitrary order. */
122     public static final int RANKING_STRATEGY_NONE = 0;
123     /** Ranked by app-provided document scores. */
124     public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
125     /** Ranked by document creation timestamps. */
126     public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
127     /** Ranked by document relevance score. */
128     public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
129     /** Ranked by number of usages, as reported by the app. */
130     public static final int RANKING_STRATEGY_USAGE_COUNT = 4;
131     /** Ranked by timestamp of last usage, as reported by the app. */
132     public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5;
133     /** Ranked by number of usages from a system UI surface. */
134     public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6;
135     /** Ranked by timestamp of last usage from a system UI surface. */
136     public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7;
137 
138     /**
139      * Order for query result.
140      *
141      * @hide
142      */
143     // NOTE: The integer values of these constants must match the proto enum constants in
144     // {@link ScoringSpecProto.Order.Code}
145     @IntDef(value = {ORDER_DESCENDING, ORDER_ASCENDING})
146     @Retention(RetentionPolicy.SOURCE)
147     public @interface Order {}
148 
149     /** Search results will be returned in a descending order. */
150     public static final int ORDER_DESCENDING = 0;
151     /** Search results will be returned in an ascending order. */
152     public static final int ORDER_ASCENDING = 1;
153 
154     /**
155      * Grouping type for result limits.
156      *
157      * @hide
158      */
159     @IntDef(
160             flag = true,
161             value = {GROUPING_TYPE_PER_PACKAGE, GROUPING_TYPE_PER_NAMESPACE})
162     @Retention(RetentionPolicy.SOURCE)
163     public @interface GroupingType {}
164 
165     /**
166      * Results should be grouped together by package for the purpose of enforcing a limit on the
167      * number of results returned per package.
168      */
169     public static final int GROUPING_TYPE_PER_PACKAGE = 0b01;
170     /**
171      * Results should be grouped together by namespace for the purpose of enforcing a limit on the
172      * number of results returned per namespace.
173      */
174     public static final int GROUPING_TYPE_PER_NAMESPACE = 0b10;
175 
176     private final Bundle mBundle;
177 
178     /** @hide */
SearchSpec(@onNull Bundle bundle)179     public SearchSpec(@NonNull Bundle bundle) {
180         Objects.requireNonNull(bundle);
181         mBundle = bundle;
182     }
183 
184     /**
185      * Returns the {@link Bundle} populated by this builder.
186      *
187      * @hide
188      */
189     @NonNull
getBundle()190     public Bundle getBundle() {
191         return mBundle;
192     }
193 
194     /** Returns how the query terms should match terms in the index. */
getTermMatch()195     public @TermMatch int getTermMatch() {
196         return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
197     }
198 
199     /**
200      * Returns the list of schema types to search for.
201      *
202      * <p>If empty, the query will search over all schema types.
203      */
204     @NonNull
getFilterSchemas()205     public List<String> getFilterSchemas() {
206         List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
207         if (schemas == null) {
208             return Collections.emptyList();
209         }
210         return Collections.unmodifiableList(schemas);
211     }
212 
213     /**
214      * Returns the list of namespaces to search over.
215      *
216      * <p>If empty, the query will search over all namespaces.
217      */
218     @NonNull
getFilterNamespaces()219     public List<String> getFilterNamespaces() {
220         List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
221         if (namespaces == null) {
222             return Collections.emptyList();
223         }
224         return Collections.unmodifiableList(namespaces);
225     }
226 
227     /**
228      * Returns the list of package name filters to search over.
229      *
230      * <p>If empty, the query will search over all packages that the caller has access to. If
231      * package names are specified which caller doesn't have access to, then those package names
232      * will be ignored.
233      */
234     @NonNull
getFilterPackageNames()235     public List<String> getFilterPackageNames() {
236         List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
237         if (packageNames == null) {
238             return Collections.emptyList();
239         }
240         return Collections.unmodifiableList(packageNames);
241     }
242 
243     /** Returns the number of results per page in the result set. */
getResultCountPerPage()244     public int getResultCountPerPage() {
245         return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
246     }
247 
248     /** Returns the ranking strategy. */
getRankingStrategy()249     public @RankingStrategy int getRankingStrategy() {
250         return mBundle.getInt(RANKING_STRATEGY_FIELD);
251     }
252 
253     /** Returns the order of returned search results (descending or ascending). */
getOrder()254     public @Order int getOrder() {
255         return mBundle.getInt(ORDER_FIELD);
256     }
257 
258     /** Returns how many documents to generate snippets for. */
getSnippetCount()259     public int getSnippetCount() {
260         return mBundle.getInt(SNIPPET_COUNT_FIELD);
261     }
262 
263     /**
264      * Returns how many matches for each property of a matching document to generate snippets for.
265      */
getSnippetCountPerProperty()266     public int getSnippetCountPerProperty() {
267         return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD);
268     }
269 
270     /** Returns the maximum size of a snippet in characters. */
getMaxSnippetSize()271     public int getMaxSnippetSize() {
272         return mBundle.getInt(MAX_SNIPPET_FIELD);
273     }
274 
275     /**
276      * Returns a map from schema type to property paths to be used for projection.
277      *
278      * <p>If the map is empty, then all properties will be retrieved for all results.
279      *
280      * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
281      * function, rather than calling it multiple times.
282      */
283     @NonNull
getProjections()284     public Map<String, List<String>> getProjections() {
285         Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
286         Set<String> schemas = typePropertyPathsBundle.keySet();
287         Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
288         for (String schema : schemas) {
289             typePropertyPathsMap.put(schema, typePropertyPathsBundle.getStringArrayList(schema));
290         }
291         return typePropertyPathsMap;
292     }
293 
294     /**
295      * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
296      * called.
297      */
getResultGroupingTypeFlags()298     public @GroupingType int getResultGroupingTypeFlags() {
299         return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
300     }
301 
302     /**
303      * Get the maximum number of results to return for each group.
304      *
305      * @return the maximum number of results to return for each group or Integer.MAX_VALUE if {@link
306      *     Builder#setResultGrouping(int, int)} was not called.
307      */
getResultGroupingLimit()308     public int getResultGroupingLimit() {
309         return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
310     }
311 
312     /** Builder for {@link SearchSpec objects}. */
313     public static final class Builder {
314         private ArrayList<String> mSchemas = new ArrayList<>();
315         private ArrayList<String> mNamespaces = new ArrayList<>();
316         private ArrayList<String> mPackageNames = new ArrayList<>();
317         private Bundle mProjectionTypePropertyMasks = new Bundle();
318 
319         private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
320         private @TermMatch int mTermMatchType = TERM_MATCH_PREFIX;
321         private int mSnippetCount = 0;
322         private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
323         private int mMaxSnippetSize = 0;
324         private @RankingStrategy int mRankingStrategy = RANKING_STRATEGY_NONE;
325         private @Order int mOrder = ORDER_DESCENDING;
326         private @GroupingType int mGroupingTypeFlags = 0;
327         private int mGroupingLimit = 0;
328         private boolean mBuilt = false;
329 
330         /**
331          * Indicates how the query terms should match {@code TermMatchCode} in the index.
332          *
333          * <p>If this method is not called, the default term match type is {@link
334          * SearchSpec#TERM_MATCH_PREFIX}.
335          */
336         @NonNull
setTermMatch(@ermMatch int termMatchType)337         public Builder setTermMatch(@TermMatch int termMatchType) {
338             Preconditions.checkArgumentInRange(
339                     termMatchType, TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX, "Term match type");
340             resetIfBuilt();
341             mTermMatchType = termMatchType;
342             return this;
343         }
344 
345         /**
346          * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
347          * have the specified schema types.
348          *
349          * <p>If unset, the query will search over all schema types.
350          */
351         @NonNull
addFilterSchemas(@onNull String... schemas)352         public Builder addFilterSchemas(@NonNull String... schemas) {
353             Objects.requireNonNull(schemas);
354             resetIfBuilt();
355             return addFilterSchemas(Arrays.asList(schemas));
356         }
357 
358         /**
359          * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
360          * have the specified schema types.
361          *
362          * <p>If unset, the query will search over all schema types.
363          */
364         @NonNull
addFilterSchemas(@onNull Collection<String> schemas)365         public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
366             Objects.requireNonNull(schemas);
367             resetIfBuilt();
368             mSchemas.addAll(schemas);
369             return this;
370         }
371 
372         /**
373          * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have
374          * the specified namespaces.
375          *
376          * <p>If unset, the query will search over all namespaces.
377          */
378         @NonNull
addFilterNamespaces(@onNull String... namespaces)379         public Builder addFilterNamespaces(@NonNull String... namespaces) {
380             Objects.requireNonNull(namespaces);
381             resetIfBuilt();
382             return addFilterNamespaces(Arrays.asList(namespaces));
383         }
384 
385         /**
386          * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have
387          * the specified namespaces.
388          *
389          * <p>If unset, the query will search over all namespaces.
390          */
391         @NonNull
addFilterNamespaces(@onNull Collection<String> namespaces)392         public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
393             Objects.requireNonNull(namespaces);
394             resetIfBuilt();
395             mNamespaces.addAll(namespaces);
396             return this;
397         }
398 
399         /**
400          * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
401          * were indexed from the specified packages.
402          *
403          * <p>If unset, the query will search over all packages that the caller has access to. If
404          * package names are specified which caller doesn't have access to, then those package names
405          * will be ignored.
406          */
407         @NonNull
addFilterPackageNames(@onNull String... packageNames)408         public Builder addFilterPackageNames(@NonNull String... packageNames) {
409             Objects.requireNonNull(packageNames);
410             resetIfBuilt();
411             return addFilterPackageNames(Arrays.asList(packageNames));
412         }
413 
414         /**
415          * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
416          * were indexed from the specified packages.
417          *
418          * <p>If unset, the query will search over all packages that the caller has access to. If
419          * package names are specified which caller doesn't have access to, then those package names
420          * will be ignored.
421          */
422         @NonNull
addFilterPackageNames(@onNull Collection<String> packageNames)423         public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) {
424             Objects.requireNonNull(packageNames);
425             resetIfBuilt();
426             mPackageNames.addAll(packageNames);
427             return this;
428         }
429 
430         /**
431          * Sets the number of results per page in the returned object.
432          *
433          * <p>The default number of results per page is 10.
434          */
435         @NonNull
setResultCountPerPage( @ntRangefrom = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage)436         public SearchSpec.Builder setResultCountPerPage(
437                 @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) {
438             Preconditions.checkArgumentInRange(
439                     resultCountPerPage, 0, MAX_NUM_PER_PAGE, "resultCountPerPage");
440             resetIfBuilt();
441             mResultCountPerPage = resultCountPerPage;
442             return this;
443         }
444 
445         /** Sets ranking strategy for AppSearch results. */
446         @NonNull
setRankingStrategy(@ankingStrategy int rankingStrategy)447         public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
448             Preconditions.checkArgumentInRange(
449                     rankingStrategy,
450                     RANKING_STRATEGY_NONE,
451                     RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP,
452                     "Result ranking strategy");
453             resetIfBuilt();
454             mRankingStrategy = rankingStrategy;
455             return this;
456         }
457 
458         /**
459          * Indicates the order of returned search results, the default is {@link #ORDER_DESCENDING},
460          * meaning that results with higher scores come first.
461          *
462          * <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}.
463          */
464         @NonNull
setOrder(@rder int order)465         public Builder setOrder(@Order int order) {
466             Preconditions.checkArgumentInRange(
467                     order, ORDER_DESCENDING, ORDER_ASCENDING, "Result ranking order");
468             resetIfBuilt();
469             mOrder = order;
470             return this;
471         }
472 
473         /**
474          * Only the first {@code snippetCount} documents based on the ranking strategy will have
475          * snippet information provided.
476          *
477          * <p>The list returned from {@link SearchResult#getMatchInfos} will contain at most this
478          * many entries.
479          *
480          * <p>If set to 0 (default), snippeting is disabled and the list returned from {@link
481          * SearchResult#getMatchInfos} will be empty.
482          */
483         @NonNull
setSnippetCount( @ntRangefrom = 0, to = MAX_SNIPPET_COUNT) int snippetCount)484         public SearchSpec.Builder setSnippetCount(
485                 @IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) {
486             Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount");
487             resetIfBuilt();
488             mSnippetCount = snippetCount;
489             return this;
490         }
491 
492         /**
493          * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty}
494          * snippets for each property of each {@link GenericDocument} will contain snippet
495          * information.
496          *
497          * <p>If set to 0, snippeting is disabled and the list returned from {@link
498          * SearchResult#getMatchInfos} will be empty.
499          *
500          * <p>The default behavior is to snippet all matches a property contains, up to the maximum
501          * value of 10,000.
502          */
503         @NonNull
setSnippetCountPerProperty( @ntRangefrom = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT) int snippetCountPerProperty)504         public SearchSpec.Builder setSnippetCountPerProperty(
505                 @IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT)
506                         int snippetCountPerProperty) {
507             Preconditions.checkArgumentInRange(
508                     snippetCountPerProperty,
509                     0,
510                     MAX_SNIPPET_PER_PROPERTY_COUNT,
511                     "snippetCountPerProperty");
512             resetIfBuilt();
513             mSnippetCountPerProperty = snippetCountPerProperty;
514             return this;
515         }
516 
517         /**
518          * Sets {@code maxSnippetSize}, the maximum snippet size. Snippet windows start at {@code
519          * maxSnippetSize/2} bytes before the middle of the matching token and end at {@code
520          * maxSnippetSize/2} bytes after the middle of the matching token. It respects token
521          * boundaries, therefore the returned window may be smaller than requested.
522          *
523          * <p>Setting {@code maxSnippetSize} to 0 will disable windowing and an empty string will be
524          * returned. If matches enabled is also set to false, then snippeting is disabled.
525          *
526          * <p>Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will
527          * return a window of "bar baz bat" which is only 11 bytes long.
528          */
529         @NonNull
setMaxSnippetSize( @ntRangefrom = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize)530         public SearchSpec.Builder setMaxSnippetSize(
531                 @IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) {
532             Preconditions.checkArgumentInRange(
533                     maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize");
534             resetIfBuilt();
535             mMaxSnippetSize = maxSnippetSize;
536             return this;
537         }
538 
539         /**
540          * Adds property paths for the specified type to be used for projection. If property paths
541          * are added for a type, then only the properties referred to will be retrieved for results
542          * of that type. If a property path that is specified isn't present in a result, it will be
543          * ignored for that result. Property paths cannot be null.
544          *
545          * <p>If no property paths are added for a particular type, then all properties of results
546          * of that type will be retrieved.
547          *
548          * <p>If property path is added for the {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD},
549          * then those property paths will apply to all results, excepting any types that have their
550          * own, specific property paths set.
551          *
552          * <p>Suppose the following document is in the index.
553          *
554          * <pre>{@code
555          * Email: Document {
556          *   sender: Document {
557          *     name: "Mr. Person"
558          *     email: "mrperson123@google.com"
559          *   }
560          *   recipients: [
561          *     Document {
562          *       name: "John Doe"
563          *       email: "johndoe123@google.com"
564          *     }
565          *     Document {
566          *       name: "Jane Doe"
567          *       email: "janedoe123@google.com"
568          *     }
569          *   ]
570          *   subject: "IMPORTANT"
571          *   body: "Limited time offer!"
572          * }
573          * }</pre>
574          *
575          * <p>Then, suppose that a query for "important" is issued with the following projection
576          * type property paths:
577          *
578          * <pre>{@code
579          * {schema: "Email", ["subject", "sender.name", "recipients.name"]}
580          * }</pre>
581          *
582          * <p>The above document will be returned as:
583          *
584          * <pre>{@code
585          * Email: Document {
586          *   sender: Document {
587          *     name: "Mr. Body"
588          *   }
589          *   recipients: [
590          *     Document {
591          *       name: "John Doe"
592          *     }
593          *     Document {
594          *       name: "Jane Doe"
595          *     }
596          *   ]
597          *   subject: "IMPORTANT"
598          * }
599          * }</pre>
600          */
601         @NonNull
addProjection( @onNull String schema, @NonNull Collection<String> propertyPaths)602         public SearchSpec.Builder addProjection(
603                 @NonNull String schema, @NonNull Collection<String> propertyPaths) {
604             Objects.requireNonNull(schema);
605             Objects.requireNonNull(propertyPaths);
606             resetIfBuilt();
607             ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
608             for (String propertyPath : propertyPaths) {
609                 Objects.requireNonNull(propertyPath);
610                 propertyPathsArrayList.add(propertyPath);
611             }
612             mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList);
613             return this;
614         }
615 
616         /**
617          * Set the maximum number of results to return for each group, where groups are defined by
618          * grouping type.
619          *
620          * <p>Calling this method will override any previous calls. So calling
621          * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7) and then calling
622          * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2) will result in only the latter, a limit
623          * of two results per package, being applied. Or calling setResultGrouping
624          * (GROUPING_TYPE_PER_PACKAGE, 1) and then calling setResultGrouping
625          * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5) will result in five results per
626          * package per namespace.
627          *
628          * @param groupingTypeFlags One or more combination of grouping types.
629          * @param limit Number of results to return per {@code groupingTypeFlags}.
630          * @throws IllegalArgumentException if groupingTypeFlags is zero.
631          */
632         // Individual parameters available from getResultGroupingTypeFlags and
633         // getResultGroupingLimit
634         @SuppressLint("MissingGetterMatchingBuilder")
635         @NonNull
setResultGrouping(@roupingType int groupingTypeFlags, int limit)636         public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) {
637             Preconditions.checkState(
638                     groupingTypeFlags != 0, "Result grouping type cannot be zero.");
639             resetIfBuilt();
640             mGroupingTypeFlags = groupingTypeFlags;
641             mGroupingLimit = limit;
642             return this;
643         }
644 
645         /** Constructs a new {@link SearchSpec} from the contents of this builder. */
646         @NonNull
build()647         public SearchSpec build() {
648             Bundle bundle = new Bundle();
649             bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
650             bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
651             bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
652             bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
653             bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);
654             bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);
655             bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);
656             bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);
657             bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);
658             bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
659             bundle.putInt(ORDER_FIELD, mOrder);
660             bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
661             bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
662             mBuilt = true;
663             return new SearchSpec(bundle);
664         }
665 
resetIfBuilt()666         private void resetIfBuilt() {
667             if (mBuilt) {
668                 mSchemas = new ArrayList<>(mSchemas);
669                 mNamespaces = new ArrayList<>(mNamespaces);
670                 mPackageNames = new ArrayList<>(mPackageNames);
671                 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
672                 mBuilt = false;
673             }
674         }
675     }
676 }
677