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 ensureSampleGroupExists(Context context, Account account)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 */ updateContacts(Context context, String account, List<RawContact> rawContacts, long groupId, long lastSyncMarker)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 */ getDirtyContacts(Context context, Account account)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 */ updateStatusMessages(Context context, List<RawContact> rawContacts)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 */ addStreamItems(Context context, List<RawContact> rawContacts, String accountName, String accountType)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 */ clearSyncFlags(Context context, List<RawContact> dirtyContacts)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 */ addContact(Context context, String accountName, RawContact rawContact, long groupId, boolean inSync, BatchOperation batchOperation)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 */ updateContact(Context context, ContentResolver resolver, RawContact rawContact, boolean updateServerId, boolean updateStatus, boolean updateAvatar, boolean inSync, long rawContactId, BatchOperation batchOperation)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 */ setAccountContactsVisibility(Context context, Account account, boolean visible)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 */ getRawContact(Context context, long rawContactId)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 */ updateContactStatus(Context context, RawContact rawContact, BatchOperation batchOperation)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 */ addContactStreamItem(Context context, long rawContactId, String accountName, String accountType, String text, String comments, BatchOperation batchOperation)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 addStreamItemPhoto(Context context, ContentResolver resolver, long streamItemId, String accountName, String accountType, BatchOperation batchOperation)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 */ clearDirtyFlag(Context context, long rawContactId, BatchOperation batchOperation)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 */ deleteContact(Context context, long rawContactId, BatchOperation batchOperation)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 */ lookupRawContact(ContentResolver resolver, long serverContactId)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 */ lookupProfile(ContentResolver resolver, long userId)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 EditorQuery()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 ProfileQuery()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 UserIdQuery()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 DirtyQuery()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 DataQuery()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 { 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