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