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