Home | History | Annotate | Download | only in platform
      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 
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 {
    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