2019-05-12 16:41:51 +00:00
|
|
|
package eu.faircode.email;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.SharedPreferences;
|
|
|
|
import android.net.ConnectivityManager;
|
2019-07-15 14:15:37 +00:00
|
|
|
import android.net.LinkProperties;
|
2019-05-12 16:41:51 +00:00
|
|
|
import android.net.Network;
|
|
|
|
import android.net.NetworkCapabilities;
|
|
|
|
import android.net.NetworkInfo;
|
|
|
|
import android.os.Build;
|
2019-05-22 13:44:10 +00:00
|
|
|
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;
|
|
|
|
|
2019-07-16 12:44:21 +00:00
|
|
|
import org.xbill.DNS.Lookup;
|
2019-09-08 11:22:44 +00:00
|
|
|
import org.xbill.DNS.MXRecord;
|
|
|
|
import org.xbill.DNS.Record;
|
2019-07-16 12:44:21 +00:00
|
|
|
import org.xbill.DNS.SimpleResolver;
|
|
|
|
import org.xbill.DNS.Type;
|
|
|
|
|
2019-07-15 14:15:37 +00:00
|
|
|
import java.net.InetAddress;
|
2019-07-16 12:44:21 +00:00
|
|
|
import java.net.UnknownHostException;
|
2019-05-12 16:41:51 +00:00
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.List;
|
2019-12-06 15:00:11 +00:00
|
|
|
import java.util.Objects;
|
2019-05-12 16:41:51 +00:00
|
|
|
|
2019-07-16 12:44:21 +00:00
|
|
|
import javax.mail.Address;
|
|
|
|
import javax.mail.internet.InternetAddress;
|
2019-05-12 16:41:51 +00:00
|
|
|
|
|
|
|
public class ConnectionHelper {
|
2019-07-16 09:02:51 +00:00
|
|
|
// https://dns.watch/
|
|
|
|
private static final String DEFAULT_DNS = "84.200.69.80";
|
2019-07-15 14:15:37 +00:00
|
|
|
|
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;
|
2019-12-08 08:00:00 +00:00
|
|
|
private Integer type = 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);
|
|
|
|
}
|
|
|
|
|
2019-12-08 08:00:00 +00:00
|
|
|
Integer getType() {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
2019-05-12 16:41:51 +00:00
|
|
|
public void update(NetworkState newState) {
|
|
|
|
connected = newState.connected;
|
|
|
|
unmetered = newState.unmetered;
|
|
|
|
suitable = newState.suitable;
|
2019-12-06 15:00:11 +00:00
|
|
|
roaming = newState.roaming;
|
2019-12-08 08:00:00 +00:00
|
|
|
type = newState.type;
|
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) &&
|
|
|
|
Objects.equals(this.roaming, other.roaming));
|
|
|
|
} else
|
|
|
|
return false;
|
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));
|
|
|
|
|
2019-12-08 08:00:00 +00:00
|
|
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
|
|
NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
|
|
|
|
if (ani != null)
|
|
|
|
state.type = ani.getType();
|
|
|
|
|
2019-07-01 18:19:14 +00:00
|
|
|
if (state.connected && !roaming) {
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
|
|
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) &&
|
|
|
|
RLAH_COUNTRY_CODES.contains(network))
|
|
|
|
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);
|
2019-09-30 16:13:40 +00:00
|
|
|
if (cm == null)
|
|
|
|
return null;
|
2019-05-12 16:41:51 +00:00
|
|
|
|
2020-03-13 06:59:29 +00:00
|
|
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
2019-09-30 16:13:40 +00:00
|
|
|
NetworkInfo ani = cm.getActiveNetworkInfo();
|
2019-05-12 16:41:51 +00:00
|
|
|
if (ani == null || !ani.isConnected())
|
|
|
|
return null;
|
|
|
|
return cm.isActiveNetworkMetered();
|
|
|
|
}
|
|
|
|
|
2019-09-30 16:13:40 +00:00
|
|
|
Network active = cm.getActiveNetwork();
|
2019-05-12 16:41:51 +00:00
|
|
|
if (active == null) {
|
|
|
|
Log.i("isMetered: no active network");
|
2019-12-23 20:29:57 +00:00
|
|
|
return null;
|
2019-05-12 16:41:51 +00:00
|
|
|
}
|
|
|
|
|
2019-09-19 08:16:46 +00:00
|
|
|
// onLost [... state: DISCONNECTED/DISCONNECTED ... available: true]
|
|
|
|
NetworkInfo ani = cm.getNetworkInfo(active);
|
|
|
|
if (ani == null || !ani.isConnected())
|
|
|
|
return null;
|
|
|
|
|
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)) {
|
2019-10-22 08:37:21 +00:00
|
|
|
// 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
|
|
|
|
|
|
|
|
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
|
|
|
|
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) {
|
|
|
|
Log.i("isMetered: no underlying network");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assume metered
|
|
|
|
Log.i("isMetered: underlying assume metered");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-05-22 13:44:10 +00:00
|
|
|
static boolean airplaneMode(Context context) {
|
|
|
|
return Settings.System.getInt(context.getContentResolver(),
|
|
|
|
Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
|
|
|
|
}
|
2019-07-15 14:15:37 +00:00
|
|
|
|
|
|
|
static String getDnsServer(Context context) {
|
|
|
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
|
|
if (cm == null)
|
|
|
|
return DEFAULT_DNS;
|
2019-07-15 14:53:47 +00:00
|
|
|
|
|
|
|
LinkProperties props = null;
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
|
|
|
for (Network network : cm.getAllNetworks()) {
|
|
|
|
NetworkInfo ni = cm.getNetworkInfo(network);
|
|
|
|
if (ni != null && ni.isConnected()) {
|
|
|
|
props = cm.getLinkProperties(network);
|
|
|
|
Log.i("Old props=" + props);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Network active = cm.getActiveNetwork();
|
|
|
|
if (active == null)
|
|
|
|
return DEFAULT_DNS;
|
|
|
|
props = cm.getLinkProperties(active);
|
|
|
|
Log.i("New props=" + props);
|
|
|
|
}
|
|
|
|
|
2019-07-15 14:15:37 +00:00
|
|
|
if (props == null)
|
|
|
|
return DEFAULT_DNS;
|
2019-07-15 14:53:47 +00:00
|
|
|
|
2019-07-15 14:15:37 +00:00
|
|
|
List<InetAddress> dns = props.getDnsServers();
|
|
|
|
if (dns.size() == 0)
|
|
|
|
return DEFAULT_DNS;
|
|
|
|
else
|
|
|
|
return dns.get(0).getHostAddress();
|
|
|
|
}
|
2019-07-16 12:44:21 +00:00
|
|
|
|
2019-07-16 17:28:42 +00:00
|
|
|
static boolean lookupMx(Address[] addresses, Context context) throws UnknownHostException {
|
|
|
|
boolean ok = true;
|
|
|
|
|
2019-07-16 12:44:21 +00:00
|
|
|
if (addresses != null)
|
2019-07-16 17:28:42 +00:00
|
|
|
for (Address address : addresses)
|
|
|
|
try {
|
|
|
|
String email = ((InternetAddress) address).getAddress();
|
2019-10-19 10:15:30 +00:00
|
|
|
if (email == null)
|
2019-07-16 17:28:42 +00:00
|
|
|
continue;
|
2019-10-19 10:15:30 +00:00
|
|
|
|
|
|
|
int d = email.lastIndexOf("@");
|
|
|
|
if (d < 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
String domain = email.substring(d + 1);
|
2019-07-16 17:28:42 +00:00
|
|
|
Lookup lookup = new Lookup(domain, Type.MX);
|
|
|
|
SimpleResolver resolver = new SimpleResolver(ConnectionHelper.getDnsServer(context));
|
|
|
|
lookup.setResolver(resolver);
|
|
|
|
Log.i("Lookup MX=" + domain + " @" + resolver.getAddress());
|
|
|
|
|
|
|
|
lookup.run();
|
|
|
|
if (lookup.getResult() == Lookup.HOST_NOT_FOUND ||
|
|
|
|
lookup.getResult() == Lookup.TYPE_NOT_FOUND) {
|
|
|
|
Log.i("Lookup MX=" + domain + " result=" + lookup.getErrorString());
|
|
|
|
throw new UnknownHostException(context.getString(R.string.title_no_server, domain));
|
|
|
|
} else if (lookup.getResult() != Lookup.SUCCESSFUL)
|
|
|
|
ok = false;
|
|
|
|
} catch (UnknownHostException ex) {
|
|
|
|
throw ex;
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
Log.e(ex);
|
|
|
|
ok = false;
|
2019-07-16 12:44:21 +00:00
|
|
|
}
|
2019-07-16 17:28:42 +00:00
|
|
|
|
|
|
|
return ok;
|
2019-07-16 12:44:21 +00:00
|
|
|
}
|
2019-09-02 06:30:13 +00:00
|
|
|
|
2019-09-08 11:22:44 +00:00
|
|
|
static InetAddress lookupMx(String domain, Context context) {
|
|
|
|
try {
|
|
|
|
Lookup lookup = new Lookup(domain, Type.MX);
|
|
|
|
SimpleResolver resolver = new SimpleResolver(getDnsServer(context));
|
|
|
|
lookup.setResolver(resolver);
|
|
|
|
Log.i("Lookup MX=" + domain + " @" + resolver.getAddress());
|
|
|
|
|
|
|
|
lookup.run();
|
|
|
|
if (lookup.getResult() == Lookup.SUCCESSFUL) {
|
|
|
|
Record[] answers = lookup.getAnswers();
|
|
|
|
if (answers != null && answers.length > 0 && answers[0] instanceof MXRecord) {
|
|
|
|
MXRecord mx = (MXRecord) answers[0];
|
|
|
|
return InetAddress.getByName(mx.getTarget().toString(true));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
Log.w(ex);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-01-08 10:38:45 +00:00
|
|
|
static String getParentDomain(String host) {
|
2019-09-02 06:30:13 +00:00
|
|
|
if (host != null) {
|
|
|
|
String[] h = host.split("\\.");
|
|
|
|
if (h.length >= 2)
|
|
|
|
return h[h.length - 2] + "." + h[h.length - 1];
|
|
|
|
}
|
|
|
|
return host;
|
|
|
|
}
|
2019-05-12 16:41:51 +00:00
|
|
|
}
|