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 ContactsSyncParser(final Context context, final ContentResolver resolver, final InputStream in, final Mailbox mailbox, final Account account, final android.accounts.Account accountManagerAccount)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 isGroupsUsed()87 public boolean isGroupsUsed() { 88 return mGroupsUsed; 89 } 90 addData(String serverId, ContactOperations ops, Entity entity)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 categoriesParser(ContactOperations ops, Entity entity)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 childrenParser(ArrayList<String> children)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 bodyParser()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 addParser(ContactOperations ops)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 getServerIdCursor(String serverId)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 getClientIdCursor(String clientId)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 deleteParser(ContactOperations ops)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 ServerChange(long _id, boolean _read)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 */ changeParser(ContactOperations ops)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 commandsParser()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 commit()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 addResponsesParser()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 changeResponsesParser()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 responsesParser()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 uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress)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 addCallerIsSyncAdapterParameter(Uri uri)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 */ dataUriFromNamedContentValues(NamedContentValues ncv)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 { 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 hasData()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 hasData()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 hasData()684 boolean hasData() { 685 return city != null || country != null || code != null || state != null 686 || street != null; 687 } 688 } 689 690 interface UntypedRow { addValues(RowBuilder builder)691 public void addValues(RowBuilder builder); isSameAs(int type, String value)692 public boolean isSameAs(int type, String value); 693 } 694 695 static class EmailRow implements UntypedRow { 696 String email; 697 String displayName; 698 EmailRow(String _email)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 addValues(RowBuilder builder)713 public void addValues(RowBuilder builder) { 714 builder.withValue(Email.DATA, email); 715 builder.withValue(Email.DISPLAY_NAME, displayName); 716 } 717 718 @Override isSameAs(int type, String value)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 ImRow(String _im)727 public ImRow(String _im) { 728 im = _im; 729 } 730 731 @Override addValues(RowBuilder builder)732 public void addValues(RowBuilder builder) { 733 builder.withValue(Im.DATA, im); 734 } 735 736 @Override isSameAs(int type, String value)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 PhoneRow(String _phone, int _type)746 public PhoneRow(String _phone, int _type) { 747 phone = _phone; 748 type = _type; 749 } 750 751 @Override addValues(RowBuilder builder)752 public void addValues(RowBuilder builder) { 753 builder.withValue(Im.DATA, phone); 754 builder.withValue(Phone.TYPE, type); 755 } 756 757 @Override isSameAs(int _type, String value)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 RowBuilder(Builder _builder)774 public RowBuilder(Builder _builder) { 775 builder = _builder; 776 } 777 RowBuilder(Builder _builder, NamedContentValues _ncv)778 public RowBuilder(Builder _builder, NamedContentValues _ncv) { 779 builder = _builder; 780 cv = _ncv.values; 781 } 782 withValueBackReference(String key, int previousResult)783 RowBuilder withValueBackReference(String key, int previousResult) { 784 builder.withValueBackReference(key, previousResult); 785 return this; 786 } 787 build()788 ContentProviderOperation build() { 789 return builder.build(); 790 } 791 withValue(String key, Object value)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 add(ContentProviderOperation op)807 public boolean add(ContentProviderOperation op) { 808 super.add(op); 809 mCount++; 810 return true; 811 } 812 newContact(final String serverId, final String emailAddress)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 delete(long id)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 execute(final Context context)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 */ findTypedData(ArrayList<NamedContentValues> list, String contentItemType, int type, String stringType)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 */ findUntypedData( ArrayList<NamedContentValues> list, int type, String contentItemType)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 */ createBuilder(Entity entity, String mimeType, int type, String stringType)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 typedRowBuilder(Entity entity, String mimeType, int type)973 private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { 974 return createBuilder(entity, mimeType, type, null); 975 } 976 untypedRowBuilder(Entity entity, String mimeType)977 private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { 978 return createBuilder(entity, mimeType, -1, null); 979 } 980 newRowBuilder(Entity entity, String mimeType)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 */ cvCompareString(ContentValues cv, String column, String oldValue)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 addChildren(Entity entity, ArrayList<String> children)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 addGroup(Entity entity, String group)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 addBirthday(Entity entity, String birthday)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 addName(Entity entity, String prefix, String givenName, String familyName, String middleName, String suffix, String yomiFirstName, String yomiLastName)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 addPersonal(Entity entity, EasPersonal personal)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 addBusiness(Entity entity, EasBusiness business)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 addPhoto(Entity entity, String photo)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 addPhone(Entity entity, int type, String phone)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 addWebpage(Entity entity, String url)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 addRelation(Entity entity, int type, String value)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 addNickname(Entity entity, String name)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 addPostal(Entity entity, int type, String street, String city, String state, String country, String code)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 */ addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, int type, int maxRows)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 addOrganization(Entity entity, int type, String company, String title, String department, String yomiCompanyName, String officeLocation)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 addNote(Entity entity, String note)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 wipe()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