• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES)91         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER)92         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT)93         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities", GAL_CONTACT_WITH_ID)94         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
95                 GAL_CONTACT_WITH_ID);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER)96         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/phones/filter/*", GAL_PHONE_FILTER)97         sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/phones/filter/*", GAL_PHONE_FILTER);
98 
99     }
100 
101     @Override
onCreate()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 
GalProjection(String[] projection)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 
GalContactRow(GalProjection projection, long contactId, String accountName, String displayName)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 
getRow()146         Object[] getRow () {
147             return row;
148         }
149 
put(String columnName, Object value)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 
addEmailAddress(MatrixCursor cursor, GalProjection galProjection, long contactId, String accountName, String displayName, String address)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 
addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId, String accountName, String displayName, int type, String number)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 
addNameRow(MatrixCursor cursor, GalProjection galProjection, long contactId, String accountName, String displayName, String firstName, String lastName)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      */
getAccountIdByName(Context context, String accountName)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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 
buildGalResultCursor(String[] projection, GalResult galResult, String sortOrder, int limit, boolean isEmailFilter, boolean isPhoneFilter)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      */
getDisplayName(GalData galDataRow, List<PhoneInfo> phones)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      */
getAlternateDisplayName(GalData galDataRow, String displayName)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 
addPhoneInfo(List<PhoneInfo> phones, String number, int type)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
getType(Uri uri)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
delete(Uri uri, String selection, String[] selectionArgs)633     public int delete(Uri uri, String selection, String[] selectionArgs) {
634         throw new UnsupportedOperationException();
635     }
636 
637     @Override
insert(Uri uri, ContentValues values)638     public Uri insert(Uri uri, ContentValues values) {
639         throw new UnsupportedOperationException();
640     }
641 
642     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)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 
GalSortKey(final String sortName, final int id)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 
NameComparator()673         public NameComparator() {
674             collator = Collator.getInstance();
675             // Case insensitive sorting
676             collator.setStrength(Collator.SECONDARY);
677         }
678 
679         @Override
compare(final GalSortKey lhs, final GalSortKey rhs)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 
PhoneInfo(String number, int type)704         private PhoneInfo(String number, int type) {
705             mNumber = number;
706             mType = type;
707         }
708     }
709 }
710