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 ContactsSyncParser(final Context context, final ContentResolver resolver, final InputStream in, final Mailbox mailbox, final Account account, final android.accounts.Account accountManagerAccount)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 isGroupsUsed()89 public boolean isGroupsUsed() { 90 return mGroupsUsed; 91 } 92 addData(String serverId, ContactOperations ops, Entity entity)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 categoriesParser(ContactOperations ops, Entity entity)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 childrenParser(ArrayList<String> children)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 bodyParser()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 addParser(ContactOperations ops)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 getServerIdCursor(String serverId)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 getClientIdCursor(String clientId)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 deleteParser(ContactOperations ops)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 ServerChange(long _id, boolean _read)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 */ changeParser(ContactOperations ops)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 commandsParser()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 commit()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