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