Adding support for a full-contact (entities) query.

Change-Id: Ib1cdd998dcc4f60124dbc37a42fd61ee0f6802fd
diff --git a/src/com/android/exchange/adapter/GalParser.java b/src/com/android/exchange/adapter/GalParser.java
index fffad70..94c8cdf 100644
--- a/src/com/android/exchange/adapter/GalParser.java
+++ b/src/com/android/exchange/adapter/GalParser.java
@@ -17,6 +17,7 @@
 
 import com.android.exchange.EasSyncService;
 import com.android.exchange.provider.GalResult;
+import com.android.exchange.provider.GalResult.GalData;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -52,22 +53,54 @@
          return mGalResult.total > 0;
      }
 
-     public void parseProperties(GalResult galResult) throws IOException {
-         String displayName = null;
-         String email = null;
-         while (nextTag(Tags.SEARCH_STORE) != END) {
-             if (tag == Tags.GAL_DISPLAY_NAME) {
-                 displayName = getValue();
-             } else if (tag == Tags.GAL_EMAIL_ADDRESS) {
-                 email = getValue();
-             } else {
-                 skipTag();
-             }
-         }
-         if (displayName != null && email != null) {
-             galResult.addGalData(0, displayName, email);
-         }
-     }
+    public void parseProperties(GalResult galResult) throws IOException {
+        GalData galData = new GalData();
+        while (nextTag(Tags.SEARCH_STORE) != END) {
+            switch(tag) {
+                // Display name and email address use both legacy and new code for galData
+                case Tags.GAL_DISPLAY_NAME: 
+                    String displayName = getValue();
+                    galData.put(GalData.DISPLAY_NAME, displayName);
+                    galData.displayName = displayName;
+                    break;
+                case Tags.GAL_EMAIL_ADDRESS:
+                    String emailAddress = getValue();
+                    galData.put(GalData.EMAIL_ADDRESS, emailAddress);
+                    galData.emailAddress = emailAddress;
+                    break;
+                case Tags.GAL_PHONE:
+                    galData.put(GalData.WORK_PHONE, getValue());
+                    break;
+                case Tags.GAL_OFFICE:
+                    galData.put(GalData.OFFICE, getValue());
+                    break;
+                case Tags.GAL_TITLE:
+                    galData.put(GalData.TITLE, getValue());
+                    break;
+                case Tags.GAL_COMPANY:
+                    galData.put(GalData.COMPANY, getValue());
+                    break;
+                case Tags.GAL_ALIAS:
+                    galData.put(GalData.ALIAS, getValue());
+                    break;
+                case Tags.GAL_FIRST_NAME:
+                    galData.put(GalData.FIRST_NAME, getValue());
+                    break;
+                case Tags.GAL_LAST_NAME:
+                    galData.put(GalData.LAST_NAME, getValue());
+                    break;
+                case Tags.GAL_HOME_PHONE:
+                    galData.put(GalData.HOME_PHONE, getValue());
+                    break;
+                case Tags.GAL_MOBILE_PHONE:
+                    galData.put(GalData.MOBILE_PHONE, getValue());
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        galResult.addGalData(galData);
+    }
 
      public void parseResult(GalResult galResult) throws IOException {
          while (nextTag(Tags.SEARCH_STORE) != END) {
diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
index 5d6121f..6d68522 100644
--- a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
+++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.exchange.provider;
 
+import com.android.email.mail.PackedString;
 import com.android.email.provider.EmailContent.Account;
 import com.android.exchange.EasSyncService;
 import com.android.exchange.SyncManager;
@@ -29,8 +30,16 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Data;
 import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.List;
 
 /**
  * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
@@ -39,13 +48,21 @@
 public class ExchangeDirectoryProvider extends ContentProvider {
     public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.directory.provider";
 
+    private static final int DEFAULT_CONTACT_ID = 1;
+
     private static final int GAL_BASE = 0;
     private static final int GAL_FILTER = GAL_BASE;
+    private static final int GAL_CONTACT = GAL_BASE + 1;
+    private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 2;
 
     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 
     static {
         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities",
+                GAL_CONTACT);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
+                GAL_CONTACT_WITH_ID);
     }
 
     @Override
@@ -53,6 +70,96 @@
         return true;
     }
 
+    static class GalProjection {
+        final int size;
+        final HashMap<String, Integer> columnMap = new HashMap<String, Integer>();
+
+        GalProjection(String[] projection) {
+            size = projection.length;
+            for (int i = 0; i < projection.length; i++) {
+                columnMap.put(projection[i], i);
+            }
+        }
+    }
+
+    static class GalContactRow {
+        private final GalProjection mProjection;
+        private Object[] row;
+        static long dataId = 1;
+
+        GalContactRow(GalProjection projection, long contactId, String lookupKey,
+                String accountName, String displayName) {
+            this.mProjection = projection;
+            row = new Object[projection.size];
+
+            put(Contacts.Entity.CONTACT_ID, contactId);
+
+            // We only have one raw contact per aggregate, so they can have the same ID
+            put(Contacts.Entity.RAW_CONTACT_ID, contactId);
+            put(Contacts.Entity.DATA_ID, dataId++);
+
+            put(Contacts.DISPLAY_NAME, displayName);
+
+            // TODO alternative display name
+            put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+
+            put(RawContacts.ACCOUNT_TYPE, com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+            put(RawContacts.ACCOUNT_NAME, accountName);
+            put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
+            put(Data.IS_READ_ONLY, 1);
+        }
+
+        Object[] getRow () {
+            return row;
+        }
+
+        void put(String columnName, Object value) {
+            Integer integer = mProjection.columnMap.get(columnName);
+            if (integer != null) {
+                row[integer] = value;
+            } else {
+                System.out.println("Unsupported column: " + columnName);
+            }
+        }
+
+        static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
+                long contactId, String lookupKey, String accountName, String displayName,
+                String address) {
+            if (!TextUtils.isEmpty(address)) {
+                GalContactRow r = new GalContactRow(
+                        galProjection, contactId, lookupKey, accountName, displayName);
+                r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+                r.put(Email.TYPE, Email.TYPE_WORK);
+                r.put(Email.ADDRESS, address);
+                cursor.addRow(r.getRow());
+            }
+        }
+
+        static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
+                String lookupKey, String accountName, String displayName, int type, String number) {
+            if (!TextUtils.isEmpty(number)) {
+                GalContactRow r = new GalContactRow(
+                        projection, contactId, lookupKey, accountName, displayName);
+                r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+                r.put(Phone.TYPE, type);
+                r.put(Phone.NUMBER, number);
+                cursor.addRow(r.getRow());
+            }
+        }
+
+        public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
+                long contactId, String lookupKey, String accountName, String displayName,
+                String firstName, String lastName) {
+            GalContactRow r = new GalContactRow(
+                    galProjection, contactId, lookupKey, accountName, displayName);
+            r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+            r.put(StructuredName.GIVEN_NAME, firstName);
+            r.put(StructuredName.FAMILY_NAME, lastName);
+            r.put(StructuredName.DISPLAY_NAME, displayName);
+            cursor.addRow(r.getRow());
+        }
+    }
+
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
@@ -67,8 +174,14 @@
         }
 
         int match = sURIMatcher.match(uri);
+        MatrixCursor cursor;
+        Object[] row;
+        PackedString ps;
+        List<String> pathSegments = uri.getPathSegments();
+        String lookupKey;
+
         switch (match) {
-            case GAL_FILTER:
+            case GAL_FILTER: {
                 String filter = uri.getLastPathSegment();
                 // We should have at least two characters before doing a GAL search
                 if (filter == null || filter.length() < 2) {
@@ -86,6 +199,31 @@
                     Binder.restoreCallingIdentity(callingId);
                 }
                 break;
+            }
+
+            case GAL_CONTACT:
+            case GAL_CONTACT_WITH_ID: {
+                GalProjection galProjection = new GalProjection(projection);
+                cursor = new MatrixCursor(projection);
+                // Handle the decomposition of the key into rows suitable for CP2
+                lookupKey = pathSegments.get(2);
+                long contactId = (match == GAL_CONTACT_WITH_ID)
+                        ? Long.parseLong(pathSegments.get(3))
+                        : DEFAULT_CONTACT_ID;
+                ps = new PackedString(lookupKey);
+                String displayName = ps.get(GalData.DISPLAY_NAME);
+                GalContactRow.addEmailAddress(cursor, galProjection, contactId, lookupKey,
+                        accountName, displayName, ps.get(GalData.EMAIL_ADDRESS));
+                GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+                        displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE));
+                GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+                        displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE));
+                GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+                        displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE));
+                GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayName,
+                        ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName);
+                return cursor;
+            }
         }
 
         return null;
@@ -93,8 +231,10 @@
 
     /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
         int displayNameIndex = -1;
+        int alternateDisplayNameIndex = -1;;
         int emailIndex = -1;
-        boolean alternateDisplayName = false;
+        int idIndex = -1;
+        int lookupIndex = -1;
 
         for (int i = 0; i < projection.length; i++) {
             String column = projection[i];
@@ -102,13 +242,14 @@
                     Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
                 displayNameIndex = i;
             } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
-                displayNameIndex = i;
-                alternateDisplayName = true;
-
+                alternateDisplayNameIndex = i;
             } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
                 emailIndex = i;
+            } else if (Contacts._ID.equals(column)) {
+                idIndex = i;
+            } else if (Contacts.LOOKUP_KEY.equals(column)) {
+                lookupIndex = i;
             }
-            // TODO other fields
         }
 
         Object[] row = new Object[projection.length];
@@ -120,12 +261,43 @@
         int count = galResult.galData.size();
         for (int i = 0; i < count; i++) {
             GalData galDataRow = galResult.galData.get(i);
+            String firstName = galDataRow.get(GalData.FIRST_NAME);
+            String lastName = galDataRow.get(GalData.LAST_NAME);
+            String displayName = galDataRow.get(GalData.DISPLAY_NAME);
+            // If we don't have a display name, try to create one using first and last name
+            if (displayName == null) {
+                if (firstName != null && lastName != null) {
+                    displayName = firstName + " " + lastName;
+                } else if (firstName != null) {
+                    displayName = firstName;
+                } else if (lastName != null) {
+                    displayName = lastName;
+                }
+            }
+            galDataRow.put(GalData.DISPLAY_NAME, displayName);
+
             if (displayNameIndex != -1) {
-                row[displayNameIndex] = galDataRow.displayName;
-                // TODO Handle alternate display name here
+                row[displayNameIndex] = displayName;
+            }
+            if (alternateDisplayNameIndex != -1) {
+                // Try to create an alternate display name, using first and last name
+                // TODO: Check with Contacts team to make sure we're using this properly
+                if (firstName != null && lastName != null) {
+                    row[alternateDisplayNameIndex] = lastName + " " + firstName;
+                } else {
+                    row[alternateDisplayNameIndex] = displayName;
+                }
             }
             if (emailIndex != -1) {
-                row[emailIndex] = galDataRow.emailAddress;
+                row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
+            }
+            if (idIndex != -1) {
+                row[idIndex] = i + 1;  // Let's be 1 based
+            }
+            if (lookupIndex != -1) {
+                // We use the packed string as our lookup key; it contains ALL of the gal data
+                // We do this because we are not able to provide a stable id to ContactsProvider
+                row[lookupIndex] = Uri.encode(galDataRow.toPackedString());
             }
             cursor.addRow(row);
         }
diff --git a/src/com/android/exchange/provider/GalResult.java b/src/com/android/exchange/provider/GalResult.java
index 3700622..e7f2477 100644
--- a/src/com/android/exchange/provider/GalResult.java
+++ b/src/com/android/exchange/provider/GalResult.java
@@ -15,6 +15,8 @@
 
 package com.android.exchange.provider;
 
+import com.android.email.mail.PackedString;
+
 import java.util.ArrayList;
 
 /**
@@ -29,19 +31,64 @@
     public GalResult() {
     }
 
+    /**
+     * Legacy method for email address autocomplete
+     */
     public void addGalData(long id, String displayName, String emailAddress) {
         galData.add(new GalData(id, displayName, emailAddress));
     }
 
-    public static class GalData {
-        final long _id;
-        final String displayName;
-        final String emailAddress;
+    public void addGalData(GalData data) {
+        galData.add(data);
+    }
 
+    public static class GalData {
+        // PackedString constants for GalData
+        public static final String ID = "_id";
+        public static final String DISPLAY_NAME = "displayName";
+        public static final String EMAIL_ADDRESS = "emailAddress";
+        public static final String WORK_PHONE = "workPhone";
+        public static final String HOME_PHONE = "homePhone";
+        public static final String MOBILE_PHONE = "mobilePhone";
+        public static final String FIRST_NAME = "firstName";
+        public static final String LAST_NAME = "lastName";
+        public static final String COMPANY = "company";
+        public static final String TITLE = "title";
+        public static final String OFFICE = "office";
+        public static final String ALIAS = "alias";
+        // The Builder we use to construct the PackedString
+        PackedString.Builder builder = new PackedString.Builder();
+
+        // The following three fields are for legacy email autocomplete
+        public long _id = 0;
+        public String displayName;
+        public String emailAddress;
+
+        /**
+         * Legacy constructor for email address autocomplete
+         */
         private GalData(long id, String _displayName, String _emailAddress) {
+            put(ID, Long.toString(id));
             _id = id;
+            put(DISPLAY_NAME, _displayName);
             displayName = _displayName;
+            put(EMAIL_ADDRESS, _emailAddress);
             emailAddress = _emailAddress;
         }
+
+        public GalData() {
+        }
+
+        public String get(String field) {
+            return builder.get(field);
+        }
+
+        public void put(String field, String value) {
+            builder.put(field, value);
+        }
+
+        public String toPackedString() {
+            return builder.toString();
+        }
     }
 }
diff --git a/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
index a623aed..e1dc223 100644
--- a/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
+++ b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
@@ -16,6 +16,7 @@
 
 package com.android.exchange.provider;
 
+import com.android.email.mail.PackedString;
 import com.android.exchange.provider.GalResult.GalData;
 
 import android.database.Cursor;
@@ -32,18 +33,19 @@
 
     // Create a test projection; we should only get back values for display name and email address
     private static final String[] GAL_RESULT_PROJECTION =
-        new String[] {Contacts.DISPLAY_NAME, CommonDataKinds.Email.ADDRESS, Contacts.CONTENT_TYPE};
-    private static final int GAL_RESULT_DISPLAY_NAME_COLUMN = 0;
-    private static final int GAL_RESULT_EMAIL_ADDRESS_COLUMN = 1;
-    private static final int GAL_RESULT_CONTENT_TYPE_COLUMN = 2;
+        new String[] {Contacts.DISPLAY_NAME, CommonDataKinds.Email.ADDRESS, Contacts.CONTENT_TYPE,
+            Contacts.LOOKUP_KEY};
+    private static final int GAL_RESULT_COLUMN_DISPLAY_NAME = 0;
+    private static final int GAL_RESULT_COLUMN_EMAIL_ADDRESS = 1;
+    private static final int GAL_RESULT_COLUMN_CONTENT_TYPE = 2;
+    private static final int GAL_RESULT_COLUMN_LOOKUP_KEY = 3;
 
-    public void testBuildGalResultCursor() {
+    public void testBuildSimpleGalResultCursor() {
         GalResult result = new GalResult();
         result.addGalData(1, "Alice Aardvark", "alice@aardvark.com");
         result.addGalData(2, "Bob Badger", "bob@badger.com");
         result.addGalData(3, "Clark Cougar", "clark@cougar.com");
         result.addGalData(4, "Dan Dolphin", "dan@dolphin.com");
-
         // Make sure our returned cursor has the expected contents
         ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
         Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
@@ -53,9 +55,67 @@
         for (int i = 0; i < 4; i++) {
             GalData data = result.galData.get(i);
             assertTrue(c.moveToNext());
-            assertEquals(data.displayName, c.getString(GAL_RESULT_DISPLAY_NAME_COLUMN));
-            assertEquals(data.emailAddress, c.getString(GAL_RESULT_EMAIL_ADDRESS_COLUMN));
-            assertNull(c.getString(GAL_RESULT_CONTENT_TYPE_COLUMN));
+            assertEquals(data.displayName, c.getString(GAL_RESULT_COLUMN_DISPLAY_NAME));
+            assertEquals(data.emailAddress, c.getString(GAL_RESULT_COLUMN_EMAIL_ADDRESS));
+            assertNull(c.getString(GAL_RESULT_COLUMN_CONTENT_TYPE));
+        }
+    }
+
+    private static final String[][] DISPLAY_NAME_TEST_FIELDS = {
+        {"Alice", "Aardvark", "Another Name"},
+        {"Alice", "Aardvark", null},
+        {"Alice", null, null},
+        {null, "Aardvark", null},
+        {null, null, null}
+    };
+    private static final int TEST_FIELD_FIRST_NAME = 0;
+    private static final int TEST_FIELD_LAST_NAME = 1;
+    private static final int TEST_FIELD_DISPLAY_NAME = 2;
+    private static final String[] EXPECTED_DISPLAY_NAMES = new String[] {"Another Name",
+        "Alice Aardvark", "Alice", "Aardvark", null};
+
+    private GalResult getTestDisplayNameResult() {
+        GalResult result = new GalResult();
+        for (int i = 0; i < DISPLAY_NAME_TEST_FIELDS.length; i++) {
+            GalData galData = new GalData();
+            String[] names = DISPLAY_NAME_TEST_FIELDS[i];
+            galData.put(GalData.FIRST_NAME, names[TEST_FIELD_FIRST_NAME]);
+            galData.put(GalData.LAST_NAME, names[TEST_FIELD_LAST_NAME]);
+            galData.put(GalData.DISPLAY_NAME, names[TEST_FIELD_DISPLAY_NAME]);
+            result.addGalData(galData);
+        }
+        return result;
+    }
+
+    public void testDisplayNameLogic() {
+        GalResult result = getTestDisplayNameResult();
+        // Make sure our returned cursor has the expected contents
+        ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+        Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+        assertNotNull(c);
+        assertEquals(MatrixCursor.class, c.getClass());
+        assertEquals(DISPLAY_NAME_TEST_FIELDS.length, c.getCount());
+        for (int i = 0; i < EXPECTED_DISPLAY_NAMES.length; i++) {
+            assertTrue(c.moveToNext());
+            assertEquals(EXPECTED_DISPLAY_NAMES[i], c.getString(GAL_RESULT_COLUMN_DISPLAY_NAME));
+        }
+    }
+
+    public void testLookupKeyLogic() {
+        GalResult result = getTestDisplayNameResult();
+        // Make sure our returned cursor has the expected contents
+        ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+        Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+        assertNotNull(c);
+        assertEquals(MatrixCursor.class, c.getClass());
+        assertEquals(DISPLAY_NAME_TEST_FIELDS.length, c.getCount());
+        for (int i = 0; i < EXPECTED_DISPLAY_NAMES.length; i++) {
+            assertTrue(c.moveToNext());
+            PackedString ps = new PackedString(c.getString(GAL_RESULT_COLUMN_LOOKUP_KEY));
+            String[] testFields = DISPLAY_NAME_TEST_FIELDS[i];
+            assertEquals(testFields[TEST_FIELD_FIRST_NAME], ps.get(GalData.FIRST_NAME));
+            assertEquals(testFields[TEST_FIELD_LAST_NAME], ps.get(GalData.LAST_NAME));
+            assertEquals(EXPECTED_DISPLAY_NAMES[i], ps.get(GalData.DISPLAY_NAME));
         }
     }
 }