Reimplement EAS contacts sync to work w/ new system facilities
* Modify to work with ContactsProvider2
* Modify to work with system AccountManager
* Modify to work with system SyncManager (for triggering user-change syncs)
* Sync server->client for adds/deletes implemented (CP2 doesn't handle delete yet)
* Sync server->client changes handled efficiently (only write changes)
* Some fields still not handled
* Rewrote most of the CPO code to handle server->client changes
* Sync client->server works for supported fields
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index aa2929b..44504b4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<!-- For EAS purposes; could be removed when EAS has a permanent home -->
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
@@ -174,6 +175,17 @@
>
</service>
+ <!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
+ <service
+ android:name="com.android.exchange.ContactsSyncAdapterService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter_contacts" />
+ </service>
+
<!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
<service
android:name="com.android.exchange.SyncManager"
diff --git a/res/xml/syncadapter_contacts.xml b/res/xml/syncadapter_contacts.xml
new file mode 100644
index 0000000..4691aee
--- /dev/null
+++ b/res/xml/syncadapter_contacts.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.contacts"
+ android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
new file mode 100644
index 0000000..2c38b9d
--- /dev/null
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+public class ContactsSyncAdapterService extends Service {
+ private final String TAG = "EAS ContactsSyncAdapterService";
+ private final SyncAdapterImpl mSyncAdapter;
+
+ private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
+ private static final String ACCOUNT_AND_TYPE_CONTACTS =
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
+
+ public ContactsSyncAdapterService() {
+ super();
+ mSyncAdapter = new SyncAdapterImpl();
+ }
+
+ private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+ public SyncAdapterImpl() {
+ super(ContactsSyncAdapterService.this);
+ }
+
+ @Override
+ public void performSync(Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult) {
+ try {
+ ContactsSyncAdapterService.this.performSync(account, extras,
+ authority, provider, syncResult);
+ } catch (OperationCanceledException e) {
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mSyncAdapter.getISyncAdapter().asBinder();
+ }
+
+ /**
+ * Partial integration with system SyncManager; we tell our EAS SyncManager to start a contacts
+ * sync when we get the signal from the system SyncManager.
+ * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
+ * be put in place at a later time.
+ */
+ private void performSync(Account account, Bundle extras, String authority,
+ ContentProviderClient provider, SyncResult syncResult)
+ throws OperationCanceledException {
+ ContentResolver cr = getContentResolver();
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(com.android.email.provider.EmailContent.Account.CONTENT_URI, ID_PROJECTION,
+ AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.mName}, null);
+ try {
+ if (accountCursor.moveToFirst()) {
+ long accountId = accountCursor.getLong(0);
+ // Now, find the contacts mailbox associated with the account
+ Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+ ACCOUNT_AND_TYPE_CONTACTS, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ Log.i(TAG, "Contact sync requested for " + account.mName);
+ // Ask for a sync from our sync manager
+ SyncManager.serviceRequest(mailboxCursor.getLong(0));
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index d8f46ab..0472aa9 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -347,6 +347,7 @@
return setupEASCommand(method, cmd, null);
}
+ @SuppressWarnings("deprecation")
private String makeUriString(String cmd, String extra) {
// Cache the authentication string and the command string
if (mDeviceId == null)
@@ -691,7 +692,7 @@
BufferedReader rdr = null;
String id;
if (f.exists() && f.canRead()) {
- rdr = new BufferedReader(new FileReader(f));
+ rdr = new BufferedReader(new FileReader(f), 128);
id = rdr.readLine();
rdr.close();
return id;
@@ -853,7 +854,9 @@
mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
try {
- if (mMailbox.mServerId.equals(Eas.ACCOUNT_MAILBOX)) {
+ if (mMailbox == null || mAccount == null) {
+ return;
+ } else if (mMailbox.mServerId.equals(Eas.ACCOUNT_MAILBOX)) {
runMain();
} else {
EasSyncAdapter target;
@@ -861,9 +864,9 @@
mProtocolVersion = mAccount.mProtocolVersion;
mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
if (mMailbox.mType == Mailbox.TYPE_CONTACTS)
- target = new EasContactsSyncAdapter(mMailbox);
+ target = new EasContactsSyncAdapter(mMailbox, this);
else {
- target = new EasEmailSyncAdapter(mMailbox);
+ target = new EasEmailSyncAdapter(mMailbox, this);
}
// We loop here because someone might have put a request in while we were syncing
// and we've missed that opportunity...
diff --git a/src/com/android/exchange/UserSyncAlarmReceiver.java b/src/com/android/exchange/EmailSyncAlarmReceiver.java
similarity index 89%
rename from src/com/android/exchange/UserSyncAlarmReceiver.java
rename to src/com/android/exchange/EmailSyncAlarmReceiver.java
index a027aa0..bea5328 100644
--- a/src/com/android/exchange/UserSyncAlarmReceiver.java
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -31,7 +31,7 @@
import java.util.ArrayList;
/**
- * UserSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
+ * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
* back to the Exchange server.
*
* Here's how this works for Email, for example:
@@ -40,15 +40,15 @@
* 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
* 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
* future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
- * 4) USAR Receiver's onReceive method is called
- * 5) USAR goes through all change and deletion records and compiles a list of mailboxes which have
+ * 4) ESAR Receiver's onReceive method is called
+ * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
* changes to be uploaded.
- * 6) USAR calls SyncManager to start syncs of those mailboxes
+ * 6) ESAR calls SyncManager to start syncs of those mailboxes
*
*/
-public class UserSyncAlarmReceiver extends BroadcastReceiver {
+public class EmailSyncAlarmReceiver extends BroadcastReceiver {
final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY, SyncColumns.DATA};
- private static String TAG = "UserSyncAlarm";
+ private static String TAG = "EmailSyncAlarm";
@Override
public void onReceive(Context context, Intent intent) {
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 07a9489..2be090b 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -59,7 +59,7 @@
/**
* The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
- * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
+ * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
* would be appropriate to use for IMAP push, when that functionality is added to the Email
* application.
*
@@ -91,17 +91,17 @@
MessageObserver mMessageObserver;
String mNextWaitReason;
IEmailServiceCallback mCallback;
-
+
RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
-
+
static private HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
static private HashMap<Long, PendingIntent> mPendingIntents =
new HashMap<Long, PendingIntent>();
static private WakeLock mWakeLock = null;
/**
- * Create the binder for EmailService implementation here. These are the calls that are
+ * Create the binder for EmailService implementation here. These are the calls that are
* defined in AbstractSyncService. Only validate is now implemented; loadAttachment currently
* spins its wheels counting up to 100%.
*/
@@ -183,24 +183,24 @@
}
};
+ class AccountList extends ArrayList<Account> {
+ private static final long serialVersionUID = 1L;
+
+ public boolean contains(long id) {
+ for (Account account: this) {
+ if (account.mId == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
class AccountObserver extends ContentObserver {
// mAccounts keeps track of Accounts that we care about (EAS for now)
AccountList mAccounts = new AccountList();
- class AccountList extends ArrayList<Account> {
- private static final long serialVersionUID = 1L;
-
- public boolean contains(long id) {
- for (Account account: this) {
- if (account.mId == id) {
- return true;
- }
- }
- return false;
- }
- }
-
public AccountObserver(Handler handler) {
super(handler);
Context context = getContext();
@@ -237,6 +237,7 @@
return false;
}
+ @Override
public void onChange(boolean selfChange) {
// A change to the list requires us to scan for deletions (to stop running syncs)
// At startup, we want to see what accounts exist and cache them
@@ -328,6 +329,7 @@
super(handler);
}
+ @Override
public void onChange(boolean selfChange) {
// See if there's anything to do...
kick();
@@ -337,7 +339,7 @@
class SyncedMessageObserver extends ContentObserver {
long maxChangedId = 0;
long maxDeletedId = 0;
- Intent syncAlarmIntent = new Intent(INSTANCE, UserSyncAlarmReceiver.class);
+ Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
PendingIntent syncAlarmPendingIntent =
PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
@@ -347,6 +349,7 @@
super(handler);
}
+ @Override
public void onChange(boolean selfChange) {
INSTANCE.log("SyncedMessage changed: (re)setting alarm for 10s");
alarmManager.set(AlarmManager.RTC_WAKEUP,
@@ -360,6 +363,7 @@
super(handler);
}
+ @Override
public void onChange(boolean selfChange) {
INSTANCE.log("MessageObserver");
// A rather blunt instrument here. But we don't have information about the URI that
@@ -374,7 +378,15 @@
}
return null;
}
-
+
+ static public AccountList getAccountList() {
+ if (INSTANCE != null) {
+ return INSTANCE.mAccountObserver.mAccounts;
+ } else {
+ return null;
+ }
+ }
+
public class SyncStatus {
static public final int NOT_RUNNING = 0;
static public final int DIED = 1;
@@ -651,13 +663,6 @@
public void run() {
mStop = false;
-// if (Debug.isDebuggerConnected()) {
-// try {
-// Thread.sleep(10000L);
-// } catch (InterruptedException e) {
-// }
-// }
-
runAwake(-1);
ContentResolver resolver = getContentResolver();
@@ -668,7 +673,7 @@
ConnectivityReceiver cr = new ConnectivityReceiver();
registerReceiver(cr, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
- ConnectivityManager cm =
+ ConnectivityManager cm =
(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
try {
@@ -721,6 +726,22 @@
}
long checkMailboxes () {
+ // First, see if any running mailboxes have been deleted
+ ArrayList<Long> deadMailboxes = new ArrayList<Long>();
+ synchronized (mSyncToken) {
+ for (long mailboxId: mServiceMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (m == null) {
+ deadMailboxes.add(mailboxId);
+ log("Stopping sync for mailbox " + mailboxId + "; record not found.");
+ }
+ }
+ }
+ // If so, stop them
+ for (Long mailboxId: deadMailboxes) {
+ stopManualSync(mailboxId);
+ }
+
long nextWait = 10*MINS;
long now = System.currentTimeMillis();
// Start up threads that need it...
@@ -800,7 +821,7 @@
}
return nextWait;
}
-
+
static public void serviceRequest(Mailbox m) {
serviceRequest(m.mId, 5*SECS);
}
@@ -881,7 +902,7 @@
/**
* Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
* an error state
- *
+ *
* @param mailboxId
* @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
*/
@@ -896,7 +917,7 @@
}
return true;
}
-
+
static public int getSyncStatus(long mailboxId) {
synchronized (mSyncToken) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) {
@@ -984,30 +1005,32 @@
* @param svc the service that is finished
*/
static public void done(AbstractSyncService svc) {
- long mailboxId = svc.mMailboxId;
- HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
- SyncError syncError = errorMap.get(mailboxId);
- INSTANCE.mServiceMap.remove(mailboxId);
- int exitStatus = svc.mExitStatus;
- switch (exitStatus) {
- case AbstractSyncService.EXIT_DONE:
- if (!svc.mPartRequests.isEmpty()) {
- // TODO Handle this case
- }
- errorMap.remove(mailboxId);
- break;
- case AbstractSyncService.EXIT_IO_ERROR:
- if (syncError != null) {
- syncError.escalate();
- } else {
- errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
- }
- kick();
- break;
- case AbstractSyncService.EXIT_LOGIN_FAILURE:
- case AbstractSyncService.EXIT_EXCEPTION:
- errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
- break;
+ synchronized(mSyncToken) {
+ long mailboxId = svc.mMailboxId;
+ HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
+ SyncError syncError = errorMap.get(mailboxId);
+ INSTANCE.mServiceMap.remove(mailboxId);
+ int exitStatus = svc.mExitStatus;
+ switch (exitStatus) {
+ case AbstractSyncService.EXIT_DONE:
+ if (!svc.mPartRequests.isEmpty()) {
+ // TODO Handle this case
+ }
+ errorMap.remove(mailboxId);
+ break;
+ case AbstractSyncService.EXIT_IO_ERROR:
+ if (syncError != null) {
+ syncError.escalate();
+ } else {
+ errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
+ }
+ kick();
+ break;
+ case AbstractSyncService.EXIT_LOGIN_FAILURE:
+ case AbstractSyncService.EXIT_EXCEPTION:
+ errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
+ break;
+ }
}
}
@@ -1034,6 +1057,6 @@
if (INSTANCE == null) {
return null;
}
- return (Context)INSTANCE;
+ return INSTANCE;
}
}
diff --git a/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java
index d0b1c52..ccbb1d4 100644
--- a/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java
@@ -29,8 +29,8 @@
*/
public class EasCalendarSyncAdapter extends EasSyncAdapter {
- public EasCalendarSyncAdapter(Mailbox mailbox) {
- super(mailbox);
+ public EasCalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
}
@Override
diff --git a/src/com/android/exchange/adapter/EasContactsSyncAdapter.java b/src/com/android/exchange/adapter/EasContactsSyncAdapter.java
index 5cd0855..a4ec93e 100644
--- a/src/com/android/exchange/adapter/EasContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasContactsSyncAdapter.java
@@ -18,15 +18,34 @@
package com.android.exchange.adapter;
import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.exchange.Eas;
import com.android.exchange.EasSyncService;
import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
import android.database.Cursor;
import android.net.Uri;
-import android.provider.Contacts;
-import android.provider.Contacts.People;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -39,14 +58,32 @@
*/
public class EasContactsSyncAdapter extends EasSyncAdapter {
- private static final String WHERE_SERVER_ID_AND_ACCOUNT = "_sync_id=?";
+ 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};
+
+ // Note: These constants are likely to change; they are internal to this class now, but
+ // may end up in the provider.
+ private static final int TYPE_EMAIL1 = 20;
+ private static final int TYPE_EMAIL2 = 21;
+ private static final int TYPE_EMAIL3 = 22;
+
+ private static final int TYPE_IM1 = 23;
+ private static final int TYPE_IM2 = 24;
+ private static final int TYPE_IM3 = 25;
+
+ private static final int TYPE_WORK2 = 26;
+ private static final int TYPE_HOME2 = 27;
+ private static final int TYPE_CAR = 28;
+ private static final int TYPE_COMPANY_MAIN = 29;
+ private static final int TYPE_MMS = 30;
+ private static final int TYPE_RADIO = 31;
ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
-
ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
- public EasContactsSyncAdapter(Mailbox mailbox) {
- super(mailbox);
+ public EasContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
}
@Override
@@ -55,57 +92,74 @@
return p.parse();
}
+ public static final class Extras {
+ private Extras() {}
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/easextras";
+
+ /**
+ * The note text.
+ * <P>Type: TEXT</P>
+ */
+ public static final String EXTRAS = "data2";
+ }
+
class EasContactsSyncParser extends EasContentParser {
String[] mBindArgument = new String[1];
-
String mMailboxIdAsString;
-
- StringBuilder mExtraData = new StringBuilder(1024);
+ Uri mAccountUri;
public EasContactsSyncParser(InputStream in, EasSyncService service) throws IOException {
super(in, service);
- //setDebug(true); // DON'T CHECK IN WITH THIS UNCOMMENTED
- }
-
- class ContactMethod {
- ContentValues values = new ContentValues();
-
- ContactMethod(int kind, int type, String value) {
- values.put(Contacts.ContactMethods.KIND, kind);
- values.put(Contacts.ContactMethods.TYPE, type);
- values.put(Contacts.ContactMethods.DATA, value);
- }
- }
-
- class Phone {
- ContentValues values = new ContentValues();
-
- Phone(int type, String value) {
- values.put(Contacts.Phones.TYPE, type);
- values.put(Contacts.Phones.NUMBER, value);
- }
+ mAccountUri = uriWithAccount(RawContacts.CONTENT_URI);
+ setDebug(false); // DON'T CHECK IN WITH THIS UNCOMMENTED
}
@Override
public void wipe() {
- // TODO Auto-generated method stub
+ // TODO Uncomment when the new provider works with this
+ //mContentResolver.delete(mAccountUri, null, null);
}
- void saveExtraData (int tag) throws IOException {
- mExtraData.append(name);
- mExtraData.append("~");
- mExtraData.append(getValue());
- mExtraData.append('~');
+ void saveExtraData (StringBuilder extras, int tag) throws IOException {
+ // TODO Handle containers (categories/children)
+ extras.append(tag);
+ extras.append("~");
+ extras.append(getValue());
+ extras.append('~');
}
- public void addData(String serverId, ArrayList<ContentProviderOperation> ops)
+ class Address {
+ String city;
+ String country;
+ String code;
+ String street;
+ String state;
+
+ boolean hasData() {
+ return city != null || country != null || code != null || state != null
+ || street != null;
+ }
+ }
+
+ public void addData(String serverId, ContactOperations ops, Entity entity)
throws IOException {
String firstName = null;
String lastName = null;
String companyName = null;
- ArrayList<ContactMethod> contactMethods = new ArrayList<ContactMethod>();
- ArrayList<Phone> phones = new ArrayList<Phone>();
+ String title = null;
+ Address home = new Address();
+ Address work = new Address();
+ Address other = new Address();
+
+ if (entity == null) {
+ ops.newContact(serverId);
+ }
+
+ StringBuilder extraData = new StringBuilder(1024);
+
while (nextTag(EasTags.SYNC_APPLICATION_DATA) != END) {
switch (tag) {
case EasTags.CONTACTS_FIRST_NAME:
@@ -117,33 +171,122 @@
case EasTags.CONTACTS_COMPANY_NAME:
companyName = getValue();
break;
+ case EasTags.CONTACTS_JOB_TITLE:
+ title = getValue();
+ break;
case EasTags.CONTACTS_EMAIL1_ADDRESS:
+ ops.addEmail(entity, TYPE_EMAIL1, getValue());
+ break;
case EasTags.CONTACTS_EMAIL2_ADDRESS:
+ ops.addEmail(entity, TYPE_EMAIL2, getValue());
+ break;
case EasTags.CONTACTS_EMAIL3_ADDRESS:
- contactMethods.add(new ContactMethod(Contacts.KIND_EMAIL,
- Contacts.ContactMethods.TYPE_OTHER, getValue()));
+ ops.addEmail(entity, TYPE_EMAIL3, getValue());
break;
case EasTags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
+ ops.addPhone(entity, TYPE_WORK2, getValue());
+ break;
case EasTags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
- phones.add(new Phone(Contacts.Phones.TYPE_WORK, getValue()));
+ ops.addPhone(entity, Phone.TYPE_WORK, getValue());
+ break;
+ case EasTags.CONTACTS2_MMS:
+ ops.addPhone(entity, TYPE_MMS, getValue());
break;
case EasTags.CONTACTS_BUSINESS_FAX_NUMBER:
- phones.add(new Phone(Contacts.Phones.TYPE_FAX_WORK, getValue()));
+ ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
+ break;
+ case EasTags.CONTACTS2_COMPANY_MAIN_PHONE:
+ ops.addPhone(entity, TYPE_COMPANY_MAIN, getValue());
break;
case EasTags.CONTACTS_HOME_FAX_NUMBER:
- phones.add(new Phone(Contacts.Phones.TYPE_FAX_HOME, getValue()));
+ ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
break;
case EasTags.CONTACTS_HOME_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_HOME, getValue());
+ break;
case EasTags.CONTACTS_HOME2_TELEPHONE_NUMBER:
- phones.add(new Phone(Contacts.Phones.TYPE_HOME, getValue()));
+ ops.addPhone(entity, TYPE_HOME2, getValue());
break;
case EasTags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
+ break;
case EasTags.CONTACTS_CAR_TELEPHONE_NUMBER:
- phones.add(new Phone(Contacts.Phones.TYPE_MOBILE, getValue()));
+ ops.addPhone(entity, TYPE_CAR, getValue());
+ break;
+ case EasTags.CONTACTS_RADIO_TELEPHONE_NUMBER:
+ ops.addPhone(entity, TYPE_RADIO, getValue());
break;
case EasTags.CONTACTS_PAGER_NUMBER:
- phones.add(new Phone(Contacts.Phones.TYPE_PAGER, getValue()));
+ ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
break;
+ case EasTags.CONTACTS2_IM_ADDRESS:
+ ops.addIm(entity, TYPE_IM1, getValue());
+ break;
+ case EasTags.CONTACTS2_IM_ADDRESS_2:
+ ops.addIm(entity, TYPE_IM2, getValue());
+ break;
+ case EasTags.CONTACTS2_IM_ADDRESS_3:
+ ops.addIm(entity, TYPE_IM3, getValue());
+ break;
+ case EasTags.CONTACTS_BUSINESS_ADDRESS_CITY:
+ work.city = getValue();
+ break;
+ case EasTags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
+ work.country = getValue();
+ break;
+ case EasTags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
+ work.code = getValue();
+ break;
+ case EasTags.CONTACTS_BUSINESS_ADDRESS_STATE:
+ work.state = getValue();
+ break;
+ case EasTags.CONTACTS_BUSINESS_ADDRESS_STREET:
+ work.street = getValue();
+ break;
+ case EasTags.CONTACTS_HOME_ADDRESS_CITY:
+ home.city = getValue();
+ break;
+ case EasTags.CONTACTS_HOME_ADDRESS_COUNTRY:
+ home.country = getValue();
+ break;
+ case EasTags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
+ home.code = getValue();
+ break;
+ case EasTags.CONTACTS_HOME_ADDRESS_STATE:
+ home.state = getValue();
+ break;
+ case EasTags.CONTACTS_HOME_ADDRESS_STREET:
+ home.street = getValue();
+ break;
+ case EasTags.CONTACTS_OTHER_ADDRESS_CITY:
+ other.city = getValue();
+ break;
+ case EasTags.CONTACTS_OTHER_ADDRESS_COUNTRY:
+ other.country = getValue();
+ break;
+ case EasTags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
+ other.code = getValue();
+ break;
+ case EasTags.CONTACTS_OTHER_ADDRESS_STATE:
+ other.state = getValue();
+ break;
+ case EasTags.CONTACTS_OTHER_ADDRESS_STREET:
+ other.street = getValue();
+ break;
+
+ case EasTags.CONTACTS_CHILDREN:
+ childrenParser(extraData);
+ break;
+
+ case EasTags.CONTACTS_CATEGORIES:
+ categoriesParser(extraData);
+ break;
+
+ // TODO We'll add this later
+ case EasTags.CONTACTS_PICTURE:
+ getValue();
+ break;
+
// All tags that we don't use (except for a few like picture and body) need to
// be saved, even if we're not using them. Otherwise, when we upload changes,
// those items will be deleted back on the server.
@@ -151,64 +294,31 @@
case EasTags.CONTACTS_ASSISTANT_NAME:
case EasTags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
case EasTags.CONTACTS_BIRTHDAY:
- case EasTags.CONTACTS_BUSINESS_ADDRESS_CITY:
- case EasTags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
- case EasTags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
- case EasTags.CONTACTS_BUSINESS_ADDRESS_STATE:
- case EasTags.CONTACTS_BUSINESS_ADDRESS_STREET:
- case EasTags.CONTACTS_CATEGORIES:
- case EasTags.CONTACTS_CATEGORY:
- case EasTags.CONTACTS_CHILDREN:
- case EasTags.CONTACTS_CHILD:
case EasTags.CONTACTS_DEPARTMENT:
case EasTags.CONTACTS_FILE_AS:
- case EasTags.CONTACTS_HOME_ADDRESS_CITY:
- case EasTags.CONTACTS_HOME_ADDRESS_COUNTRY:
- case EasTags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
- case EasTags.CONTACTS_HOME_ADDRESS_STATE:
- case EasTags.CONTACTS_HOME_ADDRESS_STREET:
- case EasTags.CONTACTS_JOB_TITLE:
+ case EasTags.CONTACTS_TITLE:
case EasTags.CONTACTS_MIDDLE_NAME:
case EasTags.CONTACTS_OFFICE_LOCATION:
- case EasTags.CONTACTS_OTHER_ADDRESS_CITY:
- case EasTags.CONTACTS_OTHER_ADDRESS_COUNTRY:
- case EasTags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
- case EasTags.CONTACTS_OTHER_ADDRESS_STATE:
- case EasTags.CONTACTS_OTHER_ADDRESS_STREET:
- case EasTags.CONTACTS_RADIO_TELEPHONE_NUMBER:
case EasTags.CONTACTS_SPOUSE:
case EasTags.CONTACTS_SUFFIX:
- case EasTags.CONTACTS_TITLE:
case EasTags.CONTACTS_WEBPAGE:
case EasTags.CONTACTS_YOMI_COMPANY_NAME:
case EasTags.CONTACTS_YOMI_FIRST_NAME:
case EasTags.CONTACTS_YOMI_LAST_NAME:
case EasTags.CONTACTS_COMPRESSED_RTF:
- //case EasTags.CONTACTS_PICTURE:
case EasTags.CONTACTS2_CUSTOMER_ID:
case EasTags.CONTACTS2_GOVERNMENT_ID:
- case EasTags.CONTACTS2_IM_ADDRESS:
- case EasTags.CONTACTS2_IM_ADDRESS_2:
- case EasTags.CONTACTS2_IM_ADDRESS_3:
case EasTags.CONTACTS2_MANAGER_NAME:
- case EasTags.CONTACTS2_COMPANY_MAIN_PHONE:
case EasTags.CONTACTS2_ACCOUNT_NAME:
case EasTags.CONTACTS2_NICKNAME:
- case EasTags.CONTACTS2_MMS:
- saveExtraData(tag);
+ saveExtraData(extraData, tag);
break;
default:
skipTag();
}
}
- // Ok, ready to create our contact...
- // First pass, no batch... Eventually, move to changesParser
- ContentValues values = new ContentValues();
-
- // TODO Do something with the extras (i.e. find a home for them)
- String extraData = mExtraData.toString();
- mService.userLog(extraData);
+ ops.addExtras(entity, extraData.toString());
// We must have first name, last name, or company name
String name;
@@ -225,31 +335,49 @@
} else {
return;
}
+ ops.addName(entity, firstName, lastName, name);
- values.put(Contacts.People.NAME, name);
- values.put("_sync_id", serverId);
- // TODO Use proper value here; need to ask jham
- //values.put("_sync_account", "EAS");
- Uri contactUri =
- Contacts.People.createPersonInMyContactsGroup(mContentResolver, values);
-
- Uri contactMethodsUri = Uri.withAppendedPath(contactUri,
- Contacts.People.ContactMethods.CONTENT_DIRECTORY);
- for (ContactMethod cm: contactMethods) {
- mContentResolver.insert(contactMethodsUri, cm.values);
- //ops.add(ContentProviderOperation
- // .newInsert(contactMethodsUri).withValues(cm.values).build());
+ if (work.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
+ work.state, work.country, work.code);
+ }
+ if (home.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
+ home.state, home.country, home.code);
+ }
+ if (other.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
+ other.state, other.country, other.code);
}
- Uri phoneUri = Uri.withAppendedPath(contactUri, People.Phones.CONTENT_DIRECTORY);
- for (Phone phone: phones) {
- mContentResolver.insert(phoneUri, phone.values);
- //ops.add(ContentProviderOperation
- // .newInsert(phoneUri).withValues(phone.values).build());
+ if (companyName != null) {
+ ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title);
}
}
- public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ private void categoriesParser(StringBuilder extras) throws IOException {
+ while (nextTag(EasTags.CONTACTS_CATEGORIES) != END) {
+ switch (tag) {
+ case EasTags.CONTACTS_CATEGORY:
+ saveExtraData(extras, tag);
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private void childrenParser(StringBuilder extras) throws IOException {
+ while (nextTag(EasTags.CONTACTS_CHILDREN) != END) {
+ switch (tag) {
+ case EasTags.CONTACTS_CHILD:
+ saveExtraData(extras, tag);
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ public void addParser(ContactOperations ops) throws IOException {
String serverId = null;
while (nextTag(EasTags.SYNC_ADD) != END) {
switch (tag) {
@@ -257,7 +385,7 @@
serverId = getValue();
break;
case EasTags.SYNC_APPLICATION_DATA:
- addData(serverId, ops);
+ addData(serverId, ops, null);
break;
default:
skipTag();
@@ -267,13 +395,11 @@
private Cursor getServerIdCursor(String serverId) {
mBindArgument[0] = serverId;
- //bindArguments[1] = "EAS";
- // TODO Find proper constant for _id
- return mContentResolver.query(Contacts.People.CONTENT_URI, new String[] {"_id"},
- WHERE_SERVER_ID_AND_ACCOUNT, mBindArgument, null);
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
+ mBindArgument, null);
}
- public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ public void deleteParser(ContactOperations ops) throws IOException {
while (nextTag(EasTags.SYNC_DELETE) != END) {
switch (tag) {
case EasTags.SYNC_SERVER_ID:
@@ -283,12 +409,7 @@
try {
if (c.moveToFirst()) {
mService.userLog("Deleting " + serverId);
- mContentResolver.delete(ContentUris
- .withAppendedId(Contacts.People.CONTENT_URI, c.getLong(0)),
- null, null);
- //ops.add(ContentProviderOperation.newDelete(
- // ContentUris.withAppendedId(Contacts.People.CONTENT_URI,
- // c.getLong(0))).build());
+ ops.delete(c.getLong(0));
}
} finally {
c.close();
@@ -311,14 +432,13 @@
}
/**
- * A change operation on a contact is implemented as a delete followed by an add, since the
- * change data is always a full contact.
- *
+ * Changes are handled row by row, and only changed/new rows are acted upon
* @param ops the array of pending ContactProviderOperations.
* @throws IOException
*/
- public void changeParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ public void changeParser(ContactOperations ops) throws IOException {
String serverId = null;
+ Entity entity = null;
while (nextTag(EasTags.SYNC_CHANGE) != END) {
switch (tag) {
case EasTags.SYNC_SERVER_ID:
@@ -326,28 +446,35 @@
Cursor c = getServerIdCursor(serverId);
try {
if (c.moveToFirst()) {
- mContentResolver.delete(ContentUris
- .withAppendedId(Contacts.People.CONTENT_URI, c.getLong(0)),
- null, null);
- //ops.add(ContentProviderOperation.newDelete(
- // ContentUris.withAppendedId(Contacts.People.CONTENT_URI,
- // c.getLong(0))).build());
- mService.userLog("Changing " + serverId);
+ // TODO Handle deleted individual rows...
+ try {
+ EntityIterator entityIterator =
+ mContentResolver.queryEntities(ContentUris
+ .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)),
+ null, null, null);
+ if (entityIterator.hasNext()) {
+ entity = entityIterator.next();
+ }
+ mService.userLog("Changing contact " + serverId);
+ } catch (RemoteException e) {
+ }
}
} finally {
c.close();
}
break;
case EasTags.SYNC_APPLICATION_DATA:
- addData(serverId, ops);
+ addData(serverId, ops, entity);
+ break;
default:
skipTag();
}
}
}
+ @Override
public void commandsParser() throws IOException {
- ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ ContactOperations ops = new ContactOperations();
while (nextTag(EasTags.SYNC_COMMANDS) != END) {
if (tag == EasTags.SYNC_ADD) {
addParser(ops);
@@ -359,22 +486,363 @@
skipTag();
}
- // Batch provider operations here
-// try {
-// mService.mContext.getContentResolver()
-// .applyBatch(ContactsProvider.EMAIL_AUTHORITY, ops);
-// } catch (RemoteException e) {
-// // There is nothing to be done here; fail by returning null
-// } catch (OperationApplicationException e) {
-// // There is nothing to be done here; fail by returning null
-// }
+ // Execute these all at once...
+ ops.execute();
+
+ if (ops.mResults != null) {
+ ContentValues cv = new ContentValues();
+ cv.put(RawContacts.DIRTY, 0);
+ for (int i = 0; i < ops.mContactIndexCount; i++) {
+ int index = ops.mContactIndexArray[i];
+ Uri u = ops.mResults[index].uri;
+ if (u != null) {
+ String idString = u.getLastPathSegment();
+ mService.mContentResolver.update(RawContacts.CONTENT_URI, cv,
+ RawContacts._ID + "=" + idString, null);
+ }
+ }
+ }
+
+ // Update the sync key in the database
+ mService.userLog("Contacts SyncKey saved as: " + mMailbox.mSyncKey);
+ ContentValues cv = new ContentValues();
+ cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey);
+ Mailbox.update(mContext, Mailbox.CONTENT_URI, mMailbox.mId, cv);
mService.userLog("Contacts SyncKey confirmed as: " + mMailbox.mSyncKey);
}
}
+
+ private Uri uriWithAccount(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, mService.mAccount.mEmailAddress)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .build();
+ }
+
+ /**
+ * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
+ * ContentProvider. It has, in addition to the Builder, ContentValues which, if present,
+ * represent the current values of that row, that can be compared against current values to
+ * see whether an update is even necessary. The methods on SmartBuilder are delegated to
+ * the Builder.
+ */
+ private class SmartBuilder {
+ Builder builder;
+ ContentValues cv;
+
+ public SmartBuilder(Builder _builder) {
+ builder = _builder;
+ }
+
+ public SmartBuilder(Builder _builder, NamedContentValues _ncv) {
+ builder = _builder;
+ cv = _ncv.values;
+ }
+
+ SmartBuilder withValues(ContentValues values) {
+ builder.withValues(values);
+ return this;
+ }
+
+ SmartBuilder withValueBackReference(String key, int previousResult) {
+ builder.withValueBackReference(key, previousResult);
+ return this;
+ }
+
+ ContentProviderOperation build() {
+ return builder.build();
+ }
+
+ SmartBuilder withValue(String key, Object value) {
+ builder.withValue(key, value);
+ return this;
+ }
+ }
+
+ private class ContactOperations extends ArrayList<ContentProviderOperation> {
+ private static final long serialVersionUID = 1L;
+ private int mCount = 0;
+ private int mContactBackValue = mCount;
+ private int[] mContactIndexArray = new int[10];
+ private int mContactIndexCount = 0;
+ private ContentProviderResult[] mResults = null;
+
+ @Override
+ public boolean add(ContentProviderOperation op) {
+ super.add(op);
+ mCount++;
+ return true;
+ }
+
+ public void newContact(String serverId) {
+ Builder builder = ContentProviderOperation
+ .newInsert(uriWithAccount(RawContacts.CONTENT_URI));
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.SOURCE_ID, serverId);
+ builder.withValues(values);
+ mContactBackValue = mCount;
+ mContactIndexArray[mContactIndexCount++] = mCount;
+ add(builder.build());
+ }
+
+ public void delete(long id) {
+ add(ContentProviderOperation.newDelete(ContentUris
+ .withAppendedId(RawContacts.CONTENT_URI, id)).build());
+ }
+
+ public void execute() {
+ try {
+ mService.userLog("Executing " + size() + " CPO's");
+ mResults = mService.mContext.getContentResolver()
+ .applyBatch(ContactsContract.AUTHORITY, this);
+ } catch (RemoteException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting contact during server update", e);
+ } catch (OperationApplicationException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting contact during server update", e);
+ }
+ }
+
+ /**
+ * Generate the uri for the data row associated with this NamedContentValues object
+ * @param ncv the NamedContentValues object
+ * @return a uri that can be used to refer to this row
+ */
+ private Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
+ long id = ncv.values.getAsLong(RawContacts._ID);
+ Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
+ return dataUri;
+ }
+
+ /**
+ * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
+ * tries to find a match, returning it
+ * @param list the list of NCV's from the contact entity
+ * @param contentItemType the mime type we're looking for
+ * @param type the subtype (e.g. HOME, WORK, etc.)
+ * @return the matching NCV or null if not found
+ */
+ private NamedContentValues findExistingData(ArrayList<NamedContentValues> list,
+ String contentItemType, int type) {
+ NamedContentValues result = null;
+
+ // Loop through the ncv's, looking for an existing row
+ for (NamedContentValues namedContentValues: list) {
+ Uri uri = namedContentValues.uri;
+ ContentValues cv = namedContentValues.values;
+ if (Data.CONTENT_URI.equals(uri)) {
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(contentItemType)) {
+ if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
+ result = namedContentValues;
+ }
+ }
+ }
+ }
+
+ // If we've found an existing data row, we'll delete it. Any rows left at the
+ // end should be deleted...
+ if (result != null) {
+ list.remove(result);
+ }
+
+ // Return the row found (or null)
+ return result;
+ }
+
+ /**
+ * Create a wrapper for a builder (insert or update) that also includes the NCV for
+ * an existing row of this type. If the SmartBuilder's cv field is not null, then
+ * it represents the current (old) values of this field. The caller can then check
+ * whether the field is now different and needs to be updated; if it's not different,
+ * the caller will simply return and not generate a new CPO. Otherwise, the builder
+ * should have its content values set, and the built CPO should be added to the
+ * ContactOperations list.
+ *
+ * @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
+ * @return the created SmartBuilder
+ */
+ public SmartBuilder createBuilder(Entity entity, String mimeType, int type) {
+ int contactId = mContactBackValue;
+ SmartBuilder builder = null;
+
+ if (entity != null) {
+ NamedContentValues ncv =
+ findExistingData(entity.getSubValues(), mimeType, type);
+ if (ncv != null) {
+ builder = new SmartBuilder(
+ ContentProviderOperation
+ .newUpdate(dataUriFromNamedContentValues(ncv)),
+ ncv);
+ } else {
+ contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
+ }
+ }
+
+ if (builder == null) {
+ builder =
+ new SmartBuilder(ContentProviderOperation.newInsert(Data.CONTENT_URI));
+ if (entity == null) {
+ builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
+ } else {
+ builder.withValue(Data.RAW_CONTACT_ID, contactId);
+ }
+
+ builder.withValue(Data.MIMETYPE, mimeType);
+ }
+
+ // Return the appropriate builder (insert or update)
+ // Caller will fill in the appropriate values; note MIMETYPE is already set
+ return builder;
+ }
+
+ /**
+ * Compare a column in a ContentValues with an (old) value, and see if they are the
+ * same. For this purpose, null and an empty string are considered the same.
+ * @param cv a ContentValues object, from a NamedContentValues
+ * @param column a column that might be in the ContentValues
+ * @param oldValue an old value (or null) to check against
+ * @return whether the column's value in the ContentValues matches oldValue
+ */
+ private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
+ if (cv.containsKey(column)) {
+ if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
+ return true;
+ }
+ } else if (oldValue == null || oldValue.length() == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ public void addEmail(Entity entity, int type, String email) {
+ SmartBuilder builder = createBuilder(entity, Email.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Email.DATA, email)) {
+ return;
+ }
+ builder.withValue(Email.TYPE, type);
+ builder.withValue(Email.DATA, email);
+ add(builder.build());
+ }
+
+ public void addName(Entity entity, String givenName, String familyName,
+ String displayName) {
+ SmartBuilder builder = createBuilder(entity, StructuredName.CONTENT_ITEM_TYPE, -1);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
+ cvCompareString(cv, StructuredName.FAMILY_NAME, familyName)) {
+ return;
+ }
+ builder.withValue(StructuredName.GIVEN_NAME, givenName);
+ builder.withValue(StructuredName.FAMILY_NAME, familyName);
+ add(builder.build());
+ }
+
+ public void addPhoto() {
+// final int photoRes = Generator.pickRandom(PHOTO_POOL);
+//
+// Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+// builder.withValueBackReference(Data.CONTACT_ID, 0);
+// builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+// builder.withValue(Photo.PHOTO, getPhotoBytes(photoRes));
+//
+// this.add(builder.build());
+ }
+
+ public void addPhone(Entity entity, int type, String phone) {
+ SmartBuilder builder = createBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
+ return;
+ }
+ builder.withValue(Phone.TYPE, type);
+ builder.withValue(Phone.NUMBER, phone);
+ add(builder.build());
+ }
+
+ public void addPostal(Entity entity, int type, String street, String city, String state,
+ String country, String code) {
+ SmartBuilder builder = createBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
+ type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
+ cvCompareString(cv, StructuredPostal.STREET, street) &&
+ cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
+ cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
+ cvCompareString(cv, StructuredPostal.REGION, state)) {
+ return;
+ }
+ builder.withValue(StructuredPostal.TYPE, type);
+ builder.withValue(StructuredPostal.CITY, city);
+ builder.withValue(StructuredPostal.STREET, street);
+ builder.withValue(StructuredPostal.COUNTRY, country);
+ builder.withValue(StructuredPostal.POSTCODE, code);
+ builder.withValue(StructuredPostal.REGION, state);
+ add(builder.build());
+ }
+
+ public void addIm(Entity entity, int type, String account) {
+ SmartBuilder builder = createBuilder(entity, Im.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Im.DATA, account)) {
+ return;
+ }
+ builder.withValue(Im.TYPE, type);
+ builder.withValue(Im.DATA, account);
+ add(builder.build());
+ }
+
+ public void addOrganization(Entity entity, int type, String company, String title) {
+ SmartBuilder builder = createBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
+ cvCompareString(cv, Organization.TITLE, title)) {
+ return;
+ }
+ builder.withValue(Organization.TYPE, type);
+ builder.withValue(Organization.COMPANY, company);
+ builder.withValue(Organization.TITLE, title);
+ add(builder.build());
+ }
+
+ // TODO
+ public void addNote(String note) {
+ Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValueBackReference(Data.RAW_CONTACT_ID, mContactBackValue);
+ builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+ builder.withValue(Note.NOTE, note);
+ add(builder.build());
+ }
+
+ public void addExtras(Entity entity, String extras) {
+ SmartBuilder builder = createBuilder(entity, Extras.CONTENT_ITEM_TYPE, -1);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Extras.EXTRAS, extras)) {
+ return;
+ }
+ builder.withValue(Extras.EXTRAS, extras);
+ add(builder.build());
+ }
+ }
+
@Override
public void cleanup(EasSyncService service) {
+ // Mark the changed contacts dirty = 0
+ // TODO Put this in a single batch
+ ContactOperations ops = new ContactOperations();
+ for (Long id: mUpdatedIdList) {
+ ops.add(ContentProviderOperation
+ .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id))
+ .withValue(RawContacts.DIRTY, 0).build());
+ }
+
+ ops.execute();
}
@Override
@@ -382,8 +850,206 @@
return "Contacts";
}
+ private void sendEmail(EasSerializer s, ContentValues cv) throws IOException {
+ String value = cv.getAsString(Email.DATA);
+ switch (cv.getAsInteger(Email.TYPE)) {
+ case TYPE_EMAIL1:
+ s.data("Email1Address", value);
+ break;
+ case TYPE_EMAIL2:
+ s.data("Email2Address", value);
+ break;
+ case TYPE_EMAIL3:
+ s.data("Email3Address", value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendIm(EasSerializer s, ContentValues cv) throws IOException {
+ String value = cv.getAsString(Email.DATA);
+ switch (cv.getAsInteger(Email.TYPE)) {
+ case TYPE_IM1:
+ s.data("IMAddress", value);
+ break;
+ case TYPE_IM2:
+ s.data("IMAddress2", value);
+ break;
+ case TYPE_IM3:
+ s.data("IMAddress3", value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendOnePostal(EasSerializer s, ContentValues cv, String[] fieldNames)
+ throws IOException{
+ if (cv.containsKey(StructuredPostal.CITY)) {
+ s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
+ }
+ if (cv.containsKey(StructuredPostal.COUNTRY)) {
+ s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
+ }
+ if (cv.containsKey(StructuredPostal.POSTCODE)) {
+ s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
+ }
+ if (cv.containsKey(StructuredPostal.REGION)) {
+ s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
+ }
+ if (cv.containsKey(StructuredPostal.STREET)) {
+ s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
+ }
+ }
+
+ private void sendStructuredPostal(EasSerializer s, ContentValues cv) throws IOException {
+ switch (cv.getAsInteger(StructuredPostal.TYPE)) {
+ case StructuredPostal.TYPE_HOME:
+ sendOnePostal(s, cv, new String[] {"HomeAddressCity", "HomeAddressCountry",
+ "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet"});
+ break;
+ case StructuredPostal.TYPE_WORK:
+ sendOnePostal(s, cv, new String[] {"BusinessAddressCity", "BusinessAddressCountry",
+ "BusinessAddressPostalCode", "BusinessAddressState",
+ "BusinessAddressStreet"});
+ break;
+ case StructuredPostal.TYPE_OTHER:
+ sendOnePostal(s, cv, new String[] {"OtherAddressCity", "OtherAddressCountry",
+ "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet"});
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendStructuredName(EasSerializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(StructuredName.FAMILY_NAME)) {
+ s.data("LastName", cv.getAsString(StructuredName.FAMILY_NAME));
+ }
+ if (cv.containsKey(StructuredName.GIVEN_NAME)) {
+ s.data("FirstName", cv.getAsString(StructuredName.GIVEN_NAME));
+ }
+ }
+
+ private void sendOrganization(EasSerializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Organization.TITLE)) {
+ s.data("JobTitle", cv.getAsString(Organization.TITLE));
+ }
+ if (cv.containsKey(Organization.COMPANY)) {
+ s.data("CompanyName", cv.getAsString(Organization.COMPANY));
+ }
+ }
+
+ private void sendPhone(EasSerializer s, ContentValues cv) throws IOException {
+ String value = cv.getAsString(Phone.NUMBER);
+ switch (cv.getAsInteger(Phone.TYPE)) {
+ case TYPE_WORK2:
+ s.data("Business2TelephoneNumber", value);
+ break;
+ case Phone.TYPE_WORK:
+ s.data("BusinessTelephoneNumber", value);
+ break;
+ case TYPE_MMS:
+ s.data("MMS", value);
+ break;
+ case Phone.TYPE_FAX_WORK:
+ s.data("BusinessFaxNumber", value);
+ break;
+ case TYPE_COMPANY_MAIN:
+ s.data("CompanyMainPhone", value);
+ break;
+ case Phone.TYPE_HOME:
+ s.data("HomeTelephoneNumber", value);
+ break;
+ case TYPE_HOME2:
+ s.data("Home2TelephoneNumber", value);
+ break;
+ case Phone.TYPE_MOBILE:
+ s.data("MobileTelephoneNumber", value);
+ break;
+ case TYPE_CAR:
+ s.data("CarTelephoneNumber", value);
+ break;
+ case Phone.TYPE_PAGER:
+ s.data("PagerNumber", value);
+ break;
+ case TYPE_RADIO:
+ s.data("RadioTelephoneNumber", value);
+ break;
+ case Phone.TYPE_FAX_HOME:
+ s.data("HomeFaxNumber", value);
+ break;
+ case TYPE_EMAIL2:
+ s.data("Email2Address", value);
+ break;
+ case TYPE_EMAIL3:
+ s.data("Email3Address", value);
+ break;
+ default:
+ break;
+ }
+ }
+
@Override
public boolean sendLocalChanges(EasSerializer s, EasSyncService service) throws IOException {
+ // First, let's find Contacts that have changed.
+ ContentResolver cr = service.mContentResolver;
+ Uri uri = RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, service.mAccount.mEmailAddress)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .build();
+
+ try {
+ // Get them all atomically
+ //EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+ EntityIterator ei = cr.queryEntities(uri, null, null, null);
+ boolean first = true;
+ while (ei.hasNext()) {
+ Entity entity = ei.next();
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
+ if (first) {
+ s.start("Commands");
+ first = false;
+ }
+ s.start("Change").data("ServerId", serverId).start("ApplicationData");
+ // Write out the data here
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ ContentValues cv = ncv.values;
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+ sendEmail(s, cv);
+ } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
+ sendPhone(s, cv);
+ } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+ sendStructuredName(s, cv);
+ } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
+ sendStructuredPostal(s, cv);
+ } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
+ sendOrganization(s, cv);
+ } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
+ sendIm(s, cv);
+ } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
+
+ } else if (mimeType.equals(Extras.CONTENT_ITEM_TYPE)) {
+
+ } else {
+ mService.userLog("Contacts upsync, unknown data: " + mimeType);
+ }
+ }
+ s.end("ApplicationData").end("Change");
+ mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
+ }
+ if (!first) {
+ s.end("Commands");
+ }
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not read dirty contacts.");
+ }
+
return false;
}
}
diff --git a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java
index d502f52..8fdb553 100644
--- a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java
@@ -63,8 +63,8 @@
ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
- public EasEmailSyncAdapter(Mailbox mailbox) {
- super(mailbox);
+ public EasEmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
}
@Override
@@ -72,10 +72,10 @@
EasEmailSyncParser p = new EasEmailSyncParser(is, service);
return p.parse();
}
-
+
public class EasEmailSyncParser extends EasContentParser {
- private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
+ private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
private String mMailboxIdAsString;
@@ -88,6 +88,7 @@
}
}
+ @Override
public void wipe() {
mContentResolver.delete(Message.CONTENT_URI,
Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
@@ -174,7 +175,7 @@
while (nextTag(EasTags.SYNC_ADD) != END) {
switch (tag) {
- case EasTags.SYNC_SERVER_ID:
+ case EasTags.SYNC_SERVER_ID:
msg.mServerId = getValue();
break;
case EasTags.SYNC_APPLICATION_DATA:
@@ -389,6 +390,7 @@
/* (non-Javadoc)
* @see com.android.exchange.adapter.EasContentParser#commandsParser()
*/
+ @Override
public void commandsParser() throws IOException {
ArrayList<Message> newEmails = new ArrayList<Message>();
ArrayList<Long> deletedEmails = new ArrayList<Long>();
@@ -436,7 +438,7 @@
mMailbox.toContentValues()).build());
addCleanupOps(ops);
-
+
try {
mService.mContext.getContentResolver()
.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
diff --git a/src/com/android/exchange/adapter/EasFolderSyncParser.java b/src/com/android/exchange/adapter/EasFolderSyncParser.java
index 3d68694..ea9df34 100644
--- a/src/com/android/exchange/adapter/EasFolderSyncParser.java
+++ b/src/com/android/exchange/adapter/EasFolderSyncParser.java
@@ -51,7 +51,7 @@
public class EasFolderSyncParser extends EasParser {
- private static boolean DEBUG_LOGGING = false;
+ private static boolean DEBUG_LOGGING = true;
public static final String TAG = "FolderSyncParser";
diff --git a/src/com/android/exchange/adapter/EasSyncAdapter.java b/src/com/android/exchange/adapter/EasSyncAdapter.java
index cf06d86..f79b68b 100644
--- a/src/com/android/exchange/adapter/EasSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasSyncAdapter.java
@@ -29,6 +29,7 @@
*/
public abstract class EasSyncAdapter {
public Mailbox mMailbox;
+ public EasSyncService mService;
// Create the data for local changes that need to be sent up to the server
public abstract boolean sendLocalChanges(EasSerializer s, EasSyncService service)
@@ -41,8 +42,9 @@
public abstract String getCollectionName();
public abstract void cleanup(EasSyncService service);
- public EasSyncAdapter(Mailbox mailbox) {
+ public EasSyncAdapter(Mailbox mailbox, EasSyncService service) {
mMailbox = mailbox;
+ mService = service;
}
}
diff --git a/src/com/android/exchange/adapter/EasTags.java b/src/com/android/exchange/adapter/EasTags.java
index 0f330e5..96de3fc 100644
--- a/src/com/android/exchange/adapter/EasTags.java
+++ b/src/com/android/exchange/adapter/EasTags.java
@@ -329,8 +329,9 @@
},
{
// 0x01 Contacts
- "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "Body",
- "BodySize", "BodyTruncated", "Business2TelephoneNumber", "BusinessAddressCity",
+ "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "ContactsBody",
+ "ContactsBodySize", "ContactsBodyTruncated", "Business2TelephoneNumber",
+ "BusinessAddressCity",
"BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState",
"BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber",
"CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child",
@@ -338,8 +339,8 @@
"FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry",
"HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber",
"HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber",
- "OfficeLocation", "OfficeAddressCity", "OfficeAddressCountry",
- "OfficeAddressPostalCode", "OfficeAddressState", "OfficeAddressStreet", "PagerNumber",
+ "OfficeLocation", "OtherAddressCity", "OtherAddressCountry",
+ "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet", "PagerNumber",
"RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName",
"YomiFirstName", "YomiLastName", "CompressedRTF", "Picture"
},
@@ -354,7 +355,7 @@
"Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek",
"Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear",
"StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData",
- "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "ContentClass",
+ "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "EmailContentClass",
"FlagType", "CompleteTime"
},
{
@@ -375,7 +376,7 @@
},
{
// 0x05 Move
- "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "Response", "Status",
+ "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus",
"DstMsgId"
},
{
@@ -384,9 +385,9 @@
{
// 0x07 FolderHierarchy
"Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type",
- "Response", "Status", "ContentClass", "Changes", "FolderAdd", "FolderDelete",
- "FolderUpdate", "FolderSyncKey", "FolderCreate", "FolderDelete", "FolderUpdate",
- "FolderSync", "Count", "Version"
+ "FolderResponse", "FolderStatus", "FolderContentClass", "Changes", "FolderAdd",
+ "FolderDelete", "FolderUpdate", "FolderSyncKey", "FolderFolderCreate",
+ "FolderFolderDelete", "FolderFolderUpdate", "FolderSync", "Count", "FolderVersion"
},
{
// 0x08 MeetingResponse
@@ -407,12 +408,12 @@
},
{
// 0x0D Ping
- "Ping", "AutdState", "Status", "HeartbeatInterval", "PingFolders", "PingFolder",
+ "Ping", "AutdState", "PingStatus", "HeartbeatInterval", "PingFolders", "PingFolder",
"PingId", "PingClass", "MaxFolders"
},
{
// 0x0E Provision
- "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "Status",
+ "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
"RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
"AlphanumericDevicePasswordRequired",
"DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength",
@@ -436,8 +437,8 @@
},
{
// 0x10 Gal
- "DisplayName", "Phone", "Office", "Title", "Company", "Alias", "FirstName", "LastName",
- "HomePhone", "MobilePhone", "EmailAddress"
+ "GalDisplayName", "GalPhone", "GalOffice", "GalTitle", "GalCompany", "GalAlias",
+ "GalFirstName", "GalLastName", "GalHomePhone", "GalMobilePhone", "GalEmailAddress"
},
{
// 0x11 AirSyncBase
diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
index 21b7284..fab5551 100644
--- a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
@@ -51,7 +51,7 @@
service.mContext = getContext();
service.mMailbox = mailbox;
service.mAccount = account;
- EasEmailSyncAdapter adapter = new EasEmailSyncAdapter(mailbox);
+ EasEmailSyncAdapter adapter = new EasEmailSyncAdapter(mailbox, service);
EasEmailSyncParser p;
p = adapter.new EasEmailSyncParser(getTestInputStream(), service);
// Test a few known types
diff --git a/tests/src/com/android/exchange/EasTagsTests.java b/tests/src/com/android/exchange/EasTagsTests.java
new file mode 100644
index 0000000..8bd4cda
--- /dev/null
+++ b/tests/src/com/android/exchange/EasTagsTests.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.adapter.EasEmailSyncAdapter;
+import com.android.exchange.adapter.EasTags;
+import com.android.exchange.adapter.EasEmailSyncAdapter.EasEmailSyncParser;
+
+import android.test.AndroidTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+public class EasTagsTests extends AndroidTestCase {
+
+ // Make sure there are no duplicates in the tags table
+ public void testNoDuplicates() {
+ String[][] allTags = EasTags.pages;
+ HashMap<String, Boolean> map = new HashMap<String, Boolean>();
+ for (String[] page: allTags) {
+ for (String tag: page) {
+ assertTrue(!map.containsKey(tag));
+ map.put(tag, true);
+ }
+ }
+ }
+}