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