Add support for note and category upload for Contacts
* Also fixed a few random bugs found while debugging
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index c539e53..ae84c9d 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -471,10 +471,11 @@
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 5890ca7..6a693f9 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -488,15 +488,17 @@
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 5c9946f..e8fbc7c 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -38,8 +38,10 @@
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 @@
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 @@
childrenParser(children);
break;
- case Tags.CONTACTS_CATEGORIES:
- categoriesParser();
- break;
-
case Tags.CONTACTS_YOMI_COMPANY_NAME:
yomiCompanyName = getValue();
break;
@@ -434,6 +433,10 @@
// 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 @@
}
}
- 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 @@
* @return the matching NCV or null if not found
*/
private NamedContentValues findExistingData(ArrayList<NamedContentValues> 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 @@
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 @@
* @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 @@
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 @@
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 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 @@
}
}
+ 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 @@
// For each of these entities, create the change commands
ContentValues entityValues = entity.getEntityValues();
String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
+ ArrayList<Integer> groupIds = new ArrayList<Integer>();
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 @@
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 @@
} finally {
ei.close();
}
-
} catch (RemoteException e) {
Log.e(TAG, "Could not read dirty contacts.");
}