diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index af37b6d863..a54e29381c 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -26,7 +26,6 @@ import android.content.SharedPreferences; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.TextUtils; @@ -1630,8 +1629,8 @@ public class FragmentMessages extends FragmentEx { if (download == 0) download = Long.MAX_VALUE; - ConnectivityManager cm = getContext().getSystemService(ConnectivityManager.class); - boolean metered = (cm == null || cm.isActiveNetworkMetered()); + Boolean isMetered = Helper.isMetered(getContext()); + boolean metered = (isMetered == null || isMetered); int count = 0; int unseen = 0; diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index f520229024..b54f97f723 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -30,6 +30,9 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -291,6 +294,95 @@ public class Helper { return filename.substring(index + 1); } + static Boolean isMetered(Context context) { + ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + + Network active = cm.getActiveNetwork(); + if (active == null) { + Log.i(Helper.TAG, "isMetered: no active network"); + return null; + } + + NetworkInfo ni = cm.getNetworkInfo(active); + if (ni == null) { + Log.i(Helper.TAG, "isMetered: active no info"); + return null; + } + + Log.i(Helper.TAG, "isMetered: active info=" + ni); + + if (!ni.isConnected()) { + Log.i(Helper.TAG, "isMetered: active not connected"); + return null; + } + + NetworkCapabilities caps = cm.getNetworkCapabilities(active); + if (caps == null) { + Log.i(Helper.TAG, "isMetered: active no caps"); + return null; + } + + Log.i(Helper.TAG, "isMetered: active caps=" + caps); + + boolean unmetered = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + if (unmetered) { + Log.i(Helper.TAG, "isMetered: active unmetered"); + return false; + } + + if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + Log.i(Helper.TAG, "isMetered: active metered"); + return true; + } + + // VPN: evaluate underlying networks + + Network[] networks = cm.getAllNetworks(); + if (networks == null) { + Log.i(Helper.TAG, "isMetered: no underlying networks"); + return null; + } + + boolean connected = false; + for (Network network : networks) { + ni = cm.getNetworkInfo(network); + if (ni == null) { + Log.i(Helper.TAG, "isMetered: no underlying info"); + return null; + } + + Log.i(Helper.TAG, "isMetered: underlying info=" + ni); + + caps = cm.getNetworkCapabilities(network); + if (caps == null) { + Log.i(Helper.TAG, "isMetered: no underlying caps"); + return null; + } + + if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + if (ni.isConnected()) { + connected = true; + Log.i(Helper.TAG, "isMetered: underlying is connected"); + + if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { + Log.i(Helper.TAG, "isMetered: underlying is unmetered"); + return false; + } + } else + Log.i(Helper.TAG, "isMetered: underlying is disconnected"); + } else + Log.i(Helper.TAG, "isMetered: underlying is VPN"); + } + + if (!connected) { + Log.i(Helper.TAG, "isMetered: underlying disconnected"); + return null; + } + + Log.i(Helper.TAG, "isMetered: underlying is metered"); + return true; + } + static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException { try { istore.connect(account.host, account.port, account.user, account.password); diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index fb997743f8..730fc47dce 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -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; @@ -944,10 +943,13 @@ public class ServiceSynchronize extends LifecycleService { db.endTransaction(); } + Boolean isMetered = Helper.isMetered(ServiceSynchronize.this); + boolean metered = (isMetered == null || isMetered); + try { db.beginTransaction(); downloadMessage( - ServiceSynchronize.this, + ServiceSynchronize.this, metered, folder, ifolder, (IMAPMessage) imessage, message.id); db.setTransactionSuccessful(); } finally { @@ -1025,9 +1027,14 @@ public class ServiceSynchronize extends LifecycleService { db.endTransaction(); } + Boolean isMetered = Helper.isMetered(ServiceSynchronize.this); + boolean metered = (isMetered == null || isMetered); + try { db.beginTransaction(); - downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), message.id); + downloadMessage( + ServiceSynchronize.this, metered, + folder, ifolder, (IMAPMessage) e.getMessage(), message.id); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -2017,11 +2024,16 @@ public class ServiceSynchronize extends LifecycleService { Message[] isub = Arrays.copyOfRange(imessages, from, i + 1); // Fetch on demand + Boolean isMetered = Helper.isMetered(ServiceSynchronize.this); + boolean metered = (isMetered == null || isMetered); + for (int j = isub.length - 1; j >= 0 && state.running(); j--) try { db.beginTransaction(); if (ids[from + j] != null) { - downloadMessage(this, folder, ifolder, (IMAPMessage) isub[j], ids[from + j]); + downloadMessage( + this, metered, + folder, ifolder, (IMAPMessage) isub[j], ids[from + j]); Thread.sleep(20); } db.setTransactionSuccessful(); @@ -2262,7 +2274,7 @@ public class ServiceSynchronize extends LifecycleService { return message; } - private static void downloadMessage(Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, long id) throws MessagingException, IOException { + private static void downloadMessage(Context context, boolean metered, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, long id) throws MessagingException, IOException { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); long download = prefs.getInt("download", 32768); if (download == 0) @@ -2275,9 +2287,6 @@ public class ServiceSynchronize extends LifecycleService { List attachments = db.attachment().getAttachments(message.id); MessageHelper helper = new MessageHelper(imessage); - ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); - boolean metered = (cm == null || cm.isActiveNetworkMetered()); - boolean fetch = false; if (!message.content) if (!metered || (message.size != null && message.size < download)) @@ -2339,16 +2348,11 @@ public class ServiceSynchronize extends LifecycleService { @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) { try { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); - boolean metered = prefs.getBoolean("metered", true); - boolean unmetered = (capabilities != null && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)); - - if (!started && (metered || unmetered)) + if (!started) { EntityLog.log(ServiceSynchronize.this, "Network " + network + " capabilities " + capabilities); - - if (!started && suitableNetwork()) - queue_reload(true, "connect " + network); + if (suitableNetwork()) + queue_reload(true, "connect " + network); + } } catch (Throwable ex) { Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); } @@ -2382,17 +2386,14 @@ public class ServiceSynchronize extends LifecycleService { } private boolean suitableNetwork() { - ConnectivityManager cm = getSystemService(ConnectivityManager.class); - Network active = cm.getActiveNetwork(); - NetworkCapabilities caps = (active == null ? null : cm.getNetworkCapabilities(active)); - NetworkInfo ni = (active == null ? null : cm.getNetworkInfo(active)); - boolean unmetered = (caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); boolean metered = prefs.getBoolean("metered", true); - boolean suitable = (active != null && (metered || unmetered)); - EntityLog.log(ServiceSynchronize.this, "suitable=" + suitable + " active=" + ni); + Boolean isMetered = Helper.isMetered(ServiceSynchronize.this); + + boolean suitable = (isMetered != null && (metered || !isMetered)); + EntityLog.log(ServiceSynchronize.this, + "suitable=" + suitable + " metered=" + metered + " isMetered=" + isMetered); // The connected state is deliberately ignored return suitable;