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 40 import com.android.emailcommon.Configuration; 41 import com.android.emailcommon.mail.PackedString; 42 import com.android.emailcommon.provider.Account; 43 import com.android.emailcommon.provider.EmailContent; 44 import com.android.emailcommon.provider.EmailContent.AccountColumns; 45 import com.android.emailcommon.service.AccountServiceProxy; 46 import com.android.emailcommon.utility.Utility; 47 import com.android.exchange.Eas; 48 import com.android.exchange.EasSyncService; 49 import com.android.exchange.R; 50 import com.android.exchange.provider.GalResult.GalData; 51 import com.android.mail.utils.LogUtils; 52 53 import java.text.Collator; 54 import java.util.ArrayList; 55 import java.util.Comparator; 56 import java.util.HashMap; 57 import java.util.HashSet; 58 import java.util.List; 59 import java.util.Set; 60 import java.util.TreeMap; 61 62 /** 63 * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is 64 * used solely to provide GAL (Global Address Lookup) service to email address adapters 65 */ 66 public class ExchangeDirectoryProvider extends ContentProvider { 67 private static final String TAG = Eas.LOG_TAG; 68 69 public static final String EXCHANGE_GAL_AUTHORITY = 70 com.android.exchange.Configuration.EXCHANGE_GAL_AUTHORITY; 71 72 private static final int DEFAULT_CONTACT_ID = 1; 73 private static final int DEFAULT_LOOKUP_LIMIT = 20; 74 75 private static final int GAL_BASE = 0; 76 private static final int GAL_DIRECTORIES = GAL_BASE; 77 private static final int GAL_FILTER = GAL_BASE + 1; 78 private static final int GAL_CONTACT = GAL_BASE + 2; 79 private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3; 80 private static final int GAL_EMAIL_FILTER = GAL_BASE + 4; 81 private static final int GAL_PHONE_FILTER = GAL_BASE + 5; 82 83 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 84 /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>(); 85 86 static { 87 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES); 88 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER); 89 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT); 90 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities", 91 GAL_CONTACT_WITH_ID); 92 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER); 93 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/phones/filter/*", GAL_PHONE_FILTER); 94 95 } 96 97 @Override 98 public boolean onCreate() { 99 EmailContent.init(getContext()); 100 return true; 101 } 102 103 static class GalProjection { 104 final int size; 105 final HashMap<String, Integer> columnMap = new HashMap<String, Integer>(); 106 107 GalProjection(String[] projection) { 108 size = projection.length; 109 for (int i = 0; i < projection.length; i++) { 110 columnMap.put(projection[i], i); 111 } 112 } 113 } 114 115 static class GalContactRow { 116 private final GalProjection mProjection; 117 private Object[] row; 118 static long dataId = 1; 119 120 GalContactRow(GalProjection projection, long contactId, String accountName, 121 String displayName) { 122 this.mProjection = projection; 123 row = new Object[projection.size]; 124 125 put(Contacts.Entity.CONTACT_ID, contactId); 126 127 // We only have one raw contact per aggregate, so they can have the same ID 128 put(Contacts.Entity.RAW_CONTACT_ID, contactId); 129 put(Contacts.Entity.DATA_ID, dataId++); 130 131 put(Contacts.DISPLAY_NAME, displayName); 132 133 // TODO alternative display name 134 put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName); 135 136 put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 137 put(RawContacts.ACCOUNT_NAME, accountName); 138 put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1); 139 put(Data.IS_READ_ONLY, 1); 140 } 141 142 Object[] getRow () { 143 return row; 144 } 145 146 void put(String columnName, Object value) { 147 final Integer integer = mProjection.columnMap.get(columnName); 148 if (integer != null) { 149 row[integer] = value; 150 } else { 151 LogUtils.e(TAG, "Unsupported column: " + columnName); 152 } 153 } 154 155 static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection, 156 long contactId, String accountName, String displayName, String address) { 157 if (!TextUtils.isEmpty(address)) { 158 final GalContactRow r = new GalContactRow( 159 galProjection, contactId, accountName, displayName); 160 r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); 161 r.put(Email.TYPE, Email.TYPE_WORK); 162 r.put(Email.ADDRESS, address); 163 cursor.addRow(r.getRow()); 164 } 165 } 166 167 static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId, 168 String accountName, String displayName, int type, String number) { 169 if (!TextUtils.isEmpty(number)) { 170 final GalContactRow r = new GalContactRow( 171 projection, contactId, accountName, displayName); 172 r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 173 r.put(Phone.TYPE, type); 174 r.put(Phone.NUMBER, number); 175 cursor.addRow(r.getRow()); 176 } 177 } 178 179 public static void addNameRow(MatrixCursor cursor, GalProjection galProjection, 180 long contactId, String accountName, String displayName, 181 String firstName, String lastName) { 182 final GalContactRow r = new GalContactRow( 183 galProjection, contactId, accountName, displayName); 184 r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 185 r.put(StructuredName.GIVEN_NAME, firstName); 186 r.put(StructuredName.FAMILY_NAME, lastName); 187 r.put(StructuredName.DISPLAY_NAME, displayName); 188 cursor.addRow(r.getRow()); 189 } 190 } 191 192 /** 193 * Find the record id of an Account, given its name (email address) 194 * @param accountName the name of the account 195 * @return the record id of the Account, or -1 if not found 196 */ 197 /*package*/ long getAccountIdByName(Context context, String accountName) { 198 Long accountId = mAccountIdMap.get(accountName); 199 if (accountId == null) { 200 accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI, 201 EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", 202 new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L); 203 if (accountId != -1) { 204 mAccountIdMap.put(accountName, accountId); 205 } 206 } 207 return accountId; 208 } 209 210 @Override 211 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 212 String sortOrder) { 213 LogUtils.d(TAG, "ExchangeDirectoryProvider: query: %s", uri.toString()); 214 final int match = sURIMatcher.match(uri); 215 final MatrixCursor cursor; 216 Object[] row; 217 final PackedString ps; 218 final String lookupKey; 219 220 switch (match) { 221 case GAL_DIRECTORIES: { 222 // Assuming that GAL can be used with all exchange accounts 223 final android.accounts.Account[] accounts = AccountManager.get(getContext()) 224 .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 225 cursor = new MatrixCursor(projection); 226 if (accounts != null) { 227 for (android.accounts.Account account : accounts) { 228 row = new Object[projection.length]; 229 230 for (int i = 0; i < projection.length; i++) { 231 final String column = projection[i]; 232 if (column.equals(Directory.ACCOUNT_NAME)) { 233 row[i] = account.name; 234 } else if (column.equals(Directory.ACCOUNT_TYPE)) { 235 row[i] = account.type; 236 } else if (column.equals(Directory.TYPE_RESOURCE_ID)) { 237 final String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE; 238 final Bundle bundle = new AccountServiceProxy(getContext()) 239 .getConfigurationData(accountType); 240 // Default to the alternative name, erring on the conservative side 241 int exchangeName = R.string.exchange_name_alternate; 242 if (bundle != null && !bundle.getBoolean( 243 Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS, 244 true)) { 245 exchangeName = R.string.exchange_name; 246 } 247 row[i] = exchangeName; 248 } else if (column.equals(Directory.DISPLAY_NAME)) { 249 // If the account name is an email address, extract 250 // the domain name and use it as the directory display name 251 final String accountName = account.name; 252 final int atIndex = accountName.indexOf('@'); 253 if (atIndex != -1 && atIndex < accountName.length() - 2) { 254 final char firstLetter = Character.toUpperCase( 255 accountName.charAt(atIndex + 1)); 256 row[i] = firstLetter + accountName.substring(atIndex + 2); 257 } else { 258 row[i] = account.name; 259 } 260 } else if (column.equals(Directory.EXPORT_SUPPORT)) { 261 row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY; 262 } else if (column.equals(Directory.SHORTCUT_SUPPORT)) { 263 row[i] = Directory.SHORTCUT_SUPPORT_NONE; 264 } 265 } 266 cursor.addRow(row); 267 } 268 } 269 return cursor; 270 } 271 272 case GAL_FILTER: 273 case GAL_PHONE_FILTER: 274 case GAL_EMAIL_FILTER: { 275 final String filter = uri.getLastPathSegment(); 276 // We should have at least two characters before doing a GAL search 277 if (filter == null || filter.length() < 2) { 278 return null; 279 } 280 281 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 282 if (accountName == null) { 283 return null; 284 } 285 286 // Enforce a limit on the number of lookup responses 287 final String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY); 288 int limit = DEFAULT_LOOKUP_LIMIT; 289 if (limitString != null) { 290 try { 291 limit = Integer.parseInt(limitString); 292 } catch (NumberFormatException e) { 293 limit = 0; 294 } 295 if (limit <= 0) { 296 throw new IllegalArgumentException("Limit not valid: " + limitString); 297 } 298 } 299 300 final long callingId = Binder.clearCallingIdentity(); 301 try { 302 // Find the account id to pass along to EasSyncService 303 final long accountId = getAccountIdByName(getContext(), accountName); 304 if (accountId == -1) { 305 // The account was deleted? 306 return null; 307 } 308 309 // Get results from the Exchange account 310 final GalResult galResult = EasSyncService.searchGal(getContext(), accountId, 311 filter, limit); 312 if (galResult != null) { 313 return buildGalResultCursor( 314 projection, galResult, match == GAL_PHONE_FILTER, sortOrder); 315 } 316 } finally { 317 Binder.restoreCallingIdentity(callingId); 318 } 319 break; 320 } 321 322 case GAL_CONTACT: 323 case GAL_CONTACT_WITH_ID: { 324 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 325 if (accountName == null) { 326 return null; 327 } 328 329 final GalProjection galProjection = new GalProjection(projection); 330 cursor = new MatrixCursor(projection); 331 // Handle the decomposition of the key into rows suitable for CP2 332 final List<String> pathSegments = uri.getPathSegments(); 333 lookupKey = pathSegments.get(2); 334 final long contactId = (match == GAL_CONTACT_WITH_ID) 335 ? Long.parseLong(pathSegments.get(3)) 336 : DEFAULT_CONTACT_ID; 337 ps = new PackedString(lookupKey); 338 final String displayName = ps.get(GalData.DISPLAY_NAME); 339 GalContactRow.addEmailAddress(cursor, galProjection, contactId, 340 accountName, displayName, ps.get(GalData.EMAIL_ADDRESS)); 341 GalContactRow.addPhoneRow(cursor, galProjection, contactId, 342 displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE)); 343 GalContactRow.addPhoneRow(cursor, galProjection, contactId, 344 displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE)); 345 GalContactRow.addPhoneRow(cursor, galProjection, contactId, 346 displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE)); 347 GalContactRow.addNameRow(cursor, galProjection, contactId, displayName, 348 ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName); 349 return cursor; 350 } 351 } 352 353 return null; 354 } 355 356 /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult, 357 boolean isPhoneFilter, String sortOrder) { 358 int displayNameIndex = -1; 359 int displayNameSourceIndex = -1; 360 int alternateDisplayNameIndex = -1; 361 int emailIndex = -1; 362 int emailTypeIndex = -1; 363 int phoneNumberIndex = -1; 364 int phoneTypeIndex = -1; 365 int hasPhoneNumberIndex = -1; 366 int idIndex = -1; 367 int contactIdIndex = -1; 368 int lookupIndex = -1; 369 370 for (int i = 0; i < projection.length; i++) { 371 final String column = projection[i]; 372 if (Contacts.DISPLAY_NAME.equals(column) || 373 Contacts.DISPLAY_NAME_PRIMARY.equals(column)) { 374 displayNameIndex = i; 375 } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) { 376 alternateDisplayNameIndex = i; 377 } else if (Contacts.DISPLAY_NAME_SOURCE.equals(column)) { 378 displayNameSourceIndex = i; 379 } else if (Contacts.HAS_PHONE_NUMBER.equals(column)) { 380 hasPhoneNumberIndex = i; 381 } else if (Contacts._ID.equals(column)) { 382 idIndex = i; 383 } else if (Phone.CONTACT_ID.equals(column)) { 384 contactIdIndex = i; 385 } else if (Contacts.LOOKUP_KEY.equals(column)) { 386 lookupIndex = i; 387 } else if (isPhoneFilter) { 388 if (Phone.NUMBER.equals(column)) { 389 phoneNumberIndex = i; 390 } else if (Phone.TYPE.equals(column)) { 391 phoneTypeIndex = i; 392 } 393 } else { 394 // Cannot support for Email and Phone in same query, so default 395 // is to return email addresses. 396 if (Email.ADDRESS.equals(column)) { 397 emailIndex = i; 398 } else if (Email.TYPE.equals(column)) { 399 emailTypeIndex = i; 400 } 401 } 402 } 403 404 final boolean useAlternateSortKey = Contacts.SORT_KEY_ALTERNATIVE.equals(sortOrder); 405 406 final TreeMap<GalSortKey, Object[]> sortedResultsMap = 407 new TreeMap<GalSortKey, Object[]>(new NameComparator()); 408 409 // id populates the _ID column and is incremented for each row in the 410 // result set, so each row has a unique id. 411 int id = 1; 412 // contactId populates the CONTACT_ID column and is incremented for 413 // each contact. For the email and phone filters, there may be more 414 // than one row with the same contactId if a given contact has multiple 415 // email addresses or multiple phone numbers. 416 int contactId = 1; 417 418 final Object[] row = new Object[projection.length]; 419 final int count = galResult.galData.size(); 420 for (int i = 0; i < count; i++) { 421 final GalData galDataRow = galResult.galData.get(i); 422 final String firstName = galDataRow.get(GalData.FIRST_NAME); 423 final String lastName = galDataRow.get(GalData.LAST_NAME); 424 String displayName = galDataRow.get(GalData.DISPLAY_NAME); 425 final List<PhoneInfo> phones = new ArrayList<PhoneInfo>(); 426 427 addPhoneInfo(phones, galDataRow.get(GalData.WORK_PHONE), Phone.TYPE_WORK); 428 addPhoneInfo(phones, galDataRow.get(GalData.OFFICE), Phone.TYPE_COMPANY_MAIN); 429 addPhoneInfo(phones, galDataRow.get(GalData.HOME_PHONE), Phone.TYPE_HOME); 430 addPhoneInfo(phones, galDataRow.get(GalData.MOBILE_PHONE), Phone.TYPE_MOBILE); 431 432 // If we don't have a display name, try to create one using first and last name 433 if (displayName == null) { 434 if (firstName != null && lastName != null) { 435 displayName = firstName + " " + lastName; 436 } else if (firstName != null) { 437 displayName = firstName; 438 } else if (lastName != null) { 439 displayName = lastName; 440 } 441 } 442 galDataRow.put(GalData.DISPLAY_NAME, displayName); 443 444 if (displayNameIndex != -1) { 445 row[displayNameIndex] = displayName; 446 } 447 448 // Try to create an alternate display name, using first and last name 449 // TODO: Check with Contacts team to make sure we're using this properly 450 final String alternateDisplayName; 451 if (firstName != null && lastName != null) { 452 alternateDisplayName = lastName + " " + firstName; 453 } else { 454 alternateDisplayName = displayName; 455 } 456 457 if (alternateDisplayNameIndex != -1) { 458 row[alternateDisplayNameIndex] = alternateDisplayName; 459 } 460 461 if (displayNameSourceIndex >= 0) { 462 row[displayNameSourceIndex] = DisplayNameSources.STRUCTURED_NAME; 463 } 464 465 final String sortName = useAlternateSortKey ? alternateDisplayName : displayName; 466 467 if (hasPhoneNumberIndex >= 0) { 468 if (phones.size() > 0) { 469 row[hasPhoneNumberIndex] = true; 470 } 471 } 472 473 if (contactIdIndex != -1) { 474 row[contactIdIndex] = contactId; 475 } 476 477 if (lookupIndex != -1) { 478 // We use the packed string as our lookup key; it contains ALL of the gal data 479 // We do this because we are not able to provide a stable id to ContactsProvider 480 row[lookupIndex] = Uri.encode(galDataRow.toPackedString()); 481 } 482 483 if (isPhoneFilter) { 484 final Set<String> uniqueNumbers = new HashSet<String>(); 485 486 for (PhoneInfo phone : phones) { 487 if (!uniqueNumbers.add(phone.mNumber)) { 488 continue; 489 } 490 if (phoneNumberIndex >= 0) { 491 row[phoneNumberIndex] = phone.mNumber; 492 } 493 if (phoneTypeIndex >= 0) { 494 row[phoneTypeIndex] = phone.mType; 495 } 496 if (idIndex != -1) { 497 row[idIndex] = id; 498 } 499 sortedResultsMap.put(new GalSortKey(sortName, id), row.clone()); 500 id++; 501 } 502 503 } else { 504 if (emailIndex != -1) { 505 row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS); 506 } 507 if (emailTypeIndex >= 0) { 508 row[emailTypeIndex] = Email.TYPE_WORK; 509 } 510 511 if (idIndex != -1) { 512 row[idIndex] = id; 513 } 514 sortedResultsMap.put(new GalSortKey(sortName, id), row.clone()); 515 id++; 516 } 517 contactId++; 518 } 519 final MatrixCursor cursor = new MatrixCursor(projection, sortedResultsMap.size()); 520 for(Object[] result : sortedResultsMap.values()) { 521 cursor.addRow(result); 522 } 523 524 return cursor; 525 } 526 527 private void addPhoneInfo(List<PhoneInfo> phones, String number, int type) { 528 if (!TextUtils.isEmpty(number)) { 529 phones.add(new PhoneInfo(number, type)); 530 } 531 } 532 533 @Override 534 public String getType(Uri uri) { 535 final int match = sURIMatcher.match(uri); 536 switch (match) { 537 case GAL_FILTER: 538 return Contacts.CONTENT_ITEM_TYPE; 539 } 540 return null; 541 } 542 543 @Override 544 public int delete(Uri uri, String selection, String[] selectionArgs) { 545 throw new UnsupportedOperationException(); 546 } 547 548 @Override 549 public Uri insert(Uri uri, ContentValues values) { 550 throw new UnsupportedOperationException(); 551 } 552 553 @Override 554 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 555 throw new UnsupportedOperationException(); 556 } 557 558 /** 559 * Sort key for Gal filter results. 560 * - primary key is name 561 * for SORT_KEY_PRIMARY, this is displayName 562 * for SORT_KEY_ALTERNATIVE, this is alternativeDisplayName 563 * if no sort order is specified, this key is empty 564 * - secondary key is id, so ordering of the original results are 565 * preserved both between contacts with the same name and for 566 * multiple results within a given contact 567 */ 568 private static class GalSortKey { 569 final String sortName; 570 final int id; 571 572 public GalSortKey(String sortName, int id) { 573 this.sortName = sortName; 574 this.id = id; 575 } 576 } 577 578 private static class NameComparator implements Comparator<GalSortKey> { 579 private final Collator collator; 580 581 public NameComparator() { 582 collator = Collator.getInstance(); 583 // Case insensitive sorting 584 collator.setStrength(Collator.SECONDARY); 585 } 586 587 @Override 588 public int compare(GalSortKey lhs, GalSortKey rhs) { 589 final int res = collator.compare(lhs.sortName, rhs.sortName); 590 if (res != 0) { 591 return res; 592 } 593 if (lhs.id != rhs.id) { 594 return lhs.id > rhs.id ? 1 : -1; 595 } 596 return 0; 597 } 598 } 599 600 private static class PhoneInfo { 601 private String mNumber; 602 private int mType; 603 604 private PhoneInfo(String number, int type) { 605 mNumber = number; 606 mType = type; 607 } 608 } 609 } 610