Merge tag 'android-13.0.0_r52' into int/13/fp3

Android 13.0.0 Release 52 (TQ3A.230605.012)

* tag 'android-13.0.0_r52':
  Remove indentation for Person.toString

Change-Id: I7182a96809ca9b28e01348529b939876b17aedb9
diff --git a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
index 027fea3..be9ac4a 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
@@ -17,23 +17,27 @@
 package com.android.server.appsearch.contactsindexer;
 
 import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.util.IndentingStringBuilder;
 import android.app.appsearch.util.LogUtil;
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
 
+import java.lang.reflect.Array;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
-import com.android.internal.util.Preconditions;
-
 /**
  * Helper class to help build the {@link Person}.
  *
@@ -211,7 +215,99 @@
         Objects.requireNonNull(person);
 
         MessageDigest md = MessageDigest.getInstance("MD5");
-        md.update(person.toString().getBytes(StandardCharsets.UTF_8));
+        md.update(generateFingerprintStringForPerson(person).getBytes(StandardCharsets.UTF_8));
         return md.digest();
     }
+
+    @VisibleForTesting
+    /** Returns a string presentation of {@link Person} for fingerprinting. */
+    static String generateFingerprintStringForPerson(@NonNull Person person) {
+        Objects.requireNonNull(person);
+
+        StringBuilder builder = new StringBuilder();
+        appendGenericDocumentString(person, builder);
+        return builder.toString();
+    }
+
+    /**
+     * Appends string representation of a {@link GenericDocument} to the {@link StringBuilder}.
+     *
+     * <p>This is basically same as
+     * {@link GenericDocument#appendGenericDocumentString(IndentingStringBuilder)}, but only keep
+     * the properties part and use a normal {@link StringBuilder} to skip the indentation.
+     */
+    private static void appendGenericDocumentString(@NonNull GenericDocument doc,
+            @NonNull StringBuilder builder) {
+        Objects.requireNonNull(doc);
+        Objects.requireNonNull(builder);
+
+        builder.append("properties: {\n");
+        String[] sortedProperties = doc.getPropertyNames().toArray(new String[0]);
+        Arrays.sort(sortedProperties);
+        for (int i = 0; i < sortedProperties.length; i++) {
+            Object property = Objects.requireNonNull(doc.getProperty(sortedProperties[i]));
+            appendPropertyString(sortedProperties[i], property, builder);
+            if (i != sortedProperties.length - 1) {
+                builder.append(",\n");
+            }
+        }
+        builder.append("\n");
+        builder.append("}");
+    }
+
+    /**
+     * Appends string representation of a {@link GenericDocument}'s property to the
+     * {@link StringBuilder}.
+     *
+     * <p>This is basically same as
+     * {@link GenericDocument#appendPropertyString(String, Object, IndentingStringBuilder)}, but
+     * use a normal {@link StringBuilder} to skip the indentation.
+     *
+     * <p>Here we still keep most of the formatting(e.g. '\n') to make sure we won't hit some
+     * possible corner cases. E.g. We will have "someProperty1: some\n Property2:..." instead of
+     * "someProperty1: someProperty2:". For latter, we can interpret it as empty string value for
+     * "someProperty1", with a different property name "someProperty2". In this case, the content is
+     * changed but fingerprint will remain same if we don't have that '\n'.
+     *
+     * <p>Plus, some basic formatting will make the testing more clear.
+     */
+    private static void appendPropertyString(
+            @NonNull String propertyName,
+            @NonNull Object property,
+            @NonNull StringBuilder builder) {
+        Objects.requireNonNull(propertyName);
+        Objects.requireNonNull(property);
+        Objects.requireNonNull(builder);
+
+        builder.append("\"").append(propertyName).append("\": [");
+        if (property instanceof GenericDocument[]) {
+            GenericDocument[] documentValues = (GenericDocument[]) property;
+            for (int i = 0; i < documentValues.length; ++i) {
+                builder.append("\n");
+                appendGenericDocumentString(documentValues[i], builder);
+                if (i != documentValues.length - 1) {
+                    builder.append(",");
+                }
+                builder.append("\n");
+            }
+            builder.append("]");
+        } else {
+            int propertyArrLength = Array.getLength(property);
+            for (int i = 0; i < propertyArrLength; i++) {
+                Object propertyElement = Array.get(property, i);
+                if (propertyElement instanceof String) {
+                    builder.append("\"").append((String) propertyElement).append("\"");
+                } else if (propertyElement instanceof byte[]) {
+                    builder.append(Arrays.toString((byte[]) propertyElement));
+                } else {
+                    builder.append(propertyElement.toString());
+                }
+                if (i != propertyArrLength - 1) {
+                    builder.append(", ");
+                } else {
+                    builder.append("]");
+                }
+            }
+        }
+    }
 }
diff --git a/testing/contactsindexertests/Android.bp b/testing/contactsindexertests/Android.bp
index 7683dac..4250e08 100644
--- a/testing/contactsindexertests/Android.bp
+++ b/testing/contactsindexertests/Android.bp
@@ -30,9 +30,9 @@
     ],
     libs: [
         "android.test.runner",
-        "framework-appsearch.impl",
         "android.test.mock",
         "android.test.base",
+        "framework-appsearch.impl",
     ],
     test_suites: [
         "general-tests",
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/PersonBuilderHelperTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/PersonBuilderHelperTest.java
index b52aacf..e2f63c3 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/PersonBuilderHelperTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/PersonBuilderHelperTest.java
@@ -16,14 +16,14 @@
 
 package com.android.server.appsearch.contactsindexer;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.net.Uri;
 
-import com.android.server.appsearch.contactsindexer.PersonBuilderHelper;
 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
 
-
-import static com.google.common.truth.Truth.assertThat;
+import com.google.common.collect.ImmutableList;
 
 import org.junit.Test;
 
@@ -278,4 +278,122 @@
         // Score should be set as base(1) + # of contactPoints + # of additionalNames.
         assertThat(person.getScore()).isEqualTo(6);
     }
+
+    @Test
+    public void testGenerateFingerprintStringForPerson() {
+        long creationTimestamp = 12345L;
+        String namespace = "namespace";
+        String id = "id";
+        int score = 3;
+        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");
+        byte[] fingerprint = "Hello world!".getBytes();
+        List<String> affiliations = ImmutableList.of("Org1", "Org2", "Org3");
+        List<String> relations = ImmutableList.of("relation1", "relation2");
+        boolean isImportant = true;
+        boolean isBot = true;
+        String note1 = "note";
+        String note2 = "note2";
+        ContactPoint contact1 = new ContactPoint.Builder(namespace, id + "1", "Home")
+                .setCreationTimestampMillis(creationTimestamp)
+                .addAddress("addr1")
+                .addPhone("phone1")
+                .addEmail("email1")
+                .addAppId("appId1")
+                .build();
+        ContactPoint contact2 = new ContactPoint.Builder(namespace, id + "2", "Work")
+                .setCreationTimestampMillis(creationTimestamp)
+                .addAddress("addr2")
+                .addPhone("phone2")
+                .addEmail("email2")
+                .addAppId("appId2")
+                .build();
+        ContactPoint contact3 = new ContactPoint.Builder(namespace, id + "3", "Other")
+                .setCreationTimestampMillis(creationTimestamp)
+                .addAddress("addr3")
+                .addPhone("phone3")
+                .addEmail("email3")
+                .addAppId("appId3")
+                .build();
+        List<String> additionalNames = ImmutableList.of("nickname", "phoneticName");
+        @Person.NameType
+        List<Long> additionalNameTypes = ImmutableList.of((long) Person.TYPE_NICKNAME,
+                (long) Person.TYPE_PHONETIC_NAME);
+        Person person = new Person.Builder(namespace, id, name)
+                .setCreationTimestampMillis(creationTimestamp)
+                .setScore(score)
+                .setGivenName(givenName)
+                .setMiddleName(middleName)
+                .setFamilyName(lastName)
+                .setExternalUri(externalUri)
+                .setImageUri(imageUri)
+                .addAdditionalName(additionalNameTypes.get(0), additionalNames.get(0))
+                .addAdditionalName(additionalNameTypes.get(1), additionalNames.get(1))
+                .addAffiliation(affiliations.get(0))
+                .addAffiliation(affiliations.get(1))
+                .addAffiliation(affiliations.get(2))
+                .addRelation(relations.get(0))
+                .addRelation(relations.get(1))
+                .setIsImportant(isImportant)
+                .setIsBot(isBot)
+                .addNote(note1)
+                .addNote(note2)
+                .setFingerprint(fingerprint)
+                .addContactPoint(contact1)
+                .addContactPoint(contact2)
+                .addContactPoint(contact3)
+                .build();
+
+        // Different from GenericDocument.toString, we will a get string representation without
+        // any indentation for Person.
+        String expected = "properties: {\n"
+                + "\"additionalNameTypes\": [1, 2],\n"
+                + "\"additionalNames\": [\"nickname\", \"phoneticName\"],\n"
+                + "\"affiliations\": [\"Org1\", \"Org2\", \"Org3\"],\n"
+                + "\"contactPoints\": [\n"
+                + "properties: {\n"
+                + "\"address\": [\"addr1\"],\n"
+                + "\"appId\": [\"appId1\"],\n"
+                + "\"email\": [\"email1\"],\n"
+                + "\"label\": [\"Home\"],\n"
+                + "\"telephone\": [\"phone1\"]\n"
+                + "},\n"
+                + "\n"
+                + "properties: {\n"
+                + "\"address\": [\"addr2\"],\n"
+                + "\"appId\": [\"appId2\"],\n"
+                + "\"email\": [\"email2\"],\n"
+                + "\"label\": [\"Work\"],\n"
+                + "\"telephone\": [\"phone2\"]\n"
+                + "},\n"
+                + "\n"
+                + "properties: {\n"
+                + "\"address\": [\"addr3\"],\n"
+                + "\"appId\": [\"appId3\"],\n"
+                + "\"email\": [\"email3\"],\n"
+                + "\"label\": [\"Other\"],\n"
+                + "\"telephone\": [\"phone3\"]\n"
+                + "}\n"
+                + "],\n"
+                + "\"externalUri\": [\"http://external.com\"],\n"
+                + "\"familyName\": [\"lastName\"],\n"
+                + "\"fingerprint\": [[72, 101, 108, 108, 111, 32, 119, 111, 114, 108, "
+                + "100, 33]],\n"
+                + "\"givenName\": [\"givenName\"],\n"
+                + "\"imageUri\": [\"http://image.com\"],\n"
+                + "\"isBot\": [true],\n"
+                + "\"isImportant\": [true],\n"
+                + "\"middleName\": [\"middleName\"],\n"
+                + "\"name\": [\"name\"],\n"
+                + "\"notes\": [\"note\", \"note2\"],\n"
+                + "\"relations\": [\"relation1\", \"relation2\"]\n"
+                + "}";
+
+        assertThat(PersonBuilderHelper.generateFingerprintStringForPerson(person)).isEqualTo(
+                expected);
+    }
 }