1 package com.android.exchange.eas; 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.database.Cursor; 11 import android.net.Uri; 12 import android.provider.ContactsContract; 13 import android.provider.ContactsContract.CommonDataKinds.Email; 14 import android.provider.ContactsContract.CommonDataKinds.Event; 15 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 16 import android.provider.ContactsContract.CommonDataKinds.Im; 17 import android.provider.ContactsContract.CommonDataKinds.Nickname; 18 import android.provider.ContactsContract.CommonDataKinds.Note; 19 import android.provider.ContactsContract.CommonDataKinds.Organization; 20 import android.provider.ContactsContract.CommonDataKinds.Phone; 21 import android.provider.ContactsContract.CommonDataKinds.Photo; 22 import android.provider.ContactsContract.CommonDataKinds.Relation; 23 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 24 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 25 import android.provider.ContactsContract.CommonDataKinds.Website; 26 import android.provider.ContactsContract.Groups; 27 import android.text.TextUtils; 28 import android.util.Base64; 29 30 import com.android.emailcommon.TrafficFlags; 31 import com.android.emailcommon.provider.Account; 32 import com.android.emailcommon.provider.Mailbox; 33 import com.android.emailcommon.utility.Utility; 34 import com.android.exchange.Eas; 35 import com.android.exchange.adapter.AbstractSyncParser; 36 import com.android.exchange.adapter.ContactsSyncParser; 37 import com.android.exchange.adapter.Serializer; 38 import com.android.exchange.adapter.Tags; 39 import com.android.mail.utils.LogUtils; 40 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.text.DateFormat; 44 import java.text.ParseException; 45 import java.text.SimpleDateFormat; 46 import java.util.ArrayList; 47 import java.util.Date; 48 import java.util.Locale; 49 import java.util.TimeZone; 50 51 /** 52 * Performs an Exchange sync for contacts. 53 * Contact state is in the contacts provider, not in our DB (and therefore not in e.g. mMailbox). 54 * The Mailbox in the Email DB is only useful for serverId and syncInterval. 55 */ 56 public class EasSyncContacts extends EasSyncCollectionTypeBase { 57 private static final String TAG = Eas.LOG_TAG; 58 59 public static final int PIM_WINDOW_SIZE_CONTACTS = 10; 60 61 private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = 62 ContactsContract.Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + 63 GroupMembership.GROUP_ROW_ID + "=?"; 64 65 private static final String[] GROUP_TITLE_PROJECTION = 66 new String[] {Groups.TITLE}; 67 private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID}; 68 69 /** The maximum number of IMs we can send for one contact. */ 70 private static final int MAX_IM_ROWS = 3; 71 /** The tags to use for IMs in an upsync. */ 72 private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS, 73 Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3}; 74 75 /** The maximum number of email addresses we can send for one contact. */ 76 private static final int MAX_EMAIL_ROWS = 3; 77 /** The tags to use for the emails in an upsync. */ 78 private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS, 79 Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS}; 80 81 /** The maximum number of phone numbers of each type we can send for one contact. */ 82 private static final int MAX_PHONE_ROWS = 2; 83 /** The tags to use for work phone numbers. */ 84 private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, 85 Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER}; 86 /** The tags to use for home phone numbers. */ 87 private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER, 88 Tags.CONTACTS_HOME2_TELEPHONE_NUMBER}; 89 90 /** The tags to use for different parts of a home address. */ 91 private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 92 Tags.CONTACTS_HOME_ADDRESS_COUNTRY, 93 Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE, 94 Tags.CONTACTS_HOME_ADDRESS_STATE, 95 Tags.CONTACTS_HOME_ADDRESS_STREET}; 96 97 /** The tags to use for different parts of a work address. */ 98 private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY, 99 Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY, 100 Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE, 101 Tags.CONTACTS_BUSINESS_ADDRESS_STATE, 102 Tags.CONTACTS_BUSINESS_ADDRESS_STREET}; 103 104 /** The tags to use for different parts of an "other" address. */ 105 private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, 106 Tags.CONTACTS_OTHER_ADDRESS_COUNTRY, 107 Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE, 108 Tags.CONTACTS_OTHER_ADDRESS_STATE, 109 Tags.CONTACTS_OTHER_ADDRESS_STREET}; 110 111 private final android.accounts.Account mAccountManagerAccount; 112 113 private final ArrayList<Long> mDeletedContacts = new ArrayList<Long>(); 114 private final ArrayList<Long> mUpdatedContacts = new ArrayList<Long>(); 115 116 // We store the parser so that we can ask it later isGroupsUsed. 117 // TODO: Can we do this more cleanly? 118 private ContactsSyncParser mParser = null; 119 120 private static final class EasChildren { EasChildren()121 private EasChildren() {} 122 123 /** MIME type used when storing this in data table. */ 124 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children"; 125 public static final int MAX_CHILDREN = 8; 126 public static final String[] ROWS = 127 new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"}; 128 } 129 130 // Classes for each type of contact. 131 // These are copied from ContactSyncAdapter, with unused fields and methods removed, but the 132 // parser hasn't been moved over yet. When that happens, the variables and functions may also 133 // need to be copied over. 134 135 /** 136 * Data and constants for a Personal contact. 137 */ 138 private static final class EasPersonal { 139 /** MIME type used when storing this in data table. */ 140 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal"; 141 public static final String ANNIVERSARY = "data2"; 142 public static final String FILE_AS = "data4"; 143 } 144 145 /** 146 * Data and constants for a Business contact. 147 */ 148 private static final class EasBusiness { 149 /** MIME type used when storing this in data table. */ 150 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business"; 151 public static final String CUSTOMER_ID = "data6"; 152 public static final String GOVERNMENT_ID = "data7"; 153 public static final String ACCOUNT_NAME = "data8"; 154 } 155 EasSyncContacts(final String emailAddress)156 public EasSyncContacts(final String emailAddress) { 157 mAccountManagerAccount = new android.accounts.Account(emailAddress, 158 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 159 } 160 161 @Override getTrafficFlag()162 public int getTrafficFlag() { 163 return TrafficFlags.DATA_CONTACTS; 164 } 165 166 @Override setSyncOptions(final Context context, final Serializer s, final double protocolVersion, final Account account, final Mailbox mailbox, final boolean isInitialSync, final int numWindows)167 public void setSyncOptions(final Context context, final Serializer s, 168 final double protocolVersion, final Account account, final Mailbox mailbox, 169 final boolean isInitialSync, final int numWindows) throws IOException { 170 if (isInitialSync) { 171 setInitialSyncOptions(s); 172 return; 173 } 174 175 final int windowSize = numWindows * PIM_WINDOW_SIZE_CONTACTS; 176 if (windowSize > MAX_WINDOW_SIZE + PIM_WINDOW_SIZE_CONTACTS) { 177 throw new IOException("Max window size reached and still no data"); 178 } 179 setPimSyncOptions(s, null, protocolVersion, 180 windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE); 181 182 setUpsyncCommands(s, context.getContentResolver(), account, mailbox, protocolVersion); 183 } 184 185 @Override getParser(final Context context, final Account account, final Mailbox mailbox, final InputStream is)186 public AbstractSyncParser getParser(final Context context, final Account account, 187 final Mailbox mailbox, final InputStream is) throws IOException { 188 mParser = new ContactsSyncParser(context, context.getContentResolver(), is, mailbox, 189 account, mAccountManagerAccount); 190 return mParser; 191 } 192 setInitialSyncOptions(final Serializer s)193 private void setInitialSyncOptions(final Serializer s) throws IOException { 194 // These are the tags we support for upload; whenever we add/remove support 195 // (in addData), we need to update this list 196 s.start(Tags.SYNC_SUPPORTED); 197 s.tag(Tags.CONTACTS_FIRST_NAME); 198 s.tag(Tags.CONTACTS_LAST_NAME); 199 s.tag(Tags.CONTACTS_MIDDLE_NAME); 200 s.tag(Tags.CONTACTS_SUFFIX); 201 s.tag(Tags.CONTACTS_COMPANY_NAME); 202 s.tag(Tags.CONTACTS_JOB_TITLE); 203 s.tag(Tags.CONTACTS_EMAIL1_ADDRESS); 204 s.tag(Tags.CONTACTS_EMAIL2_ADDRESS); 205 s.tag(Tags.CONTACTS_EMAIL3_ADDRESS); 206 s.tag(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER); 207 s.tag(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER); 208 s.tag(Tags.CONTACTS2_MMS); 209 s.tag(Tags.CONTACTS_BUSINESS_FAX_NUMBER); 210 s.tag(Tags.CONTACTS2_COMPANY_MAIN_PHONE); 211 s.tag(Tags.CONTACTS_HOME_FAX_NUMBER); 212 s.tag(Tags.CONTACTS_HOME_TELEPHONE_NUMBER); 213 s.tag(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER); 214 s.tag(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER); 215 s.tag(Tags.CONTACTS_CAR_TELEPHONE_NUMBER); 216 s.tag(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER); 217 s.tag(Tags.CONTACTS_PAGER_NUMBER); 218 s.tag(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER); 219 s.tag(Tags.CONTACTS2_IM_ADDRESS); 220 s.tag(Tags.CONTACTS2_IM_ADDRESS_2); 221 s.tag(Tags.CONTACTS2_IM_ADDRESS_3); 222 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_CITY); 223 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY); 224 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE); 225 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STATE); 226 s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STREET); 227 s.tag(Tags.CONTACTS_HOME_ADDRESS_CITY); 228 s.tag(Tags.CONTACTS_HOME_ADDRESS_COUNTRY); 229 s.tag(Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE); 230 s.tag(Tags.CONTACTS_HOME_ADDRESS_STATE); 231 s.tag(Tags.CONTACTS_HOME_ADDRESS_STREET); 232 s.tag(Tags.CONTACTS_OTHER_ADDRESS_CITY); 233 s.tag(Tags.CONTACTS_OTHER_ADDRESS_COUNTRY); 234 s.tag(Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE); 235 s.tag(Tags.CONTACTS_OTHER_ADDRESS_STATE); 236 s.tag(Tags.CONTACTS_OTHER_ADDRESS_STREET); 237 s.tag(Tags.CONTACTS_YOMI_COMPANY_NAME); 238 s.tag(Tags.CONTACTS_YOMI_FIRST_NAME); 239 s.tag(Tags.CONTACTS_YOMI_LAST_NAME); 240 s.tag(Tags.CONTACTS2_NICKNAME); 241 s.tag(Tags.CONTACTS_ASSISTANT_NAME); 242 s.tag(Tags.CONTACTS2_MANAGER_NAME); 243 s.tag(Tags.CONTACTS_SPOUSE); 244 s.tag(Tags.CONTACTS_DEPARTMENT); 245 s.tag(Tags.CONTACTS_TITLE); 246 s.tag(Tags.CONTACTS_OFFICE_LOCATION); 247 s.tag(Tags.CONTACTS2_CUSTOMER_ID); 248 s.tag(Tags.CONTACTS2_GOVERNMENT_ID); 249 s.tag(Tags.CONTACTS2_ACCOUNT_NAME); 250 s.tag(Tags.CONTACTS_ANNIVERSARY); 251 s.tag(Tags.CONTACTS_BIRTHDAY); 252 s.tag(Tags.CONTACTS_WEBPAGE); 253 s.tag(Tags.CONTACTS_PICTURE); 254 s.tag(Tags.CONTACTS_FILE_AS); 255 s.end(); // SYNC_SUPPORTED 256 } 257 258 /** 259 * Add account info and the "caller is syncadapter" param to a URI. 260 * @param uri The {@link Uri} to add to. 261 * @param emailAddress The email address to add to uri. 262 * @return 263 */ uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress)264 private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) { 265 return uri.buildUpon() 266 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, emailAddress) 267 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, 268 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE) 269 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 270 .build(); 271 } 272 273 /** 274 * Add the "caller is syncadapter" param to a URI. 275 * @param uri The {@link Uri} to add to. 276 * @return 277 */ addCallerIsSyncAdapterParameter(final Uri uri)278 private static Uri addCallerIsSyncAdapterParameter(final Uri uri) { 279 return uri.buildUpon() 280 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 281 .build(); 282 } 283 284 /** 285 * Mark contacts in dirty groups as dirty. 286 */ dirtyContactsWithinDirtyGroups(final ContentResolver cr, final Account account)287 private void dirtyContactsWithinDirtyGroups(final ContentResolver cr, final Account account) { 288 final String emailAddress = account.mEmailAddress; 289 final Cursor c = cr.query( uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress), 290 GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null); 291 if (c == null) { 292 return; 293 } 294 try { 295 if (c.getCount() > 0) { 296 final String[] updateArgs = new String[1]; 297 final ContentValues updateValues = new ContentValues(); 298 while (c.moveToNext()) { 299 // For each, "touch" all data rows with this group id; this will mark contacts 300 // in this group as dirty (per ContactsContract). We will then know to upload 301 // them to the server with the modified group information 302 final long id = c.getLong(0); 303 updateValues.put(GroupMembership.GROUP_ROW_ID, id); 304 updateArgs[0] = Long.toString(id); 305 cr.update(ContactsContract.Data.CONTENT_URI, updateValues, 306 MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs); 307 } 308 // Really delete groups that are marked deleted 309 cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress), 310 Groups.DELETED + "=1", null); 311 // Clear the dirty flag for all of our groups 312 updateValues.clear(); 313 updateValues.put(Groups.DIRTY, 0); 314 cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress), 315 updateValues, null, null); 316 } 317 } finally { 318 c.close(); 319 } 320 } 321 322 /** 323 * Helper function to safely extract a string from a content value. 324 * @param cv The {@link ContentValues} that contains the values 325 * @param column The column name in cv for the data 326 * @return The data in the column or null if it doesn't exist or is empty. 327 * @throws IOException 328 */ tryGetStringData(final ContentValues cv, final String column)329 public static String tryGetStringData(final ContentValues cv, final String column) 330 throws IOException { 331 if ((cv == null) || (column == null)) { 332 return null; 333 } 334 if (cv.containsKey(column)) { 335 final String value = cv.getAsString(column); 336 if (!TextUtils.isEmpty(value)) { 337 return value; 338 } 339 } 340 return null; 341 } 342 343 /** 344 * Helper to add a string to the upsync. 345 * @param s The {@link Serializer} for this sync request 346 * @param cv The {@link ContentValues} with the data for this string. 347 * @param column The column name in cv to find the string. 348 * @param tag The tag to use when adding to s. 349 * @return Whether or not the field was actually set. 350 * @throws IOException 351 */ sendStringData(final Serializer s, final ContentValues cv, final String column, final int tag)352 private static boolean sendStringData(final Serializer s, final ContentValues cv, 353 final String column, final int tag) throws IOException { 354 final String dataValue = tryGetStringData(cv, column); 355 if (dataValue != null) { 356 s.data(tag, dataValue); 357 return true; 358 } 359 return false; 360 } 361 362 363 // This is to catch when the contacts provider has a date in this particular wrong format. 364 private static final SimpleDateFormat SHORT_DATE_FORMAT; 365 // Array of formats we check when parsing dates from the contacts provider. 366 private static final DateFormat[] DATE_FORMATS; 367 static { 368 SHORT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); 369 SHORT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); 370 //TODO: We only handle two formatting types. The default contacts app will work with this 371 // but any other contacts apps might not. We can try harder to handle those guys too. 372 DATE_FORMATS = new DateFormat[] { Eas.DATE_FORMAT, SHORT_DATE_FORMAT }; 373 } 374 375 /** 376 * Helper to add a date to the upsync. It reads the date as a string from the 377 * {@link ContentValues} that we got from the provider, tries to parse it using various formats, 378 * and formats it correctly to send to the server. If it can't parse it, it will omit the date 379 * in the upsync; since Birthdays (the only date currently supported by this class) can be 380 * ghosted, this means that any date changes on the client will NOT be reflected on the server. 381 * @param s The {@link Serializer} for this sync request 382 * @param cv The {@link ContentValues} with the data for this string. 383 * @param column The column name in cv to find the string. 384 * @param tag The tag to use when adding to s. 385 * @throws IOException 386 */ sendDateData(final Serializer s, final ContentValues cv, final String column, final int tag)387 private static void sendDateData(final Serializer s, final ContentValues cv, 388 final String column, final int tag) throws IOException { 389 if (cv.containsKey(column)) { 390 final String value = cv.getAsString(column); 391 if (!TextUtils.isEmpty(value)) { 392 Date date; 393 // Check all the formats we know about to see if one of them works. 394 for (final DateFormat format : DATE_FORMATS) { 395 try { 396 date = format.parse(value); 397 if (date != null) { 398 // We got a legit date for this format, so send it up. 399 s.data(tag, Eas.DATE_FORMAT.format(date)); 400 return; 401 } 402 } catch (final ParseException e) { 403 // The date didn't match this particular format; keep looping. 404 } 405 } 406 } 407 } 408 } 409 410 411 /** 412 * Add a nickname to the upsync. 413 * @param s The {@link Serializer} for this sync request. 414 * @param cv The {@link ContentValues} with the data for this nickname. 415 * @throws IOException 416 */ sendNickname(final Serializer s, final ContentValues cv)417 private static void sendNickname(final Serializer s, final ContentValues cv) 418 throws IOException { 419 sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME); 420 } 421 422 /** 423 * Add children data to the upsync. 424 * @param s The {@link Serializer} for this sync request. 425 * @param cv The {@link ContentValues} with the data for a set of children. 426 * @throws IOException 427 */ sendChildren(final Serializer s, final ContentValues cv)428 private static void sendChildren(final Serializer s, final ContentValues cv) 429 throws IOException { 430 boolean first = true; 431 for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { 432 final String row = EasChildren.ROWS[i]; 433 if (cv.containsKey(row)) { 434 if (first) { 435 s.start(Tags.CONTACTS_CHILDREN); 436 first = false; 437 } 438 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row)); 439 } 440 } 441 if (!first) { 442 s.end(); 443 } 444 } 445 446 /** 447 * Add business contact info to the upsync. 448 * @param s The {@link Serializer} for this sync request. 449 * @param cv The {@link ContentValues} with the data for this business contact. 450 * @throws IOException 451 */ sendBusiness(final Serializer s, final ContentValues cv)452 private static void sendBusiness(final Serializer s, final ContentValues cv) 453 throws IOException { 454 sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME); 455 sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID); 456 sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID); 457 } 458 459 /** 460 * Add a webpage info to the upsync. 461 * @param s The {@link Serializer} for this sync request. 462 * @param cv The {@link ContentValues} with the data for this webpage. 463 * @throws IOException 464 */ sendWebpage(final Serializer s, final ContentValues cv)465 private static void sendWebpage(final Serializer s, final ContentValues cv) throws IOException { 466 sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE); 467 } 468 469 /** 470 * Add personal contact info to the upsync. 471 * @param s The {@link Serializer} for this sync request. 472 * @param cv The {@link ContentValues} with the data for this personal contact. 473 * @throws IOException 474 */ sendPersonal(final Serializer s, final ContentValues cv)475 private static void sendPersonal(final Serializer s, final ContentValues cv) 476 throws IOException { 477 sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY); 478 } 479 480 /** 481 * Add contact file_as info to the upsync. 482 * @param s The {@link Serializer} for this sync request. 483 * @param cv The {@link ContentValues} with the data for this personal contact. 484 * @throws IOException 485 */ trySendFileAs(final Serializer s, final ContentValues cv)486 private static boolean trySendFileAs(final Serializer s, final ContentValues cv) 487 throws IOException { 488 return sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS); 489 } 490 491 /** 492 * Add a phone number to the upsync. 493 * @param s The {@link Serializer} for this sync request. 494 * @param cv The {@link ContentValues} with the data for this phone number. 495 * @param workCount The number of work phone numbers already added. 496 * @param homeCount The number of home phone numbers already added. 497 * @throws IOException 498 */ sendPhone(final Serializer s, final ContentValues cv, final int workCount, final int homeCount)499 private static void sendPhone(final Serializer s, final ContentValues cv, final int workCount, 500 final int homeCount) throws IOException { 501 final String value = cv.getAsString(Phone.NUMBER); 502 if (value == null || !cv.containsKey(Phone.TYPE)) { 503 return; 504 } 505 switch (cv.getAsInteger(Phone.TYPE)) { 506 case Phone.TYPE_WORK: 507 if (workCount < MAX_PHONE_ROWS) { 508 s.data(WORK_PHONE_TAGS[workCount], value); 509 } 510 break; 511 case Phone.TYPE_MMS: 512 s.data(Tags.CONTACTS2_MMS, value); 513 break; 514 case Phone.TYPE_ASSISTANT: 515 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value); 516 break; 517 case Phone.TYPE_FAX_WORK: 518 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value); 519 break; 520 case Phone.TYPE_COMPANY_MAIN: 521 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value); 522 break; 523 case Phone.TYPE_HOME: 524 if (homeCount < MAX_PHONE_ROWS) { 525 s.data(HOME_PHONE_TAGS[homeCount], value); 526 } 527 break; 528 case Phone.TYPE_MOBILE: 529 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value); 530 break; 531 case Phone.TYPE_CAR: 532 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value); 533 break; 534 case Phone.TYPE_PAGER: 535 s.data(Tags.CONTACTS_PAGER_NUMBER, value); 536 break; 537 case Phone.TYPE_RADIO: 538 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value); 539 break; 540 case Phone.TYPE_FAX_HOME: 541 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value); 542 break; 543 default: 544 break; 545 } 546 } 547 548 /** 549 * Add a relation to the upsync. 550 * @param s The {@link Serializer} for this sync request. 551 * @param cv The {@link ContentValues} with the data for this relation. 552 * @throws IOException 553 */ sendRelation(final Serializer s, final ContentValues cv)554 private static void sendRelation(final Serializer s, final ContentValues cv) 555 throws IOException { 556 final String value = cv.getAsString(Relation.DATA); 557 if (value == null || !cv.containsKey(Relation.TYPE)) { 558 return; 559 } 560 switch (cv.getAsInteger(Relation.TYPE)) { 561 case Relation.TYPE_ASSISTANT: 562 s.data(Tags.CONTACTS_ASSISTANT_NAME, value); 563 break; 564 case Relation.TYPE_MANAGER: 565 s.data(Tags.CONTACTS2_MANAGER_NAME, value); 566 break; 567 case Relation.TYPE_SPOUSE: 568 s.data(Tags.CONTACTS_SPOUSE, value); 569 break; 570 default: 571 break; 572 } 573 } 574 575 /** 576 * Add a name to the upsync. 577 * @param s The {@link Serializer} for this sync request. 578 * @param cv The {@link ContentValues} with the data for this name. 579 * @throws IOException 580 */ 581 // TODO: This used to return a displayName, but it was always null. Figure out what it really 582 // wanted to return. sendStructuredName(final Serializer s, final ContentValues cv)583 private static void sendStructuredName(final Serializer s, final ContentValues cv) 584 throws IOException { 585 sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME); 586 sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME); 587 sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME); 588 sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX); 589 sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME); 590 sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME); 591 sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE); 592 } 593 594 /** 595 * Add an address of a particular type to the upsync. 596 * @param s The {@link Serializer} for this sync request. 597 * @param cv The {@link ContentValues} with the data for this address. 598 * @param fieldNames The field names for this address type. 599 * @throws IOException 600 */ sendOnePostal(final Serializer s, final ContentValues cv, final int[] fieldNames)601 private static void sendOnePostal(final Serializer s, final ContentValues cv, 602 final int[] fieldNames) throws IOException{ 603 sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]); 604 sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]); 605 sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]); 606 sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]); 607 sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]); 608 } 609 610 /** 611 * Add an address to the upsync. 612 * @param s The {@link Serializer} for this sync request. 613 * @param cv The {@link ContentValues} with the data for this address. 614 * @throws IOException 615 */ sendStructuredPostal(final Serializer s, final ContentValues cv)616 private static void sendStructuredPostal(final Serializer s, final ContentValues cv) 617 throws IOException { 618 if (!cv.containsKey(StructuredPostal.TYPE)) { 619 return; 620 } 621 switch (cv.getAsInteger(StructuredPostal.TYPE)) { 622 case StructuredPostal.TYPE_HOME: 623 sendOnePostal(s, cv, HOME_ADDRESS_TAGS); 624 break; 625 case StructuredPostal.TYPE_WORK: 626 sendOnePostal(s, cv, WORK_ADDRESS_TAGS); 627 break; 628 case StructuredPostal.TYPE_OTHER: 629 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS); 630 break; 631 default: 632 break; 633 } 634 } 635 636 /** 637 * Add an organization to the upsync. 638 * @param s The {@link Serializer} for this sync request. 639 * @param cv The {@link ContentValues} with the data for this organization. 640 * @throws IOException 641 */ sendOrganization(final Serializer s, final ContentValues cv)642 private static void sendOrganization(final Serializer s, final ContentValues cv) 643 throws IOException { 644 sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE); 645 sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME); 646 sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT); 647 sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION); 648 } 649 650 /** 651 * Add an IM to the upsync. 652 * @param s The {@link Serializer} for this sync request. 653 * @param cv The {@link ContentValues} with the data for this IM. 654 * @throws IOException 655 */ sendIm(final Serializer s, final ContentValues cv, final int count)656 private static void sendIm(final Serializer s, final ContentValues cv, final int count) 657 throws IOException { 658 final String value = cv.getAsString(Im.DATA); 659 if (value == null) return; 660 if (count < MAX_IM_ROWS) { 661 s.data(IM_TAGS[count], value); 662 } 663 } 664 665 /** 666 * Add a birthday to the upsync. 667 * @param s The {@link Serializer} for this sync request. 668 * @param cv The {@link ContentValues} with the data for this birthday. 669 * @throws IOException 670 */ sendBirthday(final Serializer s, final ContentValues cv)671 private static void sendBirthday(final Serializer s, final ContentValues cv) 672 throws IOException { 673 sendDateData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY); 674 } 675 676 /** 677 * Add a note to the upsync. 678 * @param s The {@link Serializer} for this sync request. 679 * @param cv The {@link ContentValues} with the data for this note. 680 * @param protocolVersion 681 * @throws IOException 682 */ sendNote(final Serializer s, final ContentValues cv, final double protocolVersion)683 private void sendNote(final Serializer s, final ContentValues cv, final double protocolVersion) 684 throws IOException { 685 // Even when there is no local note, we must explicitly upsync an empty note, 686 // which is the only way to force the server to delete any pre-existing note. 687 String note = ""; 688 if (cv.containsKey(Note.NOTE)) { 689 // EAS won't accept note data with raw newline characters 690 note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n"); 691 } 692 // Format of upsync data depends on protocol version 693 if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 694 s.start(Tags.BASE_BODY); 695 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); 696 s.end(); 697 } else { 698 s.data(Tags.CONTACTS_BODY, note); 699 } 700 } 701 702 /** 703 * Add a photo to the upsync. 704 * @param s The {@link Serializer} for this sync request. 705 * @param cv The {@link ContentValues} with the data for this photo. 706 * @throws IOException 707 */ sendPhoto(final Serializer s, final ContentValues cv)708 private static void sendPhoto(final Serializer s, final ContentValues cv) throws IOException { 709 if (cv.containsKey(Photo.PHOTO)) { 710 final byte[] bytes = cv.getAsByteArray(Photo.PHOTO); 711 final String pic = Base64.encodeToString(bytes, Base64.NO_WRAP); 712 s.data(Tags.CONTACTS_PICTURE, pic); 713 } else { 714 // Send an empty tag, which signals the server to delete any pre-existing photo 715 s.tag(Tags.CONTACTS_PICTURE); 716 } 717 } 718 719 /** 720 * Add an email address to the upsync. 721 * @param s The {@link Serializer} for this sync request. 722 * @param cv The {@link ContentValues} with the data for this email address. 723 * @param count The number of email addresses that have already been added. 724 * @param displayName The display name for this contact. 725 * @param protocolVersion 726 * @throws IOException 727 */ sendEmail(final Serializer s, final ContentValues cv, final int count, final String displayName, final double protocolVersion)728 private void sendEmail(final Serializer s, final ContentValues cv, final int count, 729 final String displayName, final double protocolVersion) throws IOException { 730 // Get both parts of the email address (a newly created one in the UI won't have a name) 731 final String addr = cv.getAsString(Email.DATA); 732 String name = cv.getAsString(Email.DISPLAY_NAME); 733 if (name == null) { 734 if (displayName != null) { 735 name = displayName; 736 } else { 737 name = addr; 738 } 739 } 740 // Compose address from name and addr 741 if (addr != null) { 742 final String value; 743 // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on 744 // an RFC822 address) 745 if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 746 value = addr; 747 } else { 748 value = '\"' + name + "\" <" + addr + '>'; 749 } 750 if (count < MAX_EMAIL_ROWS) { 751 s.data(EMAIL_TAGS[count], value); 752 } 753 } 754 } 755 756 /** 757 * Generate a default fileAs string for this contact using name and email data. 758 * Note that the user can change this in Outlook/OWA if it is not correct for them but 759 * we need to send something or else Exchange will not display a name with the contact. 760 * @param nameValues Name information to use in generating the fileAs string 761 * @param emailValues Email information to use to generate the fileAs string 762 * @return A valid fileAs string or null 763 */ generateFileAs(final ContentValues nameValues, final ArrayList<ContentValues> emailValues)764 public static String generateFileAs(final ContentValues nameValues, 765 final ArrayList<ContentValues> emailValues) throws IOException { 766 // TODO: Is there a better way of generating a default file_as that will make people 767 // happy everywhere in the world? Should we read the sort settings of the People app? 768 final String firstName = tryGetStringData(nameValues, StructuredName.GIVEN_NAME); 769 final String lastName = tryGetStringData(nameValues, StructuredName.FAMILY_NAME);; 770 final String middleName = tryGetStringData(nameValues, StructuredName.MIDDLE_NAME);; 771 final String nameSuffix = tryGetStringData(nameValues, StructuredName.SUFFIX); 772 773 if (firstName == null && lastName == null) { 774 if (emailValues == null) { 775 // Bad name, bad email list...not much we can do about it. 776 return null; 777 } 778 // The name fields didn't yield anything valuable, let's generate a file as 779 // via the email addresses that were passed in. 780 for (final ContentValues cv : emailValues) { 781 final String emailAddr = tryGetStringData(cv, Email.DATA); 782 if (emailAddr != null) { 783 return emailAddr; 784 } 785 } 786 return null; 787 } 788 // Let's try to construct this with the name only. The format is this: 789 // LastName nameSuffix, FirstName MiddleName 790 // nameSuffix is only applied if lastName exists. 791 final StringBuilder builder = new StringBuilder(); 792 if (lastName != null) { 793 builder.append(lastName); 794 if (nameSuffix != null) { 795 builder.append(" " + nameSuffix); 796 } 797 builder.append(", "); 798 } 799 if (firstName != null) { 800 builder.append(firstName + " "); 801 } 802 if (middleName != null) { 803 builder.append(middleName); 804 } 805 // We might leave a trailing space, so let's trim the string here. 806 return builder.toString().trim(); 807 } 808 809 setUpsyncCommands(final Serializer s, final ContentResolver cr, final Account account, final Mailbox mailbox, final double protocolVersion)810 private void setUpsyncCommands(final Serializer s, final ContentResolver cr, 811 final Account account, final Mailbox mailbox, final double protocolVersion) 812 throws IOException { 813 // Find any groups of ours that are dirty and dirty those groups' members 814 dirtyContactsWithinDirtyGroups(cr, account); 815 816 // First, let's find Contacts that have changed. 817 final Uri uri = uriWithAccountAndIsSyncAdapter( 818 ContactsContract.RawContactsEntity.CONTENT_URI, account.mEmailAddress); 819 820 // Get them all atomically 821 final Cursor cursor = cr.query(uri, null, ContactsContract.RawContacts.DIRTY + "=1", 822 null, null); 823 if (cursor == null) { 824 return; 825 } 826 final EntityIterator ei = ContactsContract.RawContacts.newEntityIterator(cursor); 827 final ContentValues cidValues = new ContentValues(); 828 boolean hasSetFileAs = false; 829 try { 830 boolean first = true; 831 final Uri rawContactUri = addCallerIsSyncAdapterParameter( 832 ContactsContract.RawContacts.CONTENT_URI); 833 while (ei.hasNext()) { 834 final Entity entity = ei.next(); 835 // For each of these entities, create the change commands 836 final ContentValues entityValues = entity.getEntityValues(); 837 String serverId = entityValues.getAsString(ContactsContract.RawContacts.SOURCE_ID); 838 final ArrayList<Integer> groupIds = new ArrayList<Integer>(); 839 if (first) { 840 s.start(Tags.SYNC_COMMANDS); 841 LogUtils.d(TAG, "Sending Contacts changes to the server"); 842 first = false; 843 } 844 if (serverId == null) { 845 // This is a new contact; create a clientId 846 final String clientId = 847 "new_" + mailbox.mId + '_' + System.currentTimeMillis(); 848 // We need to server id to look up the fileAs string. 849 serverId = clientId; 850 LogUtils.d(TAG, "Creating new contact with clientId: %s", clientId); 851 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 852 // And save it in the raw contact 853 cidValues.put(ContactsContract.RawContacts.SYNC1, clientId); 854 cr.update(ContentUris.withAppendedId(rawContactUri, 855 entityValues.getAsLong(ContactsContract.RawContacts._ID)), 856 cidValues, null, null); 857 } else { 858 if (entityValues.getAsInteger(ContactsContract.RawContacts.DELETED) == 1) { 859 LogUtils.d(TAG, "Deleting contact with serverId: %s", serverId); 860 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 861 mDeletedContacts.add( 862 entityValues.getAsLong(ContactsContract.RawContacts._ID)); 863 continue; 864 } 865 LogUtils.d(TAG, "Upsync change to contact with serverId: %s", serverId); 866 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 867 // We don't need to set the file has because it is not a new contact 868 // i.e. it should have the file_as if it needs one. 869 hasSetFileAs = true; 870 } 871 s.start(Tags.SYNC_APPLICATION_DATA); 872 // Write out the data here 873 int imCount = 0; 874 int emailCount = 0; 875 int homePhoneCount = 0; 876 int workPhoneCount = 0; 877 // TODO: How is this name supposed to be formed? 878 String displayName = null; 879 final ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>(); 880 ContentValues nameValues = null; 881 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 882 final ContentValues cv = ncv.values; 883 final String mimeType = cv.getAsString(ContactsContract.Data.MIMETYPE); 884 if (TextUtils.isEmpty(mimeType)) { 885 LogUtils.i(TAG, "Contacts upsync, unknown data: no mimetype set"); 886 continue; 887 } 888 889 if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 890 emailValues.add(cv); 891 } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) { 892 sendNickname(s, cv); 893 } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) { 894 sendChildren(s, cv); 895 } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) { 896 sendBusiness(s, cv); 897 } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) { 898 sendWebpage(s, cv); 899 } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) { 900 sendPersonal(s, cv); 901 hasSetFileAs = trySendFileAs(s, cv); 902 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 903 sendPhone(s, cv, workPhoneCount, homePhoneCount); 904 if (cv.containsKey(Phone.TYPE)) { 905 final int type = cv.getAsInteger(Phone.TYPE); 906 if (type == Phone.TYPE_HOME) { 907 homePhoneCount++; 908 } else if (type == Phone.TYPE_WORK) { 909 workPhoneCount++; 910 } 911 } 912 } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) { 913 sendRelation(s, cv); 914 } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 915 sendStructuredName(s, cv); 916 // Stash names here 917 nameValues = cv; 918 } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) { 919 sendStructuredPostal(s, cv); 920 } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) { 921 sendOrganization(s, cv); 922 } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { 923 sendIm(s, cv, imCount++); 924 } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) { 925 if (cv.containsKey(Event.TYPE)) { 926 final Integer eventType = cv.getAsInteger(Event.TYPE); 927 if (eventType != null && 928 eventType.equals(Event.TYPE_BIRTHDAY)) { 929 sendBirthday(s, cv); 930 } 931 } 932 } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { 933 // We must gather these, and send them together (below) 934 groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); 935 } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { 936 sendNote(s, cv, protocolVersion); 937 } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 938 sendPhoto(s, cv); 939 } else { 940 LogUtils.i(TAG, "Contacts upsync, unknown data: %s", mimeType); 941 } 942 } 943 // We do the email rows last, because we need to make sure we've found the 944 // displayName (if one exists); this would be in a StructuredName rnow 945 for (final ContentValues cv: emailValues) { 946 sendEmail(s, cv, emailCount++, displayName, protocolVersion); 947 } 948 // For Exchange, we need to make sure that we provide a fileAs string because 949 // it is used as the display name for the contact in some views. 950 if (!hasSetFileAs) { 951 String fileAs = null; 952 // Let's go grab the display_name_alt info for this contact and use 953 // that as the default fileAs. 954 final Cursor c = cr.query(ContactsContract.RawContacts.CONTENT_URI, 955 new String[]{ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE}, 956 ContactsContract.RawContacts.SYNC1 + "=?", 957 new String[]{String.valueOf(serverId)}, null); 958 try { 959 while (c.moveToNext()) { 960 final String contentValue = c.getString(0); 961 if ((contentValue != null) && (!TextUtils.isEmpty(contentValue))) { 962 fileAs = contentValue; 963 break; 964 } 965 } 966 } finally { 967 c.close(); 968 } 969 if (fileAs == null) { 970 // Just in case that property did not exist, we can generate our own 971 // rudimentary string that uses a combination of structured name fields or 972 // email addresses depending on what is available. 973 fileAs = generateFileAs(nameValues, emailValues); 974 } 975 s.data(Tags.CONTACTS_FILE_AS, fileAs); 976 } 977 // Now, we'll send up groups, if any 978 if (!groupIds.isEmpty()) { 979 boolean groupFirst = true; 980 for (final int id: groupIds) { 981 // Since we get id's from the provider, we need to find their names 982 final Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, 983 id), GROUP_TITLE_PROJECTION, null, null, null); 984 try { 985 // Presumably, this should always succeed, but ... 986 if (c.moveToFirst()) { 987 if (groupFirst) { 988 s.start(Tags.CONTACTS_CATEGORIES); 989 groupFirst = false; 990 } 991 s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); 992 } 993 } finally { 994 c.close(); 995 } 996 } 997 if (!groupFirst) { 998 s.end(); 999 } 1000 } 1001 s.end().end(); // ApplicationData & Change 1002 mUpdatedContacts.add(entityValues.getAsLong(ContactsContract.RawContacts._ID)); 1003 } 1004 if (!first) { 1005 s.end(); // Commands 1006 } 1007 } finally { 1008 ei.close(); 1009 } 1010 1011 } 1012 1013 @Override cleanup(final Context context, final Account account)1014 public void cleanup(final Context context, final Account account) { 1015 final ContentResolver cr = context.getContentResolver(); 1016 1017 // Mark the changed contacts dirty = 0 1018 // Permanently delete the user deletions 1019 ContactsSyncParser.ContactOperations ops = new ContactsSyncParser.ContactOperations(); 1020 for (final Long id: mUpdatedContacts) { 1021 ops.add(ContentProviderOperation 1022 .newUpdate(ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, 1023 id).buildUpon() 1024 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 1025 .build()) 1026 .withValue(ContactsContract.RawContacts.DIRTY, 0).build()); 1027 } 1028 for (final Long id: mDeletedContacts) { 1029 ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId( 1030 ContactsContract.RawContacts.CONTENT_URI, id).buildUpon() 1031 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build()) 1032 .build()); 1033 } 1034 ops.execute(context); 1035 if (mParser != null && mParser.isGroupsUsed()) { 1036 // Make sure the title column is set for all of our groups 1037 // And that all of our groups are visible 1038 // TODO Perhaps the visible part should only happen when the group is created, but 1039 // this is fine for now. 1040 final Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, 1041 account.mEmailAddress); 1042 final Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE}, 1043 Groups.TITLE + " IS NULL", null, null); 1044 final ContentValues values = new ContentValues(); 1045 values.put(Groups.GROUP_VISIBLE, 1); 1046 try { 1047 while (c.moveToNext()) { 1048 final String sourceId = c.getString(0); 1049 values.put(Groups.TITLE, sourceId); 1050 cr.update(uriWithAccountAndIsSyncAdapter(groupsUri, 1051 account.mEmailAddress), values, Groups.SOURCE_ID + "=?", 1052 new String[] {sourceId}); 1053 } 1054 } finally { 1055 c.close(); 1056 } 1057 } 1058 } 1059 1060 /** 1061 * Delete an account from the Contacts provider. 1062 * @param context Our {@link Context} 1063 * @param emailAddress The email address of the account we wish to delete 1064 */ wipeAccountFromContentProvider(final Context context, final String emailAddress)1065 public static void wipeAccountFromContentProvider(final Context context, 1066 final String emailAddress) { 1067 try { 1068 context.getContentResolver().delete(uriWithAccountAndIsSyncAdapter( 1069 ContactsContract.RawContacts.CONTENT_URI, emailAddress), null, null); 1070 } catch (IllegalArgumentException e) { 1071 LogUtils.e(TAG, "ContactsProvider disabled; unable to wipe account."); 1072 } 1073 } 1074 } 1075