blob: e41e683f23f5c42c3f0fe3e617398a414854aca5 [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.mail.MessagingException;
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.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.MessageColumns;
import com.android.email.provider.EmailContent.SyncColumns;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
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.util.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
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 {
public static final String TAG = "EAS SyncManager";
public static final int DEFAULT_WINDOW = Integer.MIN_VALUE;
public static final int SECS = 1000;
public static final int MINS = 60 * SECS;
public static final int SYNC_UPSYNC = 0;
public static final int SYNC_SCHEDULED = 1;
public static final int SYNC_PUSH = 2;
public static final int SYNC_PING = 3;
public static final int SYNC_SERVICE_START_SYNC = 4;
public static final int SYNC_SERVICE_PART_REQUEST = 5;
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 (" + Account.CHECK_INTERVAL_PING + ',' + Account.CHECK_INTERVAL_PUSH + ')';
// 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;
static SyncManager INSTANCE;
static Object mSyncToken = new Object();
static Thread mServiceThread = null;
HashMap<Long, AbstractSyncService> mServiceMap = new HashMap<Long, AbstractSyncService>();
HashMap<Long, SyncError> mSyncErrorMap = new HashMap<Long, SyncError>();
boolean mStop = false;
SharedPreferences mSettings;
Handler mHandler = new Handler();
AccountObserver mAccountObserver;
MailboxObserver mMailboxObserver;
SyncedMessageObserver mSyncedMessageObserver;
MessageObserver mMessageObserver;
String mNextWaitReason;
IEmailServiceCallback mCallback;
ConnectivityReceiver mConnectivityReceiver = null;
String mDeviceId = null;
RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
private HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
static private HashMap<Long, PendingIntent> mPendingIntents =
new HashMap<Long, PendingIntent>();
private WakeLock mWakeLock = null;
/**
* Proxy that can be used by various sync adapters to call 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, int statusCode, int progress)
throws RemoteException{
IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
if (cb != null) {
cb.sendMessageStatus(messageId, 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 the binder for EmailService implementation here. These are the calls that are
* defined in AbstractSyncService. Only validate is now implemented; loadAttachment currently
* spins its wheels counting up to 100%.
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
public int validate(String protocol, String host, String userName, String password,
int port, boolean ssl) throws RemoteException {
try {
AbstractSyncService.validate(EasSyncService.class, host, userName, password, port,
ssl, SyncManager.this);
return MessagingException.NO_ERROR;
} catch (MessagingException e) {
return e.getExceptionType();
}
}
public void startSync(long mailboxId) throws RemoteException {
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);
partRequest(new PartRequest(att, destinationFile, contentUriString));
}
public void updateFolderList(long accountId) throws RemoteException {
reloadFolderList(SyncManager.this, accountId, false);
}
public void setLogging(boolean on) throws RemoteException {
Eas.setUserDebug(on);
}
public void loadMore(long messageId) throws RemoteException {
// TODO Auto-generated method stub
}
// 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);
}
};
class AccountList extends ArrayList<Account> {
private static final long serialVersionUID = 1L;
public boolean contains(long id) {
for (Account account: this) {
if (account.mId == id) {
return true;
}
}
return false;
}
}
class AccountObserver extends ContentObserver {
// mAccounts keeps track of Accounts that we care about (EAS for now)
AccountList mAccounts = new AccountList();
public AccountObserver(Handler handler) {
super(handler);
Context context = getContext();
// At startup, we want to see what EAS accounts exist and cache them
Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
null, null, null);
try {
collectEasAccounts(c, mAccounts);
} finally {
c.close();
}
for (Account account: mAccounts) {
int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" + account.mId,
null);
if (cnt == 0) {
addAccountMailbox(account.mId);
}
}
}
private boolean accountChanged(Account account) {
long accountId = account.mId;
// Reload account from database to get its current state
account = Account.restoreAccountWithId(getContext(), accountId);
for (Account oldAccount: mAccounts) {
if (oldAccount.mId == accountId) {
return (oldAccount.mSyncInterval != account.mSyncInterval ||
oldAccount.mSyncLookback != account.mSyncLookback);
}
}
// Really, we can't get here, but we don't want the compiler to complain
return false;
}
@Override
public void onChange(boolean selfChange) {
// A change to the list requires us to scan for deletions (to stop running syncs)
// At startup, we want to see what accounts exist and cache them
AccountList currentAccounts = new AccountList();
Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
null, null, null);
try {
collectEasAccounts(c, currentAccounts);
for (Account account : mAccounts) {
if (!currentAccounts.contains(account.mId)) {
// This is a deletion; shut down any account-related syncs
stopAccountSyncs(account.mId, true);
} else {
// See whether any of our accounts has changed sync interval or window
if (accountChanged(account)) {
// Here's one that has...
INSTANCE.log("Account " + account.mDisplayName +
" changed; stopping running syncs...");
stopAccountSyncs(account.mId, false);
}
}
}
for (Account account: currentAccounts) {
if (!mAccounts.contains(account.mId)) {
// This is an addition; create our magic hidden mailbox...
addAccountMailbox(account.mId);
mAccounts.add(account);
}
}
} finally {
c.close();
}
// See if there's anything to do...
kick("account changed");
}
void collectEasAccounts(Cursor c, ArrayList<Account> accounts) {
Context context = getContext();
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")) {
accounts.add(new Account().restore(c));
}
}
}
}
void addAccountMailbox(long acctId) {
Account acct = Account.restoreAccountWithId(getContext(), acctId);
Mailbox main = new Mailbox();
main.mDisplayName = Eas.ACCOUNT_MAILBOX;
main.mServerId = Eas.ACCOUNT_MAILBOX + System.nanoTime();
main.mAccountKey = acct.mId;
main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
main.mSyncInterval = Account.CHECK_INTERVAL_PUSH;
main.mFlagVisible = false;
main.save(getContext());
INSTANCE.log("Initializing account: " + acct.mDisplayName);
}
void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
synchronized (mSyncToken) {
List<Long> deletedBoxes = new ArrayList<Long>();
for (Long mid : INSTANCE.mServiceMap.keySet()) {
Mailbox box = Mailbox.restoreMailboxWithId(INSTANCE, mid);
if (box != null) {
if (box.mAccountKey == acctId) {
if (!includeAccountMailbox &&
box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
continue;
}
AbstractSyncService svc = INSTANCE.mServiceMap.get(mid);
if (svc != null) {
svc.stop();
svc.mThread.interrupt();
}
deletedBoxes.add(mid);
}
}
}
for (Long mid : deletedBoxes) {
releaseMailbox(mid);
}
}
}
}
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");
}
}
}
class SyncedMessageObserver extends ContentObserver {
long maxChangedId = 0;
long maxDeletedId = 0;
Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
PendingIntent syncAlarmPendingIntent =
PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY, SyncColumns.DATA};
public SyncedMessageObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
INSTANCE.log("SyncedMessage changed: (re)setting alarm for 10s");
alarmManager.set(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + (10*SECS), syncAlarmPendingIntent);
}
}
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 AccountList getAccountList() {
if (INSTANCE != null) {
return INSTANCE.mAccountObserver.mAccounts;
} else {
return null;
}
}
public class SyncStatus {
static public final int NOT_RUNNING = 0;
static public final int DIED = 1;
static public final int SYNC = 2;
static public final int IDLE = 3;
}
class SyncError {
int reason;
boolean fatal = false;
long holdEndTime;
long holdDelay = 0;
SyncError(int _reason, boolean _fatal) {
reason = _reason;
fatal = _fatal;
escalate();
}
/**
* We increase the hold on I/O errors in 30 second increments to 5 minutes
*/
void escalate() {
if (holdDelay < 5*MINS) {
holdDelay += 30*SECS;
}
holdEndTime = System.currentTimeMillis() + holdDelay;
}
}
@Override
public IBinder onBind(Intent arg0) {
return mBinder;
}
public void log(String str) {
if (Eas.USER_LOG) {
Log.d(TAG, 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 synchronized String getDeviceId() throws IOException {
if (INSTANCE == null) {
throw new IOException();
}
// If we've already got the id, return it
if (INSTANCE.mDeviceId != null) {
return INSTANCE.mDeviceId;
}
// Otherwise, we'll read the id file or create one if it's not found
try {
File f = INSTANCE.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 = "droid" + System.currentTimeMillis();
w.write(id);
w.close();
return id;
}
} catch (FileNotFoundException e) {
// We'll just use the default below
Log.e(TAG, "Can't get device name");
} catch (IOException e) {
// We'll just use the default below
Log.e(TAG, "Can't get device name");
}
throw new IOException();
}
@Override
public void onCreate() {
if (INSTANCE != null) {
Log.d(TAG, "onCreate called on running SyncManager");
return;
}
INSTANCE = this;
mAccountObserver = new AccountObserver(mHandler);
mMailboxObserver = new MailboxObserver(mHandler);
mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
mMessageObserver = new MessageObserver(mHandler);
// Start our thread...
if (mServiceThread == null || !mServiceThread.isAlive()) {
log(mServiceThread == null ? "Starting thread..." : "Restarting thread...");
mServiceThread = new Thread(this, "SyncManager");
mServiceThread.start();
} else {
log("Attempt to start SyncManager though already started before?");
}
mDeviceId = android.provider.Settings.Secure.getString(getContentResolver(),
android.provider.Settings.Secure.ANDROID_ID);
}
@Override
public void onDestroy() {
log("!!! MaiLService onDestroy");
if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
}
if (mConnectivityReceiver != null) {
unregisterReceiver(mConnectivityReceiver);
}
clearAlarms();
mWakeLocks.clear();
mPendingIntents.clear();
INSTANCE = null;
}
static public void reloadFolderList(Context context, long accountId, boolean force) {
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(mSyncToken) {
Mailbox m = new Mailbox().restore(c);
String syncKey = m.mSyncKey;
// No need to reload the list if we don't have one
if (!force && (syncKey == null || syncKey.equals("0"))) {
return;
}
// Change all ping/push boxes to push/hold
ContentValues cv = new ContentValues();
cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH_HOLD);
context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
new String[] {Long.toString(accountId)});
INSTANCE.log("Set push/ping boxes to push/hold");
long id = m.mId;
AbstractSyncService svc = INSTANCE.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
INSTANCE.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) {
if (INSTANCE != null) {
AccountObserver obs = INSTANCE.mAccountObserver;
obs.stopAccountSyncs(acctId, false);
//obs.addAccountMailbox(acctId);
}
}
private void logLocks(String str) {
StringBuilder sb = new StringBuilder(str);
boolean first = true;
for (long id: mWakeLocks.keySet()) {
if (!first) {
sb.append(", ");
} else {
first = false;
}
sb.append(id);
}
log(sb.toString());
}
private void acquireWakeLock(long id) {
synchronized (mWakeLocks) {
Boolean lock = mWakeLocks.get(id);
if (lock == null) {
if (id > 0) {
INSTANCE.log("+WakeLock requested for " + alarmOwner(id));
}
if (mWakeLock == null) {
PowerManager pm = (PowerManager)INSTANCE
.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
mWakeLock.acquire();
INSTANCE.log("+WAKE LOCK ACQUIRED");
}
mWakeLocks.put(id, true);
logLocks("Post-acquire of WakeLock for " + alarmOwner(id) + ": ");
}
}
}
private void releaseWakeLock(long id) {
synchronized (mWakeLocks) {
Boolean lock = mWakeLocks.get(id);
if (lock != null) {
if (id > 0) {
INSTANCE.log("+WakeLock not needed for " + alarmOwner(id));
}
mWakeLocks.remove(id);
if (mWakeLocks.isEmpty()) {
if (mWakeLock != null) {
mWakeLock.release();
}
mWakeLock = null;
INSTANCE.log("+WAKE LOCK RELEASED");
} else {
logLocks("Post-release of WakeLock for " + alarmOwner(id) + ": ");
}
}
}
}
static public String alarmOwner(long id) {
if (id == -1) {
return "SyncManager";
} else
return "Mailbox " + Long.toString(id);
}
static private void clearAlarm(long id) {
synchronized (mPendingIntents) {
PendingIntent pi = mPendingIntents.get(id);
if (pi != null) {
AlarmManager alarmManager = (AlarmManager)INSTANCE
.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pi);
INSTANCE.log("+Alarm cleared for " + alarmOwner(id));
mPendingIntents.remove(id);
}
}
}
static private void setAlarm(long id, long millis) {
synchronized (mPendingIntents) {
PendingIntent pi = mPendingIntents.get(id);
if (pi == null) {
Intent i = new Intent(INSTANCE, MailboxAlarmReceiver.class);
i.putExtra("mailbox", id);
i.setData(Uri.parse("Box" + id));
pi = PendingIntent.getBroadcast(INSTANCE, 0, i, 0);
mPendingIntents.put(id, pi);
AlarmManager alarmManager = (AlarmManager)INSTANCE
.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
INSTANCE.log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
}
}
}
static private void clearAlarms() {
AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
synchronized (mPendingIntents) {
for (PendingIntent pi : mPendingIntents.values()) {
alarmManager.cancel(pi);
}
mPendingIntents.clear();
}
}
static public void runAwake(long id) {
INSTANCE.acquireWakeLock(id);
clearAlarm(id);
}
static public void runAsleep(long id, long millis) {
setAlarm(id, millis);
INSTANCE.releaseWakeLock(id);
}
static public void ping(long id) {
if (id < 0) {
kick("ping SyncManager");
} else {
AbstractSyncService service = INSTANCE.mServiceMap.get(id);
if (service != null) {
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
if (m != null) {
service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
service.mMailbox = m;
service.ping();
}
}
}
}
public class ConnectivityReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Bundle b = intent.getExtras();
if (b != null) {
NetworkInfo a = (NetworkInfo)b.get("networkInfo");
String info = "CM Info: " + a.getTypeName();
State state = a.getState();
if (state == State.CONNECTED) {
info += " CONNECTED";
kick("connected");
} else if (state == State.CONNECTING) {
info += " CONNECTING";
} else if (state == State.DISCONNECTED) {
info += " DISCONNECTED";
kick("disconnected");
} else if (state == State.DISCONNECTING) {
info += " DISCONNECTING";
} else if (state == State.SUSPENDED) {
info += " SUSPENDED";
} else if (state == State.UNKNOWN) {
info += " UNKNOWN";
}
log("CONNECTIVITY: " + info);
}
}
}
private void pause(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
}
}
private void startService(AbstractSyncService service, Mailbox m) {
synchronized (mSyncToken) {
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);
}
}
private void startService(Mailbox m, int reason, PartRequest req) {
synchronized (mSyncToken) {
Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
if (acct != null) {
AbstractSyncService service;
service = new EasSyncService(this, m);
service.mSyncReason = reason;
if (req != null) {
service.addPartRequest(req);
}
startService(service, m);
}
}
}
private void stopServices() {
synchronized (mSyncToken) {
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);
log("Shutting down " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
svc.stop();
svc.mThread.interrupt();
releaseWakeLock(mailboxId);
}
}
}
public void run() {
mStop = false;
// If we're really debugging, turn on all logging
if (Eas.DEBUG) {
Eas.USER_LOG = true;
}
if (Eas.WAIT_DEBUG) {
Debug.waitForDebugger();
}
runAwake(-1);
// 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
ContentResolver resolver = getContentResolver();
resolver.registerContentObserver(Account.CONTENT_URI, false, mAccountObserver);
resolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
resolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
resolver.registerContentObserver(Message.CONTENT_URI, false, mMessageObserver);
mConnectivityReceiver = new ConnectivityReceiver();
registerReceiver(mConnectivityReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
ConnectivityManager cm =
(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
try {
while (!mStop) {
runAwake(-1);
log("Looking for something to do...");
int cnt = 0;
while (!mStop) {
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
break;
} else {
if (cnt++ == 2) {
stopServices();
}
pause(10*SECS);
}
}
if (!mStop) {
mNextWaitReason = "Heartbeat";
long nextWait = checkMailboxes();
try {
synchronized (INSTANCE) {
if (nextWait < 0) {
log("Negative wait? Setting to 1s");
nextWait = 1*SECS;
}
if (nextWait > (30*SECS)) {
runAsleep(-1, nextWait - 1000);
}
log("Next awake in " + (nextWait / 1000) + "s: " + mNextWaitReason);
INSTANCE.wait(nextWait);
}
} catch (InterruptedException e) {
// Needs to be caught, but causes no problem
}
} else {
stopServices();
log("Shutdown requested");
return;
}
}
} finally {
log("Goodbye");
}
startService(new Intent(this, SyncManager.class));
throw new RuntimeException("MailService 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 (mSyncToken) {
for (long mailboxId: mServiceMap.keySet()) {
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, 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) {
boolean alive = svc.mThread.isAlive();
log("Deleted mailbox: " + svc.mMailboxName);
if (alive) {
stopManualSync(mailboxId);
} else {
log("Removing from serviceMap");
releaseMailbox(mailboxId);
}
}
}
long nextWait = 10*MINS;
long now = System.currentTimeMillis();
// Start up threads that need it...
Cursor c = getContentResolver().query(Mailbox.CONTENT_URI,
Mailbox.CONTENT_PROJECTION, null, null, null);
try {
while (c.moveToNext()) {
// TODO Could be much faster - just get cursor of
// ones we're watching...
long aid = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN);
// Only check mailboxes for EAS accounts
if (!mAccountObserver.mAccounts.contains(aid)) {
continue;
}
long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
AbstractSyncService service = mServiceMap.get(mid);
if (service == null) {
// Check whether we're in a hold (temporary or permanent)
SyncError syncError = mSyncErrorMap.get(mid);
if (syncError != null && (syncError.fatal || now < syncError.holdEndTime)) {
if (!syncError.fatal) {
if (syncError.holdEndTime < (now + nextWait)) {
nextWait = syncError.holdEndTime - now;
mNextWaitReason = "Release hold";
}
}
continue;
}
long freq = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
if (freq == Account.CHECK_INTERVAL_PUSH) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
startService(m, SYNC_PUSH, null);
} else if (c.getInt(Mailbox.CONTENT_TYPE_COLUMN) == Mailbox.TYPE_OUTBOX) {
int cnt = EmailContent.count(this, Message.CONTENT_URI,
"mailboxKey=" + mid + " and syncServerId=0", null);
if (cnt > 0) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
startService(new EasOutboxService(this, m), m);
}
} else if (freq > 0 && freq <= 1440) {
long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
if (now - lastSync > (freq*MINS)) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
startService(m, SYNC_SCHEDULED, null);
}
}
} else {
Thread thread = service.mThread;
if (thread != null && !thread.isAlive()) {
releaseMailbox(mid);
// Restart this if necessary
if (nextWait > 3*SECS) {
nextWait = 3*SECS;
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.ping();
} else if (requestTime > 0 && timeToRequest < nextWait) {
if (timeToRequest < 11*MINS) {
nextWait = timeToRequest < 250 ? 250 : timeToRequest;
mNextWaitReason = "Sync data change";
} else {
log("Illegal timeToRequest: " + timeToRequest);
}
}
}
}
}
}
} finally {
c.close();
}
return nextWait;
}
static public void serviceRequest(Mailbox m, int reason) {
serviceRequest(m.mId, 5*SECS, reason);
}
static public void serviceRequest(long mailboxId, int reason) {
serviceRequest(mailboxId, 5*SECS, reason);
}
static public void serviceRequest(long mailboxId, long ms, int reason) {
try {
if (INSTANCE == null) {
return;
}
AbstractSyncService service = INSTANCE.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) {
AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
if (service != null) {
service.mRequestTime = System.currentTimeMillis();
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
if (m != null) {
service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
service.mMailbox = m;
kick("service request immediate");
}
}
}
static public void partRequest(PartRequest req) {
Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
if (msg == null) {
return;
}
long mailboxId = msg.mMailboxKey;
AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
if (service == null) {
service = startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
kick("part request");
} else {
service.addPartRequest(req);
}
}
static public PartRequest hasPartRequest(long emailId, String part) {
Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
if (msg == null) {
return null;
}
long mailboxId = msg.mMailboxKey;
AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
if (service != null) {
return service.hasPartRequest(emailId, part);
}
return null;
}
static public void cancelPartRequest(long emailId, String part) {
Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
if (msg == null) {
return;
}
long mailboxId = msg.mMailboxKey;
AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
if (service != null) {
service.cancelPartRequest(emailId, part);
}
}
/**
* 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 boolean canSync(long mailboxId) {
// Already syncing...
if (INSTANCE.mServiceMap.get(mailboxId) != null) {
return false;
}
// Blocked from syncing (transient or permanent)
if (INSTANCE.mSyncErrorMap.get(mailboxId) != null) {
return false;
}
return true;
}
static public int getSyncStatus(long mailboxId) {
synchronized (mSyncToken) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) {
return SyncStatus.NOT_RUNNING;
}
AbstractSyncService svc = INSTANCE.mServiceMap.get(mailboxId);
if (svc == null) {
return SyncStatus.NOT_RUNNING;
} else {
if (!svc.mThread.isAlive()) {
return SyncStatus.DIED;
} else {
return svc.getSyncStatus();
}
}
}
}
static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) {
return null;
}
INSTANCE.log("startManualSync");
synchronized (mSyncToken) {
if (INSTANCE.mServiceMap.get(mailboxId) == null) {
INSTANCE.mSyncErrorMap.remove(mailboxId);
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
INSTANCE.log("Starting sync for " + m.mDisplayName);
INSTANCE.startService(m, reason, req);
}
}
return INSTANCE.mServiceMap.get(mailboxId);
}
// DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
static public void stopManualSync(long mailboxId) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) {
return;
}
synchronized (mSyncToken) {
AbstractSyncService svc = INSTANCE.mServiceMap.get(mailboxId);
if (svc != null) {
INSTANCE.log("Stopping sync for " + svc.mMailboxName);
svc.stop();
svc.mThread.interrupt();
INSTANCE.releaseWakeLock(mailboxId);
}
}
}
/**
* Wake up SyncManager to check for mailboxes needing service
*/
static public void kick(String reason) {
if (INSTANCE != null) {
//if (reason != null) {
// INSTANCE.log("Kick: " + reason);
//}
synchronized (INSTANCE) {
INSTANCE.notify();
}
}
}
static public void kick(long mailboxId) {
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
int syncType = m.mSyncInterval;
if (syncType == Account.CHECK_INTERVAL_PUSH) {
SyncManager.serviceRequestImmediate(mailboxId);
} else {
SyncManager.startManualSync(mailboxId, SYNC_KICK, null);
}
}
static public void accountUpdated(long acctId) {
synchronized (mSyncToken) {
for (AbstractSyncService svc : INSTANCE.mServiceMap.values()) {
if (svc.mAccount.mId == acctId) {
svc.mAccount = Account.restoreAccountWithId(INSTANCE, acctId);
}
}
}
}
/**
* 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) {
synchronized(mSyncToken) {
long mailboxId = svc.mMailboxId;
HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
SyncError syncError = errorMap.get(mailboxId);
INSTANCE.releaseMailbox(mailboxId);
int exitStatus = svc.mExitStatus;
switch (exitStatus) {
case AbstractSyncService.EXIT_DONE:
if (!svc.mPartRequests.isEmpty()) {
// TODO Handle this case
}
errorMap.remove(mailboxId);
break;
case AbstractSyncService.EXIT_IO_ERROR:
if (syncError != null) {
syncError.escalate();
} else {
errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
}
kick("i/o error in sync");
break;
case AbstractSyncService.EXIT_LOGIN_FAILURE:
case AbstractSyncService.EXIT_EXCEPTION:
errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
break;
}
}
}
public static void shutdown() {
INSTANCE.mStop = true;
kick("shutdown");
INSTANCE.stopSelf();
}
static public String serviceName(long id) {
if (id < 0) {
return "SyncManager";
} else {
AbstractSyncService service = INSTANCE.mServiceMap.get(id);
if (service != null) {
return service.mThread.getName();
} else {
return "Not running?";
}
}
}
/**
* 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() {
if (INSTANCE == null) {
return null;
}
return INSTANCE;
}
}