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