/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.appsearch.contactsindexer; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.database.Cursor; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.Data; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import com.android.server.appsearch.contactsindexer.appsearchtypes.Person; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Set; /** * Helper Class to handle data for different MIME types from CP2, and build {@link Person} from * them. * *

This class is not thread safe. * * @hide */ public final class ContactDataHandler { private final Map mHandlers; private final Set mNeededColumns; /** Constructor. */ public ContactDataHandler(Resources resources) { // Create handlers for different MIME types mHandlers = new ArrayMap<>(); mHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataHandler(resources)); mHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataHandler()); mHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneHandler(resources)); mHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, new StructuredPostalHandler(resources)); mHandlers.put(StructuredName.CONTENT_ITEM_TYPE, new StructuredNameHandler()); mHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataHandler()); mHandlers.put(Relation.CONTENT_ITEM_TYPE, new RelationDataHandler(resources)); mHandlers.put(Note.CONTENT_ITEM_TYPE, new NoteDataHandler()); // Retrieve all the needed columns from different data handlers. Set neededColumns = new ArraySet<>(); neededColumns.add(ContactsContract.Data.MIMETYPE); for (DataHandler handler : mHandlers.values()) { handler.addNeededColumns(neededColumns); } // We need to make sure this is unmodifiable since the reference is returned in // getNeededColumns(). mNeededColumns = Collections.unmodifiableSet(neededColumns); } /** Returns an unmodifiable set of columns this {@link ContactDataHandler} is asking for. */ public Set getNeededColumns() { return mNeededColumns; } /** * Adds the information of the current row from {@link ContactsContract.Data} table into the * {@link PersonBuilderHelper}. * *

By reading each row in the table, we will get the detailed information about a * Person(contact). * * @param builderHelper a helper to build the {@link Person}. */ public void convertCursorToPerson( @NonNull Cursor cursor, @NonNull PersonBuilderHelper builderHelper) { Objects.requireNonNull(cursor); Objects.requireNonNull(builderHelper); int mimetypeIndex = cursor.getColumnIndex(Data.MIMETYPE); String mimeType = cursor.getString(mimetypeIndex); DataHandler handler = mHandlers.get(mimeType); if (handler != null) { handler.addData(builderHelper, cursor); } } abstract static class DataHandler { /** Gets the column as a string. */ @Nullable protected final String getColumnString(@NonNull Cursor cursor, @NonNull String column) { Objects.requireNonNull(cursor); Objects.requireNonNull(column); int columnIndex = cursor.getColumnIndex(column); if (columnIndex == -1) { return null; } return cursor.getString(columnIndex); } /** Gets the column as an int. */ protected final int getColumnInt(@NonNull Cursor cursor, @NonNull String column) { Objects.requireNonNull(cursor); Objects.requireNonNull(column); int columnIndex = cursor.getColumnIndex(column); if (columnIndex == -1) { return 0; } return cursor.getInt(columnIndex); } /** Adds the columns needed for the {@code DataHandler}. */ public abstract void addNeededColumns(Collection columns); /** Adds the data into {@link PersonBuilderHelper}. */ public abstract void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor); } private abstract static class SingleColumnDataHandler extends DataHandler { private final String mColumn; protected SingleColumnDataHandler(@NonNull String column) { Objects.requireNonNull(column); mColumn = column; } /** Adds the columns needed for the {@code DataHandler}. */ @Override public final void addNeededColumns(@NonNull Collection columns) { Objects.requireNonNull(columns); columns.add(mColumn); } /** Adds the data into {@link PersonBuilderHelper}. */ @Override public final void addData( @NonNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor) { Objects.requireNonNull(builderHelper); Objects.requireNonNull(cursor); String data = getColumnString(cursor, mColumn); if (!TextUtils.isEmpty(data)) { addSingleColumnStringData(builderHelper, data); } } protected abstract void addSingleColumnStringData( PersonBuilderHelper builderHelper, String data); } private abstract static class ContactPointDataHandler extends DataHandler { private final Resources mResources; private final String[] mDataColumns; private final String mTypeColumn; private final String mLabelColumn; public ContactPointDataHandler( @NonNull Resources resources, @NonNull String[] dataColumns, @NonNull String typeColumn, @NonNull String labelColumn) { mResources = Objects.requireNonNull(resources); mDataColumns = Objects.requireNonNull(dataColumns); mTypeColumn = Objects.requireNonNull(typeColumn); mLabelColumn = Objects.requireNonNull(labelColumn); } /** Adds the columns needed for the {@code DataHandler}. */ @Override public final void addNeededColumns(@NonNull Collection columns) { Objects.requireNonNull(columns); columns.add(Data._ID); columns.add(Data.IS_PRIMARY); columns.add(Data.IS_SUPER_PRIMARY); for (int i = 0; i < mDataColumns.length; ++i) { columns.add(mDataColumns[i]); } columns.add(mTypeColumn); columns.add(mLabelColumn); } /** * Adds the data for ContactsPoint(email, telephone, postal addresses) into {@link * Person.Builder}. */ @Override public final void addData( @NonNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor) { Objects.requireNonNull(builderHelper); Objects.requireNonNull(cursor); Map data = new ArrayMap<>(mDataColumns.length); for (int i = 0; i < mDataColumns.length; ++i) { String col = getColumnString(cursor, mDataColumns[i]); if (!TextUtils.isEmpty(col)) { data.put(mDataColumns[i], col); } } if (!data.isEmpty()) { // get the corresponding label to the type. int type = getColumnInt(cursor, mTypeColumn); String label = getTypeLabel(mResources, type, getColumnString(cursor, mLabelColumn)); addContactPointData(builderHelper, label, data); } } @NonNull protected abstract String getTypeLabel(Resources resources, int type, String label); /** * Adds the information in the {@link Person.Builder}. * * @param builderHelper a helper to build the {@link Person}. * @param label the corresponding label to the {@code type} for the data. * @param data data read from the designed columns in the row. */ protected abstract void addContactPointData( PersonBuilderHelper builderHelper, String label, Map data); } private static final class EmailDataHandler extends ContactPointDataHandler { private static final String[] COLUMNS = { Email.ADDRESS, }; public EmailDataHandler(@NonNull Resources resources) { super(resources, COLUMNS, Email.TYPE, Email.LABEL); } /** * Adds the Email information in the {@link Person.Builder}. * * @param builderHelper a builder to build the {@link Person}. * @param label The corresponding label to the {@code type}. E.g. {@link * com.android.internal.R.string#emailTypeHome} to {@link Email#TYPE_HOME} or custom * label for the data if {@code type} is {@link Email#TYPE_CUSTOM}. * @param data data read from the designed column {@code Email.ADDRESS} in the row. */ @Override protected void addContactPointData( @NonNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map data) { Objects.requireNonNull(builderHelper); Objects.requireNonNull(data); Objects.requireNonNull(label); String email = data.get(Email.ADDRESS); if (!TextUtils.isEmpty(email)) { builderHelper.addEmailToPerson(label, email); } } @NonNull @Override protected String getTypeLabel( @NonNull Resources resources, int type, @Nullable String label) { Objects.requireNonNull(resources); return Email.getTypeLabel(resources, type, label).toString(); } } private static final class PhoneHandler extends ContactPointDataHandler { private static final String[] COLUMNS = { Phone.NUMBER, Phone.NORMALIZED_NUMBER, }; private final Resources mResources; public PhoneHandler(@NonNull Resources resources) { super(resources, COLUMNS, Phone.TYPE, Phone.LABEL); mResources = Objects.requireNonNull(resources); } /** * Adds the phone number information in the {@link Person.Builder}. * * @param builderHelper helper to build the {@link Person}. * @param label corresponding label to {@code type}. E.g. {@link * com.android.internal.R.string#phoneTypeHome} to {@link Phone#TYPE_HOME}, or custom * label for the data if {@code type} is {@link Phone#TYPE_CUSTOM}. * @param data data read from the designed columns {@link Phone#NUMBER} in the row. */ @Override protected void addContactPointData( @NonNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map data) { Objects.requireNonNull(builderHelper); Objects.requireNonNull(data); Objects.requireNonNull(label); // Add original phone number directly to the final phone number // list. E.g. (202) 555-0111 String phoneNumberOriginal = data.get(Phone.NUMBER); if (TextUtils.isEmpty(phoneNumberOriginal)) { return; } builderHelper.addPhoneToPerson(label, phoneNumberOriginal); // Try to get phone number in e164 from CP2. String phoneNumberE164FromCP2 = data.get(Phone.NORMALIZED_NUMBER); // Try to include different variants based on the national (e.g. (202) 555-0111), and // the e164 format of the original number. The variants are generated with the best // efforts, depending on the locales available in the current configuration on the // system. Set phoneNumberVariants = ContactsIndexerPhoneNumberUtils.createPhoneNumberVariants( mResources, phoneNumberOriginal, phoneNumberE164FromCP2); phoneNumberVariants.remove(phoneNumberOriginal); for (String variant : phoneNumberVariants) { // Append phone variants to a different list, which will be appended into // the final one during buildPerson. builderHelper.addPhoneVariantToPerson(label, variant); } } @NonNull @Override protected String getTypeLabel( @NonNull Resources resources, int type, @Nullable String label) { Objects.requireNonNull(resources); return Phone.getTypeLabel(resources, type, label).toString(); } } private static final class StructuredPostalHandler extends ContactPointDataHandler { private static final String[] COLUMNS = { StructuredPostal.FORMATTED_ADDRESS, }; public StructuredPostalHandler(@NonNull Resources resources) { super(resources, COLUMNS, StructuredPostal.TYPE, StructuredPostal.LABEL); } /** * Adds the postal address information in the {@link Person.Builder}. * * @param builderHelper helper to build the {@link Person}. * @param label corresponding label to {@code type}. E.g. {@link * com.android.internal.R.string#postalTypeHome} to {@link StructuredPostal#TYPE_HOME}, * or custom label for the data if {@code type} is {@link StructuredPostal#TYPE_CUSTOM}. * @param data data read from the designed column {@link StructuredPostal#FORMATTED_ADDRESS} * in the row. */ @Override protected void addContactPointData( @NonNull PersonBuilderHelper builderHelper, @NonNull String label, @NonNull Map data) { Objects.requireNonNull(builderHelper); Objects.requireNonNull(data); Objects.requireNonNull(label); String address = data.get(StructuredPostal.FORMATTED_ADDRESS); if (!TextUtils.isEmpty(address)) { builderHelper.addAddressToPerson(label, address); } } @NonNull @Override protected String getTypeLabel( @NonNull Resources resources, int type, @Nullable String label) { Objects.requireNonNull(resources); return StructuredPostal.getTypeLabel(resources, type, label).toString(); } } private static final class NicknameDataHandler extends SingleColumnDataHandler { public NicknameDataHandler() { super(Nickname.NAME); } @Override protected void addSingleColumnStringData( @NonNull PersonBuilderHelper builder, @NonNull String data) { Objects.requireNonNull(builder); Objects.requireNonNull(data); builder.getPersonBuilder().addAdditionalName(Person.TYPE_NICKNAME, data); } } private static final class StructuredNameHandler extends DataHandler { private static final String[] COLUMNS = { Data.RAW_CONTACT_ID, Data.NAME_RAW_CONTACT_ID, // Only those three fields we need to set in the builder. StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, }; /** Adds the columns needed for the {@code DataHandler}. */ @Override public final void addNeededColumns(Collection columns) { Collections.addAll(columns, COLUMNS); } /** Adds the data into {@link Person.Builder}. */ @Override public final void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor) { Objects.requireNonNull(builderHelper); String rawContactId = getColumnString(cursor, Data.RAW_CONTACT_ID); String nameRawContactId = getColumnString(cursor, Data.NAME_RAW_CONTACT_ID); String givenName = getColumnString(cursor, StructuredName.GIVEN_NAME); String familyName = getColumnString(cursor, StructuredName.FAMILY_NAME); String middleName = getColumnString(cursor, StructuredName.MIDDLE_NAME); Person.Builder builder = builderHelper.getPersonBuilder(); // only set given, middle and family name iff rawContactId is same as // nameRawContactId. In this case those three match the value for displayName in CP2. if (!TextUtils.isEmpty(rawContactId) && !TextUtils.isEmpty(nameRawContactId) && rawContactId.equals(nameRawContactId)) { if (givenName != null) { builder.setGivenName(givenName); } if (familyName != null) { builder.setFamilyName(familyName); } if (middleName != null) { builder.setMiddleName(middleName); } } } } private static final class OrganizationDataHandler extends DataHandler { private static final String[] COLUMNS = { Organization.TITLE, Organization.DEPARTMENT, Organization.COMPANY, }; private final StringBuilder mStringBuilder = new StringBuilder(); @Override public void addNeededColumns(Collection columns) { for (String column : COLUMNS) { columns.add(column); } } @Override public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) { mStringBuilder.setLength(0); for (String column : COLUMNS) { String value = getColumnString(cursor, column); if (!TextUtils.isEmpty(value)) { if (mStringBuilder.length() != 0) { mStringBuilder.append(", "); } mStringBuilder.append(value); } } if (mStringBuilder.length() > 0) { builder.getPersonBuilder().addAffiliation(mStringBuilder.toString()); } } } private static final class RelationDataHandler extends DataHandler { private static final String[] COLUMNS = { Relation.NAME, Relation.TYPE, Relation.LABEL, }; private final Resources mResources; public RelationDataHandler(@NonNull Resources resources) { mResources = resources; } @Override public void addNeededColumns(Collection columns) { for (String column : COLUMNS) { columns.add(column); } } @Override public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) { String relationName = getColumnString(cursor, Relation.NAME); if (TextUtils.isEmpty(relationName)) { // Get the relation name from type. If it is a custom type, get it from // label. int type = getColumnInt(cursor, Relation.TYPE); String label = getColumnString(cursor, Relation.LABEL); relationName = Relation.getTypeLabel(mResources, type, label).toString(); if (TextUtils.isEmpty(relationName)) { return; } } builder.getPersonBuilder().addRelation(relationName); } } private static final class NoteDataHandler extends SingleColumnDataHandler { public NoteDataHandler() { super(Note.NOTE); } @Override protected void addSingleColumnStringData( @NonNull PersonBuilderHelper builder, @NonNull String data) { Objects.requireNonNull(builder); Objects.requireNonNull(data); builder.getPersonBuilder().addNote(data); } } }