Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.exchange.provider;
     18 
     19 import android.accounts.AccountManager;
     20 import android.content.ContentProvider;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.UriMatcher;
     24 import android.database.Cursor;
     25 import android.database.MatrixCursor;
     26 import android.net.Uri;
     27 import android.os.Binder;
     28 import android.os.Bundle;
     29 import android.provider.ContactsContract;
     30 import android.provider.ContactsContract.CommonDataKinds.Email;
     31 import android.provider.ContactsContract.CommonDataKinds.Phone;
     32 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     33 import android.provider.ContactsContract.Contacts;
     34 import android.provider.ContactsContract.Contacts.Data;
     35 import android.provider.ContactsContract.Directory;
     36 import android.provider.ContactsContract.DisplayNameSources;
     37 import android.provider.ContactsContract.RawContacts;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 import android.util.Pair;
     41 
     42 import com.android.emailcommon.Configuration;
     43 import com.android.emailcommon.mail.PackedString;
     44 import com.android.emailcommon.provider.Account;
     45 import com.android.emailcommon.provider.EmailContent;
     46 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     47 import com.android.emailcommon.service.AccountServiceProxy;
     48 import com.android.emailcommon.utility.Utility;
     49 import com.android.exchange.Eas;
     50 import com.android.exchange.R;
     51 import com.android.exchange.provider.GalResult.GalData;
     52 import com.android.exchange.service.EasService;
     53 import com.android.mail.utils.LogUtils;
     54 
     55 import java.text.Collator;
     56 import java.util.ArrayList;
     57 import java.util.Comparator;
     58 import java.util.HashMap;
     59 import java.util.HashSet;
     60 import java.util.List;
     61 import java.util.Set;
     62 import java.util.TreeMap;
     63 
     64 /**
     65  * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
     66  * used solely to provide GAL (Global Address Lookup) service to email address adapters
     67  */
     68 public class ExchangeDirectoryProvider extends ContentProvider {
     69     private static final String TAG = Eas.LOG_TAG;
     70 
     71     public static final String EXCHANGE_GAL_AUTHORITY =
     72             com.android.exchange.Configuration.EXCHANGE_GAL_AUTHORITY;
     73 
     74     private static final int DEFAULT_CONTACT_ID = 1;
     75 
     76     private static final int DEFAULT_LOOKUP_LIMIT = 20;
     77     private static final int MAX_LOOKUP_LIMIT = 100;
     78 
     79     private static final int GAL_BASE = 0;
     80     private static final int GAL_DIRECTORIES = GAL_BASE;
     81     private static final int GAL_FILTER = GAL_BASE + 1;
     82     private static final int GAL_CONTACT = GAL_BASE + 2;
     83     private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
     84     private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
     85     private static final int GAL_PHONE_FILTER = GAL_BASE + 5;
     86 
     87     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     88     /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>();
     89 
     90     static {
     91         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES);
     92         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
     93         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
     94         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
     95                 GAL_CONTACT_WITH_ID);
     96         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
     97         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/phones/filter/*", GAL_PHONE_FILTER);
     98 
     99     }
    100 
    101     @Override
    102     public boolean onCreate() {
    103         EmailContent.init(getContext());
    104         return true;
    105     }
    106 
    107     static class GalProjection {
    108         final int size;
    109         final HashMap<String, Integer> columnMap = new HashMap<String, Integer>();
    110 
    111         GalProjection(String[] projection) {
    112             size = projection.length;
    113             for (int i = 0; i < projection.length; i++) {
    114                 columnMap.put(projection[i], i);
    115             }
    116         }
    117     }
    118 
    119     static class GalDisplayNameFields {
    120         private final String displayName;
    121         private final String displayNameSource;
    122         private final String alternateDisplayName;
    123 
    124         GalDisplayNameFields(PackedString ps) {
    125             displayName = ps.get(GalData.DISPLAY_NAME);
    126             displayNameSource = ps.get(GalData.DISPLAY_NAME_SOURCE);
    127             alternateDisplayName = ps.get(GalData.DISPLAY_NAME_ALTERNATIVE);
    128         }
    129 
    130         String getDisplayName() { return displayName; }
    131         String getDisplayNameSource() { return displayNameSource; }
    132         String getAlternateDisplayName() { return alternateDisplayName; }
    133     }
    134 
    135     static class GalContactRow {
    136         private final GalProjection mProjection;
    137         private Object[] row;
    138         static long dataId = 1;
    139 
    140         GalContactRow(GalProjection projection, long contactId, String accountName,
    141                 GalDisplayNameFields displayNameFields) {
    142             this.mProjection = projection;
    143             row = new Object[projection.size];
    144 
    145             put(Contacts.Entity.CONTACT_ID, contactId);
    146 
    147             // We only have one raw contact per aggregate, so they can have the same ID
    148             put(Contacts.Entity.RAW_CONTACT_ID, contactId);
    149             put(Contacts.Entity.DATA_ID, dataId++);
    150 
    151             put(Contacts.DISPLAY_NAME, displayNameFields.getDisplayName());
    152             put(Contacts.DISPLAY_NAME_SOURCE, displayNameFields.getDisplayNameSource());
    153             put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayNameFields.getAlternateDisplayName());
    154 
    155             put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    156             put(RawContacts.ACCOUNT_NAME, accountName);
    157             put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
    158             put(Data.IS_READ_ONLY, 1);
    159         }
    160 
    161         Object[] getRow () {
    162             return row;
    163         }
    164 
    165         void put(String columnName, Object value) {
    166             final Integer integer = mProjection.columnMap.get(columnName);
    167             if (integer != null) {
    168                 row[integer] = value;
    169             } else {
    170                 LogUtils.e(TAG, "Unsupported column: " + columnName);
    171             }
    172         }
    173 
    174         static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
    175                 long contactId, String accountName, GalDisplayNameFields displayNameFields,
    176                 String address) {
    177             if (!TextUtils.isEmpty(address)) {
    178                 final GalContactRow r = new GalContactRow(
    179                         galProjection, contactId, accountName, displayNameFields);
    180                 r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
    181                 r.put(Email.TYPE, Email.TYPE_WORK);
    182                 r.put(Email.ADDRESS, address);
    183                 cursor.addRow(r.getRow());
    184             }
    185         }
    186 
    187         static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
    188                 String accountName, GalDisplayNameFields displayNameFields, int type, String number) {
    189             if (!TextUtils.isEmpty(number)) {
    190                 final GalContactRow r = new GalContactRow(
    191                         projection, contactId, accountName, displayNameFields);
    192                 r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    193                 r.put(Phone.TYPE, type);
    194                 r.put(Phone.NUMBER, number);
    195                 cursor.addRow(r.getRow());
    196             }
    197         }
    198 
    199         public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
    200                 long contactId, String accountName, GalDisplayNameFields displayNameFields,
    201                 String firstName, String lastName) {
    202             final GalContactRow r = new GalContactRow(
    203                     galProjection, contactId, accountName, displayNameFields);
    204             r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    205             r.put(StructuredName.GIVEN_NAME, firstName);
    206             r.put(StructuredName.FAMILY_NAME, lastName);
    207             r.put(StructuredName.DISPLAY_NAME, displayNameFields.getDisplayName());
    208             cursor.addRow(r.getRow());
    209         }
    210     }
    211 
    212     /**
    213      * Find the record id of an Account, given its name (email address)
    214      * @param accountName the name of the account
    215      * @return the record id of the Account, or -1 if not found
    216      */
    217     /*package*/ long getAccountIdByName(Context context, String accountName) {
    218         Long accountId = mAccountIdMap.get(accountName);
    219         if (accountId == null) {
    220             accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI,
    221                     EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
    222                     new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L);
    223             if (accountId != -1) {
    224                 mAccountIdMap.put(accountName, accountId);
    225             }
    226         }
    227         return accountId;
    228     }
    229 
    230     @Override
    231     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    232             String sortOrder) {
    233         LogUtils.d(TAG, "ExchangeDirectoryProvider: query: %s", uri.toString());
    234         final int match = sURIMatcher.match(uri);
    235         final MatrixCursor cursor;
    236         Object[] row;
    237         final PackedString ps;
    238         final String lookupKey;
    239 
    240         switch (match) {
    241             case GAL_DIRECTORIES: {
    242                 // Assuming that GAL can be used with all exchange accounts
    243                 final android.accounts.Account[] accounts = AccountManager.get(getContext())
    244                         .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    245                 cursor = new MatrixCursor(projection);
    246                 if (accounts != null) {
    247                     for (android.accounts.Account account : accounts) {
    248                         row = new Object[projection.length];
    249 
    250                         for (int i = 0; i < projection.length; i++) {
    251                             final String column = projection[i];
    252                             if (column.equals(Directory.ACCOUNT_NAME)) {
    253                                 row[i] = account.name;
    254                             } else if (column.equals(Directory.ACCOUNT_TYPE)) {
    255                                 row[i] = account.type;
    256                             } else if (column.equals(Directory.TYPE_RESOURCE_ID)) {
    257                                 final String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
    258                                 final Bundle bundle = new AccountServiceProxy(getContext())
    259                                     .getConfigurationData(accountType);
    260                                 // Default to the alternative name, erring on the conservative side
    261                                 int exchangeName = R.string.exchange_name_alternate;
    262                                 if (bundle != null && !bundle.getBoolean(
    263                                         Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS,
    264                                         true)) {
    265                                     exchangeName = R.string.exchange_eas_name;
    266                                 }
    267                                 row[i] = exchangeName;
    268                             } else if (column.equals(Directory.DISPLAY_NAME)) {
    269                                 // If the account name is an email address, extract
    270                                 // the domain name and use it as the directory display name
    271                                 final String accountName = account.name;
    272                                 final int atIndex = accountName.indexOf('@');
    273                                 if (atIndex != -1 && atIndex < accountName.length() - 2) {
    274                                     final char firstLetter = Character.toUpperCase(
    275                                             accountName.charAt(atIndex + 1));
    276                                     row[i] = firstLetter + accountName.substring(atIndex + 2);
    277                                 } else {
    278                                     row[i] = account.name;
    279                                 }
    280                             } else if (column.equals(Directory.EXPORT_SUPPORT)) {
    281                                 row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY;
    282                             } else if (column.equals(Directory.SHORTCUT_SUPPORT)) {
    283                                 row[i] = Directory.SHORTCUT_SUPPORT_NONE;
    284                             }
    285                         }
    286                         cursor.addRow(row);
    287                     }
    288                 }
    289                 return cursor;
    290             }
    291 
    292             case GAL_FILTER:
    293             case GAL_PHONE_FILTER:
    294             case GAL_EMAIL_FILTER: {
    295                 final String filter = uri.getLastPathSegment();
    296                 // We should have at least two characters before doing a GAL search
    297                 if (filter == null || filter.length() < 2) {
    298                     return null;
    299                 }
    300 
    301                 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
    302                 if (accountName == null) {
    303                     return null;
    304                 }
    305 
    306                 // Enforce a limit on the number of lookup responses
    307                 final String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
    308                 int limit = DEFAULT_LOOKUP_LIMIT;
    309                 if (limitString != null) {
    310                     try {
    311                         limit = Integer.parseInt(limitString);
    312                     } catch (NumberFormatException e) {
    313                         limit = 0;
    314                     }
    315                     if (limit <= 0) {
    316                         throw new IllegalArgumentException("Limit not valid: " + limitString);
    317                     }
    318                 }
    319 
    320                 final long callingId = Binder.clearCallingIdentity();
    321                 try {
    322                     // Find the account id to pass along to EasSyncService
    323                     final long accountId = getAccountIdByName(getContext(), accountName);
    324                     if (accountId == -1) {
    325                         // The account was deleted?
    326                         return null;
    327                     }
    328 
    329                     final boolean isEmail = match == GAL_EMAIL_FILTER;
    330                     final boolean isPhone = match == GAL_PHONE_FILTER;
    331                     // For phone filter queries we request more results from the server
    332                     // than requested by the caller because we omit contacts without
    333                     // phone numbers, and the server lacks the ability to do this filtering
    334                     // for us. We then enforce the limit when constructing the cursor
    335                     // containing the results.
    336                     int queryLimit = limit;
    337                     if (isPhone) {
    338                         queryLimit = 3 * queryLimit;
    339                     }
    340                     if (queryLimit > MAX_LOOKUP_LIMIT) {
    341                         queryLimit = MAX_LOOKUP_LIMIT;
    342                     }
    343 
    344                     // Get results from the Exchange account
    345                     final GalResult galResult = EasService.searchGal(getContext(), accountId,
    346                             filter, queryLimit);
    347                     if (galResult != null && (galResult.getNumEntries() > 0)) {
    348                          return buildGalResultCursor(
    349                                  projection, galResult, sortOrder, limit, isEmail, isPhone);
    350                     }
    351                 } finally {
    352                     Binder.restoreCallingIdentity(callingId);
    353                 }
    354                 break;
    355             }
    356 
    357             case GAL_CONTACT:
    358             case GAL_CONTACT_WITH_ID: {
    359                 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
    360                 if (accountName == null) {
    361                     return null;
    362                 }
    363 
    364                 final GalProjection galProjection = new GalProjection(projection);
    365                 cursor = new MatrixCursor(projection);
    366                 // Handle the decomposition of the key into rows suitable for CP2
    367                 final List<String> pathSegments = uri.getPathSegments();
    368                 lookupKey = pathSegments.get(2);
    369                 final long contactId = (match == GAL_CONTACT_WITH_ID)
    370                         ? Long.parseLong(pathSegments.get(3))
    371                         : DEFAULT_CONTACT_ID;
    372                 ps = new PackedString(lookupKey);
    373                 final GalDisplayNameFields displayNameFields = new GalDisplayNameFields(ps);
    374                 GalContactRow.addEmailAddress(cursor, galProjection, contactId, accountName, displayNameFields,
    375                         ps.get(GalData.EMAIL_ADDRESS));
    376                 GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, displayNameFields,
    377                         Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE));
    378                 GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, displayNameFields,
    379                         Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE));
    380                 GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, displayNameFields,
    381                         Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE));
    382                 GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayNameFields,
    383                         ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME));
    384                 return cursor;
    385             }
    386         }
    387 
    388         return null;
    389     }
    390 
    391     /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult,
    392             String sortOrder, int limit, boolean isEmailFilter, boolean isPhoneFilter) {
    393         int displayNameIndex = -1;
    394         int displayNameSourceIndex = -1;
    395         int alternateDisplayNameIndex = -1;
    396         int emailIndex = -1;
    397         int emailTypeIndex = -1;
    398         int phoneNumberIndex = -1;
    399         int phoneTypeIndex = -1;
    400         int hasPhoneNumberIndex = -1;
    401         int idIndex = -1;
    402         int contactIdIndex = -1;
    403         int lookupIndex = -1;
    404 
    405         for (int i = 0; i < projection.length; i++) {
    406             final String column = projection[i];
    407             if (Contacts.DISPLAY_NAME.equals(column) ||
    408                     Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
    409                 displayNameIndex = i;
    410             } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
    411                 alternateDisplayNameIndex = i;
    412             } else if (Contacts.DISPLAY_NAME_SOURCE.equals(column)) {
    413                 displayNameSourceIndex = i;
    414             } else if (Contacts.HAS_PHONE_NUMBER.equals(column)) {
    415                 hasPhoneNumberIndex = i;
    416             } else if (Contacts._ID.equals(column)) {
    417                 idIndex = i;
    418             } else if (Phone.CONTACT_ID.equals(column)) {
    419                 contactIdIndex = i;
    420             } else if (Contacts.LOOKUP_KEY.equals(column)) {
    421                 lookupIndex = i;
    422             } else if (isPhoneFilter) {
    423                 if (Phone.NUMBER.equals(column)) {
    424                     phoneNumberIndex = i;
    425                 } else if (Phone.TYPE.equals(column)) {
    426                     phoneTypeIndex = i;
    427                 }
    428             } else {
    429                 // Cannot support for Email and Phone in same query, so default
    430                 // is to return email addresses.
    431                 if (Email.ADDRESS.equals(column)) {
    432                     emailIndex = i;
    433                 } else if (Email.TYPE.equals(column)) {
    434                     emailTypeIndex = i;
    435                 }
    436             }
    437         }
    438 
    439         boolean usePrimarySortKey = false;
    440         boolean useAlternateSortKey = false;
    441         if (Contacts.SORT_KEY_PRIMARY.equals(sortOrder)) {
    442             usePrimarySortKey = true;
    443         } else if (Contacts.SORT_KEY_ALTERNATIVE.equals(sortOrder)) {
    444             useAlternateSortKey = true;
    445         } else if (sortOrder != null && sortOrder.length() > 0) {
    446             Log.w(TAG, "Ignoring unsupported sort order: " + sortOrder);
    447         }
    448 
    449         final TreeMap<GalSortKey, Object[]> sortedResultsMap =
    450                 new TreeMap<GalSortKey, Object[]>(new NameComparator());
    451 
    452         // id populates the _ID column and is incremented for each row in the
    453         // result set, so each row has a unique id.
    454         int id = 1;
    455         // contactId populates the CONTACT_ID column and is incremented for
    456         // each contact. For the email and phone filters, there may be more
    457         // than one row with the same contactId if a given contact has multiple
    458         // email addresses or multiple phone numbers.
    459         int contactId = 1;
    460 
    461         final int count = galResult.galData.size();
    462         for (int i = 0; i < count; i++) {
    463             final GalData galDataRow = galResult.galData.get(i);
    464 
    465             final List<PhoneInfo> phones = new ArrayList<PhoneInfo>();
    466             addPhoneInfo(phones, galDataRow.get(GalData.WORK_PHONE), Phone.TYPE_WORK);
    467             addPhoneInfo(phones, galDataRow.get(GalData.OFFICE), Phone.TYPE_COMPANY_MAIN);
    468             addPhoneInfo(phones, galDataRow.get(GalData.HOME_PHONE), Phone.TYPE_HOME);
    469             addPhoneInfo(phones, galDataRow.get(GalData.MOBILE_PHONE), Phone.TYPE_MOBILE);
    470 
    471             // Track whether we added a result for this contact or not, in
    472             // order to stop once we have maxResult contacts.
    473             boolean addedContact = false;
    474 
    475             Pair<String, Integer> displayName = getDisplayName(galDataRow, phones);
    476             if (TextUtils.isEmpty(displayName.first)) {
    477                 // can't use a contact if we can't find a decent name for it.
    478                 continue;
    479             }
    480             galDataRow.put(GalData.DISPLAY_NAME, displayName.first);
    481             galDataRow.put(GalData.DISPLAY_NAME_SOURCE, String.valueOf(displayName.second));
    482 
    483             final String alternateDisplayName = getAlternateDisplayName(
    484                     galDataRow, displayName.first);
    485             final String sortName = usePrimarySortKey ? displayName.first
    486                 : (useAlternateSortKey ? alternateDisplayName : "");
    487             final Object[] row = new Object[projection.length];
    488             if (displayNameIndex != -1) {
    489                 row[displayNameIndex] = displayName.first;
    490             }
    491             if (displayNameSourceIndex != -1) {
    492                 row[displayNameSourceIndex] = displayName.second;
    493             }
    494 
    495             galDataRow.put(GalData.DISPLAY_NAME_ALTERNATIVE, alternateDisplayName);
    496             if (alternateDisplayNameIndex != -1) {
    497                 row[alternateDisplayNameIndex] = alternateDisplayName;
    498             }
    499 
    500             if (hasPhoneNumberIndex != -1) {
    501                 if (phones.size() > 0) {
    502                     row[hasPhoneNumberIndex] = true;
    503                 }
    504             }
    505 
    506             if (contactIdIndex != -1) {
    507                 row[contactIdIndex] = contactId;
    508             }
    509 
    510             if (lookupIndex != -1) {
    511                 // We use the packed string as our lookup key; it contains ALL of the gal data
    512                 // We do this because we are not able to provide a stable id to ContactsProvider
    513                 row[lookupIndex] = Uri.encode(galDataRow.toPackedString());
    514             }
    515 
    516             if (isPhoneFilter) {
    517                 final Set<String> uniqueNumbers = new HashSet<String>();
    518 
    519                 for (PhoneInfo phone : phones) {
    520                     if (!uniqueNumbers.add(phone.mNumber)) {
    521                         continue;
    522                     }
    523                     if (phoneNumberIndex != -1) {
    524                         row[phoneNumberIndex] = phone.mNumber;
    525                     }
    526                     if (phoneTypeIndex != -1) {
    527                         row[phoneTypeIndex] = phone.mType;
    528                     }
    529                     if (idIndex != -1) {
    530                         row[idIndex] = id;
    531                     }
    532                     sortedResultsMap.put(new GalSortKey(sortName, id), row.clone());
    533                     addedContact = true;
    534                     id++;
    535                 }
    536 
    537             } else {
    538                 boolean haveEmail = false;
    539                 Object address = galDataRow.get(GalData.EMAIL_ADDRESS);
    540                 if (address != null && !TextUtils.isEmpty(address.toString())) {
    541                     if (emailIndex != -1) {
    542                         row[emailIndex] = address;
    543                     }
    544                     if (emailTypeIndex != -1) {
    545                         row[emailTypeIndex] = Email.TYPE_WORK;
    546                     }
    547                     haveEmail = true;
    548                 }
    549 
    550                 if (!isEmailFilter || haveEmail) {
    551                     if (idIndex != -1) {
    552                         row[idIndex] = id;
    553                     }
    554                     sortedResultsMap.put(new GalSortKey(sortName, id), row.clone());
    555                     addedContact = true;
    556                     id++;
    557                 }
    558             }
    559             if (addedContact) {
    560                 contactId++;
    561                 if (contactId > limit) {
    562                     break;
    563                 }
    564             }
    565         }
    566         final MatrixCursor cursor = new MatrixCursor(projection, sortedResultsMap.size());
    567         for(Object[] result : sortedResultsMap.values()) {
    568             cursor.addRow(result);
    569         }
    570 
    571         return cursor;
    572     }
    573 
    574     /**
    575      * Try to create a display name from various fields.
    576      *
    577      * @return a display name for contact and its source
    578      */
    579     private static Pair<String, Integer> getDisplayName(GalData galDataRow, List<PhoneInfo> phones) {
    580         String displayName = galDataRow.get(GalData.DISPLAY_NAME);
    581         if (!TextUtils.isEmpty(displayName)) {
    582             return Pair.create(displayName, DisplayNameSources.STRUCTURED_NAME);
    583         }
    584 
    585         // try to get displayName from name fields
    586         final String firstName = galDataRow.get(GalData.FIRST_NAME);
    587         final String lastName = galDataRow.get(GalData.LAST_NAME);
    588         if (!TextUtils.isEmpty(firstName) || !TextUtils.isEmpty(lastName)) {
    589             if (!TextUtils.isEmpty(firstName) && !TextUtils.isEmpty(lastName)) {
    590                 displayName = firstName + " " + lastName;
    591             } else if (!TextUtils.isEmpty(firstName)) {
    592                 displayName = firstName;
    593             } else {
    594                 displayName = lastName;
    595             }
    596             return Pair.create(displayName, DisplayNameSources.STRUCTURED_NAME);
    597         }
    598 
    599         // try to get displayName from email
    600         final String emailAddress = galDataRow.get(GalData.EMAIL_ADDRESS);
    601         if (!TextUtils.isEmpty(emailAddress)) {
    602             return Pair.create(emailAddress, DisplayNameSources.EMAIL);
    603         }
    604 
    605         // try to get displayName from phone numbers
    606         if (phones != null && phones.size() > 0) {
    607             final PhoneInfo phone = (PhoneInfo) phones.get(0);
    608             if (phone != null && !TextUtils.isEmpty(phone.mNumber)) {
    609                 return Pair.create(phone.mNumber, DisplayNameSources.PHONE);
    610             }
    611         }
    612         return Pair.create(null, null);
    613     }
    614 
    615     /**
    616      * Try to create the alternate display name from various fields. The CP2
    617      * Alternate Display Name field is LastName FirstName to support user
    618      * choice of how to order names for display.
    619      *
    620      * @return alternate display name for contact and its source
    621      */
    622     private static String getAlternateDisplayName(GalData galDataRow, String displayName) {
    623         // try to get displayName from name fields
    624         final String firstName = galDataRow.get(GalData.FIRST_NAME);
    625         final String lastName = galDataRow.get(GalData.LAST_NAME);
    626         if (!TextUtils.isEmpty(firstName) && !TextUtils.isEmpty(lastName)) {
    627             return lastName + " " + firstName;
    628         } else if (!TextUtils.isEmpty(lastName)) {
    629             return lastName;
    630         }
    631         return displayName;
    632     }
    633 
    634     private void addPhoneInfo(List<PhoneInfo> phones, String number, int type) {
    635         if (!TextUtils.isEmpty(number)) {
    636             phones.add(new PhoneInfo(number, type));
    637         }
    638     }
    639 
    640     @Override
    641     public String getType(Uri uri) {
    642         final int match = sURIMatcher.match(uri);
    643         switch (match) {
    644             case GAL_FILTER:
    645                 return Contacts.CONTENT_ITEM_TYPE;
    646         }
    647         return null;
    648     }
    649 
    650     @Override
    651     public int delete(Uri uri, String selection, String[] selectionArgs) {
    652         throw new UnsupportedOperationException();
    653     }
    654 
    655     @Override
    656     public Uri insert(Uri uri, ContentValues values) {
    657         throw new UnsupportedOperationException();
    658     }
    659 
    660     @Override
    661     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    662         throw new UnsupportedOperationException();
    663     }
    664 
    665     /**
    666      * Sort key for Gal filter results.
    667      *  - primary key is name
    668      *      for SORT_KEY_PRIMARY, this is displayName
    669      *      for SORT_KEY_ALTERNATIVE, this is alternativeDisplayName
    670      *      if no sort order is specified, this key is empty
    671      *  - secondary key is id, so ordering of the original results are
    672      *      preserved both between contacts with the same name and for
    673      *      multiple results within a given contact
    674      */
    675     protected static class GalSortKey {
    676         final String sortName;
    677         final int id;
    678 
    679         public GalSortKey(final String sortName, final int id) {
    680             this.sortName = sortName;
    681             this.id = id;
    682         }
    683     }
    684 
    685     /**
    686      * The Comparator that is used by ExchangeDirectoryProvider
    687      */
    688     protected static class NameComparator implements Comparator<GalSortKey> {
    689         private final Collator collator;
    690 
    691         public NameComparator() {
    692             collator = Collator.getInstance();
    693             // Case insensitive sorting
    694             collator.setStrength(Collator.SECONDARY);
    695         }
    696 
    697         @Override
    698         public int compare(final GalSortKey lhs, final GalSortKey rhs) {
    699             if (lhs.sortName != null && rhs.sortName != null) {
    700                 final int res = collator.compare(lhs.sortName, rhs.sortName);
    701                 if (res != 0) {
    702                     return res;
    703                 }
    704             } else if (lhs.sortName != null) {
    705                 return 1;
    706             } else if (rhs.sortName != null) {
    707                 return -1;
    708             }
    709 
    710             // Either the names compared equally or both were null, use the id to compare.
    711             if (lhs.id != rhs.id) {
    712                 return lhs.id > rhs.id ? 1 : -1;
    713             }
    714             return 0;
    715         }
    716     }
    717 
    718     private static class PhoneInfo {
    719         private String mNumber;
    720         private int mType;
    721 
    722         private PhoneInfo(String number, int type) {
    723             mNumber = number;
    724             mType = type;
    725         }
    726     }
    727 }
    728