1 package com.android.exchange.adapter; 2 3 import android.content.ContentProviderOperation; 4 import android.content.ContentProviderOperation.Builder; 5 import android.content.ContentProviderResult; 6 import android.content.ContentResolver; 7 import android.content.ContentUris; 8 import android.content.ContentValues; 9 import android.content.Context; 10 import android.content.Entity; 11 import android.content.Entity.NamedContentValues; 12 import android.content.EntityIterator; 13 import android.content.OperationApplicationException; 14 import android.database.Cursor; 15 import android.net.Uri; 16 import android.os.RemoteException; 17 import android.provider.ContactsContract; 18 import android.provider.ContactsContract.CommonDataKinds.Email; 19 import android.provider.ContactsContract.CommonDataKinds.Event; 20 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 21 import android.provider.ContactsContract.CommonDataKinds.Im; 22 import android.provider.ContactsContract.CommonDataKinds.Nickname; 23 import android.provider.ContactsContract.CommonDataKinds.Note; 24 import android.provider.ContactsContract.CommonDataKinds.Organization; 25 import android.provider.ContactsContract.CommonDataKinds.Phone; 26 import android.provider.ContactsContract.CommonDataKinds.Photo; 27 import android.provider.ContactsContract.CommonDataKinds.Relation; 28 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 29 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 30 import android.provider.ContactsContract.CommonDataKinds.Website; 31 import android.provider.ContactsContract.Data; 32 import android.provider.ContactsContract.RawContacts; 33 import android.provider.ContactsContract.SyncState; 34 import android.provider.SyncStateContract; 35 import android.text.util.Rfc822Token; 36 import android.text.util.Rfc822Tokenizer; 37 import android.util.Base64; 38 39 import com.android.emailcommon.provider.Account; 40 import com.android.emailcommon.provider.Mailbox; 41 import com.android.emailcommon.utility.Utility; 42 import com.android.exchange.Eas; 43 import com.android.exchange.eas.EasSyncCollectionTypeBase; 44 import com.android.exchange.eas.EasSyncContacts; 45 import com.android.exchange.utility.CalendarUtilities; 46 import com.android.mail.utils.LogUtils; 47 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.util.ArrayList; 51 import java.util.GregorianCalendar; 52 import java.util.TimeZone; 53 54 public class ContactsSyncParser extends AbstractSyncParser { 55 private static final String TAG = Eas.LOG_TAG; 56 57 private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?"; 58 private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?"; 59 private static final String[] ID_PROJECTION = new String[] {RawContacts._ID}; 60 61 private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES 62 = new ArrayList<NamedContentValues>(); 63 64 private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW"; 65 66 private static final int MAX_IM_ROWS = 3; 67 private static final int MAX_EMAIL_ROWS = 3; 68 private static final int MAX_PHONE_ROWS = 2; 69 private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc. 70 private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row 71 72 String[] mBindArgument = new String[1]; 73 ContactOperations ops = new ContactOperations(); 74 private final android.accounts.Account mAccountManagerAccount; 75 private final Uri mAccountUri; 76 private boolean mGroupsUsed = false; 77 78 public ContactsSyncParser(final Context context, final ContentResolver resolver, 79 final InputStream in, final Mailbox mailbox, final Account account, 80 final android.accounts.Account accountManagerAccount) throws IOException { 81 super(context, resolver, in, mailbox, account); 82 mAccountManagerAccount = accountManagerAccount; 83 mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI, 84 mAccount.mEmailAddress); 85 } 86 87 public boolean isGroupsUsed() { 88 return mGroupsUsed; 89 } 90 91 public void addData(String serverId, ContactOperations ops, Entity entity) 92 throws IOException { 93 String prefix = null; 94 String firstName = null; 95 String lastName = null; 96 String middleName = null; 97 String suffix = null; 98 String companyName = null; 99 String yomiFirstName = null; 100 String yomiLastName = null; 101 String yomiCompanyName = null; 102 String title = null; 103 String department = null; 104 String officeLocation = null; 105 Address home = new Address(); 106 Address work = new Address(); 107 Address other = new Address(); 108 EasBusiness business = new EasBusiness(); 109 EasPersonal personal = new EasPersonal(); 110 ArrayList<String> children = new ArrayList<String>(); 111 ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>(); 112 ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>(); 113 ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>(); 114 ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>(); 115 if (entity == null) { 116 ops.newContact(serverId, mAccount.mEmailAddress); 117 } 118 119 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 120 switch (tag) { 121 case Tags.CONTACTS_FIRST_NAME: 122 firstName = getValue(); 123 break; 124 case Tags.CONTACTS_LAST_NAME: 125 lastName = getValue(); 126 break; 127 case Tags.CONTACTS_MIDDLE_NAME: 128 middleName = getValue(); 129 break; 130 case Tags.CONTACTS_SUFFIX: 131 suffix = getValue(); 132 break; 133 case Tags.CONTACTS_COMPANY_NAME: 134 companyName = getValue(); 135 break; 136 case Tags.CONTACTS_JOB_TITLE: 137 title = getValue(); 138 break; 139 case Tags.CONTACTS_EMAIL1_ADDRESS: 140 case Tags.CONTACTS_EMAIL2_ADDRESS: 141 case Tags.CONTACTS_EMAIL3_ADDRESS: 142 emails.add(new EmailRow(getValue())); 143 break; 144 case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER: 145 case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER: 146 workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK)); 147 break; 148 case Tags.CONTACTS2_MMS: 149 ops.addPhone(entity, Phone.TYPE_MMS, getValue()); 150 break; 151 case Tags.CONTACTS_BUSINESS_FAX_NUMBER: 152 ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue()); 153 break; 154 case Tags.CONTACTS2_COMPANY_MAIN_PHONE: 155 ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue()); 156 break; 157 case Tags.CONTACTS_HOME_FAX_NUMBER: 158 ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue()); 159 break; 160 case Tags.CONTACTS_HOME_TELEPHONE_NUMBER: 161 case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER: 162 homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME)); 163 break; 164 case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER: 165 ops.addPhone(entity, Phone.TYPE_MOBILE, getValue()); 166 break; 167 case Tags.CONTACTS_CAR_TELEPHONE_NUMBER: 168 ops.addPhone(entity, Phone.TYPE_CAR, getValue()); 169 break; 170 case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER: 171 ops.addPhone(entity, Phone.TYPE_RADIO, getValue()); 172 break; 173 case Tags.CONTACTS_PAGER_NUMBER: 174 ops.addPhone(entity, Phone.TYPE_PAGER, getValue()); 175 break; 176 case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER: 177 ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue()); 178 break; 179 case Tags.CONTACTS2_IM_ADDRESS: 180 case Tags.CONTACTS2_IM_ADDRESS_2: 181 case Tags.CONTACTS2_IM_ADDRESS_3: 182 ims.add(new ImRow(getValue())); 183 break; 184 case Tags.CONTACTS_BUSINESS_ADDRESS_CITY: 185 work.city = getValue(); 186 break; 187 case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY: 188 work.country = getValue(); 189 break; 190 case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE: 191 work.code = getValue(); 192 break; 193 case Tags.CONTACTS_BUSINESS_ADDRESS_STATE: 194 work.state = getValue(); 195 break; 196 case Tags.CONTACTS_BUSINESS_ADDRESS_STREET: 197 work.street = getValue(); 198 break; 199 case Tags.CONTACTS_HOME_ADDRESS_CITY: 200 home.city = getValue(); 201 break; 202 case Tags.CONTACTS_HOME_ADDRESS_COUNTRY: 203 home.country = getValue(); 204 break; 205 case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE: 206 home.code = getValue(); 207 break; 208 case Tags.CONTACTS_HOME_ADDRESS_STATE: 209 home.state = getValue(); 210 break; 211 case Tags.CONTACTS_HOME_ADDRESS_STREET: 212 home.street = getValue(); 213 break; 214 case Tags.CONTACTS_OTHER_ADDRESS_CITY: 215 other.city = getValue(); 216 break; 217 case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY: 218 other.country = getValue(); 219 break; 220 case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE: 221 other.code = getValue(); 222 break; 223 case Tags.CONTACTS_OTHER_ADDRESS_STATE: 224 other.state = getValue(); 225 break; 226 case Tags.CONTACTS_OTHER_ADDRESS_STREET: 227 other.street = getValue(); 228 break; 229 230 case Tags.CONTACTS_CHILDREN: 231 childrenParser(children); 232 break; 233 234 case Tags.CONTACTS_YOMI_COMPANY_NAME: 235 yomiCompanyName = getValue(); 236 break; 237 case Tags.CONTACTS_YOMI_FIRST_NAME: 238 yomiFirstName = getValue(); 239 break; 240 case Tags.CONTACTS_YOMI_LAST_NAME: 241 yomiLastName = getValue(); 242 break; 243 244 case Tags.CONTACTS2_NICKNAME: 245 ops.addNickname(entity, getValue()); 246 break; 247 248 case Tags.CONTACTS_ASSISTANT_NAME: 249 ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue()); 250 break; 251 case Tags.CONTACTS2_MANAGER_NAME: 252 ops.addRelation(entity, Relation.TYPE_MANAGER, getValue()); 253 break; 254 case Tags.CONTACTS_SPOUSE: 255 ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue()); 256 break; 257 case Tags.CONTACTS_DEPARTMENT: 258 department = getValue(); 259 break; 260 case Tags.CONTACTS_TITLE: 261 prefix = getValue(); 262 break; 263 264 // EAS Business 265 case Tags.CONTACTS_OFFICE_LOCATION: 266 officeLocation = getValue(); 267 break; 268 case Tags.CONTACTS2_CUSTOMER_ID: 269 business.customerId = getValue(); 270 break; 271 case Tags.CONTACTS2_GOVERNMENT_ID: 272 business.governmentId = getValue(); 273 break; 274 case Tags.CONTACTS2_ACCOUNT_NAME: 275 business.accountName = getValue(); 276 break; 277 278 // EAS Personal 279 case Tags.CONTACTS_ANNIVERSARY: 280 personal.anniversary = getValue(); 281 break; 282 case Tags.CONTACTS_BIRTHDAY: 283 ops.addBirthday(entity, getValue()); 284 break; 285 case Tags.CONTACTS_WEBPAGE: 286 ops.addWebpage(entity, getValue()); 287 break; 288 289 case Tags.CONTACTS_PICTURE: 290 ops.addPhoto(entity, getValue()); 291 break; 292 293 case Tags.BASE_BODY: 294 ops.addNote(entity, bodyParser()); 295 break; 296 case Tags.CONTACTS_BODY: 297 ops.addNote(entity, getValue()); 298 break; 299 300 case Tags.CONTACTS_CATEGORIES: 301 mGroupsUsed = true; 302 categoriesParser(ops, entity); 303 break; 304 305 default: 306 skipTag(); 307 } 308 } 309 310 ops.addName(entity, prefix, firstName, lastName, middleName, suffix, 311 yomiFirstName, yomiLastName); 312 ops.addBusiness(entity, business); 313 ops.addPersonal(entity, personal); 314 315 ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS); 316 ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS); 317 ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME, 318 MAX_PHONE_ROWS); 319 ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK, 320 MAX_PHONE_ROWS); 321 322 if (!children.isEmpty()) { 323 ops.addChildren(entity, children); 324 } 325 326 if (work.hasData()) { 327 ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city, 328 work.state, work.country, work.code); 329 } 330 if (home.hasData()) { 331 ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city, 332 home.state, home.country, home.code); 333 } 334 if (other.hasData()) { 335 ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city, 336 other.state, other.country, other.code); 337 } 338 339 if (companyName != null) { 340 ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department, 341 yomiCompanyName, officeLocation); 342 } 343 344 if (entity != null) { 345 // We've been removing rows from the list as they've been found in the xml 346 // Any that are left must have been deleted on the server 347 ArrayList<NamedContentValues> ncvList = entity.getSubValues(); 348 for (NamedContentValues ncv: ncvList) { 349 // These rows need to be deleted... 350 Uri u = dataUriFromNamedContentValues(ncv); 351 ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u)) 352 .build()); 353 } 354 } 355 } 356 357 private void categoriesParser(ContactOperations ops, Entity entity) throws IOException { 358 while (nextTag(Tags.CONTACTS_CATEGORIES) != END) { 359 switch (tag) { 360 case Tags.CONTACTS_CATEGORY: 361 ops.addGroup(entity, getValue()); 362 break; 363 default: 364 skipTag(); 365 } 366 } 367 } 368 369 private void childrenParser(ArrayList<String> children) throws IOException { 370 while (nextTag(Tags.CONTACTS_CHILDREN) != END) { 371 switch (tag) { 372 case Tags.CONTACTS_CHILD: 373 if (children.size() < EasChildren.MAX_CHILDREN) { 374 children.add(getValue()); 375 } 376 break; 377 default: 378 skipTag(); 379 } 380 } 381 } 382 383 private String bodyParser() throws IOException { 384 String body = null; 385 while (nextTag(Tags.BASE_BODY) != END) { 386 switch (tag) { 387 case Tags.BASE_DATA: 388 body = getValue(); 389 break; 390 default: 391 skipTag(); 392 } 393 } 394 return body; 395 } 396 397 public void addParser(ContactOperations ops) throws IOException { 398 String serverId = null; 399 while (nextTag(Tags.SYNC_ADD) != END) { 400 switch (tag) { 401 case Tags.SYNC_SERVER_ID: // same as 402 serverId = getValue(); 403 break; 404 case Tags.SYNC_APPLICATION_DATA: 405 addData(serverId, ops, null); 406 break; 407 default: 408 skipTag(); 409 } 410 } 411 } 412 413 private Cursor getServerIdCursor(String serverId) { 414 mBindArgument[0] = serverId; 415 return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION, 416 mBindArgument, null); 417 } 418 419 private Cursor getClientIdCursor(String clientId) { 420 mBindArgument[0] = clientId; 421 return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION, 422 mBindArgument, null); 423 } 424 425 public void deleteParser(ContactOperations ops) throws IOException { 426 while (nextTag(Tags.SYNC_DELETE) != END) { 427 switch (tag) { 428 case Tags.SYNC_SERVER_ID: 429 String serverId = getValue(); 430 // Find the message in this mailbox with the given serverId 431 Cursor c = getServerIdCursor(serverId); 432 try { 433 if (c.moveToFirst()) { 434 userLog("Deleting ", serverId); 435 ops.delete(c.getLong(0)); 436 } 437 } finally { 438 c.close(); 439 } 440 break; 441 default: 442 skipTag(); 443 } 444 } 445 } 446 447 class ServerChange { 448 long id; 449 boolean read; 450 451 ServerChange(long _id, boolean _read) { 452 id = _id; 453 read = _read; 454 } 455 } 456 457 /** 458 * Changes are handled row by row, and only changed/new rows are acted upon 459 * @param ops the array of pending ContactProviderOperations. 460 * @throws IOException 461 */ 462 public void changeParser(ContactOperations ops) throws IOException { 463 String serverId = null; 464 Entity entity = null; 465 while (nextTag(Tags.SYNC_CHANGE) != END) { 466 switch (tag) { 467 case Tags.SYNC_SERVER_ID: 468 serverId = getValue(); 469 Cursor c = getServerIdCursor(serverId); 470 try { 471 if (c.moveToFirst()) { 472 // TODO Handle deleted individual rows... 473 Uri uri = ContentUris.withAppendedId( 474 RawContacts.CONTENT_URI, c.getLong(0)); 475 uri = Uri.withAppendedPath( 476 uri, RawContacts.Entity.CONTENT_DIRECTORY); 477 EntityIterator entityIterator = RawContacts.newEntityIterator( 478 mContentResolver.query(uri, null, null, null, null)); 479 if (entityIterator.hasNext()) { 480 entity = entityIterator.next(); 481 } 482 userLog("Changing contact ", serverId); 483 } 484 } finally { 485 c.close(); 486 } 487 break; 488 case Tags.SYNC_APPLICATION_DATA: 489 addData(serverId, ops, entity); 490 break; 491 default: 492 skipTag(); 493 } 494 } 495 } 496 497 @Override 498 public void commandsParser() throws IOException { 499 while (nextTag(Tags.SYNC_COMMANDS) != END) { 500 if (tag == Tags.SYNC_ADD) { 501 addParser(ops); 502 } else if (tag == Tags.SYNC_DELETE) { 503 deleteParser(ops); 504 } else if (tag == Tags.SYNC_CHANGE) { 505 changeParser(ops); 506 } else 507 skipTag(); 508 } 509 } 510 511 @Override 512 public void commit() throws IOException { 513 // Save the syncKey here, using the Helper provider by Contacts provider 514 userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey); 515 ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI, 516 mAccountManagerAccount, mMailbox.mSyncKey.getBytes())); 517 518 // Execute these all at once... 519 ops.execute(mContext); 520 521 if (ops.mResults != null) { 522 ContentValues cv = new ContentValues(); 523 cv.put(RawContacts.DIRTY, 0); 524 for (int i = 0; i < ops.mContactIndexCount; i++) { 525 int index = ops.mContactIndexArray[i]; 526 Uri u = ops.mResults[index].uri; 527 if (u != null) { 528 String idString = u.getLastPathSegment(); 529 mContentResolver.update( 530 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv, 531 RawContacts._ID + "=" + idString, null); 532 } 533 } 534 } 535 } 536 537 public void addResponsesParser() throws IOException { 538 String serverId = null; 539 String clientId = null; 540 ContentValues cv = new ContentValues(); 541 while (nextTag(Tags.SYNC_ADD) != END) { 542 switch (tag) { 543 case Tags.SYNC_SERVER_ID: 544 serverId = getValue(); 545 break; 546 case Tags.SYNC_CLIENT_ID: 547 clientId = getValue(); 548 break; 549 case Tags.SYNC_STATUS: 550 getValue(); 551 break; 552 default: 553 skipTag(); 554 } 555 } 556 557 // This is theoretically impossible, but... 558 if (clientId == null || serverId == null) return; 559 560 Cursor c = getClientIdCursor(clientId); 561 try { 562 if (c.moveToFirst()) { 563 cv.put(RawContacts.SOURCE_ID, serverId); 564 cv.put(RawContacts.DIRTY, 0); 565 ops.add(ContentProviderOperation.newUpdate( 566 ContentUris.withAppendedId( 567 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), 568 c.getLong(0))) 569 .withValues(cv) 570 .build()); 571 userLog("New contact " + clientId + " was given serverId: " + serverId); 572 } 573 } finally { 574 c.close(); 575 } 576 } 577 578 public void changeResponsesParser() throws IOException { 579 String serverId = null; 580 String status = null; 581 while (nextTag(Tags.SYNC_CHANGE) != END) { 582 switch (tag) { 583 case Tags.SYNC_SERVER_ID: 584 serverId = getValue(); 585 break; 586 case Tags.SYNC_STATUS: 587 status = getValue(); 588 break; 589 default: 590 skipTag(); 591 } 592 } 593 if (serverId != null && status != null) { 594 userLog("Changed contact " + serverId + " failed with status: " + status); 595 } 596 } 597 598 599 @Override 600 public void responsesParser() throws IOException { 601 // Handle server responses here (for Add and Change) 602 while (nextTag(Tags.SYNC_RESPONSES) != END) { 603 if (tag == Tags.SYNC_ADD) { 604 addResponsesParser(); 605 } else if (tag == Tags.SYNC_CHANGE) { 606 changeResponsesParser(); 607 } else 608 skipTag(); 609 } 610 } 611 612 private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) { 613 return uri.buildUpon() 614 .appendQueryParameter(RawContacts.ACCOUNT_NAME, emailAddress) 615 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE) 616 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 617 .build(); 618 } 619 620 static Uri addCallerIsSyncAdapterParameter(Uri uri) { 621 return uri.buildUpon() 622 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 623 .build(); 624 } 625 626 /** 627 * Generate the uri for the data row associated with this NamedContentValues object 628 * @param ncv the NamedContentValues object 629 * @return a uri that can be used to refer to this row 630 */ 631 public static Uri dataUriFromNamedContentValues(NamedContentValues ncv) { 632 long id = ncv.values.getAsLong(RawContacts._ID); 633 Uri dataUri = ContentUris.withAppendedId(ncv.uri, id); 634 return dataUri; 635 } 636 637 public static final class EasChildren { 638 private EasChildren() {} 639 640 /** MIME type used when storing this in data table. */ 641 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children"; 642 public static final int MAX_CHILDREN = 8; 643 public static final String[] ROWS = 644 new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"}; 645 } 646 647 public static final class EasPersonal { 648 String anniversary; 649 String fileAs; 650 651 /** MIME type used when storing this in data table. */ 652 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal"; 653 public static final String ANNIVERSARY = "data2"; 654 public static final String FILE_AS = "data4"; 655 656 boolean hasData() { 657 return anniversary != null || fileAs != null; 658 } 659 } 660 661 public static final class EasBusiness { 662 String customerId; 663 String governmentId; 664 String accountName; 665 666 /** MIME type used when storing this in data table. */ 667 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business"; 668 public static final String CUSTOMER_ID = "data6"; 669 public static final String GOVERNMENT_ID = "data7"; 670 public static final String ACCOUNT_NAME = "data8"; 671 672 boolean hasData() { 673 return customerId != null || governmentId != null || accountName != null; 674 } 675 } 676 677 public static final class Address { 678 String city; 679 String country; 680 String code; 681 String street; 682 String state; 683 684 boolean hasData() { 685 return city != null || country != null || code != null || state != null 686 || street != null; 687 } 688 } 689 690 interface UntypedRow { 691 public void addValues(RowBuilder builder); 692 public boolean isSameAs(int type, String value); 693 } 694 695 static class EmailRow implements UntypedRow { 696 String email; 697 String displayName; 698 699 public EmailRow(String _email) { 700 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email); 701 // Can't happen, but belt & suspenders 702 if (tokens.length == 0) { 703 email = ""; 704 displayName = ""; 705 } else { 706 Rfc822Token token = tokens[0]; 707 email = token.getAddress(); 708 displayName = token.getName(); 709 } 710 } 711 712 @Override 713 public void addValues(RowBuilder builder) { 714 builder.withValue(Email.DATA, email); 715 builder.withValue(Email.DISPLAY_NAME, displayName); 716 } 717 718 @Override 719 public boolean isSameAs(int type, String value) { 720 return email.equalsIgnoreCase(value); 721 } 722 } 723 724 static class ImRow implements UntypedRow { 725 String im; 726 727 public ImRow(String _im) { 728 im = _im; 729 } 730 731 @Override 732 public void addValues(RowBuilder builder) { 733 builder.withValue(Im.DATA, im); 734 } 735 736 @Override 737 public boolean isSameAs(int type, String value) { 738 return im.equalsIgnoreCase(value); 739 } 740 } 741 742 static class PhoneRow implements UntypedRow { 743 String phone; 744 int type; 745 746 public PhoneRow(String _phone, int _type) { 747 phone = _phone; 748 type = _type; 749 } 750 751 @Override 752 public void addValues(RowBuilder builder) { 753 builder.withValue(Im.DATA, phone); 754 builder.withValue(Phone.TYPE, type); 755 } 756 757 @Override 758 public boolean isSameAs(int _type, String value) { 759 return type == _type && phone.equalsIgnoreCase(value); 760 } 761 } 762 763 /** 764 * RowBuilder is a wrapper for the Builder class that is used to create/update rows for a 765 * ContentProvider. It has, in addition to the Builder, ContentValues which, if present, 766 * represent the current values of that row, that can be compared against current values to 767 * see whether an update is even necessary. The methods on SmartBuilder are delegated to 768 * the Builder. 769 */ 770 private static class RowBuilder { 771 Builder builder; 772 ContentValues cv; 773 774 public RowBuilder(Builder _builder) { 775 builder = _builder; 776 } 777 778 public RowBuilder(Builder _builder, NamedContentValues _ncv) { 779 builder = _builder; 780 cv = _ncv.values; 781 } 782 783 RowBuilder withValueBackReference(String key, int previousResult) { 784 builder.withValueBackReference(key, previousResult); 785 return this; 786 } 787 788 ContentProviderOperation build() { 789 return builder.build(); 790 } 791 792 RowBuilder withValue(String key, Object value) { 793 builder.withValue(key, value); 794 return this; 795 } 796 } 797 public static class ContactOperations extends ArrayList<ContentProviderOperation> { 798 private static final long serialVersionUID = 1L; 799 private int mCount = 0; 800 private int mContactBackValue = mCount; 801 // Make an array big enough for the max possible window size. 802 private final int[] mContactIndexArray = new int[EasSyncCollectionTypeBase.MAX_WINDOW_SIZE]; 803 private int mContactIndexCount = 0; 804 private ContentProviderResult[] mResults = null; 805 806 @Override 807 public boolean add(ContentProviderOperation op) { 808 super.add(op); 809 mCount++; 810 return true; 811 } 812 813 public void newContact(final String serverId, final String emailAddress) { 814 Builder builder = ContentProviderOperation.newInsert( 815 uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI, emailAddress)); 816 ContentValues values = new ContentValues(); 817 values.put(RawContacts.SOURCE_ID, serverId); 818 builder.withValues(values); 819 mContactBackValue = mCount; 820 mContactIndexArray[mContactIndexCount++] = mCount; 821 add(builder.build()); 822 } 823 824 public void delete(long id) { 825 add(ContentProviderOperation 826 .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 827 .buildUpon() 828 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 829 .build()) 830 .build()); 831 } 832 833 public void execute(final Context context) { 834 try { 835 if (!isEmpty()) { 836 mResults = context.getContentResolver().applyBatch( 837 ContactsContract.AUTHORITY, this); 838 } 839 } catch (RemoteException e) { 840 // There is nothing sensible to be done here 841 LogUtils.e(TAG, "problem inserting contact during server update", e); 842 } catch (OperationApplicationException e) { 843 // There is nothing sensible to be done here 844 LogUtils.e(TAG, "problem inserting contact during server update", e); 845 } 846 } 847 848 /** 849 * Given the list of NamedContentValues for an entity, a mime type, and a subtype, 850 * tries to find a match, returning it 851 * @param list the list of NCV's from the contact entity 852 * @param contentItemType the mime type we're looking for 853 * @param type the subtype (e.g. HOME, WORK, etc.) 854 * @return the matching NCV or null if not found 855 */ 856 private static NamedContentValues findTypedData(ArrayList<NamedContentValues> list, 857 String contentItemType, int type, String stringType) { 858 NamedContentValues result = null; 859 860 // Loop through the ncv's, looking for an existing row 861 for (NamedContentValues namedContentValues: list) { 862 Uri uri = namedContentValues.uri; 863 ContentValues cv = namedContentValues.values; 864 if (Data.CONTENT_URI.equals(uri)) { 865 String mimeType = cv.getAsString(Data.MIMETYPE); 866 if (mimeType.equals(contentItemType)) { 867 if (stringType != null) { 868 if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) { 869 result = namedContentValues; 870 } 871 // Note Email.TYPE could be ANY type column; they are all defined in 872 // the private CommonColumns class in ContactsContract 873 // We'll accept either type < 0 (don't care), cv doesn't have a type, 874 // or the types are equal 875 } else if (type < 0 || !cv.containsKey(Email.TYPE) || 876 cv.getAsInteger(Email.TYPE) == type) { 877 result = namedContentValues; 878 } 879 } 880 } 881 } 882 883 // If we've found an existing data row, we'll delete it. Any rows left at the 884 // end should be deleted... 885 if (result != null) { 886 list.remove(result); 887 } 888 889 // Return the row found (or null) 890 return result; 891 } 892 893 /** 894 * Given the list of NamedContentValues for an entity and a mime type 895 * gather all of the matching NCV's, returning them 896 * @param list the list of NCV's from the contact entity 897 * @param contentItemType the mime type we're looking for 898 * @param type the subtype (e.g. HOME, WORK, etc.) 899 * @return the matching NCVs 900 */ 901 private static ArrayList<NamedContentValues> findUntypedData( 902 ArrayList<NamedContentValues> list, int type, String contentItemType) { 903 ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>(); 904 905 // Loop through the ncv's, looking for an existing row 906 for (NamedContentValues namedContentValues: list) { 907 Uri uri = namedContentValues.uri; 908 ContentValues cv = namedContentValues.values; 909 if (Data.CONTENT_URI.equals(uri)) { 910 String mimeType = cv.getAsString(Data.MIMETYPE); 911 if (mimeType.equals(contentItemType)) { 912 if (type != -1) { 913 int subtype = cv.getAsInteger(Phone.TYPE); 914 if (type != subtype) { 915 continue; 916 } 917 } 918 result.add(namedContentValues); 919 } 920 } 921 } 922 923 // If we've found an existing data row, we'll delete it. Any rows left at the 924 // end should be deleted... 925 for (NamedContentValues values : result) { 926 list.remove(values); 927 } 928 929 // Return the row found (or null) 930 return result; 931 } 932 933 /** 934 * Create a wrapper for a builder (insert or update) that also includes the NCV for 935 * an existing row of this type. If the SmartBuilder's cv field is not null, then 936 * it represents the current (old) values of this field. The caller can then check 937 * whether the field is now different and needs to be updated; if it's not different, 938 * the caller will simply return and not generate a new CPO. Otherwise, the builder 939 * should have its content values set, and the built CPO should be added to the 940 * ContactOperations list. 941 * 942 * @param entity the contact entity (or null if this is a new contact) 943 * @param mimeType the mime type of this row 944 * @param type the subtype of this row 945 * @param stringType for groups, the name of the group (type will be ignored), or null 946 * @return the created SmartBuilder 947 */ 948 public RowBuilder createBuilder(Entity entity, String mimeType, int type, 949 String stringType) { 950 RowBuilder builder = null; 951 952 if (entity != null) { 953 NamedContentValues ncv = 954 findTypedData(entity.getSubValues(), mimeType, type, stringType); 955 if (ncv != null) { 956 builder = new RowBuilder( 957 ContentProviderOperation 958 .newUpdate(addCallerIsSyncAdapterParameter( 959 dataUriFromNamedContentValues(ncv))), 960 ncv); 961 } 962 } 963 964 if (builder == null) { 965 builder = newRowBuilder(entity, mimeType); 966 } 967 968 // Return the appropriate builder (insert or update) 969 // Caller will fill in the appropriate values; 4 MIMETYPE is already set 970 return builder; 971 } 972 973 private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { 974 return createBuilder(entity, mimeType, type, null); 975 } 976 977 private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { 978 return createBuilder(entity, mimeType, -1, null); 979 } 980 981 private RowBuilder newRowBuilder(Entity entity, String mimeType) { 982 // This is a new row; first get the contactId 983 // If the Contact is new, use the saved back value; otherwise the value in the entity 984 int contactId = mContactBackValue; 985 if (entity != null) { 986 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID); 987 } 988 989 // Create an insert operation with the proper contactId reference 990 RowBuilder builder = 991 new RowBuilder(ContentProviderOperation.newInsert( 992 addCallerIsSyncAdapterParameter(Data.CONTENT_URI))); 993 if (entity == null) { 994 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId); 995 } else { 996 builder.withValue(Data.RAW_CONTACT_ID, contactId); 997 } 998 999 // Set the mime type of the row 1000 builder.withValue(Data.MIMETYPE, mimeType); 1001 return builder; 1002 } 1003 1004 /** 1005 * Compare a column in a ContentValues with an (old) value, and see if they are the 1006 * same. For this purpose, null and an empty string are considered the same. 1007 * @param cv a ContentValues object, from a NamedContentValues 1008 * @param column a column that might be in the ContentValues 1009 * @param oldValue an old value (or null) to check against 1010 * @return whether the column's value in the ContentValues matches oldValue 1011 */ 1012 private static boolean cvCompareString(ContentValues cv, String column, String oldValue) { 1013 if (cv.containsKey(column)) { 1014 if (oldValue != null && cv.getAsString(column).equals(oldValue)) { 1015 return true; 1016 } 1017 } else if (oldValue == null || oldValue.length() == 0) { 1018 return true; 1019 } 1020 return false; 1021 } 1022 1023 public void addChildren(Entity entity, ArrayList<String> children) { 1024 RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE); 1025 int i = 0; 1026 for (String child: children) { 1027 builder.withValue(EasChildren.ROWS[i++], child); 1028 } 1029 add(builder.build()); 1030 } 1031 1032 public void addGroup(Entity entity, String group) { 1033 RowBuilder builder = 1034 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group); 1035 builder.withValue(GroupMembership.GROUP_SOURCE_ID, group); 1036 add(builder.build()); 1037 } 1038 1039 public void addBirthday(Entity entity, String birthday) { 1040 RowBuilder builder = 1041 typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY); 1042 ContentValues cv = builder.cv; 1043 if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) { 1044 return; 1045 } 1046 // TODO: Store the date in the format expected by EAS servers. 1047 long millis = Utility.parseEmailDateTimeToMillis(birthday); 1048 GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 1049 cal.setTimeInMillis(millis); 1050 if (cal.get(GregorianCalendar.HOUR_OF_DAY) >= 12) { 1051 cal.add(GregorianCalendar.DATE, 1); 1052 } 1053 String realBirthday = CalendarUtilities.calendarToBirthdayString(cal); 1054 builder.withValue(Event.START_DATE, realBirthday); 1055 builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); 1056 add(builder.build()); 1057 } 1058 1059 public void addName(Entity entity, String prefix, String givenName, String familyName, 1060 String middleName, String suffix, String yomiFirstName, String yomiLastName) { 1061 RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE); 1062 ContentValues cv = builder.cv; 1063 if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) && 1064 cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) && 1065 cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) && 1066 cvCompareString(cv, StructuredName.PREFIX, prefix) && 1067 cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) && 1068 cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) && 1069 cvCompareString(cv, StructuredName.SUFFIX, suffix)) { 1070 return; 1071 } 1072 builder.withValue(StructuredName.GIVEN_NAME, givenName); 1073 builder.withValue(StructuredName.FAMILY_NAME, familyName); 1074 builder.withValue(StructuredName.MIDDLE_NAME, middleName); 1075 builder.withValue(StructuredName.SUFFIX, suffix); 1076 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName); 1077 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName); 1078 builder.withValue(StructuredName.PREFIX, prefix); 1079 add(builder.build()); 1080 } 1081 1082 public void addPersonal(Entity entity, EasPersonal personal) { 1083 RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE); 1084 ContentValues cv = builder.cv; 1085 if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) && 1086 cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) { 1087 return; 1088 } 1089 if (!personal.hasData()) { 1090 return; 1091 } 1092 builder.withValue(EasPersonal.FILE_AS, personal.fileAs); 1093 builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary); 1094 add(builder.build()); 1095 } 1096 1097 public void addBusiness(Entity entity, EasBusiness business) { 1098 RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE); 1099 ContentValues cv = builder.cv; 1100 if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) && 1101 cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) && 1102 cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) { 1103 return; 1104 } 1105 if (!business.hasData()) { 1106 return; 1107 } 1108 builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName); 1109 builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId); 1110 builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId); 1111 add(builder.build()); 1112 } 1113 1114 public void addPhoto(Entity entity, String photo) { 1115 RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE); 1116 // We're always going to add this; it's not worth trying to figure out whether the 1117 // picture is the same as the one stored. 1118 byte[] pic = Base64.decode(photo, Base64.DEFAULT); 1119 builder.withValue(Photo.PHOTO, pic); 1120 add(builder.build()); 1121 } 1122 1123 public void addPhone(Entity entity, int type, String phone) { 1124 RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type); 1125 ContentValues cv = builder.cv; 1126 if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) { 1127 return; 1128 } 1129 builder.withValue(Phone.TYPE, type); 1130 builder.withValue(Phone.NUMBER, phone); 1131 add(builder.build()); 1132 } 1133 1134 public void addWebpage(Entity entity, String url) { 1135 RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE); 1136 ContentValues cv = builder.cv; 1137 if (cv != null && cvCompareString(cv, Website.URL, url)) { 1138 return; 1139 } 1140 builder.withValue(Website.TYPE, Website.TYPE_WORK); 1141 builder.withValue(Website.URL, url); 1142 add(builder.build()); 1143 } 1144 1145 public void addRelation(Entity entity, int type, String value) { 1146 RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type); 1147 ContentValues cv = builder.cv; 1148 if (cv != null && cvCompareString(cv, Relation.DATA, value)) { 1149 return; 1150 } 1151 builder.withValue(Relation.TYPE, type); 1152 builder.withValue(Relation.DATA, value); 1153 add(builder.build()); 1154 } 1155 1156 public void addNickname(Entity entity, String name) { 1157 RowBuilder builder = 1158 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT); 1159 ContentValues cv = builder.cv; 1160 if (cv != null && cvCompareString(cv, Nickname.NAME, name)) { 1161 return; 1162 } 1163 builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); 1164 builder.withValue(Nickname.NAME, name); 1165 add(builder.build()); 1166 } 1167 1168 public void addPostal(Entity entity, int type, String street, String city, String state, 1169 String country, String code) { 1170 RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE, 1171 type); 1172 ContentValues cv = builder.cv; 1173 if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) && 1174 cvCompareString(cv, StructuredPostal.STREET, street) && 1175 cvCompareString(cv, StructuredPostal.COUNTRY, country) && 1176 cvCompareString(cv, StructuredPostal.POSTCODE, code) && 1177 cvCompareString(cv, StructuredPostal.REGION, state)) { 1178 return; 1179 } 1180 builder.withValue(StructuredPostal.TYPE, type); 1181 builder.withValue(StructuredPostal.CITY, city); 1182 builder.withValue(StructuredPostal.STREET, street); 1183 builder.withValue(StructuredPostal.COUNTRY, country); 1184 builder.withValue(StructuredPostal.POSTCODE, code); 1185 builder.withValue(StructuredPostal.REGION, state); 1186 add(builder.build()); 1187 } 1188 1189 /** 1190 * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to 1191 * match them with existing rows; if there's a match, everything's great. Otherwise, we 1192 * either need to add a new row for the data, or we have to replace an existing one 1193 * that no longer matches. This is similar to the way Emails are handled. 1194 */ 1195 public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, 1196 int type, int maxRows) { 1197 // Make a list of all same type rows in the existing entity 1198 ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES; 1199 ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES; 1200 if (entity != null) { 1201 oldValues = findUntypedData(entityValues, type, mimeType); 1202 entityValues = entity.getSubValues(); 1203 } 1204 1205 // These will be rows needing replacement with new values 1206 ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>(); 1207 1208 // The count of existing rows 1209 int numRows = oldValues.size(); 1210 for (UntypedRow row: rows) { 1211 boolean found = false; 1212 // If we already have this row, mark it 1213 for (NamedContentValues ncv: oldValues) { 1214 ContentValues cv = ncv.values; 1215 String data = cv.getAsString(COMMON_DATA_ROW); 1216 int rowType = -1; 1217 if (cv.containsKey(COMMON_TYPE_ROW)) { 1218 rowType = cv.getAsInteger(COMMON_TYPE_ROW); 1219 } 1220 if (row.isSameAs(rowType, data)) { 1221 cv.put(FOUND_DATA_ROW, true); 1222 // Remove this to indicate it's still being used 1223 entityValues.remove(ncv); 1224 found = true; 1225 break; 1226 } 1227 } 1228 if (!found) { 1229 // If we don't, there are two possibilities 1230 if (numRows < maxRows) { 1231 // If there are available rows, add a new one 1232 RowBuilder builder = newRowBuilder(entity, mimeType); 1233 row.addValues(builder); 1234 add(builder.build()); 1235 numRows++; 1236 } else { 1237 // Otherwise, say we need to replace a row with this 1238 rowsToReplace.add(row); 1239 } 1240 } 1241 } 1242 1243 // Go through rows needing replacement 1244 for (UntypedRow row: rowsToReplace) { 1245 for (NamedContentValues ncv: oldValues) { 1246 ContentValues cv = ncv.values; 1247 // Find a row that hasn't been used (i.e. doesn't match current rows) 1248 if (!cv.containsKey(FOUND_DATA_ROW)) { 1249 // And update it 1250 RowBuilder builder = new RowBuilder( 1251 ContentProviderOperation 1252 .newUpdate(addCallerIsSyncAdapterParameter( 1253 dataUriFromNamedContentValues(ncv))), 1254 ncv); 1255 row.addValues(builder); 1256 add(builder.build()); 1257 } 1258 } 1259 } 1260 } 1261 1262 public void addOrganization(Entity entity, int type, String company, String title, 1263 String department, String yomiCompanyName, String officeLocation) { 1264 RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type); 1265 ContentValues cv = builder.cv; 1266 if (cv != null && cvCompareString(cv, Organization.COMPANY, company) && 1267 cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) && 1268 cvCompareString(cv, Organization.DEPARTMENT, department) && 1269 cvCompareString(cv, Organization.TITLE, title) && 1270 cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) { 1271 return; 1272 } 1273 builder.withValue(Organization.TYPE, type); 1274 builder.withValue(Organization.COMPANY, company); 1275 builder.withValue(Organization.TITLE, title); 1276 builder.withValue(Organization.DEPARTMENT, department); 1277 builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName); 1278 builder.withValue(Organization.OFFICE_LOCATION, officeLocation); 1279 add(builder.build()); 1280 } 1281 1282 public void addNote(Entity entity, String note) { 1283 RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1); 1284 ContentValues cv = builder.cv; 1285 if (note == null) return; 1286 note = note.replaceAll("\r\n", "\n"); 1287 if (cv != null && cvCompareString(cv, Note.NOTE, note)) { 1288 return; 1289 } 1290 1291 // Reject notes with nothing in them. Often, we get something from Outlook when 1292 // nothing was ever entered. Sigh. 1293 int len = note.length(); 1294 int i = 0; 1295 for (; i < len; i++) { 1296 char c = note.charAt(i); 1297 if (!Character.isWhitespace(c)) { 1298 break; 1299 } 1300 } 1301 if (i == len) return; 1302 1303 builder.withValue(Note.NOTE, note); 1304 add(builder.build()); 1305 } 1306 } 1307 1308 @Override 1309 protected void wipe() { 1310 LogUtils.w(TAG, "Wiping contacts for account %d", mAccount.mId); 1311 EasSyncContacts.wipeAccountFromContentProvider(mContext, 1312 mAccount.mEmailAddress); 1313 } 1314 } 1315