1 /*
2  * Copyright (C) 2022 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.contactsindexer.appsearchtypes;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.app.appsearch.AppSearchSchema;
23 import android.app.appsearch.GenericDocument;
24 import android.net.Uri;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 import com.android.internal.util.Preconditions;
28 import com.android.server.appsearch.contactsindexer.ContactsIndexerConfig;
29 
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Objects;
35 
36 /**
37  * Represents a Person in AppSearch.
38  *
39  * @hide
40  */
41 public class Person extends GenericDocument {
42     public static final String SCHEMA_TYPE = "builtin:Person";
43 
44     /**
45      * The type of the name stored in additionalNames list. We have two parallel lists to store
46      * different names, like nicknames and phonetic names as searchable field in additionalNames.
47      *
48      * <p>Having this type for each name stored in additionalNames, so clients can distinguish the
49      * type of those names in the search result.
50      *
51      * @hide
52      */
53     @IntDef(
54             value = {
55                 TYPE_UNKNOWN,
56                 TYPE_NICKNAME,
57                 TYPE_PHONETIC_NAME,
58             })
59     @Retention(RetentionPolicy.SOURCE)
60     public @interface NameType {}
61 
62     public static final int TYPE_UNKNOWN = 0;
63     public static final int TYPE_NICKNAME = 1;
64     public static final int TYPE_PHONETIC_NAME = 2;
65 
66     // Properties
67     public static final String PERSON_PROPERTY_NAME = "name";
68     public static final String PERSON_PROPERTY_GIVEN_NAME = "givenName";
69     public static final String PERSON_PROPERTY_MIDDLE_NAME = "middleName";
70     public static final String PERSON_PROPERTY_FAMILY_NAME = "familyName";
71     public static final String PERSON_PROPERTY_EXTERNAL_URI = "externalUri";
72     public static final String PERSON_PROPERTY_ADDITIONAL_NAME_TYPES = "additionalNameTypes";
73     public static final String PERSON_PROPERTY_ADDITIONAL_NAMES = "additionalNames";
74     public static final String PERSON_PROPERTY_IS_IMPORTANT = "isImportant";
75     public static final String PERSON_PROPERTY_IS_BOT = "isBot";
76     public static final String PERSON_PROPERTY_IMAGE_URI = "imageUri";
77     public static final String PERSON_PROPERTY_CONTACT_POINTS = "contactPoints";
78     public static final String PERSON_PROPERTY_AFFILIATIONS = "affiliations";
79     public static final String PERSON_PROPERTY_RELATIONS = "relations";
80     public static final String PERSON_PROPERTY_NOTES = "notes";
81     public static final String PERSON_PROPERTY_FINGERPRINT = "fingerprint";
82 
createSchema(boolean indexFirstMiddleAndLastNames)83     private static AppSearchSchema createSchema(boolean indexFirstMiddleAndLastNames) {
84         AppSearchSchema.Builder builder =
85                 new AppSearchSchema.Builder(SCHEMA_TYPE)
86                         // full display name
87                         .addProperty(
88                                 new AppSearchSchema.StringPropertyConfig.Builder(
89                                                 PERSON_PROPERTY_NAME)
90                                         .setCardinality(
91                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
92                                         .setIndexingType(
93                                                 AppSearchSchema.StringPropertyConfig
94                                                         .INDEXING_TYPE_PREFIXES)
95                                         .setTokenizerType(
96                                                 AppSearchSchema.StringPropertyConfig
97                                                         .TOKENIZER_TYPE_PLAIN)
98                                         .build());
99 
100         if (indexFirstMiddleAndLastNames) {
101             builder
102                     // given name from CP2
103                     .addProperty(
104                             new AppSearchSchema.StringPropertyConfig.Builder(
105                                             PERSON_PROPERTY_GIVEN_NAME)
106                                     .setCardinality(
107                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
108                                     .setIndexingType(
109                                             AppSearchSchema.StringPropertyConfig
110                                                     .INDEXING_TYPE_PREFIXES)
111                                     .setTokenizerType(
112                                             AppSearchSchema.StringPropertyConfig
113                                                     .TOKENIZER_TYPE_PLAIN)
114                                     .build())
115                     // middle name from CP2
116                     .addProperty(
117                             new AppSearchSchema.StringPropertyConfig.Builder(
118                                             PERSON_PROPERTY_MIDDLE_NAME)
119                                     .setCardinality(
120                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
121                                     .setIndexingType(
122                                             AppSearchSchema.StringPropertyConfig
123                                                     .INDEXING_TYPE_PREFIXES)
124                                     .setTokenizerType(
125                                             AppSearchSchema.StringPropertyConfig
126                                                     .TOKENIZER_TYPE_PLAIN)
127                                     .build())
128                     // family name from CP2
129                     .addProperty(
130                             new AppSearchSchema.StringPropertyConfig.Builder(
131                                             PERSON_PROPERTY_FAMILY_NAME)
132                                     .setCardinality(
133                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
134                                     .setIndexingType(
135                                             AppSearchSchema.StringPropertyConfig
136                                                     .INDEXING_TYPE_PREFIXES)
137                                     .setTokenizerType(
138                                             AppSearchSchema.StringPropertyConfig
139                                                     .TOKENIZER_TYPE_PLAIN)
140                                     .build());
141         } else {
142             builder
143                     // given name from CP2
144                     .addProperty(
145                             new AppSearchSchema.StringPropertyConfig.Builder(
146                                             PERSON_PROPERTY_GIVEN_NAME)
147                                     .setCardinality(
148                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
149                                     .build())
150                     // middle name from CP2
151                     .addProperty(
152                             new AppSearchSchema.StringPropertyConfig.Builder(
153                                             PERSON_PROPERTY_MIDDLE_NAME)
154                                     .setCardinality(
155                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
156                                     .build())
157                     // family name from CP2
158                     .addProperty(
159                             new AppSearchSchema.StringPropertyConfig.Builder(
160                                             PERSON_PROPERTY_FAMILY_NAME)
161                                     .setCardinality(
162                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
163                                     .build());
164         }
165 
166         builder
167                 // lookup uri from CP2
168                 .addProperty(
169                         new AppSearchSchema.StringPropertyConfig.Builder(
170                                         PERSON_PROPERTY_EXTERNAL_URI)
171                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
172                                 .build())
173                 // corresponding name types for the names stored in additional names below.
174                 .addProperty(
175                         new AppSearchSchema.LongPropertyConfig.Builder(
176                                         PERSON_PROPERTY_ADDITIONAL_NAME_TYPES)
177                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
178                                 .build())
179                 // additional names e.g. nick names and phonetic names.
180                 .addProperty(
181                         new AppSearchSchema.StringPropertyConfig.Builder(
182                                         PERSON_PROPERTY_ADDITIONAL_NAMES)
183                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
184                                 .setIndexingType(
185                                         AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
186                                 .setTokenizerType(
187                                         AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
188                                 .build())
189                 // isImportant. It could be used to store isStarred from CP2.
190                 .addProperty(
191                         new AppSearchSchema.BooleanPropertyConfig.Builder(
192                                         PERSON_PROPERTY_IS_IMPORTANT)
193                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
194                                 .build())
195                 // isBot
196                 .addProperty(
197                         new AppSearchSchema.BooleanPropertyConfig.Builder(PERSON_PROPERTY_IS_BOT)
198                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
199                                 .build())
200                 // imageUri
201                 .addProperty(
202                         new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_IMAGE_URI)
203                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
204                                 .build())
205                 // ContactPoint
206                 .addProperty(
207                         new AppSearchSchema.DocumentPropertyConfig.Builder(
208                                         PERSON_PROPERTY_CONTACT_POINTS,
209                                         ContactPoint.SCHEMA.getSchemaType())
210                                 .setShouldIndexNestedProperties(true)
211                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
212                                 .build())
213                 // Affiliations
214                 .addProperty(
215                         new AppSearchSchema.StringPropertyConfig.Builder(
216                                         PERSON_PROPERTY_AFFILIATIONS)
217                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
218                                 .setIndexingType(
219                                         AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
220                                 .setTokenizerType(
221                                         AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
222                                 .build())
223                 // Relations
224                 .addProperty(
225                         new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_RELATIONS)
226                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
227                                 .build())
228                 // Notes
229                 .addProperty(
230                         new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_NOTES)
231                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
232                                 .setIndexingType(
233                                         AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
234                                 .setTokenizerType(
235                                         AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
236                                 .build())
237                 //
238                 // Following fields are internal to ContactsIndexer.
239                 //
240                 // Fingerprint for detecting significant changes
241                 .addProperty(
242                         new AppSearchSchema.StringPropertyConfig.Builder(
243                                         PERSON_PROPERTY_FINGERPRINT)
244                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
245                                 .build());
246         return builder.build();
247     }
248 
249     /***
250      * Returns Person schema based on current value of flag
251      * 'contacts_index_first_middle_and_last_names'. If the flag value changes after the initial
252      * schema fetch, the schema returned will be different than the original schema that was set
253      * for the Person corpus.
254      */
getSchema(ContactsIndexerConfig config)255     public static AppSearchSchema getSchema(ContactsIndexerConfig config) {
256         return createSchema(config.shouldIndexFirstMiddleAndLastNames());
257     }
258 
259     /** Constructs a {@link Person}. */
260     @VisibleForTesting
Person(@onNull GenericDocument document)261     public Person(@NonNull GenericDocument document) {
262         super(document);
263     }
264 
265     @NonNull
getName()266     public String getName() {
267         return getPropertyString(PERSON_PROPERTY_NAME);
268     }
269 
270     @Nullable
getGivenName()271     public String getGivenName() {
272         return getPropertyString(PERSON_PROPERTY_GIVEN_NAME);
273     }
274 
275     @Nullable
getMiddleName()276     public String getMiddleName() {
277         return getPropertyString(PERSON_PROPERTY_MIDDLE_NAME);
278     }
279 
280     @Nullable
getFamilyName()281     public String getFamilyName() {
282         return getPropertyString(PERSON_PROPERTY_FAMILY_NAME);
283     }
284 
285     @Nullable
getExternalUri()286     public Uri getExternalUri() {
287         String uriStr = getPropertyString(PERSON_PROPERTY_EXTERNAL_URI);
288         if (uriStr == null) {
289             return null;
290         }
291         return Uri.parse(uriStr);
292     }
293 
294     @Nullable
getImageUri()295     public Uri getImageUri() {
296         String uriStr = getPropertyString(PERSON_PROPERTY_IMAGE_URI);
297         if (uriStr == null) {
298             return null;
299         }
300         return Uri.parse(uriStr);
301     }
302 
isImportant()303     public boolean isImportant() {
304         return getPropertyBoolean(PERSON_PROPERTY_IS_IMPORTANT);
305     }
306 
isBot()307     public boolean isBot() {
308         return getPropertyBoolean(PERSON_PROPERTY_IS_BOT);
309     }
310 
311     @NonNull
312     @NameType
getAdditionalNameTypes()313     public long[] getAdditionalNameTypes() {
314         return getPropertyLongArray(PERSON_PROPERTY_ADDITIONAL_NAME_TYPES);
315     }
316 
317     @NonNull
getAdditionalNames()318     public String[] getAdditionalNames() {
319         return getPropertyStringArray(PERSON_PROPERTY_ADDITIONAL_NAMES);
320     }
321 
322     @NonNull
getAffiliations()323     public String[] getAffiliations() {
324         return getPropertyStringArray(PERSON_PROPERTY_AFFILIATIONS);
325     }
326 
327     @NonNull
getRelations()328     public String[] getRelations() {
329         return getPropertyStringArray(PERSON_PROPERTY_RELATIONS);
330     }
331 
332     @Nullable
getNotes()333     public String[] getNotes() {
334         return getPropertyStringArray(PERSON_PROPERTY_NOTES);
335     }
336 
337     // This method is expensive, and is intended to be used in tests only.
338     @NonNull
getContactPoints()339     public ContactPoint[] getContactPoints() {
340         GenericDocument[] docs = getPropertyDocumentArray(PERSON_PROPERTY_CONTACT_POINTS);
341         ContactPoint[] contactPoints = new ContactPoint[docs.length];
342         for (int i = 0; i < contactPoints.length; ++i) {
343             contactPoints[i] = new ContactPoint(docs[i]);
344         }
345         return contactPoints;
346     }
347 
348     /** Gets a byte array for the fingerprint. */
349     @NonNull
getFingerprint()350     public byte[] getFingerprint() {
351         return getPropertyBytes(PERSON_PROPERTY_FINGERPRINT);
352     }
353 
354     /** Builder for {@link Person}. */
355     public static final class Builder extends GenericDocument.Builder<Builder> {
356         @NameType private final List<Long> mAdditionalNameTypes = new ArrayList<>();
357         private final List<String> mAdditionalNames = new ArrayList<>();
358         private final List<String> mAffiliations = new ArrayList<>();
359         private final List<String> mRelations = new ArrayList<>();
360         private final List<String> mNotes = new ArrayList<>();
361         private final List<ContactPoint> mContactPoints = new ArrayList<>();
362 
363         /**
364          * Creates a new {@link ContactPoint.Builder}
365          *
366          * @param namespace The namespace of the Email.
367          * @param id The ID of the Email.
368          * @param name The name of the {@link Person}.
369          */
Builder(@onNull String namespace, @NonNull String id, @NonNull String name)370         public Builder(@NonNull String namespace, @NonNull String id, @NonNull String name) {
371             super(namespace, id, SCHEMA_TYPE);
372             setName(name);
373         }
374 
375         /** Sets the full display name. */
376         @NonNull
setName(@onNull String name)377         private Builder setName(@NonNull String name) {
378             setPropertyString(PERSON_PROPERTY_NAME, name);
379             return this;
380         }
381 
382         @NonNull
setGivenName(@onNull String givenName)383         public Builder setGivenName(@NonNull String givenName) {
384             setPropertyString(PERSON_PROPERTY_GIVEN_NAME, givenName);
385             return this;
386         }
387 
388         @NonNull
setMiddleName(@onNull String middleName)389         public Builder setMiddleName(@NonNull String middleName) {
390             setPropertyString(PERSON_PROPERTY_MIDDLE_NAME, middleName);
391             return this;
392         }
393 
394         @NonNull
setFamilyName(@onNull String familyName)395         public Builder setFamilyName(@NonNull String familyName) {
396             setPropertyString(PERSON_PROPERTY_FAMILY_NAME, familyName);
397             return this;
398         }
399 
400         @NonNull
setExternalUri(@onNull Uri externalUri)401         public Builder setExternalUri(@NonNull Uri externalUri) {
402             setPropertyString(
403                     PERSON_PROPERTY_EXTERNAL_URI, Objects.requireNonNull(externalUri).toString());
404             return this;
405         }
406 
407         @NonNull
setImageUri(@onNull Uri imageUri)408         public Builder setImageUri(@NonNull Uri imageUri) {
409             setPropertyString(
410                     PERSON_PROPERTY_IMAGE_URI, Objects.requireNonNull(imageUri).toString());
411             return this;
412         }
413 
414         @NonNull
setIsImportant(boolean isImportant)415         public Builder setIsImportant(boolean isImportant) {
416             setPropertyBoolean(PERSON_PROPERTY_IS_IMPORTANT, isImportant);
417             return this;
418         }
419 
420         @NonNull
setIsBot(boolean isBot)421         public Builder setIsBot(boolean isBot) {
422             setPropertyBoolean(PERSON_PROPERTY_IS_BOT, isBot);
423             return this;
424         }
425 
426         @NonNull
addAdditionalName(@ameType long nameType, @NonNull String name)427         public Builder addAdditionalName(@NameType long nameType, @NonNull String name) {
428             mAdditionalNameTypes.add(nameType);
429             mAdditionalNames.add(Objects.requireNonNull(name));
430             return this;
431         }
432 
433         /**
434          * Adds an affiliation for the {@link Person}, like a company name as an employee, or a
435          * university name as a student.
436          */
437         @NonNull
addAffiliation(@onNull String affiliation)438         public Builder addAffiliation(@NonNull String affiliation) {
439             mAffiliations.add(Objects.requireNonNull(affiliation));
440             return this;
441         }
442 
443         /** Adds a relation to this {@link Person}. Like "spouse", "father", etc. */
444         @NonNull
addRelation(@onNull String relation)445         public Builder addRelation(@NonNull String relation) {
446             mRelations.add(Objects.requireNonNull(relation));
447             return this;
448         }
449 
450         /** Adds a note about this {@link Person}. */
451         @NonNull
addNote(@onNull String note)452         public Builder addNote(@NonNull String note) {
453             mNotes.add(Objects.requireNonNull(note));
454             return this;
455         }
456 
457         @NonNull
addContactPoint(@onNull ContactPoint contactPoint)458         public Builder addContactPoint(@NonNull ContactPoint contactPoint) {
459             Objects.requireNonNull(contactPoint);
460             mContactPoints.add(contactPoint);
461             return this;
462         }
463 
464         /**
465          * Sets the fingerprint for this {@link Person}
466          *
467          * @param fingerprint byte array for the fingerprint. The size depends on the algorithm
468          *     being used. Right now we are using md5 and generating a 16-byte fingerprint.
469          */
470         @NonNull
setFingerprint(@onNull byte[] fingerprint)471         public Builder setFingerprint(@NonNull byte[] fingerprint) {
472             setPropertyBytes(PERSON_PROPERTY_FINGERPRINT, Objects.requireNonNull(fingerprint));
473             return this;
474         }
475 
476         @NonNull
build()477         public Person build() {
478             Preconditions.checkState(mAdditionalNameTypes.size() == mAdditionalNames.size());
479             long[] primitiveNameTypes = new long[mAdditionalNameTypes.size()];
480             for (int i = 0; i < mAdditionalNameTypes.size(); i++) {
481                 primitiveNameTypes[i] = mAdditionalNameTypes.get(i).longValue();
482             }
483             setPropertyLong(PERSON_PROPERTY_ADDITIONAL_NAME_TYPES, primitiveNameTypes);
484             setPropertyString(
485                     PERSON_PROPERTY_ADDITIONAL_NAMES, mAdditionalNames.toArray(new String[0]));
486             setPropertyString(PERSON_PROPERTY_AFFILIATIONS, mAffiliations.toArray(new String[0]));
487             setPropertyString(PERSON_PROPERTY_RELATIONS, mRelations.toArray(new String[0]));
488             setPropertyString(PERSON_PROPERTY_NOTES, mNotes.toArray(new String[0]));
489             setPropertyDocument(
490                     PERSON_PROPERTY_CONTACT_POINTS, mContactPoints.toArray(new ContactPoint[0]));
491             // TODO(b/203605504) calculate score here.
492             return new Person(super.build());
493         }
494     }
495 }
496