ContactsSyncAdapter.java revision 6137d3f2ce68db51926a5e33bf1f57e49bcf8a31
1/* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange.adapter; 19 20import com.android.email.provider.EmailContent.Mailbox; 21import com.android.exchange.Eas; 22import com.android.exchange.EasSyncService; 23 24import android.content.ContentProviderClient; 25import android.content.ContentProviderOperation; 26import android.content.ContentProviderResult; 27import android.content.ContentResolver; 28import android.content.ContentUris; 29import android.content.ContentValues; 30import android.content.Entity; 31import android.content.EntityIterator; 32import android.content.OperationApplicationException; 33import android.content.ContentProviderOperation.Builder; 34import android.content.Entity.NamedContentValues; 35import android.database.Cursor; 36import android.net.Uri; 37import android.os.RemoteException; 38import android.provider.ContactsContract; 39import android.provider.SyncStateContract; 40import android.provider.ContactsContract.Data; 41import android.provider.ContactsContract.Groups; 42import android.provider.ContactsContract.RawContacts; 43import android.provider.ContactsContract.RawContactsEntity; 44import android.provider.ContactsContract.Settings; 45import android.provider.ContactsContract.SyncState; 46import android.provider.ContactsContract.CommonDataKinds.Email; 47import android.provider.ContactsContract.CommonDataKinds.Event; 48import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 49import android.provider.ContactsContract.CommonDataKinds.Im; 50import android.provider.ContactsContract.CommonDataKinds.Nickname; 51import android.provider.ContactsContract.CommonDataKinds.Note; 52import android.provider.ContactsContract.CommonDataKinds.Organization; 53import android.provider.ContactsContract.CommonDataKinds.Phone; 54import android.provider.ContactsContract.CommonDataKinds.Photo; 55import android.provider.ContactsContract.CommonDataKinds.Relation; 56import android.provider.ContactsContract.CommonDataKinds.StructuredName; 57import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 58import android.provider.ContactsContract.CommonDataKinds.Website; 59import android.text.util.Rfc822Token; 60import android.text.util.Rfc822Tokenizer; 61import android.util.Log; 62import android.util.Base64; 63 64import java.io.IOException; 65import java.io.InputStream; 66import java.util.ArrayList; 67 68/** 69 * Sync adapter for EAS Contacts 70 * 71 */ 72public class ContactsSyncAdapter extends AbstractSyncAdapter { 73 74 private static final String TAG = "EasContactsSyncAdapter"; 75 private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?"; 76 private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?"; 77 private static final String[] ID_PROJECTION = new String[] {RawContacts._ID}; 78 private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID}; 79 80 private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES 81 = new ArrayList<NamedContentValues>(); 82 83 private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW"; 84 85 private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 86 Tags.CONTACTS_HOME_ADDRESS_COUNTRY, 87 Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE, 88 Tags.CONTACTS_HOME_ADDRESS_STATE, 89 Tags.CONTACTS_HOME_ADDRESS_STREET}; 90 91 private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY, 92 Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY, 93 Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE, 94 Tags.CONTACTS_BUSINESS_ADDRESS_STATE, 95 Tags.CONTACTS_BUSINESS_ADDRESS_STREET}; 96 97 private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 98 Tags.CONTACTS_OTHER_ADDRESS_COUNTRY, 99 Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE, 100 Tags.CONTACTS_OTHER_ADDRESS_STATE, 101 Tags.CONTACTS_OTHER_ADDRESS_STREET}; 102 103 private static final int MAX_IM_ROWS = 3; 104 private static final int MAX_EMAIL_ROWS = 3; 105 private static final int MAX_PHONE_ROWS = 2; 106 private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc. 107 private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row 108 109 private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS, 110 Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3}; 111 112 private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS, 113 Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS}; 114 115 private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, 116 Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER}; 117 118 private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER, 119 Tags.CONTACTS_HOME2_TELEPHONE_NUMBER}; 120 121 ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); 122 ArrayList<Long> mUpdatedIdList = new ArrayList<Long>(); 123 124 private boolean mGroupsUsed = false; 125 126 public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) { 127 super(mailbox, service); 128 } 129 130 static Uri addCallerIsSyncAdapterParameter(Uri uri) { 131 return uri.buildUpon() 132 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 133 .build(); 134 } 135 136 @Override 137 public boolean isSyncable() { 138 return ContentResolver.getSyncAutomatically( 139 mAccountManagerAccount, ContactsContract.AUTHORITY); 140 } 141 142 @Override 143 public boolean parse(InputStream is) throws IOException { 144 EasContactsSyncParser p = new EasContactsSyncParser(is, this); 145 return p.parse(); 146 } 147 148 interface UntypedRow { 149 public void addValues(RowBuilder builder); 150 public boolean isSameAs(int type, String value); 151 } 152 153 /** 154 * We get our SyncKey from ContactsProvider. If there's not one, we set it to "0" (the reset 155 * state) and save that away. 156 */ 157 @Override 158 public String getSyncKey() throws IOException { 159 ContentProviderClient client = 160 mService.mContentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY_URI); 161 try { 162 byte[] data = SyncStateContract.Helpers.get(client, 163 ContactsContract.SyncState.CONTENT_URI, mAccountManagerAccount); 164 if (data == null || data.length == 0) { 165 // Initialize the SyncKey 166 setSyncKey("0", false); 167 // Make sure ungrouped contacts for Exchange are defaultly visible 168 ContentValues cv = new ContentValues(); 169 cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress); 170 cv.put(Groups.ACCOUNT_TYPE, com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 171 cv.put(Settings.UNGROUPED_VISIBLE, true); 172 client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv); 173 return "0"; 174 } else { 175 return new String(data); 176 } 177 } catch (RemoteException e) { 178 throw new IOException("Can't get SyncKey from ContactsProvider"); 179 } 180 } 181 182 /** 183 * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other 184 * cases, the SyncKey is set within ContactOperations 185 */ 186 @Override 187 public void setSyncKey(String syncKey, boolean inCommands) throws IOException { 188 if ("0".equals(syncKey) || !inCommands) { 189 ContentProviderClient client = 190 mService.mContentResolver 191 .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); 192 try { 193 SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI, 194 mAccountManagerAccount, syncKey.getBytes()); 195 userLog("SyncKey set to ", syncKey, " in ContactsProvider"); 196 } catch (RemoteException e) { 197 throw new IOException("Can't set SyncKey in ContactsProvider"); 198 } 199 } 200 mMailbox.mSyncKey = syncKey; 201 } 202 203 public static final class EasChildren { 204 private EasChildren() {} 205 206 /** MIME type used when storing this in data table. */ 207 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children"; 208 public static final int MAX_CHILDREN = 8; 209 public static final String[] ROWS = 210 new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"}; 211 } 212 213 public static final class EasPersonal { 214 String anniversary; 215 String fileAs; 216 217 /** MIME type used when storing this in data table. */ 218 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal"; 219 public static final String ANNIVERSARY = "data2"; 220 public static final String FILE_AS = "data4"; 221 222 boolean hasData() { 223 return anniversary != null || fileAs != null; 224 } 225 } 226 227 public static final class EasBusiness { 228 String customerId; 229 String governmentId; 230 String accountName; 231 232 /** MIME type used when storing this in data table. */ 233 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business"; 234 public static final String CUSTOMER_ID = "data6"; 235 public static final String GOVERNMENT_ID = "data7"; 236 public static final String ACCOUNT_NAME = "data8"; 237 238 boolean hasData() { 239 return customerId != null || governmentId != null || accountName != null; 240 } 241 } 242 243 public static final class Address { 244 String city; 245 String country; 246 String code; 247 String street; 248 String state; 249 250 boolean hasData() { 251 return city != null || country != null || code != null || state != null 252 || street != null; 253 } 254 } 255 256 class EmailRow implements UntypedRow { 257 String email; 258 String displayName; 259 260 public EmailRow(String _email) { 261 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email); 262 // Can't happen, but belt & suspenders 263 if (tokens.length == 0) { 264 email = ""; 265 displayName = ""; 266 } else { 267 Rfc822Token token = tokens[0]; 268 email = token.getAddress(); 269 displayName = token.getName(); 270 } 271 } 272 273 public void addValues(RowBuilder builder) { 274 builder.withValue(Email.DATA, email); 275 builder.withValue(Email.DISPLAY_NAME, displayName); 276 } 277 278 public boolean isSameAs(int type, String value) { 279 return email.equalsIgnoreCase(value); 280 } 281 } 282 283 class ImRow implements UntypedRow { 284 String im; 285 286 public ImRow(String _im) { 287 im = _im; 288 } 289 290 public void addValues(RowBuilder builder) { 291 builder.withValue(Im.DATA, im); 292 } 293 294 public boolean isSameAs(int type, String value) { 295 return im.equalsIgnoreCase(value); 296 } 297 } 298 299 class PhoneRow implements UntypedRow { 300 String phone; 301 int type; 302 303 public PhoneRow(String _phone, int _type) { 304 phone = _phone; 305 type = _type; 306 } 307 308 public void addValues(RowBuilder builder) { 309 builder.withValue(Im.DATA, phone); 310 builder.withValue(Phone.TYPE, type); 311 } 312 313 public boolean isSameAs(int _type, String value) { 314 return type == _type && phone.equalsIgnoreCase(value); 315 } 316 } 317 318 class EasContactsSyncParser extends AbstractSyncParser { 319 320 String[] mBindArgument = new String[1]; 321 String mMailboxIdAsString; 322 Uri mAccountUri; 323 ContactOperations ops = new ContactOperations(); 324 325 public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter) 326 throws IOException { 327 super(in, adapter); 328 mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI); 329 } 330 331 @Override 332 public void wipe() { 333 mContentResolver.delete(mAccountUri, null, null); 334 } 335 336 public void addData(String serverId, ContactOperations ops, Entity entity) 337 throws IOException { 338 String fileAs = null; 339 String prefix = null; 340 String firstName = null; 341 String lastName = null; 342 String middleName = null; 343 String suffix = null; 344 String companyName = null; 345 String yomiFirstName = null; 346 String yomiLastName = null; 347 String yomiCompanyName = null; 348 String title = null; 349 String department = null; 350 String officeLocation = null; 351 Address home = new Address(); 352 Address work = new Address(); 353 Address other = new Address(); 354 EasBusiness business = new EasBusiness(); 355 EasPersonal personal = new EasPersonal(); 356 ArrayList<String> children = new ArrayList<String>(); 357 ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>(); 358 ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>(); 359 ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>(); 360 ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>(); 361 if (entity == null) { 362 ops.newContact(serverId); 363 } 364 365 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 366 switch (tag) { 367 case Tags.CONTACTS_FIRST_NAME: 368 firstName = getValue(); 369 break; 370 case Tags.CONTACTS_LAST_NAME: 371 lastName = getValue(); 372 break; 373 case Tags.CONTACTS_MIDDLE_NAME: 374 middleName = getValue(); 375 break; 376 case Tags.CONTACTS_FILE_AS: 377 fileAs = getValue(); 378 break; 379 case Tags.CONTACTS_SUFFIX: 380 suffix = getValue(); 381 break; 382 case Tags.CONTACTS_COMPANY_NAME: 383 companyName = getValue(); 384 break; 385 case Tags.CONTACTS_JOB_TITLE: 386 title = getValue(); 387 break; 388 case Tags.CONTACTS_EMAIL1_ADDRESS: 389 case Tags.CONTACTS_EMAIL2_ADDRESS: 390 case Tags.CONTACTS_EMAIL3_ADDRESS: 391 emails.add(new EmailRow(getValue())); 392 break; 393 case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER: 394 case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER: 395 workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK)); 396 break; 397 case Tags.CONTACTS2_MMS: 398 ops.addPhone(entity, Phone.TYPE_MMS, getValue()); 399 break; 400 case Tags.CONTACTS_BUSINESS_FAX_NUMBER: 401 ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue()); 402 break; 403 case Tags.CONTACTS2_COMPANY_MAIN_PHONE: 404 ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue()); 405 break; 406 case Tags.CONTACTS_HOME_FAX_NUMBER: 407 ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue()); 408 break; 409 case Tags.CONTACTS_HOME_TELEPHONE_NUMBER: 410 case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER: 411 homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME)); 412 break; 413 case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER: 414 ops.addPhone(entity, Phone.TYPE_MOBILE, getValue()); 415 break; 416 case Tags.CONTACTS_CAR_TELEPHONE_NUMBER: 417 ops.addPhone(entity, Phone.TYPE_CAR, getValue()); 418 break; 419 case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER: 420 ops.addPhone(entity, Phone.TYPE_RADIO, getValue()); 421 break; 422 case Tags.CONTACTS_PAGER_NUMBER: 423 ops.addPhone(entity, Phone.TYPE_PAGER, getValue()); 424 break; 425 case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER: 426 ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue()); 427 break; 428 case Tags.CONTACTS2_IM_ADDRESS: 429 case Tags.CONTACTS2_IM_ADDRESS_2: 430 case Tags.CONTACTS2_IM_ADDRESS_3: 431 ims.add(new ImRow(getValue())); 432 break; 433 case Tags.CONTACTS_BUSINESS_ADDRESS_CITY: 434 work.city = getValue(); 435 break; 436 case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY: 437 work.country = getValue(); 438 break; 439 case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE: 440 work.code = getValue(); 441 break; 442 case Tags.CONTACTS_BUSINESS_ADDRESS_STATE: 443 work.state = getValue(); 444 break; 445 case Tags.CONTACTS_BUSINESS_ADDRESS_STREET: 446 work.street = getValue(); 447 break; 448 case Tags.CONTACTS_HOME_ADDRESS_CITY: 449 home.city = getValue(); 450 break; 451 case Tags.CONTACTS_HOME_ADDRESS_COUNTRY: 452 home.country = getValue(); 453 break; 454 case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE: 455 home.code = getValue(); 456 break; 457 case Tags.CONTACTS_HOME_ADDRESS_STATE: 458 home.state = getValue(); 459 break; 460 case Tags.CONTACTS_HOME_ADDRESS_STREET: 461 home.street = getValue(); 462 break; 463 case Tags.CONTACTS_OTHER_ADDRESS_CITY: 464 other.city = getValue(); 465 break; 466 case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY: 467 other.country = getValue(); 468 break; 469 case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE: 470 other.code = getValue(); 471 break; 472 case Tags.CONTACTS_OTHER_ADDRESS_STATE: 473 other.state = getValue(); 474 break; 475 case Tags.CONTACTS_OTHER_ADDRESS_STREET: 476 other.street = getValue(); 477 break; 478 479 case Tags.CONTACTS_CHILDREN: 480 childrenParser(children); 481 break; 482 483 case Tags.CONTACTS_YOMI_COMPANY_NAME: 484 yomiCompanyName = getValue(); 485 break; 486 case Tags.CONTACTS_YOMI_FIRST_NAME: 487 yomiFirstName = getValue(); 488 break; 489 case Tags.CONTACTS_YOMI_LAST_NAME: 490 yomiLastName = getValue(); 491 break; 492 493 case Tags.CONTACTS2_NICKNAME: 494 ops.addNickname(entity, getValue()); 495 break; 496 497 case Tags.CONTACTS_ASSISTANT_NAME: 498 ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue()); 499 break; 500 case Tags.CONTACTS2_MANAGER_NAME: 501 ops.addRelation(entity, Relation.TYPE_MANAGER, getValue()); 502 break; 503 case Tags.CONTACTS_SPOUSE: 504 ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue()); 505 break; 506 case Tags.CONTACTS_DEPARTMENT: 507 department = getValue(); 508 break; 509 case Tags.CONTACTS_TITLE: 510 prefix = getValue(); 511 break; 512 513 // EAS Business 514 case Tags.CONTACTS_OFFICE_LOCATION: 515 officeLocation = getValue(); 516 break; 517 case Tags.CONTACTS2_CUSTOMER_ID: 518 business.customerId = getValue(); 519 break; 520 case Tags.CONTACTS2_GOVERNMENT_ID: 521 business.governmentId = getValue(); 522 break; 523 case Tags.CONTACTS2_ACCOUNT_NAME: 524 business.accountName = getValue(); 525 break; 526 527 // EAS Personal 528 case Tags.CONTACTS_ANNIVERSARY: 529 personal.anniversary = getValue(); 530 break; 531 case Tags.CONTACTS_BIRTHDAY: 532 ops.addBirthday(entity, getValue()); 533 break; 534 case Tags.CONTACTS_WEBPAGE: 535 ops.addWebpage(entity, getValue()); 536 break; 537 538 case Tags.CONTACTS_PICTURE: 539 ops.addPhoto(entity, getValue()); 540 break; 541 542 case Tags.BASE_BODY: 543 ops.addNote(entity, bodyParser()); 544 break; 545 case Tags.CONTACTS_BODY: 546 ops.addNote(entity, getValue()); 547 break; 548 549 case Tags.CONTACTS_CATEGORIES: 550 mGroupsUsed = true; 551 categoriesParser(ops, entity); 552 break; 553 554 case Tags.CONTACTS_COMPRESSED_RTF: 555 // We don't use this, and it isn't necessary to upload, so we'll ignore it 556 skipTag(); 557 break; 558 559 default: 560 skipTag(); 561 } 562 } 563 564 // We must have first name, last name, or company name 565 String name = null; 566 if (firstName != null || lastName != null) { 567 if (firstName == null) { 568 name = lastName; 569 } else if (lastName == null) { 570 name = firstName; 571 } else { 572 name = firstName + ' ' + lastName; 573 } 574 } else if (companyName != null) { 575 name = companyName; 576 } 577 578 ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name, 579 yomiFirstName, yomiLastName, fileAs); 580 ops.addBusiness(entity, business); 581 ops.addPersonal(entity, personal); 582 583 ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS); 584 ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS); 585 ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME, 586 MAX_PHONE_ROWS); 587 ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK, 588 MAX_PHONE_ROWS); 589 590 if (!children.isEmpty()) { 591 ops.addChildren(entity, children); 592 } 593 594 if (work.hasData()) { 595 ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city, 596 work.state, work.country, work.code); 597 } 598 if (home.hasData()) { 599 ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city, 600 home.state, home.country, home.code); 601 } 602 if (other.hasData()) { 603 ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city, 604 other.state, other.country, other.code); 605 } 606 607 if (companyName != null) { 608 ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department, 609 yomiCompanyName, officeLocation); 610 } 611 612 if (entity != null) { 613 // We've been removing rows from the list as they've been found in the xml 614 // Any that are left must have been deleted on the server 615 ArrayList<NamedContentValues> ncvList = entity.getSubValues(); 616 for (NamedContentValues ncv: ncvList) { 617 // These rows need to be deleted... 618 Uri u = dataUriFromNamedContentValues(ncv); 619 ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u)) 620 .build()); 621 } 622 } 623 } 624 625 private void categoriesParser(ContactOperations ops, Entity entity) throws IOException { 626 while (nextTag(Tags.CONTACTS_CATEGORIES) != END) { 627 switch (tag) { 628 case Tags.CONTACTS_CATEGORY: 629 ops.addGroup(entity, getValue()); 630 break; 631 default: 632 skipTag(); 633 } 634 } 635 } 636 637 private void childrenParser(ArrayList<String> children) throws IOException { 638 while (nextTag(Tags.CONTACTS_CHILDREN) != END) { 639 switch (tag) { 640 case Tags.CONTACTS_CHILD: 641 if (children.size() < EasChildren.MAX_CHILDREN) { 642 children.add(getValue()); 643 } 644 break; 645 default: 646 skipTag(); 647 } 648 } 649 } 650 651 private String bodyParser() throws IOException { 652 String body = null; 653 while (nextTag(Tags.BASE_BODY) != END) { 654 switch (tag) { 655 case Tags.BASE_DATA: 656 body = getValue(); 657 break; 658 default: 659 skipTag(); 660 } 661 } 662 return body; 663 } 664 665 public void addParser(ContactOperations ops) throws IOException { 666 String serverId = null; 667 while (nextTag(Tags.SYNC_ADD) != END) { 668 switch (tag) { 669 case Tags.SYNC_SERVER_ID: // same as 670 serverId = getValue(); 671 break; 672 case Tags.SYNC_APPLICATION_DATA: 673 addData(serverId, ops, null); 674 break; 675 default: 676 skipTag(); 677 } 678 } 679 } 680 681 private Cursor getServerIdCursor(String serverId) { 682 mBindArgument[0] = serverId; 683 return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION, 684 mBindArgument, null); 685 } 686 687 private Cursor getClientIdCursor(String clientId) { 688 mBindArgument[0] = clientId; 689 return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION, 690 mBindArgument, null); 691 } 692 693 public void deleteParser(ContactOperations ops) throws IOException { 694 while (nextTag(Tags.SYNC_DELETE) != END) { 695 switch (tag) { 696 case Tags.SYNC_SERVER_ID: 697 String serverId = getValue(); 698 // Find the message in this mailbox with the given serverId 699 Cursor c = getServerIdCursor(serverId); 700 try { 701 if (c.moveToFirst()) { 702 userLog("Deleting ", serverId); 703 ops.delete(c.getLong(0)); 704 } 705 } finally { 706 c.close(); 707 } 708 break; 709 default: 710 skipTag(); 711 } 712 } 713 } 714 715 class ServerChange { 716 long id; 717 boolean read; 718 719 ServerChange(long _id, boolean _read) { 720 id = _id; 721 read = _read; 722 } 723 } 724 725 /** 726 * Changes are handled row by row, and only changed/new rows are acted upon 727 * @param ops the array of pending ContactProviderOperations. 728 * @throws IOException 729 */ 730 public void changeParser(ContactOperations ops) throws IOException { 731 String serverId = null; 732 Entity entity = null; 733 while (nextTag(Tags.SYNC_CHANGE) != END) { 734 switch (tag) { 735 case Tags.SYNC_SERVER_ID: 736 serverId = getValue(); 737 Cursor c = getServerIdCursor(serverId); 738 try { 739 if (c.moveToFirst()) { 740 // TODO Handle deleted individual rows... 741 Uri uri = ContentUris.withAppendedId( 742 RawContacts.CONTENT_URI, c.getLong(0)); 743 uri = Uri.withAppendedPath( 744 uri, RawContacts.Entity.CONTENT_DIRECTORY); 745 EntityIterator entityIterator = RawContacts.newEntityIterator( 746 mContentResolver.query(uri, null, null, null, null)); 747 try { 748 if (entityIterator.hasNext()) { 749 entity = entityIterator.next(); 750 } 751 userLog("Changing contact ", serverId); 752 } catch (RemoteException e) { 753 // TODO: log the fact that we failed to read the entity 754 } 755 } 756 } finally { 757 c.close(); 758 } 759 break; 760 case Tags.SYNC_APPLICATION_DATA: 761 addData(serverId, ops, entity); 762 break; 763 default: 764 skipTag(); 765 } 766 } 767 } 768 769 @Override 770 public void commandsParser() throws IOException { 771 while (nextTag(Tags.SYNC_COMMANDS) != END) { 772 if (tag == Tags.SYNC_ADD) { 773 addParser(ops); 774 incrementChangeCount(); 775 } else if (tag == Tags.SYNC_DELETE) { 776 deleteParser(ops); 777 incrementChangeCount(); 778 } else if (tag == Tags.SYNC_CHANGE) { 779 changeParser(ops); 780 incrementChangeCount(); 781 } else 782 skipTag(); 783 } 784 } 785 786 @Override 787 public void commit() throws IOException { 788 // Save the syncKey here, using the Helper provider by Contacts provider 789 userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey); 790 ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI, 791 mAccountManagerAccount, mMailbox.mSyncKey.getBytes())); 792 793 // Execute these all at once... 794 ops.execute(); 795 796 if (ops.mResults != null) { 797 ContentValues cv = new ContentValues(); 798 cv.put(RawContacts.DIRTY, 0); 799 for (int i = 0; i < ops.mContactIndexCount; i++) { 800 int index = ops.mContactIndexArray[i]; 801 Uri u = ops.mResults[index].uri; 802 if (u != null) { 803 String idString = u.getLastPathSegment(); 804 mContentResolver.update( 805 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv, 806 RawContacts._ID + "=" + idString, null); 807 } 808 } 809 } 810 } 811 812 public void addResponsesParser() throws IOException { 813 String serverId = null; 814 String clientId = null; 815 ContentValues cv = new ContentValues(); 816 while (nextTag(Tags.SYNC_ADD) != END) { 817 switch (tag) { 818 case Tags.SYNC_SERVER_ID: 819 serverId = getValue(); 820 break; 821 case Tags.SYNC_CLIENT_ID: 822 clientId = getValue(); 823 break; 824 case Tags.SYNC_STATUS: 825 getValue(); 826 break; 827 default: 828 skipTag(); 829 } 830 } 831 832 // This is theoretically impossible, but... 833 if (clientId == null || serverId == null) return; 834 835 Cursor c = getClientIdCursor(clientId); 836 try { 837 if (c.moveToFirst()) { 838 cv.put(RawContacts.SOURCE_ID, serverId); 839 cv.put(RawContacts.DIRTY, 0); 840 ops.add(ContentProviderOperation.newUpdate( 841 ContentUris.withAppendedId( 842 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), 843 c.getLong(0))) 844 .withValues(cv) 845 .build()); 846 userLog("New contact " + clientId + " was given serverId: " + serverId); 847 } 848 } finally { 849 c.close(); 850 } 851 } 852 853 public void changeResponsesParser() throws IOException { 854 String serverId = null; 855 String status = null; 856 while (nextTag(Tags.SYNC_CHANGE) != END) { 857 switch (tag) { 858 case Tags.SYNC_SERVER_ID: 859 serverId = getValue(); 860 break; 861 case Tags.SYNC_STATUS: 862 status = getValue(); 863 break; 864 default: 865 skipTag(); 866 } 867 } 868 if (serverId != null && status != null) { 869 userLog("Changed contact " + serverId + " failed with status: " + status); 870 } 871 } 872 873 874 @Override 875 public void responsesParser() throws IOException { 876 // Handle server responses here (for Add and Change) 877 while (nextTag(Tags.SYNC_RESPONSES) != END) { 878 if (tag == Tags.SYNC_ADD) { 879 addResponsesParser(); 880 } else if (tag == Tags.SYNC_CHANGE) { 881 changeResponsesParser(); 882 } else 883 skipTag(); 884 } 885 } 886 } 887 888 889 private Uri uriWithAccountAndIsSyncAdapter(Uri uri) { 890 return uri.buildUpon() 891 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) 892 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, 893 com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE) 894 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 895 .build(); 896 } 897 898 /** 899 * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a 900 * ContentProvider. It has, in addition to the Builder, ContentValues which, if present, 901 * represent the current values of that row, that can be compared against current values to 902 * see whether an update is even necessary. The methods on SmartBuilder are delegated to 903 * the Builder. 904 */ 905 private class RowBuilder { 906 Builder builder; 907 ContentValues cv; 908 909 public RowBuilder(Builder _builder) { 910 builder = _builder; 911 } 912 913 public RowBuilder(Builder _builder, NamedContentValues _ncv) { 914 builder = _builder; 915 cv = _ncv.values; 916 } 917 918 RowBuilder withValues(ContentValues values) { 919 builder.withValues(values); 920 return this; 921 } 922 923 RowBuilder withValueBackReference(String key, int previousResult) { 924 builder.withValueBackReference(key, previousResult); 925 return this; 926 } 927 928 ContentProviderOperation build() { 929 return builder.build(); 930 } 931 932 RowBuilder withValue(String key, Object value) { 933 builder.withValue(key, value); 934 return this; 935 } 936 } 937 938 private class ContactOperations extends ArrayList<ContentProviderOperation> { 939 private static final long serialVersionUID = 1L; 940 private int mCount = 0; 941 private int mContactBackValue = mCount; 942 // Make an array big enough for the PIM window (max items we can get) 943 private int[] mContactIndexArray = 944 new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)]; 945 private int mContactIndexCount = 0; 946 private ContentProviderResult[] mResults = null; 947 948 @Override 949 public boolean add(ContentProviderOperation op) { 950 super.add(op); 951 mCount++; 952 return true; 953 } 954 955 public void newContact(String serverId) { 956 Builder builder = ContentProviderOperation 957 .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI)); 958 ContentValues values = new ContentValues(); 959 values.put(RawContacts.SOURCE_ID, serverId); 960 builder.withValues(values); 961 mContactBackValue = mCount; 962 mContactIndexArray[mContactIndexCount++] = mCount; 963 add(builder.build()); 964 } 965 966 public void delete(long id) { 967 add(ContentProviderOperation 968 .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 969 .buildUpon() 970 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 971 .build()) 972 .build()); 973 } 974 975 public void execute() { 976 synchronized (mService.getSynchronizer()) { 977 if (!mService.isStopped()) { 978 try { 979 if (!isEmpty()) { 980 mService.userLog("Executing ", size(), " CPO's"); 981 mResults = mContext.getContentResolver().applyBatch( 982 ContactsContract.AUTHORITY, this); 983 } 984 } catch (RemoteException e) { 985 // There is nothing sensible to be done here 986 Log.e(TAG, "problem inserting contact during server update", e); 987 } catch (OperationApplicationException e) { 988 // There is nothing sensible to be done here 989 Log.e(TAG, "problem inserting contact during server update", e); 990 } 991 } 992 } 993 } 994 995 /** 996 * Given the list of NamedContentValues for an entity, a mime type, and a subtype, 997 * tries to find a match, returning it 998 * @param list the list of NCV's from the contact entity 999 * @param contentItemType the mime type we're looking for 1000 * @param type the subtype (e.g. HOME, WORK, etc.) 1001 * @return the matching NCV or null if not found 1002 */ 1003 private NamedContentValues findTypedData(ArrayList<NamedContentValues> list, 1004 String contentItemType, int type, String stringType) { 1005 NamedContentValues result = null; 1006 1007 // Loop through the ncv's, looking for an existing row 1008 for (NamedContentValues namedContentValues: list) { 1009 Uri uri = namedContentValues.uri; 1010 ContentValues cv = namedContentValues.values; 1011 if (Data.CONTENT_URI.equals(uri)) { 1012 String mimeType = cv.getAsString(Data.MIMETYPE); 1013 if (mimeType.equals(contentItemType)) { 1014 if (stringType != null) { 1015 if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) { 1016 result = namedContentValues; 1017 } 1018 // Note Email.TYPE could be ANY type column; they are all defined in 1019 // the private CommonColumns class in ContactsContract 1020 // We'll accept either type < 0 (don't care), cv doesn't have a type, 1021 // or the types are equal 1022 } else if (type < 0 || !cv.containsKey(Email.TYPE) || 1023 cv.getAsInteger(Email.TYPE) == type) { 1024 result = namedContentValues; 1025 } 1026 } 1027 } 1028 } 1029 1030 // If we've found an existing data row, we'll delete it. Any rows left at the 1031 // end should be deleted... 1032 if (result != null) { 1033 list.remove(result); 1034 } 1035 1036 // Return the row found (or null) 1037 return result; 1038 } 1039 1040 /** 1041 * Given the list of NamedContentValues for an entity and a mime type 1042 * gather all of the matching NCV's, returning them 1043 * @param list the list of NCV's from the contact entity 1044 * @param contentItemType the mime type we're looking for 1045 * @param type the subtype (e.g. HOME, WORK, etc.) 1046 * @return the matching NCVs 1047 */ 1048 private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list, 1049 int type, String contentItemType) { 1050 ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>(); 1051 1052 // Loop through the ncv's, looking for an existing row 1053 for (NamedContentValues namedContentValues: list) { 1054 Uri uri = namedContentValues.uri; 1055 ContentValues cv = namedContentValues.values; 1056 if (Data.CONTENT_URI.equals(uri)) { 1057 String mimeType = cv.getAsString(Data.MIMETYPE); 1058 if (mimeType.equals(contentItemType)) { 1059 if (type != -1) { 1060 int subtype = cv.getAsInteger(Phone.TYPE); 1061 if (type != subtype) { 1062 continue; 1063 } 1064 } 1065 result.add(namedContentValues); 1066 } 1067 } 1068 } 1069 1070 // If we've found an existing data row, we'll delete it. Any rows left at the 1071 // end should be deleted... 1072 if (result != null) { 1073 list.remove(result); 1074 } 1075 1076 // Return the row found (or null) 1077 return result; 1078 } 1079 1080 /** 1081 * Create a wrapper for a builder (insert or update) that also includes the NCV for 1082 * an existing row of this type. If the SmartBuilder's cv field is not null, then 1083 * it represents the current (old) values of this field. The caller can then check 1084 * whether the field is now different and needs to be updated; if it's not different, 1085 * the caller will simply return and not generate a new CPO. Otherwise, the builder 1086 * should have its content values set, and the built CPO should be added to the 1087 * ContactOperations list. 1088 * 1089 * @param entity the contact entity (or null if this is a new contact) 1090 * @param mimeType the mime type of this row 1091 * @param type the subtype of this row 1092 * @param stringType for groups, the name of the group (type will be ignored), or null 1093 * @return the created SmartBuilder 1094 */ 1095 public RowBuilder createBuilder(Entity entity, String mimeType, int type, 1096 String stringType) { 1097 RowBuilder builder = null; 1098 1099 if (entity != null) { 1100 NamedContentValues ncv = 1101 findTypedData(entity.getSubValues(), mimeType, type, stringType); 1102 if (ncv != null) { 1103 builder = new RowBuilder( 1104 ContentProviderOperation 1105 .newUpdate(addCallerIsSyncAdapterParameter( 1106 dataUriFromNamedContentValues(ncv))), 1107 ncv); 1108 } 1109 } 1110 1111 if (builder == null) { 1112 builder = newRowBuilder(entity, mimeType); 1113 } 1114 1115 // Return the appropriate builder (insert or update) 1116 // Caller will fill in the appropriate values; 4 MIMETYPE is already set 1117 return builder; 1118 } 1119 1120 private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { 1121 return createBuilder(entity, mimeType, type, null); 1122 } 1123 1124 private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { 1125 return createBuilder(entity, mimeType, -1, null); 1126 } 1127 1128 private RowBuilder newRowBuilder(Entity entity, String mimeType) { 1129 // This is a new row; first get the contactId 1130 // If the Contact is new, use the saved back value; otherwise the value in the entity 1131 int contactId = mContactBackValue; 1132 if (entity != null) { 1133 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID); 1134 } 1135 1136 // Create an insert operation with the proper contactId reference 1137 RowBuilder builder = 1138 new RowBuilder(ContentProviderOperation.newInsert( 1139 addCallerIsSyncAdapterParameter(Data.CONTENT_URI))); 1140 if (entity == null) { 1141 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId); 1142 } else { 1143 builder.withValue(Data.RAW_CONTACT_ID, contactId); 1144 } 1145 1146 // Set the mime type of the row 1147 builder.withValue(Data.MIMETYPE, mimeType); 1148 return builder; 1149 } 1150 1151 /** 1152 * Compare a column in a ContentValues with an (old) value, and see if they are the 1153 * same. For this purpose, null and an empty string are considered the same. 1154 * @param cv a ContentValues object, from a NamedContentValues 1155 * @param column a column that might be in the ContentValues 1156 * @param oldValue an old value (or null) to check against 1157 * @return whether the column's value in the ContentValues matches oldValue 1158 */ 1159 private boolean cvCompareString(ContentValues cv, String column, String oldValue) { 1160 if (cv.containsKey(column)) { 1161 if (oldValue != null && cv.getAsString(column).equals(oldValue)) { 1162 return true; 1163 } 1164 } else if (oldValue == null || oldValue.length() == 0) { 1165 return true; 1166 } 1167 return false; 1168 } 1169 1170 public void addChildren(Entity entity, ArrayList<String> children) { 1171 RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE); 1172 int i = 0; 1173 for (String child: children) { 1174 builder.withValue(EasChildren.ROWS[i++], child); 1175 } 1176 add(builder.build()); 1177 } 1178 1179 public void addGroup(Entity entity, String group) { 1180 RowBuilder builder = 1181 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group); 1182 builder.withValue(GroupMembership.GROUP_SOURCE_ID, group); 1183 add(builder.build()); 1184 } 1185 1186 public void addBirthday(Entity entity, String birthday) { 1187 RowBuilder builder = 1188 typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY); 1189 ContentValues cv = builder.cv; 1190 if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) { 1191 return; 1192 } 1193 builder.withValue(Event.START_DATE, birthday); 1194 builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); 1195 add(builder.build()); 1196 } 1197 1198 public void addName(Entity entity, String prefix, String givenName, String familyName, 1199 String middleName, String suffix, String displayName, String yomiFirstName, 1200 String yomiLastName, String fileAs) { 1201 RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE); 1202 ContentValues cv = builder.cv; 1203 if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) && 1204 cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) && 1205 cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) && 1206 cvCompareString(cv, StructuredName.PREFIX, prefix) && 1207 cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) && 1208 cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) && 1209 //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) && 1210 cvCompareString(cv, StructuredName.SUFFIX, suffix)) { 1211 return; 1212 } 1213 builder.withValue(StructuredName.GIVEN_NAME, givenName); 1214 builder.withValue(StructuredName.FAMILY_NAME, familyName); 1215 builder.withValue(StructuredName.MIDDLE_NAME, middleName); 1216 builder.withValue(StructuredName.SUFFIX, suffix); 1217 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName); 1218 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName); 1219 builder.withValue(StructuredName.PREFIX, prefix); 1220 //builder.withValue(StructuredName.DISPLAY_NAME, fileAs); 1221 add(builder.build()); 1222 } 1223 1224 public void addPersonal(Entity entity, EasPersonal personal) { 1225 RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE); 1226 ContentValues cv = builder.cv; 1227 if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) && 1228 cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) { 1229 return; 1230 } 1231 if (!personal.hasData()) { 1232 return; 1233 } 1234 builder.withValue(EasPersonal.FILE_AS, personal.fileAs); 1235 builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary); 1236 add(builder.build()); 1237 } 1238 1239 public void addBusiness(Entity entity, EasBusiness business) { 1240 RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE); 1241 ContentValues cv = builder.cv; 1242 if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) && 1243 cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) && 1244 cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) { 1245 return; 1246 } 1247 if (!business.hasData()) { 1248 return; 1249 } 1250 builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName); 1251 builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId); 1252 builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId); 1253 add(builder.build()); 1254 } 1255 1256 public void addPhoto(Entity entity, String photo) { 1257 RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE); 1258 // We're always going to add this; it's not worth trying to figure out whether the 1259 // picture is the same as the one stored. 1260 byte[] pic = Base64.decode(photo, Base64.DEFAULT); 1261 builder.withValue(Photo.PHOTO, pic); 1262 add(builder.build()); 1263 } 1264 1265 public void addPhone(Entity entity, int type, String phone) { 1266 RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type); 1267 ContentValues cv = builder.cv; 1268 if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) { 1269 return; 1270 } 1271 builder.withValue(Phone.TYPE, type); 1272 builder.withValue(Phone.NUMBER, phone); 1273 add(builder.build()); 1274 } 1275 1276 public void addWebpage(Entity entity, String url) { 1277 RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE); 1278 ContentValues cv = builder.cv; 1279 if (cv != null && cvCompareString(cv, Website.URL, url)) { 1280 return; 1281 } 1282 builder.withValue(Website.TYPE, Website.TYPE_WORK); 1283 builder.withValue(Website.URL, url); 1284 add(builder.build()); 1285 } 1286 1287 public void addRelation(Entity entity, int type, String value) { 1288 RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type); 1289 ContentValues cv = builder.cv; 1290 if (cv != null && cvCompareString(cv, Relation.DATA, value)) { 1291 return; 1292 } 1293 builder.withValue(Relation.TYPE, type); 1294 builder.withValue(Relation.DATA, value); 1295 add(builder.build()); 1296 } 1297 1298 public void addNickname(Entity entity, String name) { 1299 RowBuilder builder = 1300 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT); 1301 ContentValues cv = builder.cv; 1302 if (cv != null && cvCompareString(cv, Nickname.NAME, name)) { 1303 return; 1304 } 1305 builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); 1306 builder.withValue(Nickname.NAME, name); 1307 add(builder.build()); 1308 } 1309 1310 public void addPostal(Entity entity, int type, String street, String city, String state, 1311 String country, String code) { 1312 RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE, 1313 type); 1314 ContentValues cv = builder.cv; 1315 if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) && 1316 cvCompareString(cv, StructuredPostal.STREET, street) && 1317 cvCompareString(cv, StructuredPostal.COUNTRY, country) && 1318 cvCompareString(cv, StructuredPostal.POSTCODE, code) && 1319 cvCompareString(cv, StructuredPostal.REGION, state)) { 1320 return; 1321 } 1322 builder.withValue(StructuredPostal.TYPE, type); 1323 builder.withValue(StructuredPostal.CITY, city); 1324 builder.withValue(StructuredPostal.STREET, street); 1325 builder.withValue(StructuredPostal.COUNTRY, country); 1326 builder.withValue(StructuredPostal.POSTCODE, code); 1327 builder.withValue(StructuredPostal.REGION, state); 1328 add(builder.build()); 1329 } 1330 1331 /** 1332 * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to 1333 * match them with existing rows; if there's a match, everything's great. Otherwise, we 1334 * either need to add a new row for the data, or we have to replace an existing one 1335 * that no longer matches. This is similar to the way Emails are handled. 1336 */ 1337 public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, 1338 int type, int maxRows) { 1339 // Make a list of all same type rows in the existing entity 1340 ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES; 1341 ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES; 1342 if (entity != null) { 1343 oldValues = findUntypedData(entityValues, type, mimeType); 1344 entityValues = entity.getSubValues(); 1345 } 1346 1347 // These will be rows needing replacement with new values 1348 ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>(); 1349 1350 // The count of existing rows 1351 int numRows = oldValues.size(); 1352 for (UntypedRow row: rows) { 1353 boolean found = false; 1354 // If we already have this row, mark it 1355 for (NamedContentValues ncv: oldValues) { 1356 ContentValues cv = ncv.values; 1357 String data = cv.getAsString(COMMON_DATA_ROW); 1358 int rowType = -1; 1359 if (cv.containsKey(COMMON_TYPE_ROW)) { 1360 rowType = cv.getAsInteger(COMMON_TYPE_ROW); 1361 } 1362 if (row.isSameAs(rowType, data)) { 1363 cv.put(FOUND_DATA_ROW, true); 1364 // Remove this to indicate it's still being used 1365 entityValues.remove(ncv); 1366 found = true; 1367 break; 1368 } 1369 } 1370 if (!found) { 1371 // If we don't, there are two possibilities 1372 if (numRows < maxRows) { 1373 // If there are available rows, add a new one 1374 RowBuilder builder = newRowBuilder(entity, mimeType); 1375 row.addValues(builder); 1376 add(builder.build()); 1377 numRows++; 1378 } else { 1379 // Otherwise, say we need to replace a row with this 1380 rowsToReplace.add(row); 1381 } 1382 } 1383 } 1384 1385 // Go through rows needing replacement 1386 for (UntypedRow row: rowsToReplace) { 1387 for (NamedContentValues ncv: oldValues) { 1388 ContentValues cv = ncv.values; 1389 // Find a row that hasn't been used (i.e. doesn't match current rows) 1390 if (!cv.containsKey(FOUND_DATA_ROW)) { 1391 // And update it 1392 RowBuilder builder = new RowBuilder( 1393 ContentProviderOperation 1394 .newUpdate(addCallerIsSyncAdapterParameter( 1395 dataUriFromNamedContentValues(ncv))), 1396 ncv); 1397 row.addValues(builder); 1398 add(builder.build()); 1399 } 1400 } 1401 } 1402 } 1403 1404 public void addOrganization(Entity entity, int type, String company, String title, 1405 String department, String yomiCompanyName, String officeLocation) { 1406 RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type); 1407 ContentValues cv = builder.cv; 1408 if (cv != null && cvCompareString(cv, Organization.COMPANY, company) && 1409 cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) && 1410 cvCompareString(cv, Organization.DEPARTMENT, department) && 1411 cvCompareString(cv, Organization.TITLE, title) && 1412 cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) { 1413 return; 1414 } 1415 builder.withValue(Organization.TYPE, type); 1416 builder.withValue(Organization.COMPANY, company); 1417 builder.withValue(Organization.TITLE, title); 1418 builder.withValue(Organization.DEPARTMENT, department); 1419 builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName); 1420 builder.withValue(Organization.OFFICE_LOCATION, officeLocation); 1421 add(builder.build()); 1422 } 1423 1424 public void addNote(Entity entity, String note) { 1425 RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1); 1426 ContentValues cv = builder.cv; 1427 if (note == null) return; 1428 note = note.replaceAll("\r\n", "\n"); 1429 if (cv != null && cvCompareString(cv, Note.NOTE, note)) { 1430 return; 1431 } 1432 1433 // Reject notes with nothing in them. Often, we get something from Outlook when 1434 // nothing was ever entered. Sigh. 1435 int len = note.length(); 1436 int i = 0; 1437 for (; i < len; i++) { 1438 char c = note.charAt(i); 1439 if (!Character.isWhitespace(c)) { 1440 break; 1441 } 1442 } 1443 if (i == len) return; 1444 1445 builder.withValue(Note.NOTE, note); 1446 add(builder.build()); 1447 } 1448 } 1449 1450 /** 1451 * Generate the uri for the data row associated with this NamedContentValues object 1452 * @param ncv the NamedContentValues object 1453 * @return a uri that can be used to refer to this row 1454 */ 1455 public Uri dataUriFromNamedContentValues(NamedContentValues ncv) { 1456 long id = ncv.values.getAsLong(RawContacts._ID); 1457 Uri dataUri = ContentUris.withAppendedId(ncv.uri, id); 1458 return dataUri; 1459 } 1460 1461 @Override 1462 public void cleanup() { 1463 // Mark the changed contacts dirty = 0 1464 // Permanently delete the user deletions 1465 ContactOperations ops = new ContactOperations(); 1466 for (Long id: mUpdatedIdList) { 1467 ops.add(ContentProviderOperation 1468 .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 1469 .buildUpon() 1470 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1471 .build()) 1472 .withValue(RawContacts.DIRTY, 0).build()); 1473 } 1474 for (Long id: mDeletedIdList) { 1475 ops.add(ContentProviderOperation 1476 .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 1477 .buildUpon() 1478 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1479 .build()) 1480 .build()); 1481 } 1482 ops.execute(); 1483 ContentResolver cr = mContext.getContentResolver(); 1484 if (mGroupsUsed) { 1485 // Make sure the title column is set for all of our groups 1486 // And that all of our groups are visible 1487 // TODO Perhaps the visible part should only happen when the group is created, but 1488 // this is fine for now. 1489 Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI); 1490 Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE}, 1491 Groups.TITLE + " IS NULL", null, null); 1492 ContentValues values = new ContentValues(); 1493 values.put(Groups.GROUP_VISIBLE, 1); 1494 try { 1495 while (c.moveToNext()) { 1496 String sourceId = c.getString(0); 1497 values.put(Groups.TITLE, sourceId); 1498 cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values, 1499 Groups.SOURCE_ID + "=?", new String[] {sourceId}); 1500 } 1501 } finally { 1502 c.close(); 1503 } 1504 } 1505 } 1506 1507 @Override 1508 public String getCollectionName() { 1509 return "Contacts"; 1510 } 1511 1512 private void sendEmail(Serializer s, ContentValues cv, int count, String displayName) 1513 throws IOException { 1514 // Get both parts of the email address (a newly created one in the UI won't have a name) 1515 String addr = cv.getAsString(Email.DATA); 1516 String name = cv.getAsString(Email.DISPLAY_NAME); 1517 if (name == null) { 1518 if (displayName != null) { 1519 name = displayName; 1520 } else { 1521 name = addr; 1522 } 1523 } 1524 // Compose address from name and addr 1525 if (addr != null) { 1526 String value = '\"' + name + "\" <" + addr + '>'; 1527 if (count < MAX_EMAIL_ROWS) { 1528 s.data(EMAIL_TAGS[count], value); 1529 } 1530 } 1531 } 1532 1533 private void sendIm(Serializer s, ContentValues cv, int count) throws IOException { 1534 String value = cv.getAsString(Im.DATA); 1535 if (value == null) return; 1536 if (count < MAX_IM_ROWS) { 1537 s.data(IM_TAGS[count], value); 1538 } 1539 } 1540 1541 private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames) 1542 throws IOException{ 1543 if (cv.containsKey(StructuredPostal.CITY)) { 1544 s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY)); 1545 } 1546 if (cv.containsKey(StructuredPostal.COUNTRY)) { 1547 s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY)); 1548 } 1549 if (cv.containsKey(StructuredPostal.POSTCODE)) { 1550 s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE)); 1551 } 1552 if (cv.containsKey(StructuredPostal.REGION)) { 1553 s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION)); 1554 } 1555 if (cv.containsKey(StructuredPostal.STREET)) { 1556 s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET)); 1557 } 1558 } 1559 1560 private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException { 1561 switch (cv.getAsInteger(StructuredPostal.TYPE)) { 1562 case StructuredPostal.TYPE_HOME: 1563 sendOnePostal(s, cv, HOME_ADDRESS_TAGS); 1564 break; 1565 case StructuredPostal.TYPE_WORK: 1566 sendOnePostal(s, cv, WORK_ADDRESS_TAGS); 1567 break; 1568 case StructuredPostal.TYPE_OTHER: 1569 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS); 1570 break; 1571 default: 1572 break; 1573 } 1574 } 1575 1576 private String sendStructuredName(Serializer s, ContentValues cv) throws IOException { 1577 String displayName = null; 1578 if (cv.containsKey(StructuredName.FAMILY_NAME)) { 1579 s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME)); 1580 } 1581 if (cv.containsKey(StructuredName.GIVEN_NAME)) { 1582 s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME)); 1583 } 1584 if (cv.containsKey(StructuredName.MIDDLE_NAME)) { 1585 s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME)); 1586 } 1587 if (cv.containsKey(StructuredName.SUFFIX)) { 1588 s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX)); 1589 } 1590 if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) { 1591 s.data(Tags.CONTACTS_YOMI_FIRST_NAME, 1592 cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); 1593 } 1594 if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) { 1595 s.data(Tags.CONTACTS_YOMI_LAST_NAME, 1596 cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME)); 1597 } 1598 if (cv.containsKey(StructuredName.PREFIX)) { 1599 s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX)); 1600 } 1601 if (cv.containsKey(StructuredName.DISPLAY_NAME)) { 1602 displayName = cv.getAsString(StructuredName.DISPLAY_NAME); 1603 s.data(Tags.CONTACTS_FILE_AS, displayName); 1604 } 1605 return displayName; 1606 } 1607 1608 private void sendBusiness(Serializer s, ContentValues cv) throws IOException { 1609 if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) { 1610 s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME)); 1611 } 1612 if (cv.containsKey(EasBusiness.CUSTOMER_ID)) { 1613 s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID)); 1614 } 1615 if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) { 1616 s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID)); 1617 } 1618 } 1619 1620 private void sendPersonal(Serializer s, ContentValues cv) throws IOException { 1621 if (cv.containsKey(EasPersonal.ANNIVERSARY)) { 1622 s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY)); 1623 } 1624 if (cv.containsKey(EasPersonal.FILE_AS)) { 1625 s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS)); 1626 } 1627 } 1628 1629 private void sendBirthday(Serializer s, ContentValues cv) throws IOException { 1630 if (cv.containsKey(Event.START_DATE)) { 1631 s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(Event.START_DATE)); 1632 } 1633 } 1634 1635 private void sendPhoto(Serializer s, ContentValues cv) throws IOException { 1636 if (cv.containsKey(Photo.PHOTO)) { 1637 byte[] bytes = cv.getAsByteArray(Photo.PHOTO); 1638 String pic = Base64.encodeToString(bytes, Base64.NO_WRAP); 1639 s.data(Tags.CONTACTS_PICTURE, pic); 1640 } 1641 } 1642 1643 private void sendOrganization(Serializer s, ContentValues cv) throws IOException { 1644 if (cv.containsKey(Organization.TITLE)) { 1645 s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE)); 1646 } 1647 if (cv.containsKey(Organization.COMPANY)) { 1648 s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY)); 1649 } 1650 if (cv.containsKey(Organization.DEPARTMENT)) { 1651 s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT)); 1652 } 1653 if (cv.containsKey(Organization.OFFICE_LOCATION)) { 1654 s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(Organization.OFFICE_LOCATION)); 1655 } 1656 } 1657 1658 private void sendNickname(Serializer s, ContentValues cv) throws IOException { 1659 if (cv.containsKey(Nickname.NAME)) { 1660 s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME)); 1661 } 1662 } 1663 1664 private void sendWebpage(Serializer s, ContentValues cv) throws IOException { 1665 if (cv.containsKey(Website.URL)) { 1666 s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL)); 1667 } 1668 } 1669 1670 private void sendNote(Serializer s, ContentValues cv) throws IOException { 1671 if (cv.containsKey(Note.NOTE)) { 1672 // EAS won't accept note data with raw newline characters 1673 String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n"); 1674 // Format of upsync data depends on protocol version 1675 if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 1676 s.start(Tags.BASE_BODY); 1677 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); 1678 s.end(); 1679 } else { 1680 s.data(Tags.CONTACTS_BODY, note); 1681 } 1682 } 1683 } 1684 1685 private void sendChildren(Serializer s, ContentValues cv) throws IOException { 1686 boolean first = true; 1687 for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { 1688 String row = EasChildren.ROWS[i]; 1689 if (cv.containsKey(row)) { 1690 if (first) { 1691 s.start(Tags.CONTACTS_CHILDREN); 1692 first = false; 1693 } 1694 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row)); 1695 } 1696 } 1697 if (!first) { 1698 s.end(); 1699 } 1700 } 1701 1702 private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount) 1703 throws IOException { 1704 String value = cv.getAsString(Phone.NUMBER); 1705 if (value == null) return; 1706 switch (cv.getAsInteger(Phone.TYPE)) { 1707 case Phone.TYPE_WORK: 1708 if (workCount < MAX_PHONE_ROWS) { 1709 s.data(WORK_PHONE_TAGS[workCount], value); 1710 } 1711 break; 1712 case Phone.TYPE_MMS: 1713 s.data(Tags.CONTACTS2_MMS, value); 1714 break; 1715 case Phone.TYPE_ASSISTANT: 1716 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value); 1717 break; 1718 case Phone.TYPE_FAX_WORK: 1719 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value); 1720 break; 1721 case Phone.TYPE_COMPANY_MAIN: 1722 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value); 1723 break; 1724 case Phone.TYPE_HOME: 1725 if (homeCount < MAX_PHONE_ROWS) { 1726 s.data(HOME_PHONE_TAGS[homeCount], value); 1727 } 1728 break; 1729 case Phone.TYPE_MOBILE: 1730 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value); 1731 break; 1732 case Phone.TYPE_CAR: 1733 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value); 1734 break; 1735 case Phone.TYPE_PAGER: 1736 s.data(Tags.CONTACTS_PAGER_NUMBER, value); 1737 break; 1738 case Phone.TYPE_RADIO: 1739 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value); 1740 break; 1741 case Phone.TYPE_FAX_HOME: 1742 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value); 1743 break; 1744 default: 1745 break; 1746 } 1747 } 1748 1749 private void sendRelation(Serializer s, ContentValues cv) throws IOException { 1750 String value = cv.getAsString(Relation.DATA); 1751 if (value == null) return; 1752 switch (cv.getAsInteger(Relation.TYPE)) { 1753 case Relation.TYPE_ASSISTANT: 1754 s.data(Tags.CONTACTS_ASSISTANT_NAME, value); 1755 break; 1756 case Relation.TYPE_MANAGER: 1757 s.data(Tags.CONTACTS2_MANAGER_NAME, value); 1758 break; 1759 case Relation.TYPE_SPOUSE: 1760 s.data(Tags.CONTACTS_SPOUSE, value); 1761 break; 1762 default: 1763 break; 1764 } 1765 } 1766 1767 @Override 1768 public boolean sendLocalChanges(Serializer s) throws IOException { 1769 // First, let's find Contacts that have changed. 1770 ContentResolver cr = mService.mContentResolver; 1771 Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() 1772 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) 1773 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, 1774 com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE) 1775 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1776 .build(); 1777 1778 if (getSyncKey().equals("0")) { 1779 return false; 1780 } 1781 1782 try { 1783 // Get them all atomically 1784 EntityIterator ei = RawContacts.newEntityIterator( 1785 cr.query(uri, null, RawContacts.DIRTY + "=1", null, null)); 1786 ContentValues cidValues = new ContentValues(); 1787 try { 1788 boolean first = true; 1789 final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI); 1790 while (ei.hasNext()) { 1791 Entity entity = ei.next(); 1792 // For each of these entities, create the change commands 1793 ContentValues entityValues = entity.getEntityValues(); 1794 String serverId = entityValues.getAsString(RawContacts.SOURCE_ID); 1795 ArrayList<Integer> groupIds = new ArrayList<Integer>(); 1796 if (first) { 1797 s.start(Tags.SYNC_COMMANDS); 1798 userLog("Sending Contacts changes to the server"); 1799 first = false; 1800 } 1801 if (serverId == null) { 1802 // This is a new contact; create a clientId 1803 String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis(); 1804 userLog("Creating new contact with clientId: ", clientId); 1805 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 1806 // And save it in the raw contact 1807 cidValues.put(ContactsContract.RawContacts.SYNC1, clientId); 1808 cr.update(ContentUris. 1809 withAppendedId(rawContactUri, 1810 entityValues.getAsLong(ContactsContract.RawContacts._ID)), 1811 cidValues, null, null); 1812 } else { 1813 if (entityValues.getAsInteger(RawContacts.DELETED) == 1) { 1814 userLog("Deleting contact with serverId: ", serverId); 1815 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 1816 mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID)); 1817 continue; 1818 } 1819 userLog("Upsync change to contact with serverId: " + serverId); 1820 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 1821 } 1822 s.start(Tags.SYNC_APPLICATION_DATA); 1823 // Write out the data here 1824 int imCount = 0; 1825 int emailCount = 0; 1826 int homePhoneCount = 0; 1827 int workPhoneCount = 0; 1828 String displayName = null; 1829 ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>(); 1830 for (NamedContentValues ncv: entity.getSubValues()) { 1831 ContentValues cv = ncv.values; 1832 String mimeType = cv.getAsString(Data.MIMETYPE); 1833 if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 1834 emailValues.add(cv); 1835 } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) { 1836 sendNickname(s, cv); 1837 } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) { 1838 sendChildren(s, cv); 1839 } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) { 1840 sendBusiness(s, cv); 1841 } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) { 1842 sendWebpage(s, cv); 1843 } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) { 1844 sendPersonal(s, cv); 1845 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 1846 sendPhone(s, cv, workPhoneCount, homePhoneCount); 1847 int type = cv.getAsInteger(Phone.TYPE); 1848 if (type == Phone.TYPE_HOME) homePhoneCount++; 1849 if (type == Phone.TYPE_WORK) workPhoneCount++; 1850 } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) { 1851 sendRelation(s, cv); 1852 } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 1853 displayName = sendStructuredName(s, cv); 1854 } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) { 1855 sendStructuredPostal(s, cv); 1856 } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) { 1857 sendOrganization(s, cv); 1858 } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { 1859 sendIm(s, cv, imCount++); 1860 } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) { 1861 Integer eventType = cv.getAsInteger(Event.TYPE); 1862 if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) { 1863 sendBirthday(s, cv); 1864 } 1865 } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { 1866 // We must gather these, and send them together (below) 1867 groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); 1868 } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { 1869 sendNote(s, cv); 1870 } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 1871 sendPhoto(s, cv); 1872 } else { 1873 userLog("Contacts upsync, unknown data: ", mimeType); 1874 } 1875 } 1876 1877 // We do the email rows last, because we need to make sure we've found the 1878 // displayName (if one exists); this would be in a StructuredName rnow 1879 for (ContentValues cv: emailValues) { 1880 sendEmail(s, cv, emailCount++, displayName); 1881 } 1882 1883 // Now, we'll send up groups, if any 1884 if (!groupIds.isEmpty()) { 1885 boolean groupFirst = true; 1886 for (int id: groupIds) { 1887 // Since we get id's from the provider, we need to find their names 1888 Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id), 1889 GROUP_PROJECTION, null, null, null); 1890 try { 1891 // Presumably, this should always succeed, but ... 1892 if (c.moveToFirst()) { 1893 if (groupFirst) { 1894 s.start(Tags.CONTACTS_CATEGORIES); 1895 groupFirst = false; 1896 } 1897 s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); 1898 } 1899 } finally { 1900 c.close(); 1901 } 1902 } 1903 if (!groupFirst) { 1904 s.end(); 1905 } 1906 } 1907 s.end().end(); // ApplicationData & Change 1908 mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID)); 1909 } 1910 if (!first) { 1911 s.end(); // Commands 1912 } 1913 } finally { 1914 ei.close(); 1915 } 1916 } catch (RemoteException e) { 1917 Log.e(TAG, "Could not read dirty contacts."); 1918 } 1919 1920 return false; 1921 } 1922} 1923