ContactsSyncAdapter.java revision 1368f7c03e121f3240cfdd1698b86715a2655729
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.codec.binary.Base64; 21import com.android.email.provider.EmailContent.Mailbox; 22import com.android.exchange.Eas; 23import com.android.exchange.EasSyncService; 24 25import android.content.ContentProviderClient; 26import android.content.ContentProviderOperation; 27import android.content.ContentProviderResult; 28import android.content.ContentResolver; 29import android.content.ContentUris; 30import android.content.ContentValues; 31import android.content.Entity; 32import android.content.EntityIterator; 33import android.content.OperationApplicationException; 34import android.content.ContentProviderOperation.Builder; 35import android.content.Entity.NamedContentValues; 36import android.database.Cursor; 37import android.net.Uri; 38import android.os.RemoteException; 39import android.provider.ContactsContract; 40import android.provider.SyncStateContract; 41import android.provider.ContactsContract.Data; 42import android.provider.ContactsContract.Groups; 43import android.provider.ContactsContract.RawContacts; 44import android.provider.ContactsContract.Settings; 45import android.provider.ContactsContract.SyncState; 46import android.provider.ContactsContract.CommonDataKinds.Birthday; 47import android.provider.ContactsContract.CommonDataKinds.Email; 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; 62 63import java.io.IOException; 64import java.io.InputStream; 65import java.util.ArrayList; 66 67/** 68 * Sync adapter for EAS Contacts 69 * 70 */ 71public class ContactsSyncAdapter extends AbstractSyncAdapter { 72 73 private static final String TAG = "EasContactsSyncAdapter"; 74 private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?"; 75 private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?"; 76 private static final String[] ID_PROJECTION = new String[] {RawContacts._ID}; 77 private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID}; 78 79 private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW"; 80 81 private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 82 Tags.CONTACTS_HOME_ADDRESS_COUNTRY, 83 Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE, 84 Tags.CONTACTS_HOME_ADDRESS_STATE, 85 Tags.CONTACTS_HOME_ADDRESS_STREET}; 86 87 private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY, 88 Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY, 89 Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE, 90 Tags.CONTACTS_BUSINESS_ADDRESS_STATE, 91 Tags.CONTACTS_BUSINESS_ADDRESS_STREET}; 92 93 private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 94 Tags.CONTACTS_OTHER_ADDRESS_COUNTRY, 95 Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE, 96 Tags.CONTACTS_OTHER_ADDRESS_STATE, 97 Tags.CONTACTS_OTHER_ADDRESS_STREET}; 98 99 private static final int MAX_IM_ROWS = 3; 100 private static final int MAX_EMAIL_ROWS = 3; 101 private static final int MAX_PHONE_ROWS = 2; 102 private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc. 103 private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row 104 105 private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS, 106 Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3}; 107 108 private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS, 109 Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS}; 110 111 private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, 112 Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER}; 113 114 private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER, 115 Tags.CONTACTS_HOME2_TELEPHONE_NUMBER}; 116 117 ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); 118 ArrayList<Long> mUpdatedIdList = new ArrayList<Long>(); 119 120 private boolean mGroupsUsed = false; 121 122 private android.accounts.Account mAccountManagerAccount; 123 124 public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) { 125 super(mailbox, service); 126 } 127 128 static Uri addCallerIsSyncAdapterParameter(Uri uri) { 129 return uri.buildUpon() 130 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 131 .build(); 132 } 133 134 @Override 135 public boolean parse(InputStream is) throws IOException { 136 EasContactsSyncParser p = new EasContactsSyncParser(is, this); 137 return p.parse(); 138 } 139 140 interface UntypedRow { 141 public void addValues(RowBuilder builder); 142 public boolean isSameAs(int type, String value); 143 } 144 145 /** 146 * We get our SyncKey from ContactsProvider. If there's not one, we set it to "0" (the reset 147 * state) and save that away. 148 */ 149 @Override 150 public String getSyncKey() throws IOException { 151 ContentProviderClient client = 152 mService.mContentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY_URI); 153 try { 154 byte[] data = SyncStateContract.Helpers.get(client, 155 ContactsContract.SyncState.CONTENT_URI, getAccountManagerAccount()); 156 if (data == null || data.length == 0) { 157 // Initialize the SyncKey 158 setSyncKey("0", false); 159 // Make sure ungrouped contacts for Exchange are defaultly visible 160 ContentValues cv = new ContentValues(); 161 cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress); 162 cv.put(Groups.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE); 163 cv.put(Settings.UNGROUPED_VISIBLE, true); 164 client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv); 165 return "0"; 166 } else { 167 return new String(data); 168 } 169 } catch (RemoteException e) { 170 throw new IOException("Can't get SyncKey from ContactsProvider"); 171 } 172 } 173 174 /** 175 * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other 176 * cases, the SyncKey is set within ContactOperations 177 */ 178 @Override 179 public void setSyncKey(String syncKey, boolean inCommands) throws IOException { 180 if ("0".equals(syncKey) || !inCommands) { 181 ContentProviderClient client = 182 mService.mContentResolver 183 .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); 184 try { 185 SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI, 186 getAccountManagerAccount(), syncKey.getBytes()); 187 userLog("SyncKey set to ", syncKey, " in ContactsProvider"); 188 } catch (RemoteException e) { 189 throw new IOException("Can't set SyncKey in ContactsProvider"); 190 } 191 } 192 mMailbox.mSyncKey = syncKey; 193 } 194 195 public android.accounts.Account getAccountManagerAccount() { 196 if (mAccountManagerAccount == null) { 197 mAccountManagerAccount = 198 new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE); 199 } 200 return mAccountManagerAccount; 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 try { 742 EntityIterator entityIterator = 743 mContentResolver.queryEntities(ContentUris 744 .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)), 745 null, null, null); 746 if (entityIterator.hasNext()) { 747 entity = entityIterator.next(); 748 } 749 userLog("Changing contact ", serverId); 750 } catch (RemoteException e) { 751 } 752 } 753 } finally { 754 c.close(); 755 } 756 break; 757 case Tags.SYNC_APPLICATION_DATA: 758 addData(serverId, ops, entity); 759 break; 760 default: 761 skipTag(); 762 } 763 } 764 } 765 766 @Override 767 public void commandsParser() throws IOException { 768 while (nextTag(Tags.SYNC_COMMANDS) != END) { 769 if (tag == Tags.SYNC_ADD) { 770 addParser(ops); 771 incrementChangeCount(); 772 } else if (tag == Tags.SYNC_DELETE) { 773 deleteParser(ops); 774 incrementChangeCount(); 775 } else if (tag == Tags.SYNC_CHANGE) { 776 changeParser(ops); 777 incrementChangeCount(); 778 } else 779 skipTag(); 780 } 781 } 782 783 @Override 784 public void commit() throws IOException { 785 // Save the syncKey here, using the Helper provider by Contacts provider 786 userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey); 787 ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI, 788 getAccountManagerAccount(), mMailbox.mSyncKey.getBytes())); 789 790 // Execute these all at once... 791 ops.execute(); 792 793 if (ops.mResults != null) { 794 ContentValues cv = new ContentValues(); 795 cv.put(RawContacts.DIRTY, 0); 796 for (int i = 0; i < ops.mContactIndexCount; i++) { 797 int index = ops.mContactIndexArray[i]; 798 Uri u = ops.mResults[index].uri; 799 if (u != null) { 800 String idString = u.getLastPathSegment(); 801 mContentResolver.update( 802 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv, 803 RawContacts._ID + "=" + idString, null); 804 } 805 } 806 } 807 } 808 809 public void addResponsesParser() throws IOException { 810 String serverId = null; 811 String clientId = null; 812 ContentValues cv = new ContentValues(); 813 while (nextTag(Tags.SYNC_ADD) != END) { 814 switch (tag) { 815 case Tags.SYNC_SERVER_ID: 816 serverId = getValue(); 817 break; 818 case Tags.SYNC_CLIENT_ID: 819 clientId = getValue(); 820 break; 821 case Tags.SYNC_STATUS: 822 getValue(); 823 break; 824 default: 825 skipTag(); 826 } 827 } 828 829 // This is theoretically impossible, but... 830 if (clientId == null || serverId == null) return; 831 832 Cursor c = getClientIdCursor(clientId); 833 try { 834 if (c.moveToFirst()) { 835 cv.put(RawContacts.SOURCE_ID, serverId); 836 cv.put(RawContacts.DIRTY, 0); 837 ops.add(ContentProviderOperation.newUpdate( 838 ContentUris.withAppendedId( 839 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), 840 c.getLong(0))) 841 .withValues(cv) 842 .build()); 843 userLog("New contact " + clientId + " was given serverId: " + serverId); 844 } 845 } finally { 846 c.close(); 847 } 848 } 849 850 public void changeResponsesParser() throws IOException { 851 String serverId = null; 852 String status = null; 853 while (nextTag(Tags.SYNC_CHANGE) != END) { 854 switch (tag) { 855 case Tags.SYNC_SERVER_ID: 856 serverId = getValue(); 857 break; 858 case Tags.SYNC_STATUS: 859 status = getValue(); 860 break; 861 default: 862 skipTag(); 863 } 864 } 865 if (serverId != null && status != null) { 866 userLog("Changed contact " + serverId + " failed with status: " + status); 867 } 868 } 869 870 871 @Override 872 public void responsesParser() throws IOException { 873 // Handle server responses here (for Add and Change) 874 while (nextTag(Tags.SYNC_RESPONSES) != END) { 875 if (tag == Tags.SYNC_ADD) { 876 addResponsesParser(); 877 } else if (tag == Tags.SYNC_CHANGE) { 878 changeResponsesParser(); 879 } else 880 skipTag(); 881 } 882 } 883 } 884 885 886 private Uri uriWithAccountAndIsSyncAdapter(Uri uri) { 887 return uri.buildUpon() 888 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) 889 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE) 890 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 891 .build(); 892 } 893 894 /** 895 * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a 896 * ContentProvider. It has, in addition to the Builder, ContentValues which, if present, 897 * represent the current values of that row, that can be compared against current values to 898 * see whether an update is even necessary. The methods on SmartBuilder are delegated to 899 * the Builder. 900 */ 901 private class RowBuilder { 902 Builder builder; 903 ContentValues cv; 904 905 public RowBuilder(Builder _builder) { 906 builder = _builder; 907 } 908 909 public RowBuilder(Builder _builder, NamedContentValues _ncv) { 910 builder = _builder; 911 cv = _ncv.values; 912 } 913 914 RowBuilder withValues(ContentValues values) { 915 builder.withValues(values); 916 return this; 917 } 918 919 RowBuilder withValueBackReference(String key, int previousResult) { 920 builder.withValueBackReference(key, previousResult); 921 return this; 922 } 923 924 ContentProviderOperation build() { 925 return builder.build(); 926 } 927 928 RowBuilder withValue(String key, Object value) { 929 builder.withValue(key, value); 930 return this; 931 } 932 } 933 934 private class ContactOperations extends ArrayList<ContentProviderOperation> { 935 private static final long serialVersionUID = 1L; 936 private int mCount = 0; 937 private int mContactBackValue = mCount; 938 // Make an array big enough for the PIM window (max items we can get) 939 private int[] mContactIndexArray = 940 new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)]; 941 private int mContactIndexCount = 0; 942 private ContentProviderResult[] mResults = null; 943 944 @Override 945 public boolean add(ContentProviderOperation op) { 946 super.add(op); 947 mCount++; 948 return true; 949 } 950 951 public void newContact(String serverId) { 952 Builder builder = ContentProviderOperation 953 .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI)); 954 ContentValues values = new ContentValues(); 955 values.put(RawContacts.SOURCE_ID, serverId); 956 builder.withValues(values); 957 mContactBackValue = mCount; 958 mContactIndexArray[mContactIndexCount++] = mCount; 959 add(builder.build()); 960 } 961 962 public void delete(long id) { 963 add(ContentProviderOperation 964 .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 965 .buildUpon() 966 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 967 .build()) 968 .build()); 969 } 970 971 public void execute() { 972 synchronized (mService.getSynchronizer()) { 973 if (!mService.isStopped()) { 974 try { 975 if (!isEmpty()) { 976 mService.userLog("Executing ", size(), " CPO's"); 977 mResults = mContext.getContentResolver().applyBatch( 978 ContactsContract.AUTHORITY, this); 979 } 980 } catch (RemoteException e) { 981 // There is nothing sensible to be done here 982 Log.e(TAG, "problem inserting contact during server update", e); 983 } catch (OperationApplicationException e) { 984 // There is nothing sensible to be done here 985 Log.e(TAG, "problem inserting contact during server update", e); 986 } 987 } 988 } 989 } 990 991 /** 992 * Given the list of NamedContentValues for an entity, a mime type, and a subtype, 993 * tries to find a match, returning it 994 * @param list the list of NCV's from the contact entity 995 * @param contentItemType the mime type we're looking for 996 * @param type the subtype (e.g. HOME, WORK, etc.) 997 * @return the matching NCV or null if not found 998 */ 999 private NamedContentValues findTypedData(ArrayList<NamedContentValues> list, 1000 String contentItemType, int type, String stringType) { 1001 NamedContentValues result = null; 1002 1003 // Loop through the ncv's, looking for an existing row 1004 for (NamedContentValues namedContentValues: list) { 1005 Uri uri = namedContentValues.uri; 1006 ContentValues cv = namedContentValues.values; 1007 if (Data.CONTENT_URI.equals(uri)) { 1008 String mimeType = cv.getAsString(Data.MIMETYPE); 1009 if (mimeType.equals(contentItemType)) { 1010 if (stringType != null) { 1011 if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) { 1012 result = namedContentValues; 1013 } 1014 // Note Email.TYPE could be ANY type column; they are all defined in 1015 // the private CommonColumns class in ContactsContract 1016 } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) { 1017 result = namedContentValues; 1018 } 1019 } 1020 } 1021 } 1022 1023 // If we've found an existing data row, we'll delete it. Any rows left at the 1024 // end should be deleted... 1025 if (result != null) { 1026 list.remove(result); 1027 } 1028 1029 // Return the row found (or null) 1030 return result; 1031 } 1032 1033 /** 1034 * Given the list of NamedContentValues for an entity and a mime type 1035 * gather all of the matching NCV's, returning them 1036 * @param list the list of NCV's from the contact entity 1037 * @param contentItemType the mime type we're looking for 1038 * @param type the subtype (e.g. HOME, WORK, etc.) 1039 * @return the matching NCVs 1040 */ 1041 private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list, 1042 int type, String contentItemType) { 1043 ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>(); 1044 1045 // Loop through the ncv's, looking for an existing row 1046 for (NamedContentValues namedContentValues: list) { 1047 Uri uri = namedContentValues.uri; 1048 ContentValues cv = namedContentValues.values; 1049 if (Data.CONTENT_URI.equals(uri)) { 1050 String mimeType = cv.getAsString(Data.MIMETYPE); 1051 if (mimeType.equals(contentItemType)) { 1052 if (type != -1) { 1053 int subtype = cv.getAsInteger(Phone.TYPE); 1054 if (type != subtype) { 1055 continue; 1056 } 1057 } 1058 result.add(namedContentValues); 1059 } 1060 } 1061 } 1062 1063 // If we've found an existing data row, we'll delete it. Any rows left at the 1064 // end should be deleted... 1065 if (result != null) { 1066 list.remove(result); 1067 } 1068 1069 // Return the row found (or null) 1070 return result; 1071 } 1072 1073 /** 1074 * Create a wrapper for a builder (insert or update) that also includes the NCV for 1075 * an existing row of this type. If the SmartBuilder's cv field is not null, then 1076 * it represents the current (old) values of this field. The caller can then check 1077 * whether the field is now different and needs to be updated; if it's not different, 1078 * the caller will simply return and not generate a new CPO. Otherwise, the builder 1079 * should have its content values set, and the built CPO should be added to the 1080 * ContactOperations list. 1081 * 1082 * @param entity the contact entity (or null if this is a new contact) 1083 * @param mimeType the mime type of this row 1084 * @param type the subtype of this row 1085 * @param stringType for groups, the name of the group (type will be ignored), or null 1086 * @return the created SmartBuilder 1087 */ 1088 public RowBuilder createBuilder(Entity entity, String mimeType, int type, 1089 String stringType) { 1090 RowBuilder builder = null; 1091 1092 if (entity != null) { 1093 NamedContentValues ncv = 1094 findTypedData(entity.getSubValues(), mimeType, type, stringType); 1095 if (ncv != null) { 1096 builder = new RowBuilder( 1097 ContentProviderOperation 1098 .newUpdate(addCallerIsSyncAdapterParameter( 1099 dataUriFromNamedContentValues(ncv))), 1100 ncv); 1101 } 1102 } 1103 1104 if (builder == null) { 1105 builder = newRowBuilder(entity, mimeType); 1106 } 1107 1108 // Return the appropriate builder (insert or update) 1109 // Caller will fill in the appropriate values; 4 MIMETYPE is already set 1110 return builder; 1111 } 1112 1113 private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { 1114 return createBuilder(entity, mimeType, type, null); 1115 } 1116 1117 private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { 1118 return createBuilder(entity, mimeType, -1, null); 1119 } 1120 1121 private RowBuilder newRowBuilder(Entity entity, String mimeType) { 1122 // This is a new row; first get the contactId 1123 // If the Contact is new, use the saved back value; otherwise the value in the entity 1124 int contactId = mContactBackValue; 1125 if (entity != null) { 1126 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID); 1127 } 1128 1129 // Create an insert operation with the proper contactId reference 1130 RowBuilder builder = 1131 new RowBuilder(ContentProviderOperation.newInsert( 1132 addCallerIsSyncAdapterParameter(Data.CONTENT_URI))); 1133 if (entity == null) { 1134 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId); 1135 } else { 1136 builder.withValue(Data.RAW_CONTACT_ID, contactId); 1137 } 1138 1139 // Set the mime type of the row 1140 builder.withValue(Data.MIMETYPE, mimeType); 1141 return builder; 1142 } 1143 1144 /** 1145 * Compare a column in a ContentValues with an (old) value, and see if they are the 1146 * same. For this purpose, null and an empty string are considered the same. 1147 * @param cv a ContentValues object, from a NamedContentValues 1148 * @param column a column that might be in the ContentValues 1149 * @param oldValue an old value (or null) to check against 1150 * @return whether the column's value in the ContentValues matches oldValue 1151 */ 1152 private boolean cvCompareString(ContentValues cv, String column, String oldValue) { 1153 if (cv.containsKey(column)) { 1154 if (oldValue != null && cv.getAsString(column).equals(oldValue)) { 1155 return true; 1156 } 1157 } else if (oldValue == null || oldValue.length() == 0) { 1158 return true; 1159 } 1160 return false; 1161 } 1162 1163 public void addChildren(Entity entity, ArrayList<String> children) { 1164 RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE); 1165 int i = 0; 1166 for (String child: children) { 1167 builder.withValue(EasChildren.ROWS[i++], child); 1168 } 1169 add(builder.build()); 1170 } 1171 1172 public void addGroup(Entity entity, String group) { 1173 RowBuilder builder = 1174 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group); 1175 builder.withValue(GroupMembership.GROUP_SOURCE_ID, group); 1176 add(builder.build()); 1177 } 1178 1179 public void addBirthday(Entity entity, String birthday) { 1180 RowBuilder builder = 1181 createBuilder(entity, Birthday.CONTENT_ITEM_TYPE, -1, birthday); 1182 builder.withValue(Birthday.BIRTHDAY, birthday); 1183 add(builder.build()); 1184 } 1185 1186 public void addName(Entity entity, String prefix, String givenName, String familyName, 1187 String middleName, String suffix, String displayName, String yomiFirstName, 1188 String yomiLastName, String fileAs) { 1189 RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE); 1190 ContentValues cv = builder.cv; 1191 if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) && 1192 cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) && 1193 cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) && 1194 cvCompareString(cv, StructuredName.PREFIX, prefix) && 1195 cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) && 1196 cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) && 1197 //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) && 1198 cvCompareString(cv, StructuredName.SUFFIX, suffix)) { 1199 return; 1200 } 1201 builder.withValue(StructuredName.GIVEN_NAME, givenName); 1202 builder.withValue(StructuredName.FAMILY_NAME, familyName); 1203 builder.withValue(StructuredName.MIDDLE_NAME, middleName); 1204 builder.withValue(StructuredName.SUFFIX, suffix); 1205 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName); 1206 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName); 1207 builder.withValue(StructuredName.PREFIX, prefix); 1208 //builder.withValue(StructuredName.DISPLAY_NAME, fileAs); 1209 add(builder.build()); 1210 } 1211 1212 public void addPersonal(Entity entity, EasPersonal personal) { 1213 RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE); 1214 ContentValues cv = builder.cv; 1215 if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) && 1216 cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) { 1217 return; 1218 } 1219 if (!personal.hasData()) { 1220 return; 1221 } 1222 builder.withValue(EasPersonal.FILE_AS, personal.fileAs); 1223 builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary); 1224 add(builder.build()); 1225 } 1226 1227 public void addBusiness(Entity entity, EasBusiness business) { 1228 RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE); 1229 ContentValues cv = builder.cv; 1230 if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) && 1231 cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) && 1232 cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) { 1233 return; 1234 } 1235 if (!business.hasData()) { 1236 return; 1237 } 1238 builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName); 1239 builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId); 1240 builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId); 1241 add(builder.build()); 1242 } 1243 1244 public void addPhoto(Entity entity, String photo) { 1245 RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE); 1246 // We're always going to add this; it's not worth trying to figure out whether the 1247 // picture is the same as the one stored. 1248 byte[] pic = Base64.decodeBase64(photo.getBytes()); 1249 builder.withValue(Photo.PHOTO, pic); 1250 add(builder.build()); 1251 } 1252 1253 public void addPhone(Entity entity, int type, String phone) { 1254 RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type); 1255 ContentValues cv = builder.cv; 1256 if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) { 1257 return; 1258 } 1259 builder.withValue(Phone.TYPE, type); 1260 builder.withValue(Phone.NUMBER, phone); 1261 add(builder.build()); 1262 } 1263 1264 public void addWebpage(Entity entity, String url) { 1265 RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE); 1266 ContentValues cv = builder.cv; 1267 if (cv != null && cvCompareString(cv, Website.URL, url)) { 1268 return; 1269 } 1270 builder.withValue(Website.TYPE, Website.TYPE_WORK); 1271 builder.withValue(Website.URL, url); 1272 add(builder.build()); 1273 } 1274 1275 public void addRelation(Entity entity, int type, String value) { 1276 RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type); 1277 ContentValues cv = builder.cv; 1278 if (cv != null && cvCompareString(cv, Relation.DATA, value)) { 1279 return; 1280 } 1281 builder.withValue(Relation.TYPE, type); 1282 builder.withValue(Relation.DATA, value); 1283 add(builder.build()); 1284 } 1285 1286 public void addNickname(Entity entity, String name) { 1287 RowBuilder builder = 1288 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT); 1289 ContentValues cv = builder.cv; 1290 if (cv != null && cvCompareString(cv, Nickname.NAME, name)) { 1291 return; 1292 } 1293 builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); 1294 builder.withValue(Nickname.NAME, name); 1295 add(builder.build()); 1296 } 1297 1298 public void addPostal(Entity entity, int type, String street, String city, String state, 1299 String country, String code) { 1300 RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE, 1301 type); 1302 ContentValues cv = builder.cv; 1303 if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) && 1304 cvCompareString(cv, StructuredPostal.STREET, street) && 1305 cvCompareString(cv, StructuredPostal.COUNTRY, country) && 1306 cvCompareString(cv, StructuredPostal.POSTCODE, code) && 1307 cvCompareString(cv, StructuredPostal.REGION, state)) { 1308 return; 1309 } 1310 builder.withValue(StructuredPostal.TYPE, type); 1311 builder.withValue(StructuredPostal.CITY, city); 1312 builder.withValue(StructuredPostal.STREET, street); 1313 builder.withValue(StructuredPostal.COUNTRY, country); 1314 builder.withValue(StructuredPostal.POSTCODE, code); 1315 builder.withValue(StructuredPostal.REGION, state); 1316 add(builder.build()); 1317 } 1318 1319 /** 1320 * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to 1321 * match them with existing rows; if there's a match, everything's great. Otherwise, we 1322 * either need to add a new row for the data, or we have to replace an existing one 1323 * that no longer matches. This is similar to the way Emails are handled. 1324 */ 1325 public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, 1326 int type, int maxRows) { 1327 // Make a list of all same type rows in the existing entity 1328 ArrayList<NamedContentValues> oldAccounts = new ArrayList<NamedContentValues>(); 1329 if (entity != null) { 1330 oldAccounts = findUntypedData(entity.getSubValues(), type, mimeType); 1331 } 1332 1333 // These will be rows needing replacement with new values 1334 ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>(); 1335 1336 // The count of existing rows 1337 int numRows = oldAccounts.size(); 1338 for (UntypedRow row: rows) { 1339 boolean found = false; 1340 // If we already have this IM address, mark it 1341 for (NamedContentValues ncv: oldAccounts) { 1342 ContentValues cv = ncv.values; 1343 String data = cv.getAsString(COMMON_DATA_ROW); 1344 int rowType = -1; 1345 if (cv.containsKey(COMMON_TYPE_ROW)) { 1346 rowType = cv.getAsInteger(COMMON_TYPE_ROW); 1347 } 1348 if (row.isSameAs(rowType, data)) { 1349 cv.put(FOUND_DATA_ROW, true); 1350 found = true; 1351 break; 1352 } 1353 } 1354 if (!found) { 1355 // If we don't, there are two possibilities 1356 if (numRows < maxRows) { 1357 // If there are available rows, add a new one 1358 RowBuilder builder = newRowBuilder(entity, mimeType); 1359 row.addValues(builder); 1360 add(builder.build()); 1361 numRows++; 1362 } else { 1363 // Otherwise, say we need to replace a row with this 1364 rowsToReplace.add(row); 1365 } 1366 } 1367 } 1368 1369 // Go through rows needing replacement 1370 for (UntypedRow row: rowsToReplace) { 1371 for (NamedContentValues ncv: oldAccounts) { 1372 ContentValues cv = ncv.values; 1373 // Find a row that hasn't been used (i.e. doesn't match current rows) 1374 if (!cv.containsKey(FOUND_DATA_ROW)) { 1375 // And update it 1376 RowBuilder builder = new RowBuilder( 1377 ContentProviderOperation 1378 .newUpdate(addCallerIsSyncAdapterParameter( 1379 dataUriFromNamedContentValues(ncv))), 1380 ncv); 1381 row.addValues(builder); 1382 add(builder.build()); 1383 } 1384 } 1385 } 1386 } 1387 1388 public void addOrganization(Entity entity, int type, String company, String title, 1389 String department, String yomiCompanyName, String officeLocation) { 1390 RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type); 1391 ContentValues cv = builder.cv; 1392 if (cv != null && cvCompareString(cv, Organization.COMPANY, company) && 1393 cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) && 1394 cvCompareString(cv, Organization.DEPARTMENT, department) && 1395 cvCompareString(cv, Organization.TITLE, title) && 1396 cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) { 1397 return; 1398 } 1399 builder.withValue(Organization.TYPE, type); 1400 builder.withValue(Organization.COMPANY, company); 1401 builder.withValue(Organization.TITLE, title); 1402 builder.withValue(Organization.DEPARTMENT, department); 1403 builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName); 1404 builder.withValue(Organization.OFFICE_LOCATION, officeLocation); 1405 add(builder.build()); 1406 } 1407 1408 public void addNote(Entity entity, String note) { 1409 RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1); 1410 ContentValues cv = builder.cv; 1411 if (note == null) return; 1412 note = note.replaceAll("\r\n", "\n"); 1413 if (cv != null && cvCompareString(cv, Note.NOTE, note)) { 1414 return; 1415 } 1416 1417 // Reject notes with nothing in them. Often, we get something from Outlook when 1418 // nothing was ever entered. Sigh. 1419 int len = note.length(); 1420 int i = 0; 1421 for (; i < len; i++) { 1422 char c = note.charAt(i); 1423 if (!Character.isWhitespace(c)) { 1424 break; 1425 } 1426 } 1427 if (i == len) return; 1428 1429 builder.withValue(Note.NOTE, note); 1430 add(builder.build()); 1431 } 1432 } 1433 1434 /** 1435 * Generate the uri for the data row associated with this NamedContentValues object 1436 * @param ncv the NamedContentValues object 1437 * @return a uri that can be used to refer to this row 1438 */ 1439 public Uri dataUriFromNamedContentValues(NamedContentValues ncv) { 1440 long id = ncv.values.getAsLong(RawContacts._ID); 1441 Uri dataUri = ContentUris.withAppendedId(ncv.uri, id); 1442 return dataUri; 1443 } 1444 1445 @Override 1446 public void cleanup() { 1447 // Mark the changed contacts dirty = 0 1448 // Permanently delete the user deletions 1449 ContactOperations ops = new ContactOperations(); 1450 for (Long id: mUpdatedIdList) { 1451 ops.add(ContentProviderOperation 1452 .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 1453 .buildUpon() 1454 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1455 .build()) 1456 .withValue(RawContacts.DIRTY, 0).build()); 1457 } 1458 for (Long id: mDeletedIdList) { 1459 ops.add(ContentProviderOperation 1460 .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 1461 .buildUpon() 1462 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1463 .build()) 1464 .build()); 1465 } 1466 ops.execute(); 1467 ContentResolver cr = mContext.getContentResolver(); 1468 if (mGroupsUsed) { 1469 // Make sure the title column is set for all of our groups 1470 // And that all of our groups are visible 1471 // TODO Perhaps the visible part should only happen when the group is created, but 1472 // this is fine for now. 1473 Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI); 1474 Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE}, 1475 Groups.TITLE + " IS NULL", null, null); 1476 ContentValues values = new ContentValues(); 1477 values.put(Groups.GROUP_VISIBLE, 1); 1478 try { 1479 while (c.moveToNext()) { 1480 String sourceId = c.getString(0); 1481 values.put(Groups.TITLE, sourceId); 1482 cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values, 1483 Groups.SOURCE_ID + "=?", new String[] {sourceId}); 1484 } 1485 } finally { 1486 c.close(); 1487 } 1488 } 1489 } 1490 1491 @Override 1492 public String getCollectionName() { 1493 return "Contacts"; 1494 } 1495 1496 private void sendEmail(Serializer s, ContentValues cv, int count, String displayName) 1497 throws IOException { 1498 // Get both parts of the email address (a newly created one in the UI won't have a name) 1499 String addr = cv.getAsString(Email.DATA); 1500 String name = cv.getAsString(Email.DISPLAY_NAME); 1501 if (name == null) { 1502 if (displayName != null) { 1503 name = displayName; 1504 } else { 1505 name = addr; 1506 } 1507 } 1508 // Compose address from name and addr 1509 if (addr != null) { 1510 String value = '\"' + name + "\" <" + addr + '>'; 1511 if (count < MAX_EMAIL_ROWS) { 1512 s.data(EMAIL_TAGS[count], value); 1513 } 1514 } 1515 } 1516 1517 private void sendIm(Serializer s, ContentValues cv, int count) throws IOException { 1518 String value = cv.getAsString(Im.DATA); 1519 if (value == null) return; 1520 if (count < MAX_IM_ROWS) { 1521 s.data(IM_TAGS[count], value); 1522 } 1523 } 1524 1525 private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames) 1526 throws IOException{ 1527 if (cv.containsKey(StructuredPostal.CITY)) { 1528 s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY)); 1529 } 1530 if (cv.containsKey(StructuredPostal.COUNTRY)) { 1531 s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY)); 1532 } 1533 if (cv.containsKey(StructuredPostal.POSTCODE)) { 1534 s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE)); 1535 } 1536 if (cv.containsKey(StructuredPostal.REGION)) { 1537 s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION)); 1538 } 1539 if (cv.containsKey(StructuredPostal.STREET)) { 1540 s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET)); 1541 } 1542 } 1543 1544 private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException { 1545 switch (cv.getAsInteger(StructuredPostal.TYPE)) { 1546 case StructuredPostal.TYPE_HOME: 1547 sendOnePostal(s, cv, HOME_ADDRESS_TAGS); 1548 break; 1549 case StructuredPostal.TYPE_WORK: 1550 sendOnePostal(s, cv, WORK_ADDRESS_TAGS); 1551 break; 1552 case StructuredPostal.TYPE_OTHER: 1553 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS); 1554 break; 1555 default: 1556 break; 1557 } 1558 } 1559 1560 private String sendStructuredName(Serializer s, ContentValues cv) throws IOException { 1561 String displayName = null; 1562 if (cv.containsKey(StructuredName.FAMILY_NAME)) { 1563 s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME)); 1564 } 1565 if (cv.containsKey(StructuredName.GIVEN_NAME)) { 1566 s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME)); 1567 } 1568 if (cv.containsKey(StructuredName.MIDDLE_NAME)) { 1569 s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME)); 1570 } 1571 if (cv.containsKey(StructuredName.SUFFIX)) { 1572 s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX)); 1573 } 1574 if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) { 1575 s.data(Tags.CONTACTS_YOMI_FIRST_NAME, 1576 cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); 1577 } 1578 if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) { 1579 s.data(Tags.CONTACTS_YOMI_LAST_NAME, 1580 cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME)); 1581 } 1582 if (cv.containsKey(StructuredName.PREFIX)) { 1583 s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX)); 1584 } 1585 if (cv.containsKey(StructuredName.DISPLAY_NAME)) { 1586 displayName = cv.getAsString(StructuredName.DISPLAY_NAME); 1587 s.data(Tags.CONTACTS_FILE_AS, displayName); 1588 } 1589 return displayName; 1590 } 1591 1592 private void sendBusiness(Serializer s, ContentValues cv) throws IOException { 1593 if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) { 1594 s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME)); 1595 } 1596 if (cv.containsKey(EasBusiness.CUSTOMER_ID)) { 1597 s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID)); 1598 } 1599 if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) { 1600 s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID)); 1601 } 1602 } 1603 1604 private void sendPersonal(Serializer s, ContentValues cv) throws IOException { 1605 if (cv.containsKey(EasPersonal.ANNIVERSARY)) { 1606 s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY)); 1607 } 1608 if (cv.containsKey(EasPersonal.FILE_AS)) { 1609 s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS)); 1610 } 1611 } 1612 1613 private void sendBirthday(Serializer s, ContentValues cv) throws IOException { 1614 if (cv.containsKey(Birthday.BIRTHDAY)) { 1615 s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(Birthday.BIRTHDAY)); 1616 } 1617 } 1618 1619 private void sendOrganization(Serializer s, ContentValues cv) throws IOException { 1620 if (cv.containsKey(Organization.TITLE)) { 1621 s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE)); 1622 } 1623 if (cv.containsKey(Organization.COMPANY)) { 1624 s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY)); 1625 } 1626 if (cv.containsKey(Organization.DEPARTMENT)) { 1627 s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT)); 1628 } 1629 if (cv.containsKey(Organization.OFFICE_LOCATION)) { 1630 s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(Organization.OFFICE_LOCATION)); 1631 } 1632 } 1633 1634 private void sendNickname(Serializer s, ContentValues cv) throws IOException { 1635 if (cv.containsKey(Nickname.NAME)) { 1636 s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME)); 1637 } 1638 } 1639 1640 private void sendWebpage(Serializer s, ContentValues cv) throws IOException { 1641 if (cv.containsKey(Website.URL)) { 1642 s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL)); 1643 } 1644 } 1645 1646 private void sendNote(Serializer s, ContentValues cv) throws IOException { 1647 if (cv.containsKey(Note.NOTE)) { 1648 // EAS won't accept note data with raw newline characters 1649 String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n"); 1650 // Format of upsync data depends on protocol version 1651 if (mService.mProtocolVersionDouble >= 12.0) { 1652 s.start(Tags.BASE_BODY); 1653 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); 1654 s.end(); 1655 } else { 1656 s.data(Tags.CONTACTS_BODY, note); 1657 } 1658 } 1659 } 1660 1661 private void sendChildren(Serializer s, ContentValues cv) throws IOException { 1662 boolean first = true; 1663 for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { 1664 String row = EasChildren.ROWS[i]; 1665 if (cv.containsKey(row)) { 1666 if (first) { 1667 s.start(Tags.CONTACTS_CHILDREN); 1668 first = false; 1669 } 1670 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row)); 1671 } 1672 } 1673 if (!first) { 1674 s.end(); 1675 } 1676 } 1677 1678 private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount) 1679 throws IOException { 1680 String value = cv.getAsString(Phone.NUMBER); 1681 if (value == null) return; 1682 switch (cv.getAsInteger(Phone.TYPE)) { 1683 case Phone.TYPE_WORK: 1684 if (workCount < MAX_PHONE_ROWS) { 1685 s.data(WORK_PHONE_TAGS[workCount], value); 1686 } 1687 break; 1688 case Phone.TYPE_MMS: 1689 s.data(Tags.CONTACTS2_MMS, value); 1690 break; 1691 case Phone.TYPE_ASSISTANT: 1692 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value); 1693 break; 1694 case Phone.TYPE_FAX_WORK: 1695 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value); 1696 break; 1697 case Phone.TYPE_COMPANY_MAIN: 1698 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value); 1699 break; 1700 case Phone.TYPE_HOME: 1701 if (homeCount < MAX_PHONE_ROWS) { 1702 s.data(HOME_PHONE_TAGS[homeCount], value); 1703 } 1704 break; 1705 case Phone.TYPE_MOBILE: 1706 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value); 1707 break; 1708 case Phone.TYPE_CAR: 1709 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value); 1710 break; 1711 case Phone.TYPE_PAGER: 1712 s.data(Tags.CONTACTS_PAGER_NUMBER, value); 1713 break; 1714 case Phone.TYPE_RADIO: 1715 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value); 1716 break; 1717 case Phone.TYPE_FAX_HOME: 1718 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value); 1719 break; 1720 default: 1721 break; 1722 } 1723 } 1724 1725 private void sendRelation(Serializer s, ContentValues cv) throws IOException { 1726 String value = cv.getAsString(Relation.DATA); 1727 if (value == null) return; 1728 switch (cv.getAsInteger(Relation.TYPE)) { 1729 case Relation.TYPE_ASSISTANT: 1730 s.data(Tags.CONTACTS_ASSISTANT_NAME, value); 1731 break; 1732 case Relation.TYPE_MANAGER: 1733 s.data(Tags.CONTACTS2_MANAGER_NAME, value); 1734 break; 1735 case Relation.TYPE_SPOUSE: 1736 s.data(Tags.CONTACTS_SPOUSE, value); 1737 break; 1738 default: 1739 break; 1740 } 1741 } 1742 1743 @Override 1744 public boolean sendLocalChanges(Serializer s) throws IOException { 1745 // First, let's find Contacts that have changed. 1746 ContentResolver cr = mService.mContentResolver; 1747 Uri uri = RawContacts.CONTENT_URI.buildUpon() 1748 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) 1749 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE) 1750 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1751 .build(); 1752 1753 if (getSyncKey().equals("0")) { 1754 return false; 1755 } 1756 1757 try { 1758 // Get them all atomically 1759 EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null); 1760 ContentValues cidValues = new ContentValues(); 1761 try { 1762 boolean first = true; 1763 final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI); 1764 while (ei.hasNext()) { 1765 Entity entity = ei.next(); 1766 // For each of these entities, create the change commands 1767 ContentValues entityValues = entity.getEntityValues(); 1768 String serverId = entityValues.getAsString(RawContacts.SOURCE_ID); 1769 ArrayList<Integer> groupIds = new ArrayList<Integer>(); 1770 if (first) { 1771 s.start(Tags.SYNC_COMMANDS); 1772 userLog("Sending Contacts changes to the server"); 1773 first = false; 1774 } 1775 if (serverId == null) { 1776 // This is a new contact; create a clientId 1777 String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis(); 1778 userLog("Creating new contact with clientId: ", clientId); 1779 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 1780 // And save it in the raw contact 1781 cidValues.put(ContactsContract.RawContacts.SYNC1, clientId); 1782 cr.update(ContentUris. 1783 withAppendedId(rawContactUri, 1784 entityValues.getAsLong(ContactsContract.RawContacts._ID)), 1785 cidValues, null, null); 1786 } else { 1787 if (entityValues.getAsInteger(RawContacts.DELETED) == 1) { 1788 userLog("Deleting contact with serverId: ", serverId); 1789 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 1790 mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID)); 1791 continue; 1792 } 1793 userLog("Upsync change to contact with serverId: " + serverId); 1794 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 1795 } 1796 s.start(Tags.SYNC_APPLICATION_DATA); 1797 // Write out the data here 1798 int imCount = 0; 1799 int emailCount = 0; 1800 int homePhoneCount = 0; 1801 int workPhoneCount = 0; 1802 String displayName = null; 1803 ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>(); 1804 for (NamedContentValues ncv: entity.getSubValues()) { 1805 ContentValues cv = ncv.values; 1806 String mimeType = cv.getAsString(Data.MIMETYPE); 1807 if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 1808 emailValues.add(cv); 1809 } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) { 1810 sendNickname(s, cv); 1811 } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) { 1812 sendChildren(s, cv); 1813 } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) { 1814 sendBusiness(s, cv); 1815 } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) { 1816 sendWebpage(s, cv); 1817 } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) { 1818 sendPersonal(s, cv); 1819 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 1820 sendPhone(s, cv, workPhoneCount, homePhoneCount); 1821 int type = cv.getAsInteger(Phone.TYPE); 1822 if (type == Phone.TYPE_HOME) homePhoneCount++; 1823 if (type == Phone.TYPE_WORK) workPhoneCount++; 1824 } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) { 1825 sendRelation(s, cv); 1826 } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 1827 displayName = sendStructuredName(s, cv); 1828 } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) { 1829 sendStructuredPostal(s, cv); 1830 } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) { 1831 sendOrganization(s, cv); 1832 } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { 1833 sendIm(s, cv, imCount++); 1834 } else if (mimeType.equals(Birthday.CONTENT_ITEM_TYPE)) { 1835 sendBirthday(s, cv); 1836 } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { 1837 // We must gather these, and send them together (below) 1838 groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); 1839 } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { 1840 sendNote(s, cv); 1841 } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 1842 // For now, the user can change the photo, but the change won't be 1843 // uploaded. 1844 } else { 1845 userLog("Contacts upsync, unknown data: ", mimeType); 1846 } 1847 } 1848 1849 // We do the email rows last, because we need to make sure we've found the 1850 // displayName (if one exists); this would be in a StructuredName rnow 1851 for (ContentValues cv: emailValues) { 1852 sendEmail(s, cv, emailCount++, displayName); 1853 } 1854 1855 // Now, we'll send up groups, if any 1856 if (!groupIds.isEmpty()) { 1857 boolean groupFirst = true; 1858 for (int id: groupIds) { 1859 // Since we get id's from the provider, we need to find their names 1860 Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id), 1861 GROUP_PROJECTION, null, null, null); 1862 try { 1863 // Presumably, this should always succeed, but ... 1864 if (c.moveToFirst()) { 1865 if (groupFirst) { 1866 s.start(Tags.CONTACTS_CATEGORIES); 1867 groupFirst = false; 1868 } 1869 s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); 1870 } 1871 } finally { 1872 c.close(); 1873 } 1874 } 1875 if (!groupFirst) { 1876 s.end(); 1877 } 1878 } 1879 s.end().end(); // ApplicationData & Change 1880 mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID)); 1881 } 1882 if (!first) { 1883 s.end(); // Commands 1884 } 1885 } finally { 1886 ei.close(); 1887 } 1888 } catch (RemoteException e) { 1889 Log.e(TAG, "Could not read dirty contacts."); 1890 } 1891 1892 return false; 1893 } 1894} 1895