Use alarm manager to keep alive

This commit is contained in:
M66B 2018-10-09 19:39:25 +00:00
parent b19f496ce0
commit 2f0babc479
5 changed files with 76 additions and 184 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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<Void>() {
@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<Boolean>() {
@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);
}
}

View File

@ -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<EntityFolder, IMAPFolder> folders = new HashMap<>();
List<Thread> pollers = new ArrayList<>();
List<Thread> syncs = new ArrayList<>();
List<Thread> 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) {

View File

@ -102,26 +102,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAfter" />
<TextView
android:id="@+id/tvInterval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_poll_interval"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etAfter" />
<EditText
android:id="@+id/etInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="9"
android:inputType="number"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInterval" />
<Button
android:id="@+id/btnSave"
android:layout_width="wrap_content"
@ -129,7 +109,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_save"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInterval" />
app:layout_constraintTop_toBottomOf="@id/etAfter" />
<ImageButton
android:id="@+id/ibDelete"
@ -138,7 +118,7 @@
android:layout_marginTop="12dp"
android:src="@drawable/baseline_delete_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInterval" />
app:layout_constraintTop_toBottomOf="@id/etAfter" />
<ProgressBar
android:id="@+id/pbSave"
@ -161,11 +141,5 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpInterval"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvInterval,etInterval" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>