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