Single sync service

This commit is contained in:
M66B 2019-03-01 19:35:30 +00:00
parent 67ec1908aa
commit c0983d24f5
9 changed files with 62 additions and 238 deletions

View File

@ -49,7 +49,7 @@ public class ActivityMain extends AppCompatActivity implements FragmentManager.O
new SimpleTask<List<EntityAccount>>() {
@Override
protected List<EntityAccount> onExecute(Context context, Bundle args) {
return DB.getInstance(context).account().getSynchronizingAccounts();
return DB.getInstance(context).account().getSynchronizingAccounts(true);
}
@Override

View File

@ -1269,7 +1269,7 @@ class Core {
if (update)
db.message().updateMessage(message);
else if (BuildConfig.DEBUG)
else if (false && BuildConfig.DEBUG)
Log.i(folder.name + " unchanged uid=" + uid);
}

View File

@ -32,8 +32,13 @@ public interface DaoAccount {
@Query("SELECT * FROM account")
List<EntityAccount> getAccounts();
@Query("SELECT * FROM account WHERE synchronize")
List<EntityAccount> getSynchronizingAccounts();
@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<EntityAccount> getSynchronizingAccounts(boolean all);
@Query("SELECT * FROM account WHERE tbd = 1")
List<EntityAccount> getAccountsTbd();

View File

@ -85,6 +85,12 @@ 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<Integer> livePendingOperationsCount();
@Query("UPDATE operation SET error = :error WHERE id = :id")
int setOperationError(long id, String error);

View File

@ -123,8 +123,8 @@ public class EntityOperation {
if (account == null) // Outbox
ServiceSend.start(context);
else if (!"connected".equals(account.state))
ServiceUI.process(context, fid);
else
ServiceSynchronize.start(context);
Log.i("Queued sync folder=" + folder);
}
@ -253,11 +253,8 @@ public class EntityOperation {
if (SEND.equals(name))
ServiceSend.start(context);
else {
EntityAccount account = db.account().getAccount(message.account);
if (account != null && !"connected".equals(account.state))
ServiceUI.process(context, operation.folder);
}
else
ServiceSynchronize.start(context);
}
@Override

View File

@ -302,11 +302,7 @@ public class FragmentFolders extends FragmentBase {
EntityOperation.sync(context, folder.id);
} else {
// Folder list
EntityAccount account = db.account().getAccount(aid);
if (account != null && !"connected".equals(account.state))
ServiceUI.fsync(context, aid);
else
ServiceSynchronize.reload(getContext(), "refresh folders");
ServiceSynchronize.reload(getContext(), "refresh folders");
}
db.setTransactionSuccessful();

View File

@ -966,7 +966,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();
result.accounts = db.account().getSynchronizingAccounts(true);
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc

View File

@ -97,7 +97,6 @@ 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;
@ -131,6 +130,16 @@ public class ServiceSynchronize extends LifecycleService {
}
});
db.operation().livePendingOperationsCount().observe(ServiceSynchronize.this, new Observer<Integer>() {
@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);
}
@ -141,8 +150,6 @@ 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);
@ -168,8 +175,8 @@ public class ServiceSynchronize extends LifecycleService {
if (action != null)
try {
switch (action) {
case "init":
serviceManager.service_init();
case "start":
serviceManager.service_start();
break;
case "alarm":
@ -238,6 +245,7 @@ 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);
@ -581,7 +589,8 @@ public class ServiceSynchronize extends LifecycleService {
idler.start();
idlers.add(idler);
EntityOperation.sync(this, folder.id);
if (enabled)
EntityOperation.sync(this, folder.id);
} else
folders.put(folder, null);
@ -868,6 +877,7 @@ 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);
@ -927,17 +937,11 @@ public class ServiceSynchronize extends LifecycleService {
EntityLog.log(ServiceSynchronize.this,
"suitable=" + suitable + " metered=" + metered + " isMetered=" + isMetered);
// The connected state is deliberately ignored
return suitable;
}
private boolean isEnabled() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this);
return prefs.getBoolean("enabled", true);
}
private void service_init() {
EntityLog.log(ServiceSynchronize.this, "Service init");
private void service_start() {
EntityLog.log(ServiceSynchronize.this, "Service start");
// Network events will manage the service
}
@ -956,14 +960,6 @@ 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");
@ -979,6 +975,9 @@ 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;
@ -993,7 +992,7 @@ public class ServiceSynchronize extends LifecycleService {
}
// Start monitoring accounts
List<EntityAccount> accounts = db.account().getSynchronizingAccounts();
List<EntityAccount> accounts = db.account().getSynchronizingAccounts(enabled);
for (final EntityAccount account : accounts) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)
if (account.notify)
@ -1035,6 +1034,7 @@ 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 && isEnabled() && suitableNetwork());
final boolean doStart = (start && suitableNetwork());
EntityLog.log(ServiceSynchronize.this, "Queue reload" +
" doStop=" + doStop + " doStart=" + doStart + " queued=" + queued + " " + reason);
@ -1108,23 +1108,19 @@ public class ServiceSynchronize extends LifecycleService {
} finally {
queued--;
EntityLog.log(ServiceSynchronize.this, "Reload done queued=" + queued);
if (queued == 0 && !isEnabled()) {
try {
Thread.sleep(STOP_DELAY);
} catch (InterruptedException ignored) {
}
if (queued == 0 && !isEnabled()) {
EntityLog.log(ServiceSynchronize.this, "Service stop");
stopSelf();
}
}
if (queued == 0 && quit)
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) {
@ -1198,16 +1194,9 @@ public class ServiceSynchronize extends LifecycleService {
// Restore schedule
schedule(context);
// Conditionally init service
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean enabled = prefs.getBoolean("enabled", true);
// Start service
start(context);
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);
}
@ -1217,6 +1206,12 @@ 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)

View File

@ -9,21 +9,10 @@ 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<EntityAccount, Store> accountStore = new HashMap<>();
static final int PI_WHY = 1;
static final int PI_SUMMARY = 2;
@ -54,31 +43,7 @@ public class ServiceUI extends IntentService {
@Override
public void onDestroy() {
Log.i("Service UI destroy");
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();
wl.release();
super.onDestroy();
}
@ -130,14 +95,6 @@ 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]);
}
@ -267,136 +224,4 @@ 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));
}
}