blob: c15f2dadf32a5fefba76b6f600e9e6fa773a5de1 [file] [log] [blame]
/*
** Copyright 2007, 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,
** See the License for the specific language governing permissions and
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** limitations under the License.
*/
package com.android.providers.contacts;
import com.google.android.collect.Sets;
import com.google.android.gdata.client.AndroidGDataClient;
import com.google.android.gdata.client.AndroidXmlParserFactory;
import com.google.android.providers.AbstractGDataSyncAdapter;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.QueryParams;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.contacts.client.ContactsClient;
import com.google.wireless.gdata.contacts.data.ContactEntry;
import com.google.wireless.gdata.contacts.data.ContactsElement;
import com.google.wireless.gdata.contacts.data.EmailAddress;
import com.google.wireless.gdata.contacts.data.GroupEntry;
import com.google.wireless.gdata.contacts.data.GroupMembershipInfo;
import com.google.wireless.gdata.contacts.data.ImAddress;
import com.google.wireless.gdata.contacts.data.Organization;
import com.google.wireless.gdata.contacts.data.PhoneNumber;
import com.google.wireless.gdata.contacts.data.PostalAddress;
import com.google.wireless.gdata.contacts.parser.xml.XmlContactsGDataParserFactory;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.ExtendedProperty;
import com.google.wireless.gdata.data.Feed;
import com.google.wireless.gdata.data.MediaEntry;
import com.google.wireless.gdata.parser.ParseException;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncContext;
import android.content.SyncResult;
import android.content.SyncableContentProvider;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemProperties;
import android.provider.Contacts;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.Extensions;
import android.provider.Contacts.GroupMembership;
import android.provider.Contacts.Groups;
import android.provider.Contacts.Organizations;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.provider.Contacts.Photos;
import android.provider.SubscribedFeeds;
import android.provider.SyncConstValue;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
import android.accounts.AccountManager;
import android.accounts.Account;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Implements a SyncAdapter for Contacts
*/
public class ContactsSyncAdapter extends AbstractGDataSyncAdapter {
private static final String USER_AGENT_APP_VERSION = "Android-GData-Contacts/1.1";
private static final String CONTACTS_FEED_URL = "http://www.google.com/m8/feeds/contacts/";
private static final String GROUPS_FEED_URL = "http://www.google.com/m8/feeds/groups/";
private static final String PHOTO_FEED_URL = "http://www.google.com/m8/feeds/photos/media/";
private final ContactsClient mContactsClient;
private static final String[] sSubscriptionProjection =
new String[] {
SubscribedFeeds.Feeds._SYNC_ACCOUNT,
SubscribedFeeds.Feeds.FEED,
SubscribedFeeds.Feeds._ID};
private static final HashMap<Byte, Integer> ENTRY_TYPE_TO_PROVIDER_PHONE;
private static final HashMap<Byte, Integer> ENTRY_TYPE_TO_PROVIDER_EMAIL;
private static final HashMap<Byte, Integer> ENTRY_TYPE_TO_PROVIDER_IM;
private static final HashMap<Byte, Integer> ENTRY_TYPE_TO_PROVIDER_POSTAL;
private static final HashMap<Byte, Integer> ENTRY_TYPE_TO_PROVIDER_ORGANIZATION;
private static final HashMap<Integer, Byte> PROVIDER_TYPE_TO_ENTRY_PHONE;
private static final HashMap<Integer, Byte> PROVIDER_TYPE_TO_ENTRY_EMAIL;
private static final HashMap<Integer, Byte> PROVIDER_TYPE_TO_ENTRY_IM;
private static final HashMap<Integer, Byte> PROVIDER_TYPE_TO_ENTRY_POSTAL;
private static final HashMap<Integer, Byte> PROVIDER_TYPE_TO_ENTRY_ORGANIZATION;
private static final HashMap<Byte, Integer> ENTRY_IM_PROTOCOL_TO_PROVIDER_PROTOCOL;
private static final HashMap<Integer, Byte> PROVIDER_IM_PROTOCOL_TO_ENTRY_PROTOCOL;
private static final int MAX_MEDIA_ENTRIES_PER_SYNC = 10;
// Only valid during a sync operation.
// If set then a getServerDiffs() was performed during this sync.
private boolean mPerformedGetServerDiffs;
// Only valid during a sync. If set then this sync was a forced sync request
private boolean mIsManualSync;
private int mPhotoDownloads;
private int mPhotoUploads;
private static final String IMAGE_MIME_TYPE = "image/*";
static {
HashMap<Byte, Integer> map;
map = new HashMap<Byte, Integer>();
map.put(ImAddress.PROTOCOL_AIM, ContactMethods.PROTOCOL_AIM);
map.put(ImAddress.PROTOCOL_GOOGLE_TALK, ContactMethods.PROTOCOL_GOOGLE_TALK);
map.put(ImAddress.PROTOCOL_ICQ, ContactMethods.PROTOCOL_ICQ);
map.put(ImAddress.PROTOCOL_JABBER, ContactMethods.PROTOCOL_JABBER);
map.put(ImAddress.PROTOCOL_MSN, ContactMethods.PROTOCOL_MSN);
map.put(ImAddress.PROTOCOL_QQ, ContactMethods.PROTOCOL_QQ);
map.put(ImAddress.PROTOCOL_SKYPE, ContactMethods.PROTOCOL_SKYPE);
map.put(ImAddress.PROTOCOL_YAHOO, ContactMethods.PROTOCOL_YAHOO);
ENTRY_IM_PROTOCOL_TO_PROVIDER_PROTOCOL = map;
PROVIDER_IM_PROTOCOL_TO_ENTRY_PROTOCOL = swapMap(map);
map = new HashMap<Byte, Integer>();
map.put(EmailAddress.TYPE_HOME, ContactMethods.TYPE_HOME);
map.put(EmailAddress.TYPE_WORK, ContactMethods.TYPE_WORK);
map.put(EmailAddress.TYPE_OTHER, ContactMethods.TYPE_OTHER);
map.put(EmailAddress.TYPE_NONE, ContactMethods.TYPE_CUSTOM);
ENTRY_TYPE_TO_PROVIDER_EMAIL = map;
PROVIDER_TYPE_TO_ENTRY_EMAIL = swapMap(map);
map = new HashMap<Byte, Integer>();
map.put(PhoneNumber.TYPE_HOME, Phones.TYPE_HOME);
map.put(PhoneNumber.TYPE_MOBILE, Phones.TYPE_MOBILE);
map.put(PhoneNumber.TYPE_PAGER, Phones.TYPE_PAGER);
map.put(PhoneNumber.TYPE_WORK, Phones.TYPE_WORK);
map.put(PhoneNumber.TYPE_HOME_FAX, Phones.TYPE_FAX_HOME);
map.put(PhoneNumber.TYPE_WORK_FAX, Phones.TYPE_FAX_WORK);
map.put(PhoneNumber.TYPE_OTHER, Phones.TYPE_OTHER);
map.put(PhoneNumber.TYPE_NONE, Phones.TYPE_CUSTOM);
ENTRY_TYPE_TO_PROVIDER_PHONE = map;
PROVIDER_TYPE_TO_ENTRY_PHONE = swapMap(map);
map = new HashMap<Byte, Integer>();
map.put(PostalAddress.TYPE_HOME, ContactMethods.TYPE_HOME);
map.put(PostalAddress.TYPE_WORK, ContactMethods.TYPE_WORK);
map.put(PostalAddress.TYPE_OTHER, ContactMethods.TYPE_OTHER);
map.put(PostalAddress.TYPE_NONE, ContactMethods.TYPE_CUSTOM);
ENTRY_TYPE_TO_PROVIDER_POSTAL = map;
PROVIDER_TYPE_TO_ENTRY_POSTAL = swapMap(map);
map = new HashMap<Byte, Integer>();
map.put(ImAddress.TYPE_HOME, ContactMethods.TYPE_HOME);
map.put(ImAddress.TYPE_WORK, ContactMethods.TYPE_WORK);
map.put(ImAddress.TYPE_OTHER, ContactMethods.TYPE_OTHER);
map.put(ImAddress.TYPE_NONE, ContactMethods.TYPE_CUSTOM);
ENTRY_TYPE_TO_PROVIDER_IM = map;
PROVIDER_TYPE_TO_ENTRY_IM = swapMap(map);
map = new HashMap<Byte, Integer>();
map.put(Organization.TYPE_WORK, Organizations.TYPE_WORK);
map.put(Organization.TYPE_OTHER, Organizations.TYPE_OTHER);
map.put(Organization.TYPE_NONE, Organizations.TYPE_CUSTOM);
ENTRY_TYPE_TO_PROVIDER_ORGANIZATION = map;
PROVIDER_TYPE_TO_ENTRY_ORGANIZATION = swapMap(map);
}
private static <A, B> HashMap<B, A> swapMap(HashMap<A, B> originalMap) {
HashMap<B, A> newMap = new HashMap<B,A>();
for (Map.Entry<A, B> entry : originalMap.entrySet()) {
final B originalValue = entry.getValue();
if (newMap.containsKey(originalValue)) {
throw new IllegalArgumentException("value " + originalValue
+ " was already encountered");
}
newMap.put(originalValue, entry.getKey());
}
return newMap;
}
protected ContactsSyncAdapter(Context context, SyncableContentProvider provider) {
super(context, provider);
mContactsClient = new ContactsClient(
new AndroidGDataClient(context, USER_AGENT_APP_VERSION),
new XmlContactsGDataParserFactory(new AndroidXmlParserFactory()));
}
protected GDataServiceClient getGDataServiceClient() {
return mContactsClient;
}
@Override
protected Entry newEntry() {
throw new UnsupportedOperationException("this should never be used");
}
protected String getFeedUrl(Account account) {
throw new UnsupportedOperationException("this should never be used");
}
protected Class getFeedEntryClass() {
throw new UnsupportedOperationException("this should never be used");
}
protected Class getFeedEntryClass(String feed) {
if (feed.startsWith(rewriteUrlforAccount(getAccount(), GROUPS_FEED_URL))) {
return GroupEntry.class;
}
if (feed.startsWith(rewriteUrlforAccount(getAccount(), CONTACTS_FEED_URL))) {
return ContactEntry.class;
}
return null;
}
@Override
public void getServerDiffs(SyncContext context, SyncData baseSyncData,
SyncableContentProvider tempProvider,
Bundle extras, Object syncInfo, SyncResult syncResult) {
mPerformedGetServerDiffs = true;
GDataSyncData syncData = (GDataSyncData)baseSyncData;
ArrayList<String> feedsToSync = new ArrayList<String>();
if (extras != null && extras.containsKey("feed")) {
feedsToSync.add((String) extras.get("feed"));
} else {
feedsToSync.add(getGroupsFeedForAccount(getAccount()));
addContactsFeedsToSync(getContext().getContentResolver(), getAccount(), feedsToSync);
feedsToSync.add(getPhotosFeedForAccount(getAccount()));
}
for (String feed : feedsToSync) {
context.setStatusText("Downloading\u2026");
if (getPhotosFeedForAccount(getAccount()).equals(feed)) {
getServerPhotos(context, feed, MAX_MEDIA_ENTRIES_PER_SYNC, syncData, syncResult);
} else {
final Class feedEntryClass = getFeedEntryClass(feed);
if (feedEntryClass != null) {
getServerDiffsImpl(context, tempProvider, feedEntryClass,
feed, null, getMaxEntriesPerSync(), syncData, syncResult);
} else {
if (Config.LOGD) {
Log.d(TAG, "ignoring sync request for unknown feed " + feed);
}
}
}
if (syncResult.hasError()) {
break;
}
}
}
/**
* Look at the groups sync settings and the overall sync preference to determine which
* feeds to sync and add them to the feedsToSync list.
*/
public static void addContactsFeedsToSync(ContentResolver cr, Account account,
Collection<String> feedsToSync) {
boolean shouldSyncEverything = getShouldSyncEverything(cr, account);
if (shouldSyncEverything) {
feedsToSync.add(getContactsFeedForAccount(account));
return;
}
Cursor cursor = cr.query(Contacts.Groups.CONTENT_URI, new String[]{Groups._SYNC_ID},
"_sync_account=? AND _sync_account_type=? AND should_sync>0",
new String[]{account.name, account.type}, null);
try {
while (cursor.moveToNext()) {
feedsToSync.add(getContactsFeedForGroup(account, cursor.getString(0)));
}
} finally {
cursor.close();
}
}
private static boolean getShouldSyncEverything(ContentResolver cr, Account account) {
// TODO(fredq) should be using account instead of null
String value = Contacts.Settings.getSetting(cr, null, Contacts.Settings.SYNC_EVERYTHING);
return !TextUtils.isEmpty(value) && !"0".equals(value);
}
private void getServerPhotos(SyncContext context, String feedUrl, int maxDownloads,
GDataSyncData syncData, SyncResult syncResult) {
final ContentResolver cr = getContext().getContentResolver();
final Account account = getAccount();
Cursor cursor = cr.query(
Photos.CONTENT_URI,
new String[]{Photos._SYNC_ID, Photos._SYNC_VERSION, Photos.PERSON_ID,
Photos.DOWNLOAD_REQUIRED, Photos._ID}, ""
+ "_sync_account=? AND _sync_account_type=? AND download_required != 0",
new String[]{account.name, account.type}, null);
try {
int numFetched = 0;
while (cursor.moveToNext()) {
if (numFetched >= maxDownloads) {
break;
}
String photoSyncId = cursor.getString(0);
String photoVersion = cursor.getString(1);
long person = cursor.getLong(2);
String photoUrl = feedUrl + "/" + photoSyncId;
long photoId = cursor.getLong(4);
try {
context.setStatusText("Downloading photo " + photoSyncId);
++numFetched;
++mPhotoDownloads;
InputStream inputStream = mContactsClient.getMediaEntryAsStream(
photoUrl, getAuthToken());
savePhoto(person, inputStream, photoVersion);
syncResult.stats.numUpdates++;
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, "error downloading " + photoUrl, e);
}
syncResult.stats.numIoExceptions++;
return;
} catch (HttpException e) {
switch (e.getStatusCode()) {
case HttpException.SC_UNAUTHORIZED:
if (Config.LOGD) {
Log.d(TAG, "not authorized to download " + photoUrl, e);
}
syncResult.stats.numAuthExceptions++;
return;
case HttpException.SC_FORBIDDEN:
case HttpException.SC_NOT_FOUND:
final String exceptionMessage = e.getMessage();
if (Config.LOGD) {
Log.d(TAG, "unable to download photo " + photoUrl + ", "
+ exceptionMessage + ", ignoring");
}
ContentValues values = new ContentValues();
values.put(Photos.SYNC_ERROR, exceptionMessage);
Uri photoUri = Uri.withAppendedPath(
ContentUris.withAppendedId(People.CONTENT_URI, photoId),
Photos.CONTENT_DIRECTORY);
cr.update(photoUri, values, null /* where */, null /* where args */);
break;
default:
if (Config.LOGD) {
Log.d(TAG, "error downloading " + photoUrl, e);
}
syncResult.stats.numIoExceptions++;
return;
}
}
}
final boolean hasMoreToSync = numFetched < cursor.getCount();
GDataSyncData.FeedData feedData =
new GDataSyncData.FeedData(0 /* no update time */,
numFetched, hasMoreToSync, null /* no lastId */,
0 /* no feed index */);
syncData.feedData.put(feedUrl, feedData);
} finally {
cursor.close();
}
}
@Override
protected void getStatsString(StringBuffer sb, SyncResult result) {
super.getStatsString(sb, result);
if (mPhotoUploads > 0) {
sb.append("p").append(mPhotoUploads);
}
if (mPhotoDownloads > 0) {
sb.append("P").append(mPhotoDownloads);
}
}
@Override
public void sendClientDiffs(SyncContext context, SyncableContentProvider clientDiffs,
SyncableContentProvider serverDiffs, SyncResult syncResult,
boolean dontSendDeletes) {
initTempProvider(clientDiffs);
sendClientDiffsImpl(context, clientDiffs, new GroupEntry(), null /* no syncInfo */,
serverDiffs, syncResult, dontSendDeletes);
// lets go ahead and commit what we have if we successfully made a change
if (syncResult.madeSomeProgress()) {
return;
}
sendClientPhotos(context, clientDiffs, null /* no syncInfo */, syncResult);
// lets go ahead and commit what we have if we successfully made a change
if (syncResult.madeSomeProgress()) {
return;
}
sendClientDiffsImpl(context, clientDiffs, new ContactEntry(), null /* no syncInfo */,
serverDiffs, syncResult, dontSendDeletes);
}
protected void sendClientPhotos(SyncContext context, ContentProvider clientDiffs,
Object syncInfo, SyncResult syncResult) {
Entry entry = new MediaEntry();
GDataServiceClient client = getGDataServiceClient();
String authToken = getAuthToken();
ContentResolver cr = getContext().getContentResolver();
final Account account = getAccount();
Cursor c = clientDiffs.query(Photos.CONTENT_URI, null /* all columns */,
null /* no where */, null /* no where args */, null /* default sort order */);
try {
int personColumn = c.getColumnIndexOrThrow(Photos.PERSON_ID);
int dataColumn = c.getColumnIndexOrThrow(Photos.DATA);
int numRows = c.getCount();
while (c.moveToNext()) {
if (mSyncCanceled) {
if (Config.LOGD) Log.d(TAG, "stopping since the sync was canceled");
break;
}
entry.clear();
context.setStatusText("Updating, " + (numRows - 1) + " to go");
cursorToBaseEntry(entry, account, c);
String editUrl = entry.getEditUri();
if (TextUtils.isEmpty(editUrl)) {
if (Config.LOGD) {
Log.d(TAG, "skipping photo edit for unsynced contact");
}
continue;
}
// Send the request and receive the response
InputStream inputStream = null;
byte[] imageData = c.getBlob(dataColumn);
if (imageData != null) {
inputStream = new ByteArrayInputStream(imageData);
}
Uri photoUri = Uri.withAppendedPath(People.CONTENT_URI,
c.getString(personColumn) + "/" + Photos.CONTENT_DIRECTORY);
try {
if (inputStream != null) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Updating photo " + entry.toString());
}
++mPhotoUploads;
client.updateMediaEntry(editUrl, inputStream, IMAGE_MIME_TYPE, authToken);
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Deleting photo " + entry.toString());
}
client.deleteEntry(editUrl, authToken);
}
// Mark that this photo is no longer dirty. The next time we sync (which
// should be soon), we will get the new version of the photo and whether
// or not there is a new one to download (e.g. if we deleted our version
// yet there is an evergreen version present).
ContentValues values = new ContentValues();
values.put(Photos.EXISTS_ON_SERVER, inputStream == null ? 0 : 1);
values.put(Photos._SYNC_DIRTY, 0);
if (cr.update(photoUri, values,
null /* no where */, null /* no where args */) != 1) {
Log.e(TAG, "error updating photo " + photoUri + " with values " + values);
syncResult.stats.numParseExceptions++;
} else {
syncResult.stats.numUpdates++;
}
continue;
} catch (ParseException e) {
Log.e(TAG, "parse error during update of " + ", skipping");
syncResult.stats.numParseExceptions++;
} catch (IOException e) {
if (Config.LOGD) {
Log.d(TAG, "io error during update of " + entry.toString()
+ ", skipping");
}
syncResult.stats.numIoExceptions++;
} catch (HttpException e) {
switch (e.getStatusCode()) {
case HttpException.SC_UNAUTHORIZED:
if (syncResult.stats.numAuthExceptions == 0) {
if (Config.LOGD) {
Log.d(TAG, "auth error during update of " + entry
+ ", skipping");
}
}
syncResult.stats.numAuthExceptions++;
AccountManager.get(getContext()).invalidateAuthToken(
"com.google", authToken);
return;
case HttpException.SC_CONFLICT:
if (Config.LOGD) {
Log.d(TAG, "conflict detected during update of " + entry
+ ", skipping");
}
syncResult.stats.numConflictDetectedExceptions++;
break;
case HttpException.SC_BAD_REQUEST:
case HttpException.SC_FORBIDDEN:
case HttpException.SC_NOT_FOUND:
case HttpException.SC_INTERNAL_SERVER_ERROR:
default:
if (Config.LOGD) {
Log.d(TAG, "error " + e.getMessage() + " during update of "
+ entry.toString() + ", skipping");
}
syncResult.stats.numIoExceptions++;
}
}
}
} finally {
c.close();
}
}
@Override
protected Cursor getCursorForTable(ContentProvider cp, Class entryClass) {
return getCursorForTableImpl(cp, entryClass);
}
protected static Cursor getCursorForTableImpl(ContentProvider cp, Class entryClass) {
if (entryClass == ContactEntry.class) {
return cp.query(People.CONTENT_URI, null, null, null, null);
}
if (entryClass == GroupEntry.class) {
return cp.query(Groups.CONTENT_URI, null, null, null, null);
}
throw new IllegalArgumentException("unexpected entry class, " + entryClass.getName());
}
@Override
protected Cursor getCursorForDeletedTable(ContentProvider cp, Class entryClass) {
return getCursorForDeletedTableImpl(cp, entryClass);
}
protected static Cursor getCursorForDeletedTableImpl(ContentProvider cp, Class entryClass) {
if (entryClass == ContactEntry.class) {
return cp.query(People.DELETED_CONTENT_URI, null, null, null, null);
}
if (entryClass == GroupEntry.class) {
return cp.query(Groups.DELETED_CONTENT_URI, null, null, null, null);
}
throw new IllegalArgumentException("unexpected entry class, " + entryClass.getName());
}
@Override
protected String cursorToEntry(SyncContext context, Cursor c, Entry baseEntry,
Object syncInfo) throws ParseException {
return cursorToEntryImpl(getContext().getContentResolver(), c, baseEntry, getAccount());
}
static protected String cursorToEntryImpl(ContentResolver cr, Cursor c, Entry entry,
Account account) throws ParseException {
cursorToBaseEntry(entry, account, c);
String createUrl = null;
if (entry instanceof ContactEntry) {
cursorToContactEntry(account, cr, c, (ContactEntry) entry);
if (entry.getEditUri() == null) {
createUrl = getContactsFeedForAccount(account);
}
} else if (entry instanceof MediaEntry) {
if (entry.getEditUri() == null) {
createUrl = getPhotosFeedForAccount(account);
}
} else {
cursorToGroupEntry(c, (GroupEntry) entry);
if (entry.getEditUri() == null) {
createUrl = getGroupsFeedForAccount(account);
}
}
return createUrl;
}
private static void cursorToGroupEntry(Cursor c, GroupEntry entry) throws ParseException {
if (!TextUtils.isEmpty(c.getString(c.getColumnIndexOrThrow(Groups.SYSTEM_ID)))) {
throw new ParseException("unable to modify system groups");
}
entry.setTitle(c.getString(c.getColumnIndexOrThrow(Groups.NAME)));
entry.setContent(c.getString(c.getColumnIndexOrThrow(Groups.NOTES)));
entry.setSystemGroup(null);
}
private static void cursorToContactEntry(Account account, ContentResolver cr, Cursor c,
ContactEntry entry)
throws ParseException {
entry.setTitle(c.getString(c.getColumnIndexOrThrow(People.NAME)));
entry.setContent(c.getString(c.getColumnIndexOrThrow(People.NOTES)));
entry.setYomiName(c.getString(c.getColumnIndexOrThrow(People.PHONETIC_NAME)));
long syncLocalId = c.getLong(c.getColumnIndexOrThrow(SyncConstValue._SYNC_LOCAL_ID));
addContactMethodsToContactEntry(cr, syncLocalId, entry);
addPhonesToContactEntry(cr, syncLocalId, entry);
addOrganizationsToContactEntry(cr, syncLocalId, entry);
addGroupMembershipToContactEntry(account, cr, syncLocalId, entry);
addExtensionsToContactEntry(cr, syncLocalId, entry);
}
@Override
protected void deletedCursorToEntry(SyncContext context, Cursor c, Entry entry) {
deletedCursorToEntryImpl(c, entry, getAccount());
}
protected boolean handleAllDeletedUnavailable(GDataSyncData syncData, String feed) {
// Contacts has no way to clear the contacts for just a given feed so it is unable
// to handle this condition itself. Instead it returns false, which tell the
// sync framework that it must handle it.
return false;
}
protected static void deletedCursorToEntryImpl(Cursor c, Entry entry, Account account) {
cursorToBaseEntry(entry, account, c);
}
private static void cursorToBaseEntry(Entry entry, Account account, Cursor c) {
String feedUrl;
if (entry instanceof ContactEntry) {
feedUrl = getContactsFeedForAccount(account);
} else if (entry instanceof GroupEntry) {
feedUrl = getGroupsFeedForAccount(account);
} else if (entry instanceof MediaEntry) {
feedUrl = getPhotosFeedForAccount(account);
} else {
throw new IllegalArgumentException("bad entry type: " + entry.getClass().getName());
}
String syncId = c.getString(c.getColumnIndexOrThrow(SyncConstValue._SYNC_ID));
if (syncId != null) {
String syncVersion = c.getString(c.getColumnIndexOrThrow(SyncConstValue._SYNC_VERSION));
entry.setId(feedUrl + "/" + syncId);
entry.setEditUri(entry.getId() + "/" + syncVersion);
}
}
private static void addPhonesToContactEntry(ContentResolver cr, long personId,
ContactEntry entry)
throws ParseException {
Cursor c = cr.query(Phones.CONTENT_URI, null, "person=" + personId, null, null);
int numberIndex = c.getColumnIndexOrThrow(People.Phones.NUMBER);
try {
while (c.moveToNext()) {
PhoneNumber phoneNumber = new PhoneNumber();
cursorToContactsElement(phoneNumber, c, PROVIDER_TYPE_TO_ENTRY_PHONE);
phoneNumber.setPhoneNumber(c.getString(numberIndex));
entry.addPhoneNumber(phoneNumber);
}
} finally {
if (c != null) c.close();
}
}
static private void addContactMethodsToContactEntry(ContentResolver cr, long personId,
ContactEntry entry) throws ParseException {
Cursor c = cr.query(ContactMethods.CONTENT_URI, null,
"person=" + personId, null, null);
int kindIndex = c.getColumnIndexOrThrow(ContactMethods.KIND);
int dataIndex = c.getColumnIndexOrThrow(ContactMethods.DATA);
int auxDataIndex = c.getColumnIndexOrThrow(ContactMethods.AUX_DATA);
try {
while (c.moveToNext()) {
int kind = c.getInt(kindIndex);
switch (kind) {
case Contacts.KIND_IM: {
ImAddress address = new ImAddress();
cursorToContactsElement(address, c, PROVIDER_TYPE_TO_ENTRY_IM);
address.setAddress(c.getString(dataIndex));
Object object = ContactMethods.decodeImProtocol(c.getString(auxDataIndex));
if (object == null) {
address.setProtocolPredefined(ImAddress.PROTOCOL_NONE);
} else if (object instanceof Integer) {
address.setProtocolPredefined(
PROVIDER_IM_PROTOCOL_TO_ENTRY_PROTOCOL.get((Integer)object));
} else {
if (!(object instanceof String)) {
throw new IllegalArgumentException("expected an String, " + object);
}
address.setProtocolPredefined(ImAddress.PROTOCOL_CUSTOM);
address.setProtocolCustom((String)object);
}
entry.addImAddress(address);
break;
}
case Contacts.KIND_POSTAL: {
PostalAddress address = new PostalAddress();
cursorToContactsElement(address, c, PROVIDER_TYPE_TO_ENTRY_POSTAL);
address.setValue(c.getString(dataIndex));
entry.addPostalAddress(address);
break;
}
case Contacts.KIND_EMAIL: {
EmailAddress address = new EmailAddress();
cursorToContactsElement(address, c, PROVIDER_TYPE_TO_ENTRY_EMAIL);
address.setAddress(c.getString(dataIndex));
entry.addEmailAddress(address);
break;
}
}
}
} finally {
if (c != null) c.close();
}
}
private static void addOrganizationsToContactEntry(ContentResolver cr, long personId,
ContactEntry entry) throws ParseException {
Cursor c = cr.query(Organizations.CONTENT_URI, null,
"person=" + personId, null, null);
try {
int companyIndex = c.getColumnIndexOrThrow(Organizations.COMPANY);
int titleIndex = c.getColumnIndexOrThrow(Organizations.TITLE);
while (c.moveToNext()) {
Organization organization = new Organization();
cursorToContactsElement(organization, c, PROVIDER_TYPE_TO_ENTRY_ORGANIZATION);
organization.setName(c.getString(companyIndex));
organization.setTitle(c.getString(titleIndex));
entry.addOrganization(organization);
}
} finally {
if (c != null) c.close();
}
}
private static void addGroupMembershipToContactEntry(Account account, ContentResolver cr,
long personId, ContactEntry entry) throws ParseException {
Cursor c = cr.query(GroupMembership.RAW_CONTENT_URI, null,
"person=" + personId, null, null);
try {
int serverIdIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID);
int localIdIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_ID);
while (c.moveToNext()) {
String serverId = c.getString(serverIdIndex);
if (serverId == null) {
final Uri groupUri = ContentUris
.withAppendedId(Groups.CONTENT_URI, c.getLong(localIdIndex));
Cursor groupCursor = cr.query(groupUri, new String[]{Groups._SYNC_ID},
null, null, null);
try {
if (groupCursor.moveToNext()) {
serverId = groupCursor.getString(0);
}
} finally {
groupCursor.close();
}
}
if (serverId == null) {
// the group hasn't been synced yet, we can't complete this operation since
// we don't know what server id to use for the group
throw new ParseException("unable to construct GroupMembershipInfo since the "
+ "group _sync_id isn't known yet, will retry later");
}
GroupMembershipInfo groupMembershipInfo = new GroupMembershipInfo();
String groupId = getCanonicalGroupsFeedForAccount(account) + "/" + serverId;
groupMembershipInfo.setGroup(groupId);
groupMembershipInfo.setDeleted(false);
entry.addGroup(groupMembershipInfo);
}
} finally {
if (c != null) c.close();
}
}
private static void addExtensionsToContactEntry(ContentResolver cr, long personId,
ContactEntry entry) throws ParseException {
Cursor c = cr.query(Extensions.CONTENT_URI, null, "person=" + personId, null, null);
try {
JSONObject jsonObject = new JSONObject();
int nameIndex = c.getColumnIndexOrThrow(Extensions.NAME);
int valueIndex = c.getColumnIndexOrThrow(Extensions.VALUE);
if (c.getCount() == 0) return;
while (c.moveToNext()) {
try {
jsonObject.put(c.getString(nameIndex), c.getString(valueIndex));
} catch (JSONException e) {
throw new ParseException("bad key or value", e);
}
}
ExtendedProperty extendedProperty = new ExtendedProperty();
extendedProperty.setName("android");
final String jsonString = jsonObject.toString();
if (jsonString == null) {
throw new ParseException("unable to convert cursor into a JSON string, "
+ DatabaseUtils.dumpCursorToString(c));
}
extendedProperty.setXmlBlob(jsonString);
entry.addExtendedProperty(extendedProperty);
} finally {
if (c != null) c.close();
}
}
private static void cursorToContactsElement(ContactsElement element,
Cursor c, HashMap<Integer, Byte> map) {
final int typeIndex = c.getColumnIndexOrThrow("type");
final int labelIndex = c.getColumnIndexOrThrow("label");
final int isPrimaryIndex = c.getColumnIndexOrThrow("isprimary");
element.setLabel(c.getString(labelIndex));
element.setType(map.get(c.getInt(typeIndex)));
element.setIsPrimary(c.getInt(isPrimaryIndex) != 0);
}
private static void contactsElementToValues(ContentValues values, ContactsElement element,
HashMap<Byte, Integer> map) {
values.put("type", map.get(element.getType()));
values.put("label", element.getLabel());
values.put("isprimary", element.isPrimary() ? 1 : 0);
}
/*
* Takes the entry, casts it to a ContactEntry and executes the appropriate
* actions on the ContentProvider to represent the entry.
*/
protected void updateProvider(Feed feed, Long syncLocalId,
Entry baseEntry, ContentProvider provider, Object syncInfo,
GDataSyncData.FeedData feedSyncData) throws ParseException {
// This is a hack to delete these incorrectly created contacts named "Starred in Android"
if (baseEntry instanceof ContactEntry
&& "Starred in Android".equals(baseEntry.getTitle())) {
Log.i(TAG, "Deleting incorrectly created contact from the server: " + baseEntry);
GDataServiceClient client = getGDataServiceClient();
try {
client.deleteEntry(baseEntry.getEditUri(), getAuthToken());
} catch (IOException e) {
Log.i(TAG, " exception while deleting contact: " + baseEntry, e);
} catch (com.google.wireless.gdata.client.HttpException e) {
Log.i(TAG, " exception while deleting contact: " + baseEntry, e);
}
}
updateProviderImpl(getAccount(), syncLocalId, baseEntry, provider);
}
protected static void updateProviderImpl(Account account, Long syncLocalId,
Entry entry, ContentProvider provider) throws ParseException {
// If this is a deleted entry then add it to the DELETED_CONTENT_URI
ContentValues deletedValues = null;
if (entry.isDeleted()) {
deletedValues = new ContentValues();
deletedValues.put(SyncConstValue._SYNC_LOCAL_ID, syncLocalId);
final String id = entry.getId();
final String editUri = entry.getEditUri();
if (!TextUtils.isEmpty(id)) {
deletedValues.put(SyncConstValue._SYNC_ID, lastItemFromUri(id));
}
if (!TextUtils.isEmpty(editUri)) {
deletedValues.put(SyncConstValue._SYNC_VERSION, lastItemFromUri(editUri));
}
deletedValues.put(SyncConstValue._SYNC_ACCOUNT, account.name);
deletedValues.put(SyncConstValue._SYNC_ACCOUNT_TYPE, account.type);
}
if (entry instanceof ContactEntry) {
if (deletedValues != null) {
provider.insert(People.DELETED_CONTENT_URI, deletedValues);
return;
}
updateProviderWithContactEntry(account, syncLocalId, (ContactEntry) entry, provider);
return;
}
if (entry instanceof GroupEntry) {
if (deletedValues != null) {
provider.insert(Groups.DELETED_CONTENT_URI, deletedValues);
return;
}
updateProviderWithGroupEntry(account, syncLocalId, (GroupEntry) entry, provider);
return;
}
throw new IllegalArgumentException("unknown entry type, " + entry.getClass().getName());
}
protected static void updateProviderWithContactEntry(Account account, Long syncLocalId,
ContactEntry entry, ContentProvider provider) throws ParseException {
final String name = entry.getTitle();
final String notes = entry.getContent();
final String yomiName = entry.getYomiName();
final String personSyncId = lastItemFromUri(entry.getId());
final String personSyncVersion = lastItemFromUri(entry.getEditUri());
// Store the info about the person
ContentValues values = new ContentValues();
values.put(People.NAME, name);
values.put(People.NOTES, notes);
values.put(People.PHONETIC_NAME, yomiName);
values.put(SyncConstValue._SYNC_ACCOUNT, account.name);
values.put(SyncConstValue._SYNC_ACCOUNT_TYPE, account.type);
values.put(SyncConstValue._SYNC_ID, personSyncId);
values.put(SyncConstValue._SYNC_DIRTY, "0");
values.put(SyncConstValue._SYNC_LOCAL_ID, syncLocalId);
values.put(SyncConstValue._SYNC_TIME, personSyncVersion);
values.put(SyncConstValue._SYNC_VERSION, personSyncVersion);
Uri personUri = provider.insert(People.CONTENT_URI, values);
// Store the photo information
final boolean photoExistsOnServer = !TextUtils.isEmpty(entry.getLinkPhotoHref());
final String photoVersion = lastItemFromUri(entry.getLinkEditPhotoHref());
values.clear();
values.put(Photos.PERSON_ID, ContentUris.parseId(personUri));
values.put(Photos.EXISTS_ON_SERVER, photoExistsOnServer ? 1 : 0);
values.put(SyncConstValue._SYNC_ACCOUNT, account.name);
values.put(SyncConstValue._SYNC_ACCOUNT_TYPE, account.type);
values.put(SyncConstValue._SYNC_ID, personSyncId);
values.put(SyncConstValue._SYNC_DIRTY, 0);
values.put(SyncConstValue._SYNC_LOCAL_ID, syncLocalId);
values.put(SyncConstValue._SYNC_TIME, photoVersion);
values.put(SyncConstValue._SYNC_VERSION, photoVersion);
if (provider.insert(Photos.CONTENT_URI, values) == null) {
Log.e(TAG, "error inserting photo row, " + values);
}
// Store each email address
for (Object object : entry.getEmailAddresses()) {
EmailAddress email = (EmailAddress) object;
values.clear();
contactsElementToValues(values, email, ENTRY_TYPE_TO_PROVIDER_EMAIL);
values.put(ContactMethods.DATA, email.getAddress());
values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
Uri uri = Uri.withAppendedPath(personUri, People.ContactMethods.CONTENT_DIRECTORY);
provider.insert(uri, values);
}
// Store each postal address
for (Object object : entry.getPostalAddresses()) {
PostalAddress address = (PostalAddress) object;
values.clear();
contactsElementToValues(values, address, ENTRY_TYPE_TO_PROVIDER_POSTAL);
values.put(ContactMethods.DATA, address.getValue());
values.put(ContactMethods.KIND, Contacts.KIND_POSTAL);
Uri uri = Uri.withAppendedPath(personUri, People.ContactMethods.CONTENT_DIRECTORY);
provider.insert(uri, values);
}
// Store each im address
for (Object object : entry.getImAddresses()) {
ImAddress address = (ImAddress) object;
values.clear();
contactsElementToValues(values, address, ENTRY_TYPE_TO_PROVIDER_IM);
values.put(ContactMethods.DATA, address.getAddress());
values.put(ContactMethods.KIND, Contacts.KIND_IM);
final byte protocolType = address.getProtocolPredefined();
if (protocolType == ImAddress.PROTOCOL_NONE) {
// don't add anything
} else if (protocolType == ImAddress.PROTOCOL_CUSTOM) {
values.put(ContactMethods.AUX_DATA,
ContactMethods.encodeCustomImProtocol(address.getProtocolCustom()));
} else {
Integer providerProtocolType =
ENTRY_IM_PROTOCOL_TO_PROVIDER_PROTOCOL .get(protocolType);
if (providerProtocolType == null) {
throw new IllegalArgumentException("unknown protocol type, " + protocolType);
}
values.put(ContactMethods.AUX_DATA,
ContactMethods.encodePredefinedImProtocol(providerProtocolType));
}
Uri uri = Uri.withAppendedPath(personUri, People.ContactMethods.CONTENT_DIRECTORY);
provider.insert(uri, values);
}
// Store each organization
for (Object object : entry.getOrganizations()) {
Organization organization = (Organization) object;
values.clear();
contactsElementToValues(values, organization, ENTRY_TYPE_TO_PROVIDER_ORGANIZATION);
values.put(Organizations.COMPANY, organization.getName());
values.put(Organizations.TITLE, organization.getTitle());
values.put(Organizations.COMPANY, organization.getName());
Uri uri = Uri.withAppendedPath(personUri, Organizations.CONTENT_DIRECTORY);
provider.insert(uri, values);
}
// Store each group
for (Object object : entry.getGroups()) {
GroupMembershipInfo groupMembershipInfo = (GroupMembershipInfo) object;
if (groupMembershipInfo.isDeleted()) {
continue;
}
values.clear();
values.put(GroupMembership.GROUP_SYNC_ACCOUNT, account.name);
values.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, account.type);
values.put(GroupMembership.GROUP_SYNC_ID,
lastItemFromUri(groupMembershipInfo.getGroup()));
Uri uri = Uri.withAppendedPath(personUri, GroupMembership.CONTENT_DIRECTORY);
provider.insert(uri, values);
}
// Store each phone number
for (Object object : entry.getPhoneNumbers()) {
PhoneNumber phone = (PhoneNumber) object;
values.clear();
contactsElementToValues(values, phone, ENTRY_TYPE_TO_PROVIDER_PHONE);
values.put(People.Phones.NUMBER, phone.getPhoneNumber());
values.put(People.Phones.LABEL, phone.getLabel());
Uri uri = Uri.withAppendedPath(personUri, People.Phones.CONTENT_DIRECTORY);
provider.insert(uri, values);
}
// Store the extended properties
for (Object object : entry.getExtendedProperties()) {
ExtendedProperty extendedProperty = (ExtendedProperty) object;
if (!"android".equals(extendedProperty.getName())) {
continue;
}
JSONObject jsonObject = null;
try {
jsonObject = new JSONObject(extendedProperty.getXmlBlob());
} catch (JSONException e) {
Log.w(TAG, "error parsing the android extended property, dropping, entry is "
+ entry.toString());
continue;
}
Iterator jsonIterator = jsonObject.keys();
while (jsonIterator.hasNext()) {
String key = (String)jsonIterator.next();
values.clear();
values.put(Extensions.NAME, key);
try {
values.put(Extensions.VALUE, jsonObject.getString(key));
} catch (JSONException e) {
// this should never happen, since we just got the key from the iterator
}
Uri uri = Uri.withAppendedPath(personUri, People.Extensions.CONTENT_DIRECTORY);
if (null == provider.insert(uri, values)) {
Log.e(TAG, "Error inserting extension into provider, uri "
+ uri + ", values " + values);
}
}
break;
}
}
protected static void updateProviderWithGroupEntry(Account account, Long syncLocalId,
GroupEntry entry, ContentProvider provider) throws ParseException {
ContentValues values = new ContentValues();
values.put(Groups.NAME, entry.getTitle());
values.put(Groups.NOTES, entry.getContent());
values.put(Groups.SYSTEM_ID, entry.getSystemGroup());
values.put(Groups._SYNC_ACCOUNT, account.name);
values.put(Groups._SYNC_ACCOUNT_TYPE, account.type);
values.put(Groups._SYNC_ID, lastItemFromUri(entry.getId()));
values.put(Groups._SYNC_DIRTY, 0);
values.put(Groups._SYNC_LOCAL_ID, syncLocalId);
final String editUri = entry.getEditUri();
final String syncVersion = editUri == null ? null : lastItemFromUri(editUri);
values.put(Groups._SYNC_TIME, syncVersion);
values.put(Groups._SYNC_VERSION, syncVersion);
provider.insert(Groups.CONTENT_URI, values);
}
private static String lastItemFromUri(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
protected void savePhoto(long person, InputStream photoInput, String photoVersion)
throws IOException {
try {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
byte[] data = new byte[1024];
while(true) {
int bytesRead = photoInput.read(data);
if (bytesRead < 0) break;
byteStream.write(data, 0, bytesRead);
}
ContentValues values = new ContentValues();
// we have to include this here otherwise the provider will set it to 1
values.put(Photos._SYNC_DIRTY, 0);
values.put(Photos.LOCAL_VERSION, photoVersion);
values.put(Photos.DATA, byteStream.toByteArray());
Uri photoUri = Uri.withAppendedPath(People.CONTENT_URI,
"" + person + "/" + Photos.CONTENT_DIRECTORY);
if (getContext().getContentResolver().update(photoUri, values,
"_sync_dirty=0", null) > 0) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "savePhoto: updated " + photoUri + " with values " + values);
}
} else {
Log.e(TAG, "savePhoto: update of " + photoUri + " with values " + values
+ " affected no rows");
}
} finally {
try {
if (photoInput != null) photoInput.close();
} catch (IOException e) {
// we don't care about exceptions here
}
}
}
/**
* Make sure the contacts subscriptions we expect based on the current
* accounts are present and that there aren't any extra subscriptions
* that we don't expect.
*/
@Override
public void onAccountsChanged(Account[] accountsArray) {
if (!"yes".equals(SystemProperties.get("ro.config.sync"))) {
return;
}
ContentResolver cr = getContext().getContentResolver();
for (Account account : accountsArray) {
// TODO(fredq) should be using account instead of null
String value = Contacts.Settings.getSetting(cr, null,
Contacts.Settings.SYNC_EVERYTHING);
if (value == null) {
// TODO(fredq) should be using account instead of null
Contacts.Settings.setSetting(cr, null, Contacts.Settings.SYNC_EVERYTHING, "1");
}
updateSubscribedFeeds(cr, account);
}
}
/**
* Returns the contacts feed url for a specific account.
* @param account The account
* @return The contacts feed url for a specific account.
*/
public static String getContactsFeedForAccount(Account account) {
String url = CONTACTS_FEED_URL + account.name + "/base2_property-android";
return rewriteUrlforAccount(account, url);
}
/**
* Returns the contacts group feed url for a specific account.
* @param account The account
* @param groupSyncId The group id
* @return The contacts feed url for a specific account and group.
*/
public static String getContactsFeedForGroup(Account account, String groupSyncId) {
String groupId = getCanonicalGroupsFeedForAccount(account);
try {
groupId = URLEncoder.encode(groupId, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("unable to url encode group: " + groupId);
}
return getContactsFeedForAccount(account) + "?group=" + groupId + "/" + groupSyncId;
}
/**
* Returns the groups feed url for a specific account.
* @param account The account
* @return The groups feed url for a specific account.
*/
public static String getGroupsFeedForAccount(Account account) {
String url = GROUPS_FEED_URL + account.name + "/base2_property-android";
return rewriteUrlforAccount(account, url);
}
/**
* Returns the groups feed url for a specific account that should be
* used as the foreign reference to this group, e.g. in the
* group membership element of the ContactEntry. The canonical groups
* feed always uses http (so it doesn't need to be rewritten) and it always
* uses the base projection.
* @param account The account
* @return The groups feed url for a specific account.
*/
public static String getCanonicalGroupsFeedForAccount(Account account) {
return GROUPS_FEED_URL + account.name + "/base";
}
/**
* Returns the photo feed url for a specific account.
* @param account The account
* @return The photo feed url for a specific account.
*/
public static String getPhotosFeedForAccount(Account account) {
String url = PHOTO_FEED_URL + account.name;
return rewriteUrlforAccount(account, url);
}
protected static boolean getFeedReturnsPartialDiffs() {
return true;
}
@Override
protected void updateQueryParameters(QueryParams params, GDataSyncData.FeedData feedSyncData) {
// we want to get the events ordered by last modified, so we can
// recover in case we cannot process the entire feed.
params.setParamValue("orderby", "lastmodified");
params.setParamValue("sortorder", "ascending");
// set showdeleted so that we get tombstones, only do this when we
// are doing an incremental sync
if (params.getUpdatedMin() != null) {
params.setParamValue("showdeleted", "true");
}
}
@Override
public void onSyncStarting(SyncContext context, Account account, boolean manualSync,
SyncResult result) {
mPerformedGetServerDiffs = false;
mIsManualSync = manualSync;
mPhotoDownloads = 0;
mPhotoUploads = 0;
super.onSyncStarting(context, account, manualSync, result);
}
@Override
public void onSyncEnding(SyncContext context, boolean success) {
final ContentResolver cr = getContext().getContentResolver();
if (success && mPerformedGetServerDiffs && !mSyncCanceled) {
final Account account = getAccount();
Cursor cursor = cr.query(
Photos.CONTENT_URI,
new String[]{Photos._SYNC_ID, Photos._SYNC_VERSION, Photos.PERSON_ID,
Photos.DOWNLOAD_REQUIRED}, ""
+ "_sync_account=? AND _sync_account_type=? AND download_required != 0",
new String[]{account.name, account.type}, null);
try {
if (cursor.getCount() != 0) {
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, mIsManualSync);
extras.putString("feed", ContactsSyncAdapter.getPhotosFeedForAccount(account));
ContentResolver.requestSync(account, Contacts.AUTHORITY, extras);
}
} finally {
cursor.close();
}
}
super.onSyncEnding(context, success);
}
public static void updateSubscribedFeeds(ContentResolver cr, Account account) {
Set<String> feedsToSync = Sets.newHashSet();
feedsToSync.add(getGroupsFeedForAccount(account));
addContactsFeedsToSync(cr, account, feedsToSync);
Cursor c = SubscribedFeeds.Feeds.query(cr, sSubscriptionProjection,
SubscribedFeeds.Feeds.AUTHORITY + "=?"
+ " AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"
+ " AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?",
new String[]{Contacts.AUTHORITY, account.name, account.type}, null);
try {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "scanning over subscriptions with authority "
+ Contacts.AUTHORITY + " and account " + account);
}
c.moveToNext();
while (!c.isAfterLast()) {
String feedInCursor = c.getString(1);
if (feedsToSync.contains(feedInCursor)) {
feedsToSync.remove(feedInCursor);
c.moveToNext();
} else {
c.deleteRow();
}
}
c.commitUpdates();
} finally {
c.close();
}
// any feeds remaining in feedsToSync need a subscription
for (String feed : feedsToSync) {
SubscribedFeeds.addFeed(cr, feed, account, Contacts.AUTHORITY, ContactsClient.SERVICE);
// request a sync of this feed
Bundle extras = new Bundle();
extras.putString("feed", feed);
ContentResolver.requestSync(account, Contacts.AUTHORITY, extras);
}
}
}