com.rukman.emde.smsgroups.syncadapter.SyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.rukman.emde.smsgroups.syncadapter.SyncAdapter.java

Source

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.rukman.emde.smsgroups.syncadapter;

import java.io.IOException;
import java.util.ArrayList;

import junit.framework.Assert;

import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.rukman.emde.smsgroups.GMSApplication;
import com.rukman.emde.smsgroups.client.JSONKeys;
import com.rukman.emde.smsgroups.client.NetworkUtilities;
import com.rukman.emde.smsgroups.data.GMSContacts;
import com.rukman.emde.smsgroups.data.GMSContacts.GMSContact;
import com.rukman.emde.smsgroups.data.GMSGroups;
import com.rukman.emde.smsgroups.data.GMSGroups.GMSGroup;
import com.rukman.emde.smsgroups.data.GMSSyncs;
import com.rukman.emde.smsgroups.data.GMSSyncs.GMSSync;
import com.rukman.emde.smsgroups.platform.GMSContactOperations;

/**
 * SyncAdapter implementation for syncing sample SyncAdapter contacts to the
 * platform ContactOperations provider.  This sample shows a basic 2-way
 * sync between the client and a sample server.  It also contains an
 * example of how to update the contacts' status messages, which
 * would be useful for a messaging or social networking client.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {

    private static final String TAG = "SyncAdapter";
    private static final boolean NOTIFY_AUTH_FAILURE = true;
    public static final String SAMPLE_GROUP_NAME = "Sample Group";

    private final AccountManager mAccountManager;

    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        mAccountManager = AccountManager.get(context);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
            SyncResult syncResult) {
        try {
            // Use the account manager to request the AuthToken we'll need
            // to talk to our sample server.  If we don't have an AuthToken
            // yet, this could involve a round-trip to the server to request
            // and AuthToken.
            if (!shouldSyncNow(account, extras, provider, syncResult)) {
                Log.i(TAG, "Syncing despite shouldSyncNow()");
            }
            final String authToken = mAccountManager.blockingGetAuthToken(account, GMSApplication.AUTHTOKEN_TYPE,
                    NOTIFY_AUTH_FAILURE);
            processDeletedContacts(provider, authToken, account, syncResult);
            processDeletedGroups(provider, authToken, account, syncResult);
            syncGroupsWithServer(provider, authToken, account, syncResult);
            updateSyncRecord(provider);

        } catch (final AuthenticatorException e) {
            Log.e(TAG, "AuthenticatorException", e);
            syncResult.stats.numParseExceptions++;
        } catch (final OperationCanceledException e) {
            Log.e(TAG, "OperationCanceledExcetpion", e);
        } catch (final IOException e) {
            Log.e(TAG, "IOException", e);
            syncResult.stats.numIoExceptions++;
        } catch (final ParseException e) {
            Log.e(TAG, "ParseException", e);
            syncResult.stats.numParseExceptions++;
        } catch (JSONException e) {
            Log.e(TAG, "JSONException", e);
            syncResult.stats.numParseExceptions++;
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException", e);
            syncResult.stats.numIoExceptions++;
        } catch (OperationApplicationException e) {
            syncResult.stats.numIoExceptions++;
            e.printStackTrace();
        }
    }

    private ContentProviderResult[] optimisticallyCreateGroupAndContacts(JSONObject group,
            ContentProviderClient provider, ContentProviderClient contactsProvider, String authToken,
            Account account, SyncResult syncResult)
            throws JSONException, RemoteException, OperationApplicationException {

        String groupCloudId = group.getString(JSONKeys.KEY_ID);
        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
        // If the first operation asserts, that means the group already exists
        // Operation 0
        ContentProviderOperation.Builder op = ContentProviderOperation.newAssertQuery(GMSGroups.CONTENT_URI)
                .withValue(GMSGroup._ID, "").withValue(GMSGroup.CLOUD_ID, "")
                .withSelection(GMSGroup.CLOUD_ID + "=?", new String[] { groupCloudId }).withExpectedCount(0);
        ops.add(op.build());
        // If we get this far, create the group from the information the JSON object
        // Operation 1
        ContentValues groupValues = GMSApplication.getGroupValues(group);
        op = ContentProviderOperation.newInsert(GMSGroups.CONTENT_URI).withValues(groupValues)
                .withValue(GMSGroup.STATUS, GMSGroup.STATUS_SYNCED);
        ops.add(op.build());
        // And add the contacts
        // Operations 2 - N + 2 where N is the number of members in group
        if (group.has(JSONKeys.KEY_MEMBERS)) {
            JSONArray membersArray = group.getJSONArray(JSONKeys.KEY_MEMBERS);
            int numMembers = membersArray.length();
            for (int j = 0; j < numMembers; ++j) {
                JSONObject member = membersArray.getJSONObject(j);
                ContentValues memberValues = GMSApplication.getMemberValues(member);
                op = ContentProviderOperation.newInsert(GMSContacts.CONTENT_URI).withValues(memberValues)
                        .withValueBackReference(GMSContact.GROUP_ID, 1)
                        .withValue(GMSContact.STATUS, GMSContact.STATUS_SYNCED);
                ops.add(op.build());
            }
        }
        ContentProviderResult[] results = provider.applyBatch(ops);
        // Create the contact on the device
        Uri groupUri = results[1].uri;
        if (groupUri != null) {
            Cursor cursor = null;
            try {
                cursor = GMSContactOperations.findGroupInContacts(contactsProvider, account, groupCloudId);
                if (cursor.getCount() > 0) {
                    Assert.assertTrue(cursor.moveToFirst());
                    long oldContactId = cursor.getLong(0);
                    GMSContactOperations.removeGroupFromContacts(contactsProvider, account, oldContactId,
                            syncResult);
                }
                long contactId = GMSContactOperations.addGroupToContacts(getContext(), contactsProvider, account,
                        group, syncResult);
                if (contactId > 0) {
                    ContentValues values = new ContentValues();
                    values.put(GMSGroup.RAW_CONTACT_ID, contactId);
                    provider.update(groupUri, values, null, null);
                    addNewGroupNotification(group);
                }
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }
        return results;
    }

    private void addNewGroupNotification(JSONObject group) {
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getContext());
        mBuilder.setSmallIcon(com.rukman.emde.smsgroups.R.drawable.ic_launcher)
                .setContentTitle("TODO: New group created")
                .setContentText("TODO: Fix this text. A new group was created");
    }

    private ContentProviderResult[] optimisticallyUpdateGroup(JSONObject group, ContentProviderClient provider,
            ContentProviderClient contactsProvider, String authToken, Account account, SyncResult syncResult)
            throws JSONException, RemoteException {

        String groupCloudId = null;
        String version = null;
        try {
            groupCloudId = group.getString(JSONKeys.KEY_ID);
            version = group.getString(JSONKeys.KEY_VERSION);

            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
            // Operation 0 - we believe the record exists, but we'll check, inside a transaction, to make sure
            ContentProviderOperation op;
            op = ContentProviderOperation.newAssertQuery(GMSGroups.CONTENT_URI)
                    .withSelection(GMSGroup.CLOUD_ID + "=?", new String[] { groupCloudId }).withExpectedCount(1)
                    .build();
            ops.add(op);
            // Operation 1 - we know it exists. If its the right version, we don't need to do the update
            // So we assert that we'll find zero records with the current version and if that's right, we'll update our
            // record, including the version with the new record data
            op = ContentProviderOperation.newAssertQuery(GMSGroups.CONTENT_URI)
                    .withSelection(GMSGroup.CLOUD_ID + "=? AND " + GMSGroup.VERSION + "=?",
                            new String[] { groupCloudId, version })
                    .withExpectedCount(0).build();
            ops.add(op);
            // If we get this far, update the existing group from the information in the JSON object
            // Operation 2
            ContentValues groupValues = GMSApplication.getGroupValues(group);
            op = ContentProviderOperation.newUpdate(GMSGroups.CONTENT_URI)
                    .withSelection(GMSGroup.CLOUD_ID + "=?", new String[] { groupCloudId }).withValues(groupValues)
                    .withValue(GMSGroup.STATUS, GMSGroup.STATUS_SYNCED).withExpectedCount(1).build();
            ops.add(op);
            return provider.applyBatch(ops);
        } catch (OperationApplicationException e) {
            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
            ContentProviderOperation op;
            // Operation 0 - we know it exists. If its the right version, we don't need to do the update
            // So we assert that we'll find zero records with the current version and if that's right, we'll update our
            // record, including the version with the new record data
            op = ContentProviderOperation.newAssertQuery(GMSGroups.CONTENT_URI)
                    .withSelection(GMSGroup.CLOUD_ID + "=? AND " + GMSGroup.VERSION + "=?",
                            new String[] { groupCloudId, version })
                    .withExpectedCount(1).build();
            ops.add(op);
            // If we get this far we only need to update the is_synced field in the database
            // Operation 1
            op = ContentProviderOperation.newUpdate(GMSGroups.CONTENT_URI)
                    .withSelection(GMSGroup.CLOUD_ID + "=?", new String[] { groupCloudId })
                    .withValue(GMSGroup.STATUS, GMSGroup.STATUS_SYNCED).withExpectedCount(1).build();
            ops.add(op);
            try {
                return provider.applyBatch(ops);
            } catch (OperationApplicationException e1) {
                e1.printStackTrace();
                syncResult.stats.numSkippedEntries++;
            }
        }
        return null;
    }

    /**
     * We know that the group exists locally, so we can use the data in the JSON group as gold
     * @param group
     * @param provider
     * @param authToken
     * @param account
     * @param syncResult
     * @throws JSONException
     * @throws RemoteException
     * @throws OperationApplicationException
     */
    private void optimisticallyAddContactsToExistingGroup(JSONObject group, ContentProviderClient provider,
            String authToken, Account account, SyncResult syncResult) throws JSONException, RemoteException {

        if (!group.has(JSONKeys.KEY_MEMBERS)) {
            return;
        }
        String groupCloudId = group.getString(JSONKeys.KEY_ID);
        Cursor groupCursor = provider.query(GMSGroups.CONTENT_URI, new String[] { GMSGroup._ID, GMSGroup.CLOUD_ID },
                GMSGroup.CLOUD_ID + "=?", new String[] { groupCloudId }, null);
        try {
            if (groupCursor == null || 1 != groupCursor.getCount() || !groupCursor.moveToFirst()) {
                syncResult.databaseError = true;
                return;
            }
            long groupId = groupCursor.getLong(0);
            if (groupId < 0L) {
                syncResult.databaseError = true;
                return;
            }
            // Optimistically add the contacts
            JSONArray membersArray = group.getJSONArray(JSONKeys.KEY_MEMBERS);
            for (int j = 0; j < membersArray.length(); ++j) {
                JSONObject member = membersArray.getJSONObject(j);
                ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
                // If the first operation asserts it means the contact exists already
                // Operation 0
                ContentProviderOperation op = ContentProviderOperation.newAssertQuery(GMSContacts.CONTENT_URI)
                        .withSelection(GMSContact.CLOUD_ID + "=? AND " + GMSContact.GROUP_ID + "=?",
                                new String[] { member.getString(JSONKeys.KEY_ID), String.valueOf(groupId) })
                        .withExpectedCount(0).build();
                ops.add(op);
                op = ContentProviderOperation.newInsert(GMSContacts.CONTENT_URI)
                        .withValues(GMSApplication.getMemberValues(member)).withValue(GMSContact.GROUP_ID, groupId)
                        .withValue(GMSContact.STATUS, GMSContact.STATUS_SYNCED).build();
                ops.add(op);
                try {
                    @SuppressWarnings("unused")
                    ContentProviderResult[] results = provider.applyBatch(ops);
                } catch (OperationApplicationException e) {
                    // The contact already exists, so we'll optionally update it, based on its version
                    Cursor contactCursor = null;
                    try {
                        contactCursor = provider.query(GMSContacts.CONTENT_URI,
                                new String[] { GMSContact._ID, GMSContact.CLOUD_ID, GMSContact.GROUP_ID },
                                GMSContact.CLOUD_ID + "=? AND " + GMSContact.GROUP_ID + "=?",
                                new String[] { member.getString(JSONKeys.KEY_ID), String.valueOf(groupId) }, null);
                        if (contactCursor == null || !contactCursor.moveToFirst()) {
                            syncResult.databaseError = true;
                            return;
                        }
                        // The member already exists, so optinally update it
                        ops = new ArrayList<ContentProviderOperation>();
                        // Operation 0 - we know it exists. If its the right version, we don't need to do the update
                        // So we assert that we'll find zero records with the current version and if that's right, we'll update our
                        // record, including the version with the new record data
                        op = ContentProviderOperation
                                .newAssertQuery(ContentUris.withAppendedId(GMSContacts.CONTENT_URI,
                                        contactCursor.getLong(0)))
                                .withSelection(GMSContact.VERSION + "=?",
                                        new String[] { member.getString(JSONKeys.KEY_VERSION) })
                                .withExpectedCount(0).build();
                        ops.add(op);
                        op = ContentProviderOperation
                                .newUpdate(ContentUris.withAppendedId(GMSContacts.CONTENT_URI,
                                        contactCursor.getLong(0)))
                                .withValues(GMSApplication.getMemberValues(member))
                                .withValue(GMSContact.STATUS, GMSContact.STATUS_SYNCED).withExpectedCount(1)
                                .build();
                        ops.add(op);
                        provider.applyBatch(ops);
                    } catch (OperationApplicationException l) {
                        ops = new ArrayList<ContentProviderOperation>();
                        // Operation 0 - we know it exists and is of the current version, so no update of attributes is needed
                        // We still have to update the status to SYNCED so we don't blow it away later.
                        op = ContentProviderOperation
                                .newUpdate(ContentUris.withAppendedId(GMSContacts.CONTENT_URI,
                                        contactCursor.getLong(0)))
                                .withValue(GMSContact.STATUS, GMSContact.STATUS_SYNCED).withExpectedCount(1)
                                .build();
                        ops.add(op);
                        try {
                            provider.applyBatch(ops);
                        } catch (OperationApplicationException e1) {
                            syncResult.stats.numSkippedEntries++;
                            e1.printStackTrace();
                        }
                    } finally {
                        if (contactCursor != null) {
                            contactCursor.close();
                        }
                    }
                }
            }
        } finally {
            if (groupCursor != null) {
                groupCursor.close();
            }
        }
    }

    private void deleteUnsyncedItems(ContentProviderClient provider, Account account, SyncResult syncResult)
            throws RemoteException, JSONException {

        Log.d(TAG, "Delete unsynced items before count: " + syncResult.stats.numDeletes);
        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
        ContentProviderOperation op;
        ContentProviderResult[] results;

        // This is the good part
        Cursor unsyncedGroupsCursor = null;
        try {
            final String SELECT = GMSGroup.STATUS + " ISNULL OR " + GMSGroup.STATUS + " != "
                    + GMSGroup.STATUS_SYNCED;
            unsyncedGroupsCursor = provider.query(GMSGroups.CONTENT_URI, new String[] { GMSGroup._ID,
                    GMSGroup.CLOUD_ID, GMSGroup.STATUS, GMSGroup.NAME, GMSGroup.RAW_CONTACT_ID }, SELECT, null,
                    null);
            ContentProviderClient contactsProvider;
            if (unsyncedGroupsCursor.moveToFirst()) {
                contactsProvider = getContext().getContentResolver()
                        .acquireContentProviderClient(ContactsContract.AUTHORITY);
                do {
                    long groupId = unsyncedGroupsCursor.getLong(0);
                    long groupRawContactId = unsyncedGroupsCursor.getLong(4);
                    Cursor memberCursor = null;
                    try {
                        memberCursor = provider.query(GMSContacts.CONTENT_URI,
                                new String[] { GMSContact._ID, GMSContact.GROUP_ID }, GMSContact.GROUP_ID + "=?",
                                new String[] { String.valueOf(groupId) }, null);
                        while (memberCursor.moveToNext()) {
                            op = ContentProviderOperation.newDelete(
                                    ContentUris.withAppendedId(GMSContacts.CONTENT_URI, memberCursor.getLong(0)))
                                    .build();
                            ops.add(op);
                        }
                    } finally {
                        if (memberCursor != null) {
                            memberCursor.close();
                        }
                    }
                    op = ContentProviderOperation
                            .newDelete(ContentUris.withAppendedId(GMSGroups.CONTENT_URI, groupId)).build();
                    ops.add(op);
                    if (groupRawContactId <= 0) {
                        Cursor accountContactsCursor = null;
                        try {
                            String sourceId = unsyncedGroupsCursor.getString(1);
                            Log.d(TAG, String.format("Unsynced Group Id: %1$d, SourceId: %2$s", groupId, sourceId));
                            accountContactsCursor = GMSContactOperations.findGroupInContacts(contactsProvider,
                                    account, sourceId);
                            if (accountContactsCursor != null && accountContactsCursor.moveToFirst()) {
                                groupRawContactId = accountContactsCursor.getLong(0);
                            }
                        } finally {
                            if (accountContactsCursor != null) {
                                accountContactsCursor.close();
                            }
                        }
                        GMSContactOperations.removeGroupFromContacts(contactsProvider, account, groupRawContactId,
                                syncResult);
                    }
                } while (unsyncedGroupsCursor.moveToNext());
            }
        } finally {
            if (unsyncedGroupsCursor != null) {
                unsyncedGroupsCursor.close();
            }
        }
        // Now delete any unsynced contacts from the local provider
        op = ContentProviderOperation.newDelete(GMSContacts.CONTENT_URI)
                .withSelection(
                        GMSContact.STATUS + " ISNULL OR " + GMSContact.STATUS + " != " + GMSContact.STATUS_SYNCED,
                        null)
                .build();
        ops.add(op);

        op = ContentProviderOperation.newUpdate(GMSGroups.CONTENT_URI).withValue(GMSGroup.STATUS, null).build();
        ops.add(op);

        op = ContentProviderOperation.newUpdate(GMSContacts.CONTENT_URI).withValue(GMSContact.STATUS, null).build();
        ops.add(op);
        try {
            results = provider.applyBatch(ops);
            int numResults = results.length;
            for (int i = 0; i < numResults; ++i) {
                // The first first N-2 results were deletes
                if (i < numResults - 2) {
                    syncResult.stats.numDeletes += results[i].count;
                } else {
                    // The last two results were updates
                    syncResult.stats.numEntries += results[i].count;
                }
            }
            Log.d(TAG, String.format("Delete unsynced items after count: %1$d, entries: %2$d",
                    syncResult.stats.numDeletes, syncResult.stats.numEntries));
        } catch (OperationApplicationException e) {
            syncResult.stats.numSkippedEntries++;
            e.printStackTrace();
        }
    }

    private void syncGroupsWithServer(ContentProviderClient provider, String authToken, Account account,
            SyncResult syncResult) throws JSONException, RemoteException, IOException {
        JSONArray groupArray = NetworkUtilities.getMyOwnedGroups(authToken);
        if (groupArray == null) {
            syncResult.delayUntil = 180;
            return;
        }
        ContentProviderClient contactsProvider = this.getContext().getContentResolver()
                .acquireContentProviderClient(ContactsContract.AUTHORITY);
        for (int i = 0; i < groupArray.length(); ++i) {
            JSONObject group = groupArray.getJSONObject(i);
            try {
                optimisticallyCreateGroupAndContacts(group, provider, contactsProvider, authToken, account,
                        syncResult);
            } catch (OperationApplicationException e) {
                // We're here because our assertion about there being no group failed
                optimisticallyUpdateGroup(group, provider, contactsProvider, authToken, account, syncResult);
                optimisticallyAddContactsToExistingGroup(group, provider, authToken, account, syncResult);
            }
        }
        deleteUnsyncedItems(provider, account, syncResult);
    }

    private void processDeletedContacts(ContentProviderClient provider, String authToken, Account account,
            SyncResult syncResult) throws IOException, JSONException, RemoteException {
        Cursor contactCursor = null;
        try {
            contactCursor = provider.query(GMSContacts.CONTENT_URI,
                    new String[] { GMSContact._ID, GMSContact.CLOUD_ID, GMSContact.STATUS, GMSContact.GROUP_ID },
                    GMSContact.STATUS + "=?", new String[] { String.valueOf(GMSContact.STATUS_DELETED) }, null);
            if (contactCursor == null) {
                syncResult.databaseError = true;
                return;
            }
            while (contactCursor.moveToNext()) {
                Cursor groupCursor = null;
                try {
                    groupCursor = provider.query(
                            ContentUris.withAppendedId(GMSGroups.CONTENT_URI, contactCursor.getLong(3)),
                            new String[] { GMSGroup.CLOUD_ID }, null, null, null);
                    if (groupCursor == null || !groupCursor.moveToFirst()) {
                        syncResult.databaseError = true;
                        break;
                    }
                    if (NetworkUtilities.removeGroupContact(authToken, groupCursor.getString(0),
                            contactCursor.getString(1)) == null) {
                        syncResult.stats.numIoExceptions++;
                    } else {
                        syncResult.stats.numDeletes++;
                    }
                } finally {
                    if (groupCursor != null) {
                        groupCursor.close();
                    }
                }
            }
        } finally {
            if (contactCursor != null) {
                contactCursor.close();
            }
        }

    }

    private void processDeletedGroups(ContentProviderClient provider, String authToken, Account account,
            SyncResult syncResult) throws RemoteException, ClientProtocolException, IOException, JSONException {
        Cursor cursor = null;
        try {
            cursor = provider.query(GMSGroups.CONTENT_URI, new String[] { GMSGroup.CLOUD_ID },
                    GMSGroup.STATUS + "=?", new String[] { String.valueOf(GMSGroup.STATUS_DELETED) }, null);
            if (cursor == null) {
                syncResult.databaseError = true;
                return;
            }
            while (cursor.moveToNext()) {
                if (NetworkUtilities.deleteGroup(authToken, cursor.getString(0)) != null) {
                    syncResult.stats.numDeletes++;
                } else {
                    syncResult.stats.numIoExceptions++;
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    private boolean shouldSyncNow(Account account, Bundle extras, ContentProviderClient provider,
            SyncResult syncResult) throws RemoteException {
        Cursor c = null;
        try {
            c = provider.query(GMSSyncs.CONTENT_URI, new String[] { GMSSync._ID, GMSSync.SYNC_DATE }, null, null,
                    GMSSync.DEFAULT_SORT_ORDER);
            if (c == null) {
                syncResult.databaseError = true;
            } else if (c.getCount() == 0) {
                // Setup our group visibility on the very first sync
                GMSContactOperations.setAccountContactsVisibility(getContext(), account, true);
            }
        } finally {
            if (c != null) {
                c.close();
            }
        }
        return true;
    }

    private long updateSyncRecord(ContentProviderClient provider)
            throws RemoteException, OperationApplicationException {
        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
        ContentProviderOperation op = ContentProviderOperation.newInsert(GMSSyncs.CONTENT_URI)
                .withValue(GMSSync.SYNC_DATE, System.currentTimeMillis()).build();
        ops.add(op);
        ContentProviderResult[] results = provider.applyBatch(ops);
        return (results[0].uri != null) ? ContentUris.parseId(results[0].uri) : -1L;
    }
}