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 }