package com.fsck.k9.service; import java.util.Collection; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.SystemClock; import com.fsck.k9.Account; import com.fsck.k9.Account.FolderMode; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Pusher; import com.fsck.k9.preferences.Storage; import com.fsck.k9.preferences.StorageEditor; import timber.log.Timber; public class MailService extends CoreService { private static final String ACTION_CHECK_MAIL = "com.fsck.k9.intent.action.MAIL_SERVICE_WAKEUP"; private static final String ACTION_RESET = "com.fsck.k9.intent.action.MAIL_SERVICE_RESET"; private static final String ACTION_RESCHEDULE_POLL = "com.fsck.k9.intent.action.MAIL_SERVICE_RESCHEDULE_POLL"; private static final String ACTION_CANCEL = "com.fsck.k9.intent.action.MAIL_SERVICE_CANCEL"; private static final String ACTION_REFRESH_PUSHERS = "com.fsck.k9.intent.action.MAIL_SERVICE_REFRESH_PUSHERS"; private static final String ACTION_RESTART_PUSHERS = "com.fsck.k9.intent.action.MAIL_SERVICE_RESTART_PUSHERS"; private static final String CONNECTIVITY_CHANGE = "com.fsck.k9.intent.action.MAIL_SERVICE_CONNECTIVITY_CHANGE"; private static final String CANCEL_CONNECTIVITY_NOTICE = "com.fsck.k9.intent.action.MAIL_SERVICE_CANCEL_CONNECTIVITY_NOTICE"; private static long nextCheck = -1; private static boolean pushingRequested = false; private static boolean pollingRequested = false; private static boolean syncNoBackground = false; private static boolean syncNoConnectivity = false; private static boolean syncBlocked = false; public static void actionReset(Context context, Integer wakeLockId) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_RESET); addWakeLockId(context, i, wakeLockId, true); context.startService(i); } public static void actionRestartPushers(Context context, Integer wakeLockId) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_RESTART_PUSHERS); addWakeLockId(context, i, wakeLockId, true); context.startService(i); } public static void actionReschedulePoll(Context context, Integer wakeLockId) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_RESCHEDULE_POLL); addWakeLockId(context, i, wakeLockId, true); context.startService(i); } public static void actionCancel(Context context, Integer wakeLockId) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_CANCEL); addWakeLockId(context, i, wakeLockId, false); // CK:Q: why should we not create a wake lock if one is not already existing like for example in actionReschedulePoll? context.startService(i); } public static void connectivityChange(Context context, Integer wakeLockId) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.CONNECTIVITY_CHANGE); addWakeLockId(context, i, wakeLockId, false); // CK:Q: why should we not create a wake lock if one is not already existing like for example in actionReschedulePoll? context.startService(i); } @Override public void onCreate() { super.onCreate(); Timber.v("***** MailService *****: onCreate"); } @Override public int startService(Intent intent, int startId) { long startTime = SystemClock.elapsedRealtime(); boolean oldIsSyncDisabled = isSyncDisabled(); boolean doBackground = true; final boolean hasConnectivity = Utility.hasConnectivity(getApplication()); boolean autoSync = ContentResolver.getMasterSyncAutomatically(); K9.BACKGROUND_OPS bOps = K9.getBackgroundOps(); switch (bOps) { case NEVER: doBackground = false; break; case ALWAYS: doBackground = true; break; case WHEN_CHECKED_AUTO_SYNC: doBackground = autoSync; break; } syncNoBackground = !doBackground; syncNoConnectivity = !hasConnectivity; syncBlocked = !(doBackground && hasConnectivity); Timber.i("MailService.onStart(%s, %d), hasConnectivity = %s, doBackground = %s", intent, startId, hasConnectivity, doBackground); // MessagingController.getInstance(getApplication()).addListener(mListener); if (ACTION_CHECK_MAIL.equals(intent.getAction())) { Timber.i("***** MailService *****: checking mail"); if (hasConnectivity && doBackground) { PollService.startService(this); } reschedulePollInBackground(hasConnectivity, doBackground, startId, false); } else if (ACTION_CANCEL.equals(intent.getAction())) { Timber.v("***** MailService *****: cancel"); cancel(); } else if (ACTION_RESET.equals(intent.getAction())) { Timber.v("***** MailService *****: reschedule"); rescheduleAllInBackground(hasConnectivity, doBackground, startId); } else if (ACTION_RESTART_PUSHERS.equals(intent.getAction())) { Timber.v("***** MailService *****: restarting pushers"); reschedulePushersInBackground(hasConnectivity, doBackground, startId); } else if (ACTION_RESCHEDULE_POLL.equals(intent.getAction())) { Timber.v("***** MailService *****: rescheduling poll"); reschedulePollInBackground(hasConnectivity, doBackground, startId, true); } else if (ACTION_REFRESH_PUSHERS.equals(intent.getAction())) { refreshPushersInBackground(hasConnectivity, doBackground, startId); } else if (CONNECTIVITY_CHANGE.equals(intent.getAction())) { rescheduleAllInBackground(hasConnectivity, doBackground, startId); Timber.i("Got connectivity action with hasConnectivity = %s, doBackground = %s", hasConnectivity, doBackground); } else if (CANCEL_CONNECTIVITY_NOTICE.equals(intent.getAction())) { /* do nothing */ } if (isSyncDisabled() != oldIsSyncDisabled) { MessagingController.getInstance(getApplication()).systemStatusChanged(); } Timber.i("MailService.onStart took %d ms", SystemClock.elapsedRealtime() - startTime); return START_NOT_STICKY; } @Override public void onDestroy() { Timber.v("***** MailService *****: onDestroy()"); super.onDestroy(); // MessagingController.getInstance(getApplication()).removeListener(mListener); } private void cancel() { Intent i = new Intent(this, MailService.class); i.setAction(ACTION_CHECK_MAIL); BootReceiver.cancelIntent(this, i); } private final static String PREVIOUS_INTERVAL = "MailService.previousInterval"; private final static String LAST_CHECK_END = "MailService.lastCheckEnd"; public static void saveLastCheckEnd(Context context) { long lastCheckEnd = System.currentTimeMillis(); Timber.i("Saving lastCheckEnd = %tc", lastCheckEnd); Preferences prefs = Preferences.getPreferences(context); Storage storage = prefs.getStorage(); StorageEditor editor = storage.edit(); editor.putLong(LAST_CHECK_END, lastCheckEnd); editor.commit(); } private void rescheduleAllInBackground(final boolean hasConnectivity, final boolean doBackground, Integer startId) { execute(getApplication(), new Runnable() { @Override public void run() { reschedulePoll(hasConnectivity, doBackground, true); reschedulePushers(hasConnectivity, doBackground); } }, K9.MAIL_SERVICE_WAKE_LOCK_TIMEOUT, startId); } private void reschedulePollInBackground(final boolean hasConnectivity, final boolean doBackground, Integer startId, final boolean considerLastCheckEnd) { execute(getApplication(), new Runnable() { public void run() { reschedulePoll(hasConnectivity, doBackground, considerLastCheckEnd); } }, K9.MAIL_SERVICE_WAKE_LOCK_TIMEOUT, startId); } private void reschedulePushersInBackground(final boolean hasConnectivity, final boolean doBackground, Integer startId) { execute(getApplication(), new Runnable() { public void run() { reschedulePushers(hasConnectivity, doBackground); } }, K9.MAIL_SERVICE_WAKE_LOCK_TIMEOUT, startId); } private void refreshPushersInBackground(boolean hasConnectivity, boolean doBackground, Integer startId) { if (hasConnectivity && doBackground) { execute(getApplication(), new Runnable() { public void run() { refreshPushers(); schedulePushers(); } }, K9.MAIL_SERVICE_WAKE_LOCK_TIMEOUT, startId); } } private void reschedulePoll(final boolean hasConnectivity, final boolean doBackground, boolean considerLastCheckEnd) { if (!(hasConnectivity && doBackground)) { Timber.i("No connectivity, canceling check for %s", getApplication().getPackageName()); nextCheck = -1; cancel(); return; } Preferences prefs = Preferences.getPreferences(MailService.this); Storage storage = prefs.getStorage(); int previousInterval = storage.getInt(PREVIOUS_INTERVAL, -1); long lastCheckEnd = storage.getLong(LAST_CHECK_END, -1); long now = System.currentTimeMillis(); if (lastCheckEnd > now) { Timber.i("The database claims that the last time mail was checked was in the future (%tc). To try to get " + "things back to normal, the last check time has been reset to: %tc", lastCheckEnd, now); lastCheckEnd = now; } int shortestInterval = -1; for (Account account : prefs.getAvailableAccounts()) { if (account.getAutomaticCheckIntervalMinutes() != -1 && account.getFolderSyncMode() != FolderMode.NONE && (account.getAutomaticCheckIntervalMinutes() < shortestInterval || shortestInterval == -1)) { shortestInterval = account.getAutomaticCheckIntervalMinutes(); } } StorageEditor editor = storage.edit(); editor.putInt(PREVIOUS_INTERVAL, shortestInterval); editor.commit(); if (shortestInterval == -1) { Timber.i("No next check scheduled for package %s", getApplication().getPackageName()); nextCheck = -1; pollingRequested = false; cancel(); } else { long delay = (shortestInterval * (60 * 1000)); long base = (previousInterval == -1 || lastCheckEnd == -1 || !considerLastCheckEnd ? System.currentTimeMillis() : lastCheckEnd); long nextTime = base + delay; Timber.i("previousInterval = %d, shortestInterval = %d, lastCheckEnd = %tc, considerLastCheckEnd = %b", previousInterval, shortestInterval, lastCheckEnd, considerLastCheckEnd); nextCheck = nextTime; pollingRequested = true; try { Timber.i("Next check for package %s scheduled for %tc", getApplication().getPackageName(), nextTime); } catch (Exception e) { // I once got a NullPointerException deep in new Date(); Timber.e(e, "Exception while logging"); } Intent i = new Intent(this, MailService.class); i.setAction(ACTION_CHECK_MAIL); BootReceiver.scheduleIntent(MailService.this, nextTime, i); } } public static boolean isSyncDisabled() { return syncBlocked || (!pollingRequested && !pushingRequested); } public static boolean hasNoConnectivity() { return syncNoConnectivity; } public static boolean isSyncNoBackground() { return syncNoBackground; } public static boolean isSyncBlocked() { return syncBlocked; } public static boolean isPollAndPushDisabled() { return (!pollingRequested && !pushingRequested); } private void stopPushers() { MessagingController.getInstance(getApplication()).stopAllPushing(); PushService.stopService(MailService.this); } private void reschedulePushers(boolean hasConnectivity, boolean doBackground) { Timber.i("Rescheduling pushers"); stopPushers(); if (!(hasConnectivity && doBackground)) { Timber.i("Not scheduling pushers: connectivity? %s -- doBackground? %s", hasConnectivity, doBackground); return; } setupPushers(); schedulePushers(); } private void setupPushers() { boolean pushing = false; for (Account account : Preferences.getPreferences(MailService.this).getAccounts()) { Timber.i("Setting up pushers for account %s", account.getDescription()); if (account.isEnabled() && account.isAvailable(getApplicationContext())) { pushing |= MessagingController.getInstance(getApplication()).setupPushing(account); } else { //TODO: setupPushing of unavailable accounts when they become available (sd-card inserted) } } if (pushing) { PushService.startService(MailService.this); } pushingRequested = pushing; } private void refreshPushers() { try { long nowTime = System.currentTimeMillis(); Timber.i("Refreshing pushers"); Collection<Pusher> pushers = MessagingController.getInstance(getApplication()).getPushers(); for (Pusher pusher : pushers) { long lastRefresh = pusher.getLastRefresh(); int refreshInterval = pusher.getRefreshInterval(); long sinceLast = nowTime - lastRefresh; if (sinceLast + 10000 > refreshInterval) { // Add 10 seconds to keep pushers in sync, avoid drift Timber.d("PUSHREFRESH: refreshing lastRefresh = %d, interval = %d, nowTime = %d, " + "sinceLast = %d", lastRefresh, refreshInterval, nowTime, sinceLast); pusher.refresh(); pusher.setLastRefresh(nowTime); } else { Timber.d("PUSHREFRESH: NOT refreshing lastRefresh = %d, interval = %d, nowTime = %d, " + "sinceLast = %d", lastRefresh, refreshInterval, nowTime, sinceLast); } } // Whenever we refresh our pushers, send any unsent messages Timber.d("PUSHREFRESH: trying to send mail in all folders!"); MessagingController.getInstance(getApplication()).sendPendingMessages(null); } catch (Exception e) { Timber.e(e, "Exception while refreshing pushers"); } } private void schedulePushers() { int minInterval = -1; Collection<Pusher> pushers = MessagingController.getInstance(getApplication()).getPushers(); for (Pusher pusher : pushers) { int interval = pusher.getRefreshInterval(); if (interval > 0 && (interval < minInterval || minInterval == -1)) { minInterval = interval; } } Timber.v("Pusher refresh interval = %d", minInterval); if (minInterval > 0) { long nextTime = System.currentTimeMillis() + minInterval; Timber.d("Next pusher refresh scheduled for %tc", nextTime); Intent i = new Intent(this, MailService.class); i.setAction(ACTION_REFRESH_PUSHERS); BootReceiver.scheduleIntent(MailService.this, nextTime, i); } } @Override public IBinder onBind(Intent intent) { // Unused return null; } public static long getNextPollTime() { return nextCheck; } }