• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
88      * into the {@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(@NonNull Cursor cursor,
96             @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(@NonNull PersonBuilderHelper builderHelper,
159                 @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(PersonBuilderHelper builderHelper,
170                 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, @NonNull String[] dataColumns,
181                 @NonNull String typeColumn, @NonNull String labelColumn) {
182             mResources = Objects.requireNonNull(resources);
183             mDataColumns = Objects.requireNonNull(dataColumns);
184             mTypeColumn = Objects.requireNonNull(typeColumn);
185             mLabelColumn = Objects.requireNonNull(labelColumn);
186         }
187 
188         /** Adds the columns needed for the {@code DataHandler}. */
189         @Override
addNeededColumns(@onNull Collection<String> columns)190         public final void addNeededColumns(@NonNull Collection<String> columns) {
191             Objects.requireNonNull(columns);
192             columns.add(Data._ID);
193             columns.add(Data.IS_PRIMARY);
194             columns.add(Data.IS_SUPER_PRIMARY);
195             for (int i = 0; i < mDataColumns.length; ++i) {
196                 columns.add(mDataColumns[i]);
197             }
198             columns.add(mTypeColumn);
199             columns.add(mLabelColumn);
200         }
201 
202         /**
203          * Adds the data for ContactsPoint(email, telephone, postal addresses) into
204          * {@link Person.Builder}.
205          */
206         @Override
addData(@onNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor)207         public final void addData(@NonNull PersonBuilderHelper builderHelper,
208                 @NonNull Cursor cursor) {
209             Objects.requireNonNull(builderHelper);
210             Objects.requireNonNull(cursor);
211 
212             Map<String, String> data = new ArrayMap<>(mDataColumns.length);
213             for (int i = 0; i < mDataColumns.length; ++i) {
214                 String col = getColumnString(cursor, mDataColumns[i]);
215                 if (!TextUtils.isEmpty(col)) {
216                     data.put(mDataColumns[i], col);
217                 }
218             }
219 
220             if (!data.isEmpty()) {
221                 // get the corresponding label to the type.
222                 int type = getColumnInt(cursor, mTypeColumn);
223                 String label = getTypeLabel(mResources, type,
224                         getColumnString(cursor, mLabelColumn));
225                 addContactPointData(builderHelper, label, data);
226             }
227         }
228 
229         @NonNull
getTypeLabel(Resources resources, int type, String label)230         protected abstract String getTypeLabel(Resources resources, int type, String label);
231 
232         /**
233          * Adds the information in the {@link Person.Builder}.
234          *
235          * @param builderHelper a helper to build the {@link Person}.
236          * @param label         the corresponding label to the {@code type} for the data.
237          * @param data          data read from the designed columns in the row.
238          */
addContactPointData( PersonBuilderHelper builderHelper, String label, Map<String, String> data)239         protected abstract void addContactPointData(
240                 PersonBuilderHelper builderHelper, String label, Map<String, String> data);
241     }
242 
243     private static final class EmailDataHandler extends ContactPointDataHandler {
244         private static final String[] COLUMNS = {
245                 Email.ADDRESS,
246         };
247 
EmailDataHandler(@onNull Resources resources)248         public EmailDataHandler(@NonNull Resources resources) {
249             super(resources, COLUMNS, Email.TYPE, Email.LABEL);
250         }
251 
252         /**
253          * Adds the Email information in the {@link Person.Builder}.
254          *
255          * @param builderHelper a builder to build the {@link Person}.
256          * @param label         The corresponding label to the {@code type}. E.g. {@link
257          *                      com.android.internal.R.string#emailTypeHome} to {@link
258          *                      Email#TYPE_HOME} or custom label for the data if {@code type} is
259          *                      {@link
260          *                      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, @NonNull String label,
266                 @NonNull Map<String, String> data) {
267             Objects.requireNonNull(builderHelper);
268             Objects.requireNonNull(data);
269             Objects.requireNonNull(label);
270             String email = data.get(Email.ADDRESS);
271             if (!TextUtils.isEmpty(email)) {
272                 builderHelper.addEmailToPerson(label, email);
273             }
274         }
275 
276         @NonNull
277         @Override
getTypeLabel(@onNull Resources resources, int type, @Nullable String label)278         protected String getTypeLabel(@NonNull Resources resources, int type,
279                 @Nullable String label) {
280             Objects.requireNonNull(resources);
281             return Email.getTypeLabel(resources, type, label).toString();
282         }
283     }
284 
285     private static final class PhoneHandler extends ContactPointDataHandler {
286         private static final String[] COLUMNS = {
287                 Phone.NUMBER,
288                 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
304          *                      Phone#TYPE_HOME}, or custom label for the data if {@code type} is
305          *                      {@link Phone#TYPE_CUSTOM}.
306          * @param data          data read from the designed columns {@link Phone#NUMBER} in the row.
307          */
308         @Override
addContactPointData( @onNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map<String, String> data)309         protected void addContactPointData(
310                 @NonNull PersonBuilderHelper builderHelper, @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(mResources,
333                             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(@NonNull Resources resources, int type,
346                 @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(
359                     resources,
360                     COLUMNS,
361                     StructuredPostal.TYPE,
362                     StructuredPostal.LABEL);
363         }
364 
365         /**
366          * Adds the postal address information in the {@link Person.Builder}.
367          *
368          * @param builderHelper helper to build the {@link Person}.
369          * @param label         corresponding label to {@code type}. E.g. {@link
370          *                      com.android.internal.R.string#postalTypeHome} to {@link
371          *                      StructuredPostal#TYPE_HOME}, or custom label for the data if {@code
372          *                      type} is {@link StructuredPostal#TYPE_CUSTOM}.
373          * @param data          data read from the designed column
374          *                      {@link StructuredPostal#FORMATTED_ADDRESS} in the row.
375          */
376         @Override
addContactPointData( @onNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map<String, String> data)377         protected void addContactPointData(
378                 @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
379                 @NonNull Map<String, String> data) {
380             Objects.requireNonNull(builderHelper);
381             Objects.requireNonNull(data);
382             Objects.requireNonNull(label);
383             String address = data.get(StructuredPostal.FORMATTED_ADDRESS);
384             if (!TextUtils.isEmpty(address)) {
385                 builderHelper.addAddressToPerson(label, address);
386             }
387         }
388 
389         @NonNull
390         @Override
getTypeLabel(@onNull Resources resources, int type, @Nullable String label)391         protected String getTypeLabel(@NonNull Resources resources, int type,
392                 @Nullable String label) {
393             Objects.requireNonNull(resources);
394             return StructuredPostal.getTypeLabel(resources, type, label).toString();
395         }
396     }
397 
398     private static final class NicknameDataHandler extends SingleColumnDataHandler {
NicknameDataHandler()399         public NicknameDataHandler() {
400             super(Nickname.NAME);
401         }
402 
403         @Override
addSingleColumnStringData(@onNull PersonBuilderHelper builder, @NonNull String data)404         protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
405                 @NonNull String data) {
406             Objects.requireNonNull(builder);
407             Objects.requireNonNull(data);
408             builder.getPersonBuilder().addAdditionalName(Person.TYPE_NICKNAME, data);
409         }
410     }
411 
412     private static final class StructuredNameHandler extends DataHandler {
413         private static final String[] COLUMNS = {
414                 Data.RAW_CONTACT_ID,
415                 Data.NAME_RAW_CONTACT_ID,
416                 // Only those three fields we need to set in the builder.
417                 StructuredName.GIVEN_NAME,
418                 StructuredName.MIDDLE_NAME,
419                 StructuredName.FAMILY_NAME,
420         };
421 
422         /** Adds the columns needed for the {@code DataHandler}. */
423         @Override
addNeededColumns(Collection<String> columns)424         public final void addNeededColumns(Collection<String> columns) {
425             Collections.addAll(columns, COLUMNS);
426         }
427 
428         /** Adds the data into {@link Person.Builder}. */
429         @Override
addData(@onNull PersonBuilderHelper builderHelper, Cursor cursor)430         public final void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor) {
431             Objects.requireNonNull(builderHelper);
432             String rawContactId = getColumnString(cursor, Data.RAW_CONTACT_ID);
433             String nameRawContactId = getColumnString(cursor, Data.NAME_RAW_CONTACT_ID);
434             String givenName = getColumnString(cursor, StructuredName.GIVEN_NAME);
435             String familyName = getColumnString(cursor, StructuredName.FAMILY_NAME);
436             String middleName = getColumnString(cursor, StructuredName.MIDDLE_NAME);
437 
438             Person.Builder builder = builderHelper.getPersonBuilder();
439             // only set given, middle and family name iff rawContactId is same as
440             // nameRawContactId. In this case those three match the value for displayName in CP2.
441             if (!TextUtils.isEmpty(rawContactId)
442                     && !TextUtils.isEmpty(nameRawContactId)
443                     && rawContactId.equals(nameRawContactId)) {
444                 if (givenName != null) {
445                     builder.setGivenName(givenName);
446                 }
447                 if (familyName != null) {
448                     builder.setFamilyName(familyName);
449                 }
450                 if (middleName != null) {
451                     builder.setMiddleName(middleName);
452                 }
453             }
454         }
455     }
456 
457     private static final class OrganizationDataHandler extends DataHandler {
458         private static final String[] COLUMNS = {
459                 Organization.TITLE,
460                 Organization.DEPARTMENT,
461                 Organization.COMPANY,
462         };
463 
464         private final StringBuilder mStringBuilder = new StringBuilder();
465 
466         @Override
addNeededColumns(Collection<String> columns)467         public void addNeededColumns(Collection<String> columns) {
468             for (String column : COLUMNS) {
469                 columns.add(column);
470             }
471         }
472 
473         @Override
addData(@onNull PersonBuilderHelper builder, Cursor cursor)474         public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) {
475             mStringBuilder.setLength(0);
476             for (String column : COLUMNS) {
477                 String value = getColumnString(cursor, column);
478                 if (!TextUtils.isEmpty(value)) {
479                     if (mStringBuilder.length() != 0) {
480                         mStringBuilder.append(", ");
481                     }
482                     mStringBuilder.append(value);
483                 }
484             }
485             if (mStringBuilder.length() > 0) {
486                 builder.getPersonBuilder().addAffiliation(mStringBuilder.toString());
487             }
488         }
489     }
490 
491     private static final class RelationDataHandler extends DataHandler {
492         private static final String[] COLUMNS = {
493                 Relation.NAME,
494                 Relation.TYPE,
495                 Relation.LABEL,
496         };
497 
498         private final Resources mResources;
499 
RelationDataHandler(@onNull Resources resources)500         public RelationDataHandler(@NonNull Resources resources) {
501             mResources = resources;
502         }
503 
504         @Override
addNeededColumns(Collection<String> columns)505         public void addNeededColumns(Collection<String> columns) {
506             for (String column : COLUMNS) {
507                 columns.add(column);
508             }
509         }
510 
511         @Override
addData(@onNull PersonBuilderHelper builder, Cursor cursor)512         public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) {
513             String relationName = getColumnString(cursor, Relation.NAME);
514             if (TextUtils.isEmpty(relationName)) {
515                 // Get the relation name from type. If it is a custom type, get it from
516                 // label.
517                 int type = getColumnInt(cursor, Relation.TYPE);
518                 String label = getColumnString(cursor, Relation.LABEL);
519                 relationName = Relation.getTypeLabel(mResources, type, label).toString();
520                 if (TextUtils.isEmpty(relationName)) {
521                     return;
522                 }
523             }
524             builder.getPersonBuilder().addRelation(relationName);
525         }
526     }
527 
528     private static final class NoteDataHandler extends SingleColumnDataHandler {
NoteDataHandler()529         public NoteDataHandler() {
530             super(Note.NOTE);
531         }
532 
533         @Override
addSingleColumnStringData(@onNull PersonBuilderHelper builder, @NonNull String data)534         protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
535                 @NonNull String data) {
536             Objects.requireNonNull(builder);
537             Objects.requireNonNull(data);
538             builder.getPersonBuilder().addNote(data);
539         }
540     }
541 }