From 948c36f47ac5bb3c47c85cd6269b188a82f458c3 Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Mon, 27 Jul 2009 10:24:58 -0700 Subject: [PATCH] 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 --- AndroidManifest.xml | 12 + res/xml/syncadapter_contacts.xml | 26 + .../activity/setup/AccountSetupOptions.java | 6 + .../email/mail/store/ExchangeStore.java | 56 +- .../service/EasAuthenticatorService.java | 14 +- .../exchange/ContactsSyncAdapterService.java | 105 ++ src/com/android/exchange/EasSyncService.java | 11 +- ...eiver.java => EmailSyncAlarmReceiver.java} | 12 +- src/com/android/exchange/SyncManager.java | 133 ++- .../adapter/EasCalendarSyncAdapter.java | 4 +- .../adapter/EasContactsSyncAdapter.java | 936 +++++++++++++++--- .../exchange/adapter/EasEmailSyncAdapter.java | 14 +- .../exchange/adapter/EasFolderSyncParser.java | 2 +- .../exchange/adapter/EasSyncAdapter.java | 4 +- src/com/android/exchange/adapter/EasTags.java | 27 +- .../exchange/EasEmailSyncAdapterTests.java | 2 +- .../com/android/exchange/EasTagsTests.java | 45 + 17 files changed, 1152 insertions(+), 257 deletions(-) create mode 100644 res/xml/syncadapter_contacts.xml create mode 100644 src/com/android/exchange/ContactsSyncAdapterService.java rename src/com/android/exchange/{UserSyncAlarmReceiver.java => EmailSyncAlarmReceiver.java} (89%) create mode 100644 tests/src/com/android/exchange/EasTagsTests.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index aa2929b3e..44504b48b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -24,6 +24,7 @@ + @@ -174,6 +175,17 @@ > + + + + + + + + + + + + + + diff --git a/src/com/android/email/activity/setup/AccountSetupOptions.java b/src/com/android/email/activity/setup/AccountSetupOptions.java index 90db20e2d..5e4270711 100644 --- a/src/com/android/email/activity/setup/AccountSetupOptions.java +++ b/src/com/android/email/activity/setup/AccountSetupOptions.java @@ -19,6 +19,7 @@ package com.android.email.activity.setup; import com.android.email.Email; import com.android.email.R; import com.android.email.mail.Store; +import com.android.email.mail.store.ExchangeStore; import com.android.email.provider.EmailContent; import android.app.Activity; @@ -120,6 +121,11 @@ public class AccountSetupOptions extends Activity implements OnClickListener { mAccount.setSyncLookback(window); } mAccount.setDefaultAccount(mDefaultView.isChecked()); + // EAS needs a hook to store account information for use by AccountManager + if (!mAccount.isSaved() && mAccount.mHostAuthRecv != null + && mAccount.mHostAuthRecv.mProtocol.equals("eas")) { + ExchangeStore.addSystemAccount(this, mAccount); + } AccountSettingsUtils.commitSettings(this, mAccount); Email.setServicesEnabled(this); AccountSetupNames.actionSetNames(this, mAccount.mId); diff --git a/src/com/android/email/mail/store/ExchangeStore.java b/src/com/android/email/mail/store/ExchangeStore.java index 8d2964bd9..b302d6228 100644 --- a/src/com/android/email/mail/store/ExchangeStore.java +++ b/src/com/android/email/mail/store/ExchangeStore.java @@ -26,6 +26,8 @@ import com.android.email.mail.MessageRetrievalListener; import com.android.email.mail.MessagingException; import com.android.email.mail.Store; import com.android.email.mail.StoreSynchronizer; +import com.android.email.provider.EmailContent.Account; +import com.android.email.service.EasAuthenticatorService; import com.android.email.service.EmailServiceProxy; import com.android.exchange.Eas; import com.android.exchange.SyncManager; @@ -115,6 +117,34 @@ public class ExchangeStore extends Store { mTransport.checkSettings(mUri); } + static public void addSystemAccount(Context context, Account acct) { + // This code was taken from sample code in AccountsTester + Bundle options = new Bundle(); + options.putString(EasAuthenticatorService.OPTIONS_USERNAME, acct.mEmailAddress); + options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, acct.mHostAuthRecv.mPassword); + Future2Callback callback = new Future2Callback() { + public void run(Future2 future) { + try { + Bundle bundle = future.getResult(); + bundle.keySet(); + Log.d(LOG_TAG, "account added: " + bundle); + } catch (OperationCanceledException e) { + Log.d(LOG_TAG, "addAccount was canceled"); + } catch (IOException e) { + Log.d(LOG_TAG, "addAccount failed: " + e); + } catch (AuthenticatorException e) { + Log.d(LOG_TAG, "addAccount failed: " + e); + } + + } + }; + // Here's where we tell AccountManager about the new account. The addAccount + // method in AccountManager calls the addAccount method in our authenticator + // service (EasAuthenticatorService) + AccountManager.get(context).addAccount(Eas.ACCOUNT_MANAGER_TYPE, null, null, + options, null, callback, null); + } + @Override public Folder getFolder(String name) throws MessagingException { synchronized (mFolders) { @@ -287,32 +317,6 @@ public class ExchangeStore extends Store { } else { throw new MessagingException(result); } - } else { - // This code was taken from sample code in AccountsTester - Bundle options = new Bundle(); - options.putString("username", mUsername); - options.putString("password", mPassword); - Future2Callback callback = new Future2Callback() { - public void run(Future2 future) { - try { - Bundle bundle = future.getResult(); - bundle.keySet(); - Log.d(TAG, "account added: " + bundle); - } catch (OperationCanceledException e) { - Log.d(TAG, "addAccount was canceled"); - } catch (IOException e) { - Log.d(TAG, "addAccount failed: " + e); - } catch (AuthenticatorException e) { - Log.d(TAG, "addAccount failed: " + e); - } - - } - }; - // Here's where we tell AccountManager about the new account. The addAccount - // method in AccountManager calls the addAccount method in our authenticator - // service (EasAuthenticatorService) - AccountManager.get(mContext).addAccount(Eas.ACCOUNT_MANAGER_TYPE, null, null, - options, null, callback, null); } } catch (RemoteException e) { throw new MessagingException("Call to validate generated an exception", e); diff --git a/src/com/android/email/service/EasAuthenticatorService.java b/src/com/android/email/service/EasAuthenticatorService.java index 90be99b70..d60a166cb 100644 --- a/src/com/android/email/service/EasAuthenticatorService.java +++ b/src/com/android/email/service/EasAuthenticatorService.java @@ -25,10 +25,8 @@ import android.accounts.AccountManager; import android.accounts.Constants; import android.accounts.NetworkErrorException; import android.app.Service; -import android.content.Intent; import android.content.Context; -import android.content.pm.PermissionInfo; -import android.content.pm.PackageManager; +import android.content.Intent; import android.os.Bundle; import android.os.IBinder; @@ -38,6 +36,8 @@ import android.os.IBinder; * password. We will need to implement confirmPassword, confirmCredentials, and updateCredentials. */ public class EasAuthenticatorService extends Service { + public static final String OPTIONS_USERNAME = "username"; + public static final String OPTIONS_PASSWORD = "password"; class EasAuthenticator extends AbstractAccountAuthenticator { public EasAuthenticator(Context context) { @@ -50,8 +50,8 @@ public class EasAuthenticatorService extends Service { throws NetworkErrorException { // The Bundle we are passed has username and password set AccountManager.get(EasAuthenticatorService.this).blockingAddAccountExplicitly( - new Account(options.getString("username"), Eas.ACCOUNT_MANAGER_TYPE), - options.getString("password"), null); + new Account(options.getString(OPTIONS_USERNAME), Eas.ACCOUNT_MANAGER_TYPE), + options.getString(OPTIONS_PASSWORD), null); Bundle b = new Bundle(); b.putString(Constants.ACCOUNT_NAME_KEY, options.getString("username")); b.putString(Constants.ACCOUNT_TYPE_KEY, Eas.ACCOUNT_MANAGER_TYPE); @@ -84,7 +84,7 @@ public class EasAuthenticatorService extends Service { @Override public String getAuthTokenLabel(String authTokenType) { - // null means we don't have compartmentalized authtoken types + // null means we don't have compartmentalized authtoken types return null; } @@ -107,7 +107,7 @@ public class EasAuthenticatorService extends Service { public IBinder onBind(Intent intent) { // TODO Replace this with an appropriate constant in AccountManager, when it's created String authenticatorIntent = "android.accounts.AccountAuthenticator"; - + if (authenticatorIntent.equals(intent.getAction())) { return new EasAuthenticator(this).getIAccountAuthenticator().asBinder(); } else { diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java new file mode 100644 index 000000000..2c38b9da1 --- /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 d8f46ab92..0472aa9f2 100644 --- a/src/com/android/exchange/EasSyncService.java +++ b/src/com/android/exchange/EasSyncService.java @@ -347,6 +347,7 @@ public class EasSyncService extends InteractiveSyncService { 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 @@ public class EasSyncService extends InteractiveSyncService { 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 @@ public class EasSyncService extends InteractiveSyncService { 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 @@ public class EasSyncService extends InteractiveSyncService { 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 a027aa097..bea5328ce 100644 --- a/src/com/android/exchange/UserSyncAlarmReceiver.java +++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java @@ -31,7 +31,7 @@ import android.util.Log; 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 @@ import java.util.ArrayList; * 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 07a948927..2be090b71 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -59,7 +59,7 @@ import java.util.List; /** * 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 @@ public class SyncManager extends Service implements Runnable { MessageObserver mMessageObserver; String mNextWaitReason; IEmailServiceCallback mCallback; - + RemoteCallbackList mCallbackList = new RemoteCallbackList(); - + static private HashMap mWakeLocks = new HashMap(); static private HashMap mPendingIntents = new HashMap(); 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 @@ public class SyncManager extends Service implements Runnable { } }; + class AccountList extends ArrayList { + 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 { - 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 @@ public class SyncManager extends Service implements Runnable { 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 @@ public class SyncManager extends Service implements Runnable { super(handler); } + @Override public void onChange(boolean selfChange) { // See if there's anything to do... kick(); @@ -337,7 +339,7 @@ public class SyncManager extends Service implements Runnable { 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 @@ public class SyncManager extends Service implements Runnable { 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 @@ public class SyncManager extends Service implements Runnable { 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 @@ public class SyncManager extends Service implements Runnable { } 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 class SyncManager extends Service implements Runnable { public void run() { mStop = false; -// if (Debug.isDebuggerConnected()) { -// try { -// Thread.sleep(10000L); -// } catch (InterruptedException e) { -// } -// } - runAwake(-1); ContentResolver resolver = getContentResolver(); @@ -668,7 +673,7 @@ public class SyncManager extends Service implements Runnable { 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 @@ public class SyncManager extends Service implements Runnable { } long checkMailboxes () { + // First, see if any running mailboxes have been deleted + ArrayList deadMailboxes = new ArrayList(); + 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 @@ public class SyncManager extends Service implements Runnable { } return nextWait; } - + static public void serviceRequest(Mailbox m) { serviceRequest(m.mId, 5*SECS); } @@ -881,7 +902,7 @@ public class SyncManager extends Service implements Runnable { /** * 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 @@ public class SyncManager extends Service implements Runnable { } return true; } - + static public int getSyncStatus(long mailboxId) { synchronized (mSyncToken) { if (INSTANCE == null || INSTANCE.mServiceMap == null) { @@ -984,30 +1005,32 @@ public class SyncManager extends Service implements Runnable { * @param svc the service that is finished */ static public void done(AbstractSyncService svc) { - long mailboxId = svc.mMailboxId; - HashMap 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 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 @@ public class SyncManager extends Service implements Runnable { 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 d0b1c52f8..ccbb1d4c2 100644 --- a/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java +++ b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java @@ -29,8 +29,8 @@ import java.io.IOException; */ 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 5cd0855a5..a4ec93e63 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 @@ import java.util.ArrayList; */ 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 mDeletedIdList = new ArrayList(); - ArrayList mUpdatedIdList = new ArrayList(); - public EasContactsSyncAdapter(Mailbox mailbox) { - super(mailbox); + public EasContactsSyncAdapter(Mailbox mailbox, EasSyncService service) { + super(mailbox, service); } @Override @@ -55,57 +92,74 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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. + *

Type: TEXT

+ */ + 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 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 contactMethods = new ArrayList(); - ArrayList phones = new ArrayList(); + 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { } 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 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { serverId = getValue(); break; case EasTags.SYNC_APPLICATION_DATA: - addData(serverId, ops); + addData(serverId, ops, null); break; default: skipTag(); @@ -267,13 +395,11 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { } /** - * 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 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 ops = new ArrayList(); + ContactOperations ops = new ContactOperations(); while (nextTag(EasTags.SYNC_COMMANDS) != END) { if (tag == EasTags.SYNC_ADD) { addParser(ops); @@ -359,22 +486,363 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 { + 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 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 @@ public class EasContactsSyncAdapter extends EasSyncAdapter { 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 d502f5255..8fdb553df 100644 --- a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java +++ b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java @@ -63,8 +63,8 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { ArrayList mDeletedIdList = new ArrayList(); ArrayList mUpdatedIdList = new ArrayList(); - public EasEmailSyncAdapter(Mailbox mailbox) { - super(mailbox); + public EasEmailSyncAdapter(Mailbox mailbox, EasSyncService service) { + super(mailbox, service); } @Override @@ -72,10 +72,10 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { 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 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { } } + @Override public void wipe() { mContentResolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + mMailbox.mId, null); @@ -174,7 +175,7 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { 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 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { /* (non-Javadoc) * @see com.android.exchange.adapter.EasContentParser#commandsParser() */ + @Override public void commandsParser() throws IOException { ArrayList newEmails = new ArrayList(); ArrayList deletedEmails = new ArrayList(); @@ -436,7 +438,7 @@ public class EasEmailSyncAdapter extends EasSyncAdapter { 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 3d686948d..ea9df3456 100644 --- a/src/com/android/exchange/adapter/EasFolderSyncParser.java +++ b/src/com/android/exchange/adapter/EasFolderSyncParser.java @@ -51,7 +51,7 @@ import java.util.List; 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 cf06d86d7..f79b68bba 100644 --- a/src/com/android/exchange/adapter/EasSyncAdapter.java +++ b/src/com/android/exchange/adapter/EasSyncAdapter.java @@ -29,6 +29,7 @@ import java.io.IOException; */ 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 class EasSyncAdapter { 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 0f330e5b7..96de3fc9d 100644 --- a/src/com/android/exchange/adapter/EasTags.java +++ b/src/com/android/exchange/adapter/EasTags.java @@ -329,8 +329,9 @@ public class EasTags { }, { // 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 @@ public class EasTags { "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 @@ public class EasTags { "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 @@ public class EasTags { }, { // 0x05 Move - "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "Response", "Status", + "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus", "DstMsgId" }, { @@ -384,9 +385,9 @@ public class EasTags { { // 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 @@ public class EasTags { }, { // 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 @@ public class EasTags { }, { // 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 21b728497..fab55512c 100644 --- a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java +++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java @@ -51,7 +51,7 @@ public class EasEmailSyncAdapterTests extends AndroidTestCase { 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 000000000..8bd4cdaf3 --- /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 map = new HashMap(); + for (String[] page: allTags) { + for (String tag: page) { + assertTrue(!map.containsKey(tag)); + map.put(tag, true); + } + } + } +}