1 /*
2  * Copyright (C) 2021 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.annotation.Nullable;
21 import android.content.res.Resources;
22 import android.database.Cursor;
23 import android.provider.ContactsContract;
24 import android.provider.ContactsContract.CommonDataKinds.Email;
25 import android.provider.ContactsContract.CommonDataKinds.Nickname;
26 import android.provider.ContactsContract.CommonDataKinds.Note;
27 import android.provider.ContactsContract.CommonDataKinds.Organization;
28 import android.provider.ContactsContract.CommonDataKinds.Phone;
29 import android.provider.ContactsContract.CommonDataKinds.Relation;
30 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
31 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
32 import android.provider.ContactsContract.Data;
33 import android.text.TextUtils;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 
37 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
38 
39 import java.util.Collection;
40 import java.util.Collections;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Set;
44 
45 /**
46  * Helper Class to handle data for different MIME types from CP2, and build {@link Person} from
47  * them.
48  *
49  * <p>This class is not thread safe.
50  *
51  * @hide
52  */
53 public final class ContactDataHandler {
54     private final Map<String, DataHandler> mHandlers;
55     private final Set<String> mNeededColumns;
56 
57     /** Constructor. */
ContactDataHandler(Resources resources)58     public ContactDataHandler(Resources resources) {
59         // Create handlers for different MIME types
60         mHandlers = new ArrayMap<>();
61         mHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataHandler(resources));
62         mHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataHandler());
63         mHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneHandler(resources));
64         mHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, new StructuredPostalHandler(resources));
65         mHandlers.put(StructuredName.CONTENT_ITEM_TYPE, new StructuredNameHandler());
66         mHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataHandler());
67         mHandlers.put(Relation.CONTENT_ITEM_TYPE, new RelationDataHandler(resources));
68         mHandlers.put(Note.CONTENT_ITEM_TYPE, new NoteDataHandler());
69 
70         // Retrieve all the needed columns from different data handlers.
71         Set<String> neededColumns = new ArraySet<>();
72         neededColumns.add(ContactsContract.Data.MIMETYPE);
73         for (DataHandler handler : mHandlers.values()) {
74             handler.addNeededColumns(neededColumns);
75         }
76         // We need to make sure this is unmodifiable since the reference is returned in
77         // getNeededColumns().
78         mNeededColumns = Collections.unmodifiableSet(neededColumns);
79     }
80 
81     /** Returns an unmodifiable set of columns this {@link ContactDataHandler} is asking for. */
getNeededColumns()82     public Set<String> getNeededColumns() {
83         return mNeededColumns;
84     }
85 
86     /**
87      * Adds the information of the current row from {@link ContactsContract.Data} table into the
88      * {@link PersonBuilderHelper}.
89      *
90      * <p>By reading each row in the table, we will get the detailed information about a
91      * Person(contact).
92      *
93      * @param builderHelper a helper to build the {@link Person}.
94      */
convertCursorToPerson( @onNull Cursor cursor, @NonNull PersonBuilderHelper builderHelper)95     public void convertCursorToPerson(
96             @NonNull Cursor cursor, @NonNull PersonBuilderHelper builderHelper) {
97         Objects.requireNonNull(cursor);
98         Objects.requireNonNull(builderHelper);
99 
100         int mimetypeIndex = cursor.getColumnIndex(Data.MIMETYPE);
101         String mimeType = cursor.getString(mimetypeIndex);
102         DataHandler handler = mHandlers.get(mimeType);
103         if (handler != null) {
104             handler.addData(builderHelper, cursor);
105         }
106     }
107 
108     abstract static class DataHandler {
109         /** Gets the column as a string. */
110         @Nullable
getColumnString(@onNull Cursor cursor, @NonNull String column)111         protected final String getColumnString(@NonNull Cursor cursor, @NonNull String column) {
112             Objects.requireNonNull(cursor);
113             Objects.requireNonNull(column);
114 
115             int columnIndex = cursor.getColumnIndex(column);
116             if (columnIndex == -1) {
117                 return null;
118             }
119             return cursor.getString(columnIndex);
120         }
121 
122         /** Gets the column as an int. */
getColumnInt(@onNull Cursor cursor, @NonNull String column)123         protected final int getColumnInt(@NonNull Cursor cursor, @NonNull String column) {
124             Objects.requireNonNull(cursor);
125             Objects.requireNonNull(column);
126 
127             int columnIndex = cursor.getColumnIndex(column);
128             if (columnIndex == -1) {
129                 return 0;
130             }
131             return cursor.getInt(columnIndex);
132         }
133 
134         /** Adds the columns needed for the {@code DataHandler}. */
addNeededColumns(Collection<String> columns)135         public abstract void addNeededColumns(Collection<String> columns);
136 
137         /** Adds the data into {@link PersonBuilderHelper}. */
addData(@onNull PersonBuilderHelper builderHelper, Cursor cursor)138         public abstract void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor);
139     }
140 
141     private abstract static class SingleColumnDataHandler extends DataHandler {
142         private final String mColumn;
143 
SingleColumnDataHandler(@onNull String column)144         protected SingleColumnDataHandler(@NonNull String column) {
145             Objects.requireNonNull(column);
146             mColumn = column;
147         }
148 
149         /** Adds the columns needed for the {@code DataHandler}. */
150         @Override
addNeededColumns(@onNull Collection<String> columns)151         public final void addNeededColumns(@NonNull Collection<String> columns) {
152             Objects.requireNonNull(columns);
153             columns.add(mColumn);
154         }
155 
156         /** Adds the data into {@link PersonBuilderHelper}. */
157         @Override
addData( @onNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor)158         public final void addData(
159                 @NonNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor) {
160             Objects.requireNonNull(builderHelper);
161             Objects.requireNonNull(cursor);
162 
163             String data = getColumnString(cursor, mColumn);
164             if (!TextUtils.isEmpty(data)) {
165                 addSingleColumnStringData(builderHelper, data);
166             }
167         }
168 
addSingleColumnStringData( PersonBuilderHelper builderHelper, String data)169         protected abstract void addSingleColumnStringData(
170                 PersonBuilderHelper builderHelper, String data);
171     }
172 
173     private abstract static class ContactPointDataHandler extends DataHandler {
174         private final Resources mResources;
175         private final String[] mDataColumns;
176         private final String mTypeColumn;
177         private final String mLabelColumn;
178 
ContactPointDataHandler( @onNull Resources resources, @NonNull String[] dataColumns, @NonNull String typeColumn, @NonNull String labelColumn)179         public ContactPointDataHandler(
180                 @NonNull Resources resources,
181                 @NonNull String[] dataColumns,
182                 @NonNull String typeColumn,
183                 @NonNull String labelColumn) {
184             mResources = Objects.requireNonNull(resources);
185             mDataColumns = Objects.requireNonNull(dataColumns);
186             mTypeColumn = Objects.requireNonNull(typeColumn);
187             mLabelColumn = Objects.requireNonNull(labelColumn);
188         }
189 
190         /** Adds the columns needed for the {@code DataHandler}. */
191         @Override
addNeededColumns(@onNull Collection<String> columns)192         public final void addNeededColumns(@NonNull Collection<String> columns) {
193             Objects.requireNonNull(columns);
194             columns.add(Data._ID);
195             columns.add(Data.IS_PRIMARY);
196             columns.add(Data.IS_SUPER_PRIMARY);
197             for (int i = 0; i < mDataColumns.length; ++i) {
198                 columns.add(mDataColumns[i]);
199             }
200             columns.add(mTypeColumn);
201             columns.add(mLabelColumn);
202         }
203 
204         /**
205          * Adds the data for ContactsPoint(email, telephone, postal addresses) into {@link
206          * Person.Builder}.
207          */
208         @Override
addData( @onNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor)209         public final void addData(
210                 @NonNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor) {
211             Objects.requireNonNull(builderHelper);
212             Objects.requireNonNull(cursor);
213 
214             Map<String, String> data = new ArrayMap<>(mDataColumns.length);
215             for (int i = 0; i < mDataColumns.length; ++i) {
216                 String col = getColumnString(cursor, mDataColumns[i]);
217                 if (!TextUtils.isEmpty(col)) {
218                     data.put(mDataColumns[i], col);
219                 }
220             }
221 
222             if (!data.isEmpty()) {
223                 // get the corresponding label to the type.
224                 int type = getColumnInt(cursor, mTypeColumn);
225                 String label =
226                         getTypeLabel(mResources, type, getColumnString(cursor, mLabelColumn));
227                 addContactPointData(builderHelper, label, data);
228             }
229         }
230 
231         @NonNull
getTypeLabel(Resources resources, int type, String label)232         protected abstract String getTypeLabel(Resources resources, int type, String label);
233 
234         /**
235          * Adds the information in the {@link Person.Builder}.
236          *
237          * @param builderHelper a helper to build the {@link Person}.
238          * @param label the corresponding label to the {@code type} for the data.
239          * @param data data read from the designed columns in the row.
240          */
addContactPointData( PersonBuilderHelper builderHelper, String label, Map<String, String> data)241         protected abstract void addContactPointData(
242                 PersonBuilderHelper builderHelper, String label, Map<String, String> data);
243     }
244 
245     private static final class EmailDataHandler extends ContactPointDataHandler {
246         private static final String[] COLUMNS = {
247             Email.ADDRESS,
248         };
249 
EmailDataHandler(@onNull Resources resources)250         public EmailDataHandler(@NonNull Resources resources) {
251             super(resources, COLUMNS, Email.TYPE, Email.LABEL);
252         }
253 
254         /**
255          * Adds the Email information in the {@link Person.Builder}.
256          *
257          * @param builderHelper a builder to build the {@link Person}.
258          * @param label The corresponding label to the {@code type}. E.g. {@link
259          *     com.android.internal.R.string#emailTypeHome} to {@link Email#TYPE_HOME} or custom
260          *     label for the data if {@code type} is {@link Email#TYPE_CUSTOM}.
261          * @param data data read from the designed column {@code Email.ADDRESS} in the row.
262          */
263         @Override
addContactPointData( @onNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map<String, String> data)264         protected void addContactPointData(
265                 @NonNull PersonBuilderHelper builderHelper,
266                 @NonNull String label,
267                 @NonNull Map<String, String> data) {
268             Objects.requireNonNull(builderHelper);
269             Objects.requireNonNull(data);
270             Objects.requireNonNull(label);
271             String email = data.get(Email.ADDRESS);
272             if (!TextUtils.isEmpty(email)) {
273                 builderHelper.addEmailToPerson(label, email);
274             }
275         }
276 
277         @NonNull
278         @Override
getTypeLabel( @onNull Resources resources, int type, @Nullable String label)279         protected String getTypeLabel(
280                 @NonNull Resources resources, int type, @Nullable String label) {
281             Objects.requireNonNull(resources);
282             return Email.getTypeLabel(resources, type, label).toString();
283         }
284     }
285 
286     private static final class PhoneHandler extends ContactPointDataHandler {
287         private static final String[] COLUMNS = {
288             Phone.NUMBER, Phone.NORMALIZED_NUMBER,
289         };
290 
291         private final Resources mResources;
292 
PhoneHandler(@onNull Resources resources)293         public PhoneHandler(@NonNull Resources resources) {
294             super(resources, COLUMNS, Phone.TYPE, Phone.LABEL);
295             mResources = Objects.requireNonNull(resources);
296         }
297 
298         /**
299          * Adds the phone number information in the {@link Person.Builder}.
300          *
301          * @param builderHelper helper to build the {@link Person}.
302          * @param label corresponding label to {@code type}. E.g. {@link
303          *     com.android.internal.R.string#phoneTypeHome} to {@link Phone#TYPE_HOME}, or custom
304          *     label for the data if {@code type} is {@link Phone#TYPE_CUSTOM}.
305          * @param data data read from the designed columns {@link Phone#NUMBER} in the row.
306          */
307         @Override
addContactPointData( @onNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map<String, String> data)308         protected void addContactPointData(
309                 @NonNull PersonBuilderHelper builderHelper,
310                 @NonNull String label,
311                 @NonNull Map<String, String> data) {
312             Objects.requireNonNull(builderHelper);
313             Objects.requireNonNull(data);
314             Objects.requireNonNull(label);
315 
316             // Add original phone number directly to the final phone number
317             // list. E.g. (202) 555-0111
318             String phoneNumberOriginal = data.get(Phone.NUMBER);
319             if (TextUtils.isEmpty(phoneNumberOriginal)) {
320                 return;
321             }
322             builderHelper.addPhoneToPerson(label, phoneNumberOriginal);
323 
324             // Try to get phone number in e164 from CP2.
325             String phoneNumberE164FromCP2 = data.get(Phone.NORMALIZED_NUMBER);
326 
327             // Try to include different variants based on the national (e.g. (202) 555-0111), and
328             // the e164 format of the original number. The variants are generated with the best
329             // efforts, depending on the locales available in the current configuration on the
330             // system.
331             Set<String> phoneNumberVariants =
332                     ContactsIndexerPhoneNumberUtils.createPhoneNumberVariants(
333                             mResources, phoneNumberOriginal, phoneNumberE164FromCP2);
334 
335             phoneNumberVariants.remove(phoneNumberOriginal);
336             for (String variant : phoneNumberVariants) {
337                 // Append phone variants to a different list, which will be appended into
338                 // the final one during buildPerson.
339                 builderHelper.addPhoneVariantToPerson(label, variant);
340             }
341         }
342 
343         @NonNull
344         @Override
getTypeLabel( @onNull Resources resources, int type, @Nullable String label)345         protected String getTypeLabel(
346                 @NonNull Resources resources, int type, @Nullable String label) {
347             Objects.requireNonNull(resources);
348             return Phone.getTypeLabel(resources, type, label).toString();
349         }
350     }
351 
352     private static final class StructuredPostalHandler extends ContactPointDataHandler {
353         private static final String[] COLUMNS = {
354             StructuredPostal.FORMATTED_ADDRESS,
355         };
356 
StructuredPostalHandler(@onNull Resources resources)357         public StructuredPostalHandler(@NonNull Resources resources) {
358             super(resources, COLUMNS, StructuredPostal.TYPE, StructuredPostal.LABEL);
359         }
360 
361         /**
362          * Adds the postal address information in the {@link Person.Builder}.
363          *
364          * @param builderHelper helper to build the {@link Person}.
365          * @param label corresponding label to {@code type}. E.g. {@link
366          *     com.android.internal.R.string#postalTypeHome} to {@link StructuredPostal#TYPE_HOME},
367          *     or custom label for the data if {@code type} is {@link StructuredPostal#TYPE_CUSTOM}.
368          * @param data data read from the designed column {@link StructuredPostal#FORMATTED_ADDRESS}
369          *     in the row.
370          */
371         @Override
addContactPointData( @onNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map<String, String> data)372         protected void addContactPointData(
373                 @NonNull PersonBuilderHelper builderHelper,
374                 @NonNull String label,
375                 @NonNull Map<String, String> data) {
376             Objects.requireNonNull(builderHelper);
377             Objects.requireNonNull(data);
378             Objects.requireNonNull(label);
379             String address = data.get(StructuredPostal.FORMATTED_ADDRESS);
380             if (!TextUtils.isEmpty(address)) {
381                 builderHelper.addAddressToPerson(label, address);
382             }
383         }
384 
385         @NonNull
386         @Override
getTypeLabel( @onNull Resources resources, int type, @Nullable String label)387         protected String getTypeLabel(
388                 @NonNull Resources resources, int type, @Nullable String label) {
389             Objects.requireNonNull(resources);
390             return StructuredPostal.getTypeLabel(resources, type, label).toString();
391         }
392     }
393 
394     private static final class NicknameDataHandler extends SingleColumnDataHandler {
NicknameDataHandler()395         public NicknameDataHandler() {
396             super(Nickname.NAME);
397         }
398 
399         @Override
addSingleColumnStringData( @onNull PersonBuilderHelper builder, @NonNull String data)400         protected void addSingleColumnStringData(
401                 @NonNull PersonBuilderHelper builder, @NonNull String data) {
402             Objects.requireNonNull(builder);
403             Objects.requireNonNull(data);
404             builder.getPersonBuilder().addAdditionalName(Person.TYPE_NICKNAME, data);
405         }
406     }
407 
408     private static final class StructuredNameHandler extends DataHandler {
409         private static final String[] COLUMNS = {
410             Data.RAW_CONTACT_ID,
411             Data.NAME_RAW_CONTACT_ID,
412             // Only those three fields we need to set in the builder.
413             StructuredName.GIVEN_NAME,
414             StructuredName.MIDDLE_NAME,
415             StructuredName.FAMILY_NAME,
416         };
417 
418         /** Adds the columns needed for the {@code DataHandler}. */
419         @Override
addNeededColumns(Collection<String> columns)420         public final void addNeededColumns(Collection<String> columns) {
421             Collections.addAll(columns, COLUMNS);
422         }
423 
424         /** Adds the data into {@link Person.Builder}. */
425         @Override
addData(@onNull PersonBuilderHelper builderHelper, Cursor cursor)426         public final void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor) {
427             Objects.requireNonNull(builderHelper);
428             String rawContactId = getColumnString(cursor, Data.RAW_CONTACT_ID);
429             String nameRawContactId = getColumnString(cursor, Data.NAME_RAW_CONTACT_ID);
430             String givenName = getColumnString(cursor, StructuredName.GIVEN_NAME);
431             String familyName = getColumnString(cursor, StructuredName.FAMILY_NAME);
432             String middleName = getColumnString(cursor, StructuredName.MIDDLE_NAME);
433 
434             Person.Builder builder = builderHelper.getPersonBuilder();
435             // only set given, middle and family name iff rawContactId is same as
436             // nameRawContactId. In this case those three match the value for displayName in CP2.
437             if (!TextUtils.isEmpty(rawContactId)
438                     && !TextUtils.isEmpty(nameRawContactId)
439                     && rawContactId.equals(nameRawContactId)) {
440                 if (givenName != null) {
441                     builder.setGivenName(givenName);
442                 }
443                 if (familyName != null) {
444                     builder.setFamilyName(familyName);
445                 }
446                 if (middleName != null) {
447                     builder.setMiddleName(middleName);
448                 }
449             }
450         }
451     }
452 
453     private static final class OrganizationDataHandler extends DataHandler {
454         private static final String[] COLUMNS = {
455             Organization.TITLE, Organization.DEPARTMENT, Organization.COMPANY,
456         };
457 
458         private final StringBuilder mStringBuilder = new StringBuilder();
459 
460         @Override
addNeededColumns(Collection<String> columns)461         public void addNeededColumns(Collection<String> columns) {
462             for (String column : COLUMNS) {
463                 columns.add(column);
464             }
465         }
466 
467         @Override
addData(@onNull PersonBuilderHelper builder, Cursor cursor)468         public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) {
469             mStringBuilder.setLength(0);
470             for (String column : COLUMNS) {
471                 String value = getColumnString(cursor, column);
472                 if (!TextUtils.isEmpty(value)) {
473                     if (mStringBuilder.length() != 0) {
474                         mStringBuilder.append(", ");
475                     }
476                     mStringBuilder.append(value);
477                 }
478             }
479             if (mStringBuilder.length() > 0) {
480                 builder.getPersonBuilder().addAffiliation(mStringBuilder.toString());
481             }
482         }
483     }
484 
485     private static final class RelationDataHandler extends DataHandler {
486         private static final String[] COLUMNS = {
487             Relation.NAME, Relation.TYPE, Relation.LABEL,
488         };
489 
490         private final Resources mResources;
491 
RelationDataHandler(@onNull Resources resources)492         public RelationDataHandler(@NonNull Resources resources) {
493             mResources = resources;
494         }
495 
496         @Override
addNeededColumns(Collection<String> columns)497         public void addNeededColumns(Collection<String> columns) {
498             for (String column : COLUMNS) {
499                 columns.add(column);
500             }
501         }
502 
503         @Override
addData(@onNull PersonBuilderHelper builder, Cursor cursor)504         public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) {
505             String relationName = getColumnString(cursor, Relation.NAME);
506             if (TextUtils.isEmpty(relationName)) {
507                 // Get the relation name from type. If it is a custom type, get it from
508                 // label.
509                 int type = getColumnInt(cursor, Relation.TYPE);
510                 String label = getColumnString(cursor, Relation.LABEL);
511                 relationName = Relation.getTypeLabel(mResources, type, label).toString();
512                 if (TextUtils.isEmpty(relationName)) {
513                     return;
514                 }
515             }
516             builder.getPersonBuilder().addRelation(relationName);
517         }
518     }
519 
520     private static final class NoteDataHandler extends SingleColumnDataHandler {
NoteDataHandler()521         public NoteDataHandler() {
522             super(Note.NOTE);
523         }
524 
525         @Override
addSingleColumnStringData( @onNull PersonBuilderHelper builder, @NonNull String data)526         protected void addSingleColumnStringData(
527                 @NonNull PersonBuilderHelper builder, @NonNull String data) {
528             Objects.requireNonNull(builder);
529             Objects.requireNonNull(data);
530             builder.getPersonBuilder().addNote(data);
531         }
532     }
533 }
534