• 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.NonNull;
21 import android.annotation.Nullable;
22 import android.app.appsearch.annotation.CanIgnoreReturnValue;
23 import android.app.appsearch.exceptions.IllegalSchemaException;
24 import android.app.appsearch.util.BundleUtil;
25 import android.app.appsearch.util.IndentingStringBuilder;
26 import android.os.Bundle;
27 import android.util.ArraySet;
28 
29 import com.android.internal.util.Preconditions;
30 
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Objects;
38 import java.util.Set;
39 
40 /**
41  * The AppSearch Schema for a particular type of document.
42  *
43  * <p>For example, an e-mail message or a music recording could be a schema type.
44  *
45  * <p>The schema consists of type information, properties, and config (like tokenization type).
46  *
47  * @see AppSearchSession#setSchema
48  */
49 public final class AppSearchSchema {
50     private static final String SCHEMA_TYPE_FIELD = "schemaType";
51     private static final String PROPERTIES_FIELD = "properties";
52 
53     private final Bundle mBundle;
54 
55     /** @hide */
AppSearchSchema(@onNull Bundle bundle)56     public AppSearchSchema(@NonNull Bundle bundle) {
57         Objects.requireNonNull(bundle);
58         mBundle = bundle;
59     }
60 
61     /**
62      * Returns the {@link Bundle} populated by this builder.
63      *
64      * @hide
65      */
66     @NonNull
getBundle()67     public Bundle getBundle() {
68         return mBundle;
69     }
70 
71     @Override
72     @NonNull
toString()73     public String toString() {
74         IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
75         appendAppSearchSchemaString(stringBuilder);
76         return stringBuilder.toString();
77     }
78 
79     /**
80      * Appends a debugging string for the {@link AppSearchSchema} instance to the given string
81      * builder.
82      *
83      * @param builder the builder to append to.
84      */
appendAppSearchSchemaString(@onNull IndentingStringBuilder builder)85     private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
86         Objects.requireNonNull(builder);
87 
88         builder.append("{\n");
89         builder.increaseIndentLevel();
90         builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
91         builder.append("properties: [\n");
92 
93         AppSearchSchema.PropertyConfig[] sortedProperties =
94                 getProperties().toArray(new AppSearchSchema.PropertyConfig[0]);
95         Arrays.sort(sortedProperties, (o1, o2) -> o1.getName().compareTo(o2.getName()));
96 
97         for (int i = 0; i < sortedProperties.length; i++) {
98             AppSearchSchema.PropertyConfig propertyConfig = sortedProperties[i];
99             builder.increaseIndentLevel();
100             propertyConfig.appendPropertyConfigString(builder);
101             if (i != sortedProperties.length - 1) {
102                 builder.append(",\n");
103             }
104             builder.decreaseIndentLevel();
105         }
106 
107         builder.append("\n");
108         builder.append("]\n");
109         builder.decreaseIndentLevel();
110         builder.append("}");
111     }
112 
113     /** Returns the name of this schema type, such as Email. */
114     @NonNull
getSchemaType()115     public String getSchemaType() {
116         return mBundle.getString(SCHEMA_TYPE_FIELD, "");
117     }
118 
119     /**
120      * Returns the list of {@link PropertyConfig}s that are part of this schema.
121      *
122      * <p>This method creates a new list when called.
123      */
124     @NonNull
125     @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})
getProperties()126     public List<PropertyConfig> getProperties() {
127         ArrayList<Bundle> propertyBundles =
128                 mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD);
129         if (propertyBundles == null || propertyBundles.isEmpty()) {
130             return Collections.emptyList();
131         }
132         List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
133         for (int i = 0; i < propertyBundles.size(); i++) {
134             ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
135         }
136         return ret;
137     }
138 
139     @Override
equals(@ullable Object other)140     public boolean equals(@Nullable Object other) {
141         if (this == other) {
142             return true;
143         }
144         if (!(other instanceof AppSearchSchema)) {
145             return false;
146         }
147         AppSearchSchema otherSchema = (AppSearchSchema) other;
148         if (!getSchemaType().equals(otherSchema.getSchemaType())) {
149             return false;
150         }
151         return getProperties().equals(otherSchema.getProperties());
152     }
153 
154     @Override
hashCode()155     public int hashCode() {
156         return Objects.hash(getSchemaType(), getProperties());
157     }
158 
159     /** Builder for {@link AppSearchSchema objects}. */
160     public static final class Builder {
161         private final String mSchemaType;
162         private ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
163         private final Set<String> mPropertyNames = new ArraySet<>();
164         private boolean mBuilt = false;
165 
166         /** Creates a new {@link AppSearchSchema.Builder}. */
Builder(@onNull String schemaType)167         public Builder(@NonNull String schemaType) {
168             Objects.requireNonNull(schemaType);
169             mSchemaType = schemaType;
170         }
171 
172         /** Adds a property to the given type. */
173         @CanIgnoreReturnValue
174         @NonNull
addProperty(@onNull PropertyConfig propertyConfig)175         public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
176             Objects.requireNonNull(propertyConfig);
177             resetIfBuilt();
178             String name = propertyConfig.getName();
179             if (!mPropertyNames.add(name)) {
180                 throw new IllegalSchemaException("Property defined more than once: " + name);
181             }
182             mPropertyBundles.add(propertyConfig.mBundle);
183             return this;
184         }
185 
186         /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
187         @NonNull
build()188         public AppSearchSchema build() {
189             Bundle bundle = new Bundle();
190             bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
191             bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
192             mBuilt = true;
193             return new AppSearchSchema(bundle);
194         }
195 
resetIfBuilt()196         private void resetIfBuilt() {
197             if (mBuilt) {
198                 mPropertyBundles = new ArrayList<>(mPropertyBundles);
199                 mBuilt = false;
200             }
201         }
202     }
203 
204     /**
205      * Common configuration for a single property (field) in a Document.
206      *
207      * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be a
208      * property.
209      */
210     public abstract static class PropertyConfig {
211         static final String NAME_FIELD = "name";
212         static final String DATA_TYPE_FIELD = "dataType";
213         static final String CARDINALITY_FIELD = "cardinality";
214 
215         /**
216          * Physical data-types of the contents of the property.
217          *
218          * <p>NOTE: The integer values of these constants must match the proto enum constants in
219          * com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
220          *
221          * @hide
222          */
223         @IntDef(
224                 value = {
225                     DATA_TYPE_STRING,
226                     DATA_TYPE_LONG,
227                     DATA_TYPE_DOUBLE,
228                     DATA_TYPE_BOOLEAN,
229                     DATA_TYPE_BYTES,
230                     DATA_TYPE_DOCUMENT,
231                 })
232         @Retention(RetentionPolicy.SOURCE)
233         public @interface DataType {}
234 
235         /** @hide */
236         public static final int DATA_TYPE_STRING = 1;
237 
238         /** @hide */
239         public static final int DATA_TYPE_LONG = 2;
240 
241         /** @hide */
242         public static final int DATA_TYPE_DOUBLE = 3;
243 
244         /** @hide */
245         public static final int DATA_TYPE_BOOLEAN = 4;
246 
247         /**
248          * Unstructured BLOB.
249          *
250          * @hide
251          */
252         public static final int DATA_TYPE_BYTES = 5;
253 
254         /**
255          * Indicates that the property is itself a {@link GenericDocument}, making it part of a
256          * hierarchical schema. Any property using this DataType MUST have a valid {@link
257          * PropertyConfig#getSchemaType}.
258          *
259          * @hide
260          */
261         public static final int DATA_TYPE_DOCUMENT = 6;
262 
263         /**
264          * The cardinality of the property (whether it is required, optional or repeated).
265          *
266          * <p>NOTE: The integer values of these constants must match the proto enum constants in
267          * com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code.
268          *
269          * @hide
270          */
271         @IntDef(
272                 value = {
273                     CARDINALITY_REPEATED,
274                     CARDINALITY_OPTIONAL,
275                     CARDINALITY_REQUIRED,
276                 })
277         @Retention(RetentionPolicy.SOURCE)
278         public @interface Cardinality {}
279 
280         /** Any number of items (including zero) [0...*]. */
281         public static final int CARDINALITY_REPEATED = 1;
282 
283         /** Zero or one value [0,1]. */
284         public static final int CARDINALITY_OPTIONAL = 2;
285 
286         /** Exactly one value [1]. */
287         public static final int CARDINALITY_REQUIRED = 3;
288 
289         final Bundle mBundle;
290 
291         @Nullable private Integer mHashCode;
292 
PropertyConfig(@onNull Bundle bundle)293         PropertyConfig(@NonNull Bundle bundle) {
294             mBundle = Objects.requireNonNull(bundle);
295         }
296 
297         @Override
298         @NonNull
toString()299         public String toString() {
300             IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
301             appendPropertyConfigString(stringBuilder);
302             return stringBuilder.toString();
303         }
304 
305         /**
306          * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the
307          * given string builder.
308          *
309          * @param builder the builder to append to.
310          */
appendPropertyConfigString(@onNull IndentingStringBuilder builder)311         void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) {
312             Objects.requireNonNull(builder);
313 
314             builder.append("{\n");
315             builder.increaseIndentLevel();
316             builder.append("name: \"").append(getName()).append("\",\n");
317 
318             if (this instanceof AppSearchSchema.StringPropertyConfig) {
319                 ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder);
320             } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) {
321                 ((DocumentPropertyConfig) this).appendDocumentPropertyConfigFields(builder);
322             } else if (this instanceof AppSearchSchema.LongPropertyConfig) {
323                 ((LongPropertyConfig) this).appendLongPropertyConfigFields(builder);
324             }
325 
326             switch (getCardinality()) {
327                 case AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED:
328                     builder.append("cardinality: CARDINALITY_REPEATED,\n");
329                     break;
330                 case AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL:
331                     builder.append("cardinality: CARDINALITY_OPTIONAL,\n");
332                     break;
333                 case AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED:
334                     builder.append("cardinality: CARDINALITY_REQUIRED,\n");
335                     break;
336                 default:
337                     builder.append("cardinality: CARDINALITY_UNKNOWN,\n");
338             }
339 
340             switch (getDataType()) {
341                 case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
342                     builder.append("dataType: DATA_TYPE_STRING,\n");
343                     break;
344                 case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG:
345                     builder.append("dataType: DATA_TYPE_LONG,\n");
346                     break;
347                 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
348                     builder.append("dataType: DATA_TYPE_DOUBLE,\n");
349                     break;
350                 case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
351                     builder.append("dataType: DATA_TYPE_BOOLEAN,\n");
352                     break;
353                 case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
354                     builder.append("dataType: DATA_TYPE_BYTES,\n");
355                     break;
356                 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
357                     builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
358                     break;
359                 default:
360                     builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
361             }
362             builder.decreaseIndentLevel();
363             builder.append("}");
364         }
365 
366         /** Returns the name of this property. */
367         @NonNull
getName()368         public String getName() {
369             return mBundle.getString(NAME_FIELD, "");
370         }
371 
372         /**
373          * Returns the type of data the property contains (such as string, int, bytes, etc).
374          *
375          * @hide
376          */
377         @DataType
getDataType()378         public int getDataType() {
379             return mBundle.getInt(DATA_TYPE_FIELD, -1);
380         }
381 
382         /**
383          * Returns the cardinality of the property (whether it is optional, required or repeated).
384          */
385         @Cardinality
getCardinality()386         public int getCardinality() {
387             return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
388         }
389 
390         @Override
equals(@ullable Object other)391         public boolean equals(@Nullable Object other) {
392             if (this == other) {
393                 return true;
394             }
395             if (!(other instanceof PropertyConfig)) {
396                 return false;
397             }
398             PropertyConfig otherProperty = (PropertyConfig) other;
399             return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
400         }
401 
402         @Override
hashCode()403         public int hashCode() {
404             if (mHashCode == null) {
405                 mHashCode = BundleUtil.deepHashCode(mBundle);
406             }
407             return mHashCode;
408         }
409 
410         /**
411          * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data
412          * type.
413          *
414          * <p>The bundle is not cloned.
415          *
416          * @throws IllegalArgumentException if the bundle does no contain a recognized value in its
417          *     {@code DATA_TYPE_FIELD}.
418          * @hide
419          */
420         @NonNull
fromBundle(@onNull Bundle propertyBundle)421         public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
422             switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
423                 case PropertyConfig.DATA_TYPE_STRING:
424                     return new StringPropertyConfig(propertyBundle);
425                 case PropertyConfig.DATA_TYPE_LONG:
426                     return new LongPropertyConfig(propertyBundle);
427                 case PropertyConfig.DATA_TYPE_DOUBLE:
428                     return new DoublePropertyConfig(propertyBundle);
429                 case PropertyConfig.DATA_TYPE_BOOLEAN:
430                     return new BooleanPropertyConfig(propertyBundle);
431                 case PropertyConfig.DATA_TYPE_BYTES:
432                     return new BytesPropertyConfig(propertyBundle);
433                 case PropertyConfig.DATA_TYPE_DOCUMENT:
434                     return new DocumentPropertyConfig(propertyBundle);
435                 default:
436                     throw new IllegalArgumentException(
437                             "Unsupported property bundle of type "
438                                     + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
439                                     + "; contents: "
440                                     + propertyBundle);
441             }
442         }
443     }
444 
445     /** Configuration for a property of type String in a Document. */
446     public static final class StringPropertyConfig extends PropertyConfig {
447         private static final String INDEXING_TYPE_FIELD = "indexingType";
448         private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
449         private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType";
450         private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation";
451 
452         /**
453          * Encapsulates the configurations on how AppSearch should query/index these terms.
454          *
455          * @hide
456          */
457         @IntDef(
458                 value = {
459                     INDEXING_TYPE_NONE,
460                     INDEXING_TYPE_EXACT_TERMS,
461                     INDEXING_TYPE_PREFIXES,
462                 })
463         @Retention(RetentionPolicy.SOURCE)
464         public @interface IndexingType {}
465 
466         /** Content in this property will not be tokenized or indexed. */
467         public static final int INDEXING_TYPE_NONE = 0;
468 
469         /**
470          * Content in this property should only be returned for queries matching the exact tokens
471          * appearing in this property.
472          *
473          * <p>For example, a property with "fool" should NOT match a query for "foo".
474          */
475         public static final int INDEXING_TYPE_EXACT_TERMS = 1;
476 
477         /**
478          * Content in this property should be returned for queries that are either exact matches or
479          * query matches of the tokens appearing in this property.
480          *
481          * <p>For example, a property with "fool" <b>should</b> match a query for "foo".
482          */
483         public static final int INDEXING_TYPE_PREFIXES = 2;
484 
485         /**
486          * Configures how tokens should be extracted from this property.
487          *
488          * <p>NOTE: The integer values of these constants must match the proto enum constants in
489          * com.google.android.icing.proto.IndexingConfig.TokenizerType.Code.
490          *
491          * @hide
492          */
493         @IntDef(
494                 value = {
495                     TOKENIZER_TYPE_NONE,
496                     TOKENIZER_TYPE_PLAIN,
497                     TOKENIZER_TYPE_VERBATIM,
498                     TOKENIZER_TYPE_RFC822
499                 })
500         @Retention(RetentionPolicy.SOURCE)
501         public @interface TokenizerType {}
502 
503         /**
504          * This value indicates that no tokens should be extracted from this property.
505          *
506          * <p>It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is {@link
507          * #INDEXING_TYPE_NONE}.
508          */
509         public static final int TOKENIZER_TYPE_NONE = 0;
510 
511         /**
512          * Tokenization for plain text. This value indicates that tokens should be extracted from
513          * this property based on word breaks. Segments of whitespace and punctuation are not
514          * considered tokens.
515          *
516          * <p>For example, a property with "foo bar. baz." will produce tokens for "foo", "bar" and
517          * "baz". The segments " " and "." will not be considered tokens.
518          *
519          * <p>It is only valid for tokenizer_type to be 'PLAIN' if {@link #getIndexingType} is
520          * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
521          */
522         public static final int TOKENIZER_TYPE_PLAIN = 1;
523 
524         /**
525          * This value indicates that no normalization or segmentation should be applied to string
526          * values that are tokenized using this type. Therefore, the output token is equivalent to
527          * the raw string value.
528          *
529          * <p>For example, a property with "Hello, world!" will produce the token "Hello, world!",
530          * preserving punctuation and capitalization, and not creating separate tokens between the
531          * space.
532          *
533          * <p>It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is
534          * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
535          */
536         public static final int TOKENIZER_TYPE_VERBATIM = 2;
537 
538         /**
539          * Tokenization for emails. This value indicates that tokens should be extracted from this
540          * property based on email structure.
541          *
542          * <p>For example, a property with "alex.sav@google.com" will produce tokens for "alex",
543          * "sav", "alex.sav", "google", "com", and "alexsav@google.com"
544          *
545          * <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
546          * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
547          */
548         public static final int TOKENIZER_TYPE_RFC822 = 3;
549 
550         /**
551          * The joinable value type of the property. By setting the appropriate joinable value type
552          * for a property, the client can use the property for joining documents from other schema
553          * types using Search API (see {@link JoinSpec}).
554          *
555          * @hide
556          */
557         // NOTE: The integer values of these constants must match the proto enum constants in
558         // com.google.android.icing.proto.JoinableConfig.ValueType.Code.
559         @IntDef(
560                 value = {
561                     JOINABLE_VALUE_TYPE_NONE,
562                     JOINABLE_VALUE_TYPE_QUALIFIED_ID,
563                 })
564         @Retention(RetentionPolicy.SOURCE)
565         public @interface JoinableValueType {}
566 
567         /** Content in this property is not joinable. */
568         public static final int JOINABLE_VALUE_TYPE_NONE = 0;
569 
570         /**
571          * Content in this string property will be used as a qualified id to join documents.
572          *
573          * <ul>
574          *   <li>Qualified id: a unique identifier for a document, and this joinable value type is
575          *       similar to primary and foreign key in relational database. See {@link
576          *       android.app.appsearch.util.DocumentIdUtil} for more details.
577          *   <li>Currently we only support single string joining, so it should only be used with
578          *       {@link PropertyConfig#CARDINALITY_OPTIONAL} and {@link
579          *       PropertyConfig#CARDINALITY_REQUIRED}.
580          * </ul>
581          */
582         public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1;
583 
StringPropertyConfig(@onNull Bundle bundle)584         StringPropertyConfig(@NonNull Bundle bundle) {
585             super(bundle);
586         }
587 
588         /** Returns how the property is indexed. */
589         @IndexingType
getIndexingType()590         public int getIndexingType() {
591             return mBundle.getInt(INDEXING_TYPE_FIELD);
592         }
593 
594         /** Returns how this property is tokenized (split into words). */
595         @TokenizerType
getTokenizerType()596         public int getTokenizerType() {
597             return mBundle.getInt(TOKENIZER_TYPE_FIELD);
598         }
599 
600         /**
601          * Returns how this property is going to be used to join documents from other schema types.
602          */
603         @JoinableValueType
getJoinableValueType()604         public int getJoinableValueType() {
605             return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE);
606         }
607 
608         /**
609          * Returns whether or not documents in this schema should be deleted when the document
610          * referenced by this field is deleted.
611          *
612          * @see JoinSpec
613          * @hide
614          */
getDeletionPropagation()615         public boolean getDeletionPropagation() {
616             return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false);
617         }
618 
619         /** Builder for {@link StringPropertyConfig}. */
620         public static final class Builder {
621             private final String mPropertyName;
622             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
623             @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
624             @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE;
625             @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
626             private boolean mDeletionPropagation = false;
627 
628             /** Creates a new {@link StringPropertyConfig.Builder}. */
Builder(@onNull String propertyName)629             public Builder(@NonNull String propertyName) {
630                 mPropertyName = Objects.requireNonNull(propertyName);
631             }
632 
633             /**
634              * Sets the cardinality of the property (whether it is optional, required or repeated).
635              *
636              * <p>If this method is not called, the default cardinality is {@link
637              * PropertyConfig#CARDINALITY_OPTIONAL}.
638              */
639             @CanIgnoreReturnValue
640             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
641             @NonNull
setCardinality(@ardinality int cardinality)642             public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
643                 Preconditions.checkArgumentInRange(
644                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
645                 mCardinality = cardinality;
646                 return this;
647             }
648 
649             /**
650              * Configures how a property should be indexed so that it can be retrieved by queries.
651              *
652              * <p>If this method is not called, the default indexing type is {@link
653              * StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by queries.
654              */
655             @CanIgnoreReturnValue
656             @NonNull
setIndexingType(@ndexingType int indexingType)657             public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
658                 Preconditions.checkArgumentInRange(
659                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
660                 mIndexingType = indexingType;
661                 return this;
662             }
663 
664             /**
665              * Configures how this property should be tokenized (split into words).
666              *
667              * <p>If this method is not called, the default indexing type is {@link
668              * StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized.
669              *
670              * <p>This method must be called with a value other than {@link
671              * StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (that is, if
672              * {@link #setIndexingType} has been called with a value other than {@link
673              * StringPropertyConfig#INDEXING_TYPE_NONE}).
674              */
675             @CanIgnoreReturnValue
676             @NonNull
setTokenizerType(@okenizerType int tokenizerType)677             public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
678                 Preconditions.checkArgumentInRange(
679                         tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_RFC822, "tokenizerType");
680                 mTokenizerType = tokenizerType;
681                 return this;
682             }
683 
684             /**
685              * Configures how this property should be used as a joining matcher.
686              *
687              * <p>If this method is not called, the default joinable value type is {@link
688              * StringPropertyConfig#JOINABLE_VALUE_TYPE_NONE}, so that it is not joinable.
689              *
690              * <p>At most, 64 properties can be set as joinable per schema.
691              */
692             @CanIgnoreReturnValue
693             @NonNull
setJoinableValueType( @oinableValueType int joinableValueType)694             public StringPropertyConfig.Builder setJoinableValueType(
695                     @JoinableValueType int joinableValueType) {
696                 Preconditions.checkArgumentInRange(
697                         joinableValueType,
698                         JOINABLE_VALUE_TYPE_NONE,
699                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
700                         "joinableValueType");
701                 mJoinableValueType = joinableValueType;
702                 return this;
703             }
704 
705             /**
706              * Configures whether or not documents in this schema will be removed when the document
707              * referred to by this property is deleted.
708              *
709              * <p>Requires that a joinable value type is set.
710              *
711              * @hide
712              */
713             @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation
714             @NonNull
setDeletionPropagation(boolean deletionPropagation)715             public Builder setDeletionPropagation(boolean deletionPropagation) {
716                 mDeletionPropagation = deletionPropagation;
717                 return this;
718             }
719 
720             /** Constructs a new {@link StringPropertyConfig} from the contents of this builder. */
721             @NonNull
build()722             public StringPropertyConfig build() {
723                 if (mTokenizerType == TOKENIZER_TYPE_NONE) {
724                     Preconditions.checkState(
725                             mIndexingType == INDEXING_TYPE_NONE,
726                             "Cannot set "
727                                     + "TOKENIZER_TYPE_NONE with an indexing type other than "
728                                     + "INDEXING_TYPE_NONE.");
729                 } else {
730                     Preconditions.checkState(
731                             mIndexingType != INDEXING_TYPE_NONE,
732                             "Cannot set " + "TOKENIZER_TYPE_PLAIN with INDEXING_TYPE_NONE.");
733                 }
734                 if (mJoinableValueType == JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
735                     Preconditions.checkState(
736                             mCardinality != CARDINALITY_REPEATED,
737                             "Cannot set JOINABLE_VALUE_TYPE_QUALIFIED_ID with"
738                                 + " CARDINALITY_REPEATED.");
739                 } else {
740                     Preconditions.checkState(
741                             !mDeletionPropagation,
742                             "Cannot set deletion "
743                                     + "propagation without setting a joinable value type");
744                 }
745                 Bundle bundle = new Bundle();
746                 bundle.putString(NAME_FIELD, mPropertyName);
747                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
748                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
749                 bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
750                 bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);
751                 bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType);
752                 bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation);
753                 return new StringPropertyConfig(bundle);
754             }
755         }
756 
757         /**
758          * Appends a debug string for the {@link StringPropertyConfig} instance to the given string
759          * builder.
760          *
761          * <p>This appends fields specific to a {@link StringPropertyConfig} instance.
762          *
763          * @param builder the builder to append to.
764          */
appendStringPropertyConfigFields(@onNull IndentingStringBuilder builder)765         void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
766             switch (getIndexingType()) {
767                 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
768                     builder.append("indexingType: INDEXING_TYPE_NONE,\n");
769                     break;
770                 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
771                     builder.append("indexingType: INDEXING_TYPE_EXACT_TERMS,\n");
772                     break;
773                 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
774                     builder.append("indexingType: INDEXING_TYPE_PREFIXES,\n");
775                     break;
776                 default:
777                     builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n");
778             }
779 
780             switch (getTokenizerType()) {
781                 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE:
782                     builder.append("tokenizerType: TOKENIZER_TYPE_NONE,\n");
783                     break;
784                 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN:
785                     builder.append("tokenizerType: TOKENIZER_TYPE_PLAIN,\n");
786                     break;
787                 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM:
788                     builder.append("tokenizerType: TOKENIZER_TYPE_VERBATIM,\n");
789                     break;
790                 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822:
791                     builder.append("tokenizerType: TOKENIZER_TYPE_RFC822,\n");
792                     break;
793                 default:
794                     builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n");
795             }
796 
797             switch (getJoinableValueType()) {
798                 case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE:
799                     builder.append("joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n");
800                     break;
801                 case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID:
802                     builder.append("joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n");
803                     break;
804                 default:
805                     builder.append("joinableValueType: JOINABLE_VALUE_TYPE_UNKNOWN,\n");
806             }
807         }
808     }
809 
810     /** Configuration for a property containing a 64-bit integer. */
811     public static final class LongPropertyConfig extends PropertyConfig {
812         private static final String INDEXING_TYPE_FIELD = "indexingType";
813 
814         /**
815          * Encapsulates the configurations on how AppSearch should query/index these 64-bit
816          * integers.
817          *
818          * @hide
819          */
820         @IntDef(value = {INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE})
821         @Retention(RetentionPolicy.SOURCE)
822         public @interface IndexingType {}
823 
824         /** Content in this property will not be indexed. */
825         public static final int INDEXING_TYPE_NONE = 0;
826 
827         /**
828          * Content in this property will be indexed and can be fetched via numeric search range
829          * query.
830          *
831          * <p>For example, a property with 1024 should match numeric search range query [0, 2000].
832          */
833         public static final int INDEXING_TYPE_RANGE = 1;
834 
LongPropertyConfig(@onNull Bundle bundle)835         LongPropertyConfig(@NonNull Bundle bundle) {
836             super(bundle);
837         }
838 
839         /** Returns how the property is indexed. */
840         @IndexingType
getIndexingType()841         public int getIndexingType() {
842             return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);
843         }
844 
845         /** Builder for {@link LongPropertyConfig}. */
846         public static final class Builder {
847             private final String mPropertyName;
848             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
849             @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
850 
851             /** Creates a new {@link LongPropertyConfig.Builder}. */
Builder(@onNull String propertyName)852             public Builder(@NonNull String propertyName) {
853                 mPropertyName = Objects.requireNonNull(propertyName);
854             }
855 
856             /**
857              * Sets the cardinality of the property (whether it is optional, required or repeated).
858              *
859              * <p>If this method is not called, the default cardinality is {@link
860              * PropertyConfig#CARDINALITY_OPTIONAL}.
861              */
862             @CanIgnoreReturnValue
863             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
864             @NonNull
setCardinality(@ardinality int cardinality)865             public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
866                 Preconditions.checkArgumentInRange(
867                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
868                 mCardinality = cardinality;
869                 return this;
870             }
871 
872             /**
873              * Configures how a property should be indexed so that it can be retrieved by queries.
874              *
875              * <p>If this method is not called, the default indexing type is {@link
876              * LongPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed and cannot be
877              * matched by queries.
878              */
879             @CanIgnoreReturnValue
880             @NonNull
setIndexingType(@ndexingType int indexingType)881             public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
882                 Preconditions.checkArgumentInRange(
883                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType");
884                 mIndexingType = indexingType;
885                 return this;
886             }
887 
888             /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */
889             @NonNull
build()890             public LongPropertyConfig build() {
891                 Bundle bundle = new Bundle();
892                 bundle.putString(NAME_FIELD, mPropertyName);
893                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
894                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
895                 bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
896                 return new LongPropertyConfig(bundle);
897             }
898         }
899 
900         /**
901          * Appends a debug string for the {@link LongPropertyConfig} instance to the given string
902          * builder.
903          *
904          * <p>This appends fields specific to a {@link LongPropertyConfig} instance.
905          *
906          * @param builder the builder to append to.
907          */
appendLongPropertyConfigFields(@onNull IndentingStringBuilder builder)908         void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
909             switch (getIndexingType()) {
910                 case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE:
911                     builder.append("indexingType: INDEXING_TYPE_NONE,\n");
912                     break;
913                 case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE:
914                     builder.append("indexingType: INDEXING_TYPE_RANGE,\n");
915                     break;
916                 default:
917                     builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n");
918             }
919         }
920     }
921 
922     /** Configuration for a property containing a double-precision decimal number. */
923     public static final class DoublePropertyConfig extends PropertyConfig {
DoublePropertyConfig(@onNull Bundle bundle)924         DoublePropertyConfig(@NonNull Bundle bundle) {
925             super(bundle);
926         }
927 
928         /** Builder for {@link DoublePropertyConfig}. */
929         public static final class Builder {
930             private final String mPropertyName;
931             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
932 
933             /** Creates a new {@link DoublePropertyConfig.Builder}. */
Builder(@onNull String propertyName)934             public Builder(@NonNull String propertyName) {
935                 mPropertyName = Objects.requireNonNull(propertyName);
936             }
937 
938             /**
939              * Sets the cardinality of the property (whether it is optional, required or repeated).
940              *
941              * <p>If this method is not called, the default cardinality is {@link
942              * PropertyConfig#CARDINALITY_OPTIONAL}.
943              */
944             @CanIgnoreReturnValue
945             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
946             @NonNull
setCardinality(@ardinality int cardinality)947             public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
948                 Preconditions.checkArgumentInRange(
949                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
950                 mCardinality = cardinality;
951                 return this;
952             }
953 
954             /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */
955             @NonNull
build()956             public DoublePropertyConfig build() {
957                 Bundle bundle = new Bundle();
958                 bundle.putString(NAME_FIELD, mPropertyName);
959                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
960                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
961                 return new DoublePropertyConfig(bundle);
962             }
963         }
964     }
965 
966     /** Configuration for a property containing a boolean. */
967     public static final class BooleanPropertyConfig extends PropertyConfig {
BooleanPropertyConfig(@onNull Bundle bundle)968         BooleanPropertyConfig(@NonNull Bundle bundle) {
969             super(bundle);
970         }
971 
972         /** Builder for {@link BooleanPropertyConfig}. */
973         public static final class Builder {
974             private final String mPropertyName;
975             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
976 
977             /** Creates a new {@link BooleanPropertyConfig.Builder}. */
Builder(@onNull String propertyName)978             public Builder(@NonNull String propertyName) {
979                 mPropertyName = Objects.requireNonNull(propertyName);
980             }
981 
982             /**
983              * Sets the cardinality of the property (whether it is optional, required or repeated).
984              *
985              * <p>If this method is not called, the default cardinality is {@link
986              * PropertyConfig#CARDINALITY_OPTIONAL}.
987              */
988             @CanIgnoreReturnValue
989             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
990             @NonNull
setCardinality(@ardinality int cardinality)991             public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
992                 Preconditions.checkArgumentInRange(
993                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
994                 mCardinality = cardinality;
995                 return this;
996             }
997 
998             /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */
999             @NonNull
build()1000             public BooleanPropertyConfig build() {
1001                 Bundle bundle = new Bundle();
1002                 bundle.putString(NAME_FIELD, mPropertyName);
1003                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
1004                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
1005                 return new BooleanPropertyConfig(bundle);
1006             }
1007         }
1008     }
1009 
1010     /** Configuration for a property containing a byte array. */
1011     public static final class BytesPropertyConfig extends PropertyConfig {
BytesPropertyConfig(@onNull Bundle bundle)1012         BytesPropertyConfig(@NonNull Bundle bundle) {
1013             super(bundle);
1014         }
1015 
1016         /** Builder for {@link BytesPropertyConfig}. */
1017         public static final class Builder {
1018             private final String mPropertyName;
1019             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
1020 
1021             /** Creates a new {@link BytesPropertyConfig.Builder}. */
Builder(@onNull String propertyName)1022             public Builder(@NonNull String propertyName) {
1023                 mPropertyName = Objects.requireNonNull(propertyName);
1024             }
1025 
1026             /**
1027              * Sets the cardinality of the property (whether it is optional, required or repeated).
1028              *
1029              * <p>If this method is not called, the default cardinality is {@link
1030              * PropertyConfig#CARDINALITY_OPTIONAL}.
1031              */
1032             @CanIgnoreReturnValue
1033             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
1034             @NonNull
setCardinality(@ardinality int cardinality)1035             public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
1036                 Preconditions.checkArgumentInRange(
1037                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
1038                 mCardinality = cardinality;
1039                 return this;
1040             }
1041 
1042             /** Constructs a new {@link BytesPropertyConfig} from the contents of this builder. */
1043             @NonNull
build()1044             public BytesPropertyConfig build() {
1045                 Bundle bundle = new Bundle();
1046                 bundle.putString(NAME_FIELD, mPropertyName);
1047                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
1048                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
1049                 return new BytesPropertyConfig(bundle);
1050             }
1051         }
1052     }
1053 
1054     /** Configuration for a property containing another Document. */
1055     public static final class DocumentPropertyConfig extends PropertyConfig {
1056         private static final String SCHEMA_TYPE_FIELD = "schemaType";
1057         private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
1058 
DocumentPropertyConfig(@onNull Bundle bundle)1059         DocumentPropertyConfig(@NonNull Bundle bundle) {
1060             super(bundle);
1061         }
1062 
1063         /** Returns the logical schema-type of the contents of this document property. */
1064         @NonNull
getSchemaType()1065         public String getSchemaType() {
1066             return Objects.requireNonNull(mBundle.getString(SCHEMA_TYPE_FIELD));
1067         }
1068 
1069         /**
1070          * Returns whether fields in the nested document should be indexed according to that
1071          * document's schema.
1072          *
1073          * <p>If false, the nested document's properties are not indexed regardless of its own
1074          * schema.
1075          */
shouldIndexNestedProperties()1076         public boolean shouldIndexNestedProperties() {
1077             return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
1078         }
1079 
1080         /** Builder for {@link DocumentPropertyConfig}. */
1081         public static final class Builder {
1082             private final String mPropertyName;
1083             private final String mSchemaType;
1084             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
1085             private boolean mShouldIndexNestedProperties = false;
1086 
1087             /**
1088              * Creates a new {@link DocumentPropertyConfig.Builder}.
1089              *
1090              * @param propertyName The logical name of the property in the schema, which will be
1091              *     used as the key for this property in {@link
1092              *     GenericDocument.Builder#setPropertyDocument}.
1093              * @param schemaType The type of documents which will be stored in this property.
1094              *     Documents of different types cannot be mixed into a single property.
1095              */
Builder(@onNull String propertyName, @NonNull String schemaType)1096             public Builder(@NonNull String propertyName, @NonNull String schemaType) {
1097                 mPropertyName = Objects.requireNonNull(propertyName);
1098                 mSchemaType = Objects.requireNonNull(schemaType);
1099             }
1100 
1101             /**
1102              * Sets the cardinality of the property (whether it is optional, required or repeated).
1103              *
1104              * <p>If this method is not called, the default cardinality is {@link
1105              * PropertyConfig#CARDINALITY_OPTIONAL}.
1106              */
1107             @CanIgnoreReturnValue
1108             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
1109             @NonNull
setCardinality(@ardinality int cardinality)1110             public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
1111                 Preconditions.checkArgumentInRange(
1112                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
1113                 mCardinality = cardinality;
1114                 return this;
1115             }
1116 
1117             /**
1118              * Configures whether fields in the nested document should be indexed according to that
1119              * document's schema.
1120              *
1121              * <p>If false, the nested document's properties are not indexed regardless of its own
1122              * schema.
1123              */
1124             @CanIgnoreReturnValue
1125             @NonNull
setShouldIndexNestedProperties( boolean indexNestedProperties)1126             public DocumentPropertyConfig.Builder setShouldIndexNestedProperties(
1127                     boolean indexNestedProperties) {
1128                 mShouldIndexNestedProperties = indexNestedProperties;
1129                 return this;
1130             }
1131 
1132             /** Constructs a new {@link PropertyConfig} from the contents of this builder. */
1133             @NonNull
build()1134             public DocumentPropertyConfig build() {
1135                 Bundle bundle = new Bundle();
1136                 bundle.putString(NAME_FIELD, mPropertyName);
1137                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
1138                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
1139                 bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);
1140                 bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType);
1141                 return new DocumentPropertyConfig(bundle);
1142             }
1143         }
1144 
1145         /**
1146          * Appends a debug string for the {@link DocumentPropertyConfig} instance to the given
1147          * string builder.
1148          *
1149          * <p>This appends fields specific to a {@link DocumentPropertyConfig} instance.
1150          *
1151          * @param builder the builder to append to.
1152          */
appendDocumentPropertyConfigFields(@onNull IndentingStringBuilder builder)1153         void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
1154             builder.append("shouldIndexNestedProperties: ")
1155                     .append(shouldIndexNestedProperties())
1156                     .append(",\n");
1157 
1158             builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
1159         }
1160     }
1161 }
1162