FairEmail/app/src/main/java/eu/faircode/email/ConnectionHelper.java

431 lines
16 KiB
Java
Raw Normal View History

2019-05-12 16:41:51 +00:00
package eu.faircode.email;
2020-04-15 18:08:17 +00:00
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
2021-01-01 07:56:36 +00:00
Copyright 2018-2021 by Marcel Bokhorst (M66B)
2020-04-15 18:08:17 +00:00
*/
2020-03-24 10:29:01 +00:00
import android.accounts.AccountsException;
2019-05-12 16:41:51 +00:00
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
import android.provider.Settings;
2019-05-12 16:41:51 +00:00
import android.telephony.TelephonyManager;
2019-12-06 15:00:11 +00:00
import androidx.annotation.Nullable;
2019-05-12 16:41:51 +00:00
import androidx.preference.PreferenceManager;
2020-03-24 10:29:01 +00:00
import com.sun.mail.iap.ConnectionException;
import java.io.IOException;
2019-05-12 16:41:51 +00:00
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
2020-04-19 15:31:50 +00:00
import java.util.Locale;
2019-12-06 15:00:11 +00:00
import java.util.Objects;
2019-05-12 16:41:51 +00:00
public class ConnectionHelper {
static final List<String> PREF_NETWORK = Collections.unmodifiableList(Arrays.asList(
"metered", "roaming", "rlah" // update network state
));
2019-05-12 16:41:51 +00:00
// Roam like at home
// https://en.wikipedia.org/wiki/European_Union_roaming_regulations
private static final List<String> RLAH_COUNTRY_CODES = Collections.unmodifiableList(Arrays.asList(
"AT", // Austria
"BE", // Belgium
"BG", // Bulgaria
"HR", // Croatia
"CY", // Cyprus
"CZ", // Czech Republic
"DK", // Denmark
"EE", // Estonia
"FI", // Finland
"FR", // France
"DE", // Germany
"GR", // Greece
"HU", // Hungary
"IS", // Iceland
"IE", // Ireland
"IT", // Italy
"LV", // Latvia
"LI", // Liechtenstein
"LT", // Lithuania
"LU", // Luxembourg
"MT", // Malta
"NL", // Netherlands
"NO", // Norway
"PL", // Poland
"PT", // Portugal
"RO", // Romania
"SK", // Slovakia
"SI", // Slovenia
"ES", // Spain
"SE", // Sweden
"GB" // United Kingdom
));
static class NetworkState {
private Boolean connected = null;
private Boolean suitable = null;
private Boolean unmetered = null;
private Boolean roaming = null;
2020-10-28 10:44:15 +00:00
private Network active = null;
2019-05-12 16:41:51 +00:00
boolean isConnected() {
return (connected != null && connected);
}
boolean isSuitable() {
return (suitable != null && suitable);
}
boolean isUnmetered() {
return (unmetered != null && unmetered);
}
boolean isRoaming() {
return (roaming != null && roaming);
}
2020-10-27 19:49:20 +00:00
Network getActive() {
return active;
}
2019-05-12 16:41:51 +00:00
public void update(NetworkState newState) {
connected = newState.connected;
suitable = newState.suitable;
2020-10-27 19:49:20 +00:00
unmetered = newState.unmetered;
2019-12-06 15:00:11 +00:00
roaming = newState.roaming;
2020-10-27 19:49:20 +00:00
active = newState.active;
2019-12-06 15:00:11 +00:00
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof NetworkState) {
NetworkState other = (NetworkState) obj;
return (Objects.equals(this.connected, other.connected) &&
Objects.equals(this.suitable, other.suitable) &&
Objects.equals(this.unmetered, other.unmetered) &&
2020-10-28 10:44:15 +00:00
Objects.equals(this.roaming, other.roaming) &&
Objects.equals(this.active, other.active));
2019-12-06 15:00:11 +00:00
} else
return false;
2019-05-12 16:41:51 +00:00
}
2020-10-28 10:44:15 +00:00
@Override
public String toString() {
return "connected=" + connected +
" suitable=" + suitable +
" unmetered=" + unmetered +
" roaming=" + roaming +
" active=" + active;
}
}
static boolean isConnected(Context context, Network network) {
NetworkInfo ni = getNetworkInfo(context, network);
return (ni != null && ni.isConnected());
}
static NetworkInfo getNetworkInfo(Context context, Network network) {
try {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
return (cm == null ? null : cm.getNetworkInfo(network));
} catch (Throwable ex) {
Log.e(ex);
return null;
}
2019-05-12 16:41:51 +00:00
}
static NetworkState getNetworkState(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean metered = prefs.getBoolean("metered", true);
boolean roaming = prefs.getBoolean("roaming", true);
2019-06-20 16:47:26 +00:00
boolean rlah = prefs.getBoolean("rlah", true);
2019-05-12 16:41:51 +00:00
NetworkState state = new NetworkState();
2019-07-01 18:19:14 +00:00
try {
Boolean isMetered = isMetered(context);
state.connected = (isMetered != null);
state.unmetered = (isMetered != null && !isMetered);
state.suitable = (isMetered != null && (metered || !isMetered));
2020-10-27 19:49:20 +00:00
state.active = getActiveNetwork(context);
2019-07-01 18:19:14 +00:00
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
2019-07-01 18:19:14 +00:00
if (state.connected && !roaming) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
2020-10-27 19:49:20 +00:00
NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
2019-07-01 18:19:14 +00:00
if (ani != null)
state.roaming = ani.isRoaming();
} else {
Network active = (cm == null ? null : cm.getActiveNetwork());
if (active != null) {
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
if (caps != null)
state.roaming = !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
}
2019-05-12 16:41:51 +00:00
}
2019-07-01 18:19:14 +00:00
if (state.roaming != null && state.roaming && rlah)
try {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (tm != null) {
String sim = tm.getSimCountryIso();
String network = tm.getNetworkCountryIso();
Log.i("Country SIM=" + sim + " network=" + network);
if (sim != null && network != null &&
RLAH_COUNTRY_CODES.contains(sim.toUpperCase()) &&
RLAH_COUNTRY_CODES.contains(network.toUpperCase()))
2019-07-01 18:19:14 +00:00
state.roaming = false;
}
} catch (Throwable ex) {
Log.w(ex);
2019-05-12 16:41:51 +00:00
}
2019-07-01 18:19:14 +00:00
}
} catch (Throwable ex) {
Log.e(ex);
2019-05-12 16:41:51 +00:00
}
return state;
}
private static Boolean isMetered(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
2020-08-30 06:20:57 +00:00
if (cm == null) {
Log.i("isMetered: no connectivity manager");
2019-09-30 16:13:40 +00:00
return null;
2020-08-30 06:20:57 +00:00
}
2019-05-12 16:41:51 +00:00
2021-01-03 13:33:56 +00:00
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
2019-09-30 16:13:40 +00:00
NetworkInfo ani = cm.getActiveNetworkInfo();
2021-01-03 13:33:56 +00:00
if (ani == null || !ani.isConnected())
2019-05-12 16:41:51 +00:00
return null;
2021-01-03 13:33:56 +00:00
return cm.isActiveNetworkMetered();
}
Network active = cm.getActiveNetwork();
if (active == null) {
Log.i("isMetered: no active network");
return null;
2019-05-12 16:41:51 +00:00
}
2020-11-22 13:18:55 +00:00
// onLost [... state: SUSPENDED/SUSPENDED ... available: true]
2019-09-19 08:16:46 +00:00
// onLost [... state: DISCONNECTED/DISCONNECTED ... available: true]
NetworkInfo ani = cm.getNetworkInfo(active);
2020-11-22 13:18:55 +00:00
if (ani == null || ani.getState() != NetworkInfo.State.CONNECTED) {
Log.i("isMetered: no/connected active info ani=" + ani);
if (ani == null ||
ani.getState() != NetworkInfo.State.SUSPENDED ||
ani.getType() != ConnectivityManager.TYPE_VPN)
return null;
2020-08-30 06:20:57 +00:00
}
2019-09-19 08:16:46 +00:00
2019-05-12 16:41:51 +00:00
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
if (caps == null) {
Log.i("isMetered: active no caps");
return null; // network unknown
}
Log.i("isMetered: active caps=" + caps);
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
Log.i("isMetered: no internet");
return null;
}
if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)) {
Log.i("isMetered: active restricted");
return null;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) {
Log.i("isMetered: active background");
return null;
}
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
// NET_CAPABILITY_NOT_METERED is unreliable on older Android versions
boolean metered = cm.isActiveNetworkMetered();
Log.i("isMetered: active not VPN metered=" + metered);
return metered;
2019-05-12 16:41:51 +00:00
}
// VPN: evaluate underlying networks
2020-07-26 18:19:59 +00:00
Integer transport = null;
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
transport = NetworkCapabilities.TRANSPORT_CELLULAR;
else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
transport = NetworkCapabilities.TRANSPORT_WIFI;
2019-05-12 16:41:51 +00:00
boolean underlying = false;
Network[] networks = cm.getAllNetworks();
2019-06-14 09:00:34 +00:00
for (Network network : networks) {
caps = cm.getNetworkCapabilities(network);
if (caps == null) {
Log.i("isMetered: no underlying caps");
continue; // network unknown
}
2019-05-12 16:41:51 +00:00
2019-06-14 09:00:34 +00:00
Log.i("isMetered: underlying caps=" + caps);
2019-05-12 16:41:51 +00:00
2019-06-14 09:00:34 +00:00
if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
Log.i("isMetered: underlying no internet");
continue;
}
2019-05-12 16:41:51 +00:00
2019-06-14 09:00:34 +00:00
if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)) {
Log.i("isMetered: underlying restricted");
continue;
}
2019-05-12 16:41:51 +00:00
2019-06-14 09:00:34 +00:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) {
Log.i("isMetered: underlying background");
continue;
}
2019-05-12 16:41:51 +00:00
2020-07-26 18:19:59 +00:00
if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) &&
(caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) &&
(transport != null && !caps.hasTransport(transport))) {
Log.i("isMetered: underlying other transport");
continue;
}
2019-06-14 09:00:34 +00:00
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
underlying = true;
Log.i("isMetered: underlying is connected");
2019-05-12 16:41:51 +00:00
2019-06-14 09:00:34 +00:00
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
Log.i("isMetered: underlying is unmetered");
return false;
2019-05-12 16:41:51 +00:00
}
}
2019-06-14 09:00:34 +00:00
}
2019-05-12 16:41:51 +00:00
if (!underlying) {
2020-04-03 07:15:36 +00:00
// VPN-only network via USB is possible
boolean metered = cm.isActiveNetworkMetered();
Log.i("isMetered: no underlying network metered=" + metered);
return metered;
2019-05-12 16:41:51 +00:00
}
// Assume metered
Log.i("isMetered: underlying assume metered");
return true;
}
2020-07-28 14:58:10 +00:00
static Network getActiveNetwork(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null)
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return cm.getActiveNetwork();
NetworkInfo ani = cm.getActiveNetworkInfo();
if (ani == null)
return null;
Network[] networks = cm.getAllNetworks();
for (Network network : networks) {
NetworkInfo ni = cm.getNetworkInfo(network);
if (ni == null)
continue;
if (ni.getType() == ani.getType() &&
ni.getSubtype() == ani.getSubtype())
return network;
}
return null;
}
2020-03-24 10:29:01 +00:00
static boolean isIoError(Throwable ex) {
while (ex != null) {
if (isMaxConnections(ex.getMessage()) ||
ex instanceof IOException ||
ex instanceof ConnectionException ||
ex instanceof AccountsException ||
2020-12-18 16:51:21 +00:00
"EOF on socket".equals(ex.getMessage()) ||
2020-03-24 10:29:01 +00:00
"failed to connect".equals(ex.getMessage()))
return true;
ex = ex.getCause();
}
return false;
}
2020-10-28 19:09:38 +00:00
static boolean isMaxConnections(Throwable ex) {
while (ex != null) {
if (isMaxConnections(ex.getMessage()))
return true;
ex = ex.getCause();
}
return false;
}
2020-03-24 10:29:01 +00:00
static boolean isMaxConnections(String message) {
return (message != null &&
(message.contains("Too many simultaneous connections") /* Gmail */ ||
message.contains("Maximum number of connections") /* ... from user+IP exceeded */ /* Dovecot */ ||
message.contains("Too many concurrent connections") /* ... to this mailbox */ ||
2020-05-15 13:45:14 +00:00
message.contains("User is authenticated but not connected") /* Outlook */ ||
2020-10-01 06:42:34 +00:00
message.contains("Account is temporarily unavailable") /* Arcor.de / TalkTalk.net */ ||
message.contains("Connection dropped by server?")));
2020-03-24 10:29:01 +00:00
}
2020-04-08 12:59:33 +00:00
static Boolean isSyntacticallyInvalid(Throwable ex) {
if (ex.getMessage() == null)
return false;
// 501 HELO requires valid address
// 501 Syntactically invalid HELO argument(s)
String message = ex.getMessage().toLowerCase(Locale.ROOT);
return message.contains("syntactically invalid") ||
message.contains("requires valid address");
2020-04-08 12:59:33 +00:00
}
2020-02-12 11:56:19 +00:00
static boolean vpnActive(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null)
return false;
try {
for (Network network : cm.getAllNetworks()) {
NetworkCapabilities caps = cm.getNetworkCapabilities(network);
if (caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN))
return true;
}
} catch (Throwable ex) {
Log.w(ex);
}
return false;
}
static boolean airplaneMode(Context context) {
return Settings.System.getInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
2019-05-12 16:41:51 +00:00
}