diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java index c539e538c..ae84c9d4b 100644 --- a/src/com/android/exchange/EasSyncService.java +++ b/src/com/android/exchange/EasSyncService.java @@ -471,10 +471,11 @@ public class EasSyncService extends InteractiveSyncService { userLog(mVersions); userLog("Using version " + mProtocolVersion); } else { + errorLog("No protocol versions in OPTIONS response"); throw new IOException(); } } else { - userLog("OPTIONS command failed; throwing IOException"); + errorLog("OPTIONS command failed; throwing IOException"); throw new IOException(); } } diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index 5890ca7cf..6a693f9d7 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -488,15 +488,17 @@ public class SyncManager extends Service implements Runnable { long id = c.getLong(Mailbox.CONTENT_ID_COLUMN); AbstractSyncService svc = INSTANCE.mServiceMap.get(id); // Tell the service we're done - svc.stop(); - // Interrupt the thread so that it can stop - Thread thread = svc.mThread; - thread.setName(thread.getName() + " (Stopped)"); - thread.interrupt(); - // Abandon the service - INSTANCE.mServiceMap.remove(id); - // And have it start naturally - kick(); + if (svc != null) { + svc.stop(); + // Interrupt the thread so that it can stop + Thread thread = svc.mThread; + thread.setName(thread.getName() + " (Stopped)"); + thread.interrupt(); + // Abandon the service + INSTANCE.mServiceMap.remove(id); + // And have it start naturally + kick(); + } } } } finally { diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java index 5c9946fc2..e8fbc7c89 100644 --- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java +++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java @@ -38,8 +38,10 @@ import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Note; @@ -64,6 +66,7 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { private static final String TAG = "EasContactsSyncAdapter"; private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?"; private static final String[] ID_PROJECTION = new String[] {RawContacts._ID}; + private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID}; // Note: These constants are likely to change; they are internal to this class now, but // may end up in the provider. @@ -359,10 +362,6 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { childrenParser(children); break; - case Tags.CONTACTS_CATEGORIES: - categoriesParser(); - break; - case Tags.CONTACTS_YOMI_COMPANY_NAME: yomiCompanyName = getValue(); break; @@ -434,6 +433,10 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { // TODO Handle Categories/Category // If we don't handle this properly, we'll lose the information if/when we // upload changes to the server! + case Tags.CONTACTS_CATEGORIES: + categoriesParser(ops, entity); + break; + case Tags.CONTACTS_COMPRESSED_RTF: // We don't use this, and it isn't necessary to upload, so we'll ignore it @@ -488,10 +491,11 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { } } - private void categoriesParser() throws IOException { + private void categoriesParser(ContactOperations ops, Entity entity) throws IOException { while (nextTag(Tags.CONTACTS_CATEGORIES) != END) { switch (tag) { case Tags.CONTACTS_CATEGORY: + ops.addGroup(entity, getValue()); default: skipTag(); } @@ -780,7 +784,7 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { * @return the matching NCV or null if not found */ private NamedContentValues findExistingData(ArrayList list, - String contentItemType, int type) { + String contentItemType, int type, String stringType) { NamedContentValues result = null; // Loop through the ncv's, looking for an existing row @@ -790,13 +794,20 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { if (Data.CONTENT_URI.equals(uri)) { String mimeType = cv.getAsString(Data.MIMETYPE); if (mimeType.equals(contentItemType)) { - if (type < 0 || cv.getAsInteger(Email.TYPE) == type) { + if (stringType != null) { + if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) { + result = namedContentValues; + } + // Note Email.TYPE could be ANY type column; they are all defined in + // the private CommonColumns class in ContactsContract + } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) { result = namedContentValues; } } } } + // TODO Handle deleted items // If we've found an existing data row, we'll delete it. Any rows left at the // end should be deleted... if (result != null) { @@ -819,15 +830,21 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { * @param entity the contact entity (or null if this is a new contact) * @param mimeType the mime type of this row * @param type the subtype of this row + * @param stringType for groups, the name of the group (type will be ignored), or null * @return the created SmartBuilder */ public SmartBuilder createBuilder(Entity entity, String mimeType, int type) { + return createBuilder(entity, mimeType, type, null); + } + + public SmartBuilder createBuilder(Entity entity, String mimeType, int type, + String stringType) { int contactId = mContactBackValue; SmartBuilder builder = null; if (entity != null) { NamedContentValues ncv = - findExistingData(entity.getSubValues(), mimeType, type); + findExistingData(entity.getSubValues(), mimeType, type, stringType); if (ncv != null) { builder = new SmartBuilder( ContentProviderOperation @@ -894,6 +911,13 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { add(builder.build()); } + public void addGroup(Entity entity, String group) { + SmartBuilder builder = + createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group); + builder.withValue(GroupMembership.GROUP_SOURCE_ID, group); + add(builder.build()); + } + public void addName(Entity entity, String givenName, String familyName, String middleName, String suffix, String displayName) { SmartBuilder builder = createBuilder(entity, StructuredName.CONTENT_ITEM_TYPE, -1); @@ -977,7 +1001,14 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { SmartBuilder builder = createBuilder(entity, Photo.CONTENT_ITEM_TYPE, -1); // We're always going to add this; it's not worth trying to figure out whether the // picture is the same as the one stored. - builder.withValue(Photo.PHOTO, Base64.decodeToObject(photo)); + byte[] pic = Base64.decode(photo); +// Bitmap b = BitmapFactory.decodeByteArray (pic, 0, pic.length); +// if (b == null) { +// mService.userLog("Bitmap creation failed"); +// } else { +// mService.userLog("W00t! Bitmap creation worked!"); +// } + builder.withValue(Photo.PHOTO, pic); add(builder.build()); } @@ -1050,6 +1081,9 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { public void addNote(Entity entity, String note) { SmartBuilder builder = createBuilder(entity, Note.CONTENT_ITEM_TYPE, -1); ContentValues cv = builder.cv; + if (note != null) { + note.replace("\r\n", "\n"); + } if (cv != null && cvCompareString(cv, Note.NOTE, note)) { return; } @@ -1245,6 +1279,21 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { } } + private void sendNote(Serializer s, ContentValues cv) throws IOException { + if (cv.containsKey(Note.NOTE)) { + // EAS won't accept note data with raw newline characters + String note = cv.getAsString(Note.NOTE).replace("\n", "\r\n"); + // Format of data depends on protocol version + if (mService.mProtocolVersionDouble >= 12.0) { + s.start(Tags.BASE_BODY); + s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); + s.end(); + } else { + s.data(Tags.CONTACTS_BODY, note); + } + } + } + private void sendChildren(Serializer s, ContentValues cv) throws IOException { boolean first = true; for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { @@ -1335,12 +1384,13 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { // For each of these entities, create the change commands ContentValues entityValues = entity.getEntityValues(); String serverId = entityValues.getAsString(RawContacts.SOURCE_ID); + ArrayList groupIds = new ArrayList(); if (first) { s.start(Tags.SYNC_COMMANDS); first = false; } s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId) - .start(Tags.SYNC_APPLICATION_DATA); + .start(Tags.SYNC_APPLICATION_DATA); // Write out the data here for (NamedContentValues ncv: entity.getSubValues()) { ContentValues cv = ncv.values; @@ -1367,15 +1417,43 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { sendOrganization(s, cv); } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { sendIm(s, cv); + } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { + // We must gather these, and send them together (below) + groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { - // TODO Should we upload this (it will be plain text) + sendNote(s, cv); } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { - // TODO Decide whether to upload new photos - // For now, we're not going to upload photos + // For now, the user can change the photo, but the change won't be + // uploaded. } else { mService.userLog("Contacts upsync, unknown data: " + mimeType); } } + + // Now, we'll send up groups, if any + if (!groupIds.isEmpty()) { + boolean groupFirst = true; + for (int id: groupIds) { + // Since we get id's from the provider, we need to find their names + Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id), + GROUP_PROJECTION, null, null, null); + try { + // Presumably, this should always succeed, but ... + if (c.moveToFirst()) { + if (groupFirst) { + s.start(Tags.CONTACTS_CATEGORIES); + groupFirst = false; + } + s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); + } + } finally { + c.close(); + } + } + if (!groupFirst) { + s.end(); + } + } s.end().end(); // ApplicationData & Change mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID)); } @@ -1385,7 +1463,6 @@ public class ContactsSyncAdapter extends AbstractSyncAdapter { } finally { ei.close(); } - } catch (RemoteException e) { Log.e(TAG, "Could not read dirty contacts."); }