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
17package com.android.exchange.provider;
18
19import android.accounts.AccountManager;
20import android.content.ContentProvider;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.UriMatcher;
24import android.database.Cursor;
25import android.database.MatrixCursor;
26import android.net.Uri;
27import android.os.Binder;
28import android.os.Bundle;
29import android.provider.ContactsContract;
30import android.provider.ContactsContract.CommonDataKinds.Email;
31import android.provider.ContactsContract.CommonDataKinds.Phone;
32import android.provider.ContactsContract.CommonDataKinds.StructuredName;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Contacts.Data;
35import android.provider.ContactsContract.Directory;
36import android.provider.ContactsContract.DisplayNameSources;
37import android.provider.ContactsContract.RawContacts;
38import android.text.TextUtils;
39import android.util.Log;
40import android.util.Pair;
41
42import com.android.emailcommon.Configuration;
43import com.android.emailcommon.mail.PackedString;
44import com.android.emailcommon.provider.Account;
45import com.android.emailcommon.provider.EmailContent;
46import com.android.emailcommon.provider.EmailContent.AccountColumns;
47import com.android.emailcommon.service.AccountServiceProxy;
48import com.android.emailcommon.utility.Utility;
49import com.android.exchange.Eas;
50import com.android.exchange.R;
51import com.android.exchange.provider.GalResult.GalData;
52import com.android.exchange.service.EasService;
53import com.android.mail.utils.LogUtils;
54
55import java.text.Collator;
56import java.util.ArrayList;
57import java.util.Comparator;
58import java.util.HashMap;
59import java.util.HashSet;
60import java.util.List;
61import java.util.Set;
62import 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 */
68public 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 = EasService.searchGal(getContext(), accountId,
330                            filter, queryLimit);
331                    if (galResult != null && (galResult.getNumEntries() > 0)) {
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