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