1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package com.example.android.samplesync.platform; 17 18 import com.example.android.samplesync.Constants; 19 import com.example.android.samplesync.R; 20 import com.example.android.samplesync.client.RawContact; 21 22 import android.accounts.Account; 23 import android.content.ContentResolver; 24 import android.content.ContentUris; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.database.Cursor; 28 import android.net.Uri; 29 import android.provider.ContactsContract; 30 import android.provider.ContactsContract.CommonDataKinds.Email; 31 import android.provider.ContactsContract.CommonDataKinds.Im; 32 import android.provider.ContactsContract.CommonDataKinds.Phone; 33 import android.provider.ContactsContract.CommonDataKinds.Photo; 34 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 35 import android.provider.ContactsContract.Contacts; 36 import android.provider.ContactsContract.Data; 37 import android.provider.ContactsContract.Groups; 38 import android.provider.ContactsContract.RawContacts; 39 import android.provider.ContactsContract.Settings; 40 import android.provider.ContactsContract.StatusUpdates; 41 import android.util.Log; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 46 /** 47 * Class for managing contacts sync related mOperations 48 */ 49 public class ContactManager { 50 51 /** 52 * Custom IM protocol used when storing status messages. 53 */ 54 public static final String CUSTOM_IM_PROTOCOL = "SampleSyncAdapter"; 55 56 private static final String TAG = "ContactManager"; 57 58 public static final String SAMPLE_GROUP_NAME = "Sample Group"; 59 ensureSampleGroupExists(Context context, Account account)60 public static long ensureSampleGroupExists(Context context, Account account) { 61 final ContentResolver resolver = context.getContentResolver(); 62 63 // Lookup the sample group 64 long groupId = 0; 65 final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { Groups._ID }, 66 Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=? AND " + 67 Groups.TITLE + "=?", 68 new String[] { account.name, account.type, SAMPLE_GROUP_NAME }, null); 69 if (cursor != null) { 70 try { 71 if (cursor.moveToFirst()) { 72 groupId = cursor.getLong(0); 73 } 74 } finally { 75 cursor.close(); 76 } 77 } 78 79 if (groupId == 0) { 80 // Sample group doesn't exist yet, so create it 81 final ContentValues contentValues = new ContentValues(); 82 contentValues.put(Groups.ACCOUNT_NAME, account.name); 83 contentValues.put(Groups.ACCOUNT_TYPE, account.type); 84 contentValues.put(Groups.TITLE, SAMPLE_GROUP_NAME); 85 contentValues.put(Groups.GROUP_IS_READ_ONLY, true); 86 87 final Uri newGroupUri = resolver.insert(Groups.CONTENT_URI, contentValues); 88 groupId = ContentUris.parseId(newGroupUri); 89 } 90 return groupId; 91 } 92 93 /** 94 * Take a list of updated contacts and apply those changes to the 95 * contacts database. Typically this list of contacts would have been 96 * returned from the server, and we want to apply those changes locally. 97 * 98 * @param context The context of Authenticator Activity 99 * @param account The username for the account 100 * @param rawContacts The list of contacts to update 101 * @param lastSyncMarker The previous server sync-state 102 * @return the server syncState that should be used in our next 103 * sync request. 104 */ updateContacts(Context context, String account, List<RawContact> rawContacts, long groupId, long lastSyncMarker)105 public static synchronized long updateContacts(Context context, String account, 106 List<RawContact> rawContacts, long groupId, long lastSyncMarker) { 107 108 long currentSyncMarker = lastSyncMarker; 109 final ContentResolver resolver = context.getContentResolver(); 110 final BatchOperation batchOperation = new BatchOperation(context, resolver); 111 final List<RawContact> newUsers = new ArrayList<RawContact>(); 112 113 Log.d(TAG, "In SyncContacts"); 114 for (final RawContact rawContact : rawContacts) { 115 // The server returns a syncState (x) value with each contact record. 116 // The syncState is sequential, so higher values represent more recent 117 // changes than lower values. We keep track of the highest value we 118 // see, and consider that a "high water mark" for the changes we've 119 // received from the server. That way, on our next sync, we can just 120 // ask for changes that have occurred since that most-recent change. 121 if (rawContact.getSyncState() > currentSyncMarker) { 122 currentSyncMarker = rawContact.getSyncState(); 123 } 124 125 // If the server returned a clientId for this user, then it's likely 126 // that the user was added here, and was just pushed to the server 127 // for the first time. In that case, we need to update the main 128 // row for this contact so that the RawContacts.SOURCE_ID value 129 // contains the correct serverId. 130 final long rawContactId; 131 final boolean updateServerId; 132 if (rawContact.getRawContactId() > 0) { 133 rawContactId = rawContact.getRawContactId(); 134 updateServerId = true; 135 } else { 136 long serverContactId = rawContact.getServerContactId(); 137 rawContactId = lookupRawContact(resolver, serverContactId); 138 updateServerId = false; 139 } 140 if (rawContactId != 0) { 141 if (!rawContact.isDeleted()) { 142 updateContact(context, resolver, rawContact, updateServerId, 143 true, true, true, rawContactId, batchOperation); 144 } else { 145 deleteContact(context, rawContactId, batchOperation); 146 } 147 } else { 148 Log.d(TAG, "In addContact"); 149 if (!rawContact.isDeleted()) { 150 newUsers.add(rawContact); 151 addContact(context, account, rawContact, groupId, true, batchOperation); 152 } 153 } 154 // A sync adapter should batch operations on multiple contacts, 155 // because it will make a dramatic performance difference. 156 // (UI updates, etc) 157 if (batchOperation.size() >= 50) { 158 batchOperation.execute(); 159 } 160 } 161 batchOperation.execute(); 162 163 return currentSyncMarker; 164 } 165 166 /** 167 * Return a list of the local contacts that have been marked as 168 * "dirty", and need syncing to the SampleSync server. 169 * 170 * @param context The context of Authenticator Activity 171 * @param account The account that we're interested in syncing 172 * @return a list of Users that are considered "dirty" 173 */ getDirtyContacts(Context context, Account account)174 public static List<RawContact> getDirtyContacts(Context context, Account account) { 175 Log.i(TAG, "*** Looking for local dirty contacts"); 176 List<RawContact> dirtyContacts = new ArrayList<RawContact>(); 177 178 final ContentResolver resolver = context.getContentResolver(); 179 final Cursor c = resolver.query(DirtyQuery.CONTENT_URI, 180 DirtyQuery.PROJECTION, 181 DirtyQuery.SELECTION, 182 new String[] {account.name}, 183 null); 184 try { 185 while (c.moveToNext()) { 186 final long rawContactId = c.getLong(DirtyQuery.COLUMN_RAW_CONTACT_ID); 187 final long serverContactId = c.getLong(DirtyQuery.COLUMN_SERVER_ID); 188 final boolean isDirty = "1".equals(c.getString(DirtyQuery.COLUMN_DIRTY)); 189 final boolean isDeleted = "1".equals(c.getString(DirtyQuery.COLUMN_DELETED)); 190 191 // The system actually keeps track of a change version number for 192 // each contact. It may be something you're interested in for your 193 // client-server sync protocol. We're not using it in this example, 194 // other than to log it. 195 final long version = c.getLong(DirtyQuery.COLUMN_VERSION); 196 197 Log.i(TAG, "Dirty Contact: " + Long.toString(rawContactId)); 198 Log.i(TAG, "Contact Version: " + Long.toString(version)); 199 200 if (isDeleted) { 201 Log.i(TAG, "Contact is marked for deletion"); 202 RawContact rawContact = RawContact.createDeletedContact(rawContactId, 203 serverContactId); 204 dirtyContacts.add(rawContact); 205 } else if (isDirty) { 206 RawContact rawContact = getRawContact(context, rawContactId); 207 Log.i(TAG, "Contact Name: " + rawContact.getBestName()); 208 dirtyContacts.add(rawContact); 209 } 210 } 211 212 } finally { 213 if (c != null) { 214 c.close(); 215 } 216 } 217 return dirtyContacts; 218 } 219 220 /** 221 * Update the status messages for a list of users. This is typically called 222 * for contacts we've just added to the system, since we can't monkey with 223 * the contact's status until they have a profileId. 224 * 225 * @param context The context of Authenticator Activity 226 * @param rawContacts The list of users we want to update 227 */ updateStatusMessages(Context context, List<RawContact> rawContacts)228 public static void updateStatusMessages(Context context, List<RawContact> rawContacts) { 229 final ContentResolver resolver = context.getContentResolver(); 230 final BatchOperation batchOperation = new BatchOperation(context, resolver); 231 for (RawContact rawContact : rawContacts) { 232 updateContactStatus(context, rawContact, batchOperation); 233 } 234 batchOperation.execute(); 235 } 236 237 /** 238 * After we've finished up a sync operation, we want to clean up the sync-state 239 * so that we're ready for the next time. This involves clearing out the 'dirty' 240 * flag on the synced contacts - but we also have to finish the DELETE operation 241 * on deleted contacts. When the user initially deletes them on the client, they're 242 * marked for deletion - but they're not actually deleted until we delete them 243 * again, and include the ContactsContract.CALLER_IS_SYNCADAPTER parameter to 244 * tell the contacts provider that we're really ready to let go of this contact. 245 * 246 * @param context The context of Authenticator Activity 247 * @param dirtyContacts The list of contacts that we're cleaning up 248 */ clearSyncFlags(Context context, List<RawContact> dirtyContacts)249 public static void clearSyncFlags(Context context, List<RawContact> dirtyContacts) { 250 Log.i(TAG, "*** Clearing Sync-related Flags"); 251 final ContentResolver resolver = context.getContentResolver(); 252 final BatchOperation batchOperation = new BatchOperation(context, resolver); 253 for (RawContact rawContact : dirtyContacts) { 254 if (rawContact.isDeleted()) { 255 Log.i(TAG, "Deleting contact: " + Long.toString(rawContact.getRawContactId())); 256 deleteContact(context, rawContact.getRawContactId(), batchOperation); 257 } else if (rawContact.isDirty()) { 258 Log.i(TAG, "Clearing dirty flag for: " + rawContact.getBestName()); 259 clearDirtyFlag(context, rawContact.getRawContactId(), batchOperation); 260 } 261 } 262 batchOperation.execute(); 263 } 264 265 /** 266 * Adds a single contact to the platform contacts provider. 267 * This can be used to respond to a new contact found as part 268 * of sync information returned from the server, or because a 269 * user added a new contact. 270 * 271 * @param context the Authenticator Activity context 272 * @param accountName the account the contact belongs to 273 * @param rawContact the sample SyncAdapter User object 274 * @param groupId the id of the sample group 275 * @param inSync is the add part of a client-server sync? 276 * @param batchOperation allow us to batch together multiple operations 277 * into a single provider call 278 */ addContact(Context context, String accountName, RawContact rawContact, long groupId, boolean inSync, BatchOperation batchOperation)279 public static void addContact(Context context, String accountName, RawContact rawContact, 280 long groupId, boolean inSync, BatchOperation batchOperation) { 281 282 // Put the data in the contacts provider 283 final ContactOperations contactOp = ContactOperations.createNewContact( 284 context, rawContact.getServerContactId(), accountName, inSync, batchOperation); 285 286 contactOp.addName(rawContact.getFullName(), rawContact.getFirstName(), 287 rawContact.getLastName()) 288 .addEmail(rawContact.getEmail()) 289 .addPhone(rawContact.getCellPhone(), Phone.TYPE_MOBILE) 290 .addPhone(rawContact.getHomePhone(), Phone.TYPE_HOME) 291 .addPhone(rawContact.getOfficePhone(), Phone.TYPE_WORK) 292 .addGroupMembership(groupId) 293 .addAvatar(rawContact.getAvatarUrl()); 294 295 // If we have a serverId, then go ahead and create our status profile. 296 // Otherwise skip it - and we'll create it after we sync-up to the 297 // server later on. 298 if (rawContact.getServerContactId() > 0) { 299 contactOp.addProfileAction(rawContact.getServerContactId()); 300 } 301 } 302 303 /** 304 * Updates a single contact to the platform contacts provider. 305 * This method can be used to update a contact from a sync 306 * operation or as a result of a user editing a contact 307 * record. 308 * 309 * This operation is actually relatively complex. We query 310 * the database to find all the rows of info that already 311 * exist for this Contact. For rows that exist (and thus we're 312 * modifying existing fields), we create an update operation 313 * to change that field. But for fields we're adding, we create 314 * "add" operations to create new rows for those fields. 315 * 316 * @param context the Authenticator Activity context 317 * @param resolver the ContentResolver to use 318 * @param rawContact the sample SyncAdapter contact object 319 * @param updateStatus should we update this user's status 320 * @param updateAvatar should we update this user's avatar image 321 * @param inSync is the update part of a client-server sync? 322 * @param rawContactId the unique Id for this rawContact in contacts 323 * provider 324 * @param batchOperation allow us to batch together multiple operations 325 * into a single provider call 326 */ updateContact(Context context, ContentResolver resolver, RawContact rawContact, boolean updateServerId, boolean updateStatus, boolean updateAvatar, boolean inSync, long rawContactId, BatchOperation batchOperation)327 public static void updateContact(Context context, ContentResolver resolver, 328 RawContact rawContact, boolean updateServerId, boolean updateStatus, boolean updateAvatar, 329 boolean inSync, long rawContactId, BatchOperation batchOperation) { 330 331 boolean existingCellPhone = false; 332 boolean existingHomePhone = false; 333 boolean existingWorkPhone = false; 334 boolean existingEmail = false; 335 boolean existingAvatar = false; 336 337 final Cursor c = 338 resolver.query(DataQuery.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION, 339 new String[] {String.valueOf(rawContactId)}, null); 340 final ContactOperations contactOp = 341 ContactOperations.updateExistingContact(context, rawContactId, 342 inSync, batchOperation); 343 try { 344 // Iterate over the existing rows of data, and update each one 345 // with the information we received from the server. 346 while (c.moveToNext()) { 347 final long id = c.getLong(DataQuery.COLUMN_ID); 348 final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE); 349 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id); 350 if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 351 contactOp.updateName(uri, 352 c.getString(DataQuery.COLUMN_GIVEN_NAME), 353 c.getString(DataQuery.COLUMN_FAMILY_NAME), 354 c.getString(DataQuery.COLUMN_FULL_NAME), 355 rawContact.getFirstName(), 356 rawContact.getLastName(), 357 rawContact.getFullName()); 358 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 359 final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE); 360 if (type == Phone.TYPE_MOBILE) { 361 existingCellPhone = true; 362 contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER), 363 rawContact.getCellPhone(), uri); 364 } else if (type == Phone.TYPE_HOME) { 365 existingHomePhone = true; 366 contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER), 367 rawContact.getHomePhone(), uri); 368 } else if (type == Phone.TYPE_WORK) { 369 existingWorkPhone = true; 370 contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER), 371 rawContact.getOfficePhone(), uri); 372 } 373 } else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 374 existingEmail = true; 375 contactOp.updateEmail(rawContact.getEmail(), 376 c.getString(DataQuery.COLUMN_EMAIL_ADDRESS), uri); 377 } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 378 existingAvatar = true; 379 contactOp.updateAvatar(rawContact.getAvatarUrl(), uri); 380 } 381 } // while 382 } finally { 383 c.close(); 384 } 385 386 // Add the cell phone, if present and not updated above 387 if (!existingCellPhone) { 388 contactOp.addPhone(rawContact.getCellPhone(), Phone.TYPE_MOBILE); 389 } 390 // Add the home phone, if present and not updated above 391 if (!existingHomePhone) { 392 contactOp.addPhone(rawContact.getHomePhone(), Phone.TYPE_HOME); 393 } 394 395 // Add the work phone, if present and not updated above 396 if (!existingWorkPhone) { 397 contactOp.addPhone(rawContact.getOfficePhone(), Phone.TYPE_WORK); 398 } 399 // Add the email address, if present and not updated above 400 if (!existingEmail) { 401 contactOp.addEmail(rawContact.getEmail()); 402 } 403 // Add the avatar if we didn't update the existing avatar 404 if (!existingAvatar) { 405 contactOp.addAvatar(rawContact.getAvatarUrl()); 406 } 407 408 // If we need to update the serverId of the contact record, take 409 // care of that. This will happen if the contact is created on the 410 // client, and then synced to the server. When we get the updated 411 // record back from the server, we can set the SOURCE_ID property 412 // on the contact, so we can (in the future) lookup contacts by 413 // the serverId. 414 if (updateServerId) { 415 Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 416 contactOp.updateServerId(rawContact.getServerContactId(), uri); 417 } 418 419 // If we don't have a status profile, then create one. This could 420 // happen for contacts that were created on the client - we don't 421 // create the status profile until after the first sync... 422 final long serverId = rawContact.getServerContactId(); 423 final long profileId = lookupProfile(resolver, serverId); 424 if (profileId <= 0) { 425 contactOp.addProfileAction(serverId); 426 } 427 } 428 429 /** 430 * When we first add a sync adapter to the system, the contacts from that 431 * sync adapter will be hidden unless they're merged/grouped with an existing 432 * contact. But typically we want to actually show those contacts, so we 433 * need to mess with the Settings table to get them to show up. 434 * 435 * @param context the Authenticator Activity context 436 * @param account the Account who's visibility we're changing 437 * @param visible true if we want the contacts visible, false for hidden 438 */ setAccountContactsVisibility(Context context, Account account, boolean visible)439 public static void setAccountContactsVisibility(Context context, Account account, 440 boolean visible) { 441 ContentValues values = new ContentValues(); 442 values.put(RawContacts.ACCOUNT_NAME, account.name); 443 values.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE); 444 values.put(Settings.UNGROUPED_VISIBLE, visible ? 1 : 0); 445 446 context.getContentResolver().insert(Settings.CONTENT_URI, values); 447 } 448 449 /** 450 * Return a User object with data extracted from a contact stored 451 * in the local contacts database. 452 * 453 * Because a contact is actually stored over several rows in the 454 * database, our query will return those multiple rows of information. 455 * We then iterate over the rows and build the User structure from 456 * what we find. 457 * 458 * @param context the Authenticator Activity context 459 * @param rawContactId the unique ID for the local contact 460 * @return a User object containing info on that contact 461 */ getRawContact(Context context, long rawContactId)462 private static RawContact getRawContact(Context context, long rawContactId) { 463 String firstName = null; 464 String lastName = null; 465 String fullName = null; 466 String cellPhone = null; 467 String homePhone = null; 468 String workPhone = null; 469 String email = null; 470 long serverId = -1; 471 472 final ContentResolver resolver = context.getContentResolver(); 473 final Cursor c = 474 resolver.query(DataQuery.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION, 475 new String[] {String.valueOf(rawContactId)}, null); 476 try { 477 while (c.moveToNext()) { 478 final long id = c.getLong(DataQuery.COLUMN_ID); 479 final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE); 480 final long tempServerId = c.getLong(DataQuery.COLUMN_SERVER_ID); 481 if (tempServerId > 0) { 482 serverId = tempServerId; 483 } 484 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id); 485 if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { 486 lastName = c.getString(DataQuery.COLUMN_FAMILY_NAME); 487 firstName = c.getString(DataQuery.COLUMN_GIVEN_NAME); 488 fullName = c.getString(DataQuery.COLUMN_FULL_NAME); 489 } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { 490 final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE); 491 if (type == Phone.TYPE_MOBILE) { 492 cellPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER); 493 } else if (type == Phone.TYPE_HOME) { 494 homePhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER); 495 } else if (type == Phone.TYPE_WORK) { 496 workPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER); 497 } 498 } else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { 499 email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS); 500 } 501 } // while 502 } finally { 503 c.close(); 504 } 505 506 // Now that we've extracted all the information we care about, 507 // create the actual User object. 508 RawContact rawContact = RawContact.create(fullName, firstName, lastName, cellPhone, 509 workPhone, homePhone, email, null, false, rawContactId, serverId); 510 511 return rawContact; 512 } 513 514 /** 515 * Update the status message associated with the specified user. The status 516 * message would be something that is likely to be used by IM or social 517 * networking sync providers, and less by a straightforward contact provider. 518 * But it's a useful demo to see how it's done. 519 * 520 * @param context the Authenticator Activity context 521 * @param rawContact the contact whose status we should update 522 * @param batchOperation allow us to batch together multiple operations 523 */ updateContactStatus(Context context, RawContact rawContact, BatchOperation batchOperation)524 private static void updateContactStatus(Context context, RawContact rawContact, 525 BatchOperation batchOperation) { 526 final ContentValues values = new ContentValues(); 527 final ContentResolver resolver = context.getContentResolver(); 528 529 final long userId = rawContact.getServerContactId(); 530 final String username = rawContact.getUserName(); 531 final String status = rawContact.getStatus(); 532 533 // Look up the user's sample SyncAdapter data row 534 final long profileId = lookupProfile(resolver, userId); 535 536 // Insert the activity into the stream 537 if (profileId > 0) { 538 values.put(StatusUpdates.DATA_ID, profileId); 539 values.put(StatusUpdates.STATUS, status); 540 values.put(StatusUpdates.PROTOCOL, Im.PROTOCOL_CUSTOM); 541 values.put(StatusUpdates.CUSTOM_PROTOCOL, CUSTOM_IM_PROTOCOL); 542 values.put(StatusUpdates.IM_ACCOUNT, username); 543 values.put(StatusUpdates.IM_HANDLE, userId); 544 values.put(StatusUpdates.STATUS_RES_PACKAGE, context.getPackageName()); 545 values.put(StatusUpdates.STATUS_ICON, R.drawable.icon); 546 values.put(StatusUpdates.STATUS_LABEL, R.string.label); 547 batchOperation.add(ContactOperations.newInsertCpo(StatusUpdates.CONTENT_URI, 548 false, true).withValues(values).build()); 549 } 550 } 551 552 /** 553 * Clear the local system 'dirty' flag for a contact. 554 * 555 * @param context the Authenticator Activity context 556 * @param rawContactId the id of the contact update 557 * @param batchOperation allow us to batch together multiple operations 558 */ clearDirtyFlag(Context context, long rawContactId, BatchOperation batchOperation)559 private static void clearDirtyFlag(Context context, long rawContactId, 560 BatchOperation batchOperation) { 561 final ContactOperations contactOp = 562 ContactOperations.updateExistingContact(context, rawContactId, true, 563 batchOperation); 564 565 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 566 contactOp.updateDirtyFlag(false, uri); 567 } 568 569 /** 570 * Deletes a contact from the platform contacts provider. This method is used 571 * both for contacts that were deleted locally and then that deletion was synced 572 * to the server, and for contacts that were deleted on the server and the 573 * deletion was synced to the client. 574 * 575 * @param context the Authenticator Activity context 576 * @param rawContactId the unique Id for this rawContact in contacts 577 * provider 578 */ deleteContact(Context context, long rawContactId, BatchOperation batchOperation)579 private static void deleteContact(Context context, long rawContactId, 580 BatchOperation batchOperation) { 581 582 batchOperation.add(ContactOperations.newDeleteCpo( 583 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), 584 true, true).build()); 585 } 586 587 /** 588 * Returns the RawContact id for a sample SyncAdapter contact, or 0 if the 589 * sample SyncAdapter user isn't found. 590 * 591 * @param resolver the content resolver to use 592 * @param serverContactId the sample SyncAdapter user ID to lookup 593 * @return the RawContact id, or 0 if not found 594 */ lookupRawContact(ContentResolver resolver, long serverContactId)595 private static long lookupRawContact(ContentResolver resolver, long serverContactId) { 596 597 long rawContactId = 0; 598 final Cursor c = resolver.query( 599 UserIdQuery.CONTENT_URI, 600 UserIdQuery.PROJECTION, 601 UserIdQuery.SELECTION, 602 new String[] {String.valueOf(serverContactId)}, 603 null); 604 try { 605 if ((c != null) && c.moveToFirst()) { 606 rawContactId = c.getLong(UserIdQuery.COLUMN_RAW_CONTACT_ID); 607 } 608 } finally { 609 if (c != null) { 610 c.close(); 611 } 612 } 613 return rawContactId; 614 } 615 616 /** 617 * Returns the Data id for a sample SyncAdapter contact's profile row, or 0 618 * if the sample SyncAdapter user isn't found. 619 * 620 * @param resolver a content resolver 621 * @param userId the sample SyncAdapter user ID to lookup 622 * @return the profile Data row id, or 0 if not found 623 */ lookupProfile(ContentResolver resolver, long userId)624 private static long lookupProfile(ContentResolver resolver, long userId) { 625 626 long profileId = 0; 627 final Cursor c = 628 resolver.query(Data.CONTENT_URI, ProfileQuery.PROJECTION, ProfileQuery.SELECTION, 629 new String[] {String.valueOf(userId)}, null); 630 try { 631 if ((c != null) && c.moveToFirst()) { 632 profileId = c.getLong(ProfileQuery.COLUMN_ID); 633 } 634 } finally { 635 if (c != null) { 636 c.close(); 637 } 638 } 639 return profileId; 640 } 641 642 final public static class EditorQuery { 643 EditorQuery()644 private EditorQuery() { 645 } 646 647 public static final String[] PROJECTION = new String[] { 648 RawContacts.ACCOUNT_NAME, 649 Data._ID, 650 RawContacts.Entity.DATA_ID, 651 Data.MIMETYPE, 652 Data.DATA1, 653 Data.DATA2, 654 Data.DATA3, 655 Data.DATA15, 656 Data.SYNC1 657 }; 658 659 public static final int COLUMN_ACCOUNT_NAME = 0; 660 public static final int COLUMN_RAW_CONTACT_ID = 1; 661 public static final int COLUMN_DATA_ID = 2; 662 public static final int COLUMN_MIMETYPE = 3; 663 public static final int COLUMN_DATA1 = 4; 664 public static final int COLUMN_DATA2 = 5; 665 public static final int COLUMN_DATA3 = 6; 666 public static final int COLUMN_DATA15 = 7; 667 public static final int COLUMN_SYNC1 = 8; 668 669 public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1; 670 public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2; 671 public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1; 672 public static final int COLUMN_EMAIL_TYPE = COLUMN_DATA2; 673 public static final int COLUMN_FULL_NAME = COLUMN_DATA1; 674 public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2; 675 public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3; 676 public static final int COLUMN_AVATAR_IMAGE = COLUMN_DATA15; 677 public static final int COLUMN_SYNC_DIRTY = COLUMN_SYNC1; 678 679 public static final String SELECTION = Data.RAW_CONTACT_ID + "=?"; 680 } 681 682 /** 683 * Constants for a query to find a contact given a sample SyncAdapter user 684 * ID. 685 */ 686 final private static class ProfileQuery { 687 ProfileQuery()688 private ProfileQuery() { 689 } 690 691 public final static String[] PROJECTION = new String[] {Data._ID}; 692 693 public final static int COLUMN_ID = 0; 694 695 public static final String SELECTION = 696 Data.MIMETYPE + "='" + SampleSyncAdapterColumns.MIME_PROFILE + "' AND " 697 + SampleSyncAdapterColumns.DATA_PID + "=?"; 698 } 699 700 /** 701 * Constants for a query to find a contact given a sample SyncAdapter user 702 * ID. 703 */ 704 final private static class UserIdQuery { 705 UserIdQuery()706 private UserIdQuery() { 707 } 708 709 public final static String[] PROJECTION = new String[] { 710 RawContacts._ID, 711 RawContacts.CONTACT_ID 712 }; 713 714 public final static int COLUMN_RAW_CONTACT_ID = 0; 715 public final static int COLUMN_LINKED_CONTACT_ID = 1; 716 717 public final static Uri CONTENT_URI = RawContacts.CONTENT_URI; 718 719 public static final String SELECTION = 720 RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND " 721 + RawContacts.SOURCE_ID + "=?"; 722 } 723 724 /** 725 * Constants for a query to find SampleSyncAdapter contacts that are 726 * in need of syncing to the server. This should cover new, edited, 727 * and deleted contacts. 728 */ 729 final private static class DirtyQuery { 730 DirtyQuery()731 private DirtyQuery() { 732 } 733 734 public final static String[] PROJECTION = new String[] { 735 RawContacts._ID, 736 RawContacts.SOURCE_ID, 737 RawContacts.DIRTY, 738 RawContacts.DELETED, 739 RawContacts.VERSION 740 }; 741 742 public final static int COLUMN_RAW_CONTACT_ID = 0; 743 public final static int COLUMN_SERVER_ID = 1; 744 public final static int COLUMN_DIRTY = 2; 745 public final static int COLUMN_DELETED = 3; 746 public final static int COLUMN_VERSION = 4; 747 748 public static final Uri CONTENT_URI = RawContacts.CONTENT_URI.buildUpon() 749 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 750 .build(); 751 752 public static final String SELECTION = 753 RawContacts.DIRTY + "=1 AND " 754 + RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND " 755 + RawContacts.ACCOUNT_NAME + "=?"; 756 } 757 758 /** 759 * Constants for a query to get contact data for a given rawContactId 760 */ 761 final private static class DataQuery { 762 DataQuery()763 private DataQuery() { 764 } 765 766 public static final String[] PROJECTION = 767 new String[] {Data._ID, RawContacts.SOURCE_ID, Data.MIMETYPE, Data.DATA1, 768 Data.DATA2, Data.DATA3, Data.DATA15, Data.SYNC1}; 769 770 public static final int COLUMN_ID = 0; 771 public static final int COLUMN_SERVER_ID = 1; 772 public static final int COLUMN_MIMETYPE = 2; 773 public static final int COLUMN_DATA1 = 3; 774 public static final int COLUMN_DATA2 = 4; 775 public static final int COLUMN_DATA3 = 5; 776 public static final int COLUMN_DATA15 = 6; 777 public static final int COLUMN_SYNC1 = 7; 778 779 public static final Uri CONTENT_URI = Data.CONTENT_URI; 780 781 public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1; 782 public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2; 783 public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1; 784 public static final int COLUMN_EMAIL_TYPE = COLUMN_DATA2; 785 public static final int COLUMN_FULL_NAME = COLUMN_DATA1; 786 public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2; 787 public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3; 788 public static final int COLUMN_AVATAR_IMAGE = COLUMN_DATA15; 789 public static final int COLUMN_SYNC_DIRTY = COLUMN_SYNC1; 790 791 public static final String SELECTION = Data.RAW_CONTACT_ID + "=?"; 792 } 793 794 /** 795 * Constants for a query to read basic contact columns 796 */ 797 final public static class ContactQuery { ContactQuery()798 private ContactQuery() { 799 } 800 801 public static final String[] PROJECTION = 802 new String[] {Contacts._ID, Contacts.DISPLAY_NAME}; 803 804 public static final int COLUMN_ID = 0; 805 public static final int COLUMN_DISPLAY_NAME = 1; 806 } 807 } 808