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