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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 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 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 { 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