From 90444cd65e37637019266c4d8d71e302568df41a Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 2 Mar 2019 07:15:49 +0000 Subject: [PATCH] Revert "Single sync service" This reverts commit c0983d24f596120d2f8ebe818ed06056f01bcc62. --- .../java/eu/faircode/email/ActivityMain.java | 2 +- app/src/main/java/eu/faircode/email/Core.java | 2 +- .../java/eu/faircode/email/DaoAccount.java | 9 +- .../java/eu/faircode/email/DaoOperation.java | 6 - .../eu/faircode/email/EntityOperation.java | 11 +- .../eu/faircode/email/FragmentFolders.java | 6 +- .../eu/faircode/email/FragmentMessages.java | 2 +- .../eu/faircode/email/ServiceSynchronize.java | 85 +++++---- .../java/eu/faircode/email/ServiceUI.java | 177 +++++++++++++++++- 9 files changed, 238 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/ActivityMain.java b/app/src/main/java/eu/faircode/email/ActivityMain.java index 0e5ddaa0cf..b1593834ad 100644 --- a/app/src/main/java/eu/faircode/email/ActivityMain.java +++ b/app/src/main/java/eu/faircode/email/ActivityMain.java @@ -49,7 +49,7 @@ public class ActivityMain extends AppCompatActivity implements FragmentManager.O new SimpleTask>() { @Override protected List onExecute(Context context, Bundle args) { - return DB.getInstance(context).account().getSynchronizingAccounts(true); + return DB.getInstance(context).account().getSynchronizingAccounts(); } @Override diff --git a/app/src/main/java/eu/faircode/email/Core.java b/app/src/main/java/eu/faircode/email/Core.java index 636aaa1034..40976f0dff 100644 --- a/app/src/main/java/eu/faircode/email/Core.java +++ b/app/src/main/java/eu/faircode/email/Core.java @@ -1269,7 +1269,7 @@ class Core { if (update) db.message().updateMessage(message); - else if (false && BuildConfig.DEBUG) + else if (BuildConfig.DEBUG) Log.i(folder.name + " unchanged uid=" + uid); } diff --git a/app/src/main/java/eu/faircode/email/DaoAccount.java b/app/src/main/java/eu/faircode/email/DaoAccount.java index f30a262e64..15df8e51fc 100644 --- a/app/src/main/java/eu/faircode/email/DaoAccount.java +++ b/app/src/main/java/eu/faircode/email/DaoAccount.java @@ -32,13 +32,8 @@ public interface DaoAccount { @Query("SELECT * FROM account") List getAccounts(); - @Query("SELECT account.* FROM account" + - " LEFT JOIN folder ON folder.account = account.id" + // not outbox - " LEFT JOIN operation ON operation.folder = folder.id" + - " WHERE account.synchronize" + - " GROUP BY account.id" + - " HAVING :all OR COUNT(operation.id) > 0") - List getSynchronizingAccounts(boolean all); + @Query("SELECT * FROM account WHERE synchronize") + List getSynchronizingAccounts(); @Query("SELECT * FROM account WHERE tbd = 1") List getAccountsTbd(); diff --git a/app/src/main/java/eu/faircode/email/DaoOperation.java b/app/src/main/java/eu/faircode/email/DaoOperation.java index 79403fd470..786db60752 100644 --- a/app/src/main/java/eu/faircode/email/DaoOperation.java +++ b/app/src/main/java/eu/faircode/email/DaoOperation.java @@ -85,12 +85,6 @@ public interface DaoOperation { " AND message = :message") int getOperationCount(long folder, long message); - @Query("SELECT COUNT(operation.id) FROM operation" + - " JOIN folder ON folder.id = operation.folder" + - " JOIN account ON account.id = folder.account" + // not outbox - " WHERE account.synchronize") - LiveData livePendingOperationsCount(); - @Query("UPDATE operation SET error = :error WHERE id = :id") int setOperationError(long id, String error); diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index b1321055d5..882d9ff770 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -123,8 +123,8 @@ public class EntityOperation { if (account == null) // Outbox ServiceSend.start(context); - else - ServiceSynchronize.start(context); + else if (!"connected".equals(account.state)) + ServiceUI.process(context, fid); Log.i("Queued sync folder=" + folder); } @@ -253,8 +253,11 @@ public class EntityOperation { if (SEND.equals(name)) ServiceSend.start(context); - else - ServiceSynchronize.start(context); + else { + EntityAccount account = db.account().getAccount(message.account); + if (account != null && !"connected".equals(account.state)) + ServiceUI.process(context, operation.folder); + } } @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentFolders.java b/app/src/main/java/eu/faircode/email/FragmentFolders.java index ce50a97b9c..f3eddf40a6 100644 --- a/app/src/main/java/eu/faircode/email/FragmentFolders.java +++ b/app/src/main/java/eu/faircode/email/FragmentFolders.java @@ -302,7 +302,11 @@ public class FragmentFolders extends FragmentBase { EntityOperation.sync(context, folder.id); } else { // Folder list - ServiceSynchronize.reload(getContext(), "refresh folders"); + EntityAccount account = db.account().getAccount(aid); + if (account != null && !"connected".equals(account.state)) + ServiceUI.fsync(context, aid); + else + ServiceSynchronize.reload(getContext(), "refresh folders"); } db.setTransactionSuccessful(); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index a700184ce5..5ee1364f96 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -973,7 +973,7 @@ public class FragmentMessages extends FragmentBase { if (result.hasTrash == null) result.hasTrash = false; if (result.hasJunk == null) result.hasJunk = false; - result.accounts = db.account().getSynchronizingAccounts(true); + result.accounts = db.account().getSynchronizingAccounts(); final Collator collator = Collator.getInstance(Locale.getDefault()); collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index a7ec51f14f..650d0a5e10 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -97,6 +97,7 @@ public class ServiceSynchronize extends LifecycleService { private static final long RECONNECT_BACKOFF = 90 * 1000L; // milliseconds private static final int ACCOUNT_ERROR_AFTER = 90; // minutes private static final int BACKOFF_ERROR_AFTER = 16; // seconds + private static final long STOP_DELAY = 5000L; // milliseconds static final int PI_ALARM = 1; @@ -130,16 +131,6 @@ public class ServiceSynchronize extends LifecycleService { } }); - db.operation().livePendingOperationsCount().observe(ServiceSynchronize.this, new Observer() { - @Override - public void onChanged(Integer ops) { - Log.i("Pending ops=" + ops); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); - boolean enabled = prefs.getBoolean("enabled", true); - serviceManager.quit(!enabled && ops == 0); - } - }); - JobDaily.schedule(this); } @@ -150,6 +141,8 @@ public class ServiceSynchronize extends LifecycleService { ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); cm.unregisterNetworkCallback(serviceManager); + serviceManager.service_destroy(); + Widget.update(this, -1); JobDaily.cancel(this); @@ -175,8 +168,8 @@ public class ServiceSynchronize extends LifecycleService { if (action != null) try { switch (action) { - case "start": - serviceManager.service_start(); + case "init": + serviceManager.service_init(); break; case "alarm": @@ -245,7 +238,6 @@ public class ServiceSynchronize extends LifecycleService { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE); //System.setProperty("mail.socket.debug", Boolean.toString(debug)); - boolean enabled = prefs.getBoolean("enabled", true); // Create session Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure); @@ -589,8 +581,7 @@ public class ServiceSynchronize extends LifecycleService { idler.start(); idlers.add(idler); - if (enabled) - EntityOperation.sync(this, folder.id); + EntityOperation.sync(this, folder.id); } else folders.put(folder, null); @@ -877,7 +868,6 @@ public class ServiceSynchronize extends LifecycleService { private Core.State state; private boolean started = false; private int queued = 0; - private boolean quit = false; private long lastLost = 0; private ExecutorService queue = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); @@ -937,11 +927,17 @@ public class ServiceSynchronize extends LifecycleService { EntityLog.log(ServiceSynchronize.this, "suitable=" + suitable + " metered=" + metered + " isMetered=" + isMetered); + // The connected state is deliberately ignored return suitable; } - private void service_start() { - EntityLog.log(ServiceSynchronize.this, "Service start"); + private boolean isEnabled() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); + return prefs.getBoolean("enabled", true); + } + + private void service_init() { + EntityLog.log(ServiceSynchronize.this, "Service init"); // Network events will manage the service } @@ -960,6 +956,14 @@ public class ServiceSynchronize extends LifecycleService { } } + private void service_destroy() { + synchronized (this) { + EntityLog.log(ServiceSynchronize.this, "Service destroy"); + if (started) + queue_reload(false, "service destroy"); + } + } + private void start() { EntityLog.log(ServiceSynchronize.this, "Main start"); @@ -975,9 +979,6 @@ public class ServiceSynchronize extends LifecycleService { try { wl.acquire(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); - boolean enabled = prefs.getBoolean("enabled", true); - final DB db = DB.getInstance(ServiceSynchronize.this); long ago = new Date().getTime() - lastLost; @@ -992,7 +993,7 @@ public class ServiceSynchronize extends LifecycleService { } // Start monitoring accounts - List accounts = db.account().getSynchronizingAccounts(enabled); + List accounts = db.account().getSynchronizingAccounts(); for (final EntityAccount account : accounts) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) if (account.notify) @@ -1034,7 +1035,6 @@ public class ServiceSynchronize extends LifecycleService { astate.stop(); for (Core.State astate : threadState) astate.join(); - threadState.clear(); EntityLog.log(ServiceSynchronize.this, "Main exited"); @@ -1063,7 +1063,7 @@ public class ServiceSynchronize extends LifecycleService { private void queue_reload(final boolean start, final String reason) { final boolean doStop = started; - final boolean doStart = (start && suitableNetwork()); + final boolean doStart = (start && isEnabled() && suitableNetwork()); EntityLog.log(ServiceSynchronize.this, "Queue reload" + " doStop=" + doStop + " doStart=" + doStart + " queued=" + queued + " " + reason); @@ -1108,19 +1108,23 @@ public class ServiceSynchronize extends LifecycleService { } finally { queued--; EntityLog.log(ServiceSynchronize.this, "Reload done queued=" + queued); - if (queued == 0 && quit) - stopSelf(); + + if (queued == 0 && !isEnabled()) { + try { + Thread.sleep(STOP_DELAY); + } catch (InterruptedException ignored) { + } + if (queued == 0 && !isEnabled()) { + EntityLog.log(ServiceSynchronize.this, "Service stop"); + stopSelf(); + } + } + wl.release(); } } }); } - - private void quit(boolean quit) { - this.quit = quit; - if (quit && queued == 0) - queue_reload(false, "quit"); - } } private static void schedule(Context context) { @@ -1194,9 +1198,16 @@ public class ServiceSynchronize extends LifecycleService { // Restore schedule schedule(context); - // Start service - start(context); + // Conditionally init service + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean enabled = prefs.getBoolean("enabled", true); + + int accounts = db.account().getSynchronizingAccounts().size(); + if (enabled && accounts > 0) + ContextCompat.startForegroundService(context, + new Intent(context, ServiceSynchronize.class) + .setAction("init")); } catch (Throwable ex) { Log.e(ex); } @@ -1206,12 +1217,6 @@ public class ServiceSynchronize extends LifecycleService { } } - static void start(Context context) { - ContextCompat.startForegroundService(context, - new Intent(context, ServiceSynchronize.class) - .setAction("start")); - } - static void reschedule(Context context) { ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class) diff --git a/app/src/main/java/eu/faircode/email/ServiceUI.java b/app/src/main/java/eu/faircode/email/ServiceUI.java index 6b3205954b..d9a8d15796 100644 --- a/app/src/main/java/eu/faircode/email/ServiceUI.java +++ b/app/src/main/java/eu/faircode/email/ServiceUI.java @@ -9,10 +9,21 @@ import android.net.Uri; import android.os.PowerManager; import android.preference.PreferenceManager; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; + import androidx.annotation.Nullable; public class ServiceUI extends IntentService { private PowerManager.WakeLock wl; + private Map accountStore = new HashMap<>(); static final int PI_WHY = 1; static final int PI_SUMMARY = 2; @@ -43,7 +54,31 @@ public class ServiceUI extends IntentService { @Override public void onDestroy() { Log.i("Service UI destroy"); - wl.release(); + + final DB db = DB.getInstance(this); + + new Thread(new Runnable() { + @Override + public void run() { + try { + for (EntityAccount account : accountStore.keySet()) + try { + Log.i(account.name + " closing"); + db.account().setAccountState(account.id, "closing"); + accountStore.get(account).close(); + } catch (Throwable ex) { + Log.w(ex); + } finally { + Log.i(account.name + " closed"); + db.account().setAccountState(account.id, null); + } + accountStore.clear(); + } finally { + wl.release(); + } + } + }).start(); + super.onDestroy(); } @@ -95,6 +130,14 @@ public class ServiceUI extends IntentService { onSnooze(id); break; + case "process": + onProcessOperations(id); + break; + + case "fsync": + onFolderSync(id); + break; + default: Log.w("Unknown action: " + parts[0]); } @@ -224,4 +267,136 @@ public class ServiceUI extends IntentService { db.endTransaction(); } } + + private void onProcessOperations(long fid) { + DB db = DB.getInstance(this); + + EntityFolder folder = db.folder().getFolder(fid); + if (folder == null) + return; + EntityAccount account = db.account().getAccount(folder.account); + if (account == null) + return; + + Folder ifolder = null; + try { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE); + + // Create session + Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure); + final Session isession = Session.getInstance(props, null); + isession.setDebug(debug); + + Store istore = accountStore.get(account.id); + if (istore == null || !istore.isConnected()) { + // Connect account + Log.i(account.name + " connecting"); + db.account().setAccountState(account.id, "connecting"); + istore = isession.getStore(account.getProtocol()); + Helper.connect(this, istore, account); + db.account().setAccountState(account.id, "connected"); + db.account().setAccountConnected(account.id, new Date().getTime()); + db.account().setAccountError(account.id, null); + Log.i(account.name + " connected"); + + accountStore.put(account, istore); + } else + Log.i(account + " reusing connection"); + + // Connect folder + Log.i(folder.name + " connecting"); + db.folder().setFolderState(folder.id, "connecting"); + ifolder = istore.getFolder(folder.name); + ifolder.open(Folder.READ_WRITE); + db.folder().setFolderState(folder.id, "connected"); + db.folder().setFolderError(folder.id, null); + Log.i(folder.name + " connected"); + + // Process operations + Core.processOperations(this, account, folder, isession, istore, ifolder, new Core.State()); + + } catch (Throwable ex) { + Log.w(ex); + Core.reportError(this, account, folder, ex); + db.account().setAccountError(account.id, Helper.formatThrowable(ex)); + db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, false)); + } finally { + if (ifolder != null) + try { + Log.i(folder.name + " closing"); + db.folder().setFolderState(folder.id, "closing"); + ifolder.close(); + } catch (MessagingException ex) { + Log.w(ex); + } finally { + Log.i(folder.name + " closed"); + db.folder().setFolderState(folder.id, null); + db.folder().setFolderSyncState(folder.id, null); + } + } + } + + private void onFolderSync(long aid) { + DB db = DB.getInstance(this); + EntityAccount account = db.account().getAccount(aid); + if (account == null) + return; + + Store istore = null; + try { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE); + + // Create session + Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure); + final Session isession = Session.getInstance(props, null); + isession.setDebug(debug); + + // Connect account + Log.i(account.name + " connecting"); + db.account().setAccountState(account.id, "connecting"); + istore = isession.getStore(account.getProtocol()); + Helper.connect(this, istore, account); + db.account().setAccountState(account.id, "connected"); + db.account().setAccountConnected(account.id, new Date().getTime()); + db.account().setAccountError(account.id, null); + Log.i(account.name + " connected"); + + // Synchronize folders + Core.onSynchronizeFolders(this, account, istore, new Core.State()); + + } catch (Throwable ex) { + Log.w(ex); + Core.reportError(this, account, null, ex); + db.account().setAccountError(account.id, Helper.formatThrowable(ex)); + } finally { + if (istore != null) { + Log.i(account.name + " closing"); + db.account().setAccountState(account.id, "closing"); + + try { + istore.close(); + } catch (MessagingException ex) { + Log.e(ex); + } + + Log.i(account.name + " closed"); + } + + db.account().setAccountState(account.id, null); + } + } + + public static void process(Context context, long folder) { + context.startService( + new Intent(context, ServiceUI.class) + .setAction("process:" + folder)); + } + + public static void fsync(Context context, long account) { + context.startService( + new Intent(context, ServiceUI.class) + .setAction("fsync:" + account)); + } }