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.NetworkUtilities; 21 22 import android.content.ContentProviderOperation; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.net.Uri; 26 import android.provider.ContactsContract; 27 import android.provider.ContactsContract.CommonDataKinds.Email; 28 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 29 import android.provider.ContactsContract.CommonDataKinds.Phone; 30 import android.provider.ContactsContract.CommonDataKinds.Photo; 31 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 32 import android.provider.ContactsContract.Data; 33 import android.provider.ContactsContract.RawContacts; 34 import android.text.TextUtils; 35 36 /** 37 * Helper class for storing data in the platform content providers. 38 */ 39 public class ContactOperations { 40 private final ContentValues mValues; 41 private final BatchOperation mBatchOperation; 42 private final Context mContext; 43 private boolean mIsSyncOperation; 44 private long mRawContactId; 45 private int mBackReference; 46 private boolean mIsNewContact; 47 48 /** 49 * Since we're sending a lot of contact provider operations in a single 50 * batched operation, we want to make sure that we "yield" periodically 51 * so that the Contact Provider can write changes to the DB, and can 52 * open a new transaction. This prevents ANR (application not responding) 53 * errors. The recommended time to specify that a yield is permitted is 54 * with the first operation on a particular contact. So if we're updating 55 * multiple fields for a single contact, we make sure that we call 56 * withYieldAllowed(true) on the first field that we update. We use 57 * mIsYieldAllowed to keep track of what value we should pass to 58 * withYieldAllowed(). 59 */ 60 private boolean mIsYieldAllowed; 61 62 /** 63 * Returns an instance of ContactOperations instance for adding new contact 64 * to the platform contacts provider. 65 * 66 * @param context the Authenticator Activity context 67 * @param userId the userId of the sample SyncAdapter user object 68 * @param accountName the username for the SyncAdapter account 69 * @param isSyncOperation are we executing this as part of a sync operation? 70 * @return instance of ContactOperations 71 */ 72 public static ContactOperations createNewContact(Context context, long userId, 73 String accountName, boolean isSyncOperation, BatchOperation batchOperation) { 74 return new ContactOperations(context, userId, accountName, isSyncOperation, batchOperation); 75 } 76 77 /** 78 * Returns an instance of ContactOperations for updating existing contact in 79 * the platform contacts provider. 80 * 81 * @param context the Authenticator Activity context 82 * @param rawContactId the unique Id of the existing rawContact 83 * @param isSyncOperation are we executing this as part of a sync operation? 84 * @return instance of ContactOperations 85 */ 86 public static ContactOperations updateExistingContact(Context context, long rawContactId, 87 boolean isSyncOperation, BatchOperation batchOperation) { 88 return new ContactOperations(context, rawContactId, isSyncOperation, batchOperation); 89 } 90 91 public ContactOperations(Context context, boolean isSyncOperation, 92 BatchOperation batchOperation) { 93 mValues = new ContentValues(); 94 mIsYieldAllowed = true; 95 mIsSyncOperation = isSyncOperation; 96 mContext = context; 97 mBatchOperation = batchOperation; 98 } 99 100 public ContactOperations(Context context, long userId, String accountName, 101 boolean isSyncOperation, BatchOperation batchOperation) { 102 this(context, isSyncOperation, batchOperation); 103 mBackReference = mBatchOperation.size(); 104 mIsNewContact = true; 105 mValues.put(RawContacts.SOURCE_ID, userId); 106 mValues.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE); 107 mValues.put(RawContacts.ACCOUNT_NAME, accountName); 108 ContentProviderOperation.Builder builder = 109 newInsertCpo(RawContacts.CONTENT_URI, mIsSyncOperation, true).withValues(mValues); 110 mBatchOperation.add(builder.build()); 111 } 112 113 public ContactOperations(Context context, long rawContactId, boolean isSyncOperation, 114 BatchOperation batchOperation) { 115 this(context, isSyncOperation, batchOperation); 116 mIsNewContact = false; 117 mRawContactId = rawContactId; 118 } 119 120 /** 121 * Adds a contact name. We can take either a full name ("Bob Smith") or separated 122 * first-name and last-name ("Bob" and "Smith"). 123 * 124 * @param fullName The full name of the contact - typically from an edit form 125 * Can be null if firstName/lastName are specified. 126 * @param firstName The first name of the contact - can be null if fullName 127 * is specified. 128 * @param lastName The last name of the contact - can be null if fullName 129 * is specified. 130 * @return instance of ContactOperations 131 */ 132 public ContactOperations addName(String fullName, String firstName, String lastName) { 133 mValues.clear(); 134 135 if (!TextUtils.isEmpty(fullName)) { 136 mValues.put(StructuredName.DISPLAY_NAME, fullName); 137 mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 138 } else { 139 if (!TextUtils.isEmpty(firstName)) { 140 mValues.put(StructuredName.GIVEN_NAME, firstName); 141 mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 142 } 143 if (!TextUtils.isEmpty(lastName)) { 144 mValues.put(StructuredName.FAMILY_NAME, lastName); 145 mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 146 } 147 } 148 if (mValues.size() > 0) { 149 addInsertOp(); 150 } 151 return this; 152 } 153 154 /** 155 * Adds an email 156 * 157 * @param the email address we're adding 158 * @return instance of ContactOperations 159 */ 160 public ContactOperations addEmail(String email) { 161 mValues.clear(); 162 if (!TextUtils.isEmpty(email)) { 163 mValues.put(Email.DATA, email); 164 mValues.put(Email.TYPE, Email.TYPE_OTHER); 165 mValues.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE); 166 addInsertOp(); 167 } 168 return this; 169 } 170 171 /** 172 * Adds a phone number 173 * 174 * @param phone new phone number for the contact 175 * @param phoneType the type: cell, home, etc. 176 * @return instance of ContactOperations 177 */ 178 public ContactOperations addPhone(String phone, int phoneType) { 179 mValues.clear(); 180 if (!TextUtils.isEmpty(phone)) { 181 mValues.put(Phone.NUMBER, phone); 182 mValues.put(Phone.TYPE, phoneType); 183 mValues.put(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 184 addInsertOp(); 185 } 186 return this; 187 } 188 189 /** 190 * Adds a group membership 191 * 192 * @param id The id of the group to assign 193 * @return instance of ContactOperations 194 */ 195 public ContactOperations addGroupMembership(long groupId) { 196 mValues.clear(); 197 mValues.put(GroupMembership.GROUP_ROW_ID, groupId); 198 mValues.put(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 199 addInsertOp(); 200 return this; 201 } 202 203 public ContactOperations addAvatar(String avatarUrl) { 204 if (avatarUrl != null) { 205 byte[] avatarBuffer = NetworkUtilities.downloadAvatar(avatarUrl); 206 if (avatarBuffer != null) { 207 mValues.clear(); 208 mValues.put(Photo.PHOTO, avatarBuffer); 209 mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 210 addInsertOp(); 211 } 212 } 213 return this; 214 } 215 216 /** 217 * Adds a profile action 218 * 219 * @param userId the userId of the sample SyncAdapter user object 220 * @return instance of ContactOperations 221 */ 222 public ContactOperations addProfileAction(long userId) { 223 mValues.clear(); 224 if (userId != 0) { 225 mValues.put(SampleSyncAdapterColumns.DATA_PID, userId); 226 mValues.put(SampleSyncAdapterColumns.DATA_SUMMARY, mContext 227 .getString(R.string.profile_action)); 228 mValues.put(SampleSyncAdapterColumns.DATA_DETAIL, mContext 229 .getString(R.string.view_profile)); 230 mValues.put(Data.MIMETYPE, SampleSyncAdapterColumns.MIME_PROFILE); 231 addInsertOp(); 232 } 233 return this; 234 } 235 236 /** 237 * Updates contact's serverId 238 * 239 * @param serverId the serverId for this contact 240 * @param uri Uri for the existing raw contact to be updated 241 * @return instance of ContactOperations 242 */ 243 public ContactOperations updateServerId(long serverId, Uri uri) { 244 mValues.clear(); 245 mValues.put(RawContacts.SOURCE_ID, serverId); 246 addUpdateOp(uri); 247 return this; 248 } 249 250 /** 251 * Updates contact's email 252 * 253 * @param email email id of the sample SyncAdapter user 254 * @param uri Uri for the existing raw contact to be updated 255 * @return instance of ContactOperations 256 */ 257 public ContactOperations updateEmail(String email, String existingEmail, Uri uri) { 258 if (!TextUtils.equals(existingEmail, email)) { 259 mValues.clear(); 260 mValues.put(Email.DATA, email); 261 addUpdateOp(uri); 262 } 263 return this; 264 } 265 266 /** 267 * Updates contact's name. The caller can either provide first-name 268 * and last-name fields or a full-name field. 269 * 270 * @param uri Uri for the existing raw contact to be updated 271 * @param existingFirstName the first name stored in provider 272 * @param existingLastName the last name stored in provider 273 * @param existingFullName the full name stored in provider 274 * @param firstName the new first name to store 275 * @param lastName the new last name to store 276 * @param fullName the new full name to store 277 * @return instance of ContactOperations 278 */ 279 public ContactOperations updateName(Uri uri, 280 String existingFirstName, 281 String existingLastName, 282 String existingFullName, 283 String firstName, 284 String lastName, 285 String fullName) { 286 287 mValues.clear(); 288 if (TextUtils.isEmpty(fullName)) { 289 if (!TextUtils.equals(existingFirstName, firstName)) { 290 mValues.put(StructuredName.GIVEN_NAME, firstName); 291 } 292 if (!TextUtils.equals(existingLastName, lastName)) { 293 mValues.put(StructuredName.FAMILY_NAME, lastName); 294 } 295 } else { 296 if (!TextUtils.equals(existingFullName, fullName)) { 297 mValues.put(StructuredName.DISPLAY_NAME, fullName); 298 } 299 } 300 if (mValues.size() > 0) { 301 addUpdateOp(uri); 302 } 303 return this; 304 } 305 306 public ContactOperations updateDirtyFlag(boolean isDirty, Uri uri) { 307 int isDirtyValue = isDirty ? 1 : 0; 308 mValues.clear(); 309 mValues.put(RawContacts.DIRTY, isDirtyValue); 310 addUpdateOp(uri); 311 return this; 312 } 313 314 /** 315 * Updates contact's phone 316 * 317 * @param existingNumber phone number stored in contacts provider 318 * @param phone new phone number for the contact 319 * @param uri Uri for the existing raw contact to be updated 320 * @return instance of ContactOperations 321 */ 322 public ContactOperations updatePhone(String existingNumber, String phone, Uri uri) { 323 if (!TextUtils.equals(phone, existingNumber)) { 324 mValues.clear(); 325 mValues.put(Phone.NUMBER, phone); 326 addUpdateOp(uri); 327 } 328 return this; 329 } 330 331 public ContactOperations updateAvatar(String avatarUrl, Uri uri) { 332 if (avatarUrl != null) { 333 byte[] avatarBuffer = NetworkUtilities.downloadAvatar(avatarUrl); 334 if (avatarBuffer != null) { 335 mValues.clear(); 336 mValues.put(Photo.PHOTO, avatarBuffer); 337 mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 338 addUpdateOp(uri); 339 } 340 } 341 return this; 342 } 343 344 /** 345 * Updates contact's profile action 346 * 347 * @param userId sample SyncAdapter user id 348 * @param uri Uri for the existing raw contact to be updated 349 * @return instance of ContactOperations 350 */ 351 public ContactOperations updateProfileAction(Integer userId, Uri uri) { 352 mValues.clear(); 353 mValues.put(SampleSyncAdapterColumns.DATA_PID, userId); 354 addUpdateOp(uri); 355 return this; 356 } 357 358 /** 359 * Adds an insert operation into the batch 360 */ 361 private void addInsertOp() { 362 363 if (!mIsNewContact) { 364 mValues.put(Phone.RAW_CONTACT_ID, mRawContactId); 365 } 366 ContentProviderOperation.Builder builder = 367 newInsertCpo(Data.CONTENT_URI, mIsSyncOperation, mIsYieldAllowed); 368 builder.withValues(mValues); 369 if (mIsNewContact) { 370 builder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference); 371 } 372 mIsYieldAllowed = false; 373 mBatchOperation.add(builder.build()); 374 } 375 376 /** 377 * Adds an update operation into the batch 378 */ 379 private void addUpdateOp(Uri uri) { 380 ContentProviderOperation.Builder builder = 381 newUpdateCpo(uri, mIsSyncOperation, mIsYieldAllowed).withValues(mValues); 382 mIsYieldAllowed = false; 383 mBatchOperation.add(builder.build()); 384 } 385 386 public static ContentProviderOperation.Builder newInsertCpo(Uri uri, 387 boolean isSyncOperation, boolean isYieldAllowed) { 388 return ContentProviderOperation 389 .newInsert(addCallerIsSyncAdapterParameter(uri, isSyncOperation)) 390 .withYieldAllowed(isYieldAllowed); 391 } 392 393 public static ContentProviderOperation.Builder newUpdateCpo(Uri uri, 394 boolean isSyncOperation, boolean isYieldAllowed) { 395 return ContentProviderOperation 396 .newUpdate(addCallerIsSyncAdapterParameter(uri, isSyncOperation)) 397 .withYieldAllowed(isYieldAllowed); 398 } 399 400 public static ContentProviderOperation.Builder newDeleteCpo(Uri uri, 401 boolean isSyncOperation, boolean isYieldAllowed) { 402 return ContentProviderOperation 403 .newDelete(addCallerIsSyncAdapterParameter(uri, isSyncOperation)) 404 .withYieldAllowed(isYieldAllowed); 405 } 406 407 private static Uri addCallerIsSyncAdapterParameter(Uri uri, boolean isSyncOperation) { 408 if (isSyncOperation) { 409 // If we're in the middle of a real sync-adapter operation, then go ahead 410 // and tell the Contacts provider that we're the sync adapter. That 411 // gives us some special permissions - like the ability to really 412 // delete a contact, and the ability to clear the dirty flag. 413 // 414 // If we're not in the middle of a sync operation (for example, we just 415 // locally created/edited a new contact), then we don't want to use 416 // the special permissions, and the system will automagically mark 417 // the contact as 'dirty' for us! 418 return uri.buildUpon() 419 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 420 .build(); 421 } 422 return uri; 423 } 424 } 425