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