From 062d2e0deb9135f0f17bc319513f59d4620a48fb Mon Sep 17 00:00:00 2001 From: M66B Date: Fri, 6 Dec 2019 20:58:56 +0100 Subject: [PATCH] Individual account management proof of concept --- .../java/eu/faircode/email/DaoAccount.java | 4 +- .../eu/faircode/email/ServiceSynchronize.java | 185 ++++++++++++++---- .../email/TupleAccountNetworkState.java | 32 ++- .../eu/faircode/email/TupleAccountState.java | 24 ++- 4 files changed, 205 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/DaoAccount.java b/app/src/main/java/eu/faircode/email/DaoAccount.java index 109dfb7ba0..4bae682455 100644 --- a/app/src/main/java/eu/faircode/email/DaoAccount.java +++ b/app/src/main/java/eu/faircode/email/DaoAccount.java @@ -71,10 +71,10 @@ public interface DaoAccount { @Query("SELECT account.*" + ", SUM(folder.synchronize) AS folders" + - ", COUNT(operation.id) AS operations" + + ", (SELECT COUNT(id) FROM operation" + + " WHERE operation.account = account.id AND operation.name <> '" + EntityOperation.SEND + "') AS operations" + " FROM account" + " LEFT JOIN folder ON folder.account = account.id" + - " LEFT JOIN operation ON operation.folder = folder.id" + " GROUP BY account.id" + " ORDER BY account.id") LiveData> liveAccountState(); diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 2894a4302c..511a5f6890 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -51,11 +51,13 @@ import com.sun.mail.imap.IMAPFolder; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Random; @@ -84,7 +86,7 @@ import me.leolin.shortcutbadger.ShortcutBadger; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; -public class ServiceSynchronize extends ServiceBase { +public class ServiceSynchronize extends ServiceBase implements SharedPreferences.OnSharedPreferenceChangeListener { private ConnectionHelper.NetworkState networkState = new ConnectionHelper.NetworkState(); private Core.State state = null; private int lastStartId = -1; @@ -120,21 +122,25 @@ public class ServiceSynchronize extends ServiceBase { private ConnectionHelper.NetworkState lastNetworkState = null; private List lastAccountStates = null; + private void post(boolean reload) { + post(reload, lastNetworkState, lastAccountStates); + } + private void post(ConnectionHelper.NetworkState networkState) { lastNetworkState = networkState; - post(lastNetworkState, lastAccountStates); + post(false, lastNetworkState, lastAccountStates); } private void post(List accountStates) { lastAccountStates = accountStates; - post(lastNetworkState, lastAccountStates); + post(false, lastNetworkState, lastAccountStates); } - private void post(ConnectionHelper.NetworkState networkState, List accountStates) { + private void post(boolean reload, ConnectionHelper.NetworkState networkState, List accountStates) { if (networkState != null && accountStates != null) { List result = new ArrayList<>(); for (TupleAccountState accountState : accountStates) - result.add(new TupleAccountNetworkState(networkState, accountState)); + result.add(new TupleAccountNetworkState(reload, networkState, accountState)); postValue(result); } } @@ -162,12 +168,13 @@ public class ServiceSynchronize extends ServiceBase { DB db = DB.getInstance(this); - db.account().liveAccountState().observe(this, new Observer>() { - @Override - public void onChanged(List accountStates) { - liveAccountState.postValue(accountStates); - } - }); + if (BuildConfig.DEBUG) + db.account().liveAccountState().observe(this, new Observer>() { + @Override + public void onChanged(List accountStates) { + liveAccountState.postValue(accountStates); + } + }); liveAccountNetworkState.addSource(liveNetworkState, new Observer() { @Override @@ -184,9 +191,96 @@ public class ServiceSynchronize extends ServiceBase { }); liveAccountNetworkState.observe(this, new Observer>() { + private List accountStates = new ArrayList<>(); + private Map serviceStates = new Hashtable<>(); + @Override public void onChanged(List accountNetworkStates) { - Log.i("Account network states=" + accountNetworkStates.size()); + boolean running = false; + + for (TupleAccountNetworkState current : accountNetworkStates) { + if (current.accountState.shouldRun()) + running = true; + + int index = accountStates.indexOf(current); + if (index < 0) { + if (current.shouldRun()) + start(current); + } else { + TupleAccountNetworkState prev = accountStates.get(index); + accountStates.remove(index); + + if (current.reload || + !prev.accountState.equals(current.accountState) || + prev.shouldRun() != current.shouldRun()) { + Log.i("XXX account=" + current + + " reload=" + current.reload + " change=" + !prev.accountState.equals(current.accountState)); + if (prev.shouldRun()) + stop(prev); + if (current.shouldRun()) + start(prev); + } + } + + accountStates.add(current); + } + + if (!running) + stopSelf(); + } + + private void start(final TupleAccountNetworkState accountNetworkState) { + queue.submit(new Runnable() { + @Override + public void run() { + long ago = new Date().getTime() - lastLost; + if (ago < RECONNECT_BACKOFF) + try { + long backoff = RECONNECT_BACKOFF - ago; + EntityLog.log(ServiceSynchronize.this, accountNetworkState + " backoff=" + (backoff / 1000)); + Thread.sleep(backoff); + } catch (InterruptedException ex) { + Log.w(accountNetworkState + " backoff " + ex.toString()); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (accountNetworkState.accountState.notify) + accountNetworkState.accountState.createNotificationChannel(ServiceSynchronize.this); + else + accountNetworkState.accountState.deleteNotificationChannel(ServiceSynchronize.this); + } + + Log.i(accountNetworkState.accountState.host + "/" + accountNetworkState.accountState.user + " run"); + final Core.State astate = new Core.State(state); + astate.runnable(new Runnable() { + @Override + public void run() { + try { + monitorAccount(accountNetworkState.accountState, astate, sync); + } catch (Throwable ex) { + Log.e(accountNetworkState.accountState.name, ex); + } + } + }, "sync.account." + accountNetworkState.accountState.id); + astate.start(); + Log.i("XXX started=" + accountNetworkState); + serviceStates.put(accountNetworkState, astate); + } + }); + } + + private void stop(final TupleAccountNetworkState accountNetworkState) { + final Core.State state = serviceStates.get(accountNetworkState); + serviceStates.remove(accountNetworkState); + queue.submit(new Runnable() { + @Override + public void run() { + Log.i("XXX stop=" + accountNetworkState); + state.stop(); + state.join(); + Log.i("XXX stopped=" + accountNetworkState); + } + }); } }); @@ -329,12 +423,30 @@ public class ServiceSynchronize extends ServiceBase { last = current; } }); + + prefs.registerOnSharedPreferenceChangeListener(this); + } + + private static final List POST_EVAL = + Collections.unmodifiableList(Arrays.asList("metered", "roaming", "rlah")); + private static final List POST_RELOAD = + Collections.unmodifiableList(Arrays.asList("socks_enabled", "socks_proxy", "subscribed_only", "debug")); + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if (POST_EVAL.contains(key)) + liveAccountNetworkState.post(true); + else if (POST_RELOAD.contains(key)) + liveAccountNetworkState.post(true); } @Override public void onDestroy() { EntityLog.log(this, "Service destroy"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + unregisterReceiver(onScreenOff); unregisterReceiver(connectionChangedReceiver); @@ -661,29 +773,31 @@ public class ServiceSynchronize extends ServiceBase { } // Start monitoring accounts - List accounts = db.account().getSynchronizingAccounts(); - for (final EntityAccount account : accounts) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (account.notify) - account.createNotificationChannel(ServiceSynchronize.this); - else - account.deleteNotificationChannel(ServiceSynchronize.this); - } + if (!BuildConfig.DEBUG) { + List accounts = db.account().getSynchronizingAccounts(); + for (final EntityAccount account : accounts) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (account.notify) + account.createNotificationChannel(ServiceSynchronize.this); + else + account.deleteNotificationChannel(ServiceSynchronize.this); + } - Log.i(account.host + "/" + account.user + " run"); - final Core.State astate = new Core.State(state); - astate.runnable(new Runnable() { - @Override - public void run() { - try { - monitorAccount(account, astate, sync); - } catch (Throwable ex) { - Log.e(account.name, ex); + Log.i(account.host + "/" + account.user + " run"); + final Core.State astate = new Core.State(state); + astate.runnable(new Runnable() { + @Override + public void run() { + try { + monitorAccount(account, astate, sync); + } catch (Throwable ex) { + Log.e(account.name, ex); + } } - } - }, "sync.account." + account.id); - astate.start(); - state.childs.add(astate); + }, "sync.account." + account.id); + astate.start(); + state.childs.add(astate); + } } EntityLog.log(ServiceSynchronize.this, "Main started"); @@ -964,6 +1078,8 @@ public class ServiceSynchronize extends ServiceBase { if (ex.getCause() instanceof BadCommandException) throw ex; + if ("connection failure".equals(ex.getMessage())) + throw ex; db.folder().setFolderError(folder.id, Log.formatThrowable(ex)); continue; @@ -1475,7 +1591,8 @@ public class ServiceSynchronize extends ServiceBase { private void updateState() { ConnectionHelper.NetworkState ns = ConnectionHelper.getNetworkState(ServiceSynchronize.this); - liveNetworkState.postValue(ns); + if (BuildConfig.DEBUG) + liveNetworkState.postValue(ns); networkState.update(ns); if (lastSuitable == null || lastSuitable != networkState.isSuitable()) { diff --git a/app/src/main/java/eu/faircode/email/TupleAccountNetworkState.java b/app/src/main/java/eu/faircode/email/TupleAccountNetworkState.java index e1f14bae9c..2bed4b5c44 100644 --- a/app/src/main/java/eu/faircode/email/TupleAccountNetworkState.java +++ b/app/src/main/java/eu/faircode/email/TupleAccountNetworkState.java @@ -19,13 +19,43 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public class TupleAccountNetworkState { + public boolean reload; public ConnectionHelper.NetworkState networkState; public TupleAccountState accountState; - public TupleAccountNetworkState(ConnectionHelper.NetworkState networkState, TupleAccountState accountState) { + public TupleAccountNetworkState(boolean reload, ConnectionHelper.NetworkState networkState, TupleAccountState accountState) { + this.reload = reload; this.networkState = networkState; this.accountState = accountState; } + + public boolean shouldRun() { + return (this.networkState.isSuitable() && + this.accountState.shouldRun()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof TupleAccountNetworkState) { + TupleAccountNetworkState other = (TupleAccountNetworkState) obj; + return this.accountState.id.equals(other.accountState.id); + } else + return false; + } + + @Override + public int hashCode() { + return accountState.id.hashCode(); + } + + @NonNull + @Override + public String toString() { + return accountState.name; + } } diff --git a/app/src/main/java/eu/faircode/email/TupleAccountState.java b/app/src/main/java/eu/faircode/email/TupleAccountState.java index 9ad9a06fc9..8d8e0a8c67 100644 --- a/app/src/main/java/eu/faircode/email/TupleAccountState.java +++ b/app/src/main/java/eu/faircode/email/TupleAccountState.java @@ -19,7 +19,10 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ +import java.util.Objects; + public class TupleAccountState extends EntityAccount { + // TODO: folder property changes (name, synchronize, poll) public int folders; public int operations; @@ -27,10 +30,25 @@ public class TupleAccountState extends EntityAccount { public boolean equals(Object obj) { if (obj instanceof TupleAccountState) { TupleAccountState other = (TupleAccountState) obj; - return (super.equals(obj) && // TODO selected attributes - this.folders == other.folders && - this.operations == other.operations); + return (this.host.equals(other.host) && + this.starttls == other.starttls && + this.insecure == other.insecure && + this.port.equals(other.port) && + // auth_type + this.user.equals(other.user) && + this.password.equals(other.password) && + Objects.equals(this.realm, other.realm) && + this.notify == other.notify && + this.poll_interval.equals(other.poll_interval) && + this.partial_fetch == other.partial_fetch && + this.ignore_size == other.ignore_size && + this.use_date == other.use_date && + this.folders == other.folders); } else return false; } + + boolean shouldRun() { + return (synchronize && (folders > 0 || operations > 0)); + } }