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