Enhance GAL Provider

Support Phone Filter
Support Display Name in Email Filter

Bug: 11026456

Change-Id: Ief94796ea259618ab261907626dace095670bd1b
diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
index 8e61e9a..9f04a95 100644
--- a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
+++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -27,13 +27,13 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.provider.ContactsContract;
-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.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 
@@ -50,8 +50,14 @@
 import com.android.exchange.provider.GalResult.GalData;
 import com.android.mail.utils.LogUtils;
 
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.TreeMap;
 
 /**
  * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
@@ -72,6 +78,7 @@
     private static final int GAL_CONTACT = GAL_BASE + 2;
     private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
     private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
+    private static final int GAL_PHONE_FILTER = GAL_BASE + 5;
 
     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>();
@@ -83,6 +90,8 @@
         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
                 GAL_CONTACT_WITH_ID);
         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/phones/filter/*", GAL_PHONE_FILTER);
+
     }
 
     @Override
@@ -135,7 +144,7 @@
         }
 
         void put(String columnName, Object value) {
-            Integer integer = mProjection.columnMap.get(columnName);
+            final Integer integer = mProjection.columnMap.get(columnName);
             if (integer != null) {
                 row[integer] = value;
             } else {
@@ -146,7 +155,7 @@
         static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
                 long contactId, String accountName, String displayName, String address) {
             if (!TextUtils.isEmpty(address)) {
-                GalContactRow r = new GalContactRow(
+                final GalContactRow r = new GalContactRow(
                         galProjection, contactId, accountName, displayName);
                 r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
                 r.put(Email.TYPE, Email.TYPE_WORK);
@@ -158,7 +167,7 @@
         static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
                 String accountName, String displayName, int type, String number) {
             if (!TextUtils.isEmpty(number)) {
-                GalContactRow r = new GalContactRow(
+                final GalContactRow r = new GalContactRow(
                         projection, contactId, accountName, displayName);
                 r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
                 r.put(Phone.TYPE, type);
@@ -170,7 +179,7 @@
         public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
                 long contactId, String accountName, String displayName,
                 String firstName, String lastName) {
-            GalContactRow r = new GalContactRow(
+            final GalContactRow r = new GalContactRow(
                     galProjection, contactId, accountName, displayName);
             r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
             r.put(StructuredName.GIVEN_NAME, firstName);
@@ -202,16 +211,16 @@
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
         LogUtils.d(TAG, "ExchangeDirectoryProvider: query: %s", uri.toString());
-        int match = sURIMatcher.match(uri);
-        MatrixCursor cursor;
+        final int match = sURIMatcher.match(uri);
+        final MatrixCursor cursor;
         Object[] row;
-        PackedString ps;
-        String lookupKey;
+        final PackedString ps;
+        final String lookupKey;
 
         switch (match) {
             case GAL_DIRECTORIES: {
                 // Assuming that GAL can be used with all exchange accounts
-                android.accounts.Account[] accounts = AccountManager.get(getContext())
+                final android.accounts.Account[] accounts = AccountManager.get(getContext())
                         .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
                 cursor = new MatrixCursor(projection);
                 if (accounts != null) {
@@ -219,15 +228,14 @@
                         row = new Object[projection.length];
 
                         for (int i = 0; i < projection.length; i++) {
-                            String column = projection[i];
+                            final String column = projection[i];
                             if (column.equals(Directory.ACCOUNT_NAME)) {
                                 row[i] = account.name;
                             } else if (column.equals(Directory.ACCOUNT_TYPE)) {
                                 row[i] = account.type;
                             } else if (column.equals(Directory.TYPE_RESOURCE_ID)) {
-                                Bundle bundle = null;
-                                String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
-                                bundle = new AccountServiceProxy(getContext())
+                                final String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
+                                final Bundle bundle = new AccountServiceProxy(getContext())
                                     .getConfigurationData(accountType);
                                 // Default to the alternative name, erring on the conservative side
                                 int exchangeName = R.string.exchange_name_alternate;
@@ -241,7 +249,7 @@
                                 // If the account name is an email address, extract
                                 // the domain name and use it as the directory display name
                                 final String accountName = account.name;
-                                int atIndex = accountName.indexOf('@');
+                                final int atIndex = accountName.indexOf('@');
                                 if (atIndex != -1 && atIndex < accountName.length() - 2) {
                                     final char firstLetter = Character.toUpperCase(
                                             accountName.charAt(atIndex + 1));
@@ -262,20 +270,21 @@
             }
 
             case GAL_FILTER:
+            case GAL_PHONE_FILTER:
             case GAL_EMAIL_FILTER: {
-                String filter = uri.getLastPathSegment();
+                final String filter = uri.getLastPathSegment();
                 // We should have at least two characters before doing a GAL search
                 if (filter == null || filter.length() < 2) {
                     return null;
                 }
 
-                String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+                final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
                 if (accountName == null) {
                     return null;
                 }
 
                 // Enforce a limit on the number of lookup responses
-                String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
+                final String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
                 int limit = DEFAULT_LOOKUP_LIMIT;
                 if (limitString != null) {
                     try {
@@ -288,20 +297,21 @@
                     }
                 }
 
-                long callingId = Binder.clearCallingIdentity();
+                final long callingId = Binder.clearCallingIdentity();
                 try {
                     // Find the account id to pass along to EasSyncService
-                    long accountId = getAccountIdByName(getContext(), accountName);
+                    final long accountId = getAccountIdByName(getContext(), accountName);
                     if (accountId == -1) {
                         // The account was deleted?
                         return null;
                     }
 
                     // Get results from the Exchange account
-                    GalResult galResult = EasSyncService.searchGal(getContext(), accountId,
+                    final GalResult galResult = EasSyncService.searchGal(getContext(), accountId,
                             filter, limit);
                     if (galResult != null) {
-                        return buildGalResultCursor(projection, galResult);
+                        return buildGalResultCursor(
+                                projection, galResult, match == GAL_PHONE_FILTER, sortOrder);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(callingId);
@@ -311,21 +321,21 @@
 
             case GAL_CONTACT:
             case GAL_CONTACT_WITH_ID: {
-                String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+                final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
                 if (accountName == null) {
                     return null;
                 }
 
-                GalProjection galProjection = new GalProjection(projection);
+                final GalProjection galProjection = new GalProjection(projection);
                 cursor = new MatrixCursor(projection);
                 // Handle the decomposition of the key into rows suitable for CP2
-                List<String> pathSegments = uri.getPathSegments();
+                final List<String> pathSegments = uri.getPathSegments();
                 lookupKey = pathSegments.get(2);
-                long contactId = (match == GAL_CONTACT_WITH_ID)
+                final 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);
+                final String displayName = ps.get(GalData.DISPLAY_NAME);
                 GalContactRow.addEmailAddress(cursor, galProjection, contactId,
                         accountName, displayName, ps.get(GalData.EMAIL_ADDRESS));
                 GalContactRow.addPhoneRow(cursor, galProjection, contactId,
@@ -343,41 +353,82 @@
         return null;
     }
 
-    /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
+    /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult,
+            boolean isPhoneFilter, String sortOrder) {
         int displayNameIndex = -1;
-        int alternateDisplayNameIndex = -1;;
+        int displayNameSourceIndex = -1;
+        int alternateDisplayNameIndex = -1;
         int emailIndex = -1;
+        int emailTypeIndex = -1;
+        int phoneNumberIndex = -1;
+        int phoneTypeIndex = -1;
+        int hasPhoneNumberIndex = -1;
         int idIndex = -1;
+        int contactIdIndex = -1;
         int lookupIndex = -1;
 
         for (int i = 0; i < projection.length; i++) {
-            String column = projection[i];
+            final String column = projection[i];
             if (Contacts.DISPLAY_NAME.equals(column) ||
                     Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
                 displayNameIndex = i;
             } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
                 alternateDisplayNameIndex = i;
-            } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
-                emailIndex = i;
+            } else if (Contacts.DISPLAY_NAME_SOURCE.equals(column)) {
+                displayNameSourceIndex = i;
+            } else if (Contacts.HAS_PHONE_NUMBER.equals(column)) {
+                hasPhoneNumberIndex = i;
             } else if (Contacts._ID.equals(column)) {
                 idIndex = i;
+            } else if (Phone.CONTACT_ID.equals(column)) {
+                contactIdIndex = i;
             } else if (Contacts.LOOKUP_KEY.equals(column)) {
                 lookupIndex = i;
+            } else if (isPhoneFilter) {
+                if (Phone.NUMBER.equals(column)) {
+                    phoneNumberIndex = i;
+                } else if (Phone.TYPE.equals(column)) {
+                    phoneTypeIndex = i;
+                }
+            } else {
+                // Cannot support for Email and Phone in same query, so default
+                // is to return email addresses.
+                if (Email.ADDRESS.equals(column)) {
+                    emailIndex = i;
+                } else if (Email.TYPE.equals(column)) {
+                    emailTypeIndex = i;
+                }
             }
         }
 
-        Object[] row = new Object[projection.length];
+        final boolean useAlternateSortKey = Contacts.SORT_KEY_ALTERNATIVE.equals(sortOrder);
 
-        /*
-         * ContactsProvider will ensure that every request has a non-null projection.
-         */
-        MatrixCursor cursor = new MatrixCursor(projection);
-        int count = galResult.galData.size();
+        final TreeMap<GalSortKey, Object[]> sortedResultsMap =
+                new TreeMap<GalSortKey, Object[]>(new NameComparator());
+
+        // id populates the _ID column and is incremented for each row in the
+        // result set, so each row has a unique id.
+        int id = 1;
+        // contactId populates the CONTACT_ID column and is incremented for
+        // each contact. For the email and phone filters, there may be more
+        // than one row with the same contactId if a given contact has multiple
+        // email addresses or multiple phone numbers.
+        int contactId = 1;
+
+        final Object[] row = new Object[projection.length];
+        final 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);
+            final GalData galDataRow = galResult.galData.get(i);
+            final String firstName = galDataRow.get(GalData.FIRST_NAME);
+            final String lastName = galDataRow.get(GalData.LAST_NAME);
             String displayName = galDataRow.get(GalData.DISPLAY_NAME);
+            final List<PhoneInfo> phones = new ArrayList<PhoneInfo>();
+
+            addPhoneInfo(phones, galDataRow.get(GalData.WORK_PHONE), Phone.TYPE_WORK);
+            addPhoneInfo(phones, galDataRow.get(GalData.OFFICE), Phone.TYPE_COMPANY_MAIN);
+            addPhoneInfo(phones, galDataRow.get(GalData.HOME_PHONE), Phone.TYPE_HOME);
+            addPhoneInfo(phones, galDataRow.get(GalData.MOBILE_PHONE), Phone.TYPE_MOBILE);
+
             // 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) {
@@ -393,34 +444,94 @@
             if (displayNameIndex != -1) {
                 row[displayNameIndex] = displayName;
             }
+
+            // 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
+            final String alternateDisplayName;
+            if (firstName != null && lastName != null) {
+                alternateDisplayName = lastName + " " + firstName;
+            } else {
+                alternateDisplayName = 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;
+                row[alternateDisplayNameIndex] = alternateDisplayName;
+            }
+
+            if (displayNameSourceIndex >= 0) {
+                row[displayNameSourceIndex] = DisplayNameSources.STRUCTURED_NAME;
+            }
+
+            final String sortName = useAlternateSortKey ? alternateDisplayName : displayName;
+
+            if (hasPhoneNumberIndex >= 0) {
+                if (phones.size() > 0) {
+                    row[hasPhoneNumberIndex] = true;
                 }
             }
-            if (emailIndex != -1) {
-                row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
+
+            if (contactIdIndex != -1) {
+                row[contactIdIndex] = contactId;
             }
-            if (idIndex != -1) {
-                row[idIndex] = i + 1;  // Let's be 1 based
+
+            if (isPhoneFilter) {
+                final Set<String> uniqueNumbers = new HashSet<String>();
+
+                for (PhoneInfo phone : phones) {
+                    if (!uniqueNumbers.add(phone.mNumber)) {
+                        continue;
+                    }
+                    if (phoneNumberIndex >= 0) {
+                        row[phoneNumberIndex] = phone.mNumber;
+                    }
+                    if (phoneTypeIndex >= 0) {
+                        row[phoneTypeIndex] = phone.mType;
+                    }
+                    if (idIndex != -1) {
+                        row[idIndex] = id;
+                    }
+                    sortedResultsMap.put(new GalSortKey(sortName, id), row.clone());
+                    id++;
+                }
+
+            } else {
+                if (emailIndex != -1) {
+                    row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
+                }
+                if (emailTypeIndex >= 0) {
+                    row[emailTypeIndex] = Email.TYPE_WORK;
+                }
+
+                if (idIndex != -1) {
+                    row[idIndex] = id;
+                }
+                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());
+                }
+                sortedResultsMap.put(new GalSortKey(sortName, id), row.clone());
+                id++;
             }
-            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);
+            contactId++;
         }
+        final MatrixCursor cursor = new MatrixCursor(projection, sortedResultsMap.size());
+        for(Object[] result : sortedResultsMap.values()) {
+            cursor.addRow(result);
+        }
+
         return cursor;
     }
 
+    private void addPhoneInfo(List<PhoneInfo> phones, String number, int type) {
+        if (number != null) {
+            phones.add(new PhoneInfo(number, type));
+        }
+    }
+
     @Override
     public String getType(Uri uri) {
-        int match = sURIMatcher.match(uri);
+        final int match = sURIMatcher.match(uri);
         switch (match) {
             case GAL_FILTER:
                 return Contacts.CONTENT_ITEM_TYPE;
@@ -442,4 +553,56 @@
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         throw new UnsupportedOperationException();
     }
+
+    /**
+     * Sort key for Gal filter results.
+     *  - primary key is name
+     *      for SORT_KEY_PRIMARY, this is displayName
+     *      for SORT_KEY_ALTERNATIVE, this is alternativeDisplayName
+     *      if no sort order is specified, this key is empty
+     *  - secondary key is id, so ordering of the original results are
+     *      preserved both between contacts with the same name and for
+     *      multiple results within a given contact
+     */
+    private static class GalSortKey {
+        final String sortName;
+        final int    id;
+
+        public GalSortKey(String sortName, int id) {
+            this.sortName = sortName;
+            this.id = id;
+        }
+    }
+
+    private static class NameComparator implements Comparator<GalSortKey> {
+        private final Collator collator;
+
+        public NameComparator() {
+            collator = Collator.getInstance();
+            // Case insensitive sorting
+            collator.setStrength(Collator.SECONDARY);
+        }
+
+        @Override
+        public int compare(GalSortKey lhs, GalSortKey rhs) {
+            final int res = collator.compare(lhs.sortName, rhs.sortName);
+            if (res != 0) {
+                return res;
+            }
+            if (lhs.id != rhs.id) {
+                return lhs.id > rhs.id ? 1 : -1;
+            }
+            return 0;
+        }
+    }
+
+    private static class PhoneInfo {
+        private String mNumber;
+        private int mType;
+
+        private PhoneInfo(String number, int type) {
+            mNumber = number;
+            mType = type;
+        }
+    }
 }