diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 9a32b3ff83..f8ba787a5e 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -361,13 +361,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } }); - db.message().liveUnseenNotify().observe(this, new Observer>() { - @Override - public void onChanged(List messages) { - Core.notifyMessages(ActivityView.this, messages); - } - }); - if (getSupportFragmentManager().getFragments().size() == 0 && !getIntent().hasExtra(Intent.EXTRA_PROCESS_TEXT)) init(); diff --git a/app/src/main/java/eu/faircode/email/AdapterFolder.java b/app/src/main/java/eu/faircode/email/AdapterFolder.java index ee2ece6fe2..ff218ae82c 100644 --- a/app/src/main/java/eu/faircode/email/AdapterFolder.java +++ b/app/src/main/java/eu/faircode/email/AdapterFolder.java @@ -133,8 +133,7 @@ public class AdapterFolder extends RecyclerView.Adapter FOREGROUND = Arrays.asList(KEYWORD, HEADERS, RAW, BODY, ATTACHMENT); - static void queue(Context context, DB db, EntityMessage message, String name, Object... values) { JSONArray jargs = new JSONArray(); for (Object value : values) @@ -214,46 +209,37 @@ public class EntityOperation { if (SEND.equals(name)) ServiceSend.start(context); - else if (FOREGROUND.contains(name)) { - EntityAccount account = db.account().getAccount(message.account); - if (account != null && !"connected".equals(account.state)) - WorkerOperations.queue(operation.folder); - } + else + ServiceSynchronize.process(context); } static void sync(Context context, long fid, boolean foreground) { DB db = DB.getInstance(context); - if (db.operation().getOperationCount(fid, EntityOperation.SYNC) == 0) { - - EntityFolder folder = db.folder().getFolder(fid); - EntityAccount account = null; - if (folder.account != null) - account = db.account().getAccount(folder.account); - JSONArray jargs = folder.getSyncArgs(); - jargs.put(foreground); + EntityFolder folder = db.folder().getFolder(fid); + EntityAccount account = null; + if (folder.account != null) + account = db.account().getAccount(folder.account); + if (db.operation().getOperationCount(fid, EntityOperation.SYNC) == 0) { EntityOperation operation = new EntityOperation(); operation.folder = folder.id; operation.message = null; operation.name = SYNC; - operation.args = jargs.toString(); + operation.args = folder.getSyncArgs().toString(); operation.created = new Date().getTime(); operation.id = db.operation().insertOperation(operation); - if (account != null && !"connected".equals(account.state)) { - db.folder().setFolderState(fid, "waiting"); - db.folder().setFolderSyncState(fid, "manual"); - } else - db.folder().setFolderSyncState(fid, "requested"); + Log.i("Queued sync folder=" + folder); + } - if (account == null) // Outbox - ServiceSend.start(context); - else if (foreground && !"connected".equals(account.state)) - WorkerOperations.queue(fid); + if (foreground) + db.folder().setFolderSyncState(fid, "requested"); - Log.i("Queued sync folder=" + folder + " foreground=" + foreground); - } + if (account == null) // Outbox + ServiceSend.start(context); + else if (foreground) + ServiceSynchronize.process(context); } @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentFolders.java b/app/src/main/java/eu/faircode/email/FragmentFolders.java index 68536333f0..793baff253 100644 --- a/app/src/main/java/eu/faircode/email/FragmentFolders.java +++ b/app/src/main/java/eu/faircode/email/FragmentFolders.java @@ -302,11 +302,12 @@ public class FragmentFolders extends FragmentBase { EntityOperation.sync(context, folder.id, true); } else { // Folder list - EntityAccount account = db.account().getAccount(aid); - if (account != null && !"connected".equals(account.state)) - WorkerFolderSync.queue(aid); - else + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean enabled = prefs.getBoolean("enabled", true); + if (enabled) ServiceSynchronize.reload(getContext(), "refresh folders"); + else + ServiceSynchronize.process(context); } 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 4822748e1a..5526d502d1 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -1438,8 +1438,6 @@ public class FragmentMessages extends FragmentBase { for (String name : values.keySet()) outState.putLongArray("fair:name:" + name, Helper.toLongArray(values.get(name))); - outState.putBoolean("fair:refreshing", swipeRefresh.isRefreshing()); - if (rvMessage != null) { Parcelable rv = rvMessage.getLayoutManager().onSaveInstanceState(); outState.putParcelable("fair:rv", rv); @@ -1464,8 +1462,6 @@ public class FragmentMessages extends FragmentBase { values.get(name).add(value); } - swipeRefresh.setRefreshing(savedInstanceState.getBoolean("fair:refreshing")); - if (rvMessage != null) { Parcelable rv = savedInstanceState.getBundle("fair:rv"); rvMessage.getLayoutManager().onRestoreInstanceState(rv); @@ -1519,6 +1515,7 @@ public class FragmentMessages extends FragmentBase { public void onChanged(List folders) { if (folders == null) folders = new ArrayList<>(); + Log.i("Folder state updated count=" + folders.size()); int unseen = 0; boolean errors = false; @@ -1536,7 +1533,7 @@ public class FragmentMessages extends FragmentBase { boolean refreshing = false; for (TupleFolderEx folder : folders) - if (folder.isSynchronizing()) { + if (folder.sync_state != null) { refreshing = true; break; } @@ -1559,6 +1556,7 @@ public class FragmentMessages extends FragmentBase { db.folder().liveFolderEx(folder).observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(@Nullable TupleFolderEx folder) { + Log.i("Folder state updated"); if (folder == null) setSubtitle(null); else { @@ -1575,7 +1573,7 @@ public class FragmentMessages extends FragmentBase { } } - boolean refreshing = (folder != null && folder.isSynchronizing()); + boolean refreshing = (folder != null && folder.sync_state != null); if (!refreshing && manual) { manual = false; diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index f97026f1b4..88db295930 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -41,9 +41,6 @@ import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.IMAPMessage; import com.sun.mail.imap.IMAPStore; -import org.json.JSONArray; -import org.json.JSONException; - import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -100,9 +97,11 @@ 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 ONESHOT_DURATION = 60 * 1000L; // milliseconds private static final long STOP_DELAY = 5000L; // milliseconds static final int PI_ALARM = 1; + static final int PI_ONESHOT = 2; @Override public void onCreate() { @@ -184,6 +183,14 @@ public class ServiceSynchronize extends LifecycleService { serviceManager.service_reload(intent.getStringExtra("reason")); break; + case "oneshot_start": + serviceManager.service_oneshot(true); + break; + + case "oneshot_end": + serviceManager.service_oneshot(false); + break; + default: Log.w("Unknown action: " + action); } @@ -924,7 +931,9 @@ public class ServiceSynchronize extends LifecycleService { private boolean isEnabled() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); - return prefs.getBoolean("enabled", true); + boolean enabled = prefs.getBoolean("enabled", true); + boolean oneshot = prefs.getBoolean("oneshot", false); + return (enabled || oneshot); } private void service_init() { @@ -947,6 +956,32 @@ public class ServiceSynchronize extends LifecycleService { } } + private void service_oneshot(boolean start) { + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + + Intent alarm = new Intent(ServiceSynchronize.this, ServiceSynchronize.class); + alarm.setAction("oneshot_end"); + PendingIntent piOneshot; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + piOneshot = PendingIntent.getService(ServiceSynchronize.this, PI_ONESHOT, alarm, PendingIntent.FLAG_UPDATE_CURRENT); + else + piOneshot = PendingIntent.getForegroundService(ServiceSynchronize.this, PI_ONESHOT, alarm, PendingIntent.FLAG_UPDATE_CURRENT); + + am.cancel(piOneshot); + + if (start) { + // Network events will manage the service + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + ONESHOT_DURATION, piOneshot); + else + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + ONESHOT_DURATION, piOneshot); + } else { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); + prefs.edit().putBoolean("oneshot", false).apply(); + queue_reload(false, "oneshot"); + } + } + private void service_destroy() { synchronized (this) { EntityLog.log(ServiceSynchronize.this, "Service destroy"); @@ -1105,21 +1140,8 @@ public class ServiceSynchronize extends LifecycleService { Thread.sleep(STOP_DELAY); } catch (InterruptedException ignored) { } - if (queued == 0 && !isEnabled()) { - EntityLog.log(ServiceSynchronize.this, "Service stop"); - List ops = db.operation().getOperations(EntityOperation.SYNC); - for (EntityOperation op : ops) - try { - JSONArray jargs = new JSONArray(op.args); - if (!jargs.getBoolean(3) /* foreground */) { - Log.i("Deleting bacground SYNC args=" + jargs); - db.operation().deleteOperation(op.id); - } - } catch (JSONException ex) { - Log.e(ex); - } - stopSelf(); - } + if (queued == 0 && !isEnabled()) + stopService(); } wl.release(); @@ -1127,12 +1149,27 @@ public class ServiceSynchronize extends LifecycleService { } }); } + + private void stopService() { + EntityLog.log(ServiceSynchronize.this, "Service stop"); + + DB db = DB.getInstance(ServiceSynchronize.this); + List ops = db.operation().getOperations(EntityOperation.SYNC); + for (EntityOperation op : ops) + db.folder().setFolderSyncState(op.folder, null); + + stopSelf(); + } } private static void schedule(Context context) { Intent alarm = new Intent(context, ServiceSynchronize.class); alarm.setAction("alarm"); - PendingIntent piAlarm = PendingIntent.getService(context, PI_ALARM, alarm, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent piAlarm; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + piAlarm = PendingIntent.getService(context, PI_ALARM, alarm, PendingIntent.FLAG_UPDATE_CURRENT); + else + piAlarm = PendingIntent.getForegroundService(context, PI_ALARM, alarm, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.cancel(piAlarm); @@ -1193,6 +1230,18 @@ public class ServiceSynchronize extends LifecycleService { try { DB db = DB.getInstance(context); + // Reset state + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit().remove("oneshot").apply(); + + for (EntityAccount account : db.account().getAccounts()) + db.account().setAccountState(account.id, null); + + for (EntityFolder folder : db.folder().getFolders()) { + db.folder().setFolderState(folder.id, null); + db.folder().setFolderSyncState(folder.id, null); + } + // Restore snooze timers for (EntityMessage message : db.message().getSnoozed()) EntityMessage.snooze(context, message.id, message.ui_snoozed); @@ -1201,11 +1250,8 @@ public class ServiceSynchronize extends LifecycleService { schedule(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) @@ -1231,4 +1277,16 @@ public class ServiceSynchronize extends LifecycleService { .setAction("reload") .putExtra("reason", reason)); } + + static void process(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean enabled = prefs.getBoolean("enabled", true); + boolean oneshot = prefs.getBoolean("oneshot", false); + if (!enabled && !oneshot) { + prefs.edit().putBoolean("oneshot", true).apply(); + ContextCompat.startForegroundService(context, + new Intent(context, ServiceSynchronize.class) + .setAction("oneshot_start")); + } + } } diff --git a/app/src/main/java/eu/faircode/email/TupleFolderEx.java b/app/src/main/java/eu/faircode/email/TupleFolderEx.java index 5b6207dafd..a201ea2988 100644 --- a/app/src/main/java/eu/faircode/email/TupleFolderEx.java +++ b/app/src/main/java/eu/faircode/email/TupleFolderEx.java @@ -29,13 +29,6 @@ public class TupleFolderEx extends EntityFolder { public int content; public int unseen; - boolean isSynchronizing() { - return (sync_state != null && - (EntityFolder.OUTBOX.equals(type) || - !"requested".equals(sync_state) || - "connected".equals(accountState))); - } - @Override public boolean equals(Object obj) { if (obj instanceof TupleFolderEx) { diff --git a/app/src/main/java/eu/faircode/email/WorkerFolderSync.java b/app/src/main/java/eu/faircode/email/WorkerFolderSync.java deleted file mode 100644 index 4878f67d85..0000000000 --- a/app/src/main/java/eu/faircode/email/WorkerFolderSync.java +++ /dev/null @@ -1,100 +0,0 @@ -package eu.faircode.email; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import java.util.Date; -import java.util.Properties; - -import javax.mail.Session; -import javax.mail.Store; - -import androidx.annotation.NonNull; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -public class WorkerFolderSync extends Worker { - public WorkerFolderSync(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @NonNull - @Override - public Result doWork() { - long aid = getInputData().getLong("account", -1); - Log.i("Work account=" + aid); - - DB db = DB.getInstance(getApplicationContext()); - EntityAccount account = db.account().getAccount(aid); - if (account == null) - return Result.success(); - - Store istore = null; - try { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE); - - String protocol = account.getProtocol(); - - // Get properties - Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure); - props.put("mail." + protocol + ".separatestoreconnection", "true"); - - // Create session - 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(protocol); - Helper.connect(getApplicationContext(), 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(getApplicationContext(), account, istore, new Core.State()); - - } catch (Throwable ex) { - Log.w(ex); - Core.reportError(getApplicationContext(), account, null, ex); - db.account().setAccountError(account.id, Helper.formatThrowable(ex)); - } finally { - if (istore != null) - try { - Log.i(account.name + " closing"); - db.account().setAccountState(account.id, "closing"); - istore.close(); - } catch (Throwable ex) { - Log.e(ex); - } finally { - Log.i(account.name + " closed"); - db.account().setAccountState(account.id, null); - } - } - - return Result.success(); - } - - static void queue(long aid) { - String tag = WorkerFolderSync.class.getSimpleName() + ":" + aid; - Log.i("Queuing " + tag); - - Data data = new Data.Builder().putLong("account", aid).build(); - OneTimeWorkRequest workRequest = - new OneTimeWorkRequest.Builder(WorkerFolderSync.class) - .addTag(tag) - .setInputData(data) - .build(); - WorkManager.getInstance().enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, workRequest); - - Log.i("Queued " + tag); - } -} diff --git a/app/src/main/java/eu/faircode/email/WorkerOperations.java b/app/src/main/java/eu/faircode/email/WorkerOperations.java deleted file mode 100644 index eb6f85acac..0000000000 --- a/app/src/main/java/eu/faircode/email/WorkerOperations.java +++ /dev/null @@ -1,134 +0,0 @@ -package eu.faircode.email; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import java.util.Date; -import java.util.Properties; - -import javax.mail.Folder; -import javax.mail.Session; -import javax.mail.Store; -import javax.mail.event.StoreEvent; -import javax.mail.event.StoreListener; - -import androidx.annotation.NonNull; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -public class WorkerOperations extends Worker { - public WorkerOperations(@NonNull Context context, @NonNull WorkerParameters args) { - super(context, args); - } - - @NonNull - @Override - public Result doWork() { - long fid = getInputData().getLong("folder", -1); - Log.i("Work folder=" + fid); - - final DB db = DB.getInstance(getApplicationContext()); - - final EntityFolder folder = db.folder().getFolder(fid); - if (folder == null) - return Result.success(); - final EntityAccount account = db.account().getAccount(folder.account); - if (account == null) - return Result.success(); - - Store istore = null; - try { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE); - - String protocol = account.getProtocol(); - - // Get properties - Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure); - props.put("mail." + protocol + ".separatestoreconnection", "true"); - - // Create session - 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(protocol); - Helper.connect(getApplicationContext(), 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"); - - // Listen for store events - istore.addStoreListener(new StoreListener() { - @Override - public void notification(StoreEvent e) { - if (e.getMessageType() == StoreEvent.ALERT) { - db.account().setAccountError(account.id, e.getMessage()); - Core.reportError( - getApplicationContext(), account, null, - new Core.AlertException(e.getMessage())); - } else - Log.i(account.name + " notice: " + e.getMessage()); - } - }); - - // Connect folder - Log.i(folder.name + " connecting"); - db.folder().setFolderState(folder.id, "connecting"); - Folder 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(getApplicationContext(), account, folder, isession, istore, ifolder, new Core.State()); - - } catch (Throwable ex) { - Log.w(ex); - Core.reportError(getApplicationContext(), account, folder, ex); - db.account().setAccountError(account.id, Helper.formatThrowable(ex)); - db.folder().setFolderError(folder.id, Helper.formatThrowable(ex, false)); - } finally { - if (istore != null) - try { - Log.i(account.name + " closing"); - db.account().setAccountState(account.id, "closing"); - db.folder().setFolderState(folder.id, "closing"); - istore.close(); - } catch (Throwable ex) { - Log.w(ex); - } finally { - Log.i(account.name + " closed"); - db.account().setAccountState(account.id, "closed"); - db.folder().setFolderState(folder.id, null); - db.folder().setFolderSyncState(folder.id, null); - } - } - - return Result.success(); - } - - static void queue(long fid) { - String tag = WorkerOperations.class.getSimpleName() + ":" + fid; - Log.i("Queuing " + tag); - - Data data = new Data.Builder().putLong("folder", fid).build(); - OneTimeWorkRequest workRequest = - new OneTimeWorkRequest.Builder(WorkerOperations.class) - .addTag(tag) - .setInputData(data) - .build(); - WorkManager.getInstance().enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, workRequest); - - Log.i("Queued " + tag); - } -}