Added setting to use unmetered connections only

pull/146/head
M66B 7 years ago
parent fc10cb600a
commit 57ad9e0e92

@ -42,6 +42,7 @@ import androidx.appcompat.widget.SwitchCompat;
public class FragmentOptions extends FragmentEx implements SharedPreferences.OnSharedPreferenceChangeListener {
private SwitchCompat swEnabled;
private SwitchCompat swMetered;
private SwitchCompat swAvatars;
private SwitchCompat swIdenticons;
private SwitchCompat swCompact;
@ -67,6 +68,7 @@ public class FragmentOptions extends FragmentEx implements SharedPreferences.OnS
// Get controls
swEnabled = view.findViewById(R.id.swEnabled);
swMetered = view.findViewById(R.id.swMetered);
swCompact = view.findViewById(R.id.swCompact);
swAvatars = view.findViewById(R.id.swAvatars);
swIdenticons = view.findViewById(R.id.swIdenticons);
@ -92,10 +94,16 @@ public class FragmentOptions extends FragmentEx implements SharedPreferences.OnS
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
prefs.edit().putBoolean("enabled", checked).apply();
if (checked)
ServiceSynchronize.start(getContext());
else
ServiceSynchronize.stop(getContext());
ServiceSynchronize.reload(getContext(), "enabled=" + checked);
}
});
swMetered.setChecked(prefs.getBoolean("metered", true));
swMetered.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
prefs.edit().putBoolean("metered", checked).apply();
ServiceSynchronize.reload(getContext(), "metered=" + checked);
}
});

@ -63,10 +63,9 @@ public class MessageHelper {
private MimeMessage imessage;
private String raw = null;
final static int NETWORK_TIMEOUT = 60 * 1000; // milliseconds
final static int CLOSE_TIMEOUT = 20 * 1000; // milliseconds
final static int FETCH_SIZE = 1024 * 1024; // bytes, default 16K
final static int POOL_TIMEOUT = 3 * 60 * 1000; // milliseconds, default 45 sec
private final static int NETWORK_TIMEOUT = 60 * 1000; // milliseconds
private final static int FETCH_SIZE = 1024 * 1024; // bytes, default 16K
private final static int POOL_TIMEOUT = 3 * 60 * 1000; // milliseconds, default 45 sec
static Properties getSessionProperties(int auth_type, boolean insecure) {
Properties props = new Properties();

@ -22,22 +22,14 @@ package eu.faircode.email;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class ReceiverAutostart extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()))
new Thread(new Runnable() {
@Override
public void run() {
int synchronizing = DB.getInstance(context).account().getSynchronizingAccountCount();
Log.i(Helper.TAG, "Synchronizing accounts=" + synchronizing);
if (synchronizing > 0)
ServiceSynchronize.init(context);
JobDaily.schedule(context);
}
}).start();
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
EntityLog.log(context, intent.getAction());
ServiceSynchronize.init(context);
}
}
}

@ -36,7 +36,6 @@ import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Build;
@ -136,9 +135,10 @@ public class ServiceSynchronize extends LifecycleService {
private static final int CONNECT_BACKOFF_AlARM = 15; // minutes
private static final int SYNC_BATCH_SIZE = 20;
private static final int DOWNLOAD_BATCH_SIZE = 20;
private static final long RECONNECT_BACKOFF = 60 * 1000L; // milliseconds
private static final long RECONNECT_BACKOFF = 90 * 1000L; // milliseconds
private static final int PREVIEW_SIZE = 250;
private static final int ACCOUNT_ERROR_AFTER = 90; // minutes
private static final long STOP_DELAY = 5000L; // milliseconds
static final int PI_WHY = 1;
static final int PI_CLEAR = 2;
@ -242,7 +242,7 @@ public class ServiceSynchronize extends LifecycleService {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
cm.unregisterNetworkCallback(serviceManager);
serviceManager.onLost(null);
serviceManager.service_destroy();
Widget.update(this, -1);
@ -279,13 +279,15 @@ public class ServiceSynchronize extends LifecycleService {
prefs.edit().putBoolean("why", true).apply();
startActivity(why);
}
} else if ("start".equals(action))
serviceManager.queue_start();
else if ("stop".equals(action))
serviceManager.queue_stop();
else if ("reload".equals(action))
serviceManager.queue_reload();
else if ("clear".equals(action)) {
} else if ("init".equals(action)) {
// Network events will manage the service
serviceManager.service_init();
} else if ("reload".equals(action)) {
serviceManager.queue_reload(true, intent.getStringExtra("reason"));
} else if ("clear".equals(action)) {
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
@ -293,6 +295,7 @@ public class ServiceSynchronize extends LifecycleService {
return null;
}
}.load(this, new Bundle());
} else if (action.startsWith("seen:") ||
action.startsWith("archive:") ||
action.startsWith("trash:") ||
@ -668,6 +671,8 @@ public class ServiceSynchronize extends LifecycleService {
nm.notify(action, 1, getNotificationError(action, ex).build());
}
// connection failure: Too many simultaneous connections
if (BuildConfig.DEBUG &&
!(ex instanceof SendFailedException) &&
!(ex instanceof MailConnectException) &&
@ -731,7 +736,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, account.name + " event: " + e.getMessage());
if (BuildConfig.DEBUG)
db.account().setAccountError(account.id, e.getMessage());
state.semaphore.release();
state.thread.interrupt();
yieldWakelock();
} finally {
wl.release();
@ -751,7 +756,6 @@ public class ServiceSynchronize extends LifecycleService {
wl.acquire();
Log.i(Helper.TAG, "Folder created=" + e.getFolder().getFullName());
reload(ServiceSynchronize.this, "folder created");
yieldWakelock();
} finally {
wl.release();
}
@ -769,7 +773,6 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, "Renamed to " + name + " count=" + count);
reload(ServiceSynchronize.this, "folder renamed");
yieldWakelock();
} finally {
wl.release();
}
@ -781,7 +784,6 @@ public class ServiceSynchronize extends LifecycleService {
wl.acquire();
Log.i(Helper.TAG, "Folder deleted=" + e.getFolder().getFullName());
reload(ServiceSynchronize.this, "folder deleted");
yieldWakelock();
} finally {
wl.release();
}
@ -852,7 +854,11 @@ public class ServiceSynchronize extends LifecycleService {
try {
ifolder.open(Folder.READ_WRITE);
} catch (Throwable ex) {
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
if (ex instanceof MessagingException && "connection failure".equals(ex.getMessage())) {
Throwable ex1 = new MessagingException("Too many simultaneous connections?", (MessagingException) ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex1));
} else
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
throw ex;
}
folders.put(folder, ifolder);
@ -923,10 +929,8 @@ public class ServiceSynchronize extends LifecycleService {
} 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));
state.semaphore.release();
state.thread.interrupt();
yieldWakelock();
} finally {
wl.release();
@ -954,10 +958,9 @@ public class ServiceSynchronize extends LifecycleService {
} 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));
state.semaphore.release();
state.thread.interrupt();
yieldWakelock();
} finally {
wl.release();
}
@ -1008,10 +1011,8 @@ public class ServiceSynchronize extends LifecycleService {
} 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));
state.semaphore.release();
state.thread.interrupt();
yieldWakelock();
} finally {
wl.release();
@ -1022,10 +1023,8 @@ public class ServiceSynchronize extends LifecycleService {
} 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));
state.semaphore.release();
state.thread.interrupt();
yieldWakelock();
} finally {
wl.release();
@ -1051,10 +1050,8 @@ public class ServiceSynchronize extends LifecycleService {
} 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));
state.semaphore.release();
state.thread.interrupt();
yieldWakelock();
} finally {
Log.i(Helper.TAG, folder.name + " end idle");
@ -1129,7 +1126,6 @@ public class ServiceSynchronize extends LifecycleService {
} 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 {
if (shouldClose) {
@ -1248,29 +1244,11 @@ public class ServiceSynchronize extends LifecycleService {
// Close store
try {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
EntityLog.log(ServiceSynchronize.this, account.name + " store closing");
istore.close();
EntityLog.log(ServiceSynchronize.this, account.name + " store closed");
} catch (Throwable ex) {
Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
}
}
});
t.start();
try {
t.join(MessageHelper.CLOSE_TIMEOUT);
if (t.isAlive()) {
Log.w(Helper.TAG, account.name + " Close timeout");
t.interrupt();
}
} catch (InterruptedException ex) {
Log.w(Helper.TAG, account.name + " close wait " + ex.toString());
t.interrupt();
}
EntityLog.log(ServiceSynchronize.this, account.name + " store closing");
istore.close();
EntityLog.log(ServiceSynchronize.this, account.name + " store closed");
} catch (Throwable ex) {
Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
EntityLog.log(this, account.name + " closed");
db.account().setAccountState(account.id, null);
@ -2271,68 +2249,78 @@ public class ServiceSynchronize extends LifecycleService {
private class ServiceManager extends ConnectivityManager.NetworkCallback {
private ServiceState state;
private boolean running = false;
private boolean started = false;
private int queued = 0;
private long lastLost = 0;
private EntityFolder outbox = null;
private ExecutorService lifecycle = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this);
boolean metered = prefs.getBoolean("metered", true);
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
NetworkCapabilities nc = cm.getNetworkCapabilities(network);
boolean unmetered = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
if (!started && (metered || unmetered))
EntityLog.log(ServiceSynchronize.this, "Network " + network + " capabilities " + capabilities);
if (!started && suitableNetwork())
queue_reload(true, "connect " + network);
}
@Override
public void onAvailable(Network network) {
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
NetworkInfo ni = cm.getNetworkInfo(network);
EntityLog.log(ServiceSynchronize.this, "Network available " + network + " running=" + running + " " + ni);
EntityLog.log(ServiceSynchronize.this, "Available " + network + " " + cm.getNetworkInfo(network));
if (!running) {
running = true;
queued++;
lifecycle.submit(new Runnable() {
@Override
public void run() {
try {
Log.i(Helper.TAG, "Starting service");
start();
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
queued--;
}
}
});
}
if (!started && suitableNetwork())
queue_reload(true, "connect " + network);
}
@Override
public void onLost(Network network) {
EntityLog.log(ServiceSynchronize.this, "Network lost " + network + " running=" + running);
if (running) {
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
NetworkInfo ani = (network == null ? null : cm.getActiveNetworkInfo());
EntityLog.log(ServiceSynchronize.this, "Network active=" + (ani == null ? null : ani.toString()));
if (ani == null || !ani.isConnected()) {
EntityLog.log(ServiceSynchronize.this, "Network disconnected=" + ani);
running = false;
lastLost = new Date().getTime();
queued++;
lifecycle.submit(new Runnable() {
@Override
public void run() {
try {
stop();
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
queued--;
}
}
});
}
EntityLog.log(ServiceSynchronize.this, "Lost " + network);
if (started && !suitableNetwork()) {
lastLost = new Date().getTime();
queue_reload(false, "disconnect " + network);
}
}
private void start() {
private boolean suitableNetwork() {
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
Network network = cm.getActiveNetwork();
NetworkCapabilities nc = (network == null ? null : cm.getNetworkCapabilities(network));
boolean unmetered = (!cm.isActiveNetworkMetered() ||
(nc != null && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this);
boolean metered = prefs.getBoolean("metered", true);
// The connected state is deliberately ignored
return (metered || unmetered);
}
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_destroy() {
EntityLog.log(ServiceSynchronize.this, "Service destroy");
if (started)
queue_reload(false, "service destroy");
}
private void _start() {
EntityLog.log(ServiceSynchronize.this, "Main start queued=" + queued);
state = new ServiceState();
@ -2352,15 +2340,7 @@ public class ServiceSynchronize extends LifecycleService {
outbox = db.folder().getOutbox();
if (outbox == null) {
EntityLog.log(ServiceSynchronize.this, "No outbox, halt");
serviceManager.queue_stop();
return;
}
List<EntityAccount> accounts = db.account().getAccounts(true);
if (accounts.size() == 0) {
EntityLog.log(ServiceSynchronize.this, "No accounts, halt");
serviceManager.queue_stop();
EntityLog.log(ServiceSynchronize.this, "No outbox");
return;
}
@ -2391,6 +2371,7 @@ public class ServiceSynchronize extends LifecycleService {
.putExtra("folder", outbox.id));
// Start monitoring accounts
List<EntityAccount> accounts = db.account().getAccounts(true);
for (final EntityAccount account : accounts) {
Log.i(Helper.TAG, account.host + "/" + account.user + " run");
final ServiceState astate = new ServiceState();
@ -2425,8 +2406,9 @@ public class ServiceSynchronize extends LifecycleService {
for (ServiceState astate : threadState) {
astate.running = false;
astate.semaphore.release();
join(astate.thread);
}
for (ServiceState astate : threadState)
join(astate.thread);
threadState.clear();
// Stop monitoring outbox
@ -2449,7 +2431,7 @@ public class ServiceSynchronize extends LifecycleService {
yieldWakelock();
}
private void stop() {
private void _stop() {
PowerManager pm = getSystemService(PowerManager.class);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
@ -2471,68 +2453,41 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private void queue_reload() {
if (running) {
queued++;
lifecycle.submit(new Runnable() {
@Override
public void run() {
try {
stop();
start();
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
queued--;
}
}
});
}
}
private void queue_reload(final boolean start, String reason) {
EntityLog.log(ServiceSynchronize.this, "Reload start=" + start +
" started=" + started + " queued=" + queued + " " + reason);
private void queue_start() {
if (!running) {
running = true;
queued++;
lifecycle.submit(new Runnable() {
@Override
public void run() {
try {
start();
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
queued--;
}
}
});
}
}
final boolean doStop = started;
final boolean doStart = (start && isEnabled() && suitableNetwork());
private void queue_stop() {
if (running) {
running = false;
queued++;
lifecycle.submit(new Runnable() {
@Override
public void run() {
try {
stop();
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
if (--queued == 0) {
try {
Thread.sleep(3000);
} catch (InterruptedException ignored) {
}
if (queued == 0)
stopSelf();
queued++;
lifecycle.submit(new Runnable() {
@Override
public void run() {
try {
if (doStop)
_stop();
if (doStart)
_start();
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
queued--;
if (queued == 0 && !isEnabled()) {
try {
Thread.sleep(STOP_DELAY);
} catch (InterruptedException ignored) {
}
if (queued == 0 && !isEnabled()) {
EntityLog.log(ServiceSynchronize.this, "Service stop");
stopSelf();
}
}
}
});
}
}
});
started = doStart;
}
private BroadcastReceiver outboxReceiver = new BroadcastReceiver() {
@ -2598,24 +2553,20 @@ public class ServiceSynchronize extends LifecycleService {
public static void init(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean("enabled", true))
start(context);
}
public static void start(Context context) {
ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class).setAction("start"));
}
public static void stop(Context context) {
ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class).setAction("stop"));
if (prefs.getBoolean("enabled", true)) {
ContextCompat.startForegroundService(context,
new Intent(context, ServiceSynchronize.class)
.setAction("init"));
JobDaily.schedule(context);
}
}
public static void reload(Context context, String reason) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean("enabled", true)) {
Log.i(Helper.TAG, "Reload because of '" + reason + "'");
ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class).setAction("reload"));
}
ContextCompat.startForegroundService(context,
new Intent(context, ServiceSynchronize.class)
.setAction("reload")
.putExtra("reason", reason));
JobDaily.schedule(context);
}
private class ServiceState {

@ -69,9 +69,6 @@ public class ServiceTileSynchronize extends TileService implements SharedPrefere
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean enabled = !prefs.getBoolean("enabled", false);
prefs.edit().putBoolean("enabled", enabled).apply();
if (enabled)
ServiceSynchronize.start(this);
else
ServiceSynchronize.stop(this);
ServiceSynchronize.reload(this, "tile=" + enabled);
}
}

@ -20,6 +20,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swMetered"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_metered"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/swEnabled" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swCompact"
android:layout_width="match_parent"
@ -27,7 +36,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_advanced_compact"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/swEnabled" />
app:layout_constraintTop_toBottomOf="@id/swMetered" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swAvatars"

@ -97,6 +97,7 @@
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_enabled">Synchronize</string>
<string name="title_advanced_metered">Use metered connections</string>
<string name="title_advanced_compact">Compact message view</string>
<string name="title_advanced_avatars">Show contact photos</string>
<string name="title_advanced_identicons">Show identicons</string>

Loading…
Cancel
Save