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