• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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