Merge "Import revised translations" into froyo
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 198c9dd..a21a23d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -170,6 +170,23 @@
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
+
+        <!--
+            This activity catches shortcuts to account created on Android 1.6 and before,
+            and redirects to MessageList.
+            singleTask is necessary to make sure the activity is really launched.
+            Without it, the framework brings up the app to front, but doesn't necessarily
+            launch the activity.
+        -->
+        <activity
+            android:name=".activity.FolderMessageList"
+            android:launchMode="singleTask"
+            >
+            <intent-filter>
+                <!-- This action is only to allow an entry point for launcher shortcuts -->
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
                 
         <activity
             android:name=".activity.MessageView"
diff --git a/src/com/android/exchange/CalendarSyncAdapterService.java b/src/com/android/exchange/CalendarSyncAdapterService.java
index d2f4ecf..357d67f 100644
--- a/src/com/android/exchange/CalendarSyncAdapterService.java
+++ b/src/com/android/exchange/CalendarSyncAdapterService.java
@@ -43,6 +43,8 @@
 
     private static final String ACCOUNT_AND_TYPE_CALENDAR =
         MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
+    private static final String DIRTY_IN_ACCOUNT =
+        Events._SYNC_DIRTY + "=1 AND " + Events._SYNC_ACCOUNT + "=?";
 
     public CalendarSyncAdapterService() {
         super();
@@ -93,16 +95,13 @@
             throws OperationCanceledException {
         ContentResolver cr = context.getContentResolver();
         boolean logging = Eas.USER_LOG;
-        if (logging) {
-            Log.d(TAG, "performSync");
-        }
         if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
             Cursor c = cr.query(Events.CONTENT_URI,
-                    new String[] {Events._ID}, Events._SYNC_DIRTY + "=1", null, null);
+                    new String[] {Events._ID}, DIRTY_IN_ACCOUNT, new String[] {account.name}, null);
             try {
                 if (!c.moveToFirst()) {
                     if (logging) {
-                        Log.d(TAG, "Upload sync; no changes");
+                        Log.d(TAG, "No changes for " + account.name);
                     }
                     return;
                 }
@@ -125,7 +124,7 @@
                 try {
                      if (mailboxCursor.moveToFirst()) {
                         if (logging) {
-                            Log.d(TAG, "Calendar sync requested for " + account.name);
+                            Log.d(TAG, "Upload sync requested for " + account.name);
                         }
                         // Ask for a sync from our sync manager
                         SyncManager.serviceRequest(mailboxCursor.getLong(0),
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 4aee013..0d6695f 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -86,7 +86,7 @@
 import android.provider.Calendar.Events;
 import android.util.Log;
 import android.util.Xml;
-import android.util.base64.Base64;
+import android.util.Base64;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -696,7 +696,7 @@
      */
     static public GalResult searchGal(Context context, long accountId, String filter)
     {
-        Account acct = SyncManager.getAccountList().getById(accountId);
+        Account acct = SyncManager.getAccountById(accountId);
         if (acct != null) {
             HostAuth ha = HostAuth.restoreHostAuthWithId(context, acct.mHostAuthKeyRecv);
             EasSyncService svc = new EasSyncService("%GalLookupk%");
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 527f03b..14d839a 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -173,10 +173,12 @@
     public static final int PING_STATUS_UNABLE = 3;
 
     // We synchronize on this for all actions affecting the service and error maps
-    private static Object sSyncToken = new Object();
+    private static final Object sSyncLock = new Object();
     // All threads can use this lock to wait for connectivity
-    public static Object sConnectivityLock = new Object();
+    public static final Object sConnectivityLock = new Object();
     public static boolean sConnectivityHold = false;
+    // Keep our cached list of active Accounts here
+    public static final AccountList sAccountList = new AccountList();
 
     // Keeps track of running services (by mailbox id)
     private HashMap<Long, AbstractSyncService> mServiceMap =
@@ -189,7 +191,6 @@
     private HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
     // The actual WakeLock obtained by SyncManager
     private WakeLock mWakeLock = null;
-    private static final AccountList EMPTY_ACCOUNT_LIST = new AccountList();
 
     // Observers that we use to look for changed mail-related data
     private Handler mHandler = new Handler();
@@ -268,8 +269,6 @@
             IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
             if (cb != null) {
                 cb.syncMailboxStatus(mailboxId, statusCode, progress);
-            } else if (INSTANCE != null) {
-                log("orphan syncMailboxStatus, id=" + mailboxId + " status=" + statusCode);
             }
         }
     };
@@ -337,7 +336,7 @@
         public void hostChanged(long accountId) throws RemoteException {
             SyncManager syncManager = INSTANCE;
             if (syncManager == null) return;
-            synchronized (sSyncToken) {
+            synchronized (sSyncLock) {
                 HashMap<Long, SyncError> syncErrorMap = syncManager.mSyncErrorMap;
                 ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
                 // Go through the various error mailboxes
@@ -402,7 +401,7 @@
         private static final long serialVersionUID = 1L;
 
         public boolean contains(long id) {
-            for (Account account: this) {
+            for (Account account : this) {
                 if (account.mId == id) {
                     return true;
                 }
@@ -411,7 +410,7 @@
         }
 
         public Account getById(long id) {
-            for (Account account: this) {
+            for (Account account : this) {
                 if (account.mId == id) {
                     return account;
                 }
@@ -421,29 +420,30 @@
     }
 
     class AccountObserver extends ContentObserver {
-        // mAccounts keeps track of Accounts that we care about (EAS for now)
-        AccountList mAccounts = new AccountList();
         String mSyncableEasMailboxSelector = null;
         String mEasAccountSelector = null;
 
         public AccountObserver(Handler handler) {
             super(handler);
             // At startup, we want to see what EAS accounts exist and cache them
-            Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
-                    null, null, null);
-            try {
-                collectEasAccounts(c, mAccounts);
-            } finally {
-                c.close();
-            }
-
-            // Create the account mailbox for any account that doesn't have one
             Context context = getContext();
-            for (Account account: mAccounts) {
-                int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" + account.mId,
-                        null);
-                if (cnt == 0) {
-                    addAccountMailbox(account.mId);
+            synchronized (sAccountList) {
+                Cursor c = getContentResolver().query(Account.CONTENT_URI,
+                        Account.CONTENT_PROJECTION, null, null, null);
+                // Build the account list from the cursor
+                try {
+                    collectEasAccounts(c, sAccountList);
+                } finally {
+                    c.close();
+                }
+
+                // Create an account mailbox for any account without one
+                for (Account account : sAccountList) {
+                    int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
+                            + account.mId, null);
+                    if (cnt == 0) {
+                        addAccountMailbox(account.mId);
+                    }
                 }
             }
         }
@@ -457,13 +457,15 @@
             if (mSyncableEasMailboxSelector == null) {
                 StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
                 boolean first = true;
-                for (Account account: mAccounts) {
-                    if (!first) {
-                        sb.append(',');
-                    } else {
-                        first = false;
+                synchronized (sAccountList) {
+                    for (Account account : sAccountList) {
+                        if (!first) {
+                            sb.append(',');
+                        } else {
+                            first = false;
+                        }
+                        sb.append(account.mId);
                     }
-                    sb.append(account.mId);
                 }
                 sb.append(')');
                 mSyncableEasMailboxSelector = sb.toString();
@@ -480,13 +482,15 @@
             if (mEasAccountSelector == null) {
                 StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
                 boolean first = true;
-                for (Account account: mAccounts) {
-                    if (!first) {
-                        sb.append(',');
-                    } else {
-                        first = false;
+                synchronized (sAccountList) {
+                    for (Account account : sAccountList) {
+                        if (!first) {
+                            sb.append(',');
+                        } else {
+                            first = false;
+                        }
+                        sb.append(account.mId);
                     }
-                    sb.append(account.mId);
                 }
                 sb.append(')');
                 mEasAccountSelector = sb.toString();
@@ -498,91 +502,94 @@
             return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
         }
 
+        private void onAccountChanged() {
+            maybeStartSyncManagerThread();
+            Context context = getContext();
+
+            // A change to the list requires us to scan for deletions (stop running syncs)
+            // At startup, we want to see what accounts exist and cache them
+            AccountList currentAccounts = new AccountList();
+            Cursor c = getContentResolver().query(Account.CONTENT_URI,
+                    Account.CONTENT_PROJECTION, null, null, null);
+            try {
+                collectEasAccounts(c, currentAccounts);
+                synchronized (sAccountList) {
+                    for (Account account : sAccountList) {
+                        // Ignore accounts not fully created
+                        if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
+                            log("Account observer noticed incomplete account; ignoring");
+                            continue;
+                        } else if (!currentAccounts.contains(account.mId)) {
+                            // This is a deletion; shut down any account-related syncs
+                            stopAccountSyncs(account.mId, true);
+                            // Delete this from AccountManager...
+                            android.accounts.Account acct = new android.accounts.Account(
+                                    account.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+                            AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
+                            mSyncableEasMailboxSelector = null;
+                            mEasAccountSelector = null;
+                        } else {
+                            // An account has changed
+                            Account updatedAccount = Account.restoreAccountWithId(context,
+                                    account.mId);
+                            if (account.mSyncInterval != updatedAccount.mSyncInterval
+                                    || account.mSyncLookback != updatedAccount.mSyncLookback) {
+                                // Set pushable boxes' interval to the interval of the Account
+                                ContentValues cv = new ContentValues();
+                                cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
+                                getContentResolver().update(Mailbox.CONTENT_URI, cv,
+                                        WHERE_IN_ACCOUNT_AND_PUSHABLE, new String[] {
+                                            Long.toString(account.mId)
+                                        });
+                                // Stop all current syncs; the appropriate ones will restart
+                                log("Account " + account.mDisplayName + " changed; stop syncs");
+                                stopAccountSyncs(account.mId, true);
+                            }
+
+                            // See if this account is no longer on security hold
+                            if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
+                                releaseSyncHolds(SyncManager.this,
+                                        AbstractSyncService.EXIT_SECURITY_FAILURE, account);
+                            }
+
+                            // Put current values into our cached account
+                            account.mSyncInterval = updatedAccount.mSyncInterval;
+                            account.mSyncLookback = updatedAccount.mSyncLookback;
+                            account.mFlags = updatedAccount.mFlags;
+                        }
+                    }
+                    // Look for new accounts
+                    for (Account account : currentAccounts) {
+                        if (!sAccountList.contains(account.mId)) {
+                            // This is an addition; create our magic hidden mailbox...
+                            log("Account observer found new account: " + account.mDisplayName);
+                            addAccountMailbox(account.mId);
+                            // Don't forget to cache the HostAuth
+                            HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
+                                    account.mHostAuthKeyRecv);
+                            account.mHostAuthRecv = ha;
+                            sAccountList.add(account);
+                            mSyncableEasMailboxSelector = null;
+                            mEasAccountSelector = null;
+                        }
+                    }
+                    // Finally, make sure our account list is up to date
+                    sAccountList.clear();
+                    sAccountList.addAll(currentAccounts);
+                }
+            } finally {
+                c.close();
+            }
+
+            // See if there's anything to do...
+            kick("account changed");
+        }
+
         @Override
         public void onChange(boolean selfChange) {
             new Thread(new Runnable() {
                public void run() {
-                    maybeStartSyncManagerThread();
-                    Context context = getContext();
-
-                    // A change to the list requires us to scan for deletions (stop running syncs)
-                    // At startup, we want to see what accounts exist and cache them
-                    AccountList currentAccounts = new AccountList();
-                    Cursor c = getContentResolver().query(Account.CONTENT_URI,
-                            Account.CONTENT_PROJECTION, null, null, null);
-                    try {
-                        collectEasAccounts(c, currentAccounts);
-                        for (Account account : mAccounts) {
-                            // Ignore accounts not fully created
-                            if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
-                                log("Account observer noticed incomplete account; ignoring");
-                                continue;
-                            } else if (!currentAccounts.contains(account.mId)) {
-                                // This is a deletion; shut down any account-related syncs
-                                stopAccountSyncs(account.mId, true);
-                                // Delete this from AccountManager...
-                                android.accounts.Account acct =
-                                    new android.accounts.Account(account.mEmailAddress,
-                                            Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                                AccountManager.get(SyncManager.this).removeAccount(acct,
-                                        null, null);
-                                mSyncableEasMailboxSelector = null;
-                                mEasAccountSelector = null;
-                            } else {
-                                // An account has changed
-                                Account updatedAccount =
-                                    Account.restoreAccountWithId(context, account.mId);
-                                if (account.mSyncInterval != updatedAccount.mSyncInterval ||
-                                        account.mSyncLookback != updatedAccount.mSyncLookback) {
-                                    // Set pushable boxes' interval to the interval of the Account
-                                    ContentValues cv = new ContentValues();
-                                    cv.put(MailboxColumns.SYNC_INTERVAL,
-                                            updatedAccount.mSyncInterval);
-                                    getContentResolver().update(Mailbox.CONTENT_URI, cv,
-                                            WHERE_IN_ACCOUNT_AND_PUSHABLE,
-                                            new String[] {Long.toString(account.mId)});
-                                    // Stop all current syncs; the appropriate ones will restart
-                                    log("Account " + account.mDisplayName + " changed; stop syncs");
-                                    stopAccountSyncs(account.mId, true);
-                                }
-
-                                // See if this account is no longer on security hold
-                                if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
-                                    releaseSyncHolds(SyncManager.this,
-                                            AbstractSyncService.EXIT_SECURITY_FAILURE, account);
-                                }
-
-                                // Put current values into our cached account
-                                account.mSyncInterval = updatedAccount.mSyncInterval;
-                                account.mSyncLookback = updatedAccount.mSyncLookback;
-                                account.mFlags = updatedAccount.mFlags;
-                            }
-                        }
-
-                        // Look for new accounts
-                        for (Account account: currentAccounts) {
-                            if (!mAccounts.contains(account.mId)) {
-                                // This is an addition; create our magic hidden mailbox...
-                                log("Account observer found new account: " + account.mDisplayName);
-                                addAccountMailbox(account.mId);
-                                // Don't forget to cache the HostAuth
-                                HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
-                                        account.mHostAuthKeyRecv);
-                                account.mHostAuthRecv = ha;
-                                mAccounts.add(account);
-                                mSyncableEasMailboxSelector = null;
-                                mEasAccountSelector = null;
-                            }
-                        }
-
-                        // Finally, make sure mAccounts is up to date
-                        mAccounts = currentAccounts;
-                    } finally {
-                        c.close();
-                    }
-
-                    // See if there's anything to do...
-                    kick("account changed");
+                   onAccountChanged();
                 }}).start();
         }
 
@@ -799,10 +806,8 @@
         return sCallbackProxy;
     }
 
-    static public AccountList getAccountList() {
-        SyncManager syncManager = INSTANCE;
-        if (syncManager == null) return EMPTY_ACCOUNT_LIST;
-        return syncManager.mAccountObserver.mAccounts;
+    static public Account getAccountById(long accountId) {
+        return sAccountList.getById(accountId);
     }
 
     static public String getEasAccountSelector() {
@@ -811,10 +816,6 @@
         return syncManager.mAccountObserver.getAccountKeyWhere();
     }
 
-    private Account getAccountById(long accountId) {
-        return mAccountObserver.mAccounts.getById(accountId);
-    }
-
     public class SyncStatus {
         static public final int NOT_RUNNING = 0;
         static public final int DIED = 1;
@@ -844,6 +845,28 @@
         }
     }
 
+    private void logSyncHolds() {
+        if (Eas.USER_LOG && !mSyncErrorMap.isEmpty()) {
+            log("Sync holds:");
+            long time = System.currentTimeMillis();
+            synchronized (sSyncLock) {
+                for (long mailboxId : mSyncErrorMap.keySet()) {
+                    Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+                    if (m == null) {
+                        log("Mailbox " + mailboxId + " no longer exists");
+                    } else {
+                        SyncError error = mSyncErrorMap.get(mailboxId);
+                        log("Mailbox " + m.mDisplayName + ", error = " + error.reason
+                                + ", fatal = " + error.fatal);
+                        if (error.holdEndTime > 0) {
+                            log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Release a specific type of hold (the reason) for the specified Account; if the account
      * is null, mailboxes from all accounts with the specified hold will be released
@@ -856,7 +879,7 @@
     }
 
     private void releaseSyncHoldsImpl(Context context, int reason, Account account) {
-        synchronized(sSyncToken) {
+        synchronized(sSyncLock) {
             ArrayList<Long> releaseList = new ArrayList<Long>();
             for (long mailboxId: mSyncErrorMap.keySet()) {
                 if (account != null) {
@@ -901,9 +924,10 @@
                     if (syncManager != null) {
                         android.accounts.Account[] accountMgrList = AccountManager.get(syncManager)
                             .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
-                        AccountList providerList = getAccountList();
-                        reconcileAccountsWithAccountManager(syncManager,
-                                providerList, accountMgrList, false, mResolver);
+                        synchronized (sAccountList) {
+                            reconcileAccountsWithAccountManager(syncManager, sAccountList,
+                                    accountMgrList, false, mResolver);
+                        }
                     }
                 }
             }.start();
@@ -1117,7 +1141,7 @@
     }
 
     private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
-        synchronized (sSyncToken) {
+        synchronized (sSyncLock) {
             List<Long> deletedBoxes = new ArrayList<Long>();
             for (Long mid : mServiceMap.keySet()) {
                 Mailbox box = Mailbox.restoreMailboxWithId(this, mid);
@@ -1159,7 +1183,7 @@
                     Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
         try {
             if (c.moveToFirst()) {
-                synchronized(sSyncToken) {
+                synchronized(sSyncLock) {
                     Mailbox m = new Mailbox().restore(c);
                     Account acct = Account.restoreAccountWithId(context, accountId);
                     if (acct == null) {
@@ -1421,10 +1445,11 @@
      * Make our sync settings match those of AccountManager
      */
     private void checkPIMSyncSettings() {
-        List<Account> easAccounts = getAccountList();
-        for (Account easAccount: easAccounts) {
-            updatePIMSyncSettings(easAccount, Mailbox.TYPE_CONTACTS, ContactsContract.AUTHORITY);
-            updatePIMSyncSettings(easAccount, Mailbox.TYPE_CALENDAR, Calendar.AUTHORITY);
+        synchronized (sAccountList) {
+            for (Account account : sAccountList) {
+                updatePIMSyncSettings(account, Mailbox.TYPE_CONTACTS, ContactsContract.AUTHORITY);
+                updatePIMSyncSettings(account, Mailbox.TYPE_CALENDAR, Calendar.AUTHORITY);
+            }
         }
     }
 
@@ -1512,15 +1537,6 @@
         }
     }
 
-    private void releaseConnectivityLock(String reason) {
-        // Clear i/o error holds for all accounts
-        releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null);
-        synchronized (sConnectivityLock) {
-            sConnectivityLock.notifyAll();
-        }
-        kick(reason);
-    }
-
     public class ConnectivityReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -1533,7 +1549,10 @@
                     if (state == State.CONNECTED) {
                         info += " CONNECTED";
                         log(info);
-                        releaseConnectivityLock("connected");
+                        synchronized (sConnectivityLock) {
+                            sConnectivityLock.notifyAll();
+                        }
+                        kick("connected");
                     } else if (state == State.DISCONNECTED) {
                         info += " DISCONNECTED";
                         log(info);
@@ -1552,8 +1571,10 @@
                 // Otherwise, stop all syncs
                 } else {
                     log("Background data off: stop all syncs");
-                    for (Account account: SyncManager.getAccountList())
-                        SyncManager.stopAccountSyncs(account.mId);
+                    synchronized (sAccountList) {
+                        for (Account account : sAccountList)
+                            SyncManager.stopAccountSyncs(account.mId);
+                    }
                 }
             }
         }
@@ -1566,7 +1587,8 @@
      * @param m the Mailbox on which the service will operate
      */
     private void startServiceThread(AbstractSyncService service, Mailbox m) {
-        synchronized (sSyncToken) {
+        if (m == null) return;
+        synchronized (sSyncLock) {
             String mailboxName = m.mDisplayName;
             String accountName = service.mAccount.mDisplayName;
             Thread thread = new Thread(service, mailboxName + "(" + accountName + ")");
@@ -1601,8 +1623,8 @@
 
     private void requestSync(Mailbox m, int reason, Request req) {
         // Don't sync if there's no connectivity
-        if (sConnectivityHold) return;
-        synchronized (sSyncToken) {
+        if (sConnectivityHold || (m == null)) return;
+        synchronized (sSyncLock) {
             Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
             if (acct != null) {
                 // Always make sure there's not a running instance of this service
@@ -1621,7 +1643,7 @@
     }
 
     private void stopServiceThreads() {
-        synchronized (sSyncToken) {
+        synchronized (sSyncLock) {
             ArrayList<Long> toStop = new ArrayList<Long>();
 
             // Keep track of which services to stop
@@ -1645,22 +1667,27 @@
     }
 
     private void waitForConnectivity() {
-        int cnt = 0;
+        boolean waiting = false;
+        ConnectivityManager cm =
+            (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
         while (!mStop) {
-            ConnectivityManager cm =
-                (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
             NetworkInfo info = cm.getActiveNetworkInfo();
             if (info != null) {
-                //log("NetworkInfo: " + info.getTypeName() + ", " + info.getState().name());
+                // We're done if there's an active network
+                if (waiting) {
+                    // If we've been waiting, release any I/O error holds
+                    releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null);
+                    // And log what's still being held
+                    logSyncHolds();
+                }
                 return;
             } else {
-
-                // If we're waiting for the long haul, shut down running service threads
-                if (++cnt > 1) {
+                // If this is our first time through the loop, shut down running service threads
+                if (!waiting) {
+                    waiting = true;
                     stopServiceThreads();
                 }
-
-                // Wait until a network is connected, but let the device sleep
+                // Wait until a network is connected (or 10 mins), but let the device sleep
                 // We'll set an alarm just in case we don't get notified (bugs happen)
                 synchronized (sConnectivityLock) {
                     runAsleep(SYNC_MANAGER_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
@@ -1670,6 +1697,7 @@
                         sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
                         log("Connectivity lock released...");
                     } catch (InterruptedException e) {
+                        // This is fine; we just go around the loop again
                     } finally {
                         sConnectivityHold = false;
                     }
@@ -1809,7 +1837,7 @@
     private long checkMailboxes () {
         // First, see if any running mailboxes have been deleted
         ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
-        synchronized (sSyncToken) {
+        synchronized (sSyncLock) {
             for (long mailboxId: mServiceMap.keySet()) {
                 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
                 if (m == null) {
@@ -1840,7 +1868,10 @@
 
         // Start up threads that need it; use a query which finds eas mailboxes where the
         // the sync interval is not "never".  This is the set of mailboxes that we control
-        if (mAccountObserver == null) return nextWait;
+        if (mAccountObserver == null) {
+            log("mAccountObserver null; service died??");
+            return nextWait;
+        }
         Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
                 mAccountObserver.getSyncableEasMailboxWhere(), null, null);
 
@@ -1851,7 +1882,7 @@
             while (c.moveToNext()) {
                 long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
                 AbstractSyncService service = null;
-                synchronized (sSyncToken) {
+                synchronized (sSyncLock) {
                     service = mServiceMap.get(mid);
                 }
                 if (service == null) {
@@ -1952,8 +1983,12 @@
                     }
                 } else {
                     Thread thread = service.mThread;
-                    // Look for threads that have died but aren't in an error state
-                    if (thread != null && !thread.isAlive() && !mSyncErrorMap.containsKey(mid)) {
+                    // Look for threads that have died and remove them from the map
+                    if (thread != null && !thread.isAlive()) {
+                        if (Eas.USER_LOG) {
+                            log("Dead thread, mailbox released: " +
+                                    c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
+                        }
                         releaseMailbox(mid);
                         // Restart this if necessary
                         if (nextWait > 3*SECONDS) {
@@ -1995,7 +2030,7 @@
         Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
         // Never allow manual start of Drafts or Outbox via serviceRequest
         if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
-            log("Ignoring serviceRequest for drafts/outbox");
+            log("Ignoring serviceRequest for drafts/outbox/null mailbox");
             return;
         }
         try {
@@ -2073,7 +2108,7 @@
     static public AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
         SyncManager syncManager = INSTANCE;
         if (syncManager == null) return null;
-        synchronized (sSyncToken) {
+        synchronized (sSyncLock) {
             if (syncManager.mServiceMap.get(mailboxId) == null) {
                 syncManager.mSyncErrorMap.remove(mailboxId);
                 Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
@@ -2090,7 +2125,7 @@
     static private void stopManualSync(long mailboxId) {
         SyncManager syncManager = INSTANCE;
         if (syncManager == null) return;
-        synchronized (sSyncToken) {
+        synchronized (sSyncLock) {
             AbstractSyncService svc = syncManager.mServiceMap.get(mailboxId);
             if (svc != null) {
                 log("Stopping sync for " + svc.mMailboxName);
@@ -2123,7 +2158,7 @@
     static public void accountUpdated(long acctId) {
         SyncManager syncManager = INSTANCE;
         if (syncManager == null) return;
-        synchronized (sSyncToken) {
+        synchronized (sSyncLock) {
             for (AbstractSyncService svc : syncManager.mServiceMap.values()) {
                 if (svc.mAccount.mId == acctId) {
                     svc.mAccount = Account.restoreAccountWithId(syncManager, acctId);
@@ -2139,7 +2174,7 @@
     static public void removeFromSyncErrorMap(long mailboxId) {
         SyncManager syncManager = INSTANCE;
         if (syncManager == null) return;
-        synchronized(sSyncToken) {
+        synchronized(sSyncLock) {
             syncManager.mSyncErrorMap.remove(mailboxId);
         }
     }
@@ -2153,7 +2188,7 @@
     static public void done(AbstractSyncService svc) {
         SyncManager syncManager = INSTANCE;
         if (syncManager == null) return;
-        synchronized(sSyncToken) {
+        synchronized(sSyncLock) {
             long mailboxId = svc.mMailboxId;
             HashMap<Long, SyncError> errorMap = syncManager.mSyncErrorMap;
             SyncError syncError = errorMap.get(mailboxId);
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 78152a0..580c0f0 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -1622,7 +1622,8 @@
                         for (String removedAttendee: originalAttendeeList) {
                             // Send a cancellation message to each of them
                             msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
-                                    Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount);
+                                    Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
+                                    false);
                             if (msg != null) {
                                 // Just send it to the removed attendee
                                 msg.mTo = removedAttendee;
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index ccfaa32..ed45265 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -59,7 +59,7 @@
 import android.text.util.Rfc822Token;
 import android.text.util.Rfc822Tokenizer;
 import android.util.Log;
-import android.util.base64.Base64;
+import android.util.Base64;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -1017,7 +1017,10 @@
                             }
                         // Note Email.TYPE could be ANY type column; they are all defined in
                         // the private CommonColumns class in ContactsContract
-                        } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
+                        // We'll accept either type < 0 (don't care), cv doesn't have a type,
+                        // or the types are equal
+                        } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
+                                cv.getAsInteger(Email.TYPE) == type) {
                             result = namedContentValues;
                         }
                     }
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index e7caf86..5cade38 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -45,7 +45,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
-import android.util.base64.Base64;
+import android.util.Base64;
 import android.webkit.MimeTypeMap;
 
 import java.io.IOException;
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index d8216ac..4981622 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -43,10 +43,9 @@
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
 import android.provider.Calendar.EventsEntity;
-import android.provider.Calendar.Reminders;
 import android.text.format.Time;
 import android.util.Log;
-import android.util.base64.Base64;
+import android.util.Base64;
 
 import java.io.IOException;
 import java.text.DateFormat;
@@ -1210,6 +1209,12 @@
      */
     static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
             int messageFlag, String uid, Account account) {
+        return createMessageForEntity(context, entity, messageFlag, uid, account,
+                true /*requireAddressees*/);
+    }
+
+    static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
+            int messageFlag, String uid, Account account, boolean requireAddressees) {
         ContentValues entityValues = entity.getEntityValues();
         ArrayList<NamedContentValues> subValues = entity.getSubValues();
         boolean isException = entityValues.containsKey(Events.ORIGINAL_EVENT);
@@ -1338,56 +1343,42 @@
             if (titleId != 0) {
                 msg.mSubject = resources.getString(titleId, title);
             }
+
+            // Build the text for the message, starting with an initial line describing the
+            // exception (if this is one)
+            StringBuilder sb = new StringBuilder();
+            if (isException && method.equals("REQUEST")) {
+                // Add the line, depending on whether this is a cancellation or update
+                Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
+                String dateString = DateFormat.getDateInstance().format(date);
+                if (titleId == R.string.meeting_canceled) {
+                    sb.append(resources.getString(R.string.exception_cancel, dateString));
+                } else {
+                    sb.append(resources.getString(R.string.exception_updated, dateString));
+                }
+                sb.append("\n\n");
+            }
+            String text =
+                CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
+
+            if (text.length() > 0) {
+                ics.writeTag("DESCRIPTION", text);
+            }
+            // And store the message text
+            msg.mText = text;
             if (method.equals("REQUEST")) {
                 if (entityValues.containsKey(Events.ALL_DAY)) {
                     Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
                     ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
                 }
 
-                // Build the text for the message, starting with an initial line describing the
-                // exception
-                StringBuilder sb = new StringBuilder();
-                if (isException) {
-                    // Add the line, depending on whether this is a cancellation or update
-                    Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
-                    String dateString = DateFormat.getDateInstance().format(date);
-                    if (titleId == R.string.meeting_canceled) {
-                        sb.append(resources.getString(R.string.exception_cancel, dateString));
-                    } else {
-                        sb.append(resources.getString(R.string.exception_updated, dateString));
-                    }
-                    sb.append("\n\n");
-                }
-                String text =
-                    CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
-
-                    // If we've got anything here, write it into the ics file
-                if (text.length() > 0) {
-                    ics.writeTag("DESCRIPTION", text);
-                }
-                // And store the message text
-                msg.mText = text;
-
                 String rrule = entityValues.getAsString(Events.RRULE);
                 if (rrule != null) {
                     ics.writeTag("RRULE", rrule);
                 }
 
-                // Handle associated data EXCEPT for attendees, which have to be grouped
-                for (NamedContentValues ncv: subValues) {
-                    Uri ncvUri = ncv.uri;
-                    if (ncvUri.equals(Reminders.CONTENT_URI)) {
-                        // TODO Consider sending alarm information in the meeting request, though
-                        // it's not obviously appropriate (i.e. telling the user what alarm to use)
-                        // This should be for REQUEST only
-                        // Here's what the VALARM would look like:
-                        //                  BEGIN:VALARM
-                        //                  ACTION:DISPLAY
-                        //                  DESCRIPTION:REMINDER
-                        //                  TRIGGER;RELATED=START:-PT15M
-                        //                  END:VALARM
-                    }
-                }
+                // If we decide to send alarm information in the meeting request ics file,
+                // handle it here by looping through the subvalues
             }
 
             // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
@@ -1466,8 +1457,9 @@
                 }
             }
 
-            // If we have no "to" list, we're done
-            if (toList.isEmpty()) return null;
+            // If we have no "to" list and addressees are required (the default), we're done
+            if (toList.isEmpty() && requireAddressees) return null;
+
             // Write out the "to" list
             Address[] toArray = new Address[toList.size()];
             int i = 0;
@@ -1513,11 +1505,20 @@
      * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
      * @param the unique id of this Event, or null if it can be retrieved from the Event
      * @param the user's account
+     * @param requireAddressees if true (the default), no Message is returned if there aren't any
+     *  addressees; if false, return the Message regardless (addressees will be filled in later)
      * @return a Message with many fields pre-filled (more later)
      * @throws RemoteException if there is an issue retrieving the Event from CalendarProvider
      */
     static public EmailContent.Message createMessageForEventId(Context context, long eventId,
             int messageFlag, String uid, Account account) throws RemoteException {
+        return createMessageForEventId(context, eventId, messageFlag, uid, account,
+                true /*requireAddressees*/);
+    }
+
+    static public EmailContent.Message createMessageForEventId(Context context, long eventId,
+            int messageFlag, String uid, Account account, boolean requireAddressees)
+            throws RemoteException {
         ContentResolver cr = context.getContentResolver();
         EntityIterator eventIterator =
             EventsEntity.newEntityIterator(
@@ -1527,7 +1528,8 @@
         try {
             while (eventIterator.hasNext()) {
                 Entity entity = eventIterator.next();
-                return createMessageForEntity(context, entity, messageFlag, uid, account);
+                return createMessageForEntity(context, entity, messageFlag, uid, account,
+                        requireAddressees);
             }
         } finally {
             eventIterator.close();
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
index 53c0925..764a8df 100644
--- a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -244,8 +244,11 @@
 
         // Now check some of the fields of the message
         assertEquals(Address.pack(new Address[] {new Address(ORGANIZER)}), msg.mTo);
-        String accept = getContext().getResources().getString(R.string.meeting_accepted, title);
+        Resources resources = getContext().getResources();
+        String accept = resources.getString(R.string.meeting_accepted, title);
         assertEquals(accept, msg.mSubject);
+        assertNotNull(msg.mText);
+        assertTrue(msg.mText.contains(resources.getString(R.string.meeting_where, "")));
 
         // And make sure we have an attachment
         assertNotNull(msg.mAttachments);