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 
     40 import com.android.emailcommon.Configuration;
     41 import com.android.emailcommon.mail.PackedString;
     42 import com.android.emailcommon.provider.Account;
     43 import com.android.emailcommon.provider.EmailContent;
     44 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     45 import com.android.emailcommon.service.AccountServiceProxy;
     46 import com.android.emailcommon.utility.Utility;
     47 import com.android.exchange.Eas;
     48 import com.android.exchange.EasSyncService;
     49 import com.android.exchange.R;
     50 import com.android.exchange.provider.GalResult.GalData;
     51 import com.android.mail.utils.LogUtils;
     52 
     53 import java.text.Collator;
     54 import java.util.ArrayList;
     55 import java.util.Comparator;
     56 import java.util.HashMap;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Set;
     60 import java.util.TreeMap;
     61 
     62 /**
     63  * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
     64  * used solely to provide GAL (Global Address Lookup) service to email address adapters
     65  */
     66 public class ExchangeDirectoryProvider extends ContentProvider {
     67     private static final String TAG = Eas.LOG_TAG;
     68 
     69     public static final String EXCHANGE_GAL_AUTHORITY =
     70             com.android.exchange.Configuration.EXCHANGE_GAL_AUTHORITY;
     71 
     72     private static final int DEFAULT_CONTACT_ID = 1;
     73     private static final int DEFAULT_LOOKUP_LIMIT = 20;
     74 
     75     private static final int GAL_BASE = 0;
     76     private static final int GAL_DIRECTORIES = GAL_BASE;
     77     private static final int GAL_FILTER = GAL_BASE + 1;
     78     private static final int GAL_CONTACT = GAL_BASE + 2;
     79     private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
     80     private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
     81     private static final int GAL_PHONE_FILTER = GAL_BASE + 5;
     82 
     83     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     84     /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>();
     85 
     86     static {
     87         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES);
     88         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
     89         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
     90         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
     91                 GAL_CONTACT_WITH_ID);
     92         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
     93         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/phones/filter/*", GAL_PHONE_FILTER);
     94 
     95     }
     96 
     97     @Override
     98     public boolean onCreate() {
     99         EmailContent.init(getContext());
    100         return true;
    101     }
    102 
    103     static class GalProjection {
    104         final int size;
    105         final HashMap<String, Integer> columnMap = new HashMap<String, Integer>();
    106 
    107         GalProjection(String[] projection) {
    108             size = projection.length;
    109             for (int i = 0; i < projection.length; i++) {
    110                 columnMap.put(projection[i], i);
    111             }
    112         }
    113     }
    114 
    115     static class GalContactRow {
    116         private final GalProjection mProjection;
    117         private Object[] row;
    118         static long dataId = 1;
    119 
    120         GalContactRow(GalProjection projection, long contactId, String accountName,
    121                 String displayName) {
    122             this.mProjection = projection;
    123             row = new Object[projection.size];
    124 
    125             put(Contacts.Entity.CONTACT_ID, contactId);
    126 
    127             // We only have one raw contact per aggregate, so they can have the same ID
    128             put(Contacts.Entity.RAW_CONTACT_ID, contactId);
    129             put(Contacts.Entity.DATA_ID, dataId++);
    130 
    131             put(Contacts.DISPLAY_NAME, displayName);
    132 
    133             // TODO alternative display name
    134             put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
    135 
    136             put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    137             put(RawContacts.ACCOUNT_NAME, accountName);
    138             put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
    139             put(Data.IS_READ_ONLY, 1);
    140         }
    141 
    142         Object[] getRow () {
    143             return row;
    144         }
    145 
    146         void put(String columnName, Object value) {
    147             final Integer integer = mProjection.columnMap.get(columnName);
    148             if (integer != null) {
    149                 row[integer] = value;
    150             } else {
    151                 LogUtils.e(TAG, "Unsupported column: " + columnName);
    152             }
    153         }
    154 
    155         static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
    156                 long contactId, String accountName, String displayName, String address) {
    157             if (!TextUtils.isEmpty(address)) {
    158                 final GalContactRow r = new GalContactRow(
    159                         galProjection, contactId, accountName, displayName);
    160                 r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
    161                 r.put(Email.TYPE, Email.TYPE_WORK);
    162                 r.put(Email.ADDRESS, address);
    163                 cursor.addRow(r.getRow());
    164             }
    165         }
    166 
    167         static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
    168                 String accountName, String displayName, int type, String number) {
    169             if (!TextUtils.isEmpty(number)) {
    170                 final GalContactRow r = new GalContactRow(
    171                         projection, contactId, accountName, displayName);
    172                 r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    173                 r.put(Phone.TYPE, type);
    174                 r.put(Phone.NUMBER, number);
    175                 cursor.addRow(r.getRow());
    176             }
    177         }
    178 
    179         public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
    180                 long contactId, String accountName, String displayName,
    181                 String firstName, String lastName) {
    182             final GalContactRow r = new GalContactRow(
    183                     galProjection, contactId, accountName, displayName);
    184             r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    185             r.put(StructuredName.GIVEN_NAME, firstName);
    186             r.put(StructuredName.FAMILY_NAME, lastName);
    187             r.put(StructuredName.DISPLAY_NAME, displayName);
    188             cursor.addRow(r.getRow());
    189         }
    190     }
    191 
    192     /**
    193      * Find the record id of an Account, given its name (email address)
    194      * @param accountName the name of the account
    195      * @return the record id of the Account, or -1 if not found
    196      */
    197     /*package*/ long getAccountIdByName(Context context, String accountName) {
    198         Long accountId = mAccountIdMap.get(accountName);
    199         if (accountId == null) {
    200             accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI,
    201                     EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
    202                     new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L);
    203             if (accountId != -1) {
    204                 mAccountIdMap.put(accountName, accountId);
    205             }
    206         }
    207         return accountId;
    208     }
    209 
    210     @Override
    211     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    212             String sortOrder) {
    213         LogUtils.d(TAG, "ExchangeDirectoryProvider: query: %s", uri.toString());
    214         final int match = sURIMatcher.match(uri);
    215         final MatrixCursor cursor;
    216         Object[] row;
    217         final PackedString ps;
    218         final String lookupKey;
    219 
    220         switch (match) {
    221             case GAL_DIRECTORIES: {
    222                 // Assuming that GAL can be used with all exchange accounts
    223                 final android.accounts.Account[] accounts = AccountManager.get(getContext())
    224                         .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    225                 cursor = new MatrixCursor(projection);
    226                 if (accounts != null) {
    227                     for (android.accounts.Account account : accounts) {
    228                         row = new Object[projection.length];
    229 
    230                         for (int i = 0; i < projection.length; i++) {
    231                             final String column = projection[i];
    232                             if (column.equals(Directory.ACCOUNT_NAME)) {
    233                                 row[i] = account.name;
    234                             } else if (column.equals(Directory.ACCOUNT_TYPE)) {
    235                                 row[i] = account.type;
    236                             } else if (column.equals(Directory.TYPE_RESOURCE_ID)) {
    237                                 final String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
    238                                 final Bundle bundle = new AccountServiceProxy(getContext())
    239                                     .getConfigurationData(accountType);
    240                                 // Default to the alternative name, erring on the conservative side
    241                                 int exchangeName = R.string.exchange_name_alternate;
    242                                 if (bundle != null && !bundle.getBoolean(
    243                                         Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS,
    244                                         true)) {
    245                                     exchangeName = R.string.exchange_name;
    246                                 }
    247                                 row[i] = exchangeName;
    248                             } else if (column.equals(Directory.DISPLAY_NAME)) {
    249                                 // If the account name is an email address, extract
    250                                 // the domain name and use it as the directory display name
    251                                 final String accountName = account.name;
    252                                 final int atIndex = accountName.indexOf('@');
    253                                 if (atIndex != -1 && atIndex < accountName.length() - 2) {
    254                                     final char firstLetter = Character.toUpperCase(
    255                                             accountName.charAt(atIndex + 1));
    256                                     row[i] = firstLetter + accountName.substring(atIndex + 2);
    257                                 } else {
    258                                     row[i] = account.name;
    259                                 }
    260                             } else if (column.equals(Directory.EXPORT_SUPPORT)) {
    261                                 row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY;
    262                             } else if (column.equals(Directory.SHORTCUT_SUPPORT)) {
    263                                 row[i] = Directory.SHORTCUT_SUPPORT_NONE;
    264                             }
    265                         }
    266                         cursor.addRow(row);
    267                     }
    268                 }
    269                 return cursor;
    270             }
    271 
    272             case GAL_FILTER:
    273             case GAL_PHONE_FILTER:
    274             case GAL_EMAIL_FILTER: {
    275                 final String filter = uri.getLastPathSegment();
    276                 // We should have at least two characters before doing a GAL search
    277                 if (filter == null || filter.length() < 2) {
    278                     return null;
    279                 }
    280 
    281                 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
    282                 if (accountName == null) {
    283                     return null;
    284                 }
    285 
    286                 // Enforce a limit on the number of lookup responses
    287                 final String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
    288                 int limit = DEFAULT_LOOKUP_LIMIT;
    289                 if (limitString != null) {
    290                     try {
    291                         limit = Integer.parseInt(limitString);
    292                     } catch (NumberFormatException e) {
    293                         limit = 0;
    294                     }
    295                     if (limit <= 0) {
    296                         throw new IllegalArgumentException("Limit not valid: " + limitString);
    297                     }
    298                 }
    299 
    300                 final long callingId = Binder.clearCallingIdentity();
    301                 try {
    302                     // Find the account id to pass along to EasSyncService
    303                     final long accountId = getAccountIdByName(getContext(), accountName);
    304                     if (accountId == -1) {
    305                         // The account was deleted?
    306                         return null;
    307                     }
    308 
    309                     // Get results from the Exchange account
    310                     final GalResult galResult = EasSyncService.searchGal(getContext(), accountId,
    311                             filter, limit);
    312                     if (galResult != null) {
    313                         return buildGalResultCursor(
    314                                 projection, galResult, match == GAL_PHONE_FILTER, sortOrder);
    315                     }
    316                 } finally {
    317                     Binder.restoreCallingIdentity(callingId);
    318                 }
    319                 break;
    320             }
    321 
    322             case GAL_CONTACT:
    323             case GAL_CONTACT_WITH_ID: {
    324                 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
    325                 if (accountName == null) {
    326                     return null;
    327                 }
    328 
    329                 final GalProjection galProjection = new GalProjection(projection);
    330                 cursor = new MatrixCursor(projection);
    331                 // Handle the decomposition of the key into rows suitable for CP2
    332                 final List<String> pathSegments = uri.getPathSegments();
    333                 lookupKey = pathSegments.get(2);
    334                 final long contactId = (match == GAL_CONTACT_WITH_ID)
    335                         ? Long.parseLong(pathSegments.get(3))
    336                         : DEFAULT_CONTACT_ID;
    337                 ps = new PackedString(lookupKey);
    338                 final String displayName = ps.get(GalData.DISPLAY_NAME);
    339                 GalContactRow.addEmailAddress(cursor, galProjection, contactId,
    340                         accountName, displayName, ps.get(GalData.EMAIL_ADDRESS));
    341                 GalContactRow.addPhoneRow(cursor, galProjection, contactId,
    342                         displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE));
    343                 GalContactRow.addPhoneRow(cursor, galProjection, contactId,
    344                         displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE));
    345                 GalContactRow.addPhoneRow(cursor, galProjection, contactId,
    346                         displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE));
    347                 GalContactRow.addNameRow(cursor, galProjection, contactId, displayName,
    348                         ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName);
    349                 return cursor;
    350             }
    351         }
    352 
    353         return null;
    354     }
    355 
    356     /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult,
    357             boolean isPhoneFilter, String sortOrder) {
    358         int displayNameIndex = -1;
    359         int displayNameSourceIndex = -1;
    360         int alternateDisplayNameIndex = -1;
    361         int emailIndex = -1;
    362         int emailTypeIndex = -1;
    363         int phoneNumberIndex = -1;
    364         int phoneTypeIndex = -1;
    365         int hasPhoneNumberIndex = -1;
    366         int idIndex = -1;
    367         int contactIdIndex = -1;
    368         int lookupIndex = -1;
    369 
    370         for (int i = 0; i < projection.length; i++) {
    371             final String column = projection[i];
    372             if (Contacts.DISPLAY_NAME.equals(column) ||
    373                     Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
    374                 displayNameIndex = i;
    375             } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
    376                 alternateDisplayNameIndex = i;
    377             } else if (Contacts.DISPLAY_NAME_SOURCE.equals(column)) {
    378                 displayNameSourceIndex = i;
    379             } else if (Contacts.HAS_PHONE_NUMBER.equals(column)) {
    380                 hasPhoneNumberIndex = i;
    381             } else if (Contacts._ID.equals(column)) {
    382                 idIndex = i;
    383             } else if (Phone.CONTACT_ID.equals(column)) {
    384                 contactIdIndex = i;
    385             } else if (Contacts.LOOKUP_KEY.equals(column)) {
    386                 lookupIndex = i;
    387             } else if (isPhoneFilter) {
    388                 if (Phone.NUMBER.equals(column)) {
    389                     phoneNumberIndex = i;
    390                 } else if (Phone.TYPE.equals(column)) {
    391                     phoneTypeIndex = i;
    392                 }
    393             } else {
    394                 // Cannot support for Email and Phone in same query, so default
    395                 // is to return email addresses.
    396                 if (Email.ADDRESS.equals(column)) {
    397                     emailIndex = i;
    398                 } else if (Email.TYPE.equals(column)) {
    399                     emailTypeIndex = i;
    400                 }
    401             }
    402         }
    403 
    404         final boolean useAlternateSortKey = Contacts.SORT_KEY_ALTERNATIVE.equals(sortOrder);
    405 
    406         final TreeMap<GalSortKey, Object[]> sortedResultsMap =
    407                 new TreeMap<GalSortKey, Object[]>(new NameComparator());
    408 
    409         // id populates the _ID column and is incremented for each row in the
    410         // result set, so each row has a unique id.
    411         int id = 1;
    412         // contactId populates the CONTACT_ID column and is incremented for
    413         // each contact. For the email and phone filters, there may be more
    414         // than one row with the same contactId if a given contact has multiple
    415         // email addresses or multiple phone numbers.
    416         int contactId = 1;
    417 
    418         final Object[] row = new Object[projection.length];
    419         final int count = galResult.galData.size();
    420         for (int i = 0; i < count; i++) {
    421             final GalData galDataRow = galResult.galData.get(i);
    422             final String firstName = galDataRow.get(GalData.FIRST_NAME);
    423             final String lastName = galDataRow.get(GalData.LAST_NAME);
    424             String displayName = galDataRow.get(GalData.DISPLAY_NAME);
    425             final List<PhoneInfo> phones = new ArrayList<PhoneInfo>();
    426 
    427             addPhoneInfo(phones, galDataRow.get(GalData.WORK_PHONE), Phone.TYPE_WORK);
    428             addPhoneInfo(phones, galDataRow.get(GalData.OFFICE), Phone.TYPE_COMPANY_MAIN);
    429             addPhoneInfo(phones, galDataRow.get(GalData.HOME_PHONE), Phone.TYPE_HOME);
    430             addPhoneInfo(phones, galDataRow.get(GalData.MOBILE_PHONE), Phone.TYPE_MOBILE);
    431 
    432             // If we don't have a display name, try to create one using first and last name
    433             if (displayName == null) {
    434                 if (firstName != null && lastName != null) {
    435                     displayName = firstName + " " + lastName;
    436                 } else if (firstName != null) {
    437                     displayName = firstName;
    438                 } else if (lastName != null) {
    439                     displayName = lastName;
    440                 }
    441             }
    442             galDataRow.put(GalData.DISPLAY_NAME, displayName);
    443 
    444             if (displayNameIndex != -1) {
    445                 row[displayNameIndex] = displayName;
    446             }
    447 
    448             // Try to create an alternate display name, using first and last name
    449             // TODO: Check with Contacts team to make sure we're using this properly
    450             final String alternateDisplayName;
    451             if (firstName != null && lastName != null) {
    452                 alternateDisplayName = lastName + " " + firstName;
    453             } else {
    454                 alternateDisplayName = displayName;
    455             }
    456 
    457             if (alternateDisplayNameIndex != -1) {
    458                 row[alternateDisplayNameIndex] = alternateDisplayName;
    459             }
    460 
    461             if (displayNameSourceIndex >= 0) {
    462                 row[displayNameSourceIndex] = DisplayNameSources.STRUCTURED_NAME;
    463             }
    464 
    465             final String sortName = useAlternateSortKey ? alternateDisplayName : displayName;
    466 
    467             if (hasPhoneNumberIndex >= 0) {
    468                 if (phones.size() > 0) {
    469                     row[hasPhoneNumberIndex] = true;
    470                 }
    471             }
    472 
    473             if (contactIdIndex != -1) {
    474                 row[contactIdIndex] = contactId;
    475             }
    476 
    477             if (lookupIndex != -1) {
    478                 // We use the packed string as our lookup key; it contains ALL of the gal data
    479                 // We do this because we are not able to provide a stable id to ContactsProvider
    480                 row[lookupIndex] = Uri.encode(galDataRow.toPackedString());
    481             }
    482 
    483             if (isPhoneFilter) {
    484                 final Set<String> uniqueNumbers = new HashSet<String>();
    485 
    486                 for (PhoneInfo phone : phones) {
    487                     if (!uniqueNumbers.add(phone.mNumber)) {
    488                         continue;
    489                     }
    490                     if (phoneNumberIndex >= 0) {
    491                         row[phoneNumberIndex] = phone.mNumber;
    492                     }
    493                     if (phoneTypeIndex >= 0) {
    494                         row[phoneTypeIndex] = phone.mType;
    495                     }
    496                     if (idIndex != -1) {
    497                         row[idIndex] = id;
    498                     }
    499                     sortedResultsMap.put(new GalSortKey(sortName, id), row.clone());
    500                     id++;
    501                 }
    502 
    503             } else {
    504                 if (emailIndex != -1) {
    505                     row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
    506                 }
    507                 if (emailTypeIndex >= 0) {
    508                     row[emailTypeIndex] = Email.TYPE_WORK;
    509                 }
    510 
    511                 if (idIndex != -1) {
    512                     row[idIndex] = id;
    513                 }
    514                 sortedResultsMap.put(new GalSortKey(sortName, id), row.clone());
    515                 id++;
    516             }
    517             contactId++;
    518         }
    519         final MatrixCursor cursor = new MatrixCursor(projection, sortedResultsMap.size());
    520         for(Object[] result : sortedResultsMap.values()) {
    521             cursor.addRow(result);
    522         }
    523 
    524         return cursor;
    525     }
    526 
    527     private void addPhoneInfo(List<PhoneInfo> phones, String number, int type) {
    528         if (!TextUtils.isEmpty(number)) {
    529             phones.add(new PhoneInfo(number, type));
    530         }
    531     }
    532 
    533     @Override
    534     public String getType(Uri uri) {
    535         final int match = sURIMatcher.match(uri);
    536         switch (match) {
    537             case GAL_FILTER:
    538                 return Contacts.CONTENT_ITEM_TYPE;
    539         }
    540         return null;
    541     }
    542 
    543     @Override
    544     public int delete(Uri uri, String selection, String[] selectionArgs) {
    545         throw new UnsupportedOperationException();
    546     }
    547 
    548     @Override
    549     public Uri insert(Uri uri, ContentValues values) {
    550         throw new UnsupportedOperationException();
    551     }
    552 
    553     @Override
    554     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    555         throw new UnsupportedOperationException();
    556     }
    557 
    558     /**
    559      * Sort key for Gal filter results.
    560      *  - primary key is name
    561      *      for SORT_KEY_PRIMARY, this is displayName
    562      *      for SORT_KEY_ALTERNATIVE, this is alternativeDisplayName
    563      *      if no sort order is specified, this key is empty
    564      *  - secondary key is id, so ordering of the original results are
    565      *      preserved both between contacts with the same name and for
    566      *      multiple results within a given contact
    567      */
    568     private static class GalSortKey {
    569         final String sortName;
    570         final int    id;
    571 
    572         public GalSortKey(String sortName, int id) {
    573             this.sortName = sortName;
    574             this.id = id;
    575         }
    576     }
    577 
    578     private static class NameComparator implements Comparator<GalSortKey> {
    579         private final Collator collator;
    580 
    581         public NameComparator() {
    582             collator = Collator.getInstance();
    583             // Case insensitive sorting
    584             collator.setStrength(Collator.SECONDARY);
    585         }
    586 
    587         @Override
    588         public int compare(GalSortKey lhs, GalSortKey rhs) {
    589             final int res = collator.compare(lhs.sortName, rhs.sortName);
    590             if (res != 0) {
    591                 return res;
    592             }
    593             if (lhs.id != rhs.id) {
    594                 return lhs.id > rhs.id ? 1 : -1;
    595             }
    596             return 0;
    597         }
    598     }
    599 
    600     private static class PhoneInfo {
    601         private String mNumber;
    602         private int mType;
    603 
    604         private PhoneInfo(String number, int type) {
    605             mNumber = number;
    606             mType = type;
    607         }
    608     }
    609 }
    610