package eu.faircode.email; 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; import android.telephony.TelephonyManager; import androidx.preference.PreferenceManager; import com.bugsnag.android.BreadcrumbType; import com.bugsnag.android.Bugsnag; import com.sun.mail.imap.IMAPStore; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.mail.MessagingException; public class ConnectionHelper { // Roam like at home // https://en.wikipedia.org/wiki/European_Union_roaming_regulations private static final List 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 final int AUTH_TYPE_PASSWORD = 1; static final int AUTH_TYPE_GMAIL = 2; static class NetworkState { private Boolean connected = null; private Boolean suitable = null; private Boolean unmetered = null; private Boolean roaming = null; boolean isConnected() { return (connected != null && connected); } boolean isSuitable() { return (suitable != null && suitable); } boolean isUnmetered() { return (unmetered != null && unmetered); } boolean isRoaming() { return (roaming != null && roaming); } public void update(NetworkState newState) { connected = newState.connected; unmetered = newState.unmetered; suitable = newState.suitable; } } static NetworkState getNetworkState(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean metered = prefs.getBoolean("metered", true); boolean roaming = prefs.getBoolean("roaming", true); boolean rlah = prefs.getBoolean("rlah", true); NetworkState state = new NetworkState(); Boolean isMetered = isMetered(context); state.connected = (isMetered != null); state.unmetered = (isMetered != null && !isMetered); state.suitable = (isMetered != null && (metered || !isMetered)); if (state.connected && !roaming) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo()); 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); } } 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); } } return state; } private static Boolean isMetered(Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo()); if (ani == null || !ani.isConnected()) return null; return cm.isActiveNetworkMetered(); } Network active = (cm == null ? null : cm.getActiveNetwork()); if (active == null) { Log.i("isMetered: no active network"); return null; } 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)) { boolean unmetered = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); Log.i("isMetered: active not VPN unmetered=" + unmetered); return !unmetered; } // VPN: evaluate underlying networks boolean underlying = false; Network[] networks = cm.getAllNetworks(); for (Network network : networks) { caps = cm.getNetworkCapabilities(network); if (caps == null) { Log.i("isMetered: no underlying caps"); continue; // network unknown } Log.i("isMetered: underlying caps=" + caps); if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { Log.i("isMetered: underlying no internet"); continue; } if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)) { Log.i("isMetered: underlying restricted"); continue; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) { Log.i("isMetered: underlying background"); continue; } if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { underlying = true; Log.i("isMetered: underlying is connected"); if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { Log.i("isMetered: underlying is unmetered"); return false; } } } if (!underlying) { Log.i("isMetered: no underlying network"); return null; } // Assume metered Log.i("isMetered: underlying assume metered"); return true; } static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException { istore.connect(account.host, account.port, account.user, account.password); // https://www.ietf.org/rfc/rfc2971.txt if (istore.hasCapability("ID")) try { Map id = new LinkedHashMap<>(); id.put("name", context.getString(R.string.app_name)); id.put("version", BuildConfig.VERSION_NAME); Map sid = istore.id(id); if (sid != null) { Map crumb = new HashMap<>(); for (String key : sid.keySet()) { crumb.put(key, sid.get(key)); Log.i("Server " + key + "=" + sid.get(key)); } Bugsnag.leaveBreadcrumb("server", BreadcrumbType.LOG, crumb); } } catch (MessagingException ex) { Log.w(ex); } } static boolean airplaneMode(Context context) { return Settings.System.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0; } }