diff --git a/app/src/main/java/eu/faircode/email/DaoFolder.java b/app/src/main/java/eu/faircode/email/DaoFolder.java index 109af8834f..bcb54ebbf8 100644 --- a/app/src/main/java/eu/faircode/email/DaoFolder.java +++ b/app/src/main/java/eu/faircode/email/DaoFolder.java @@ -124,9 +124,8 @@ public interface DaoFolder { ", synchronize = :synchronize" + ", unified = :unified" + ", `after` = :after" + - ", `poll_interval` = :poll_interval" + " WHERE id = :id") - int setFolderProperties(long id, String name, String display, boolean hide, boolean synchronize, boolean unified, int after, Integer poll_interval); + int setFolderProperties(long id, String name, String display, boolean hide, boolean synchronize, boolean unified, int after); @Query("UPDATE folder SET name = :name WHERE account = :account AND name = :old") int renameFolder(long account, String old, String name); diff --git a/app/src/main/java/eu/faircode/email/EntityFolder.java b/app/src/main/java/eu/faircode/email/EntityFolder.java index e2961a61d1..8fcc0b0b06 100644 --- a/app/src/main/java/eu/faircode/email/EntityFolder.java +++ b/app/src/main/java/eu/faircode/email/EntityFolder.java @@ -60,7 +60,7 @@ public class EntityFolder implements Serializable { public String type; @NonNull public Boolean synchronize; - public Integer poll_interval; + public Integer poll_interval; // obsolete @NonNull public Integer after; // days public String display; diff --git a/app/src/main/java/eu/faircode/email/FragmentFolder.java b/app/src/main/java/eu/faircode/email/FragmentFolder.java index 04d8a2a40d..7c7e1d640e 100644 --- a/app/src/main/java/eu/faircode/email/FragmentFolder.java +++ b/app/src/main/java/eu/faircode/email/FragmentFolder.java @@ -44,7 +44,6 @@ import javax.mail.Session; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.Group; import androidx.lifecycle.Observer; public class FragmentFolder extends FragmentEx { @@ -55,12 +54,10 @@ public class FragmentFolder extends FragmentEx { private CheckBox cbSynchronize; private CheckBox cbUnified; private EditText etAfter; - private EditText etInterval; private Button btnSave; private ImageButton ibDelete; private ProgressBar pbSave; private ProgressBar pbWait; - private Group grpInterval; private long id = -1; private long account = -1; @@ -89,12 +86,10 @@ public class FragmentFolder extends FragmentEx { cbSynchronize = view.findViewById(R.id.cbSynchronize); cbUnified = view.findViewById(R.id.cbUnified); etAfter = view.findViewById(R.id.etAfter); - etInterval = view.findViewById(R.id.etInterval); btnSave = view.findViewById(R.id.btnSave); ibDelete = view.findViewById(R.id.ibDelete); pbSave = view.findViewById(R.id.pbSave); pbWait = view.findViewById(R.id.pbWait); - grpInterval = view.findViewById(R.id.grpInterval); btnSave.setOnClickListener(new View.OnClickListener() { @Override @@ -113,7 +108,6 @@ public class FragmentFolder extends FragmentEx { args.putBoolean("unified", cbUnified.isChecked()); args.putBoolean("synchronize", cbSynchronize.isChecked()); args.putString("after", etAfter.getText().toString()); - args.putString("interval", etInterval.getText().toString()); new SimpleTask() { @Override @@ -126,12 +120,10 @@ public class FragmentFolder extends FragmentEx { boolean unified = args.getBoolean("unified"); boolean synchronize = args.getBoolean("synchronize"); String after = args.getString("after"); - String interval = args.getString("interval"); if (TextUtils.isEmpty(display) || display.equals(name)) display = null; int days = (TextUtils.isEmpty(after) ? EntityFolder.DEFAULT_USER_SYNC : Integer.parseInt(after)); - Integer poll_interval = (TextUtils.isEmpty(interval) ? null : Integer.parseInt(interval)); IMAPStore istore = null; DB db = DB.getInstance(getContext()); @@ -165,7 +157,6 @@ public class FragmentFolder extends FragmentEx { create.unified = unified; create.synchronize = synchronize; create.after = days; - create.poll_interval = poll_interval; db.folder().insertFolder(create); } else { Log.i(Helper.TAG, "Renaming folder=" + name); @@ -180,7 +171,7 @@ public class FragmentFolder extends FragmentEx { if (folder != null) { Log.i(Helper.TAG, "Updating folder=" + name); - db.folder().setFolderProperties(id, name, display, hide, synchronize, unified, days, poll_interval); + db.folder().setFolderProperties(id, name, display, hide, synchronize, unified, days); if (!synchronize) db.folder().setFolderError(id, null); } @@ -303,7 +294,6 @@ public class FragmentFolder extends FragmentEx { ibDelete.setVisibility(View.GONE); pbSave.setVisibility(View.GONE); pbWait.setVisibility(View.VISIBLE); - grpInterval.setVisibility(View.GONE); return view; } @@ -330,7 +320,6 @@ public class FragmentFolder extends FragmentEx { cbUnified.setChecked(folder == null ? false : folder.unified); cbSynchronize.setChecked(folder == null || folder.synchronize); etAfter.setText(Integer.toString(folder == null ? EntityFolder.DEFAULT_USER_SYNC : folder.after)); - etInterval.setText(folder == null || folder.poll_interval == null ? null : Integer.toString(folder.poll_interval)); } // Consider previous save as cancelled @@ -341,57 +330,5 @@ public class FragmentFolder extends FragmentEx { ibDelete.setVisibility(folder == null || !EntityFolder.USER.equals(folder.type) ? View.GONE : View.VISIBLE); } }); - - Bundle args = new Bundle(); - args.putLong("id", id); - args.putLong("account", account); - - new SimpleTask() { - @Override - protected Boolean onLoad(Context context, Bundle args) throws Throwable { - long fid = args.getLong("id"); - long aid = args.getLong("account"); - - IMAPStore istore = null; - DB db = DB.getInstance(getContext()); - try { - db.beginTransaction(); - - EntityAccount account; - if (fid < 0) - account = db.account().getAccount(aid); - else { - EntityFolder folder = db.folder().getFolder(fid); - account = db.account().getAccount(folder.account); - } - - db.setTransactionSuccessful(); - - Properties props = MessageHelper.getSessionProperties(account.auth_type); - Session isession = Session.getInstance(props, null); - istore = (IMAPStore) isession.getStore("imaps"); - istore.connect(account.host, account.port, account.user, account.password); - - return istore.hasCapability("IDLE"); - } finally { - db.endTransaction(); - - if (istore != null) - istore.close(); - } - } - - @Override - protected void onLoaded(Bundle args, Boolean capIdle) { - grpInterval.setVisibility(capIdle ? View.GONE : View.VISIBLE); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - grpInterval.setVisibility(View.VISIBLE); - if (BuildConfig.DEBUG) - Helper.unexpectedError(getContext(), ex); - } - }.load(this, args); } } diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 6ef735b533..54161b2a1c 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -20,6 +20,7 @@ package eu.faircode.email; */ import android.Manifest; +import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -78,9 +79,7 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.Semaphore; import javax.mail.Address; import javax.mail.AuthenticationFailedException; @@ -571,7 +570,7 @@ public class ServiceSynchronize extends LifecycleService { final IMAPStore istore = (IMAPStore) isession.getStore("imaps"); final Map folders = new HashMap<>(); - List pollers = new ArrayList<>(); + List syncs = new ArrayList<>(); List idlers = new ArrayList<>(); try { // Listen for store events @@ -613,7 +612,6 @@ public class ServiceSynchronize extends LifecycleService { // Listen for connection events istore.addConnectionListener(new ConnectionAdapter() { - @Override public void opened(ConnectionEvent e) { Log.i(Helper.TAG, account.name + " opened"); @@ -664,8 +662,8 @@ public class ServiceSynchronize extends LifecycleService { db.folder().setFolderState(folder.id, "connected"); db.folder().setFolderError(folder.id, null); - // Keep folder connection alive - Thread poller = new Thread(new Runnable() { + // Synchronize folder + Thread sync = new Thread(new Runnable() { @Override public void run() { try { @@ -797,46 +795,6 @@ public class ServiceSynchronize extends LifecycleService { } } }); - - if (!capIdle) { - Log.i(Helper.TAG, folder.name + " start polling"); - - PowerManager pm = getSystemService(PowerManager.class); - PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, account.name + "/" + folder.name); - - final Thread pthread = Thread.currentThread(); - int rate = (folder.poll_interval == null ? 9 : folder.poll_interval); - ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); - ScheduledFuture future = scheduler.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - Log.i(Helper.TAG, folder.name + " wakeup poll"); - pthread.interrupt(); - } - }, rate, rate, TimeUnit.MINUTES); - - while (state.running) { - try { - Thread.sleep(Long.MAX_VALUE); - } catch (InterruptedException ex) { - Log.w(Helper.TAG, folder.name + " poll " + ex.toString()); - } - - try { - wl.acquire(); - synchronizeMessages(account, folder, ifolder, state); - } catch (Throwable ex) { - Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); - reportError(account.name, folder.name, ex); - - db.folder().setFolderError(folder.id, Helper.formatThrowable(ex)); - } finally { - wl.release(); - } - } - - future.cancel(false); - } } catch (Throwable ex) { Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, folder.name, ex); @@ -844,16 +802,13 @@ public class ServiceSynchronize extends LifecycleService { db.folder().setFolderError(folder.id, Helper.formatThrowable(ex)); state.thread.interrupt(); - } finally { - if (!capIdle) - Log.i(Helper.TAG, folder.name + " end polling"); } } - }, "sync.poller." + folder.id); - poller.start(); - pollers.add(poller); + }, "sync." + folder.id); + sync.start(); + syncs.add(sync); - // Receive folder events + // Idle folder if (capIdle) { Thread idler = new Thread(new Runnable() { @Override @@ -876,7 +831,7 @@ public class ServiceSynchronize extends LifecycleService { Log.i(Helper.TAG, folder.name + " end idle"); } } - }, "sync.idle." + folder.id); + }, "idler." + folder.id); idler.start(); idlers.add(idler); } @@ -884,6 +839,7 @@ public class ServiceSynchronize extends LifecycleService { backoff = CONNECT_BACKOFF_START; + // Process folder actions BroadcastReceiver processFolder = new BroadcastReceiver() { @Override public void onReceive(Context context, final Intent intent) { @@ -961,52 +917,78 @@ public class ServiceSynchronize extends LifecycleService { f.addAction(ACTION_SYNCHRONIZE_FOLDER); f.addAction(ACTION_PROCESS_OPERATIONS); f.addDataType("account/" + account.id); + LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this); lbm.registerReceiver(processFolder, f); - try { - PowerManager pm = getSystemService(PowerManager.class); - PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, account.name); + // Create barrier + final Semaphore sem = new Semaphore(0); - ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); - ScheduledFuture future = scheduler.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - Log.i(Helper.TAG, account.name + " wakeup check"); - state.thread.interrupt(); - } - }, account.poll_interval, account.poll_interval, TimeUnit.MINUTES); + // Keep alive + final PowerManager pm = getSystemService(PowerManager.class); + final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "account." + account.id); - // Keep store alive - while (state.running) { - EntityLog.log(this, account.name + " wait=" + account.poll_interval); + final AlarmManager am = getSystemService(AlarmManager.class); + final String id = BuildConfig.APPLICATION_ID + ".POLL." + account.id; + final PendingIntent pi = PendingIntent.getBroadcast(ServiceSynchronize.this, 0, new Intent(id), 0); + + BroadcastReceiver alive = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + EntityLog.log(context, account.name + " keep alive"); try { - Thread.sleep(Long.MAX_VALUE); - } catch (InterruptedException ex) { - Log.w(Helper.TAG, account.name + " wait " + ex.toString()); - } - - if (state.running) try { wl.acquire(); if (!istore.isConnected()) throw new StoreClosedException(istore); for (EntityFolder folder : folders.keySet()) - if (!folders.get(folder).isOpen()) - throw new FolderClosedException(folders.get(folder)); + if (capIdle) { + if (!folders.get(folder).isOpen()) + throw new FolderClosedException(folders.get(folder)); + } else + synchronizeMessages(account, folder, folders.get(folder), state); + } catch (Throwable ex) { + Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex)); + reportError(account.name, null, ex); + + db.account().setAccountError(account.id, Helper.formatThrowable(ex)); + + sem.release(); } finally { wl.release(); } + + // Reschedule alarm + am.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + account.poll_interval * 60 * 1000L, + pi); } + }; + registerReceiver(alive, new IntentFilter(id)); - future.cancel(false); + // Schedule alarm + EntityLog.log(this, account.name + " wait=" + account.poll_interval); + am.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + account.poll_interval * 60 * 1000L, + pi); - Log.i(Helper.TAG, account.name + " done running=" + state.running); + // Wait for interrupt or exception + try { + sem.acquire(); + } catch (InterruptedException ex) { + Log.w(Helper.TAG, account.name + " semaphore " + ex.toString()); } finally { + // Cleanup + am.cancel(pi); + unregisterReceiver(alive); lbm.unregisterReceiver(processFolder); } + + Log.i(Helper.TAG, account.name + " done running=" + state.running); } catch (Throwable ex) { Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(account.name, null, ex); @@ -1018,10 +1000,16 @@ public class ServiceSynchronize extends LifecycleService { for (EntityFolder folder : folders.keySet()) db.folder().setFolderState(folder.id, "closing"); - // Stop pollers - for (Thread poller : pollers) { - poller.interrupt(); - join(poller); + // Stop syncs + for (Thread sync : syncs) { + sync.interrupt(); + join(sync); + } + + // Stop idlers + for (Thread idler : idlers) { + idler.interrupt(); + join(idler); } // Close store @@ -1053,12 +1041,6 @@ public class ServiceSynchronize extends LifecycleService { for (EntityFolder folder : folders.keySet()) db.folder().setFolderState(folder.id, null); } - - // Stop idlers - for (Thread idler : idlers) { - idler.interrupt(); - join(idler); - } } if (state.running) { diff --git a/app/src/main/res/layout/fragment_folder.xml b/app/src/main/res/layout/fragment_folder.xml index d723ddbc1e..924841fdbf 100644 --- a/app/src/main/res/layout/fragment_folder.xml +++ b/app/src/main/res/layout/fragment_folder.xml @@ -102,26 +102,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvAfter" /> - - - -