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 android.content.ContentProviderClient; 21 import android.content.ContentProviderOperation; 22 import android.content.ContentProviderOperation.Builder; 23 import android.content.ContentProviderResult; 24 import android.content.ContentResolver; 25 import android.content.ContentUris; 26 import android.content.ContentValues; 27 import android.content.Entity; 28 import android.content.Entity.NamedContentValues; 29 import android.content.EntityIterator; 30 import android.content.OperationApplicationException; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.RemoteException; 34 import android.provider.ContactsContract; 35 import android.provider.ContactsContract.CommonDataKinds.Email; 36 import android.provider.ContactsContract.CommonDataKinds.Event; 37 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 38 import android.provider.ContactsContract.CommonDataKinds.Im; 39 import android.provider.ContactsContract.CommonDataKinds.Nickname; 40 import android.provider.ContactsContract.CommonDataKinds.Note; 41 import android.provider.ContactsContract.CommonDataKinds.Organization; 42 import android.provider.ContactsContract.CommonDataKinds.Phone; 43 import android.provider.ContactsContract.CommonDataKinds.Photo; 44 import android.provider.ContactsContract.CommonDataKinds.Relation; 45 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 46 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 47 import android.provider.ContactsContract.CommonDataKinds.Website; 48 import android.provider.ContactsContract.Data; 49 import android.provider.ContactsContract.Groups; 50 import android.provider.ContactsContract.RawContacts; 51 import android.provider.ContactsContract.RawContactsEntity; 52 import android.provider.ContactsContract.Settings; 53 import android.provider.ContactsContract.SyncState; 54 import android.provider.SyncStateContract; 55 import android.text.TextUtils; 56 import android.text.util.Rfc822Token; 57 import android.text.util.Rfc822Tokenizer; 58 import android.util.Base64; 59 import android.util.Log; 60 61 import com.android.emailcommon.utility.Utility; 62 import com.android.exchange.CommandStatusException; 63 import com.android.exchange.Eas; 64 import com.android.exchange.EasSyncService; 65 import com.android.exchange.utility.CalendarUtilities; 66 67 import java.io.IOException; 68 import java.io.InputStream; 69 import java.util.ArrayList; 70 import java.util.GregorianCalendar; 71 import java.util.TimeZone; 72 73 /** 74 * Sync adapter for EAS Contacts 75 * 76 */ 77 public 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 ContactsSyncAdapter(EasSyncService service)138 public ContactsSyncAdapter(EasSyncService service) { 139 super(service); 140 mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI); 141 mContentResolver = mContext.getContentResolver(); 142 } 143 addCallerIsSyncAdapterParameter(Uri uri)144 static Uri addCallerIsSyncAdapterParameter(Uri uri) { 145 return uri.buildUpon() 146 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 147 .build(); 148 } 149 150 @Override sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)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 isSyncable()221 public boolean isSyncable() { 222 return ContentResolver.getSyncAutomatically( 223 mAccountManagerAccount, ContactsContract.AUTHORITY); 224 } 225 226 @Override parse(InputStream is)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 wipe()234 public void wipe() { 235 mContentResolver.delete(mAccountUri, null, null); 236 } 237 238 interface UntypedRow { addValues(RowBuilder builder)239 public void addValues(RowBuilder builder); isSameAs(int type, String value)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 getSyncKey()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 setSyncKey(String syncKey, boolean inCommands)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 { 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 hasData()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 hasData()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 hasData()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 EmailRow(String _email)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 addValues(RowBuilder builder)367 public void addValues(RowBuilder builder) { 368 builder.withValue(Email.DATA, email); 369 builder.withValue(Email.DISPLAY_NAME, displayName); 370 } 371 372 @Override isSameAs(int type, String value)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 ImRow(String _im)381 public ImRow(String _im) { 382 im = _im; 383 } 384 385 @Override addValues(RowBuilder builder)386 public void addValues(RowBuilder builder) { 387 builder.withValue(Im.DATA, im); 388 } 389 390 @Override isSameAs(int type, String value)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 PhoneRow(String _phone, int _type)400 public PhoneRow(String _phone, int _type) { 401 phone = _phone; 402 type = _type; 403 } 404 405 @Override addValues(RowBuilder builder)406 public void addValues(RowBuilder builder) { 407 builder.withValue(Im.DATA, phone); 408 builder.withValue(Phone.TYPE, type); 409 } 410 411 @Override isSameAs(int _type, String value)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 EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)423 public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter) 424 throws IOException { 425 super(in, adapter); 426 } 427 addData(String serverId, ContactOperations ops, Entity entity)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 categoriesParser(ContactOperations ops, Entity entity)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 childrenParser(ArrayList<String> children)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 bodyParser()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 addParser(ContactOperations ops)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 getServerIdCursor(String serverId)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 getClientIdCursor(String clientId)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 deleteParser(ContactOperations ops)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 ServerChange(long _id, boolean _read)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 */ changeParser(ContactOperations ops)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 commandsParser()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 commit()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 addResponsesParser()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 changeResponsesParser()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 responsesParser()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 uriWithAccountAndIsSyncAdapter(Uri uri)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 RowBuilder(Builder _builder)987 public RowBuilder(Builder _builder) { 988 builder = _builder; 989 } 990 RowBuilder(Builder _builder, NamedContentValues _ncv)991 public RowBuilder(Builder _builder, NamedContentValues _ncv) { 992 builder = _builder; 993 cv = _ncv.values; 994 } 995 withValues(ContentValues values)996 RowBuilder withValues(ContentValues values) { 997 builder.withValues(values); 998 return this; 999 } 1000 withValueBackReference(String key, int previousResult)1001 RowBuilder withValueBackReference(String key, int previousResult) { 1002 builder.withValueBackReference(key, previousResult); 1003 return this; 1004 } 1005 build()1006 ContentProviderOperation build() { 1007 return builder.build(); 1008 } 1009 withValue(String key, Object value)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 add(ContentProviderOperation op)1027 public boolean add(ContentProviderOperation op) { 1028 super.add(op); 1029 mCount++; 1030 return true; 1031 } 1032 newContact(String serverId)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 delete(long id)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 execute()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 */ findTypedData(ArrayList<NamedContentValues> list, String contentItemType, int type, String stringType)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 */ findUntypedData(ArrayList<NamedContentValues> list, int type, String contentItemType)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 */ createBuilder(Entity entity, String mimeType, int type, String stringType)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 typedRowBuilder(Entity entity, String mimeType, int type)1198 private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { 1199 return createBuilder(entity, mimeType, type, null); 1200 } 1201 untypedRowBuilder(Entity entity, String mimeType)1202 private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { 1203 return createBuilder(entity, mimeType, -1, null); 1204 } 1205 newRowBuilder(Entity entity, String mimeType)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 */ cvCompareString(ContentValues cv, String column, String oldValue)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 addChildren(Entity entity, ArrayList<String> children)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 addGroup(Entity entity, String group)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 addBirthday(Entity entity, String birthday)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 addName(Entity entity, String prefix, String givenName, String familyName, String middleName, String suffix, String displayName, String yomiFirstName, String yomiLastName)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 addPersonal(Entity entity, EasPersonal personal)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 addBusiness(Entity entity, EasBusiness business)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 addPhoto(Entity entity, String photo)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 addPhone(Entity entity, int type, String phone)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 addWebpage(Entity entity, String url)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 addRelation(Entity entity, int type, String value)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 addNickname(Entity entity, String name)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 addPostal(Entity entity, int type, String street, String city, String state, String country, String code)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 */ addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, int type, int maxRows)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 addOrganization(Entity entity, int type, String company, String title, String department, String yomiCompanyName, String officeLocation)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 addNote(Entity entity, String note)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 */ dataUriFromNamedContentValues(NamedContentValues ncv)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 cleanup()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 getCollectionName()1591 public String getCollectionName() { 1592 return "Contacts"; 1593 } 1594 sendEmail(Serializer s, ContentValues cv, int count, String displayName)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 sendIm(Serializer s, ContentValues cv, int count)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 sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)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 sendStructuredPostal(Serializer s, ContentValues cv)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 sendStringData(Serializer s, ContentValues cv, String column, int tag)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 sendStructuredName(Serializer s, ContentValues cv)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 sendBusiness(Serializer s, ContentValues cv)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 sendPersonal(Serializer s, ContentValues cv)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 sendBirthday(Serializer s, ContentValues cv)1689 private void sendBirthday(Serializer s, ContentValues cv) throws IOException { 1690 sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY); 1691 } 1692 sendPhoto(Serializer s, ContentValues cv)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 sendOrganization(Serializer s, ContentValues cv)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 sendNickname(Serializer s, ContentValues cv)1711 private void sendNickname(Serializer s, ContentValues cv) throws IOException { 1712 sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME); 1713 } 1714 sendWebpage(Serializer s, ContentValues cv)1715 private void sendWebpage(Serializer s, ContentValues cv) throws IOException { 1716 sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE); 1717 } 1718 sendNote(Serializer s, ContentValues cv)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 sendChildren(Serializer s, ContentValues cv)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 sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)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 sendRelation(Serializer s, ContentValues cv)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 dirtyContactsWithinDirtyGroups()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 sendLocalChanges(Serializer s)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