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