1 package com.android.exchange.service; 2 3 import android.content.ContentProviderOperation; 4 import android.content.ContentResolver; 5 import android.content.ContentUris; 6 import android.content.ContentValues; 7 import android.content.Context; 8 import android.content.Entity; 9 import android.content.EntityIterator; 10 import android.content.SyncResult; 11 import android.database.Cursor; 12 import android.net.Uri; 13 import android.os.Bundle; 14 import android.provider.ContactsContract; 15 import android.provider.ContactsContract.CommonDataKinds.Email; 16 import android.provider.ContactsContract.CommonDataKinds.Event; 17 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 18 import android.provider.ContactsContract.CommonDataKinds.Im; 19 import android.provider.ContactsContract.CommonDataKinds.Nickname; 20 import android.provider.ContactsContract.CommonDataKinds.Note; 21 import android.provider.ContactsContract.CommonDataKinds.Organization; 22 import android.provider.ContactsContract.CommonDataKinds.Phone; 23 import android.provider.ContactsContract.CommonDataKinds.Photo; 24 import android.provider.ContactsContract.CommonDataKinds.Relation; 25 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 26 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 27 import android.provider.ContactsContract.CommonDataKinds.Website; 28 import android.provider.ContactsContract.Groups; 29 import android.text.TextUtils; 30 import android.util.Base64; 31 32 import com.android.emailcommon.TrafficFlags; 33 import com.android.emailcommon.provider.Account; 34 import com.android.emailcommon.provider.Mailbox; 35 import com.android.exchange.Eas; 36 import com.android.exchange.adapter.AbstractSyncParser; 37 import com.android.exchange.adapter.ContactsSyncParser; 38 import com.android.exchange.adapter.Serializer; 39 import com.android.exchange.adapter.Tags; 40 import com.android.mail.utils.LogUtils; 41 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.util.ArrayList; 45 46 /** 47 * Performs an Exchange sync for contacts. 48 * Contact state is in the contacts provider, not in our DB (and therefore not in e.g. mMailbox). 49 * The Mailbox in the Email DB is only useful for serverId and syncInterval. 50 */ 51 public class EasContactsSyncHandler extends EasSyncHandler { 52 private static final String TAG = Eas.LOG_TAG; 53 54 private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = 55 ContactsContract.Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + 56 GroupMembership.GROUP_ROW_ID + "=?"; 57 58 private static final String[] GROUP_TITLE_PROJECTION = 59 new String[] {Groups.TITLE}; 60 private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID}; 61 62 /** The maximum number of IMs we can send for one contact. */ 63 private static final int MAX_IM_ROWS = 3; 64 /** The tags to use for IMs in an upsync. */ 65 private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS, 66 Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3}; 67 68 /** The maximum number of email addresses we can send for one contact. */ 69 private static final int MAX_EMAIL_ROWS = 3; 70 /** The tags to use for the emails in an upsync. */ 71 private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS, 72 Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS}; 73 74 /** The maximum number of phone numbers of each type we can send for one contact. */ 75 private static final int MAX_PHONE_ROWS = 2; 76 /** The tags to use for work phone numbers. */ 77 private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, 78 Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER}; 79 /** The tags to use for home phone numbers. */ 80 private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER, 81 Tags.CONTACTS_HOME2_TELEPHONE_NUMBER}; 82 83 /** The tags to use for different parts of a home address. */ 84 private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 85 Tags.CONTACTS_HOME_ADDRESS_COUNTRY, 86 Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE, 87 Tags.CONTACTS_HOME_ADDRESS_STATE, 88 Tags.CONTACTS_HOME_ADDRESS_STREET}; 89 90 /** The tags to use for different parts of a work address. */ 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 /** The tags to use for different parts of an "other" address. */ 98 private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 99 Tags.CONTACTS_OTHER_ADDRESS_COUNTRY, 100 Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE, 101 Tags.CONTACTS_OTHER_ADDRESS_STATE, 102 Tags.CONTACTS_OTHER_ADDRESS_STREET}; 103 104 private final android.accounts.Account mAccountManagerAccount; 105 106 private final ArrayList<Long> mDeletedContacts = new ArrayList<Long>(); 107 private final ArrayList<Long> mUpdatedContacts = new ArrayList<Long>(); 108 109 // We store the parser so that we can ask it later isGroupsUsed. 110 // TODO: Can we do this more cleanly? 111 private ContactsSyncParser mParser = null; 112 113 private static final class EasChildren { 114 private EasChildren() {} 115 116 /** MIME type used when storing this in data table. */ 117 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children"; 118 public static final int MAX_CHILDREN = 8; 119 public static final String[] ROWS = 120 new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"}; 121 } 122 123 // Classes for each type of contact. 124 // These are copied from ContactSyncAdapter, with unused fields and methods removed, but the 125 // parser hasn't been moved over yet. When that happens, the variables and functions may also 126 // need to be copied over. 127 128 /** 129 * Data and constants for a Personal contact. 130 */ 131 private static final class EasPersonal { 132 /** MIME type used when storing this in data table. */ 133 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal"; 134 public static final String ANNIVERSARY = "data2"; 135 public static final String FILE_AS = "data4"; 136 } 137 138 /** 139 * Data and constants for a Business contact. 140 */ 141 private static final class EasBusiness { 142 /** MIME type used when storing this in data table. */ 143 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business"; 144 public static final String CUSTOMER_ID = "data6"; 145 public static final String GOVERNMENT_ID = "data7"; 146 public static final String ACCOUNT_NAME = "data8"; 147 } 148 149 public EasContactsSyncHandler(final Context context, final ContentResolver contentResolver, 150 final android.accounts.Account accountManagerAccount, final Account account, 151 final Mailbox mailbox, final Bundle syncExtras, final SyncResult syncResult) { 152 super(context, contentResolver, account, mailbox, syncExtras, syncResult); 153 mAccountManagerAccount = accountManagerAccount; 154 } 155 156 @Override 157 protected int getTrafficFlag() { 158 return TrafficFlags.DATA_CONTACTS; 159 } 160 161 @Override 162 protected String getFolderClassName() { 163 return "Contacts"; 164 } 165 166 @Override 167 protected AbstractSyncParser getParser(final InputStream is) throws IOException { 168 // Store the parser because we'll want to ask it about whether groups are used later. 169 // TODO: It'd be nice to find a cleaner way to get this result back from the parser. 170 mParser = new ContactsSyncParser(mContext, mContentResolver, is, 171 mMailbox, mAccount, mAccountManagerAccount); 172 return mParser; 173 } 174 175 @Override 176 protected void setInitialSyncOptions(final Serializer s) throws IOException { 177 // These are the tags we support for upload; whenever we add/remove support 178 // (in addData), we need to update this list 179 s.start(Tags.SYNC_SUPPORTED); 180 s.tag(Tags.CONTACTS_FIRST_NAME); 181 s.tag(Tags.CONTACTS_LAST_NAME); 182 s.tag(Tags.CONTACTS_MIDDLE_NAME); 183 s.tag(Tags.CONTACTS_SUFFIX); 184 s.tag(Tags.CONTACTS_COMPANY_NAME); 185 s.tag(Tags.CONTACTS_JOB_TITLE); 186 s.tag(Tags.CONTACTS_EMAIL1_ADDRESS); 187 s.tag(Tags.CONTACTS_EMAIL2_ADDRESS); 188 s.tag(Tags.CONTACTS_EMAIL3_ADDRESS); 189 s.tag(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER); 190 s.tag(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER); 191 s.tag(Tags.CONTACTS2_MMS); 192 s.tag(Tags.CONTACTS_BUSINESS_FAX_NUMBER); 193 s.tag(Tags.CONTACTS2_COMPANY_MAIN_PHONE); 194 s.tag(Tags.CONTACTS_HOME_FAX_NUMBER); 195 s.tag(Tags.CONTACTS_HOME_TELEPHONE_NUMBER); 196 s.tag(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER); 197 s.tag(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER); 198 s.tag(Tags.CONTACTS_CAR_TELEPHONE_NUMBER); 199 s.tag(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER); 200 s.tag(Tags.CONTACTS_PAGER_NUMBER); 201 s.tag(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER); 202 s.tag(Tags.CONTACTS2_IM_ADDRESS); 203 s.tag(Tags.CONTACTS2_IM_ADDRESS_2); 204 s.tag(Tags.CONTACTS2_IM_ADDRESS_3); 205 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_CITY); 206 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY); 207 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE); 208 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STATE); 209 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STREET); 210 s.tag(Tags.CONTACTS_HOME_ADDRESS_CITY); 211 s.tag(Tags.CONTACTS_HOME_ADDRESS_COUNTRY); 212 s.tag(Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE); 213 s.tag(Tags.CONTACTS_HOME_ADDRESS_STATE); 214 s.tag(Tags.CONTACTS_HOME_ADDRESS_STREET); 215 s.tag(Tags.CONTACTS_OTHER_ADDRESS_CITY); 216 s.tag(Tags.CONTACTS_OTHER_ADDRESS_COUNTRY); 217 s.tag(Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE); 218 s.tag(Tags.CONTACTS_OTHER_ADDRESS_STATE); 219 s.tag(Tags.CONTACTS_OTHER_ADDRESS_STREET); 220 s.tag(Tags.CONTACTS_YOMI_COMPANY_NAME); 221 s.tag(Tags.CONTACTS_YOMI_FIRST_NAME); 222 s.tag(Tags.CONTACTS_YOMI_LAST_NAME); 223 s.tag(Tags.CONTACTS2_NICKNAME); 224 s.tag(Tags.CONTACTS_ASSISTANT_NAME); 225 s.tag(Tags.CONTACTS2_MANAGER_NAME); 226 s.tag(Tags.CONTACTS_SPOUSE); 227 s.tag(Tags.CONTACTS_DEPARTMENT); 228 s.tag(Tags.CONTACTS_TITLE); 229 s.tag(Tags.CONTACTS_OFFICE_LOCATION); 230 s.tag(Tags.CONTACTS2_CUSTOMER_ID); 231 s.tag(Tags.CONTACTS2_GOVERNMENT_ID); 232 s.tag(Tags.CONTACTS2_ACCOUNT_NAME); 233 s.tag(Tags.CONTACTS_ANNIVERSARY); 234 s.tag(Tags.CONTACTS_BIRTHDAY); 235 s.tag(Tags.CONTACTS_WEBPAGE); 236 s.tag(Tags.CONTACTS_PICTURE); 237 s.end(); // SYNC_SUPPORTED 238 } 239 240 @Override 241 protected void setNonInitialSyncOptions(final Serializer s, int numWindows) throws IOException { 242 final int windowSize = numWindows * PIM_WINDOW_SIZE_CONTACTS; 243 if (windowSize > MAX_WINDOW_SIZE + PIM_WINDOW_SIZE_CONTACTS) { 244 throw new IOException("Max window size reached and still no data"); 245 } 246 setPimSyncOptions(s, null, windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE); 247 } 248 249 /** 250 * Add account info and the "caller is syncadapter" param to a URI. 251 * @param uri The {@link Uri} to add to. 252 * @param emailAddress The email address to add to uri. 253 * @return 254 */ 255 private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) { 256 return uri.buildUpon() 257 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, emailAddress) 258 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, 259 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE) 260 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 261 .build(); 262 } 263 264 /** 265 * Add the "caller is syncadapter" param to a URI. 266 * @param uri The {@link Uri} to add to. 267 * @return 268 */ 269 private static Uri addCallerIsSyncAdapterParameter(final Uri uri) { 270 return uri.buildUpon() 271 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 272 .build(); 273 } 274 275 /** 276 * Mark contacts in dirty groups as dirty. 277 */ 278 private void dirtyContactsWithinDirtyGroups() { 279 final String emailAddress = mAccount.mEmailAddress; 280 final Cursor c = mContentResolver.query( 281 uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress), 282 GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null); 283 if (c == null) { 284 return; 285 } 286 try { 287 if (c.getCount() > 0) { 288 final String[] updateArgs = new String[1]; 289 final ContentValues updateValues = new ContentValues(); 290 while (c.moveToNext()) { 291 // For each, "touch" all data rows with this group id; this will mark contacts 292 // in this group as dirty (per ContactsContract). We will then know to upload 293 // them to the server with the modified group information 294 final long id = c.getLong(0); 295 updateValues.put(GroupMembership.GROUP_ROW_ID, id); 296 updateArgs[0] = Long.toString(id); 297 mContentResolver.update(ContactsContract.Data.CONTENT_URI, updateValues, 298 MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs); 299 } 300 // Really delete groups that are marked deleted 301 mContentResolver.delete(uriWithAccountAndIsSyncAdapter( 302 Groups.CONTENT_URI, emailAddress), 303 Groups.DELETED + "=1", null); 304 // Clear the dirty flag for all of our groups 305 updateValues.clear(); 306 updateValues.put(Groups.DIRTY, 0); 307 mContentResolver.update(uriWithAccountAndIsSyncAdapter( 308 Groups.CONTENT_URI, emailAddress), updateValues, null, 309 null); 310 } 311 } finally { 312 c.close(); 313 } 314 } 315 316 /** 317 * Helper to add a string to the upsync. 318 * @param s The {@link Serializer} for this sync request 319 * @param cv The {@link ContentValues} with the data for this string. 320 * @param column The column name in cv to find the string. 321 * @param tag The tag to use when adding to s. 322 * @throws IOException 323 */ 324 private static void sendStringData(final Serializer s, final ContentValues cv, 325 final String column, final int tag) throws IOException { 326 if (cv.containsKey(column)) { 327 final String value = cv.getAsString(column); 328 if (!TextUtils.isEmpty(value)) { 329 s.data(tag, value); 330 } 331 } 332 } 333 334 /** 335 * Add a nickname to the upsync. 336 * @param s The {@link Serializer} for this sync request. 337 * @param cv The {@link ContentValues} with the data for this nickname. 338 * @throws IOException 339 */ 340 private static void sendNickname(final Serializer s, final ContentValues cv) 341 throws IOException { 342 sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME); 343 } 344 345 /** 346 * Add children data to the upsync. 347 * @param s The {@link Serializer} for this sync request. 348 * @param cv The {@link ContentValues} with the data for a set of children. 349 * @throws IOException 350 */ 351 private static void sendChildren(final Serializer s, final ContentValues cv) 352 throws IOException { 353 boolean first = true; 354 for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { 355 final String row = EasChildren.ROWS[i]; 356 if (cv.containsKey(row)) { 357 if (first) { 358 s.start(Tags.CONTACTS_CHILDREN); 359 first = false; 360 } 361 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row)); 362 } 363 } 364 if (!first) { 365 s.end(); 366 } 367 } 368 369 /** 370 * Add business contact info to the upsync. 371 * @param s The {@link Serializer} for this sync request. 372 * @param cv The {@link ContentValues} with the data for this business contact. 373 * @throws IOException 374 */ 375 private static void sendBusiness(final Serializer s, final ContentValues cv) 376 throws IOException { 377 sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME); 378 sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID); 379 sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID); 380 } 381 382 /** 383 * Add a webpage info to the upsync. 384 * @param s The {@link Serializer} for this sync request. 385 * @param cv The {@link ContentValues} with the data for this webpage. 386 * @throws IOException 387 */ 388 private static void sendWebpage(final Serializer s, final ContentValues cv) throws IOException { 389 sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE); 390 } 391 392 /** 393 * Add personal contact info to the upsync. 394 * @param s The {@link Serializer} for this sync request. 395 * @param cv The {@link ContentValues} with the data for this personal contact. 396 * @throws IOException 397 */ 398 private static void sendPersonal(final Serializer s, final ContentValues cv) 399 throws IOException { 400 sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY); 401 sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS); 402 } 403 404 /** 405 * Add a phone number to the upsync. 406 * @param s The {@link Serializer} for this sync request. 407 * @param cv The {@link ContentValues} with the data for this phone number. 408 * @param workCount The number of work phone numbers already added. 409 * @param homeCount The number of home phone numbers already added. 410 * @throws IOException 411 */ 412 private static void sendPhone(final Serializer s, final ContentValues cv, final int workCount, 413 final int homeCount) throws IOException { 414 final String value = cv.getAsString(Phone.NUMBER); 415 if (value == null) return; 416 switch (cv.getAsInteger(Phone.TYPE)) { 417 case Phone.TYPE_WORK: 418 if (workCount < MAX_PHONE_ROWS) { 419 s.data(WORK_PHONE_TAGS[workCount], value); 420 } 421 break; 422 case Phone.TYPE_MMS: 423 s.data(Tags.CONTACTS2_MMS, value); 424 break; 425 case Phone.TYPE_ASSISTANT: 426 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value); 427 break; 428 case Phone.TYPE_FAX_WORK: 429 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value); 430 break; 431 case Phone.TYPE_COMPANY_MAIN: 432 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value); 433 break; 434 case Phone.TYPE_HOME: 435 if (homeCount < MAX_PHONE_ROWS) { 436 s.data(HOME_PHONE_TAGS[homeCount], value); 437 } 438 break; 439 case Phone.TYPE_MOBILE: 440 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value); 441 break; 442 case Phone.TYPE_CAR: 443 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value); 444 break; 445 case Phone.TYPE_PAGER: 446 s.data(Tags.CONTACTS_PAGER_NUMBER, value); 447 break; 448 case Phone.TYPE_RADIO: 449 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value); 450 break; 451 case Phone.TYPE_FAX_HOME: 452 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value); 453 break; 454 default: 455 break; 456 } 457 } 458 459 /** 460 * Add a relation to the upsync. 461 * @param s The {@link Serializer} for this sync request. 462 * @param cv The {@link ContentValues} with the data for this relation. 463 * @throws IOException 464 */ 465 private static void sendRelation(final Serializer s, final ContentValues cv) 466 throws IOException { 467 final String value = cv.getAsString(Relation.DATA); 468 if (value == null) return; 469 switch (cv.getAsInteger(Relation.TYPE)) { 470 case Relation.TYPE_ASSISTANT: 471 s.data(Tags.CONTACTS_ASSISTANT_NAME, value); 472 break; 473 case Relation.TYPE_MANAGER: 474 s.data(Tags.CONTACTS2_MANAGER_NAME, value); 475 break; 476 case Relation.TYPE_SPOUSE: 477 s.data(Tags.CONTACTS_SPOUSE, value); 478 break; 479 default: 480 break; 481 } 482 } 483 484 /** 485 * Add a name to the upsync. 486 * @param s The {@link Serializer} for this sync request. 487 * @param cv The {@link ContentValues} with the data for this name. 488 * @throws IOException 489 */ 490 // TODO: This used to return a displayName, but it was always null. Figure out what it really 491 // wanted to return. 492 private static void sendStructuredName(final Serializer s, final ContentValues cv) 493 throws IOException { 494 sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME); 495 sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME); 496 sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME); 497 sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX); 498 sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME); 499 sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME); 500 sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE); 501 } 502 503 /** 504 * Add an address of a particular type to the upsync. 505 * @param s The {@link Serializer} for this sync request. 506 * @param cv The {@link ContentValues} with the data for this address. 507 * @param fieldNames The field names for this address type. 508 * @throws IOException 509 */ 510 private static void sendOnePostal(final Serializer s, final ContentValues cv, 511 final int[] fieldNames) throws IOException{ 512 sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]); 513 sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]); 514 sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]); 515 sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]); 516 sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]); 517 } 518 519 /** 520 * Add an address to the upsync. 521 * @param s The {@link Serializer} for this sync request. 522 * @param cv The {@link ContentValues} with the data for this address. 523 * @throws IOException 524 */ 525 private static void sendStructuredPostal(final Serializer s, final ContentValues cv) 526 throws IOException { 527 switch (cv.getAsInteger(StructuredPostal.TYPE)) { 528 case StructuredPostal.TYPE_HOME: 529 sendOnePostal(s, cv, HOME_ADDRESS_TAGS); 530 break; 531 case StructuredPostal.TYPE_WORK: 532 sendOnePostal(s, cv, WORK_ADDRESS_TAGS); 533 break; 534 case StructuredPostal.TYPE_OTHER: 535 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS); 536 break; 537 default: 538 break; 539 } 540 } 541 542 /** 543 * Add an organization to the upsync. 544 * @param s The {@link Serializer} for this sync request. 545 * @param cv The {@link ContentValues} with the data for this organization. 546 * @throws IOException 547 */ 548 private static void sendOrganization(final Serializer s, final ContentValues cv) 549 throws IOException { 550 sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE); 551 sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME); 552 sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT); 553 sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION); 554 } 555 556 /** 557 * Add an IM to the upsync. 558 * @param s The {@link Serializer} for this sync request. 559 * @param cv The {@link ContentValues} with the data for this IM. 560 * @throws IOException 561 */ 562 private static void sendIm(final Serializer s, final ContentValues cv, final int count) 563 throws IOException { 564 final String value = cv.getAsString(Im.DATA); 565 if (value == null) return; 566 if (count < MAX_IM_ROWS) { 567 s.data(IM_TAGS[count], value); 568 } 569 } 570 571 /** 572 * Add a birthday to the upsync. 573 * @param s The {@link Serializer} for this sync request. 574 * @param cv The {@link ContentValues} with the data for this birthday. 575 * @throws IOException 576 */ 577 private static void sendBirthday(final Serializer s, final ContentValues cv) 578 throws IOException { 579 sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY); 580 } 581 582 /** 583 * Add a note to the upsync. 584 * @param s The {@link Serializer} for this sync request. 585 * @param cv The {@link ContentValues} with the data for this note. 586 * @throws IOException 587 */ 588 private void sendNote(final Serializer s, final ContentValues cv) throws IOException { 589 // Even when there is no local note, we must explicitly upsync an empty note, 590 // which is the only way to force the server to delete any pre-existing note. 591 String note = ""; 592 if (cv.containsKey(Note.NOTE)) { 593 // EAS won't accept note data with raw newline characters 594 note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n"); 595 } 596 // Format of upsync data depends on protocol version 597 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 598 s.start(Tags.BASE_BODY); 599 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); 600 s.end(); 601 } else { 602 s.data(Tags.CONTACTS_BODY, note); 603 } 604 } 605 606 /** 607 * Add a photo to the upsync. 608 * @param s The {@link Serializer} for this sync request. 609 * @param cv The {@link ContentValues} with the data for this photo. 610 * @throws IOException 611 */ 612 private static void sendPhoto(final Serializer s, final ContentValues cv) throws IOException { 613 if (cv.containsKey(Photo.PHOTO)) { 614 final byte[] bytes = cv.getAsByteArray(Photo.PHOTO); 615 final String pic = Base64.encodeToString(bytes, Base64.NO_WRAP); 616 s.data(Tags.CONTACTS_PICTURE, pic); 617 } else { 618 // Send an empty tag, which signals the server to delete any pre-existing photo 619 s.tag(Tags.CONTACTS_PICTURE); 620 } 621 } 622 623 /** 624 * Add an email address to the upsync. 625 * @param s The {@link Serializer} for this sync request. 626 * @param cv The {@link ContentValues} with the data for this email address. 627 * @param count The number of email addresses that have already been added. 628 * @param displayName The display name for this contact. 629 * @throws IOException 630 */ 631 private void sendEmail(final Serializer s, final ContentValues cv, final int count, 632 final String displayName) throws IOException { 633 // Get both parts of the email address (a newly created one in the UI won't have a name) 634 final String addr = cv.getAsString(Email.DATA); 635 String name = cv.getAsString(Email.DISPLAY_NAME); 636 if (name == null) { 637 if (displayName != null) { 638 name = displayName; 639 } else { 640 name = addr; 641 } 642 } 643 // Compose address from name and addr 644 if (addr != null) { 645 final String value; 646 // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on 647 // an RFC822 address) 648 if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 649 value = addr; 650 } else { 651 value = '\"' + name + "\" <" + addr + '>'; 652 } 653 if (count < MAX_EMAIL_ROWS) { 654 s.data(EMAIL_TAGS[count], value); 655 } 656 } 657 } 658 659 @Override 660 protected void setUpsyncCommands(final Serializer s) throws IOException { 661 // Find any groups of ours that are dirty and dirty those groups' members 662 dirtyContactsWithinDirtyGroups(); 663 664 // First, let's find Contacts that have changed. 665 final Uri uri = uriWithAccountAndIsSyncAdapter( 666 ContactsContract.RawContactsEntity.CONTENT_URI, mAccount.mEmailAddress); 667 668 // Get them all atomically 669 final EntityIterator ei = ContactsContract.RawContacts.newEntityIterator( 670 mContentResolver.query(uri, null, ContactsContract.RawContacts.DIRTY + "=1", null, 671 null)); 672 final ContentValues cidValues = new ContentValues(); 673 try { 674 boolean first = true; 675 final Uri rawContactUri = addCallerIsSyncAdapterParameter( 676 ContactsContract.RawContacts.CONTENT_URI); 677 while (ei.hasNext()) { 678 final Entity entity = ei.next(); 679 // For each of these entities, create the change commands 680 final ContentValues entityValues = entity.getEntityValues(); 681 final String serverId = 682 entityValues.getAsString(ContactsContract.RawContacts.SOURCE_ID); 683 final ArrayList<Integer> groupIds = new ArrayList<Integer>(); 684 if (first) { 685 s.start(Tags.SYNC_COMMANDS); 686 LogUtils.d(TAG, "Sending Contacts changes to the server"); 687 first = false; 688 } 689 if (serverId == null) { 690 // This is a new contact; create a clientId 691 final String clientId = 692 "new_" + mMailbox.mId + '_' + System.currentTimeMillis(); 693 LogUtils.d(TAG, "Creating new contact with clientId: %s", clientId); 694 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 695 // And save it in the raw contact 696 cidValues.put(ContactsContract.RawContacts.SYNC1, clientId); 697 mContentResolver.update(ContentUris.withAppendedId(rawContactUri, 698 entityValues.getAsLong(ContactsContract.RawContacts._ID)), 699 cidValues, null, null); 700 } else { 701 if (entityValues.getAsInteger(ContactsContract.RawContacts.DELETED) == 1) { 702 LogUtils.d(TAG, "Deleting contact with serverId: %s", serverId); 703 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 704 mDeletedContacts.add( 705 entityValues.getAsLong(ContactsContract.RawContacts._ID)); 706 continue; 707 } 708 LogUtils.d(TAG, "Upsync change to contact with serverId: %s", serverId); 709 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 710 } 711 s.start(Tags.SYNC_APPLICATION_DATA); 712 // Write out the data here 713 int imCount = 0; 714 int emailCount = 0; 715 int homePhoneCount = 0; 716 int workPhoneCount = 0; 717 // TODO: How is this name supposed to be formed? 718 String displayName = null; 719 final ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>(); 720 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 721 final ContentValues cv = ncv.values; 722 final String mimeType = cv.getAsString(ContactsContract.Data.MIMETYPE); 723 if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 724 emailValues.add(cv); 725 } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) { 726 sendNickname(s, cv); 727 } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) { 728 sendChildren(s, cv); 729 } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) { 730 sendBusiness(s, cv); 731 } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) { 732 sendWebpage(s, cv); 733 } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) { 734 sendPersonal(s, cv); 735 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 736 sendPhone(s, cv, workPhoneCount, homePhoneCount); 737 int type = cv.getAsInteger(Phone.TYPE); 738 if (type == Phone.TYPE_HOME) homePhoneCount++; 739 if (type == Phone.TYPE_WORK) workPhoneCount++; 740 } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) { 741 sendRelation(s, cv); 742 } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 743 sendStructuredName(s, cv); 744 } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) { 745 sendStructuredPostal(s, cv); 746 } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) { 747 sendOrganization(s, cv); 748 } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { 749 sendIm(s, cv, imCount++); 750 } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) { 751 Integer eventType = cv.getAsInteger(Event.TYPE); 752 if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) { 753 sendBirthday(s, cv); 754 } 755 } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { 756 // We must gather these, and send them together (below) 757 groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); 758 } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { 759 sendNote(s, cv); 760 } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 761 sendPhoto(s, cv); 762 } else { 763 LogUtils.i(TAG, "Contacts upsync, unknown data: %s", mimeType); 764 } 765 } 766 767 // We do the email rows last, because we need to make sure we've found the 768 // displayName (if one exists); this would be in a StructuredName rnow 769 for (final ContentValues cv: emailValues) { 770 sendEmail(s, cv, emailCount++, displayName); 771 } 772 773 // Now, we'll send up groups, if any 774 if (!groupIds.isEmpty()) { 775 boolean groupFirst = true; 776 for (final int id: groupIds) { 777 // Since we get id's from the provider, we need to find their names 778 final Cursor c = mContentResolver.query(ContentUris.withAppendedId( 779 Groups.CONTENT_URI, id), 780 GROUP_TITLE_PROJECTION, null, null, null); 781 try { 782 // Presumably, this should always succeed, but ... 783 if (c.moveToFirst()) { 784 if (groupFirst) { 785 s.start(Tags.CONTACTS_CATEGORIES); 786 groupFirst = false; 787 } 788 s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); 789 } 790 } finally { 791 c.close(); 792 } 793 } 794 if (!groupFirst) { 795 s.end(); 796 } 797 } 798 s.end().end(); // ApplicationData & Change 799 mUpdatedContacts.add(entityValues.getAsLong(ContactsContract.RawContacts._ID)); 800 } 801 if (!first) { 802 s.end(); // Commands 803 } 804 } finally { 805 ei.close(); 806 } 807 808 } 809 810 @Override 811 protected void cleanup(final int syncResult) { 812 if (syncResult == SYNC_RESULT_FAILED) { 813 return; 814 } 815 816 // Mark the changed contacts dirty = 0 817 // Permanently delete the user deletions 818 ContactsSyncParser.ContactOperations ops = new ContactsSyncParser.ContactOperations(); 819 for (final Long id: mUpdatedContacts) { 820 ops.add(ContentProviderOperation 821 .newUpdate(ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, 822 id).buildUpon() 823 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 824 .build()) 825 .withValue(ContactsContract.RawContacts.DIRTY, 0).build()); 826 } 827 for (final Long id: mDeletedContacts) { 828 ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId( 829 ContactsContract.RawContacts.CONTENT_URI, id).buildUpon() 830 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build()) 831 .build()); 832 } 833 ops.execute(mContext); 834 if (mParser != null && mParser.isGroupsUsed()) { 835 // Make sure the title column is set for all of our groups 836 // And that all of our groups are visible 837 // TODO Perhaps the visible part should only happen when the group is created, but 838 // this is fine for now. 839 final Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, 840 mAccount.mEmailAddress); 841 final Cursor c = mContentResolver.query(groupsUri, 842 new String[] {Groups.SOURCE_ID, Groups.TITLE}, 843 Groups.TITLE + " IS NULL", null, null); 844 final ContentValues values = new ContentValues(); 845 values.put(Groups.GROUP_VISIBLE, 1); 846 try { 847 while (c.moveToNext()) { 848 final String sourceId = c.getString(0); 849 values.put(Groups.TITLE, sourceId); 850 mContentResolver.update(uriWithAccountAndIsSyncAdapter(groupsUri, 851 mAccount.mEmailAddress), values, Groups.SOURCE_ID + "=?", 852 new String[] {sourceId}); 853 } 854 } finally { 855 c.close(); 856 } 857 } 858 } 859 860 /** 861 * Delete an account from the Contacts provider. 862 * @param context Our {@link Context} 863 * @param emailAddress The email address of the account we wish to delete 864 */ 865 public static void wipeAccountFromContentProvider(final Context context, 866 final String emailAddress) { 867 context.getContentResolver().delete(uriWithAccountAndIsSyncAdapter( 868 ContactsContract.RawContacts.CONTENT_URI, emailAddress), null, null); 869 } 870 } 871