Preliminary GAL/Contacts integration for EAS

Change-Id: I9997ac96f83f427c71caf12d591ba6069bedf935
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bdcd4c8..6d4ff98 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -339,12 +339,20 @@
         <!--EXCHANGE-REMOVE-SECTION-START-->
         <!-- In this release, GAL information is used locally only, so we used the same
              strict permissions. -->
+        <!-- NOTE: ExchangeGalProvider will replace ExchangeProvider after integration with
+             the new GAL/contacts implementation -->
         <provider
             android:name="com.android.exchange.provider.ExchangeProvider"
             android:authorities="com.android.exchange.provider"
             android:multiprocess="true"
             android:permission="com.android.email.permission.ACCESS_PROVIDER"
             />
+        <provider
+            android:name="com.android.exchange.provider.ExchangeGalProvider"
+            android:authorities="com.android.exchange.gal.provider"
+            android:readPermission="android.permission.READ_CONTACTS"
+            android:multiprocess="false"
+            />
         <!--EXCHANGE-REMOVE-SECTION-END-->
 
     </application>
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index f58604c..aeeb90b 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -788,7 +788,7 @@
      * @param context caller's context
      * @param accountId the account Id to search
      * @param filter the characters entered so far
-     * @return a result record or null
+     * @return a result record or null for no data
      *
      * TODO: shorter timeout for interactive lookup
      * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index f2d6e57..890a99a 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -420,6 +420,15 @@
             }
             return null;
         }
+
+        public Account getByName(String accountName) {
+            for (Account account : this) {
+                if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
+                    return account;
+                }
+            }
+            return null;
+        }
     }
 
     class AccountObserver extends ContentObserver {
@@ -837,6 +846,17 @@
         return null;
     }
 
+    static public Account getAccountByName(String accountName) {
+        SyncManager syncManager = INSTANCE;
+        if (syncManager != null) {
+            AccountList accountList = syncManager.mAccountList;
+            synchronized (accountList) {
+                return accountList.getByName(accountName);
+            }
+        }
+        return null;
+    }
+
     static public String getEasAccountSelector() {
         SyncManager syncManager = INSTANCE;
         if (syncManager != null && syncManager.mAccountObserver != null) {
diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
new file mode 100644
index 0000000..fde34df
--- /dev/null
+++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 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.provider;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.SyncManager;
+import com.android.exchange.provider.GalResult.GalData;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
+ * used solely to provide GAL (Global Address Lookup) service to email address adapters
+ */
+public class ExchangeDirectoryProvider extends ContentProvider {
+    public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.gal.provider";
+
+    private static final int GAL_BASE = 0;
+    private static final int GAL_FILTER = GAL_BASE;
+
+    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+        if (accountName == null) {
+            return null;
+        }
+
+        Account account = SyncManager.getAccountByName(accountName);
+        if (account == null) {
+            return null;
+        }
+
+        int match = sURIMatcher.match(uri);
+        switch (match) {
+            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) {
+                    return null;
+                }
+                long callingId = Binder.clearCallingIdentity();
+                try {
+                    // Get results from the Exchange account
+                    GalResult galResult = EasSyncService.searchGal(getContext(), account.mId,
+                            filter);
+                    if (galResult != null) {
+                        return buildGalResultCursor(projection, galResult);
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(callingId);
+                }
+                break;
+        }
+
+        return null;
+    }
+
+    /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
+        int displayNameIndex = -1;
+        int emailIndex = -1;
+        boolean alternateDisplayName = false;
+
+        for (int i = 0; i < projection.length; i++) {
+            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)) {
+                displayNameIndex = i;
+                alternateDisplayName = true;
+
+            } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
+                emailIndex = i;
+            }
+            // TODO other fields
+        }
+
+        Object[] row = new Object[projection.length];
+
+        /*
+         * ContactsProvider will ensure that every request has a non-null projection.
+         */
+        MatrixCursor cursor = new MatrixCursor(projection);
+        int count = galResult.galData.size();
+        for (int i = 0; i < count; i++) {
+            GalData galDataRow = galResult.galData.get(i);
+            if (displayNameIndex != -1) {
+                row[displayNameIndex] = galDataRow.displayName;
+                // TODO Handle alternate display name here
+            }
+            if (emailIndex != -1) {
+                row[emailIndex] = galDataRow.emailAddress;
+            }
+            cursor.addRow(row);
+        }
+        return cursor;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        int match = sURIMatcher.match(uri);
+        switch (match) {
+            case GAL_FILTER:
+                return Contacts.CONTENT_ITEM_TYPE;
+        }
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
new file mode 100644
index 0000000..a623aed
--- /dev/null
+++ b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.provider;
+
+import com.android.exchange.provider.GalResult.GalData;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.test.AndroidTestCase;
+
+/**
+ * You can run this entire test case with:
+ *   runtest -c com.android.exchange.provider.ExchangeDirectoryProviderTests email
+ */
+public class ExchangeDirectoryProviderTests extends AndroidTestCase {
+
+    // 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;
+
+    public void testBuildGalResultCursor() {
+        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);
+        assertNotNull(c);
+        assertEquals(MatrixCursor.class, c.getClass());
+        assertEquals(4, c.getCount());
+        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));
+        }
+    }
+}