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