• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import android.annotation.NonNull;
20 import android.app.appsearch.GenericDocument;
21 import android.app.appsearch.util.IndentingStringBuilder;
22 import android.app.appsearch.util.LogUtil;
23 import android.util.ArrayMap;
24 import android.util.Log;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 import com.android.internal.util.Preconditions;
28 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
29 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
30 
31 import java.lang.reflect.Array;
32 import java.nio.charset.StandardCharsets;
33 import java.security.MessageDigest;
34 import java.security.NoSuchAlgorithmException;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 
41 /**
42  * Helper class to help build the {@link Person}.
43  *
44  * <p>It takes a {@link Person.Builder} with a map to help handle and aggregate {@link
45  * ContactPoint}s, and put them in the {@link Person} during the build.
46  *
47  * <p>This class is not thread safe.
48  *
49  * @hide
50  */
51 public final class PersonBuilderHelper {
52     static final String TAG = "PersonBuilderHelper";
53     static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
54     static final int BASE_SCORE = 1;
55 
56     // We want to store id separately even if we do have it set in the builder, since we
57     // can't get its value out of the builder, which will be used to fetch fingerprints.
58     final private String mId;
59     final private Person.Builder mBuilder;
60     private long mCreationTimestampMillis = -1;
61     private Map<String, ContactPointBuilderHelper> mContactPointBuilderHelpers = new ArrayMap<>();
62 
PersonBuilderHelper(@onNull String id, @NonNull Person.Builder builder)63     public PersonBuilderHelper(@NonNull String id, @NonNull Person.Builder builder) {
64         Objects.requireNonNull(id);
65         Objects.requireNonNull(builder);
66         mId = id;
67         mBuilder = builder;
68     }
69 
70     /**
71      * Helper class to construct a {@link ContactPoint}.
72      *
73      * <p>In this helper, besides a {@link ContactPoint.Builder}, it contains a list of phone number
74      * variants, so we can append those at the end of the final phone number list in {@link
75      * #buildContactPoint()}.
76      */
77     private static class ContactPointBuilderHelper {
78         final ContactPoint.Builder mBuilder;
79         List<String> mPhoneNumberVariants = new ArrayList<>();
80 
ContactPointBuilderHelper(@onNull ContactPoint.Builder builder)81         ContactPointBuilderHelper(@NonNull ContactPoint.Builder builder) {
82             mBuilder = Objects.requireNonNull(builder);
83         }
84 
addPhoneNumberVariant(@onNull String phoneNumberVariant)85         ContactPointBuilderHelper addPhoneNumberVariant(@NonNull String phoneNumberVariant) {
86             mPhoneNumberVariants.add(Objects.requireNonNull(phoneNumberVariant));
87             return this;
88         }
89 
buildContactPoint()90         ContactPoint buildContactPoint() {
91             // Append the phone number variants at the end of phone number list. So the original
92             // phone numbers can appear first in the list.
93             for (int i = 0; i < mPhoneNumberVariants.size(); ++i) {
94                 mBuilder.addPhone(mPhoneNumberVariants.get(i));
95             }
96             return mBuilder.build();
97         }
98     }
99 
100     /**
101      * A {@link Person} is built and returned based on the current properties set in this helper.
102      *
103      * <p>A fingerprint is automatically generated and set.
104      */
105     @NonNull
buildPerson()106     public Person buildPerson() {
107         Preconditions.checkState(mCreationTimestampMillis >= 0,
108                 "creationTimestamp must be explicitly set in the PersonBuilderHelper.");
109 
110         for (ContactPointBuilderHelper builderHelper : mContactPointBuilderHelpers.values()) {
111             // We don't need to reset it for generating fingerprint. But still set it 0 here to
112             // avoid creationTimestamp automatically generated using current time. So our testing
113             // could be easier.
114             builderHelper.mBuilder.setCreationTimestampMillis(0);
115             mBuilder.addContactPoint(builderHelper.buildContactPoint());
116         }
117         // Set the fingerprint and creationTimestamp to 0 to calculate the actual fingerprint.
118         mBuilder.setScore(0);
119         mBuilder.setFingerprint(EMPTY_BYTE_ARRAY);
120         mBuilder.setCreationTimestampMillis(0);
121         // Build a person for generating the fingerprint.
122         Person contactForFingerPrint = mBuilder.build();
123         try {
124             byte[] fingerprint = generateFingerprintMD5(contactForFingerPrint);
125             // This is an "a priori" document score that doesn't take any usage into account.
126             // Hence, the heuristic that's used to assign the document score is to add the
127             // presence or count of all the salient properties of the contact.
128             int score = BASE_SCORE + contactForFingerPrint.getContactPoints().length
129                     + contactForFingerPrint.getAdditionalNames().length;
130             mBuilder.setScore(score);
131             mBuilder.setFingerprint(fingerprint);
132             mBuilder.setCreationTimestampMillis(mCreationTimestampMillis);
133         } catch (NoSuchAlgorithmException e) {
134             // debug logging here to avoid flooding the log.
135             if (LogUtil.DEBUG) {
136                 Log.d(TAG,
137                         "Failed to generate fingerprint for contact "
138                                 + contactForFingerPrint.getId(),
139                         e);
140             }
141         }
142         // Build a final person with fingerprint set.
143         return mBuilder.build();
144     }
145 
146     /** Gets the ID of this {@link Person}. */
147     @NonNull
getId()148     String getId() {
149         return mId;
150     }
151 
152     @NonNull
getPersonBuilder()153     public Person.Builder getPersonBuilder() {
154         return mBuilder;
155     }
156 
157     @NonNull
getOrCreateContactPointBuilderHelper(@onNull String label)158     private ContactPointBuilderHelper getOrCreateContactPointBuilderHelper(@NonNull String label) {
159         ContactPointBuilderHelper builderHelper = mContactPointBuilderHelpers.get(
160                 Objects.requireNonNull(label));
161         if (builderHelper == null) {
162             builderHelper = new ContactPointBuilderHelper(
163                     new ContactPoint.Builder(AppSearchHelper.NAMESPACE_NAME,
164                             /*id=*/"", // doesn't matter for this nested type.
165                             label));
166             mContactPointBuilderHelpers.put(label, builderHelper);
167         }
168 
169         return builderHelper;
170     }
171 
172     @NonNull
setCreationTimestampMillis(long creationTimestampMillis)173     public PersonBuilderHelper setCreationTimestampMillis(long creationTimestampMillis) {
174         mCreationTimestampMillis = creationTimestampMillis;
175         return this;
176     }
177 
178     @NonNull
addAppIdToPerson(@onNull String label, @NonNull String appId)179     public PersonBuilderHelper addAppIdToPerson(@NonNull String label, @NonNull String appId) {
180         getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
181                 .addAppId(Objects.requireNonNull(appId));
182         return this;
183     }
184 
addEmailToPerson(@onNull String label, @NonNull String email)185     public PersonBuilderHelper addEmailToPerson(@NonNull String label, @NonNull String email) {
186         getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
187                 .addEmail(Objects.requireNonNull(email));
188         return this;
189     }
190 
191     @NonNull
addAddressToPerson(@onNull String label, @NonNull String address)192     public PersonBuilderHelper addAddressToPerson(@NonNull String label, @NonNull String address) {
193         getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
194                 .addAddress(Objects.requireNonNull(address));
195         return this;
196     }
197 
198     @NonNull
addPhoneToPerson(@onNull String label, @NonNull String phone)199     public PersonBuilderHelper addPhoneToPerson(@NonNull String label, @NonNull String phone) {
200         getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
201                 .addPhone(Objects.requireNonNull(phone));
202         return this;
203     }
204 
205     @NonNull
addPhoneVariantToPerson(@onNull String label, @NonNull String phoneVariant)206     public PersonBuilderHelper addPhoneVariantToPerson(@NonNull String label,
207             @NonNull String phoneVariant) {
208         getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label))
209                 .addPhoneNumberVariant(Objects.requireNonNull(phoneVariant));
210         return this;
211     }
212 
213     @NonNull
generateFingerprintMD5(@onNull Person person)214     static byte[] generateFingerprintMD5(@NonNull Person person) throws NoSuchAlgorithmException {
215         Objects.requireNonNull(person);
216 
217         MessageDigest md = MessageDigest.getInstance("MD5");
218         md.update(generateFingerprintStringForPerson(person).getBytes(StandardCharsets.UTF_8));
219         return md.digest();
220     }
221 
222     @VisibleForTesting
223     /** Returns a string presentation of {@link Person} for fingerprinting. */
generateFingerprintStringForPerson(@onNull Person person)224     static String generateFingerprintStringForPerson(@NonNull Person person) {
225         Objects.requireNonNull(person);
226 
227         StringBuilder builder = new StringBuilder();
228         appendGenericDocumentString(person, builder);
229         return builder.toString();
230     }
231 
232     /**
233      * Appends string representation of a {@link GenericDocument} to the {@link StringBuilder}.
234      *
235      * <p>This is basically same as
236      * {@link GenericDocument#appendGenericDocumentString(IndentingStringBuilder)}, but only keep
237      * the properties part and use a normal {@link StringBuilder} to skip the indentation.
238      */
appendGenericDocumentString(@onNull GenericDocument doc, @NonNull StringBuilder builder)239     private static void appendGenericDocumentString(@NonNull GenericDocument doc,
240             @NonNull StringBuilder builder) {
241         Objects.requireNonNull(doc);
242         Objects.requireNonNull(builder);
243 
244         builder.append("properties: {\n");
245         String[] sortedProperties = doc.getPropertyNames().toArray(new String[0]);
246         Arrays.sort(sortedProperties);
247         for (int i = 0; i < sortedProperties.length; i++) {
248             Object property = Objects.requireNonNull(doc.getProperty(sortedProperties[i]));
249             appendPropertyString(sortedProperties[i], property, builder);
250             if (i != sortedProperties.length - 1) {
251                 builder.append(",\n");
252             }
253         }
254         builder.append("\n");
255         builder.append("}");
256     }
257 
258     /**
259      * Appends string representation of a {@link GenericDocument}'s property to the
260      * {@link StringBuilder}.
261      *
262      * <p>This is basically same as
263      * {@link GenericDocument#appendPropertyString(String, Object, IndentingStringBuilder)}, but
264      * use a normal {@link StringBuilder} to skip the indentation.
265      *
266      * <p>Here we still keep most of the formatting(e.g. '\n') to make sure we won't hit some
267      * possible corner cases. E.g. We will have "someProperty1: some\n Property2:..." instead of
268      * "someProperty1: someProperty2:". For latter, we can interpret it as empty string value for
269      * "someProperty1", with a different property name "someProperty2". In this case, the content is
270      * changed but fingerprint will remain same if we don't have that '\n'.
271      *
272      * <p>Plus, some basic formatting will make the testing more clear.
273      */
appendPropertyString( @onNull String propertyName, @NonNull Object property, @NonNull StringBuilder builder)274     private static void appendPropertyString(
275             @NonNull String propertyName,
276             @NonNull Object property,
277             @NonNull StringBuilder builder) {
278         Objects.requireNonNull(propertyName);
279         Objects.requireNonNull(property);
280         Objects.requireNonNull(builder);
281 
282         builder.append("\"").append(propertyName).append("\": [");
283         if (property instanceof GenericDocument[]) {
284             GenericDocument[] documentValues = (GenericDocument[]) property;
285             for (int i = 0; i < documentValues.length; ++i) {
286                 builder.append("\n");
287                 appendGenericDocumentString(documentValues[i], builder);
288                 if (i != documentValues.length - 1) {
289                     builder.append(",");
290                 }
291                 builder.append("\n");
292             }
293             builder.append("]");
294         } else {
295             int propertyArrLength = Array.getLength(property);
296             for (int i = 0; i < propertyArrLength; i++) {
297                 Object propertyElement = Array.get(property, i);
298                 if (propertyElement instanceof String) {
299                     builder.append("\"").append((String) propertyElement).append("\"");
300                 } else if (propertyElement instanceof byte[]) {
301                     builder.append(Arrays.toString((byte[]) propertyElement));
302                 } else {
303                     builder.append(propertyElement.toString());
304                 }
305                 if (i != propertyArrLength - 1) {
306                     builder.append(", ");
307                 } else {
308                     builder.append("]");
309                 }
310             }
311         }
312     }
313 }
314