Java tutorial
/* * 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; } }