b/11435875. Add support for fileAs when uploading new contacts.

When a new contact is created on the device, we do not push a fileAs up to Exchange
and that causes problems when you view the contact via OWA to check the results.
The challenge is to not push a fileAs if one was already created (i.e. the contact
was created on the server). We handle this by actually syncing the fileAs from the
server and being smart enough not to overwrite if it exists on the upload. Generation
of the fileAs string is done in 1 of 2 methods.  The first is to use the alternative
display name stored in the RawContacts information for this contact.  The fallback
method is to generate a fileAs string using a combination of structured name fields
and available email addresses.  Unit tests are included.

Change-Id: I957071da758801d2e5c2799fc1f0b4fdbe0b4e4d
diff --git a/src/com/android/exchange/adapter/ContactsSyncParser.java b/src/com/android/exchange/adapter/ContactsSyncParser.java
index 3d74415..75ee50e 100644
--- a/src/com/android/exchange/adapter/ContactsSyncParser.java
+++ b/src/com/android/exchange/adapter/ContactsSyncParser.java
@@ -279,6 +279,9 @@
                 case Tags.CONTACTS_ANNIVERSARY:
                     personal.anniversary = getValue();
                     break;
+                case Tags.CONTACTS_FILE_AS:
+                    personal.fileAs = getValue();
+                    break;
                 case Tags.CONTACTS_BIRTHDAY:
                     ops.addBirthday(entity, getValue());
                     break;
diff --git a/src/com/android/exchange/eas/EasSyncContacts.java b/src/com/android/exchange/eas/EasSyncContacts.java
index 3a6ffc0..8c4f970 100644
--- a/src/com/android/exchange/eas/EasSyncContacts.java
+++ b/src/com/android/exchange/eas/EasSyncContacts.java
@@ -30,6 +30,7 @@
 import com.android.emailcommon.TrafficFlags;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.utility.Utility;
 import com.android.exchange.Eas;
 import com.android.exchange.adapter.AbstractSyncParser;
 import com.android.exchange.adapter.ContactsSyncParser;
@@ -135,7 +136,7 @@
      * Data and constants for a Personal contact.
      */
     private static final class EasPersonal {
-            /** MIME type used when storing this in data table. */
+        /** MIME type used when storing this in data table. */
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
         public static final String ANNIVERSARY = "data2";
         public static final String FILE_AS = "data4";
@@ -250,6 +251,7 @@
         s.tag(Tags.CONTACTS_BIRTHDAY);
         s.tag(Tags.CONTACTS_WEBPAGE);
         s.tag(Tags.CONTACTS_PICTURE);
+        s.tag(Tags.CONTACTS_FILE_AS);
         s.end(); // SYNC_SUPPORTED
     }
 
@@ -318,21 +320,43 @@
     }
 
     /**
+     * Helper function to safely extract a string from a content value.
+     * @param cv The {@link ContentValues} that contains the values
+     * @param column The column name in cv for the data
+     * @return The data in the column or null if it doesn't exist or is empty.
+     * @throws IOException
+     */
+    public static String tryGetStringData(final ContentValues cv, final String column)
+            throws IOException {
+        if ((cv == null) || (column == null)) {
+            return null;
+        }
+        if (cv.containsKey(column)) {
+            final String value = cv.getAsString(column);
+            if (!TextUtils.isEmpty(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Helper to add a string to the upsync.
      * @param s The {@link Serializer} for this sync request
      * @param cv The {@link ContentValues} with the data for this string.
      * @param column The column name in cv to find the string.
      * @param tag The tag to use when adding to s.
+     * @return Whether or not the field was actually set.
      * @throws IOException
      */
-    private static void sendStringData(final Serializer s, final ContentValues cv,
+    private static boolean sendStringData(final Serializer s, final ContentValues cv,
             final String column, final int tag) throws IOException {
-        if (cv.containsKey(column)) {
-            final String value = cv.getAsString(column);
-            if (!TextUtils.isEmpty(value)) {
-                s.data(tag, value);
-            }
+        final String dataValue = tryGetStringData(cv, column);
+        if (dataValue != null) {
+            s.data(tag, dataValue);
+            return true;
         }
+        return false;
     }
 
 
@@ -451,7 +475,17 @@
     private static void sendPersonal(final Serializer s, final ContentValues cv)
             throws IOException {
         sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
-        sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
+    }
+
+    /**
+     * Add contact file_as info to the upsync.
+     * @param s The {@link Serializer} for this sync request.
+     * @param cv The {@link ContentValues} with the data for this personal contact.
+     * @throws IOException
+     */
+    private static boolean trySendFileAs(final Serializer s, final ContentValues cv)
+            throws IOException {
+        return sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
     }
 
     /**
@@ -712,6 +746,60 @@
         }
     }
 
+    /**
+     * Generate a default fileAs string for this contact using name and email data.
+     * Note that the user can change this in Outlook/OWA if it is not correct for them but
+     * we need to send something or else Exchange will not display a name with the contact.
+     * @param nameValues Name information to use in generating the fileAs string
+     * @param emailValues Email information to use to generate the fileAs string
+     * @return A valid fileAs string or null
+     */
+    public static String generateFileAs(final ContentValues nameValues,
+            final ArrayList<ContentValues> emailValues) throws IOException {
+        // TODO: Is there a better way of generating a default file_as that will make people
+        // happy everywhere in the world? Should we read the sort settings of the People app?
+        final String firstName = tryGetStringData(nameValues, StructuredName.GIVEN_NAME);
+        final String lastName = tryGetStringData(nameValues, StructuredName.FAMILY_NAME);;
+        final String middleName = tryGetStringData(nameValues, StructuredName.MIDDLE_NAME);;
+        final String nameSuffix = tryGetStringData(nameValues, StructuredName.SUFFIX);
+
+        if (firstName == null && lastName == null) {
+            if (emailValues == null) {
+                // Bad name, bad email list...not much we can do about it.
+                return null;
+            }
+            // The name fields didn't yield anything valuable, let's generate a file as
+            // via the email addresses that were passed in.
+            for (final ContentValues cv : emailValues) {
+                final String emailAddr = tryGetStringData(cv, Email.DATA);
+                if (emailAddr != null) {
+                    return emailAddr;
+                }
+            }
+            return null;
+        }
+        // Let's try to construct this with the name only. The format is this:
+        // LastName nameSuffix, FirstName MiddleName
+        // nameSuffix is only applied if lastName exists.
+        final StringBuilder builder = new StringBuilder();
+        if (lastName != null) {
+            builder.append(lastName);
+            if (nameSuffix != null) {
+                builder.append(" " + nameSuffix);
+            }
+            builder.append(", ");
+        }
+        if (firstName != null) {
+            builder.append(firstName + " ");
+        }
+        if (middleName != null) {
+            builder.append(middleName);
+        }
+        // We might leave a trailing space, so let's trim the string here.
+        return builder.toString().trim();
+    }
+
+
     private void setUpsyncCommands(final Serializer s, final ContentResolver cr,
             final Account account, final Mailbox mailbox, final double protocolVersion)
             throws IOException {
@@ -726,6 +814,7 @@
         final EntityIterator ei = ContactsContract.RawContacts.newEntityIterator(
                 cr.query(uri, null, ContactsContract.RawContacts.DIRTY + "=1", null, null));
         final ContentValues cidValues = new ContentValues();
+        boolean hasSetFileAs = false;
         try {
             boolean first = true;
             final Uri rawContactUri = addCallerIsSyncAdapterParameter(
@@ -734,8 +823,7 @@
                 final Entity entity = ei.next();
                 // For each of these entities, create the change commands
                 final ContentValues entityValues = entity.getEntityValues();
-                final String serverId =
-                        entityValues.getAsString(ContactsContract.RawContacts.SOURCE_ID);
+                String serverId = entityValues.getAsString(ContactsContract.RawContacts.SOURCE_ID);
                 final ArrayList<Integer> groupIds = new ArrayList<Integer>();
                 if (first) {
                     s.start(Tags.SYNC_COMMANDS);
@@ -746,6 +834,8 @@
                     // This is a new contact; create a clientId
                     final String clientId =
                             "new_" + mailbox.mId + '_' + System.currentTimeMillis();
+                    // We need to server id to look up the fileAs string.
+                    serverId = clientId;
                     LogUtils.d(TAG, "Creating new contact with clientId: %s", clientId);
                     s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
                     // And save it in the raw contact
@@ -763,6 +853,9 @@
                     }
                     LogUtils.d(TAG, "Upsync change to contact with serverId: %s", serverId);
                     s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+                    // We don't need to set the file has because it is not a new contact
+                    // i.e. it should have the file_as if it needs one.
+                    hasSetFileAs = true;
                 }
                 s.start(Tags.SYNC_APPLICATION_DATA);
                 // Write out the data here
@@ -773,6 +866,7 @@
                 // TODO: How is this name supposed to be formed?
                 String displayName = null;
                 final ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
+                ContentValues nameValues = null;
                 for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
                     final ContentValues cv = ncv.values;
                     final String mimeType = cv.getAsString(ContactsContract.Data.MIMETYPE);
@@ -788,6 +882,7 @@
                         sendWebpage(s, cv);
                     } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
                         sendPersonal(s, cv);
+                        hasSetFileAs = trySendFileAs(s, cv);
                     } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
                         sendPhone(s, cv, workPhoneCount, homePhoneCount);
                         int type = cv.getAsInteger(Phone.TYPE);
@@ -797,6 +892,8 @@
                         sendRelation(s, cv);
                     } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
                         sendStructuredName(s, cv);
+                        // Stash names here
+                        nameValues = cv;
                     } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
                         sendStructuredPostal(s, cv);
                     } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
@@ -819,13 +916,40 @@
                         LogUtils.i(TAG, "Contacts upsync, unknown data: %s", mimeType);
                     }
                 }
-
                 // We do the email rows last, because we need to make sure we've found the
                 // displayName (if one exists); this would be in a StructuredName rnow
                 for (final ContentValues cv: emailValues) {
                     sendEmail(s, cv, emailCount++, displayName, protocolVersion);
                 }
-
+                // For Exchange, we need to make sure that we provide a fileAs string because
+                // it is used as the display name for the contact in some views.
+                if (!hasSetFileAs) {
+                    String fileAs = null;
+                    // Let's go grab the display_name_alt info for this contact and use
+                    // that as the default fileAs.
+                    final Cursor c = cr.query(ContactsContract.RawContacts.CONTENT_URI,
+                            new String[]{ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE},
+                            ContactsContract.RawContacts.SYNC1 + "=?",
+                            new String[]{String.valueOf(serverId)}, null);
+                    try {
+                        while (c.moveToNext()) {
+                            final String contentValue = c.getString(0);
+                            if ((contentValue != null) && (!TextUtils.isEmpty(contentValue))) {
+                                fileAs = contentValue;
+                                break;
+                            }
+                        }
+                    } finally {
+                        c.close();
+                    }
+                    if (fileAs == null) {
+                        // Just in case that property did not exist, we can generate our own
+                        // rudimentary string that uses a combination of structured name fields or
+                        // email addresses depending on what is available.
+                        fileAs = generateFileAs(nameValues, emailValues);
+                    }
+                    s.data(Tags.CONTACTS_FILE_AS, fileAs);
+                }
                 // Now, we'll send up groups, if any
                 if (!groupIds.isEmpty()) {
                     boolean groupFirst = true;
diff --git a/tests/src/com/android/exchange/eas/EasSyncContactsTests.java b/tests/src/com/android/exchange/eas/EasSyncContactsTests.java
new file mode 100644
index 0000000..8958d04
--- /dev/null
+++ b/tests/src/com/android/exchange/eas/EasSyncContactsTests.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2014 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.exchange.eas;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.exchange.utility.ExchangeTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.eas.EasSyncContactsTests exchange
+ */
+@SmallTest
+public class EasSyncContactsTests extends ExchangeTestCase {
+
+    // Return null.
+    public void testTryGetStringDataEverythingNull() throws IOException {
+        final String result = EasSyncContacts.tryGetStringData(null, null);
+        assertNull(result);
+    }
+
+    // Return null.
+    public void testTryGetStringDataNullContentValues() throws IOException {
+        final String result = EasSyncContacts.tryGetStringData(null, "TestColumn");
+        assertNull(result);
+    }
+
+    // Return null.
+    public void testTryGetStringDataNullColumnName() throws IOException {
+        final ContentValues contentValues = new ContentValues();
+        contentValues.put("test_column", "test_value");
+        final String result = EasSyncContacts.tryGetStringData(contentValues, null);
+        assertNull(result);
+    }
+
+    // Return null.
+    public void testTryGetStringDataDoesNotContainColumn() throws IOException {
+        final ContentValues contentValues = new ContentValues();
+        contentValues.put("test_column", "test_value");
+        final String result = EasSyncContacts.tryGetStringData(contentValues, "does_not_exist");
+        assertNull(result);
+    }
+
+    // Return null.
+    public void testTryGetStringDataEmptyColumnValue() throws IOException {
+        final String columnName = "test_column";
+        final ContentValues contentValues = new ContentValues();
+        contentValues.put(columnName, "");
+        final String result = EasSyncContacts.tryGetStringData(contentValues, columnName);
+        assertNull(result);
+    }
+
+    // Return the data type forced to be a string.
+    // TODO: Test other data types.
+    public void testTryGetStringDataWrongType() throws IOException {
+        final String columnName = "test_column";
+        final Integer columnValue = new Integer(10);
+        final String columnValueAsString = columnValue.toString();
+        final ContentValues contentValues = new ContentValues();
+        contentValues.put(columnName, columnValue);
+        final String result = EasSyncContacts.tryGetStringData(contentValues, columnName);
+        assert(result.equals(columnValueAsString));
+    }
+
+    // Return the value as a string.
+    public void testTryGetStringDataSuccess() throws IOException {
+        final String columnName = "test_column";
+        final String columnValue = "test_value";
+        final ContentValues contentValues = new ContentValues();
+        contentValues.put(columnName, columnValue);
+        final String result = EasSyncContacts.tryGetStringData(contentValues, columnName);
+        assertTrue(result.equals(columnValue));
+    }
+
+    // Return null.
+    public void testGenerateFileAsNullParameters() throws IOException {
+        final String result = EasSyncContacts.generateFileAs(null, null);
+        assertNull(result);
+    }
+
+    // Should still return null because there is no name and no email.
+    public void testGenerateFileAsNullNameValuesAndEmptyList() throws IOException {
+        final ArrayList<ContentValues> emailList = new ArrayList<ContentValues>();
+        final String result = EasSyncContacts.generateFileAs(null, emailList);
+        assertNull(result);
+    }
+
+    // Just return the first email address that was passed in.
+    public void testGenerateFileAsNullNameValues() throws IOException {
+        final ArrayList<ContentValues> emailList = new ArrayList<ContentValues>();
+        final ContentValues emailValue = new ContentValues();
+        final String emailString = "anthonylee@google.com";
+        emailValue.put(Email.DATA, emailString);
+        emailList.add(emailValue);
+        final String result = EasSyncContacts.generateFileAs(null, emailList);
+        assertTrue(result.equals(emailString));
+    }
+
+    // Just return the formatted name.
+    public void testGenerateFileAsNullEmailValues() throws IOException {
+        final ContentValues nameValues = new ContentValues();
+        final String firstName = "Joe";
+        final String middleName = "Bob";
+        final String lastName = "Smith";
+        final String suffix = "Jr.";
+        nameValues.put(StructuredName.GIVEN_NAME, firstName);
+        nameValues.put(StructuredName.FAMILY_NAME, lastName);
+        nameValues.put(StructuredName.MIDDLE_NAME, middleName);
+        nameValues.put(StructuredName.SUFFIX, suffix);
+        final String result = EasSyncContacts.generateFileAs(nameValues, null);
+        final String generatedName = lastName + " " + suffix + ", " + firstName + " " + middleName;
+        assertTrue(generatedName.equals(result));
+    }
+
+    // This will generate a string that is similar to the full string but with no first name.
+    public void testGenerateFileAsNullFirstName() throws IOException {
+        final ContentValues nameValues = new ContentValues();
+        final String middleName = "Bob";
+        final String lastName = "Smith";
+        final String suffix = "Jr.";
+        nameValues.put(StructuredName.FAMILY_NAME, lastName);
+        nameValues.put(StructuredName.MIDDLE_NAME, middleName);
+        nameValues.put(StructuredName.SUFFIX, suffix);
+        final String result = EasSyncContacts.generateFileAs(nameValues, null);
+        final String generatedName = lastName + " " + suffix + ", " + middleName;
+        assertTrue(generatedName.equals(result));
+    }
+
+    // This will generate a string that is missing both the last name and the suffix.
+    public void testGenerateFileAsNullLastName() throws IOException {
+        final ContentValues nameValues = new ContentValues();
+        final String firstName = "Joe";
+        final String middleName = "Bob";
+        final String suffix = "Jr.";
+        nameValues.put(StructuredName.GIVEN_NAME, firstName);
+        nameValues.put(StructuredName.MIDDLE_NAME, middleName);
+        nameValues.put(StructuredName.SUFFIX, suffix);
+        final String result = EasSyncContacts.generateFileAs(nameValues, null);
+        final String generatedName = firstName + " " + middleName;
+        assertTrue(generatedName.equals(result));
+    }
+
+    // This will generate a string that is similar to the full name but missing the middle name.
+    public void testGenerateFileAsNullMiddleName() throws IOException {
+        final ContentValues nameValues = new ContentValues();
+        final String firstName = "Joe";
+        final String lastName = "Smith";
+        final String suffix = "Jr.";
+        nameValues.put(StructuredName.GIVEN_NAME, firstName);
+        nameValues.put(StructuredName.FAMILY_NAME, lastName);
+        nameValues.put(StructuredName.SUFFIX, suffix);
+        final String result = EasSyncContacts.generateFileAs(nameValues, null);
+        final String generatedName = lastName + " " + suffix + ", " + firstName;
+        assertTrue(generatedName.equals(result));
+    }
+
+    // Similar to the full name but no suffix.
+    public void testGenerateFileAsNullSuffix() throws IOException {
+        final ContentValues nameValues = new ContentValues();
+        final String firstName = "Joe";
+        final String middleName = "Bob";
+        final String lastName = "Smith";
+        nameValues.put(StructuredName.GIVEN_NAME, firstName);
+        nameValues.put(StructuredName.FAMILY_NAME, lastName);
+        nameValues.put(StructuredName.MIDDLE_NAME, middleName);
+        final String result = EasSyncContacts.generateFileAs(nameValues, null);
+        final String generatedName = lastName + ", " + firstName + " " + middleName;
+        assertTrue(generatedName.equals(result));
+    }
+}