blob: 3f7918cd5186397bb7db68a35b2ebaab779d4206 [file] [log] [blame]
/*
* 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.android.exchange;
import com.android.email.AccountBackupRestore;
import com.android.email.Email;
import com.android.email.SecurityPolicy;
import com.android.email.mail.MessagingException;
import com.android.email.mail.transport.SSLUtils;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.provider.EmailContent.HostAuthColumns;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.SyncColumns;
import com.android.email.service.EmailServiceStatus;
import com.android.email.service.IEmailService;
import com.android.email.service.IEmailServiceCallback;
import com.android.exchange.adapter.CalendarSyncAdapter;
import com.android.exchange.utility.FileLogger;
import org.apache.http.conn.ClientConnectionManager;
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.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OnAccountsUpdateListener;
import android.accounts.OperationCanceledException;
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.content.SyncStatusObserver;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.NetworkInfo.State;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.PowerManager.WakeLock;
import android.provider.Calendar;
import android.provider.ContactsContract;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
import android.util.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
* adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
* would be appropriate to use for IMAP push, when that functionality is added to the Email
* application.
*
* The Email application communicates with EAS sync adapters via SyncManager's binder interface,
* which exposes UI-related functionality to the application (see the definitions below)
*
* SyncManager uses ContentObservers to detect changes to accounts, mailboxes, and messages in
* order to maintain proper 2-way syncing of data. (More documentation to follow)
*
*/
public class SyncManager extends Service implements Runnable {
private static final String TAG = "EAS SyncManager";
// The SyncManager's mailbox "id"
private static final int SYNC_MANAGER_ID = -1;
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_MANAGER_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 SyncManager.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;
// startSync was requested of SyncManager
public static final int SYNC_SERVICE_START_SYNC = 4;
// A part request (attachment load, for now) was sent to SyncManager
public static final int SYNC_SERVICE_PART_REQUEST = 5;
// Misc.
public static final int SYNC_KICK = 6;
private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
" IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
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 + ')';
private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
AbstractSyncService.EAS_PROTOCOL + "\"";
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 (";
private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
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;
// 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;
// Keep our cached list of active Accounts here
public static final AccountList sAccountList = new AccountList();
// Keeps track of running services (by mailbox id)
private HashMap<Long, AbstractSyncService> mServiceMap =
new HashMap<Long, AbstractSyncService>();
// Keeps track of services whose last sync ended with an error (by mailbox id)
/*package*/ HashMap<Long, SyncError> mSyncErrorMap = new HashMap<Long, SyncError>();
// Keeps track of which services require a wake lock (by mailbox id)
private HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
// Keeps track of PendingIntents for mailbox alarms (by mailbox id)
private HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
// The actual WakeLock obtained by SyncManager
private WakeLock mWakeLock = null;
// Observers that we use to look for changed mail-related data
private Handler mHandler = new Handler();
private AccountObserver mAccountObserver;
private MailboxObserver mMailboxObserver;
private SyncedMessageObserver mSyncedMessageObserver;
private MessageObserver mMessageObserver;
private EasSyncStatusObserver mSyncStatusObserver;
private EasAccountsUpdatedListener mAccountsUpdatedListener;
private HashMap<Long, CalendarObserver> mCalendarObservers =
new HashMap<Long, CalendarObserver>();
private ContentResolver mResolver;
// The singleton SyncManager object, with its thread and stop flag
protected static SyncManager INSTANCE;
private static Thread sServiceThread = null;
// Cached unique device id
private static String sDeviceId = null;
// ConnectionManager that all EAS threads can use
private static ClientConnectionManager sClientConnectionManager = null;
private boolean mStop = false;
// The reason for SyncManager'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;
private ConnectivityReceiver mBackgroundDataSettingReceiver = null;
private volatile boolean mBackgroundData = true;
// The callback sent in from the UI using setCallback
private IEmailServiceCallback mCallback;
private RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
/**
* Proxy that can be used by various sync adapters to tie into SyncManager's callback system.
* Used this way: SyncManager.callback().callbackMethod(args...);
* The proxy wraps checking for existence of a SyncManager instance and an active callback.
* Failures of these callbacks can be safely ignored.
*/
static private final IEmailServiceCallback.Stub sCallbackProxy =
new IEmailServiceCallback.Stub() {
public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
int progress) throws RemoteException {
IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
if (cb != null) {
cb.loadAttachmentStatus(messageId, attachmentId, statusCode, progress);
}
}
public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
throws RemoteException {
IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
if (cb != null) {
cb.sendMessageStatus(messageId, subject, statusCode, progress);
}
}
public void syncMailboxListStatus(long accountId, int statusCode, int progress)
throws RemoteException {
IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
if (cb != null) {
cb.syncMailboxListStatus(accountId, statusCode, progress);
}
}
public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
throws RemoteException {
IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
if (cb != null) {
cb.syncMailboxStatus(mailboxId, statusCode, progress);
}
}
};
/**
* Create our EmailService implementation here.
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
public int validate(String protocol, String host, String userName, String password,
int port, boolean ssl, boolean trustCertificates) throws RemoteException {
try {
AbstractSyncService.validate(EasSyncService.class, host, userName, password, port,
ssl, trustCertificates, SyncManager.this);
return MessagingException.NO_ERROR;
} catch (MessagingException e) {
return e.getExceptionType();
}
}
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return new EasSyncService().tryAutodiscover(userName, password);
}
public void startSync(long mailboxId) throws RemoteException {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
checkSyncManagerServiceRunning();
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
if (m == null) return;
if (m.mType == Mailbox.TYPE_OUTBOX) {
// We're using SERVER_ID to indicate an error condition (it has no other use for
// sent mail) Upon request to sync the Outbox, we clear this so that all messages
// are candidates for sending.
ContentValues cv = new ContentValues();
cv.put(SyncColumns.SERVER_ID, 0);
syncManager.getContentResolver().update(Message.CONTENT_URI,
cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
// Clear the error state; the Outbox sync will be started from checkMailboxes
syncManager.mSyncErrorMap.remove(mailboxId);
kick("start outbox");
// Outbox can't be synced in EAS
return;
} else if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_TRASH) {
// Drafts & Trash can't be synced in EAS
return;
}
startManualSync(mailboxId, SyncManager.SYNC_SERVICE_START_SYNC, null);
}
public void stopSync(long mailboxId) throws RemoteException {
stopManualSync(mailboxId);
}
public void loadAttachment(long attachmentId, String destinationFile,
String contentUriString) throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
sendMessageRequest(new PartRequest(att, destinationFile, contentUriString));
}
public void updateFolderList(long accountId) throws RemoteException {
reloadFolderList(SyncManager.this, accountId, false);
}
public void hostChanged(long accountId) throws RemoteException {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
synchronized (sSyncLock) {
HashMap<Long, SyncError> syncErrorMap = syncManager.mSyncErrorMap;
ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
// Go through the various error mailboxes
for (long mailboxId: syncErrorMap.keySet()) {
SyncError error = syncErrorMap.get(mailboxId);
// If it's a login failure, look a little harder
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
// If it's for the account whose host has changed, clear the error
// If the mailbox is no longer around, remove the entry in the map
if (m == null) {
deletedMailboxes.add(mailboxId);
} else if (m.mAccountKey == accountId) {
error.fatal = false;
error.holdEndTime = 0;
}
}
for (long mailboxId: deletedMailboxes) {
syncErrorMap.remove(mailboxId);
}
}
// Stop any running syncs
syncManager.stopAccountSyncs(accountId, true);
// Kick SyncManager
kick("host changed");
}
public void setLogging(int on) throws RemoteException {
Eas.setUserDebug(on);
}
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
sendMessageRequest(new MeetingResponseRequest(messageId, response));
}
public void loadMore(long messageId) throws RemoteException {
}
// The following three methods are not implemented in this version
public boolean createFolder(long accountId, String name) throws RemoteException {
return false;
}
public boolean deleteFolder(long accountId, String name) throws RemoteException {
return false;
}
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
return false;
}
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
if (mCallback != null) {
mCallbackList.unregister(mCallback);
}
mCallback = cb;
mCallbackList.register(cb);
}
};
static class AccountList extends ArrayList<Account> {
private static final long serialVersionUID = 1L;
public boolean contains(long id) {
for (Account account : this) {
if (account.mId == id) {
return true;
}
}
return false;
}
public Account getById(long id) {
for (Account account : this) {
if (account.mId == id) {
return account;
}
}
return null;
}
}
class AccountObserver extends ContentObserver {
String mSyncableEasMailboxSelector = null;
String mEasAccountSelector = null;
public AccountObserver(Handler handler) {
super(handler);
// At startup, we want to see what EAS accounts exist and cache them
Context context = getContext();
synchronized (sAccountList) {
Cursor c = getContentResolver().query(Account.CONTENT_URI,
Account.CONTENT_PROJECTION, null, null, null);
// Build the account list from the cursor
try {
collectEasAccounts(c, sAccountList);
} finally {
c.close();
}
// Create an account mailbox for any account without one
for (Account account : sAccountList) {
int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
+ account.mId, null);
if (cnt == 0) {
addAccountMailbox(account.mId);
}
}
}
}
/**
* 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 getSyncableEasMailboxWhere() {
if (mSyncableEasMailboxSelector == null) {
StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
boolean first = true;
synchronized (sAccountList) {
for (Account account : sAccountList) {
if (!first) {
sb.append(',');
} else {
first = false;
}
sb.append(account.mId);
}
}
sb.append(')');
mSyncableEasMailboxSelector = sb.toString();
}
return mSyncableEasMailboxSelector;
}
/**
* Returns a String suitable for appending to a where clause that selects for all eas
* accounts.
* @return a String in the form "accountKey in (a, b, c...)" that is not to be cached
*/
public String getAccountKeyWhere() {
if (mEasAccountSelector == null) {
StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
boolean first = true;
synchronized (sAccountList) {
for (Account account : sAccountList) {
if (!first) {
sb.append(',');
} else {
first = false;
}
sb.append(account.mId);
}
}
sb.append(')');
mEasAccountSelector = sb.toString();
}
return mEasAccountSelector;
}
private boolean onSecurityHold(Account account) {
return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
}
private void onAccountChanged() {
maybeStartSyncManagerThread();
Context context = getContext();
// A change to the list requires us to scan for deletions (stop running syncs)
// At startup, we want to see what accounts exist and cache them
AccountList currentAccounts = new AccountList();
Cursor c = getContentResolver().query(Account.CONTENT_URI,
Account.CONTENT_PROJECTION, null, null, null);
try {
collectEasAccounts(c, currentAccounts);
synchronized (sAccountList) {
for (Account account : sAccountList) {
// Ignore accounts not fully created
if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
log("Account observer noticed incomplete account; ignoring");
continue;
} else if (!currentAccounts.contains(account.mId)) {
// This is a deletion; shut down any account-related syncs
stopAccountSyncs(account.mId, true);
// Delete this from AccountManager...
android.accounts.Account acct = new android.accounts.Account(
account.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
mSyncableEasMailboxSelector = null;
mEasAccountSelector = null;
} else {
// An account has changed
Account updatedAccount = Account.restoreAccountWithId(context,
account.mId);
if (account.mSyncInterval != updatedAccount.mSyncInterval
|| account.mSyncLookback != updatedAccount.mSyncLookback) {
// Set pushable boxes' interval to the interval of the Account
ContentValues cv = new ContentValues();
cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
getContentResolver().update(Mailbox.CONTENT_URI, cv,
WHERE_IN_ACCOUNT_AND_PUSHABLE, new String[] {
Long.toString(account.mId)
});
// Stop all current syncs; the appropriate ones will restart
log("Account " + account.mDisplayName + " changed; stop syncs");
stopAccountSyncs(account.mId, true);
}
// See if this account is no longer on security hold
if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
releaseSyncHolds(SyncManager.this,
AbstractSyncService.EXIT_SECURITY_FAILURE, account);
}
// Put current values into our cached account
account.mSyncInterval = updatedAccount.mSyncInterval;
account.mSyncLookback = updatedAccount.mSyncLookback;
account.mFlags = updatedAccount.mFlags;
}
}
// Look for new accounts
for (Account account : currentAccounts) {
if (!sAccountList.contains(account.mId)) {
// This is an addition; create our magic hidden mailbox...
log("Account observer found new account: " + account.mDisplayName);
addAccountMailbox(account.mId);
// Don't forget to cache the HostAuth
HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
account.mHostAuthKeyRecv);
account.mHostAuthRecv = ha;
sAccountList.add(account);
mSyncableEasMailboxSelector = null;
mEasAccountSelector = null;
}
}
// Finally, make sure our account list is up to date
sAccountList.clear();
sAccountList.addAll(currentAccounts);
}
} finally {
c.close();
}
// See if there's anything to do...
kick("account changed");
}
@Override
public void onChange(boolean selfChange) {
new Thread(new Runnable() {
public void run() {
onAccountChanged();
}}).start();
}
private void collectEasAccounts(Cursor c, ArrayList<Account> accounts) {
Context context = getContext();
if (context == null) return;
while (c.moveToNext()) {
long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
if (hostAuthId > 0) {
HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
if (ha != null && ha.mProtocol.equals("eas")) {
Account account = new Account().restore(c);
// Cache the HostAuth
account.mHostAuthRecv = ha;
accounts.add(account);
}
}
}
}
private void addAccountMailbox(long acctId) {
Account acct = Account.restoreAccountWithId(getContext(), acctId);
Mailbox main = new Mailbox();
main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX;
main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime();
main.mAccountKey = acct.mId;
main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
main.mFlagVisible = false;
main.save(getContext());
log("Initializing account: " + acct.mDisplayName);
}
}
/**
* 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
*/
private void unregisterCalendarObservers() {
for (CalendarObserver observer: mCalendarObservers.values()) {
mResolver.unregisterContentObserver(observer);
}
mCalendarObservers.clear();
}
private class CalendarObserver extends ContentObserver {
long mAccountId;
long mCalendarId;
long mSyncEvents;
String mAccountName;
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},
CalendarSyncAdapter.CALENDAR_SELECTION,
new String[] {account.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE},
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() {
public void run() {
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)
EasSyncService service =
new EasSyncService(INSTANCE, mailbox);
CalendarSyncAdapter adapter =
new CalendarSyncAdapter(mailbox, service);
try {
adapter.setSyncKey("0", false);
} catch (IOException e) {
// The provider can't be reached; nothing to be done
}
// Reset the sync key locally
cv.put(Mailbox.SYNC_KEY, "0");
cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
// Delete all events in this calendar using the sync adapter
// parameter so that the deletion is only local
Uri eventsAsSyncAdapter =
Events.CONTENT_URI.buildUpon()
.appendQueryParameter(
Calendar.CALLER_IS_SYNCADAPTER, "true")
.build();
mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
new String[] {Long.toString(mCalendarId)});
} else {
// Set sync back to push
cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
kick("calendar sync changed");
}
// Update the calendar mailbox with new settings
mResolver.update(ContentUris.withAppendedId(
Mailbox.CONTENT_URI, mailbox.mId), cv, null, null);
// Save away the new value
mSyncEvents = newSyncEvents;
}
}
} finally {
c.close();
}
}}).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) {
log("SyncedMessage changed: (re)setting alarm for 10s");
alarmManager.set(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
}
}
private class MessageObserver extends ContentObserver {
public MessageObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
// A rather blunt instrument here. But we don't have information about the URI that
// triggered this, though it must have been an insert
if (!selfChange) {
kick(null);
}
}
}
static public IEmailServiceCallback callback() {
return sCallbackProxy;
}
static public Account getAccountById(long accountId) {
synchronized (sAccountList) {
return sAccountList.getById(accountId);
}
}
static public String getEasAccountSelector() {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return null;
return syncManager.mAccountObserver.getAccountKeyWhere();
}
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*/ class SyncError {
int reason;
boolean fatal = false;
long holdDelay = 15*SECONDS;
long holdEndTime = System.currentTimeMillis() + holdDelay;
SyncError(int _reason, boolean _fatal) {
reason = _reason;
fatal = _fatal;
}
/**
* We double the holdDelay from 15 seconds through 4 mins
*/
void escalate() {
if (holdDelay < HOLD_DELAY_MAXIMUM) {
holdDelay *= 2;
}
holdEndTime = System.currentTimeMillis() + holdDelay;
}
}
private void logSyncHolds() {
if (Eas.USER_LOG && !mSyncErrorMap.isEmpty()) {
log("Sync holds:");
long time = System.currentTimeMillis();
synchronized (sSyncLock) {
for (long mailboxId : mSyncErrorMap.keySet()) {
Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
if (m == null) {
log("Mailbox " + mailboxId + " no longer exists");
} else {
SyncError error = mSyncErrorMap.get(mailboxId);
log("Mailbox " + m.mDisplayName + ", error = " + error.reason
+ ", fatal = " + error.fatal);
if (error.holdEndTime > 0) {
log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
}
}
}
}
}
}
/**
* Release a specific type of hold (the reason) for the specified Account; if the account
* is null, mailboxes from all accounts with the specified hold will be released
* @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
* @param account an Account whose mailboxes should be released (or all if null)
*/
/*package*/ void releaseSyncHolds(Context context, int reason, Account account) {
releaseSyncHoldsImpl(context, reason, account);
kick("security release");
}
private void releaseSyncHoldsImpl(Context context, int reason, Account account) {
synchronized(sSyncLock) {
ArrayList<Long> releaseList = new ArrayList<Long>();
for (long mailboxId: mSyncErrorMap.keySet()) {
if (account != null) {
Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId);
if (m == null) {
releaseList.add(mailboxId);
} else if (m.mAccountKey != account.mId) {
continue;
}
}
SyncError error = mSyncErrorMap.get(mailboxId);
if (error.reason == reason) {
releaseList.add(mailboxId);
}
}
for (long mailboxId: releaseList) {
mSyncErrorMap.remove(mailboxId);
}
}
}
public class EasSyncStatusObserver implements SyncStatusObserver {
public void onStatusChanged(int which) {
// We ignore the argument (we can only get called in one case - when settings change)
if (INSTANCE != null) {
checkPIMSyncSettings();
}
}
}
/**
* The reconciler (which is called from this listener) can make blocking calls back into
* the account manager. So, in this callback we spin up a worker thread to call the
* reconciler.
*/
public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
public void onAccountsUpdated(android.accounts.Account[] accounts) {
new Thread() {
@Override
public void run() {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
android.accounts.Account[] accountMgrList = AccountManager.get(syncManager)
.getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
synchronized (sAccountList) {
// Make sure we have an up-to-date sAccountList
mAccountObserver.onAccountChanged();
reconcileAccountsWithAccountManager(syncManager, sAccountList,
accountMgrList, false, mResolver);
}
}
}
}.start();
}
}
protected static void log(String str) {
if (Eas.USER_LOG) {
Log.d(TAG, str);
if (Eas.FILE_LOG) {
FileLogger.log(TAG, str);
}
}
}
protected static void alwaysLog(String str) {
if (!Eas.USER_LOG) {
Log.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 droid<n> where <n> is system time.
* 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() throws IOException {
return getDeviceId(null);
}
static public synchronized String getDeviceId(Context context) throws IOException {
SyncManager syncManager = INSTANCE;
if (sDeviceId != null) {
return sDeviceId;
} else if (syncManager == null && context == null) {
throw new IOException("No context for getDeviceId");
} else if (context == null) {
context = syncManager;
}
// Otherwise, we'll read the id file or create one if it's not found
try {
File f = context.getFileStreamPath("deviceName");
BufferedReader rdr = null;
String id;
if (f.exists() && f.canRead()) {
rdr = new BufferedReader(new FileReader(f), 128);
id = rdr.readLine();
rdr.close();
return id;
} else if (f.createNewFile()) {
BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
id = "android" + System.currentTimeMillis();
w.write(id);
w.close();
sDeviceId = id;
return id;
}
} catch (IOException e) {
}
throw new IOException("Can't get device name");
}
@Override
public IBinder onBind(Intent arg0) {
return mBinder;
}
/**
* Note that there are two ways the EAS SyncManager 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)
*/
@Override
public void onCreate() {
alwaysLog("!!! EAS SyncManager, onCreate");
if (INSTANCE == null) {
INSTANCE = this;
mResolver = getContentResolver();
mAccountObserver = new AccountObserver(mHandler);
mResolver.registerContentObserver(Account.CONTENT_URI, true, mAccountObserver);
mMailboxObserver = new MailboxObserver(mHandler);
mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
mMessageObserver = new MessageObserver(mHandler);
mSyncStatusObserver = new EasSyncStatusObserver();
} else {
alwaysLog("!!! EAS SyncManager onCreated, but INSTANCE not null??");
}
if (sDeviceId == null) {
try {
getDeviceId(this);
} catch (IOException e) {
// We can't run in this situation
throw new RuntimeException();
}
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
alwaysLog("!!! EAS SyncManager, onStartCommand");
// Restore accounts, if it has not happened already
AccountBackupRestore.restoreAccountsIfNeeded(this);
maybeStartSyncManagerThread();
if (sServiceThread == null) {
alwaysLog("!!! EAS SyncManager, stopping self");
stopSelf();
}
return Service.START_STICKY;
}
@Override
public void onDestroy() {
alwaysLog("!!! EAS SyncManager, onDestroy");
if (INSTANCE != null) {
INSTANCE = null;
mResolver.unregisterContentObserver(mAccountObserver);
unregisterCalendarObservers();
mResolver = null;
mAccountObserver = null;
mMailboxObserver = null;
mSyncedMessageObserver = null;
mMessageObserver = null;
mSyncStatusObserver = null;
mAccountsUpdatedListener = null;
}
}
void maybeStartSyncManagerThread() {
// Start our thread...
// See if there are any EAS accounts; otherwise, just go away
if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
if (sServiceThread == null || !sServiceThread.isAlive()) {
log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
sServiceThread = new Thread(this, "SyncManager");
sServiceThread.start();
}
}
}
static void checkSyncManagerServiceRunning() {
// Get the service thread running if it isn't
// This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
// com.android.email) and hasn't been restarted
// See the comment for onCreate for details
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
if (sServiceThread == null) {
alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
syncManager.startService(new Intent(syncManager, SyncManager.class));
}
}
static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
public int getMaxForRoute(HttpRoute route) {
return 8;
}
};
static public synchronized ClientConnectionManager getClientConnectionManager() {
if (sClientConnectionManager == null) {
// Create a registry for our three schemes; http and https will use built-in factories
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
// Use "insecure" socket factory.
SSLSocketFactory sf = new SSLSocketFactory(SSLUtils.getSSLSocketFactory(true));
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
// Register the httpts scheme with our factory
registry.register(new Scheme("httpts", sf, 443));
// And create a ccm with our registry
HttpParams params = new BasicHttpParams();
params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
sClientConnectionManager = new ThreadSafeClientConnManager(params, registry);
}
// Null is a valid return result if we get an exception
return sClientConnectionManager;
}
public static void stopAccountSyncs(long acctId) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
syncManager.stopAccountSyncs(acctId, true);
}
}
private 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);
}
}
}
static private void reloadFolderListFailed(long accountId) {
try {
callback().syncMailboxListStatus(accountId,
EmailServiceStatus.ACCOUNT_UNINITIALIZED, 0);
} catch (RemoteException e1) {
// Don't care if this fails
}
}
static public void reloadFolderList(Context context, long accountId, boolean force) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
MailboxColumns.TYPE + "=?",
new String[] {Long.toString(accountId),
Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
try {
if (c.moveToFirst()) {
synchronized(sSyncLock) {
Mailbox m = new Mailbox().restore(c);
Account acct = Account.restoreAccountWithId(context, accountId);
if (acct == null) {
reloadFolderListFailed(accountId);
return;
}
String syncKey = acct.mSyncKey;
// No need to reload the list if we don't have one
if (!force && (syncKey == null || syncKey.equals("0"))) {
reloadFolderListFailed(accountId);
return;
}
// Change all ping/push boxes to push/hold
ContentValues cv = new ContentValues();
cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
new String[] {Long.toString(accountId)});
log("Set push/ping boxes to push/hold");
long id = m.mId;
AbstractSyncService svc = syncManager.mServiceMap.get(id);
// Tell the service we're done
if (svc != null) {
synchronized (svc.getSynchronizer()) {
svc.stop();
}
// Interrupt the thread so that it can stop
Thread thread = svc.mThread;
thread.setName(thread.getName() + " (Stopped)");
thread.interrupt();
// Abandon the service
syncManager.releaseMailbox(id);
// And have it start naturally
kick("reload folder list");
}
}
}
} finally {
c.close();
}
}
/**
* Informs SyncManager 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 folderListReloaded(long acctId) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
syncManager.stopAccountSyncs(acctId, false);
kick("reload folder list");
}
}
private void acquireWakeLock(long id) {
synchronized (mWakeLocks) {
Boolean 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();
//log("+WAKE LOCK ACQUIRED");
}
mWakeLocks.put(id, true);
}
}
}
private void releaseWakeLock(long id) {
synchronized (mWakeLocks) {
Boolean lock = mWakeLocks.get(id);
if (lock != null) {
mWakeLocks.remove(id);
if (mWakeLocks.isEmpty()) {
if (mWakeLock != null) {
mWakeLock.release();
}
mWakeLock = null;
//log("+WAKE LOCK RELEASED");
} else {
}
}
}
}
static public String alarmOwner(long id) {
if (id == SYNC_MANAGER_ID) {
return "SyncManager";
} else {
String name = Long.toString(id);
if (Eas.USER_LOG && 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 void runAwake(long id) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
syncManager.acquireWakeLock(id);
syncManager.clearAlarm(id);
}
}
static public void runAsleep(long id, long millis) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
syncManager.setAlarm(id, millis);
syncManager.releaseWakeLock(id);
}
}
static public void clearWatchdogAlarm(long id) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
syncManager.clearAlarm(id);
}
}
static public void setWatchdogAlarm(long id, long millis) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
syncManager.setAlarm(id, millis);
}
}
static public void alert(Context context, final long id) {
final SyncManager syncManager = INSTANCE;
checkSyncManagerServiceRunning();
if (id < 0) {
kick("ping SyncManager");
} else if (syncManager == null) {
context.startService(new Intent(context, SyncManager.class));
} else {
final AbstractSyncService service = syncManager.mServiceMap.get(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
new Thread(new Runnable() {
public void run() {
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, 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 (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;
service.alarm();
}
}}).start();
}
}
}
/**
* See if we need to change the syncInterval for any of our PIM mailboxes based on changes
* to settings in the AccountManager (sync settings).
* This code is called 1) when SyncManager starts, and 2) when SyncManager is running and there
* are changes made (this is detected via a SyncStatusObserver)
*/
private void updatePIMSyncSettings(Account providerAccount, int mailboxType, String authority) {
ContentValues cv = new ContentValues();
long mailboxId =
Mailbox.findMailboxOfType(this, providerAccount.mId, mailboxType);
// Presumably there is one, but if not, it's ok. Just move on...
if (mailboxId != Mailbox.NO_MAILBOX) {
// Create an AccountManager style Account
android.accounts.Account acct =
new android.accounts.Account(providerAccount.mEmailAddress,
Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
// Get the mailbox; this happens rarely so it's ok to get it all
Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
if (mailbox == null) return;
int syncInterval = mailbox.mSyncInterval;
// If we're syncable, look further...
if (ContentResolver.getIsSyncable(acct, authority) > 0) {
// If we're supposed to sync automatically (push), set to push if it's not
if (ContentResolver.getSyncAutomatically(acct, authority)) {
if (syncInterval == Mailbox.CHECK_INTERVAL_NEVER || syncInterval > 0) {
log("Sync for " + mailbox.mDisplayName + " in " + acct.name + ": push");
cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
}
// If we're NOT supposed to push, and we're not set up that way, change it
} else if (syncInterval != Mailbox.CHECK_INTERVAL_NEVER) {
log("Sync for " + mailbox.mDisplayName + " in " + acct.name + ": manual");
cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
}
// If not, set it to never check
} else if (syncInterval != Mailbox.CHECK_INTERVAL_NEVER) {
log("Sync for " + mailbox.mDisplayName + " in " + acct.name + ": manual");
cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
}
// If we've made a change, update the Mailbox, and kick
if (cv.containsKey(MailboxColumns.SYNC_INTERVAL)) {
mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
cv,null, null);
kick("sync settings change");
}
}
}
/**
* Make our sync settings match those of AccountManager
*/
private void checkPIMSyncSettings() {
synchronized (sAccountList) {
for (Account account : sAccountList) {
updatePIMSyncSettings(account, Mailbox.TYPE_CONTACTS, ContactsContract.AUTHORITY);
updatePIMSyncSettings(account, Mailbox.TYPE_CALENDAR, Calendar.AUTHORITY);
}
}
}
/**
* Compare our account list (obtained from EmailProvider) with the account list owned by
* AccountManager. If there are any orphans (an account in one list without a corresponding
* account in the other list), delete the orphan, as these must remain in sync.
*
* Note that the duplication of account information is caused by the Email application's
* incomplete integration with AccountManager.
*
* This function may not be called from the main/UI thread, because it makes blocking calls
* into the account manager.
*
* @param context The context in which to operate
* @param cachedEasAccounts the exchange provider accounts to work from
* @param accountManagerAccounts The account manager accounts to work from
* @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
* @param resolver the content resolver for making provider updates (injected for testability)
*/
/* package */ static void reconcileAccountsWithAccountManager(Context context,
List<Account> cachedEasAccounts, android.accounts.Account[] accountManagerAccounts,
boolean blockExternalChanges, ContentResolver resolver) {
// First, look through our cached EAS Accounts (from EmailProvider) to make sure there's a
// corresponding AccountManager account
boolean accountsDeleted = false;
for (Account providerAccount: cachedEasAccounts) {
String providerAccountName = providerAccount.mEmailAddress;
boolean found = false;
for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
found = true;
break;
}
}
if (!found) {
if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
log("Account reconciler noticed incomplete account; ignoring");
continue;
}
// This account has been deleted in the AccountManager!
alwaysLog("Account deleted in AccountManager; deleting from provider: " +
providerAccountName);
// TODO This will orphan downloaded attachments; need to handle this
resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
providerAccount.mId), null, null);
accountsDeleted = true;
}
}
// Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
// account from EmailProvider
for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
String accountManagerAccountName = accountManagerAccount.name;
boolean found = false;
for (Account cachedEasAccount: cachedEasAccounts) {
if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
found = true;
}
}
if (!found) {
// This account has been deleted from the EmailProvider database
alwaysLog("Account deleted from provider; deleting from AccountManager: " +
accountManagerAccountName);
// Delete the account
AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
.removeAccount(accountManagerAccount, null, null);
try {
// Note: All of the potential errors from removeAccount() are simply logged
// here, as there is nothing to actually do about them.
blockingResult.getResult();
} catch (OperationCanceledException e) {
Log.w(Email.LOG_TAG, e.toString());
} catch (AuthenticatorException e) {
Log.w(Email.LOG_TAG, e.toString());
} catch (IOException e) {
Log.w(Email.LOG_TAG, e.toString());
}
accountsDeleted = true;
}
}
// If we changed the list of accounts, refresh the backup & security settings
if (!blockExternalChanges && accountsDeleted) {
AccountBackupRestore.backupAccounts(context);
SecurityPolicy.getInstance(context).reducePolicies();
}
}
public class ConnectivityReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
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");
}
}
} else if (intent.getAction().equals(
ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) {
ConnectivityManager cm = (ConnectivityManager)SyncManager.this
.getSystemService(Context.CONNECTIVITY_SERVICE);
mBackgroundData = cm.getBackgroundDataSetting();
// If background data is now on, we want to kick SyncManager
if (mBackgroundData) {
kick("background data on");
log("Background data on; restart syncs");
// Otherwise, stop all syncs
} else {
log("Background data off: stop all syncs");
synchronized (sAccountList) {
for (Account account : sAccountList)
SyncManager.stopAccountSyncs(account.mId);
}
}
}
}
}
/**
* 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, Mailbox m) {
if (m == null) return;
synchronized (sSyncLock) {
String mailboxName = m.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(m.mId, service);
runAwake(m.mId);
if ((m.mServerId != null) && !m.mServerId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
stopPing(m.mAccountKey);
}
}
}
/**
* Stop any ping in progress for the given account
* @param accountId
*/
private void stopPing(long accountId) {
// Go through our active mailboxes looking for the right one
for (long mailboxId: mServiceMap.keySet()) {
Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
if (m != null) {
if (m.mAccountKey == accountId &&
m.mServerId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
// Here's our account mailbox; reset him (stopping pings)
AbstractSyncService svc = mServiceMap.get(mailboxId);
svc.reset();
}
}
}
}
private void requestSync(Mailbox m, int reason, Request req) {
// Don't sync if there's no connectivity
if (sConnectivityHold || (m == null)) return;
synchronized (sSyncLock) {
Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
if (acct != null) {
// Always make sure there's not a running instance of this service
AbstractSyncService service = mServiceMap.get(m.mId);
if (service == null) {
service = new EasSyncService(this, m);
if (!((EasSyncService)service).mIsValid) return;
service.mSyncReason = reason;
if (req != null) {
service.addRequest(req);
}
startServiceThread(service, m);
}
}
}
}
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)this.getSystemService(Context.CONNECTIVITY_SERVICE);
while (!mStop) {
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null) {
// 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(SYNC_MANAGER_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(SYNC_MANAGER_ID);
}
}
}
}
public void run() {
mStop = false;
// If we're really debugging, turn on all logging
if (Eas.DEBUG) {
Eas.USER_LOG = true;
Eas.PARSER_LOG = true;
Eas.FILE_LOG = true;
}
// If we need to wait for the debugger, do so
if (Eas.WAIT_DEBUG) {
Debug.waitForDebugger();
}
// 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
mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
mSyncStatusObserver);
mAccountsUpdatedListener = new EasAccountsUpdatedListener();
AccountManager.get(getApplication())
.addOnAccountsUpdatedListener(mAccountsUpdatedListener, mHandler, true);
// Set up receivers for ConnectivityManager
mConnectivityReceiver = new ConnectivityReceiver();
registerReceiver(mConnectivityReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
mBackgroundDataSettingReceiver = new ConnectivityReceiver();
registerReceiver(mBackgroundDataSettingReceiver,
new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
// Save away background data setting; we'll keep track of it with the receiver
ConnectivityManager cm =
(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
mBackgroundData = cm.getBackgroundDataSetting();
// See if any settings have changed while we weren't running...
checkPIMSyncSettings();
try {
while (!mStop) {
runAwake(SYNC_MANAGER_ID);
waitForConnectivity();
mNextWaitReason = "Heartbeat";
long nextWait = checkMailboxes();
try {
synchronized (this) {
if (!mKicked) {
if (nextWait < 0) {
log("Negative wait? Setting to 1s");
nextWait = 1*SECONDS;
}
if (nextWait > 10*SECONDS) {
log("Next awake in " + nextWait / 1000 + "s: " + mNextWaitReason);
runAsleep(SYNC_MANAGER_ID, nextWait + (3*SECONDS));
}
wait(nextWait);
}
}
} catch (InterruptedException e) {
// Needs to be caught, but causes no problem
} finally {
synchronized (this) {
if (mKicked) {
//log("Wait deferred due to kick");
mKicked = false;
}
}
}
}
log("Shutdown requested");
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException in SyncManager", e);
throw e;
} finally {
log("Finishing SyncManager");
// Lots of cleanup here
// Stop our running syncs
stopServiceThreads();
// Stop receivers and content observers
if (mConnectivityReceiver != null) {
unregisterReceiver(mConnectivityReceiver);
}
if (mBackgroundDataSettingReceiver != null) {
unregisterReceiver(mBackgroundDataSettingReceiver);
}
if (INSTANCE != null) {
ContentResolver resolver = getContentResolver();
resolver.unregisterContentObserver(mAccountObserver);
resolver.unregisterContentObserver(mMailboxObserver);
resolver.unregisterContentObserver(mSyncedMessageObserver);
resolver.unregisterContentObserver(mMessageObserver);
unregisterCalendarObservers();
}
// Don't leak the Intent associated with this listener
if (mAccountsUpdatedListener != null) {
AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
mAccountsUpdatedListener = null;
}
// Clear pending alarms and associated Intents
clearAlarms();
// Release our wake lock, if we have one
synchronized (mWakeLocks) {
if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
}
}
log("Goodbye");
}
if (!mStop) {
// If this wasn't intentional, try to restart the service
throw new RuntimeException("EAS SyncManager crash; please restart me...");
}
}
private void releaseMailbox(long mailboxId) {
mServiceMap.remove(mailboxId);
releaseWakeLock(mailboxId);
}
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_MANAGER_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.getSyncableEasMailboxWhere(), null, null);
// Contacts/Calendar obey this setting from ContentResolver
// Mail is on its own schedule
boolean masterAutoSync = ContentResolver.getMasterSyncAutomatically();
try {
while (c.moveToNext()) {
long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
AbstractSyncService service = null;
synchronized (sSyncLock) {
service = mServiceMap.get(mid);
}
if (service == null) {
// We handle a few types of mailboxes specially
int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
// If background data is off, we only sync Outbox
// Manual syncs are initiated elsewhere, so they will continue to be respected
if (!mBackgroundData && type != Mailbox.TYPE_OUTBOX) {
continue;
}
// Check whether we're in a hold (temporary or permanent)
SyncError syncError = mSyncErrorMap.get(mid);
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;
}
}
if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
// We don't sync these automatically if master auto sync is off
if (!masterAutoSync) {
continue;
}
// Get the right authority for the mailbox
String authority;
Account account =
getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
if (account != null) {
if (type == Mailbox.TYPE_CONTACTS) {
authority = ContactsContract.AUTHORITY;
} else {
authority = Calendar.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);
}
}
android.accounts.Account a =
new android.accounts.Account(account.mEmailAddress,
Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
// See if "sync automatically" is set; if not, punt
if (!ContentResolver.getSyncAutomatically(a, authority)) {
continue;
}
}
} else if (type == Mailbox.TYPE_TRASH) {
continue;
}
// Otherwise, we use the sync interval
long interval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
if (interval == Mailbox.CHECK_INTERVAL_PUSH) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
requestSync(m, SYNC_PUSH, null);
} else if (type == Mailbox.TYPE_OUTBOX) {
int cnt = EmailContent.count(this, Message.CONTENT_URI,
EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
new String[] {Long.toString(mid)});
if (cnt > 0) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
startServiceThread(new EasOutboxService(this, m), m);
}
} else if (interval > 0 && interval <= ONE_DAY_MINUTES) {
long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
long sinceLastSync = now - lastSync;
if (sinceLastSync < 0) {
log("WHOA! lastSync in the future for mailbox: " + mid);
sinceLastSync = interval*MINUTES;
}
long toNextSync = interval*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 (Eas.USER_LOG) {
log("Next sync for " + name + " in " + nextWait/1000 + "s");
}
mNextWaitReason = "Scheduled sync, " + name;
} else if (Eas.USER_LOG) {
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 (Eas.USER_LOG) {
log("Dead thread, mailbox released: " +
c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
}
releaseMailbox(mid);
// Restart this if necessary
if (nextWait > 3*SECONDS) {
nextWait = 3*SECONDS;
mNextWaitReason = "Clean up dead thread(s)";
}
} else {
long requestTime = service.mRequestTime;
if (requestTime > 0) {
long timeToRequest = requestTime - now;
if (service instanceof AbstractSyncService && 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);
}
static public void serviceRequest(long mailboxId, long ms, int reason) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
// Never allow manual start of Drafts or Outbox via serviceRequest
if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
log("Ignoring serviceRequest for drafts/outbox/null mailbox");
return;
}
try {
AbstractSyncService service = syncManager.mServiceMap.get(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 syncManager = INSTANCE;
if (syncManager == null) return;
AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
if (service != null) {
service.mRequestTime = System.currentTimeMillis();
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
if (m != null) {
service.mAccount = Account.restoreAccountWithId(syncManager, m.mAccountKey);
service.mMailbox = m;
kick("service request immediate");
}
}
}
static public void sendMessageRequest(Request req) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
Message msg = Message.restoreMessageWithId(syncManager, req.mMessageId);
if (msg == null) {
return;
}
long mailboxId = msg.mMailboxKey;
AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
if (service == null) {
service = 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 syncManager = INSTANCE;
if (syncManager == null) return PING_STATUS_OK;
// Already syncing...
if (syncManager.mServiceMap.get(mailboxId) != null) {
return PING_STATUS_RUNNING;
}
// No errors or a transient error, don't ping...
SyncError error = syncManager.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 AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return null;
synchronized (sSyncLock) {
if (syncManager.mServiceMap.get(mailboxId) == null) {
syncManager.mSyncErrorMap.remove(mailboxId);
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
if (m != null) {
log("Starting sync for " + m.mDisplayName);
syncManager.requestSync(m, reason, req);
}
}
}
return syncManager.mServiceMap.get(mailboxId);
}
// DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
static private void stopManualSync(long mailboxId) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
synchronized (sSyncLock) {
AbstractSyncService svc = syncManager.mServiceMap.get(mailboxId);
if (svc != null) {
log("Stopping sync for " + svc.mMailboxName);
svc.stop();
svc.mThread.interrupt();
syncManager.releaseWakeLock(mailboxId);
}
}
}
/**
* Wake up SyncManager to check for mailboxes needing service
*/
static public void kick(String reason) {
SyncManager syncManager = INSTANCE;
if (syncManager != null) {
synchronized (syncManager) {
//INSTANCE.log("Kick: " + reason);
syncManager.mKicked = true;
syncManager.notify();
}
}
if (sConnectivityLock != null) {
synchronized (sConnectivityLock) {
sConnectivityLock.notify();
}
}
}
static public void accountUpdated(long acctId) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
synchronized (sSyncLock) {
for (AbstractSyncService svc : syncManager.mServiceMap.values()) {
if (svc.mAccount.mId == acctId) {
svc.mAccount = Account.restoreAccountWithId(syncManager, acctId);
}
}
}
}
/**
* Tell SyncManager 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 syncManager = INSTANCE;
if (syncManager == null) return;
synchronized(sSyncLock) {
syncManager.mSyncErrorMap.remove(mailboxId);
}
}
/**
* 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 syncManager = INSTANCE;
if (syncManager == null) return;
synchronized(sSyncLock) {
long mailboxId = svc.mMailboxId;
HashMap<Long, SyncError> errorMap = syncManager.mSyncErrorMap;
SyncError syncError = errorMap.get(mailboxId);
syncManager.releaseMailbox(mailboxId);
int exitStatus = svc.mExitStatus;
switch (exitStatus) {
case AbstractSyncService.EXIT_DONE:
if (!svc.mRequests.isEmpty()) {
// TODO Handle this case
}
errorMap.remove(mailboxId);
break;
// I/O errors get retried at increasing intervals
case AbstractSyncService.EXIT_IO_ERROR:
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
if (m == null) return;
if (syncError != null) {
syncError.escalate();
log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
} else {
errorMap.put(mailboxId, syncManager.new SyncError(exitStatus, false));
log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
}
break;
// These errors are not retried automatically
case AbstractSyncService.EXIT_SECURITY_FAILURE:
case AbstractSyncService.EXIT_LOGIN_FAILURE:
case AbstractSyncService.EXIT_EXCEPTION:
errorMap.put(mailboxId, syncManager.new SyncError(exitStatus, true));
break;
}
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;
}
}