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