Add ContactDataHandler to convert CP2 contacts to Person objects

Add classes to handle data from CP2 to AppSearch:
1) Added two appsearchtypes ContactPoint and Person
2) Added ContactDataHandler to handle different mime types
3) Added ContactDataHandlerTest

One Pager: go/appsearch-contacts-indexer-one-pager
Design doc: go/appsearch-contacts-indexer(Implementation Plan:
https://docs.google.com/document/d/1qfek20XQQYh02V2J6K0pXfrhnXYmVH-1NqwI8Xiqd10/edit#heading=h.8tlk1jy5d7ek)
Prototype: ag/16130927

Bug: 203605504
Test: atest ContactDataHandlerTest
Change-Id: Id21bd349f0cde409aaead891f1c9baa31f090545
Merged-In: Id21bd349f0cde409aaead891f1c9baa31f090545
(cherry picked from commit d2c6ccfbca57eaf0e3a145906281782f341e6fb4)
diff --git a/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java b/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
new file mode 100644
index 0000000..dada468
--- /dev/null
+++ b/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 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;
+
+/**
+ * Helper class to manage the Person corpus in AppSearch.
+ *
+ * @hide
+ */
+public class AppSearchHelper {
+    public static final int BASE_SCORE = 1;
+    // Namespace needed to be used for ContactsIndexer to index the contacts
+    public static final String NAMESPACE = "";
+}
\ No newline at end of file
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java b/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java
new file mode 100644
index 0000000..4a93c61
--- /dev/null
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java
@@ -0,0 +1,390 @@
+/*
+ * 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.Phone;
+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.
+ *
+ * @hide
+ */
+public final class ContactDataHandler {
+    private final Map<String, DataHandler> mHandlers;
+    private final Set<String> 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());
+
+        // Retrieve all the needed columns from different data handlers.
+        Set<String> 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);
+    }
+
+    /**
+     * Adds the information of the current row from {@link ContactsContract.Data} table
+     * into the {@link PersonBuilderHelper}.
+     *
+     * <p>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<String> 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<String> 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 mDataColumn;
+        private final String mTypeColumn;
+        private final String mLabelColumn;
+
+        public ContactPointDataHandler(
+                @NonNull Resources resources, @NonNull String dataColumn,
+                @NonNull String typeColumn, @NonNull String labelColumn) {
+            mResources = Objects.requireNonNull(resources);
+            mDataColumn = Objects.requireNonNull(dataColumn);
+            mTypeColumn = Objects.requireNonNull(typeColumn);
+            mLabelColumn = Objects.requireNonNull(labelColumn);
+        }
+
+        /** Adds the columns needed for the {@code DataHandler}. */
+        @Override
+        public final void addNeededColumns(@NonNull Collection<String> columns) {
+            Objects.requireNonNull(columns);
+            columns.add(Data._ID);
+            columns.add(Data.IS_PRIMARY);
+            columns.add(Data.IS_SUPER_PRIMARY);
+            columns.add(mDataColumn);
+            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);
+
+            String data = getColumnString(cursor, mDataColumn);
+            if (!TextUtils.isEmpty(mDataColumn)) {
+                // 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 column in the row.
+         */
+        protected abstract void addContactPointData(
+                PersonBuilderHelper builderHelper, String label, String data);
+    }
+
+    private static final class EmailDataHandler extends ContactPointDataHandler {
+        public EmailDataHandler(@NonNull Resources resources) {
+            super(resources, Email.ADDRESS, 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 String data) {
+            Objects.requireNonNull(builderHelper);
+            Objects.requireNonNull(data);
+            Objects.requireNonNull(label);
+            builderHelper.addEmailToPerson(label, data);
+        }
+
+        @NonNull
+        @Override
+        protected String getTypeLabel(@NonNull Resources resources, int type,
+                @NonNull String label) {
+            Objects.requireNonNull(resources);
+            Objects.requireNonNull(label);
+            return Email.getTypeLabel(resources, type, label).toString();
+        }
+    }
+
+    private static final class PhoneHandler extends ContactPointDataHandler {
+        public PhoneHandler(@NonNull Resources resources) {
+            super(resources, Phone.NUMBER, Phone.TYPE, Phone.LABEL);
+        }
+
+        /**
+         * 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 column {@link Phone#NUMBER} in the row.
+         */
+        @Override
+        protected void addContactPointData(
+                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
+                @NonNull String data) {
+            Objects.requireNonNull(builderHelper);
+            Objects.requireNonNull(data);
+            Objects.requireNonNull(label);
+            builderHelper.addPhoneToPerson(label, data);
+        }
+
+        @NonNull
+        @Override
+        protected String getTypeLabel(@NonNull Resources resources, int type,
+                @NonNull String label) {
+            Objects.requireNonNull(resources);
+            Objects.requireNonNull(label);
+            return Phone.getTypeLabel(resources, type, label).toString();
+        }
+    }
+
+    private static final class StructuredPostalHandler extends ContactPointDataHandler {
+        public StructuredPostalHandler(@NonNull Resources resources) {
+            super(
+                    resources,
+                    StructuredPostal.FORMATTED_ADDRESS,
+                    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 String data) {
+            Objects.requireNonNull(builderHelper);
+            Objects.requireNonNull(data);
+            Objects.requireNonNull(label);
+            builderHelper.addAddressToPerson(label, data);
+        }
+
+        @NonNull
+        @Override
+        protected String getTypeLabel(@NonNull Resources resources, int type,
+                @NonNull String label) {
+            Objects.requireNonNull(resources);
+            Objects.requireNonNull(label);
+            return StructuredPostal.getTypeLabel(resources, type, label).toString();
+        }
+    }
+
+    private static final class NicknameDataHandler extends SingleColumnDataHandler {
+        protected NicknameDataHandler() {
+            super(Nickname.NAME);
+        }
+
+        @Override
+        protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
+                @NonNull String data) {
+            Objects.requireNonNull(builder);
+            Objects.requireNonNull(data);
+            builder.getPersonBuilder().addAdditionalName(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<String> 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);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsContentObserver.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsContentObserver.java
index 5d0bbf2..d0fac4f 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsContentObserver.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsContentObserver.java
@@ -22,9 +22,6 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.UserHandle;
-import android.util.Log;
-
-import com.android.internal.util.Preconditions;
 
 import java.util.Collection;
 import java.util.Objects;
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsPerUserIndexer.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsPerUserIndexer.java
index 0d54c67..9b6368e 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsPerUserIndexer.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsPerUserIndexer.java
@@ -32,8 +32,6 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Objects;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
diff --git a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
new file mode 100644
index 0000000..97de574
--- /dev/null
+++ b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 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.util.ArrayMap;
+
+import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
+import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Helper class to help build the {@link Person}.
+ *
+ * <p>It takes a {@link Person.Builder} with a map to help handle and aggregate {@link
+ * ContactPoint}s.
+ */
+public class PersonBuilderHelper {
+    final private Person.Builder mBuilder;
+    private Map<String, ContactPoint.Builder> mContactPointBuilders = new ArrayMap<>();
+
+    public PersonBuilderHelper(@NonNull Person.Builder builder) {
+        Objects.requireNonNull(builder);
+        mBuilder = builder;
+    }
+
+    @NonNull
+    public Person buildPerson() {
+        for (ContactPoint.Builder builder : mContactPointBuilders.values()) {
+            mBuilder.addContactPoint(builder.build());
+        }
+        return mBuilder.build();
+    }
+
+    @NonNull
+    public Person.Builder getPersonBuilder() {
+        return mBuilder;
+    }
+
+    @NonNull
+    private ContactPoint.Builder getOrCreateContactPointBuilder(@NonNull String label) {
+        ContactPoint.Builder builder = mContactPointBuilders.get(Objects.requireNonNull(label));
+        if (builder == null) {
+            builder = new ContactPoint.Builder(AppSearchHelper.NAMESPACE,
+                    AppSearchHelper.NAMESPACE,
+                    label);
+            mContactPointBuilders.put(label, builder);
+        }
+
+        return builder;
+    }
+
+    @NonNull
+    public PersonBuilderHelper addAppIdToPerson(@NonNull String label, @NonNull String appId) {
+        getOrCreateContactPointBuilder(Objects.requireNonNull(label))
+                .addAppId(Objects.requireNonNull(appId));
+        return this;
+    }
+
+    public PersonBuilderHelper addEmailToPerson(@NonNull String label, @NonNull String email) {
+        getOrCreateContactPointBuilder(Objects.requireNonNull(label))
+                .addEmail(Objects.requireNonNull(email));
+        return this;
+    }
+
+    @NonNull
+    public PersonBuilderHelper addAddressToPerson(@NonNull String label, @NonNull String address) {
+        getOrCreateContactPointBuilder(Objects.requireNonNull(label))
+                .addAddress(Objects.requireNonNull(address));
+        return this;
+    }
+
+    @NonNull
+    public PersonBuilderHelper addPhoneToPerson(@NonNull String label, @NonNull String phone) {
+        getOrCreateContactPointBuilder(Objects.requireNonNull(label))
+                .addPhone(Objects.requireNonNull(phone));
+        return this;
+    }
+}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java
new file mode 100644
index 0000000..dd68a37
--- /dev/null
+++ b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2022 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.appsearchtypes;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a ContactPoint in AppSearch.
+ *
+ * @hide
+ */
+public final class ContactPoint extends GenericDocument {
+    public static final String SCHEMA_TYPE = "builtin:ContactPoint";
+
+    // Properties
+    @VisibleForTesting
+    public static final String CONTACT_POINT_PROPERTY_LABEL = "label";
+    @VisibleForTesting
+    public static final String CONTACT_POINT_PROPERTY_APP_ID = "appId";
+    @VisibleForTesting
+    public static final String CONTACT_POINT_PROPERTY_ADDRESS = "address";
+    @VisibleForTesting
+    public static final String CONTACT_POINT_PROPERTY_EMAIL = "email";
+    @VisibleForTesting
+    public static final String CONTACT_POINT_PROPERTY_TELEPHONE = "telephone";
+
+    // Schema
+    static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(
+            SCHEMA_TYPE)
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    CONTACT_POINT_PROPERTY_LABEL)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            // appIds
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    CONTACT_POINT_PROPERTY_APP_ID)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            // address
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    CONTACT_POINT_PROPERTY_ADDRESS)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            // email
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    CONTACT_POINT_PROPERTY_EMAIL)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            // telephone
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    CONTACT_POINT_PROPERTY_TELEPHONE)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            .build();
+
+    /** Constructs a {@link ContactPoint}. */
+    ContactPoint(@NonNull GenericDocument document) {
+        super(document);
+    }
+
+    @NonNull
+    public String getLabel() {
+        return getPropertyString(CONTACT_POINT_PROPERTY_LABEL);
+    }
+
+    @NonNull
+    public String[] getAppIds() {
+        return getPropertyStringArray(CONTACT_POINT_PROPERTY_APP_ID);
+    }
+
+    @NonNull
+    public String[] getAddresses() {
+        return getPropertyStringArray(CONTACT_POINT_PROPERTY_ADDRESS);
+    }
+
+    @NonNull
+    public String[] getEmails() {
+        return getPropertyStringArray(CONTACT_POINT_PROPERTY_EMAIL);
+    }
+
+    @NonNull
+    public String[] getPhones() {
+        return getPropertyStringArray(CONTACT_POINT_PROPERTY_TELEPHONE);
+    }
+
+    /** Builder for {@link ContactPoint}. */
+    public static final class Builder extends GenericDocument.Builder<Builder> {
+        private List<String> mAppIds = new ArrayList<>();
+        private List<String> mAddresses = new ArrayList<>();
+        private List<String> mEmails = new ArrayList<>();
+        private List<String> mTelephones = new ArrayList<>();
+
+        /**
+         * Creates a new {@link Builder}
+         *
+         * @param namespace The namespace of the Email.
+         * @param id        The ID of the Email.
+         * @param label     The label for this {@link ContactPoint}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String label) {
+            super(namespace, id, SCHEMA_TYPE);
+            setLabel(label);
+        }
+
+        @NonNull
+        private Builder setLabel(@NonNull String label) {
+            setPropertyString(CONTACT_POINT_PROPERTY_LABEL, label);
+            return this;
+        }
+
+        /**
+         * Add a unique AppId for this {@link ContactPoint}.
+         *
+         * @param appId a unique identifier for the application.
+         */
+        @NonNull
+        public Builder addAppId(@NonNull String appId) {
+            Objects.requireNonNull(appId);
+            mAppIds.add(appId);
+            return this;
+        }
+
+        @NonNull
+        public Builder addAddress(@NonNull String address) {
+            Objects.requireNonNull(address);
+            mAddresses.add(address);
+            return this;
+        }
+
+        @NonNull
+        public Builder addEmail(@NonNull String email) {
+            Objects.requireNonNull(email);
+            mEmails.add(email);
+            return this;
+        }
+
+        @NonNull
+        public Builder addPhone(@NonNull String phone) {
+            Objects.requireNonNull(phone);
+            mTelephones.add(phone);
+            return this;
+        }
+
+        @NonNull
+        public ContactPoint build() {
+            setPropertyString(CONTACT_POINT_PROPERTY_APP_ID, mAppIds.toArray(new String[0]));
+            setPropertyString(CONTACT_POINT_PROPERTY_EMAIL, mEmails.toArray(new String[0]));
+            setPropertyString(CONTACT_POINT_PROPERTY_ADDRESS, mAddresses.toArray(new String[0]));
+            setPropertyString(CONTACT_POINT_PROPERTY_TELEPHONE, mTelephones.toArray(new String[0]));
+            return new ContactPoint(super.build());
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java
new file mode 100644
index 0000000..c8b2c5b
--- /dev/null
+++ b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2022 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.appsearchtypes;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a Person in AppSearch.
+ *
+ * @hide
+ */
+public class Person extends GenericDocument {
+    static final String SCHEMA_TYPE = "builtin:Person";
+
+    // Properties
+    static final String PERSON_PROPERTY_NAME = "name";
+    static final String PERSON_PROPERTY_GIVEN_NAME = "givenName";
+    static final String PERSON_PROPERTY_MIDDLE_NAME = "middleName";
+    static final String PERSON_PROPERTY_FAMILY_NAME = "familyName";
+    static final String PERSON_PROPERTY_EXTERNAL_URI = "externalUri";
+    static final String PERSON_PROPERTY_ADDITIONAL_NAME = "additionalName";
+    static final String PERSON_PROPERTY_IS_IMPORTANT = "isImportant";
+    static final String PERSON_PROPERTY_IS_BOT = "isBot";
+    static final String PERSON_PROPERTY_IMAGE_URI = "imageUri";
+    static final String PERSON_PROPERTY_CONTACT_POINT = "contactPoint";
+
+    static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+            // full display name
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_NAME)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            // given name from CP2
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    PERSON_PROPERTY_GIVEN_NAME)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // middle name from CP2
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    PERSON_PROPERTY_MIDDLE_NAME)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // family name from CP2
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    PERSON_PROPERTY_FAMILY_NAME)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // lookup uri from CP2
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    PERSON_PROPERTY_EXTERNAL_URI)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // additional names e.g. nick names and phonetic names.
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    PERSON_PROPERTY_ADDITIONAL_NAME)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            // isImportant. It could be used to store isStarred from CP2.
+            .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                    PERSON_PROPERTY_IS_IMPORTANT)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // isBot
+            .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                    PERSON_PROPERTY_IS_BOT)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // imageUri
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    PERSON_PROPERTY_IMAGE_URI)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            // ContactPoint
+            .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                    PERSON_PROPERTY_CONTACT_POINT,
+                    ContactPoint.SCHEMA.getSchemaType())
+                    .setShouldIndexNestedProperties(true)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .build();
+
+    /** Constructs a {@link Person}. */
+    Person(@NonNull GenericDocument document) {
+        super(document);
+    }
+
+    @NonNull
+    public String getName() {
+        return getPropertyString(PERSON_PROPERTY_NAME);
+    }
+
+    @Nullable
+    public String getGivenName() {
+        return getPropertyString(PERSON_PROPERTY_GIVEN_NAME);
+    }
+
+    @Nullable
+    public String getMiddleName() {
+        return getPropertyString(PERSON_PROPERTY_MIDDLE_NAME);
+    }
+
+    @Nullable
+    public String getFamilyName() {
+        return getPropertyString(PERSON_PROPERTY_FAMILY_NAME);
+    }
+
+    @Nullable
+    public Uri getExternalUri() {
+        String uriStr = getPropertyString(PERSON_PROPERTY_EXTERNAL_URI);
+        if (uriStr == null) {
+            return null;
+        }
+        return Uri.parse(uriStr);
+    }
+
+    @Nullable
+    public Uri getImageUri() {
+        String uriStr = getPropertyString(PERSON_PROPERTY_IMAGE_URI);
+        if (uriStr == null) {
+            return null;
+        }
+        return Uri.parse(uriStr);
+    }
+
+    public boolean isImportant() {
+        return getPropertyBoolean(PERSON_PROPERTY_IS_IMPORTANT);
+    }
+
+    public boolean isBot() {
+        return getPropertyBoolean(PERSON_PROPERTY_IS_BOT);
+    }
+
+    @NonNull
+    public String[] getAdditionalNames() {
+        return getPropertyStringArray(PERSON_PROPERTY_ADDITIONAL_NAME);
+    }
+
+    // This method is expensive, and is intended to be used in tests only.
+    @NonNull
+    public ContactPoint[] getContactPoints() {
+        GenericDocument[] docs = getPropertyDocumentArray(PERSON_PROPERTY_CONTACT_POINT);
+        ContactPoint[] contactPoints = new ContactPoint[docs.length];
+        for (int i = 0; i < contactPoints.length; ++i) {
+            contactPoints[i] = new ContactPoint(docs[i]);
+        }
+        return contactPoints;
+    }
+
+    /** Builder for {@link Person}. */
+    public static final class Builder extends GenericDocument.Builder<Builder> {
+        private final List<String> mAdditionalNames = new ArrayList<>();
+        private final List<ContactPoint> mContactPoints = new ArrayList<>();
+
+        /**
+         * Creates a new {@link ContactPoint.Builder}
+         *
+         * @param namespace The namespace of the Email.
+         * @param id        The ID of the Email.
+         * @param name      The name of the {@link Person}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String name) {
+            super(namespace, id, SCHEMA_TYPE);
+            setName(name);
+        }
+
+        /** Sets the full display name. */
+        @NonNull
+        private Builder setName(@NonNull String name) {
+            setPropertyString(PERSON_PROPERTY_NAME, name);
+            return this;
+        }
+
+        @NonNull
+        public Builder setGivenName(@NonNull String givenName) {
+            setPropertyString(PERSON_PROPERTY_GIVEN_NAME, givenName);
+            return this;
+        }
+
+        @NonNull
+        public Builder setMiddleName(@NonNull String middleName) {
+            setPropertyString(PERSON_PROPERTY_MIDDLE_NAME, middleName);
+            return this;
+        }
+
+        @NonNull
+        public Builder setFamilyName(@NonNull String familyName) {
+            setPropertyString(PERSON_PROPERTY_FAMILY_NAME, familyName);
+            return this;
+        }
+
+        @NonNull
+        public Builder setExternalUri(@NonNull Uri externalUri) {
+            setPropertyString(PERSON_PROPERTY_EXTERNAL_URI,
+                    Objects.requireNonNull(externalUri).toString());
+            return this;
+        }
+
+        @NonNull
+        public Builder setImageUri(@NonNull Uri imageUri) {
+            setPropertyString(PERSON_PROPERTY_IMAGE_URI,
+                    Objects.requireNonNull(imageUri).toString());
+            return this;
+        }
+
+        @NonNull
+        public Builder setIsImportant(boolean isImportant) {
+            setPropertyBoolean(PERSON_PROPERTY_IS_IMPORTANT, isImportant);
+            return this;
+        }
+
+        @NonNull
+        public Builder setIsBot(boolean isBot) {
+            setPropertyBoolean(PERSON_PROPERTY_IS_BOT, isBot);
+            return this;
+        }
+
+        @NonNull
+        public Builder addAdditionalName(@NonNull String additionalName) {
+            Objects.requireNonNull(additionalName);
+            mAdditionalNames.add(additionalName);
+            return this;
+        }
+
+        @NonNull
+        public Builder addContactPoint(@NonNull ContactPoint contactPoint) {
+            Objects.requireNonNull(contactPoint);
+            mContactPoints.add(contactPoint);
+            return this;
+        }
+
+        @NonNull
+        public Person build() {
+            setPropertyString(PERSON_PROPERTY_ADDITIONAL_NAME,
+                    mAdditionalNames.toArray(new String[0]));
+            setPropertyDocument(PERSON_PROPERTY_CONTACT_POINT,
+                    mContactPoints.toArray(new ContactPoint[0]));
+            return new Person(super.build());
+        }
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/contactsindexer/ContactDataHandlerTest.java b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/ContactDataHandlerTest.java
new file mode 100644
index 0000000..181fc9b
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/ContactDataHandlerTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2022 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 src.com.android.server.appsearch.contactsindexer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentValues;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Data;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.appsearch.contactsindexer.ContactDataHandler;
+import com.android.server.appsearch.contactsindexer.PersonBuilderHelper;
+import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
+import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class ContactDataHandlerTest {
+    private static final String TEST_NAMESPACE = "TESTNAMESPACE";
+    private static final String TEST_ID = "TESTID";
+
+    private ContactDataHandler mContactDataHandler;
+    private Resources mResources;
+
+    // Make a MatrixCursor based on ContentValues and return it.
+    private static Cursor makeCursorFromContentValues(ContentValues values) {
+        MatrixCursor cursor = new MatrixCursor(values.keySet().toArray(new String[0]));
+        MatrixCursor.RowBuilder builder = cursor.newRow();
+        for (String key : values.keySet()) {
+            builder.add(key, values.get(key));
+        }
+        return cursor;
+    }
+
+    // Read from a single-line cursor and populate the data into the builderHelper.
+    private void convertRowToPerson(Cursor cursor, PersonBuilderHelper builderHelper) {
+        if (cursor != null) {
+            try {
+                assertThat(cursor.getCount()).isEqualTo(1);
+                assertThat(cursor.moveToFirst()).isTrue();
+                mContactDataHandler.convertCursorToPerson(cursor, builderHelper);
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mResources = ApplicationProvider.getApplicationContext().getResources();
+        mContactDataHandler =
+                new ContactDataHandler(mResources);
+    }
+
+    @Test
+    public void testConvertCurrentRowToPersonWhenCursorNotSet_expectException() {
+        PersonBuilderHelper builderHelper = new PersonBuilderHelper(
+                new Person.Builder("namespace", "id", "name"));
+        assertThrows(NullPointerException.class, () ->
+                mContactDataHandler.convertCursorToPerson(/*cursor=*/ null, builderHelper));
+    }
+
+    @Test
+    public void testConvertCurrentRowToPerson_email() {
+        int type = 1; // Home
+        String name = "name";
+        String address = "emailAddress@google.com";
+        String label = "Home";
+        ContentValues values = new ContentValues();
+        values.put(Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE);
+        values.put(CommonDataKinds.Email.ADDRESS, address);
+        values.put(CommonDataKinds.Email.TYPE, type);
+        values.put(CommonDataKinds.Email.LABEL, label);
+        Cursor cursor = makeCursorFromContentValues(values);
+
+        Person personExpected = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID, name).setCreationTimestampMillis(
+                        0)).addEmailToPerson(
+                CommonDataKinds.Email.getTypeLabel(mResources, type, label).toString(),
+                address).buildPerson();
+
+        PersonBuilderHelper helperTested = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                        name).setCreationTimestampMillis(0));
+        convertRowToPerson(cursor, helperTested);
+        Person personTested = helperTested.buildPerson();
+
+        ContactPoint[] contactPoints = personTested.getContactPoints();
+        assertThat(contactPoints.length).isEqualTo(1);
+        assertThat(contactPoints[0].getLabel()).isEqualTo(label);
+        assertThat(contactPoints[0].getEmails().length).isEqualTo(1);
+        assertThat(contactPoints[0].getEmails()[0]).isEqualTo(address);
+        TestUtils.assertEquals(personTested, personExpected);
+    }
+
+    @Test
+    public void testConvertCurrentRowToPerson_nickName() {
+        String name = "name";
+        String nick = "nickName";
+        ContentValues values = new ContentValues();
+        values.put(Data.MIMETYPE, CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
+        values.put(CommonDataKinds.Nickname.NAME, nick);
+        Cursor cursor = makeCursorFromContentValues(values);
+
+        Person personExpected = new Person.Builder(TEST_NAMESPACE, TEST_ID, name)
+                .setCreationTimestampMillis(0)
+                .addAdditionalName(nick)
+                .build();
+
+        PersonBuilderHelper helperTested = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                        name).setCreationTimestampMillis(0));
+        convertRowToPerson(cursor, helperTested);
+        Person personTested = helperTested.buildPerson();
+
+        String[] additionalNames = personTested.getAdditionalNames();
+        assertThat(additionalNames.length).isEqualTo(1);
+        assertThat(additionalNames[0]).isEqualTo(nick);
+        TestUtils.assertEquals(personTested, personExpected);
+    }
+
+    @Test
+    public void testConvertCurrentRowToPerson_phone() {
+        String name = "name";
+        String number = "phoneNumber";
+        int type = 1; // Home
+        String label = "Home";
+        ContentValues values = new ContentValues();
+        values.put(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+        values.put(CommonDataKinds.Phone.NUMBER, number);
+        values.put(CommonDataKinds.Phone.TYPE, type);
+        values.put(CommonDataKinds.Phone.LABEL, label);
+        Cursor cursor = makeCursorFromContentValues(values);
+
+        Person personExpected = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID, name).setCreationTimestampMillis(
+                        0)).addPhoneToPerson(
+                CommonDataKinds.Phone.getTypeLabel(mResources, type, label).toString(),
+                number).buildPerson();
+
+        PersonBuilderHelper helperTested = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                        name).setCreationTimestampMillis(0));
+        convertRowToPerson(cursor, helperTested);
+        Person personTested = helperTested.buildPerson();
+
+        ContactPoint[] contactPoints = personTested.getContactPoints();
+        assertThat(contactPoints.length).isEqualTo(1);
+        assertThat(contactPoints[0].getLabel()).isEqualTo(label);
+        assertThat(contactPoints[0].getPhones().length).isEqualTo(1);
+        assertThat(contactPoints[0].getPhones()[0]).isEqualTo(number);
+        TestUtils.assertEquals(personTested, personExpected);
+    }
+
+    @Test
+    public void testConvertCurrentRowToPerson_postal() {
+        String name = "name";
+        int type = 1; // Home
+        String postal = "structuredPostalFormattedAddress";
+        String label = "Home";
+        ContentValues values = new ContentValues();
+        values.put(Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
+        values.put(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, postal);
+        values.put(CommonDataKinds.StructuredPostal.TYPE, type);
+        values.put(CommonDataKinds.StructuredPostal.LABEL, label);
+        Cursor cursor = makeCursorFromContentValues(values);
+
+        Person personExpected = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID, name).setCreationTimestampMillis(
+                        0)).addAddressToPerson(
+                CommonDataKinds.StructuredPostal.getTypeLabel(mResources, type, label)
+                        .toString(), postal).buildPerson();
+
+        PersonBuilderHelper helperTested = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                        name).setCreationTimestampMillis(0));
+        convertRowToPerson(cursor, helperTested);
+        Person personTested = helperTested.buildPerson();
+
+        ContactPoint[] contactPoints = personTested.getContactPoints();
+        assertThat(contactPoints.length).isEqualTo(1);
+        assertThat(contactPoints[0].getLabel()).isEqualTo(label);
+        assertThat(contactPoints[0].getAddresses().length).isEqualTo(1);
+        assertThat(contactPoints[0].getAddresses()[0]).isEqualTo(postal);
+        TestUtils.assertEquals(personTested, personExpected);
+    }
+
+    @Test
+    public void testConvertCurrentRowToPerson_name() {
+        String name = "name";
+        String rawContactId = "raw" + TEST_ID;
+        String givenName = "structuredNameGivenName" + TEST_ID;
+        String middleName = "structuredNameMiddleName" + TEST_ID;
+        String familyName = "structuredNameFamilyName" + TEST_ID;
+        ContentValues values = new ContentValues();
+        values.put(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
+        values.put(Data.RAW_CONTACT_ID, rawContactId);
+        values.put(Data.NAME_RAW_CONTACT_ID, rawContactId);
+        values.put(CommonDataKinds.StructuredName.GIVEN_NAME, givenName);
+        values.put(CommonDataKinds.StructuredName.MIDDLE_NAME, middleName);
+        values.put(CommonDataKinds.StructuredName.FAMILY_NAME, familyName);
+        Cursor cursor = makeCursorFromContentValues(values);
+
+        Person personExpected = new Person.Builder(TEST_NAMESPACE, TEST_ID, name)
+                .setCreationTimestampMillis(0)
+                .setGivenName(givenName)
+                .setMiddleName(middleName)
+                .setFamilyName(familyName)
+                .build();
+
+        PersonBuilderHelper helperTested = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                        name).setCreationTimestampMillis(0));
+        convertRowToPerson(cursor, helperTested);
+        Person personTested = helperTested.buildPerson();
+
+        assertThat(personTested.getGivenName()).isEqualTo(givenName);
+        assertThat(personTested.getMiddleName()).isEqualTo(middleName);
+        assertThat(personTested.getFamilyName()).isEqualTo(familyName);
+        TestUtils.assertEquals(personTested, personExpected);
+    }
+
+    @Test
+    public void testHandleCurrentRowWithUnknownMimeType() {
+        // Change the Mimetype of StructuredName.
+        String name = "name";
+        MatrixCursor cursor = new MatrixCursor(
+                new String[]{Data.MIMETYPE, CommonDataKinds.StructuredName.GIVEN_NAME});
+        cursor.newRow().add("testUnknownMimeType", "testGivenName");
+
+        Person personExpected = new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                name).setCreationTimestampMillis(0).build();
+        PersonBuilderHelper helperTested = new PersonBuilderHelper(
+                new Person.Builder(TEST_NAMESPACE, TEST_ID,
+                        name).setCreationTimestampMillis(0));
+        convertRowToPerson(cursor, helperTested);
+        Person personTested = helperTested.buildPerson();
+
+        // Couldn't read values correctly from an unknown mime type.
+        assertThat(personTested.getGivenName()).isNull();
+        TestUtils.assertEquals(personTested, personExpected);
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/contactsindexer/TestUtils.java b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/TestUtils.java
new file mode 100644
index 0000000..c37a504
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/TestUtils.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 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 src.com.android.server.appsearch.contactsindexer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.NonNull;
+
+import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
+import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
+
+import java.util.Objects;
+
+class TestUtils {
+    static void assertEquals(@NonNull ContactPoint actual,
+            @NonNull ContactPoint expected) {
+        Objects.requireNonNull(actual);
+        Objects.requireNonNull(expected);
+
+        if (actual == expected) {
+            return;
+        }
+
+        assertThat(actual.getId()).isEqualTo(expected.getId());
+        assertThat(actual.getLabel()).isEqualTo(expected.getLabel());
+        assertThat(actual.getAppIds()).isEqualTo(expected.getAppIds());
+        assertThat(actual.getEmails()).isEqualTo(expected.getEmails());
+        assertThat(actual.getAddresses()).isEqualTo(expected.getAddresses());
+        assertThat(actual.getPhones()).isEqualTo(expected.getPhones());
+    }
+
+    static void assertEquals(@NonNull Person actual, @NonNull Person expected) {
+        Objects.requireNonNull(actual);
+        Objects.requireNonNull(expected);
+
+        if (actual == expected) {
+            return;
+        }
+
+        assertThat(actual.getId()).isEqualTo(expected.getId());
+        assertThat(actual.getName()).isEqualTo(expected.getName());
+        assertThat(actual.getGivenName()).isEqualTo(expected.getGivenName());
+        assertThat(actual.getMiddleName()).isEqualTo(expected.getMiddleName());
+        assertThat(actual.getFamilyName()).isEqualTo(expected.getFamilyName());
+        assertThat(actual.getExternalUri()).isEqualTo(expected.getExternalUri());
+        assertThat(actual.getImageUri()).isEqualTo(expected.getImageUri());
+        assertThat(actual.isImportant()).isEqualTo(expected.isImportant());
+        assertThat(actual.isBot()).isEqualTo(expected.isBot());
+        assertThat(actual.getAdditionalNames()).isEqualTo(expected.getAdditionalNames());
+        // Compare two contact point arrays. We can't directly use assert(genericDoc1).isEqualTo
+        // (genericDoc2) since the creationTimestamps are different, and they can't easily be
+        // reset to 0.
+        ContactPoint[] contactPointsActual = actual.getContactPoints();
+        ContactPoint[] contactPointsExpected = expected.getContactPoints();
+        assertThat(contactPointsActual.length).isEqualTo(contactPointsExpected.length);
+        for (int i = 0; i < contactPointsActual.length; ++i) {
+            assertEquals(contactPointsActual[i], contactPointsExpected[i]);
+        }
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPointTest.java b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPointTest.java
new file mode 100644
index 0000000..6030e56
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPointTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 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 src.com.android.server.appsearch.contactsindexer.appsearchtypes;
+
+import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
+
+import com.google.common.collect.ImmutableList;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class ContactPointTest {
+    @Test
+    public void testBuilder() {
+        String namespace = "";
+        String id = "id";
+        String label = "label";
+        List<String> emails = ImmutableList.of("email1", "email2");
+        List<String> addresses = ImmutableList.of("addr1", "addr2", "addr3");
+        List<String> telephones = ImmutableList.of("phone1");
+        List<String> appIds = ImmutableList.of("appId1", "appId2", "appId3");
+
+        ContactPoint.Builder contactPointBuilder =
+                new ContactPoint.Builder(namespace, id, label);
+        for (String email : emails) {
+            contactPointBuilder.addEmail(email);
+        }
+        for (String address : addresses) {
+            contactPointBuilder.addAddress(address);
+        }
+        for (String telephone : telephones) {
+            contactPointBuilder.addPhone(telephone);
+        }
+        for (String appId : appIds) {
+            contactPointBuilder.addAppId(appId);
+        }
+        ContactPoint contactPoint = contactPointBuilder.build();
+
+        assertThat(contactPoint.getId()).isEqualTo(id);
+        assertThat(contactPoint.getLabel()).isEqualTo(label);
+        assertThat(contactPoint.getEmails()).isEqualTo(emails.toArray(new String[0]));
+        assertThat(contactPoint.getAddresses()).isEqualTo(addresses.toArray(new String[0]));
+        assertThat(contactPoint.getPhones()).isEqualTo(telephones.toArray(new String[0]));
+        assertThat(contactPoint.getAppIds()).isEqualTo(appIds.toArray(new String[0]));
+    }
+}
\ No newline at end of file
diff --git a/testing/servicestests/src/com/android/server/appsearch/contactsindexer/appsearchtypes/PersonTest.java b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/appsearchtypes/PersonTest.java
new file mode 100644
index 0000000..18042f7
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/contactsindexer/appsearchtypes/PersonTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2022 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 src.com.android.server.appsearch.contactsindexer.appsearchtypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+
+import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
+import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class PersonTest {
+    @Test
+    public void testBuilder() {
+        String namespace = "namespace";
+        String id = "id";
+        String name = "name";
+        String givenName = "givenName";
+        String middleName = "middleName";
+        String lastName = "lastName";
+        Uri externalUri = Uri.parse("http://external.com");
+        Uri imageUri = Uri.parse("http://image.com");
+        List<String> additionalNames = Arrays.asList("name1", "name2");
+        boolean isImportant = true;
+        boolean isBot = true;
+        ContactPoint contact1 = new ContactPoint.Builder(namespace, id + "1", "Home")
+                .addAddress("addr1")
+                .addPhone("phone1")
+                .addEmail("email1")
+                .addAppId("appId1")
+                .build();
+        ContactPoint contact2 = new ContactPoint.Builder(namespace, id + "2", "Work")
+                .addAddress("addr2")
+                .addPhone("phone2")
+                .addEmail("email2")
+                .addAppId("appId2")
+                .build();
+
+        Person person = new Person.Builder(namespace, id, name)
+                .setGivenName(givenName)
+                .setMiddleName(middleName)
+                .setFamilyName(lastName)
+                .setExternalUri(externalUri)
+                .setImageUri(imageUri)
+                .addAdditionalName(additionalNames.get(0))
+                .addAdditionalName(additionalNames.get(1))
+                .setIsImportant(isImportant)
+                .setIsBot(isBot)
+                .addContactPoint(contact1)
+                .addContactPoint(contact2)
+                .build();
+
+        assertThat(person.getName()).isEqualTo(name);
+        assertThat(person.getGivenName()).isEqualTo(givenName);
+        assertThat(person.getMiddleName()).isEqualTo(middleName);
+        assertThat(person.getFamilyName()).isEqualTo(lastName);
+        assertThat(person.getExternalUri().toString()).isEqualTo(externalUri.toString());
+        assertThat(person.getImageUri().toString()).isEqualTo(imageUri.toString());
+        assertThat(person.getAdditionalNames().length).isEqualTo(2);
+        assertThat(person.getAdditionalNames()[0]).isEqualTo(additionalNames.get(0));
+        assertThat(person.getAdditionalNames()[1]).isEqualTo(additionalNames.get(1));
+        assertThat(person.getContactPoints().length).isEqualTo(2);
+        assertThat(person.getContactPoints()[0]).isEqualTo(contact1);
+        assertThat(person.getContactPoints()[1]).isEqualTo(contact2);
+    }
+}
\ No newline at end of file