com.chen.emailsync.SyncManager.java Source code

Java tutorial

Introduction

Here is the source code for com.chen.emailsync.SyncManager.java

Source

/*
 * Copyright (C) 2008-2009 Marc Blank
 * Licensed to 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.chen.emailsync;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.State;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.Process;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.ContactsContract;

import com.chen.emailcommon.TempDirectory;
import com.chen.emailcommon.provider.Account;
import com.chen.emailcommon.provider.EmailContent;
import com.chen.emailcommon.provider.EmailContent.Body;
import com.chen.emailcommon.provider.EmailContent.BodyColumns;
import com.chen.emailcommon.provider.EmailContent.MailboxColumns;
import com.chen.emailcommon.provider.EmailContent.Message;
import com.chen.emailcommon.provider.EmailContent.MessageColumns;
import com.chen.emailcommon.provider.EmailContent.SyncColumns;
import com.chen.emailcommon.provider.HostAuth;
import com.chen.emailcommon.provider.Mailbox;
import com.chen.emailcommon.provider.Policy;
import com.chen.emailcommon.provider.ProviderUnavailableException;
import com.chen.emailcommon.service.AccountServiceProxy;
import com.chen.emailcommon.service.EmailServiceProxy;
import com.chen.emailcommon.service.IEmailServiceCallback.Stub;
import com.chen.emailcommon.service.PolicyServiceProxy;
import com.chen.emailcommon.utility.EmailClientConnectionManager;
import com.chen.emailcommon.utility.Utility;
import com.chen.mail.utils.LogUtils;

import org.apache.http.conn.params.ConnManagerPNames;
import org.apache.http.conn.params.ConnPerRoute;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The SyncServiceManager handles the lifecycle of various sync adapters used by services that
 * cannot rely on the system SyncManager
 *
 * SyncServiceManager uses ContentObservers to detect changes to accounts, mailboxes, & messages in
 * order to maintain proper 2-way syncing of data.  (More documentation to follow)
 *
 */
public abstract class SyncManager extends Service implements Runnable {

    private static String TAG = "SyncManager";

    // The SyncServiceManager's mailbox "id"
    public static final int EXTRA_MAILBOX_ID = -1;
    public static final int SYNC_SERVICE_MAILBOX_ID = 0;

    private static final int SECONDS = 1000;
    private static final int MINUTES = 60 * SECONDS;
    private static final int ONE_DAY_MINUTES = 1440;

    private static final int SYNC_SERVICE_HEARTBEAT_TIME = 15 * MINUTES;
    private static final int CONNECTIVITY_WAIT_TIME = 10 * MINUTES;

    // Sync hold constants for services with transient errors
    private static final int HOLD_DELAY_MAXIMUM = 4 * MINUTES;

    // Reason codes when SyncServiceManager.kick is called (mainly for debugging)
    // UI has changed data, requiring an upsync of changes
    public static final int SYNC_UPSYNC = 0;
    // A scheduled sync (when not using push)
    public static final int SYNC_SCHEDULED = 1;
    // Mailbox was marked push
    public static final int SYNC_PUSH = 2;
    // A ping (EAS push signal) was received
    public static final int SYNC_PING = 3;
    // Misc.
    public static final int SYNC_KICK = 4;
    // A part request (attachment load, for now) was sent to SyncServiceManager
    public static final int SYNC_SERVICE_PART_REQUEST = 5;

    // Requests >= SYNC_CALLBACK_START generate callbacks to the UI
    public static final int SYNC_CALLBACK_START = 6;
    // startSync was requested of SyncServiceManager (other than due to user request)
    public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0;
    // startSync was requested of SyncServiceManager (due to user request)
    public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1;

    protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE = MailboxColumns.ACCOUNT_KEY + "=? and type in ("
            + Mailbox.TYPE_INBOX + ',' + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ','
            + Mailbox.TYPE_CALENDAR + ')';
    protected static final String WHERE_IN_ACCOUNT_AND_TYPE_INBOX = MailboxColumns.ACCOUNT_KEY + "=? and type = "
            + Mailbox.TYPE_INBOX;
    private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
    private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN = "(" + MailboxColumns.TYPE + '='
            + Mailbox.TYPE_OUTBOX + " or " + MailboxColumns.SYNC_INTERVAL + "<" + Mailbox.CHECK_INTERVAL_NEVER + ')'
            + " and " + MailboxColumns.ACCOUNT_KEY + " in (";

    public static final int SEND_FAILED = 1;
    public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED = MessageColumns.MAILBOX_KEY + "=? and ("
            + SyncColumns.SERVER_ID + " is null or " + SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';

    public static final String CALENDAR_SELECTION = Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE
            + "=?";
    private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";

    // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
    // The format is S<type_char>:<exit_char>:<change_count>
    public static final int STATUS_TYPE_CHAR = 1;
    public static final int STATUS_EXIT_CHAR = 3;
    public static final int STATUS_CHANGE_COUNT_OFFSET = 5;

    // Ready for ping
    public static final int PING_STATUS_OK = 0;
    // Service already running (can't ping)
    public static final int PING_STATUS_RUNNING = 1;
    // Service waiting after I/O error (can't ping)
    public static final int PING_STATUS_WAITING = 2;
    // Service had a fatal error; can't run
    public static final int PING_STATUS_UNABLE = 3;
    // Service is disabled by user (checkbox)
    public static final int PING_STATUS_DISABLED = 4;

    private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1;

    // We synchronize on this for all actions affecting the service and error maps
    private static final Object sSyncLock = new Object();
    // All threads can use this lock to wait for connectivity
    public static final Object sConnectivityLock = new Object();
    public static boolean sConnectivityHold = false;

    // Keeps track of running services (by mailbox id)
    public final HashMap<Long, AbstractSyncService> mServiceMap = new HashMap<Long, AbstractSyncService>();
    // Keeps track of services whose last sync ended with an error (by mailbox id)
    /*package*/ public ConcurrentHashMap<Long, SyncError> mSyncErrorMap = new ConcurrentHashMap<Long, SyncError>();
    // Keeps track of which services require a wake lock (by mailbox id)
    private final HashMap<Long, Long> mWakeLocks = new HashMap<Long, Long>();
    // Keeps track of which services have held a wake lock (by mailbox id)
    private final HashMap<Long, Long> mWakeLocksHistory = new HashMap<Long, Long>();
    // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
    private final HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
    // The actual WakeLock obtained by SyncServiceManager
    private WakeLock mWakeLock = null;
    // Keep our cached list of active Accounts here
    public final AccountList mAccountList = new AccountList();
    // Keep track of when we started up
    private long mServiceStartTime;

    // Observers that we use to look for changed mail-related data
    private final Handler mHandler = new Handler();
    private AccountObserver mAccountObserver;
    private MailboxObserver mMailboxObserver;
    private SyncedMessageObserver mSyncedMessageObserver;

    // Concurrent because CalendarSyncAdapter can modify the map during a wipe
    private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers = new ConcurrentHashMap<Long, CalendarObserver>();

    public ContentResolver mResolver;

    // The singleton SyncServiceManager object, with its thread and stop flag
    protected static SyncManager INSTANCE;
    protected static Thread sServiceThread = null;
    // Cached unique device id
    protected static String sDeviceId = null;
    // HashMap of ConnectionManagers that all EAS threads can use (by HostAuth id)
    private static HashMap<Long, EmailClientConnectionManager> sClientConnectionManagers = new HashMap<Long, EmailClientConnectionManager>();
    // Count of ClientConnectionManager shutdowns
    private static volatile int sClientConnectionManagerShutdownCount = 0;

    private static volatile boolean sStartingUp = false;
    private static volatile boolean sStop = false;

    // The reason for SyncServiceManager's next wakeup call
    private String mNextWaitReason;
    // Whether we have an unsatisfied "kick" pending
    private boolean mKicked = false;

    // Receiver of connectivity broadcasts
    private ConnectivityReceiver mConnectivityReceiver = null;
    // The most current NetworkInfo (from ConnectivityManager)
    private NetworkInfo mNetworkInfo;

    // For sync logging
    protected static boolean sUserLog = false;
    protected static boolean sFileLog = false;

    /**
     * Return an AccountObserver for this manager; the subclass must implement the newAccount()
     * method, which is called whenever the observer discovers that a new account has been created.
     * The subclass should do any housekeeping necessary
     * @param handler a Handler
     * @return the AccountObserver
     */
    public abstract AccountObserver getAccountObserver(Handler handler);

    /**
     * Perform any housekeeping necessary upon startup of the manager
     */
    public abstract void onStartup();

    /**
     * Returns a String that can be used as a WHERE clause in SQLite that selects accounts whose
     * syncs are managed by this manager
     * @return the account selector String
     */
    public abstract String getAccountsSelector();

    /**
     * Returns an appropriate sync service for the passed in mailbox
     * @param context the caller's context
     * @param mailbox the Mailbox to be synced
     * @return a service that will sync the Mailbox
     */
    public abstract AbstractSyncService getServiceForMailbox(Context context, Mailbox mailbox);

    /**
     * Return a list of all Accounts in EmailProvider.  Because the result of this call may be used
     * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
     * @param context the caller's context
     * @param accounts a list that Accounts will be added into
     * @return the list of Accounts
     * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
     */
    public abstract AccountList collectAccounts(Context context, AccountList accounts);

    /**
     * Returns the AccountManager type (e.g. com.android.exchange) for this sync service
     */
    public abstract String getAccountManagerType();

    /**
     * Returns the intent used for this sync service
     */
    public abstract Intent getServiceIntent();

    /**
     * Returns the callback proxy used for communicating back with the Email app
     */
    public abstract Stub getCallbackProxy();

    /**
     * Called when a sync service has started (in case any action is needed). This method must
     * not perform any long-lived actions (db access, network access, etc)
     */
    public abstract void onStartService(Mailbox mailbox);

    public class AccountList extends ArrayList<Account> {
        private static final long serialVersionUID = 1L;

        private final WeakHashMap<Account, android.accounts.Account> mAmMap = new WeakHashMap<Account, android.accounts.Account>();

        @Override
        public boolean add(Account account) {
            // Cache the account manager account
            mAmMap.put(account, account.getAccountManagerAccount(getAccountManagerType()));
            super.add(account);
            return true;
        }

        public android.accounts.Account getAmAccount(Account account) {
            return mAmMap.get(account);
        }

        public boolean contains(long id) {
            for (Account account : this) {
                if (account.mId == id) {
                    return true;
                }
            }
            return false;
        }

        public Account getById(long id) {
            for (Account account : this) {
                if (account.mId == id) {
                    return account;
                }
            }
            return null;
        }

        public Account getByName(String accountName) {
            for (Account account : this) {
                if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
                    return account;
                }
            }
            return null;
        }
    }

    public static void setUserDebug(int state) {
        sUserLog = (state & EmailServiceProxy.DEBUG_BIT) != 0;
        sFileLog = (state & EmailServiceProxy.DEBUG_FILE_BIT) != 0;
        if (sFileLog) {
            sUserLog = true;
        }
        LogUtils.d("Sync Debug", "Logging: " + (sUserLog ? "User " : "") + (sFileLog ? "File" : ""));
    }

    private static boolean onSecurityHold(Account account) {
        return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
    }

    public static String getAccountSelector() {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return "";
        return ssm.getAccountsSelector();
    }

    public abstract class AccountObserver extends ContentObserver {
        String mSyncableMailboxSelector = null;
        String mAccountSelector = null;

        // Runs when SyncServiceManager first starts
        @SuppressWarnings("deprecation")
        public AccountObserver(Handler handler) {
            super(handler);
            // At startup, we want to see what EAS accounts exist and cache them
            // TODO: Move database work out of UI thread
            Context context = getContext();
            synchronized (mAccountList) {
                try {
                    collectAccounts(context, mAccountList);
                } catch (ProviderUnavailableException e) {
                    // Just leave if EmailProvider is unavailable
                    return;
                }
                // Create an account mailbox for any account without one
                for (Account account : mAccountList) {
                    int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" + account.mId, null);
                    if (cnt == 0) {
                        // This case handles a newly created account
                        newAccount(account.mId);
                    }
                }
            }
            // Run through accounts and update account hold information
            Utility.runAsync(new Runnable() {
                @Override
                public void run() {
                    synchronized (mAccountList) {
                        for (Account account : mAccountList) {
                            if (onSecurityHold(account)) {
                                // If we're in a security hold, and our policies are active, release
                                // the hold
                                if (PolicyServiceProxy.isActive(SyncManager.this, null)) {
                                    PolicyServiceProxy.setAccountHoldFlag(SyncManager.this, account, false);
                                    log("isActive true; release hold for " + account.mDisplayName);
                                }
                            }
                        }
                    }
                }
            });
        }

        /**
         * Returns a String suitable for appending to a where clause that selects for all syncable
         * mailboxes in all eas accounts
         * @return a complex selection string that is not to be cached
         */
        public String getSyncableMailboxWhere() {
            if (mSyncableMailboxSelector == null) {
                StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
                boolean first = true;
                synchronized (mAccountList) {
                    for (Account account : mAccountList) {
                        if (!first) {
                            sb.append(',');
                        } else {
                            first = false;
                        }
                        sb.append(account.mId);
                    }
                }
                sb.append(')');
                mSyncableMailboxSelector = sb.toString();
            }
            return mSyncableMailboxSelector;
        }

        private void onAccountChanged() {
            try {
                maybeStartSyncServiceManagerThread();
                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();
                try {
                    collectAccounts(context, currentAccounts);
                } catch (ProviderUnavailableException e) {
                    // Just leave if EmailProvider is unavailable
                    return;
                }
                synchronized (mAccountList) {
                    for (Account account : mAccountList) {
                        boolean accountIncomplete = (account.mFlags & Account.FLAGS_INCOMPLETE) != 0;
                        // If the current list doesn't include this account and the account wasn't
                        // incomplete, then this is a deletion
                        if (!currentAccounts.contains(account.mId) && !accountIncomplete) {
                            // The implication is that the account has been deleted; let's find out
                            alwaysLog("Observer found deleted account: " + account.mDisplayName);
                            // Run the reconciler (the reconciliation itself runs in the Email app)
                            runAccountReconcilerSync(SyncManager.this);
                            // See if the account is still around
                            Account deletedAccount = Account.restoreAccountWithId(context, account.mId);
                            if (deletedAccount != null) {
                                // It is; add it to our account list
                                alwaysLog("Account still in provider: " + account.mDisplayName);
                                currentAccounts.add(account);
                            } else {
                                // It isn't; stop syncs and clear our selectors
                                alwaysLog("Account deletion confirmed: " + account.mDisplayName);
                                stopAccountSyncs(account.mId, true);
                                mSyncableMailboxSelector = null;
                                mAccountSelector = null;
                            }
                        } else {
                            // Get the newest version of this account
                            Account updatedAccount = Account.restoreAccountWithId(context, account.mId);
                            if (updatedAccount == null)
                                continue;
                            if (account.mSyncInterval != updatedAccount.mSyncInterval
                                    || account.mSyncLookback != updatedAccount.mSyncLookback) {
                                // Set the inbox interval to the interval of the Account
                                // This setting should NOT affect other boxes
                                ContentValues cv = new ContentValues();
                                cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
                                getContentResolver().update(Mailbox.CONTENT_URI, cv,
                                        WHERE_IN_ACCOUNT_AND_TYPE_INBOX,
                                        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 (!mAccountList.contains(account.mId)) {
                            // Don't forget to cache the HostAuth
                            HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(), account.mHostAuthKeyRecv);
                            if (ha == null)
                                continue;
                            account.mHostAuthRecv = ha;
                            // This is an addition; create our magic hidden mailbox...
                            log("Account observer found new account: " + account.mDisplayName);
                            newAccount(account.mId);
                            mAccountList.add(account);
                            mSyncableMailboxSelector = null;
                            mAccountSelector = null;
                        }
                    }
                    // Finally, make sure our account list is up to date
                    mAccountList.clear();
                    mAccountList.addAll(currentAccounts);
                }

                // See if there's anything to do...
                kick("account changed");
            } catch (ProviderUnavailableException e) {
                alwaysLog("Observer failed; provider unavailable");
            }
        }

        @Override
        public void onChange(boolean selfChange) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    onAccountChanged();
                }
            }, "Account Observer").start();
        }

        public abstract void newAccount(long acctId);
    }

    /**
     * Register a specific Calendar's data observer; we need to recognize when the SYNC_EVENTS
     * column has changed (when sync has turned off or on)
     * @param account the Account whose Calendar we're observing
     */
    private void registerCalendarObserver(Account account) {
        // Get a new observer
        CalendarObserver observer = new CalendarObserver(mHandler, account);
        if (observer.mCalendarId != 0) {
            // If we find the Calendar (and we'd better) register it and store it in the map
            mCalendarObservers.put(account.mId, observer);
            mResolver.registerContentObserver(
                    ContentUris.withAppendedId(Calendars.CONTENT_URI, observer.mCalendarId), false, observer);
        }
    }

    /**
     * Unregister all CalendarObserver's
     */
    static public void unregisterCalendarObservers() {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        ContentResolver resolver = ssm.mResolver;
        for (CalendarObserver observer : ssm.mCalendarObservers.values()) {
            resolver.unregisterContentObserver(observer);
        }
        ssm.mCalendarObservers.clear();
    }

    public static Uri asSyncAdapter(Uri uri, String account, String accountType) {
        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
    }

    /**
     * Return the syncable state of an account's calendar, as determined by the sync_events column
     * of our Calendar (from CalendarProvider2)
     * Note that the current state of sync_events is cached in our CalendarObserver
     * @param accountId the id of the account whose calendar we are checking
     * @return whether or not syncing of events is enabled
     */
    private boolean isCalendarEnabled(long accountId) {
        CalendarObserver observer = mCalendarObservers.get(accountId);
        if (observer != null) {
            return (observer.mSyncEvents == 1);
        }
        // If there's no observer, there's no Calendar in CalendarProvider2, so we return true
        // to allow Calendar creation
        return true;
    }

    private class CalendarObserver extends ContentObserver {
        final long mAccountId;
        final String mAccountName;
        long mCalendarId;
        long mSyncEvents;

        public CalendarObserver(Handler handler, Account account) {
            super(handler);
            mAccountId = account.mId;
            mAccountName = account.mEmailAddress;
            // Find the Calendar for this account
            Cursor c = mResolver.query(Calendars.CONTENT_URI, new String[] { Calendars._ID, Calendars.SYNC_EVENTS },
                    CALENDAR_SELECTION, new String[] { account.mEmailAddress, getAccountManagerType() }, null);
            if (c != null) {
                // Save its id and its sync events status
                try {
                    if (c.moveToFirst()) {
                        mCalendarId = c.getLong(0);
                        mSyncEvents = c.getLong(1);
                    }
                } finally {
                    c.close();
                }
            }
        }

        @Override
        public synchronized void onChange(boolean selfChange) {
            // See if the user has changed syncing of our calendar
            if (!selfChange) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Cursor c = mResolver.query(Calendars.CONTENT_URI,
                                    new String[] { Calendars.SYNC_EVENTS }, Calendars._ID + "=?",
                                    new String[] { Long.toString(mCalendarId) }, null);
                            if (c == null)
                                return;
                            // Get its sync events; if it's changed, we've got work to do
                            try {
                                if (c.moveToFirst()) {
                                    long newSyncEvents = c.getLong(0);
                                    if (newSyncEvents != mSyncEvents) {
                                        log("_sync_events changed for calendar in " + mAccountName);
                                        Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE, mAccountId,
                                                Mailbox.TYPE_CALENDAR);
                                        // Sanity check for mailbox deletion
                                        if (mailbox == null)
                                            return;
                                        ContentValues cv = new ContentValues();
                                        if (newSyncEvents == 0) {
                                            // When sync is disabled, we're supposed to delete
                                            // all events in the calendar
                                            log("Deleting events and setting syncKey to 0 for " + mAccountName);
                                            // First, stop any sync that's ongoing
                                            stopManualSync(mailbox.mId);
                                            // Set the syncKey to 0 (reset)
                                            AbstractSyncService service = getServiceForMailbox(INSTANCE, mailbox);
                                            service.resetCalendarSyncKey();
                                            // Reset the sync key locally and stop syncing
                                            cv.put(Mailbox.SYNC_KEY, "0");
                                            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
                                            mResolver.update(
                                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId),
                                                    cv, null, null);
                                            // Delete all events using the sync adapter
                                            // parameter so that the deletion is only local
                                            Uri eventsAsSyncAdapter = asSyncAdapter(Events.CONTENT_URI,
                                                    mAccountName, getAccountManagerType());
                                            mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
                                                    new String[] { Long.toString(mCalendarId) });
                                        } else {
                                            // Make this a push mailbox and kick; this will start
                                            // a resync of the Calendar; the account mailbox will
                                            // ping on this during the next cycle of the ping loop
                                            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
                                            mResolver.update(
                                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId),
                                                    cv, null, null);
                                            kick("calendar sync changed");
                                        }

                                        // Save away the new value
                                        mSyncEvents = newSyncEvents;
                                    }
                                }
                            } finally {
                                c.close();
                            }
                        } catch (ProviderUnavailableException e) {
                            LogUtils.w(TAG, "Observer failed; provider unavailable");
                        }
                    }
                }, "Calendar Observer").start();
            }
        }
    }

    private class MailboxObserver extends ContentObserver {
        public MailboxObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            // See if there's anything to do...
            if (!selfChange) {
                kick("mailbox changed");
            }
        }
    }

    private class SyncedMessageObserver extends ContentObserver {
        Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
        PendingIntent syncAlarmPendingIntent = PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
        AlarmManager alarmManager = (AlarmManager) INSTANCE.getSystemService(Context.ALARM_SERVICE);

        public SyncedMessageObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10 * SECONDS,
                    syncAlarmPendingIntent);
        }
    }

    static public Account getAccountById(long accountId) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            AccountList accountList = ssm.mAccountList;
            synchronized (accountList) {
                return accountList.getById(accountId);
            }
        }
        return null;
    }

    static public Account getAccountByName(String accountName) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            AccountList accountList = ssm.mAccountList;
            synchronized (accountList) {
                return accountList.getByName(accountName);
            }
        }
        return null;
    }

    public class SyncStatus {
        static public final int NOT_RUNNING = 0;
        static public final int DIED = 1;
        static public final int SYNC = 2;
        static public final int IDLE = 3;
    }

    /*package*/ public class SyncError {
        int reason;
        public boolean fatal = false;
        long holdDelay = 15 * SECONDS;
        public long holdEndTime = System.currentTimeMillis() + holdDelay;

        public SyncError(int _reason, boolean _fatal) {
            reason = _reason;
            fatal = _fatal;
        }

        /**
         * We double the holdDelay from 15 seconds through 8 mins
         */
        void escalate() {
            if (holdDelay <= HOLD_DELAY_MAXIMUM) {
                holdDelay *= 2;
            }
            holdEndTime = System.currentTimeMillis() + holdDelay;
        }
    }

    private void logSyncHolds() {
        if (sUserLog) {
            log("Sync holds:");
            long time = System.currentTimeMillis();
            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);
                    if (error != null) {
                        log("Mailbox " + m.mDisplayName + ", error = " + error.reason + ", fatal = " + error.fatal);
                        if (error.holdEndTime > 0) {
                            log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
                        }
                    }
                }
            }
        }
    }

    /**
     * Release security holds for the specified account
     * @param account the account whose Mailboxes should be released from security hold
     */
    static public void releaseSecurityHold(Account account) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE, account);
        }
    }

    /**
     * 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
     * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
     * @param account an Account whose mailboxes should be released (or all if null)
     * @return whether or not any mailboxes were released
     */
    public /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) {
        boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account);
        kick("security release");
        return holdWasReleased;
    }

    private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) {
        boolean holdWasReleased = false;
        for (long mailboxId : mSyncErrorMap.keySet()) {
            if (account != null) {
                Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId);
                if (m == null) {
                    mSyncErrorMap.remove(mailboxId);
                } else if (m.mAccountKey != account.mId) {
                    continue;
                }
            }
            SyncError error = mSyncErrorMap.get(mailboxId);
            if (error != null && error.reason == reason) {
                mSyncErrorMap.remove(mailboxId);
                holdWasReleased = true;
            }
        }
        return holdWasReleased;
    }

    public static void log(String str) {
        log(TAG, str);
    }

    public static void log(String tag, String str) {
        if (sUserLog) {
            LogUtils.d(tag, str);
            if (sFileLog) {
                FileLogger.log(tag, str);
            }
        }
    }

    public static void alwaysLog(String str) {
        if (!sUserLog) {
            LogUtils.d(TAG, str);
        } else {
            log(str);
        }
    }

    /**
     * EAS requires a unique device id, so that sync is possible from a variety of different
     * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
     * device that doesn't provide one, we can create it as "device".
     * This would work on a real device as well, but it would be better to use the "real" id if
     * it's available
     */
    static public String getDeviceId(Context context) {
        if (sDeviceId == null) {
            sDeviceId = new AccountServiceProxy(context).getDeviceId();
            alwaysLog("Received deviceId from Email app: " + sDeviceId);
        }
        return sDeviceId;
    }

    static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
        @Override
        public int getMaxForRoute(HttpRoute route) {
            return 8;
        }
    };

    static public synchronized EmailClientConnectionManager getClientConnectionManager(Context context,
            HostAuth hostAuth) {
        // We'll use a different connection manager for each HostAuth
        EmailClientConnectionManager mgr = null;
        // We don't save managers for validation/autodiscover
        if (hostAuth.mId != HostAuth.NOT_SAVED) {
            mgr = sClientConnectionManagers.get(hostAuth.mId);
        }
        if (mgr == null) {
            // After two tries, kill the process.  Most likely, this will happen in the background
            // The service will restart itself after about 5 seconds
            if (sClientConnectionManagerShutdownCount > MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS) {
                alwaysLog("Shutting down process to unblock threads");
                Process.killProcess(Process.myPid());
            }
            HttpParams params = new BasicHttpParams();
            params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
            params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
            boolean ssl = hostAuth.shouldUseSsl();
            int port = hostAuth.mPort;
            mgr = EmailClientConnectionManager.newInstance(context, params, hostAuth);
            log("Creating connection manager for port " + port + ", ssl: " + ssl);
            sClientConnectionManagers.put(hostAuth.mId, mgr);
        }
        // Null is a valid return result if we get an exception
        return mgr;
    }

    static private synchronized void shutdownConnectionManager() {
        log("Shutting down ClientConnectionManagers");
        for (EmailClientConnectionManager mgr : sClientConnectionManagers.values()) {
            mgr.shutdown();
        }
        sClientConnectionManagers.clear();
    }

    public static void stopAccountSyncs(long acctId) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.stopAccountSyncs(acctId, true);
        }
    }

    public void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
        synchronized (sSyncLock) {
            List<Long> deletedBoxes = new ArrayList<Long>();
            for (Long mid : mServiceMap.keySet()) {
                Mailbox box = Mailbox.restoreMailboxWithId(this, mid);
                if (box != null) {
                    if (box.mAccountKey == acctId) {
                        if (!includeAccountMailbox && box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
                            AbstractSyncService svc = mServiceMap.get(mid);
                            if (svc != null) {
                                svc.stop();
                            }
                            continue;
                        }
                        AbstractSyncService svc = mServiceMap.get(mid);
                        if (svc != null) {
                            svc.stop();
                            Thread t = svc.mThread;
                            if (t != null) {
                                t.interrupt();
                            }
                        }
                        deletedBoxes.add(mid);
                    }
                }
            }
            for (Long mid : deletedBoxes) {
                releaseMailbox(mid);
            }
        }
    }

    /**
     * Informs SyncServiceManager that an account has a new folder list; as a result, any existing
     * folder might have become invalid.  Therefore, we act as if the account has been deleted, and
     * then we reinitialize it.
     *
     * @param acctId
     */
    static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.stopAccountSyncs(acctId, false);
            kick("reload folder list");
        }
    }

    private boolean hasWakeLock(long id) {
        synchronized (mWakeLocks) {
            return mWakeLocks.get(id) != null;
        }
    }

    private void acquireWakeLock(long id) {
        synchronized (mWakeLocks) {
            Long lock = mWakeLocks.get(id);
            if (lock == null) {
                if (mWakeLock == null) {
                    PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
                    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
                    mWakeLock.acquire();
                    // STOPSHIP Remove
                    log("+WAKE LOCK ACQUIRED");
                }
                mWakeLocks.put(id, System.currentTimeMillis());
            }
        }
    }

    private void releaseWakeLock(long id) {
        synchronized (mWakeLocks) {
            Long lock = mWakeLocks.get(id);
            if (lock != null) {
                Long startTime = mWakeLocks.remove(id);
                Long historicalTime = mWakeLocksHistory.get(id);
                if (historicalTime == null) {
                    historicalTime = 0L;
                }
                mWakeLocksHistory.put(id, historicalTime + (System.currentTimeMillis() - startTime));
                if (mWakeLocks.isEmpty()) {
                    if (mWakeLock != null) {
                        mWakeLock.release();
                    }
                    mWakeLock = null;
                    // STOPSHIP Remove
                    log("+WAKE LOCK RELEASED");
                } else {
                    log("Release request for lock not held: " + id);
                }
            }
        }
    }

    static public String alarmOwner(long id) {
        if (id == EXTRA_MAILBOX_ID) {
            return TAG;
        } else {
            String name = Long.toString(id);
            if (sUserLog && INSTANCE != null) {
                Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
                if (m != null) {
                    name = m.mDisplayName + '(' + m.mAccountKey + ')';
                }
            }
            return "Mailbox " + name;
        }
    }

    private void clearAlarm(long id) {
        synchronized (mPendingIntents) {
            PendingIntent pi = mPendingIntents.get(id);
            if (pi != null) {
                AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
                alarmManager.cancel(pi);
                //log("+Alarm cleared for " + alarmOwner(id));
                mPendingIntents.remove(id);
            }
        }
    }

    private void setAlarm(long id, long millis) {
        synchronized (mPendingIntents) {
            PendingIntent pi = mPendingIntents.get(id);
            if (pi == null) {
                Intent i = new Intent(this, MailboxAlarmReceiver.class);
                i.putExtra("mailbox", id);
                i.setData(Uri.parse("Box" + id));
                pi = PendingIntent.getBroadcast(this, 0, i, 0);
                mPendingIntents.put(id, pi);

                AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
                alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
                //log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
            }
        }
    }

    private void clearAlarms() {
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        synchronized (mPendingIntents) {
            for (PendingIntent pi : mPendingIntents.values()) {
                alarmManager.cancel(pi);
            }
            mPendingIntents.clear();
        }
    }

    static public boolean isHoldingWakeLock(long id) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            return ssm.hasWakeLock(id);
        }
        return false;
    }

    static public void runAwake(long id) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.acquireWakeLock(id);
            ssm.clearAlarm(id);
        }
    }

    static public void runAsleep(long id, long millis) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.setAlarm(id, millis);
            ssm.releaseWakeLock(id);
        }
    }

    static public void clearWatchdogAlarm(long id) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.clearAlarm(id);
        }
    }

    static public void setWatchdogAlarm(long id, long millis) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.setAlarm(id, millis);
        }
    }

    static public void alert(Context context, final long id) {
        final SyncManager ssm = INSTANCE;
        checkSyncManagerRunning();
        if (id < 0) {
            log("SyncServiceManager alert");
            kick("ping SyncServiceManager");
        } else if (ssm == null) {
            context.startService(new Intent(context, SyncManager.class));
        } else {
            final AbstractSyncService service = ssm.getRunningService(id);
            if (service != null) {
                // Handle alerts in a background thread, as we are typically called from a
                // broadcast receiver, and are therefore running in the UI thread
                String threadName = "SyncServiceManager Alert: ";
                if (service.mMailbox != null) {
                    threadName += service.mMailbox.mDisplayName;
                }
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Mailbox m = Mailbox.restoreMailboxWithId(ssm, id);
                        if (m != null) {
                            // We ignore drafts completely (doesn't sync).  Changes in Outbox are
                            // handled in the checkMailboxes loop, so we can ignore these pings.
                            if (sUserLog) {
                                LogUtils.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")");
                            }
                            if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
                                String[] args = new String[] { Long.toString(m.mId) };
                                ContentResolver resolver = INSTANCE.mResolver;
                                resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
                                resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
                                return;
                            }
                            service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
                            service.mMailbox = m;
                            // Send the alarm to the sync service
                            if (!service.alarm()) {
                                // A false return means that we were forced to interrupt the thread
                                // In this case, we release the mailbox so that we can start another
                                // thread to do the work
                                log("Alarm failed; releasing mailbox");
                                synchronized (sSyncLock) {
                                    ssm.releaseMailbox(id);
                                }
                                // Shutdown the connection manager; this should close all of our
                                // sockets and generate IOExceptions all around.
                                SyncManager.shutdownConnectionManager();
                            }
                        }
                    }
                }, threadName).start();
            }
        }
    }

    public class ConnectivityReceiver extends BroadcastReceiver {
        @SuppressWarnings("deprecation")
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle b = intent.getExtras();
            if (b != null) {
                NetworkInfo a = (NetworkInfo) b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
                String info = "Connectivity alert for " + a.getTypeName();
                State state = a.getState();
                if (state == State.CONNECTED) {
                    info += " CONNECTED";
                    log(info);
                    synchronized (sConnectivityLock) {
                        sConnectivityLock.notifyAll();
                    }
                    kick("connected");
                } else if (state == State.DISCONNECTED) {
                    info += " DISCONNECTED";
                    log(info);
                    kick("disconnected");
                }
            }
        }
    }

    /**
     * Starts a service thread and enters it into the service map
     * This is the point of instantiation of all sync threads
     * @param service the service to start
     * @param m the Mailbox on which the service will operate
     */
    private void startServiceThread(AbstractSyncService service) {
        final Mailbox mailbox = service.mMailbox;
        synchronized (sSyncLock) {
            String mailboxName = mailbox.mDisplayName;
            String accountName = service.mAccount.mDisplayName;
            Thread thread = new Thread(service, mailboxName + "[" + accountName + "]");
            log("Starting thread for " + mailboxName + " in account " + accountName);
            thread.start();
            mServiceMap.put(mailbox.mId, service);
            runAwake(mailbox.mId);
        }
        onStartService(mailbox);
    }

    private void requestSync(Mailbox m, int reason, Request req) {
        int syncStatus = EmailContent.SYNC_STATUS_BACKGROUND;
        // Don't sync if there's no connectivity
        if (sConnectivityHold || (m == null) || sStop) {
            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
                AbstractSyncService service = mServiceMap.get(m.mId);
                if (service == null) {
                    service = getServiceForMailbox(this, m);
                    if (!service.mIsValid)
                        return;
                    service.mSyncReason = reason;
                    if (req != null) {
                        service.addRequest(req);
                    }
                    startServiceThread(service);
                    if (reason >= SYNC_CALLBACK_START) {
                        syncStatus = EmailContent.SYNC_STATUS_USER;
                    }
                    setMailboxSyncStatus(m.mId, syncStatus);
                }
            }
        }
    }

    public void setMailboxSyncStatus(long id, int status) {
        ContentValues values = new ContentValues();
        values.put(Mailbox.UI_SYNC_STATUS, status);
        mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
    }

    public void setMailboxLastSyncResult(long id, int result) {
        ContentValues values = new ContentValues();
        values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
        mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
    }

    private void stopServiceThreads() {
        synchronized (sSyncLock) {
            ArrayList<Long> toStop = new ArrayList<Long>();

            // Keep track of which services to stop
            for (Long mailboxId : mServiceMap.keySet()) {
                toStop.add(mailboxId);
            }

            // Shut down all of those running services
            for (Long mailboxId : toStop) {
                AbstractSyncService svc = mServiceMap.get(mailboxId);
                if (svc != null) {
                    log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
                    svc.stop();
                    if (svc.mThread != null) {
                        svc.mThread.interrupt();
                    }
                }
                releaseWakeLock(mailboxId);
            }
        }
    }

    private void waitForConnectivity() {
        boolean waiting = false;
        ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        while (!sStop) {
            NetworkInfo info = cm.getActiveNetworkInfo();
            if (info != null) {
                mNetworkInfo = info;
                // 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 this is our first time through the loop, shut down running service threads
                if (!waiting) {
                    waiting = true;
                    stopServiceThreads();
                }
                // 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(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME + 5 * SECONDS);
                    try {
                        log("Connectivity lock...");
                        sConnectivityHold = true;
                        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;
                    }
                    runAwake(EXTRA_MAILBOX_ID);
                }
            }
        }
    }

    /**
     * Note that there are two ways the EAS SyncServiceManager service can be created:
     *
     * 1) as a background service instantiated via startService (which happens on boot, when the
     * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
     * sync, etc. and
     * 2) to execute an RPC call from the UI, in which case the background service will already be
     * running most of the time (unless we're creating a first EAS account)
     *
     * If the running background service detects that there are no EAS accounts (on boot, if none
     * were created, or afterward if the last remaining EAS account is deleted), it will call
     * stopSelf() to terminate operation.
     *
     * The goal is to ensure that the background service is running at all times when there is at
     * least one EAS account in existence
     *
     * Because there are edge cases in which our process can crash (typically, this has been seen
     * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
     * background service having been started.  We explicitly try to start the service in Welcome
     * (to handle the case of the app having been reloaded).  We also start the service on any
     * startSync call (if it isn't already running)
     */
    @SuppressWarnings("deprecation")
    @Override
    public void onCreate() {
        TAG = getClass().getSimpleName();
        EmailContent.init(this);
        Utility.runAsync(new Runnable() {
            @Override
            public void run() {
                // Quick checks first, before getting the lock
                if (sStartingUp)
                    return;
                synchronized (sSyncLock) {
                    alwaysLog("!!! onCreate");
                    // Try to start up properly; we might be coming back from a crash that the Email
                    // application isn't aware of.
                    startService(getServiceIntent());
                    if (sStop) {
                        return;
                    }
                }
            }
        });
    }

    @SuppressWarnings("deprecation")
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        alwaysLog("!!! onStartCommand, startingUp = " + sStartingUp + ", running = " + (INSTANCE != null));
        if (!sStartingUp && INSTANCE == null) {
            sStartingUp = true;
            Utility.runAsync(new Runnable() {
                @Override
                public void run() {
                    try {
                        synchronized (sSyncLock) {
                            // SyncServiceManager cannot start unless we connect to AccountService
                            if (!new AccountServiceProxy(SyncManager.this).test()) {
                                alwaysLog("!!! Email application not found; stopping self");
                                stopSelf();
                            }
                            String deviceId = getDeviceId(SyncManager.this);
                            if (deviceId == null) {
                                alwaysLog("!!! deviceId unknown; stopping self and retrying");
                                stopSelf();
                                // Try to restart ourselves in a few seconds
                                Utility.runAsync(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            Thread.sleep(5000);
                                        } catch (InterruptedException e) {
                                        }
                                        startService(getServiceIntent());
                                    }
                                });
                                return;
                            }
                            // Run the reconciler and clean up mismatched accounts - if we weren't
                            // running when accounts were deleted, it won't have been called.
                            runAccountReconcilerSync(SyncManager.this);
                            // Update other services depending on final account configuration
                            maybeStartSyncServiceManagerThread();
                            if (sServiceThread == null) {
                                log("!!! EAS SyncServiceManager, stopping self");
                                stopSelf();
                            } else if (sStop) {
                                // If we were trying to stop, attempt a restart in 5 secs
                                setAlarm(SYNC_SERVICE_MAILBOX_ID, 5 * SECONDS);
                            } else {
                                mServiceStartTime = System.currentTimeMillis();
                            }
                        }
                    } finally {
                        sStartingUp = false;
                    }
                }
            });
        }
        return Service.START_STICKY;
    }

    public static void reconcileAccounts(Context context) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.runAccountReconcilerSync(context);
        }
    }

    protected abstract void runAccountReconcilerSync(Context context);

    @SuppressWarnings("deprecation")
    @Override
    public void onDestroy() {
        log("!!! onDestroy");
        // Handle shutting down off the UI thread
        Utility.runAsync(new Runnable() {
            @Override
            public void run() {
                // Quick checks first, before getting the lock
                if (INSTANCE == null || sServiceThread == null)
                    return;
                synchronized (sSyncLock) {
                    // Stop the sync manager thread and return
                    if (sServiceThread != null) {
                        sStop = true;
                        sServiceThread.interrupt();
                    }
                }
            }
        });
    }

    void maybeStartSyncServiceManagerThread() {
        // Start our thread...
        // See if there are any EAS accounts; otherwise, just go away
        if (sServiceThread == null || !sServiceThread.isAlive()) {
            AccountList currentAccounts = new AccountList();
            try {
                collectAccounts(this, currentAccounts);
            } catch (ProviderUnavailableException e) {
                // Just leave if EmailProvider is unavailable
                return;
            }
            if (!currentAccounts.isEmpty()) {
                log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
                sServiceThread = new Thread(this, TAG);
                INSTANCE = this;
                sServiceThread.start();
            }
        }
    }

    /**
     * Start up the SyncManager service if it's not already running
     * This is a stopgap for cases in which SyncServiceManager died (due to a crash somewhere in
     * com.chen.email) and hasn't been restarted. See the comment for onCreate for details
     */
    static void checkSyncManagerRunning() {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        if (sServiceThread == null) {
            log("!!! checkSyncServiceManagerServiceRunning; starting service...");
            ssm.startService(new Intent(ssm, SyncManager.class));
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    public void run() {
        sStop = false;
        alwaysLog("Service thread running");

        TempDirectory.setTempDirectory(this);

        // Synchronize here to prevent a shutdown from happening while we initialize our observers
        // and receivers
        synchronized (sSyncLock) {
            if (INSTANCE != null) {
                mResolver = getContentResolver();

                // Set up our observers; we need them to know when to start/stop various syncs based
                // on the insert/delete/update of mailboxes and accounts
                // We also observe synced messages to trigger upsyncs at the appropriate time
                mAccountObserver = getAccountObserver(mHandler);
                mResolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
                mMailboxObserver = new MailboxObserver(mHandler);
                mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
                mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
                mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);

                mConnectivityReceiver = new ConnectivityReceiver();
                registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

                onStartup();
            }
        }

        try {
            // Loop indefinitely until we're shut down
            while (!sStop) {
                runAwake(EXTRA_MAILBOX_ID);
                waitForConnectivity();
                mNextWaitReason = null;
                long nextWait = checkMailboxes();
                try {
                    synchronized (this) {
                        if (!mKicked) {
                            if (nextWait < 0) {
                                log("Negative wait? Setting to 1s");
                                nextWait = 1 * SECONDS;
                            }
                            if (nextWait > 10 * SECONDS) {
                                if (mNextWaitReason != null) {
                                    log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason);
                                }
                                runAsleep(EXTRA_MAILBOX_ID, nextWait + (3 * SECONDS));
                            }
                            wait(nextWait);
                        }
                    }
                } catch (InterruptedException e) {
                    // Needs to be caught, but causes no problem
                    log("SyncServiceManager interrupted");
                } finally {
                    synchronized (this) {
                        if (mKicked) {
                            //log("Wait deferred due to kick");
                            mKicked = false;
                        }
                    }
                }
            }
            log("Shutdown requested");
        } catch (ProviderUnavailableException pue) {
            // Shutdown cleanly in this case
            // NOTE: Sync adapters will also crash with this error, but that is already handled
            // in the adapters themselves, i.e. they return cleanly via done().  When the Email
            // process starts running again, remote processes will be started again in due course
            LogUtils.e(TAG, "EmailProvider unavailable; shutting down");
            // Ask for our service to be restarted; this should kick-start the Email process as well
            startService(new Intent(this, SyncManager.class));
        } catch (RuntimeException e) {
            // Crash; this is a completely unexpected runtime error
            LogUtils.e(TAG, "RuntimeException", e);
            throw e;
        } finally {
            shutdown();
        }
    }

    private void shutdown() {
        synchronized (sSyncLock) {
            // If INSTANCE is null, we've already been shut down
            if (INSTANCE != null) {
                log("Shutting down...");

                // Stop our running syncs
                stopServiceThreads();

                // Stop receivers
                if (mConnectivityReceiver != null) {
                    unregisterReceiver(mConnectivityReceiver);
                }

                // Unregister observers
                ContentResolver resolver = getContentResolver();
                if (mSyncedMessageObserver != null) {
                    resolver.unregisterContentObserver(mSyncedMessageObserver);
                    mSyncedMessageObserver = null;
                }
                if (mAccountObserver != null) {
                    resolver.unregisterContentObserver(mAccountObserver);
                    mAccountObserver = null;
                }
                if (mMailboxObserver != null) {
                    resolver.unregisterContentObserver(mMailboxObserver);
                    mMailboxObserver = null;
                }
                unregisterCalendarObservers();

                // Clear pending alarms and associated Intents
                clearAlarms();

                // Release our wake lock, if we have one
                synchronized (mWakeLocks) {
                    if (mWakeLock != null) {
                        mWakeLock.release();
                        mWakeLock = null;
                    }
                }

                INSTANCE = null;
                sServiceThread = null;
                sStop = false;
                log("Goodbye");
            }
        }
    }

    /**
     * Release a mailbox from the service map and release its wake lock.
     * NOTE: This method MUST be called while holding sSyncLock!
     *
     * @param mailboxId the id of the mailbox to be released
     */
    public void releaseMailbox(long mailboxId) {
        mServiceMap.remove(mailboxId);
        releaseWakeLock(mailboxId);
    }

    /**
     * Retrieve a running sync service for the passed-in mailbox id in a threadsafe manner
     *
     * @param mailboxId the id of the mailbox whose service is to be found
     * @return the running service (a subclass of AbstractSyncService) or null if none
     */
    public AbstractSyncService getRunningService(long mailboxId) {
        synchronized (sSyncLock) {
            return mServiceMap.get(mailboxId);
        }
    }

    /**
     * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent
     * @param c the cursor to an Outbox
     * @return true if there is mail to be sent
     */
    private boolean hasSendableMessages(Cursor outboxCursor) {
        Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
                MAILBOX_KEY_AND_NOT_SEND_FAILED,
                new String[] { Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN)) }, null);
        try {
            while (c.moveToNext()) {
                if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) {
                    return true;
                }
            }
        } finally {
            if (c != null) {
                c.close();
            }
        }
        return false;
    }

    /**
     * Taken from ConnectivityManager using public constants
     */
    public static boolean isNetworkTypeMobile(int networkType) {
        switch (networkType) {
        case ConnectivityManager.TYPE_MOBILE:
        case ConnectivityManager.TYPE_MOBILE_MMS:
        case ConnectivityManager.TYPE_MOBILE_SUPL:
        case ConnectivityManager.TYPE_MOBILE_DUN:
        case ConnectivityManager.TYPE_MOBILE_HIPRI:
            return true;
        default:
            return false;
        }
    }

    /**
     * Determine whether the account is allowed to sync automatically, as opposed to manually, based
     * on whether the "require manual sync when roaming" policy is in force and applicable
     * @param account the account
     * @return whether or not the account can sync automatically
     */
    /*package*/ public static boolean canAutoSync(Account account) {
        SyncManager ssm = INSTANCE;
        if (ssm == null) {
            return false;
        }
        NetworkInfo networkInfo = ssm.mNetworkInfo;

        // Enforce manual sync only while roaming here
        long policyKey = account.mPolicyKey;
        // Quick exit from this check
        if ((policyKey != 0) && (networkInfo != null) && isNetworkTypeMobile(networkInfo.getType())) {
            // We'll cache the Policy data here
            Policy policy = account.mPolicy;
            if (policy == null) {
                policy = Policy.restorePolicyWithId(INSTANCE, policyKey);
                account.mPolicy = policy;
                if (!PolicyServiceProxy.isActive(ssm, policy)) {
                    PolicyServiceProxy.setAccountHoldFlag(ssm, account, true);
                    log("canAutoSync; policies not active, set hold flag");
                    return false;
                }
            }
            if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Convenience method to determine whether Email sync is enabled for a given account
     * @param account the Account in question
     * @return whether Email sync is enabled
     */
    private static boolean canSyncEmail(android.accounts.Account account) {
        return ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY);
    }

    /**
     * Determine whether a mailbox of a given type in a given account can be synced automatically
     * by SyncServiceManager.  This is an increasingly complex determination, taking into account
     * security policies and user settings (both within the Email application and in the Settings
     * application)
     *
     * @param account the Account that the mailbox is in
     * @param type the type of the Mailbox
     * @return whether or not to start a sync
     */
    private boolean isMailboxSyncable(Account account, int type) {
        // This 'if' statement performs checks to see whether or not a mailbox is a
        // candidate for syncing based on policies, user settings, & other restrictions
        if (type == Mailbox.TYPE_OUTBOX) {
            // Outbox is always syncable
            return true;
        } else if (type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
            // Always sync EAS mailbox unless master sync is off
            return ContentResolver.getMasterSyncAutomatically();
        } else if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
            // Contacts/Calendar obey this setting from ContentResolver
            if (!ContentResolver.getMasterSyncAutomatically()) {
                return false;
            }
            // Get the right authority for the mailbox
            String authority;
            if (type == Mailbox.TYPE_CONTACTS) {
                authority = ContactsContract.AUTHORITY;
            } else {
                authority = CalendarContract.AUTHORITY;
                if (!mCalendarObservers.containsKey(account.mId)) {
                    // Make sure we have an observer for this Calendar, as
                    // we need to be able to detect sync state changes, sigh
                    registerCalendarObserver(account);
                }
            }
            // See if "sync automatically" is set; if not, punt
            if (!ContentResolver.getSyncAutomatically(mAccountList.getAmAccount(account), authority)) {
                return false;
                // See if the calendar is enabled from the Calendar app UI; if not, punt
            } else if ((type == Mailbox.TYPE_CALENDAR) && !isCalendarEnabled(account.mId)) {
                return false;
            }
            // Never automatically sync trash
        } else if (type == Mailbox.TYPE_TRASH) {
            return false;
            // For non-outbox, non-account mail, we do two checks:
            // 1) are we restricted by policy (i.e. manual sync only),
            // 2) has the user checked the "Sync Email" box in Account Settings, and
        } else if (!canAutoSync(account) || !canSyncEmail(mAccountList.getAmAccount(account))) {
            return false;
        }
        return true;
    }

    private long checkMailboxes() {
        // First, see if any running mailboxes have been deleted
        ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
        synchronized (sSyncLock) {
            for (long mailboxId : mServiceMap.keySet()) {
                Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
                if (m == null) {
                    deletedMailboxes.add(mailboxId);
                }
            }
            // If so, stop them or remove them from the map
            for (Long mailboxId : deletedMailboxes) {
                AbstractSyncService svc = mServiceMap.get(mailboxId);
                if (svc == null || svc.mThread == null) {
                    releaseMailbox(mailboxId);
                    continue;
                } else {
                    boolean alive = svc.mThread.isAlive();
                    log("Deleted mailbox: " + svc.mMailboxName);
                    if (alive) {
                        stopManualSync(mailboxId);
                    } else {
                        log("Removing from serviceMap");
                        releaseMailbox(mailboxId);
                    }
                }
            }
        }

        long nextWait = SYNC_SERVICE_HEARTBEAT_TIME;
        long now = System.currentTimeMillis();

        // 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) {
            log("mAccountObserver null; service died??");
            return nextWait;
        }

        Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
                mAccountObserver.getSyncableMailboxWhere(), null, null);
        if (c == null)
            throw new ProviderUnavailableException();
        try {
            while (c.moveToNext()) {
                long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
                AbstractSyncService service = getRunningService(mailboxId);
                if (service == null) {
                    // Get the cached account
                    Account account = getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
                    if (account == null)
                        continue;

                    // We handle a few types of mailboxes specially
                    int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
                    if (!isMailboxSyncable(account, mailboxType)) {
                        continue;
                    }

                    // Check whether we're in a hold (temporary or permanent)
                    SyncError syncError = mSyncErrorMap.get(mailboxId);
                    if (syncError != null) {
                        // Nothing we can do about fatal errors
                        if (syncError.fatal)
                            continue;
                        if (now < syncError.holdEndTime) {
                            // If release time is earlier than next wait time,
                            // move next wait time up to the release time
                            if (syncError.holdEndTime < now + nextWait) {
                                nextWait = syncError.holdEndTime - now;
                                mNextWaitReason = "Release hold";
                            }
                            continue;
                        } else {
                            // Keep the error around, but clear the end time
                            syncError.holdEndTime = 0;
                        }
                    }

                    // Otherwise, we use the sync interval
                    long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
                    if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
                        Mailbox m = EmailContent.getContent(c, Mailbox.class);
                        requestSync(m, SYNC_PUSH, null);
                    } else if (mailboxType == Mailbox.TYPE_OUTBOX) {
                        if (hasSendableMessages(c)) {
                            Mailbox m = EmailContent.getContent(c, Mailbox.class);
                            startServiceThread(getServiceForMailbox(this, m));
                        }
                    } else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
                        // TODO: Migrating to use system SyncManager, so this should be dead code.
                        long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
                        long sinceLastSync = now - lastSync;
                        long toNextSync = syncInterval * MINUTES - sinceLastSync;
                        String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
                        if (toNextSync <= 0) {
                            Mailbox m = EmailContent.getContent(c, Mailbox.class);
                            requestSync(m, SYNC_SCHEDULED, null);
                        } else if (toNextSync < nextWait) {
                            nextWait = toNextSync;
                            if (sUserLog) {
                                log("Next sync for " + name + " in " + nextWait / 1000 + "s");
                            }
                            mNextWaitReason = "Scheduled sync, " + name;
                        } else if (sUserLog) {
                            log("Next sync for " + name + " in " + toNextSync / 1000 + "s");
                        }
                    }
                } else {
                    Thread thread = service.mThread;
                    // Look for threads that have died and remove them from the map
                    if (thread != null && !thread.isAlive()) {
                        if (sUserLog) {
                            log("Dead thread, mailbox released: "
                                    + c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
                        }
                        synchronized (sSyncLock) {
                            releaseMailbox(mailboxId);
                        }
                        // Restart this if necessary
                        if (nextWait > 3 * SECONDS) {
                            nextWait = 3 * SECONDS;
                            mNextWaitReason = "Clean up dead thread(s)";
                        }
                    } else {
                        long requestTime = service.mRequestTime;
                        if (requestTime > 0) {
                            long timeToRequest = requestTime - now;
                            if (timeToRequest <= 0) {
                                service.mRequestTime = 0;
                                service.alarm();
                            } else if (requestTime > 0 && timeToRequest < nextWait) {
                                if (timeToRequest < 11 * MINUTES) {
                                    nextWait = timeToRequest < 250 ? 250 : timeToRequest;
                                    mNextWaitReason = "Sync data change";
                                } else {
                                    log("Illegal timeToRequest: " + timeToRequest);
                                }
                            }
                        }
                    }
                }
            }
        } finally {
            c.close();
        }
        return nextWait;
    }

    static public void serviceRequest(long mailboxId, int reason) {
        serviceRequest(mailboxId, 5 * SECONDS, reason);
    }

    /**
     * Return a boolean indicating whether the mailbox can be synced
     * @param m the mailbox
     * @return whether or not the mailbox can be synced
     */
    public static boolean isSyncable(Mailbox m) {
        return m.mType != Mailbox.TYPE_DRAFTS && m.mType != Mailbox.TYPE_OUTBOX && m.mType != Mailbox.TYPE_SEARCH
                && m.mType < Mailbox.TYPE_NOT_SYNCABLE;
    }

    static public void serviceRequest(long mailboxId, long ms, int reason) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
        if (m == null || !isSyncable(m))
            return;
        try {
            AbstractSyncService service = ssm.getRunningService(mailboxId);
            if (service != null) {
                service.mRequestTime = System.currentTimeMillis() + ms;
                kick("service request");
            } else {
                startManualSync(mailboxId, reason, null);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static public void serviceRequestImmediate(long mailboxId) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        AbstractSyncService service = ssm.getRunningService(mailboxId);
        if (service != null) {
            service.mRequestTime = System.currentTimeMillis();
            Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
            if (m != null) {
                service.mAccount = Account.restoreAccountWithId(ssm, m.mAccountKey);
                service.mMailbox = m;
                kick("service request immediate");
            }
        }
    }

    static public void sendMessageRequest(Request req) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        Message msg = Message.restoreMessageWithId(ssm, req.mMessageId);
        if (msg == null)
            return;
        long mailboxId = msg.mMailboxKey;
        Mailbox mailbox = Mailbox.restoreMailboxWithId(ssm, mailboxId);
        if (mailbox == null)
            return;

        // If we're loading an attachment for Outbox, we want to look at the source message
        // to find the loading mailbox
        if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
            long sourceId = Utility.getFirstRowLong(ssm, Body.CONTENT_URI,
                    new String[] { BodyColumns.SOURCE_MESSAGE_KEY }, BodyColumns.MESSAGE_KEY + "=?",
                    new String[] { Long.toString(msg.mId) }, null, 0, -1L);
            if (sourceId != -1L) {
                EmailContent.Message sourceMsg = EmailContent.Message.restoreMessageWithId(ssm, sourceId);
                if (sourceMsg != null) {
                    mailboxId = sourceMsg.mMailboxKey;
                }
            }
        }
        sendRequest(mailboxId, req);
    }

    static public void sendRequest(long mailboxId, Request req) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        AbstractSyncService service = ssm.getRunningService(mailboxId);
        if (service == null) {
            startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
            kick("part request");
        } else {
            service.addRequest(req);
        }
    }

    /**
     * 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)
     */
    static public int pingStatus(long mailboxId) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return PING_STATUS_OK;
        // Already syncing...
        if (ssm.getRunningService(mailboxId) != null) {
            return PING_STATUS_RUNNING;
        }
        // No errors or a transient error, don't ping...
        SyncError error = ssm.mSyncErrorMap.get(mailboxId);
        if (error != null) {
            if (error.fatal) {
                return PING_STATUS_UNABLE;
            } else if (error.holdEndTime > 0) {
                return PING_STATUS_WAITING;
            }
        }
        return PING_STATUS_OK;
    }

    static public void startManualSync(long mailboxId, int reason, Request req) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        synchronized (sSyncLock) {
            AbstractSyncService svc = ssm.mServiceMap.get(mailboxId);
            if (svc == null) {
                if (ssm.mSyncErrorMap.containsKey(mailboxId) && reason == SyncManager.SYNC_UPSYNC) {
                    return;
                } else if (reason != SyncManager.SYNC_UPSYNC) {
                    ssm.mSyncErrorMap.remove(mailboxId);
                }
                Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
                if (m != null) {
                    log("Starting sync for " + m.mDisplayName);
                    ssm.requestSync(m, reason, req);
                }
            } else {
                // If this is a ui request, set the sync reason for the service
                if (reason >= SYNC_CALLBACK_START) {
                    svc.mSyncReason = reason;
                }
            }
        }
    }

    // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
    static public void stopManualSync(long mailboxId) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        synchronized (sSyncLock) {
            AbstractSyncService svc = ssm.mServiceMap.get(mailboxId);
            if (svc != null) {
                log("Stopping sync for " + svc.mMailboxName);
                svc.stop();
                svc.mThread.interrupt();
                ssm.releaseWakeLock(mailboxId);
            }
        }
    }

    /**
     * Wake up SyncServiceManager to check for mailboxes needing service
     */
    static public void kick(String reason) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            synchronized (ssm) {
                //INSTANCE.log("Kick: " + reason);
                ssm.mKicked = true;
                ssm.notify();
            }
        }
        if (sConnectivityLock != null) {
            synchronized (sConnectivityLock) {
                sConnectivityLock.notify();
            }
        }
    }

    /**
     * Tell SyncServiceManager to remove the mailbox from the map of mailboxes with sync errors
     * @param mailboxId the id of the mailbox
     */
    static public void removeFromSyncErrorMap(long mailboxId) {
        SyncManager ssm = INSTANCE;
        if (ssm != null) {
            ssm.mSyncErrorMap.remove(mailboxId);
        }
    }

    private boolean isRunningInServiceThread(long mailboxId) {
        AbstractSyncService syncService = getRunningService(mailboxId);
        Thread thisThread = Thread.currentThread();
        return syncService != null && syncService.mThread != null && thisThread == syncService.mThread;
    }

    /**
     * Sent by services indicating that their thread is finished; action depends on the exitStatus
     * of the service.
     *
     * @param svc the service that is finished
     */
    static public void done(AbstractSyncService svc) {
        SyncManager ssm = INSTANCE;
        if (ssm == null)
            return;
        synchronized (sSyncLock) {
            long mailboxId = svc.mMailboxId;
            // If we're no longer the syncing thread for the mailbox, just return
            if (!ssm.isRunningInServiceThread(mailboxId)) {
                return;
            }
            ssm.releaseMailbox(mailboxId);
            ssm.setMailboxSyncStatus(mailboxId, EmailContent.SYNC_STATUS_NONE);

            ConcurrentHashMap<Long, SyncError> errorMap = ssm.mSyncErrorMap;
            SyncError syncError = errorMap.get(mailboxId);

            int exitStatus = svc.mExitStatus;
            Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId);
            if (m == null)
                return;

            if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) {
                long accountId = m.mAccountKey;
                Account account = Account.restoreAccountWithId(ssm, accountId);
                if (account == null)
                    return;
                if (ssm.releaseSyncHolds(ssm, AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
                    new AccountServiceProxy(ssm).notifyLoginSucceeded(accountId);
                }
            }

            int lastResult = EmailContent.LAST_SYNC_RESULT_SUCCESS;
            // For error states, whether the error is fatal (won't automatically be retried)
            boolean errorIsFatal = true;
            try {
                switch (exitStatus) {
                case AbstractSyncService.EXIT_DONE:
                    if (svc.hasPendingRequests()) {
                        // TODO Handle this case
                    }
                    errorMap.remove(mailboxId);
                    // If we've had a successful sync, clear the shutdown count
                    synchronized (SyncManager.class) {
                        sClientConnectionManagerShutdownCount = 0;
                    }
                    // Leave now; other statuses are errors
                    return;
                // I/O errors get retried at increasing intervals
                case AbstractSyncService.EXIT_IO_ERROR:
                    if (syncError != null) {
                        syncError.escalate();
                        log(m.mDisplayName + " held for " + (syncError.holdDelay / 1000) + "s");
                        return;
                    } else {
                        log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
                    }
                    lastResult = EmailContent.LAST_SYNC_RESULT_CONNECTION_ERROR;
                    errorIsFatal = false;
                    break;
                // These errors are not retried automatically
                case AbstractSyncService.EXIT_LOGIN_FAILURE:
                    new AccountServiceProxy(ssm).notifyLoginFailed(m.mAccountKey, svc.mExitReason);
                    lastResult = EmailContent.LAST_SYNC_RESULT_AUTH_ERROR;
                    break;
                case AbstractSyncService.EXIT_SECURITY_FAILURE:
                case AbstractSyncService.EXIT_ACCESS_DENIED:
                    lastResult = EmailContent.LAST_SYNC_RESULT_SECURITY_ERROR;
                    break;
                case AbstractSyncService.EXIT_EXCEPTION:
                    lastResult = EmailContent.LAST_SYNC_RESULT_INTERNAL_ERROR;
                    break;
                }
                // Add this box to the error map
                errorMap.put(mailboxId, ssm.new SyncError(exitStatus, errorIsFatal));
            } finally {
                // Always set the last result
                ssm.setMailboxLastSyncResult(mailboxId, lastResult);
                kick("sync completed");
            }
        }
    }

    /**
     * Given the status string from a Mailbox, return the type code for the last sync
     * @param status the syncStatus column of a Mailbox
     * @return
     */
    static public int getStatusType(String status) {
        if (status == null) {
            return -1;
        } else {
            return status.charAt(STATUS_TYPE_CHAR) - '0';
        }
    }

    /**
     * Given the status string from a Mailbox, return the change count for the last sync
     * The change count is the number of adds + deletes + changes in the last sync
     * @param status the syncStatus column of a Mailbox
     * @return
     */
    static public int getStatusChangeCount(String status) {
        try {
            String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
            return Integer.parseInt(s);
        } catch (RuntimeException e) {
            return -1;
        }
    }

    static public Context getContext() {
        return INSTANCE;
    }

    private void writeWakeLockTimes(PrintWriter pw, HashMap<Long, Long> map, boolean historical) {
        long now = System.currentTimeMillis();
        for (long mailboxId : map.keySet()) {
            Long time = map.get(mailboxId);
            if (time == null) {
                // Just in case...
                continue;
            }
            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
            StringBuilder sb = new StringBuilder();
            if (mailboxId == EXTRA_MAILBOX_ID) {
                sb.append("    SyncManager");
            } else if (mailbox == null) {
                sb.append("    Mailbox " + mailboxId + " (deleted?)");
            } else {
                String protocol = Account.getProtocol(this, mailbox.mAccountKey);
                sb.append("    Mailbox " + mailboxId + " (" + protocol + ", type " + mailbox.mType + ")");
            }
            long logTime = historical ? time : (now - time);
            sb.append(" held for " + (logTime / 1000) + "s");
            pw.println(sb.toString());
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        long uptime = System.currentTimeMillis() - mServiceStartTime;
        pw.println("SyncManager: " + TAG + " up for " + (uptime / 1000 / 60) + " m");
        if (mWakeLock != null) {
            pw.println("  Holding WakeLock");
            writeWakeLockTimes(pw, mWakeLocks, false);
        } else {
            pw.println("  Not holding WakeLock");
        }
        if (!mWakeLocksHistory.isEmpty()) {
            pw.println("  Historical times");
            writeWakeLockTimes(pw, mWakeLocksHistory, true);
        }
    }
}