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