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