ContactsSyncAdapter.java revision 6d9e76c4e8c70785f58a4f2bf30fe0c5e9f45efd
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 = uriWithAccount(RawContacts.CONTENT_URI); 305 } 306 307 @Override 308 public void wipe() { 309 mContentResolver.delete(mAccountUri.buildUpon() 310 .appendQueryParameter(ContactsContract.RawContacts.DELETE_PERMANENTLY, "true") 311 .build(), null, null); 312 } 313 314 public void addData(String serverId, ContactOperations ops, Entity entity) 315 throws IOException { 316 String prefix = null; 317 String firstName = null; 318 String lastName = null; 319 String middleName = null; 320 String suffix = null; 321 String companyName = null; 322 String yomiFirstName = null; 323 String yomiLastName = null; 324 String yomiCompanyName = null; 325 String title = null; 326 String department = null; 327 Address home = new Address(); 328 Address work = new Address(); 329 Address other = new Address(); 330 EasBusiness business = new EasBusiness(); 331 EasPersonal personal = new EasPersonal(); 332 ArrayList<String> children = new ArrayList<String>(); 333 ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>(); 334 ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>(); 335 if (entity == null) { 336 ops.newContact(serverId); 337 } 338 339 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 340 switch (tag) { 341 case Tags.CONTACTS_FIRST_NAME: 342 firstName = getValue(); 343 break; 344 case Tags.CONTACTS_LAST_NAME: 345 lastName = getValue(); 346 break; 347 case Tags.CONTACTS_MIDDLE_NAME: 348 middleName = getValue(); 349 break; 350 case Tags.CONTACTS_SUFFIX: 351 suffix = getValue(); 352 break; 353 case Tags.CONTACTS_COMPANY_NAME: 354 companyName = getValue(); 355 break; 356 case Tags.CONTACTS_JOB_TITLE: 357 title = getValue(); 358 break; 359 case Tags.CONTACTS_EMAIL1_ADDRESS: 360 case Tags.CONTACTS_EMAIL2_ADDRESS: 361 case Tags.CONTACTS_EMAIL3_ADDRESS: 362 emails.add(new EmailRow(getValue())); 363 break; 364 case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER: 365 ops.addPhone(entity, TYPE_WORK2, getValue()); 366 break; 367 case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER: 368 ops.addPhone(entity, Phone.TYPE_WORK, getValue()); 369 break; 370 case Tags.CONTACTS2_MMS: 371 ops.addPhone(entity, TYPE_MMS, getValue()); 372 break; 373 case Tags.CONTACTS_BUSINESS_FAX_NUMBER: 374 ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue()); 375 break; 376 case Tags.CONTACTS2_COMPANY_MAIN_PHONE: 377 ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue()); 378 break; 379 case Tags.CONTACTS_HOME_FAX_NUMBER: 380 ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue()); 381 break; 382 case Tags.CONTACTS_HOME_TELEPHONE_NUMBER: 383 ops.addPhone(entity, Phone.TYPE_HOME, getValue()); 384 break; 385 case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER: 386 ops.addPhone(entity, TYPE_HOME2, getValue()); 387 break; 388 case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER: 389 ops.addPhone(entity, Phone.TYPE_MOBILE, getValue()); 390 break; 391 case Tags.CONTACTS_CAR_TELEPHONE_NUMBER: 392 ops.addPhone(entity, Phone.TYPE_CAR, getValue()); 393 break; 394 case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER: 395 ops.addPhone(entity, Phone.TYPE_RADIO, getValue()); 396 break; 397 case Tags.CONTACTS_PAGER_NUMBER: 398 ops.addPhone(entity, Phone.TYPE_PAGER, getValue()); 399 break; 400 case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER: 401 ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue()); 402 break; 403 case Tags.CONTACTS2_IM_ADDRESS: 404 case Tags.CONTACTS2_IM_ADDRESS_2: 405 case Tags.CONTACTS2_IM_ADDRESS_3: 406 ims.add(new ImRow(getValue())); 407 break; 408 case Tags.CONTACTS_BUSINESS_ADDRESS_CITY: 409 work.city = getValue(); 410 break; 411 case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY: 412 work.country = getValue(); 413 break; 414 case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE: 415 work.code = getValue(); 416 break; 417 case Tags.CONTACTS_BUSINESS_ADDRESS_STATE: 418 work.state = getValue(); 419 break; 420 case Tags.CONTACTS_BUSINESS_ADDRESS_STREET: 421 work.street = getValue(); 422 break; 423 case Tags.CONTACTS_HOME_ADDRESS_CITY: 424 home.city = getValue(); 425 break; 426 case Tags.CONTACTS_HOME_ADDRESS_COUNTRY: 427 home.country = getValue(); 428 break; 429 case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE: 430 home.code = getValue(); 431 break; 432 case Tags.CONTACTS_HOME_ADDRESS_STATE: 433 home.state = getValue(); 434 break; 435 case Tags.CONTACTS_HOME_ADDRESS_STREET: 436 home.street = getValue(); 437 break; 438 case Tags.CONTACTS_OTHER_ADDRESS_CITY: 439 other.city = getValue(); 440 break; 441 case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY: 442 other.country = getValue(); 443 break; 444 case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE: 445 other.code = getValue(); 446 break; 447 case Tags.CONTACTS_OTHER_ADDRESS_STATE: 448 other.state = getValue(); 449 break; 450 case Tags.CONTACTS_OTHER_ADDRESS_STREET: 451 other.street = getValue(); 452 break; 453 454 case Tags.CONTACTS_CHILDREN: 455 childrenParser(children); 456 break; 457 458 case Tags.CONTACTS_YOMI_COMPANY_NAME: 459 yomiCompanyName = getValue(); 460 break; 461 case Tags.CONTACTS_YOMI_FIRST_NAME: 462 yomiFirstName = getValue(); 463 break; 464 case Tags.CONTACTS_YOMI_LAST_NAME: 465 yomiLastName = getValue(); 466 break; 467 468 case Tags.CONTACTS2_NICKNAME: 469 ops.addNickname(entity, getValue()); 470 break; 471 472 case Tags.CONTACTS_ASSISTANT_NAME: 473 ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue()); 474 break; 475 case Tags.CONTACTS2_MANAGER_NAME: 476 ops.addRelation(entity, Relation.TYPE_MANAGER, getValue()); 477 break; 478 case Tags.CONTACTS_SPOUSE: 479 ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue()); 480 break; 481 case Tags.CONTACTS_DEPARTMENT: 482 department = getValue(); 483 break; 484 case Tags.CONTACTS_TITLE: 485 prefix = getValue(); 486 break; 487 488 // EAS Business 489 case Tags.CONTACTS_OFFICE_LOCATION: 490 business.officeLocation = getValue(); 491 break; 492 case Tags.CONTACTS2_CUSTOMER_ID: 493 business.customerId = getValue(); 494 break; 495 case Tags.CONTACTS2_GOVERNMENT_ID: 496 business.governmentId = getValue(); 497 break; 498 case Tags.CONTACTS2_ACCOUNT_NAME: 499 business.accountName = getValue(); 500 break; 501 502 // EAS Personal 503 case Tags.CONTACTS_ANNIVERSARY: 504 personal.anniversary = getValue(); 505 break; 506 case Tags.CONTACTS_BIRTHDAY: 507 personal.birthday = getValue(); 508 break; 509 case Tags.CONTACTS_FILE_AS: 510 personal.fileAs = getValue(); 511 break; 512 case Tags.CONTACTS_WEBPAGE: 513 ops.addWebpage(entity, getValue()); 514 break; 515 516 case Tags.CONTACTS_PICTURE: 517 ops.addPhoto(entity, getValue()); 518 break; 519 520 case Tags.BASE_BODY: 521 ops.addNote(entity, bodyParser()); 522 break; 523 case Tags.CONTACTS_BODY: 524 ops.addNote(entity, getValue()); 525 break; 526 527 case Tags.CONTACTS_CATEGORIES: 528 mGroupsUsed = true; 529 categoriesParser(ops, entity); 530 break; 531 532 case Tags.CONTACTS_COMPRESSED_RTF: 533 // We don't use this, and it isn't necessary to upload, so we'll ignore it 534 skipTag(); 535 break; 536 537 default: 538 skipTag(); 539 } 540 } 541 542 // We must have first name, last name, or company name 543 String name = null; 544 if (firstName != null || lastName != null) { 545 if (firstName == null) { 546 name = lastName; 547 } else if (lastName == null) { 548 name = firstName; 549 } else { 550 name = firstName + ' ' + lastName; 551 } 552 } else if (companyName != null) { 553 name = companyName; 554 } 555 556 ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name, 557 yomiFirstName, yomiLastName); 558 ops.addBusiness(entity, business); 559 ops.addPersonal(entity, personal); 560 561 ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, MAX_EMAIL_ROWS); 562 ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, MAX_IM_ROWS); 563 564 if (!children.isEmpty()) { 565 ops.addChildren(entity, children); 566 } 567 568 if (work.hasData()) { 569 ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city, 570 work.state, work.country, work.code); 571 } 572 if (home.hasData()) { 573 ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city, 574 home.state, home.country, home.code); 575 } 576 if (other.hasData()) { 577 ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city, 578 other.state, other.country, other.code); 579 } 580 581 if (companyName != null) { 582 ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department, 583 yomiCompanyName); 584 } 585 586 if (entity != null) { 587 // We've been removing rows from the list as they've been found in the xml 588 // Any that are left must have been deleted on the server 589 ArrayList<NamedContentValues> ncvList = entity.getSubValues(); 590 for (NamedContentValues ncv: ncvList) { 591 // These rows need to be deleted... 592 Uri u = dataUriFromNamedContentValues(ncv); 593 ops.add(ContentProviderOperation.newDelete(u).build()); 594 } 595 } 596 } 597 598 private void categoriesParser(ContactOperations ops, Entity entity) throws IOException { 599 while (nextTag(Tags.CONTACTS_CATEGORIES) != END) { 600 switch (tag) { 601 case Tags.CONTACTS_CATEGORY: 602 ops.addGroup(entity, getValue()); 603 break; 604 default: 605 skipTag(); 606 } 607 } 608 } 609 610 private void childrenParser(ArrayList<String> children) throws IOException { 611 while (nextTag(Tags.CONTACTS_CHILDREN) != END) { 612 switch (tag) { 613 case Tags.CONTACTS_CHILD: 614 if (children.size() < EasChildren.MAX_CHILDREN) { 615 children.add(getValue()); 616 } 617 break; 618 default: 619 skipTag(); 620 } 621 } 622 } 623 624 private String bodyParser() throws IOException { 625 String body = null; 626 while (nextTag(Tags.BASE_BODY) != END) { 627 switch (tag) { 628 case Tags.BASE_DATA: 629 body = getValue(); 630 break; 631 default: 632 skipTag(); 633 } 634 } 635 return body; 636 } 637 638 public void addParser(ContactOperations ops) throws IOException { 639 String serverId = null; 640 while (nextTag(Tags.SYNC_ADD) != END) { 641 switch (tag) { 642 case Tags.SYNC_SERVER_ID: // same as 643 serverId = getValue(); 644 break; 645 case Tags.SYNC_APPLICATION_DATA: 646 addData(serverId, ops, null); 647 break; 648 default: 649 skipTag(); 650 } 651 } 652 } 653 654 private Cursor getServerIdCursor(String serverId) { 655 mBindArgument[0] = serverId; 656 return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION, 657 mBindArgument, null); 658 } 659 660 private Cursor getClientIdCursor(String clientId) { 661 mBindArgument[0] = clientId; 662 return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION, 663 mBindArgument, null); 664 } 665 666 public void deleteParser(ContactOperations ops) throws IOException { 667 while (nextTag(Tags.SYNC_DELETE) != END) { 668 switch (tag) { 669 case Tags.SYNC_SERVER_ID: 670 String serverId = getValue(); 671 // Find the message in this mailbox with the given serverId 672 Cursor c = getServerIdCursor(serverId); 673 try { 674 if (c.moveToFirst()) { 675 userLog("Deleting ", serverId); 676 ops.delete(c.getLong(0)); 677 } 678 } finally { 679 c.close(); 680 } 681 break; 682 default: 683 skipTag(); 684 } 685 } 686 } 687 688 class ServerChange { 689 long id; 690 boolean read; 691 692 ServerChange(long _id, boolean _read) { 693 id = _id; 694 read = _read; 695 } 696 } 697 698 /** 699 * Changes are handled row by row, and only changed/new rows are acted upon 700 * @param ops the array of pending ContactProviderOperations. 701 * @throws IOException 702 */ 703 public void changeParser(ContactOperations ops) throws IOException { 704 String serverId = null; 705 Entity entity = null; 706 while (nextTag(Tags.SYNC_CHANGE) != END) { 707 switch (tag) { 708 case Tags.SYNC_SERVER_ID: 709 serverId = getValue(); 710 Cursor c = getServerIdCursor(serverId); 711 try { 712 if (c.moveToFirst()) { 713 // TODO Handle deleted individual rows... 714 try { 715 EntityIterator entityIterator = 716 mContentResolver.queryEntities(ContentUris 717 .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)), 718 null, null, null); 719 if (entityIterator.hasNext()) { 720 entity = entityIterator.next(); 721 } 722 userLog("Changing contact ", serverId); 723 } catch (RemoteException e) { 724 } 725 } 726 } finally { 727 c.close(); 728 } 729 break; 730 case Tags.SYNC_APPLICATION_DATA: 731 addData(serverId, ops, entity); 732 break; 733 default: 734 skipTag(); 735 } 736 } 737 } 738 739 @Override 740 public void commandsParser() throws IOException { 741 while (nextTag(Tags.SYNC_COMMANDS) != END) { 742 if (tag == Tags.SYNC_ADD) { 743 addParser(ops); 744 incrementChangeCount(); 745 } else if (tag == Tags.SYNC_DELETE) { 746 deleteParser(ops); 747 incrementChangeCount(); 748 } else if (tag == Tags.SYNC_CHANGE) { 749 changeParser(ops); 750 incrementChangeCount(); 751 } else 752 skipTag(); 753 } 754 } 755 756 @Override 757 public void commit() throws IOException { 758 // Save the syncKey here, using the Helper provider by Contacts provider 759 userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey); 760 ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI, 761 getAccountManagerAccount(), mMailbox.mSyncKey.getBytes())); 762 763 // Execute these all at once... 764 ops.execute(); 765 766 if (ops.mResults != null) { 767 ContentValues cv = new ContentValues(); 768 cv.put(RawContacts.DIRTY, 0); 769 for (int i = 0; i < ops.mContactIndexCount; i++) { 770 int index = ops.mContactIndexArray[i]; 771 Uri u = ops.mResults[index].uri; 772 if (u != null) { 773 String idString = u.getLastPathSegment(); 774 mContentResolver.update(RawContacts.CONTENT_URI, cv, 775 RawContacts._ID + "=" + idString, null); 776 } 777 } 778 } 779 } 780 781 public void addResponsesParser() throws IOException { 782 String serverId = null; 783 String clientId = null; 784 ContentValues cv = new ContentValues(); 785 while (nextTag(Tags.SYNC_ADD) != END) { 786 switch (tag) { 787 case Tags.SYNC_SERVER_ID: 788 serverId = getValue(); 789 break; 790 case Tags.SYNC_CLIENT_ID: 791 clientId = getValue(); 792 break; 793 case Tags.SYNC_STATUS: 794 getValue(); 795 break; 796 default: 797 skipTag(); 798 } 799 } 800 801 // This is theoretically impossible, but... 802 if (clientId == null || serverId == null) return; 803 804 Cursor c = getClientIdCursor(clientId); 805 try { 806 if (c.moveToFirst()) { 807 cv.put(RawContacts.SOURCE_ID, serverId); 808 cv.put(RawContacts.DIRTY, 0); 809 ops.add(ContentProviderOperation.newUpdate(ContentUris 810 .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0))) 811 .withValues(cv) 812 .build()); 813 userLog("New contact " + clientId + " was given serverId: " + serverId); 814 } 815 } finally { 816 c.close(); 817 } 818 } 819 @Override 820 public void responsesParser() throws IOException { 821 // Handle server responses here (for Add and Change) 822 while (nextTag(Tags.SYNC_RESPONSES) != END) { 823 if (tag == Tags.SYNC_ADD) { 824 addResponsesParser(); 825 } else if (tag == Tags.SYNC_CHANGE) { 826 //changeResponsesParser(); 827 } else 828 skipTag(); 829 } 830 } 831 } 832 833 834 private Uri uriWithAccount(Uri uri) { 835 return uri.buildUpon() 836 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) 837 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE) 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(uriWithAccount(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.RawContacts.DELETE_PERMANENTLY, 914 "true") 915 .build()) 916 .build()); 917 } 918 919 public void execute() { 920 synchronized (mService.getSynchronizer()) { 921 if (!mService.isStopped()) { 922 try { 923 mService.userLog("Executing ", size(), " CPO's"); 924 mResults = mContext.getContentResolver().applyBatch( 925 ContactsContract.AUTHORITY, this); 926 } catch (RemoteException e) { 927 // There is nothing sensible to be done here 928 Log.e(TAG, "problem inserting contact during server update", e); 929 } catch (OperationApplicationException e) { 930 // There is nothing sensible to be done here 931 Log.e(TAG, "problem inserting contact during server update", e); 932 } 933 } 934 } 935 } 936 937 /** 938 * Given the list of NamedContentValues for an entity, a mime type, and a subtype, 939 * tries to find a match, returning it 940 * @param list the list of NCV's from the contact entity 941 * @param contentItemType the mime type we're looking for 942 * @param type the subtype (e.g. HOME, WORK, etc.) 943 * @return the matching NCV or null if not found 944 */ 945 private NamedContentValues findTypedData(ArrayList<NamedContentValues> list, 946 String contentItemType, int type, String stringType) { 947 NamedContentValues result = null; 948 949 // Loop through the ncv's, looking for an existing row 950 for (NamedContentValues namedContentValues: list) { 951 Uri uri = namedContentValues.uri; 952 ContentValues cv = namedContentValues.values; 953 if (Data.CONTENT_URI.equals(uri)) { 954 String mimeType = cv.getAsString(Data.MIMETYPE); 955 if (mimeType.equals(contentItemType)) { 956 if (stringType != null) { 957 if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) { 958 result = namedContentValues; 959 } 960 // Note Email.TYPE could be ANY type column; they are all defined in 961 // the private CommonColumns class in ContactsContract 962 } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) { 963 result = namedContentValues; 964 } 965 } 966 } 967 } 968 969 // If we've found an existing data row, we'll delete it. Any rows left at the 970 // end should be deleted... 971 if (result != null) { 972 list.remove(result); 973 } 974 975 // Return the row found (or null) 976 return result; 977 } 978 979 /** 980 * Given the list of NamedContentValues for an entity and a mime type 981 * gather all of the matching NCV's, returning them 982 * @param list the list of NCV's from the contact entity 983 * @param contentItemType the mime type we're looking for 984 * @param type the subtype (e.g. HOME, WORK, etc.) 985 * @return the matching NCVs 986 */ 987 private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list, 988 String contentItemType) { 989 ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>(); 990 991 // Loop through the ncv's, looking for an existing row 992 for (NamedContentValues namedContentValues: list) { 993 Uri uri = namedContentValues.uri; 994 ContentValues cv = namedContentValues.values; 995 if (Data.CONTENT_URI.equals(uri)) { 996 String mimeType = cv.getAsString(Data.MIMETYPE); 997 if (mimeType.equals(contentItemType)) { 998 result.add(namedContentValues); 999 } 1000 } 1001 } 1002 1003 // If we've found an existing data row, we'll delete it. Any rows left at the 1004 // end should be deleted... 1005 if (result != null) { 1006 list.remove(result); 1007 } 1008 1009 // Return the row found (or null) 1010 return result; 1011 } 1012 1013 /** 1014 * Create a wrapper for a builder (insert or update) that also includes the NCV for 1015 * an existing row of this type. If the SmartBuilder's cv field is not null, then 1016 * it represents the current (old) values of this field. The caller can then check 1017 * whether the field is now different and needs to be updated; if it's not different, 1018 * the caller will simply return and not generate a new CPO. Otherwise, the builder 1019 * should have its content values set, and the built CPO should be added to the 1020 * ContactOperations list. 1021 * 1022 * @param entity the contact entity (or null if this is a new contact) 1023 * @param mimeType the mime type of this row 1024 * @param type the subtype of this row 1025 * @param stringType for groups, the name of the group (type will be ignored), or null 1026 * @return the created SmartBuilder 1027 */ 1028 public RowBuilder createBuilder(Entity entity, String mimeType, int type, 1029 String stringType) { 1030 RowBuilder builder = null; 1031 1032 if (entity != null) { 1033 NamedContentValues ncv = 1034 findTypedData(entity.getSubValues(), mimeType, type, stringType); 1035 if (ncv != null) { 1036 builder = new RowBuilder( 1037 ContentProviderOperation 1038 .newUpdate(dataUriFromNamedContentValues(ncv)), 1039 ncv); 1040 } 1041 } 1042 1043 if (builder == null) { 1044 builder = newRowBuilder(entity, mimeType); 1045 } 1046 1047 // Return the appropriate builder (insert or update) 1048 // Caller will fill in the appropriate values; 4 MIMETYPE is already set 1049 return builder; 1050 } 1051 1052 private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { 1053 return createBuilder(entity, mimeType, type, null); 1054 } 1055 1056 private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { 1057 return createBuilder(entity, mimeType, -1, null); 1058 } 1059 1060 private RowBuilder newRowBuilder(Entity entity, String mimeType) { 1061 // This is a new row; first get the contactId 1062 // If the Contact is new, use the saved back value; otherwise the value in the entity 1063 int contactId = mContactBackValue; 1064 if (entity != null) { 1065 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID); 1066 } 1067 1068 // Create an insert operation with the proper contactId reference 1069 RowBuilder builder = 1070 new RowBuilder(ContentProviderOperation.newInsert(Data.CONTENT_URI)); 1071 if (entity == null) { 1072 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId); 1073 } else { 1074 builder.withValue(Data.RAW_CONTACT_ID, contactId); 1075 } 1076 1077 // Set the mime type of the row 1078 builder.withValue(Data.MIMETYPE, mimeType); 1079 return builder; 1080 } 1081 1082 /** 1083 * Compare a column in a ContentValues with an (old) value, and see if they are the 1084 * same. For this purpose, null and an empty string are considered the same. 1085 * @param cv a ContentValues object, from a NamedContentValues 1086 * @param column a column that might be in the ContentValues 1087 * @param oldValue an old value (or null) to check against 1088 * @return whether the column's value in the ContentValues matches oldValue 1089 */ 1090 private boolean cvCompareString(ContentValues cv, String column, String oldValue) { 1091 if (cv.containsKey(column)) { 1092 if (oldValue != null && cv.getAsString(column).equals(oldValue)) { 1093 return true; 1094 } 1095 } else if (oldValue == null || oldValue.length() == 0) { 1096 return true; 1097 } 1098 return false; 1099 } 1100 1101 public void addChildren(Entity entity, ArrayList<String> children) { 1102 RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE); 1103 int i = 0; 1104 for (String child: children) { 1105 builder.withValue(EasChildren.ROWS[i++], child); 1106 } 1107 add(builder.build()); 1108 } 1109 1110 public void addGroup(Entity entity, String group) { 1111 RowBuilder builder = 1112 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group); 1113 builder.withValue(GroupMembership.GROUP_SOURCE_ID, group); 1114 add(builder.build()); 1115 } 1116 1117 public void addName(Entity entity, String prefix, String givenName, String familyName, 1118 String middleName, String suffix, String displayName, String yomiFirstName, 1119 String yomiLastName) { 1120 RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE); 1121 ContentValues cv = builder.cv; 1122 if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) && 1123 cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) && 1124 cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) && 1125 cvCompareString(cv, StructuredName.PREFIX, prefix) && 1126 cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) && 1127 cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) && 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 add(builder.build()); 1139 } 1140 1141 public void addPersonal(Entity entity, EasPersonal personal) { 1142 RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE); 1143 ContentValues cv = builder.cv; 1144 if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) && 1145 cvCompareString(cv, EasPersonal.BIRTHDAY, personal.birthday) && 1146 cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) { 1147 return; 1148 } 1149 if (!personal.hasData()) { 1150 return; 1151 } 1152 builder.withValue(EasPersonal.BIRTHDAY, personal.birthday); 1153 builder.withValue(EasPersonal.FILE_AS, personal.fileAs); 1154 builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary); 1155 add(builder.build()); 1156 } 1157 1158 public void addBusiness(Entity entity, EasBusiness business) { 1159 RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE); 1160 ContentValues cv = builder.cv; 1161 if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) && 1162 cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) && 1163 cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId) && 1164 cvCompareString(cv, EasBusiness.OFFICE_LOCATION, business.officeLocation)) { 1165 return; 1166 } 1167 if (!business.hasData()) { 1168 return; 1169 } 1170 builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName); 1171 builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId); 1172 builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId); 1173 builder.withValue(EasBusiness.OFFICE_LOCATION, business.officeLocation); 1174 add(builder.build()); 1175 } 1176 1177 public void addPhoto(Entity entity, String photo) { 1178 RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE); 1179 // We're always going to add this; it's not worth trying to figure out whether the 1180 // picture is the same as the one stored. 1181 byte[] pic = Base64.decodeBase64(photo.getBytes()); 1182 builder.withValue(Photo.PHOTO, pic); 1183 add(builder.build()); 1184 } 1185 1186 public void addPhone(Entity entity, int type, String phone) { 1187 RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type); 1188 ContentValues cv = builder.cv; 1189 if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) { 1190 return; 1191 } 1192 builder.withValue(Phone.TYPE, type); 1193 builder.withValue(Phone.NUMBER, phone); 1194 add(builder.build()); 1195 } 1196 1197 public void addWebpage(Entity entity, String url) { 1198 RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE); 1199 ContentValues cv = builder.cv; 1200 if (cv != null && cvCompareString(cv, Website.URL, url)) { 1201 return; 1202 } 1203 builder.withValue(Website.TYPE, Website.TYPE_WORK); 1204 builder.withValue(Website.URL, url); 1205 add(builder.build()); 1206 } 1207 1208 public void addRelation(Entity entity, int type, String value) { 1209 RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type); 1210 ContentValues cv = builder.cv; 1211 if (cv != null && cvCompareString(cv, Relation.DATA, value)) { 1212 return; 1213 } 1214 builder.withValue(Relation.TYPE, type); 1215 builder.withValue(Relation.DATA, value); 1216 add(builder.build()); 1217 } 1218 1219 public void addNickname(Entity entity, String name) { 1220 RowBuilder builder = 1221 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT); 1222 ContentValues cv = builder.cv; 1223 if (cv != null && cvCompareString(cv, Nickname.NAME, name)) { 1224 return; 1225 } 1226 builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); 1227 builder.withValue(Nickname.NAME, name); 1228 add(builder.build()); 1229 } 1230 1231 public void addPostal(Entity entity, int type, String street, String city, String state, 1232 String country, String code) { 1233 RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE, 1234 type); 1235 ContentValues cv = builder.cv; 1236 if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) && 1237 cvCompareString(cv, StructuredPostal.STREET, street) && 1238 cvCompareString(cv, StructuredPostal.COUNTRY, country) && 1239 cvCompareString(cv, StructuredPostal.POSTCODE, code) && 1240 cvCompareString(cv, StructuredPostal.REGION, state)) { 1241 return; 1242 } 1243 builder.withValue(StructuredPostal.TYPE, type); 1244 builder.withValue(StructuredPostal.CITY, city); 1245 builder.withValue(StructuredPostal.STREET, street); 1246 builder.withValue(StructuredPostal.COUNTRY, country); 1247 builder.withValue(StructuredPostal.POSTCODE, code); 1248 builder.withValue(StructuredPostal.REGION, state); 1249 add(builder.build()); 1250 } 1251 1252 /** 1253 * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to 1254 * match them with existing rows; if there's a match, everything's great. Otherwise, we 1255 * either need to add a new row for the data, or we have to replace an existing one 1256 * that no longer matches. This is similar to the way Emails are handled. 1257 */ 1258 public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, 1259 int maxRows) { 1260 // Make a list of all same type rows in the existing entity 1261 ArrayList<NamedContentValues> oldAccounts = new ArrayList<NamedContentValues>(); 1262 if (entity != null) { 1263 oldAccounts = findUntypedData(entity.getSubValues(), mimeType); 1264 } 1265 1266 // These will be rows needing replacement with new values 1267 ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>(); 1268 1269 // The count of existing rows 1270 int numRows = oldAccounts.size(); 1271 for (UntypedRow row: rows) { 1272 boolean found = false; 1273 // If we already have this IM address, mark it 1274 for (NamedContentValues ncv: oldAccounts) { 1275 ContentValues cv = ncv.values; 1276 String data = cv.getAsString(COMMON_DATA_ROW); 1277 if (row.isSameAs(data)) { 1278 cv.put(FOUND_DATA_ROW, true); 1279 found = true; 1280 break; 1281 } 1282 } 1283 if (!found) { 1284 // If we don't, there are two possibilities 1285 if (numRows < maxRows) { 1286 // If there are available rows, add a new one 1287 RowBuilder builder = newRowBuilder(entity, mimeType); 1288 row.addValues(builder); 1289 add(builder.build()); 1290 numRows++; 1291 } else { 1292 // Otherwise, say we need to replace a row with this 1293 rowsToReplace.add(row); 1294 } 1295 } 1296 } 1297 1298 // Go through rows needing replacement 1299 for (UntypedRow row: rowsToReplace) { 1300 for (NamedContentValues ncv: oldAccounts) { 1301 ContentValues cv = ncv.values; 1302 // Find a row that hasn't been used (i.e. doesn't match current rows) 1303 if (!cv.containsKey(FOUND_DATA_ROW)) { 1304 // And update it 1305 RowBuilder builder = new RowBuilder( 1306 ContentProviderOperation 1307 .newUpdate(dataUriFromNamedContentValues(ncv)), 1308 ncv); 1309 row.addValues(builder); 1310 add(builder.build()); 1311 } 1312 } 1313 } 1314 } 1315 1316 public void addOrganization(Entity entity, int type, String company, String title, 1317 String department, String yomiCompanyName) { 1318 RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type); 1319 ContentValues cv = builder.cv; 1320 if (cv != null && cvCompareString(cv, Organization.COMPANY, company) && 1321 cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) && 1322 cvCompareString(cv, Organization.DEPARTMENT, department) && 1323 cvCompareString(cv, Organization.TITLE, title)) { 1324 return; 1325 } 1326 builder.withValue(Organization.TYPE, type); 1327 builder.withValue(Organization.COMPANY, company); 1328 builder.withValue(Organization.TITLE, title); 1329 builder.withValue(Organization.DEPARTMENT, department); 1330 builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName); 1331 add(builder.build()); 1332 } 1333 1334 public void addNote(Entity entity, String note) { 1335 RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1); 1336 ContentValues cv = builder.cv; 1337 if (note != null) { 1338 note = note.replaceAll("\r\n", "\n"); 1339 } 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 .withValue(RawContacts.DIRTY, 0).build()); 1381 } 1382 for (Long id: mDeletedIdList) { 1383 ops.add(ContentProviderOperation 1384 .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) 1385 .buildUpon() 1386 .appendQueryParameter(ContactsContract.RawContacts.DELETE_PERMANENTLY, 1387 "true") 1388 .build()) 1389 .build()); 1390 } 1391 ops.execute(); 1392 ContentResolver cr = mContext.getContentResolver(); 1393 if (mGroupsUsed) { 1394 // Make sure the title column is set for all of our groups 1395 // And that all of our groups are visible 1396 // TODO Perhaps the visible part should only happen when the group is created, but 1397 // this is fine for now. 1398 Uri groupsUri = uriWithAccount(Groups.CONTENT_URI); 1399 Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE}, 1400 Groups.TITLE + " IS NULL", null, null); 1401 ContentValues values = new ContentValues(); 1402 values.put(Groups.GROUP_VISIBLE, 1); 1403 try { 1404 while (c.moveToNext()) { 1405 String sourceId = c.getString(0); 1406 values.put(Groups.TITLE, sourceId); 1407 cr.update(uriWithAccount(groupsUri), values, Groups.SOURCE_ID + "=?", 1408 new String[] {sourceId}); 1409 } 1410 } finally { 1411 c.close(); 1412 } 1413 } 1414 } 1415 1416 @Override 1417 public String getCollectionName() { 1418 return "Contacts"; 1419 } 1420 1421 private void sendEmail(Serializer s, ContentValues cv, int count) throws IOException { 1422 // Get both parts of the email address (a newly created one in the UI won't have a name) 1423 String addr = cv.getAsString(Email.DATA); 1424 String name = cv.getAsString(Email.DISPLAY_NAME); 1425 if (name == null) { 1426 name = addr; 1427 } 1428 // Compose address from name and addr 1429 if (addr != null) { 1430 String value = '\"' + name + "\" <" + addr + '>'; 1431 if (count < MAX_EMAIL_ROWS) { 1432 s.data(EMAIL_TAGS[count], value); 1433 } 1434 } 1435 } 1436 1437 private void sendIm(Serializer s, ContentValues cv, int count) throws IOException { 1438 String value = cv.getAsString(Im.DATA); 1439 if (value == null) return; 1440 if (count < MAX_IM_ROWS) { 1441 s.data(IM_TAGS[count], value); 1442 } 1443 } 1444 1445 private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames) 1446 throws IOException{ 1447 if (cv.containsKey(StructuredPostal.CITY)) { 1448 s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY)); 1449 } 1450 if (cv.containsKey(StructuredPostal.COUNTRY)) { 1451 s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY)); 1452 } 1453 if (cv.containsKey(StructuredPostal.POSTCODE)) { 1454 s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE)); 1455 } 1456 if (cv.containsKey(StructuredPostal.REGION)) { 1457 s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION)); 1458 } 1459 if (cv.containsKey(StructuredPostal.STREET)) { 1460 s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET)); 1461 } 1462 } 1463 1464 private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException { 1465 switch (cv.getAsInteger(StructuredPostal.TYPE)) { 1466 case StructuredPostal.TYPE_HOME: 1467 sendOnePostal(s, cv, HOME_ADDRESS_TAGS); 1468 break; 1469 case StructuredPostal.TYPE_WORK: 1470 sendOnePostal(s, cv, WORK_ADDRESS_TAGS); 1471 break; 1472 case StructuredPostal.TYPE_OTHER: 1473 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS); 1474 break; 1475 default: 1476 break; 1477 } 1478 } 1479 1480 private void sendStructuredName(Serializer s, ContentValues cv) throws IOException { 1481 if (cv.containsKey(StructuredName.FAMILY_NAME)) { 1482 s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME)); 1483 } 1484 if (cv.containsKey(StructuredName.GIVEN_NAME)) { 1485 s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME)); 1486 } 1487 if (cv.containsKey(StructuredName.MIDDLE_NAME)) { 1488 s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME)); 1489 } 1490 if (cv.containsKey(StructuredName.SUFFIX)) { 1491 s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX)); 1492 } 1493 if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) { 1494 s.data(Tags.CONTACTS_YOMI_FIRST_NAME, 1495 cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); 1496 } 1497 if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) { 1498 s.data(Tags.CONTACTS_YOMI_LAST_NAME, 1499 cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME)); 1500 } 1501 if (cv.containsKey(StructuredName.PREFIX)) { 1502 s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX)); 1503 } 1504 } 1505 1506 private void sendBusiness(Serializer s, ContentValues cv) throws IOException { 1507 if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) { 1508 s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME)); 1509 } 1510 if (cv.containsKey(EasBusiness.CUSTOMER_ID)) { 1511 s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID)); 1512 } 1513 if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) { 1514 s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID)); 1515 } 1516 if (cv.containsKey(EasBusiness.OFFICE_LOCATION)) { 1517 s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(EasBusiness.OFFICE_LOCATION)); 1518 } 1519 } 1520 1521 private void sendPersonal(Serializer s, ContentValues cv) throws IOException { 1522 if (cv.containsKey(EasPersonal.ANNIVERSARY)) { 1523 s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY)); 1524 } 1525 if (cv.containsKey(EasPersonal.BIRTHDAY)) { 1526 s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(EasPersonal.BIRTHDAY)); 1527 } 1528 if (cv.containsKey(EasPersonal.FILE_AS)) { 1529 s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS)); 1530 } 1531 } 1532 1533 private void sendOrganization(Serializer s, ContentValues cv) throws IOException { 1534 if (cv.containsKey(Organization.TITLE)) { 1535 s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE)); 1536 } 1537 if (cv.containsKey(Organization.COMPANY)) { 1538 s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY)); 1539 } 1540 if (cv.containsKey(Organization.DEPARTMENT)) { 1541 s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT)); 1542 } 1543 } 1544 1545 private void sendNickname(Serializer s, ContentValues cv) throws IOException { 1546 if (cv.containsKey(Nickname.NAME)) { 1547 s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME)); 1548 } 1549 } 1550 1551 private void sendWebpage(Serializer s, ContentValues cv) throws IOException { 1552 if (cv.containsKey(Website.URL)) { 1553 s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL)); 1554 } 1555 } 1556 1557 private void sendNote(Serializer s, ContentValues cv) throws IOException { 1558 if (cv.containsKey(Note.NOTE)) { 1559 // EAS won't accept note data with raw newline characters 1560 String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n"); 1561 // Format of upsync data depends on protocol version 1562 if (mService.mProtocolVersionDouble >= 12.0) { 1563 s.start(Tags.BASE_BODY); 1564 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); 1565 s.end(); 1566 } else { 1567 s.data(Tags.CONTACTS_BODY, note); 1568 } 1569 } 1570 } 1571 1572 private void sendChildren(Serializer s, ContentValues cv) throws IOException { 1573 boolean first = true; 1574 for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { 1575 String row = EasChildren.ROWS[i]; 1576 if (cv.containsKey(row)) { 1577 if (first) { 1578 s.start(Tags.CONTACTS_CHILDREN); 1579 first = false; 1580 } 1581 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row)); 1582 } 1583 } 1584 if (!first) { 1585 s.end(); 1586 } 1587 } 1588 1589 private void sendPhone(Serializer s, ContentValues cv) throws IOException { 1590 String value = cv.getAsString(Phone.NUMBER); 1591 if (value == null) return; 1592 switch (cv.getAsInteger(Phone.TYPE)) { 1593 case TYPE_WORK2: 1594 s.data(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER, value); 1595 break; 1596 case Phone.TYPE_WORK: 1597 s.data(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, value); 1598 break; 1599 case TYPE_MMS: 1600 s.data(Tags.CONTACTS2_MMS, value); 1601 break; 1602 case Phone.TYPE_FAX_WORK: 1603 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value); 1604 break; 1605 case Phone.TYPE_COMPANY_MAIN: 1606 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value); 1607 break; 1608 case Phone.TYPE_HOME: 1609 s.data(Tags.CONTACTS_HOME_TELEPHONE_NUMBER, value); 1610 break; 1611 case TYPE_HOME2: 1612 s.data(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER, value); 1613 break; 1614 case Phone.TYPE_MOBILE: 1615 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value); 1616 break; 1617 case Phone.TYPE_CAR: 1618 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value); 1619 break; 1620 case Phone.TYPE_PAGER: 1621 s.data(Tags.CONTACTS_PAGER_NUMBER, value); 1622 break; 1623 case Phone.TYPE_RADIO: 1624 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value); 1625 break; 1626 case Phone.TYPE_FAX_HOME: 1627 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value); 1628 break; 1629 default: 1630 break; 1631 } 1632 } 1633 1634 private void sendRelation(Serializer s, ContentValues cv) throws IOException { 1635 String value = cv.getAsString(Relation.DATA); 1636 if (value == null) return; 1637 switch (cv.getAsInteger(Relation.TYPE)) { 1638 case Relation.TYPE_ASSISTANT: 1639 s.data(Tags.CONTACTS_ASSISTANT_NAME, value); 1640 break; 1641 case Relation.TYPE_MANAGER: 1642 s.data(Tags.CONTACTS2_MANAGER_NAME, value); 1643 break; 1644 case Relation.TYPE_SPOUSE: 1645 s.data(Tags.CONTACTS_SPOUSE, value); 1646 break; 1647 default: 1648 break; 1649 } 1650 } 1651 1652 @Override 1653 public boolean sendLocalChanges(Serializer s) throws IOException { 1654 // First, let's find Contacts that have changed. 1655 ContentResolver cr = mService.mContentResolver; 1656 Uri uri = RawContacts.CONTENT_URI.buildUpon() 1657 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) 1658 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE) 1659 .build(); 1660 1661 if (getSyncKey().equals("0")) { 1662 return false; 1663 } 1664 1665 try { 1666 // Get them all atomically 1667 EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null); 1668 ContentValues cidValues = new ContentValues(); 1669 try { 1670 boolean first = true; 1671 while (ei.hasNext()) { 1672 Entity entity = ei.next(); 1673 // For each of these entities, create the change commands 1674 ContentValues entityValues = entity.getEntityValues(); 1675 String serverId = entityValues.getAsString(RawContacts.SOURCE_ID); 1676 ArrayList<Integer> groupIds = new ArrayList<Integer>(); 1677 if (first) { 1678 s.start(Tags.SYNC_COMMANDS); 1679 userLog("Sending Contacts changes to the server"); 1680 first = false; 1681 } 1682 if (serverId == null) { 1683 // This is a new contact; create a clientId 1684 String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis(); 1685 userLog("Creating new contact with clientId: ", clientId); 1686 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 1687 // And save it in the raw contact 1688 cidValues.put(ContactsContract.RawContacts.SYNC1, clientId); 1689 cr.update(ContentUris. 1690 withAppendedId(ContactsContract.RawContacts.CONTENT_URI, 1691 entityValues.getAsLong(ContactsContract.RawContacts._ID)), 1692 cidValues, null, null); 1693 } else { 1694 if (entityValues.getAsInteger(RawContacts.DELETED) == 1) { 1695 userLog("Deleting contact with serverId: ", serverId); 1696 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 1697 mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID)); 1698 continue; 1699 } 1700 userLog("Upsync change to contact with serverId: " + serverId); 1701 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 1702 } 1703 s.start(Tags.SYNC_APPLICATION_DATA); 1704 // Write out the data here 1705 int imCount = 0; 1706 int emailCount = 0; 1707 for (NamedContentValues ncv: entity.getSubValues()) { 1708 ContentValues cv = ncv.values; 1709 String mimeType = cv.getAsString(Data.MIMETYPE); 1710 if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 1711 sendEmail(s, cv, emailCount++); 1712 } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) { 1713 sendNickname(s, cv); 1714 } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) { 1715 sendChildren(s, cv); 1716 } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) { 1717 sendBusiness(s, cv); 1718 } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) { 1719 sendWebpage(s, cv); 1720 } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) { 1721 sendPersonal(s, cv); 1722 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 1723 sendPhone(s, cv); 1724 } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) { 1725 sendRelation(s, cv); 1726 } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 1727 sendStructuredName(s, cv); 1728 } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) { 1729 sendStructuredPostal(s, cv); 1730 } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) { 1731 sendOrganization(s, cv); 1732 } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { 1733 sendIm(s, cv, imCount++); 1734 } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { 1735 // We must gather these, and send them together (below) 1736 groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); 1737 } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { 1738 sendNote(s, cv); 1739 } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 1740 // For now, the user can change the photo, but the change won't be 1741 // uploaded. 1742 } else { 1743 userLog("Contacts upsync, unknown data: ", mimeType); 1744 } 1745 } 1746 1747 // Now, we'll send up groups, if any 1748 if (!groupIds.isEmpty()) { 1749 boolean groupFirst = true; 1750 for (int id: groupIds) { 1751 // Since we get id's from the provider, we need to find their names 1752 Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id), 1753 GROUP_PROJECTION, null, null, null); 1754 try { 1755 // Presumably, this should always succeed, but ... 1756 if (c.moveToFirst()) { 1757 if (groupFirst) { 1758 s.start(Tags.CONTACTS_CATEGORIES); 1759 groupFirst = false; 1760 } 1761 s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); 1762 } 1763 } finally { 1764 c.close(); 1765 } 1766 } 1767 if (!groupFirst) { 1768 s.end(); 1769 } 1770 } 1771 s.end().end(); // ApplicationData & Change 1772 mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID)); 1773 } 1774 if (!first) { 1775 s.end(); // Commands 1776 } 1777 } finally { 1778 ei.close(); 1779 } 1780 } catch (RemoteException e) { 1781 Log.e(TAG, "Could not read dirty contacts."); 1782 } 1783 1784 return false; 1785 } 1786} 1787