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