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 */ createNewContact(Context context, long userId, String accountName, boolean isSyncOperation, BatchOperation batchOperation)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 */ updateExistingContact(Context context, long rawContactId, boolean isSyncOperation, BatchOperation batchOperation)86 public static ContactOperations updateExistingContact(Context context, long rawContactId, 87 boolean isSyncOperation, BatchOperation batchOperation) { 88 return new ContactOperations(context, rawContactId, isSyncOperation, batchOperation); 89 } 90 ContactOperations(Context context, boolean isSyncOperation, BatchOperation batchOperation)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 ContactOperations(Context context, long userId, String accountName, boolean isSyncOperation, BatchOperation batchOperation)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 ContactOperations(Context context, long rawContactId, boolean isSyncOperation, BatchOperation batchOperation)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 */ addName(String fullName, String firstName, String lastName)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 */ addEmail(String email)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 */ addPhone(String phone, int phoneType)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 */ addGroupMembership(long groupId)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 addAvatar(String avatarUrl)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 */ addProfileAction(long userId)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 */ updateServerId(long serverId, Uri uri)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 */ updateEmail(String email, String existingEmail, Uri uri)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 */ updateName(Uri uri, String existingFirstName, String existingLastName, String existingFullName, String firstName, String lastName, String fullName)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 updateDirtyFlag(boolean isDirty, Uri uri)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 */ updatePhone(String existingNumber, String phone, Uri uri)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 updateAvatar(String avatarUrl, Uri uri)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 */ updateProfileAction(Integer userId, Uri uri)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 */ addInsertOp()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 */ addUpdateOp(Uri uri)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 newInsertCpo(Uri uri, boolean isSyncOperation, boolean isYieldAllowed)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 newUpdateCpo(Uri uri, boolean isSyncOperation, boolean isYieldAllowed)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 newDeleteCpo(Uri uri, boolean isSyncOperation, boolean isYieldAllowed)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 addCallerIsSyncAdapterParameter(Uri uri, boolean isSyncOperation)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