Refactoring: connectionHelper

This commit is contained in:
M66B 2019-05-12 18:41:51 +02:00
parent 09ace5e05a
commit 76d53e9e6b
19 changed files with 372 additions and 350 deletions

View File

@ -94,7 +94,8 @@ public class ActivityEml extends ActivityBase {
AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(uri, "*/*", null);
try (InputStream is = new BufferedInputStream(descriptor.createInputStream())) {
Properties props = MessageHelper.getSessionProperties(Helper.AUTH_TYPE_PASSWORD, null, false);
Properties props = MessageHelper.getSessionProperties(
ConnectionHelper.AUTH_TYPE_PASSWORD, null, false);
Session isession = Session.getInstance(props, null);
MimeMessage mmessage = new MimeMessage(isession, is);

View File

@ -1335,7 +1335,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
} else {
// Decode message
Properties props = MessageHelper.getSessionProperties(Helper.AUTH_TYPE_PASSWORD, null, false);
Properties props = MessageHelper.getSessionProperties(
ConnectionHelper.AUTH_TYPE_PASSWORD, null, false);
Session isession = Session.getInstance(props, null);
ByteArrayInputStream is = new ByteArrayInputStream(decrypted.toByteArray());
MimeMessage imessage = new MimeMessage(isession, is);

View File

@ -450,7 +450,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
protected Void onExecute(Context context, Bundle args) {
long fid = args.getLong("folder");
if (!Helper.getNetworkState(context).isSuitable())
if (!ConnectionHelper.getNetworkState(context).isSuitable())
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
boolean now = true;

View File

@ -3000,7 +3000,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
this.zoom = zoom;
this.sort = sort;
this.filter_duplicates = filter_duplicates;
this.suitable = Helper.getNetworkState(context).isSuitable();
this.suitable = ConnectionHelper.getNetworkState(context).isSuitable();
this.properties = properties;
this.colorPrimary = Helper.resolveColor(context, R.attr.colorPrimary);
@ -3096,7 +3096,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
void checkInternet() {
boolean suitable = Helper.getNetworkState(context).isSuitable();
boolean suitable = ConnectionHelper.getNetworkState(context).isSuitable();
if (this.suitable != suitable) {
this.suitable = suitable;
notifyDataSetChanged();

View File

@ -270,7 +270,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
if (imessages == null)
try {
// Check connectivity
if (!Helper.getNetworkState(context).isSuitable())
if (!ConnectionHelper.getNetworkState(context).isSuitable())
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@ -288,7 +288,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
Log.i("Boundary server connecting account=" + account.name);
istore = (IMAPStore) isession.getStore(protocol);
Helper.connect(context, istore, account);
ConnectionHelper.connect(context, istore, account);
Log.i("Boundary server opening folder=" + browsable.name);
ifolder = (IMAPFolder) istore.getFolder(browsable.name);

View File

@ -0,0 +1,303 @@
package eu.faircode.email;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
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.telephony.TelephonyManager;
import androidx.preference.PreferenceManager;
import com.bugsnag.android.BreadcrumbType;
import com.bugsnag.android.Bugsnag;
import com.sun.mail.imap.IMAPStore;
import java.io.IOException;
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.AuthenticationFailedException;
import javax.mail.MessagingException;
public class ConnectionHelper {
// 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 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 rlah = prefs.getBoolean("rlah", true);
boolean roaming = prefs.getBoolean("roaming", 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.getActiveNetworkInfo();
if (ani != null)
state.roaming = ani.isRoaming();
} else {
Network active = 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.getActiveNetworkInfo();
if (ani == null || !ani.isConnected())
return null;
return cm.isActiveNetworkMetered();
}
Network active = 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();
if (networks != null)
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, AuthenticatorException, OperationCanceledException, IOException {
try {
istore.connect(account.host, account.port, account.user, account.password);
} catch (AuthenticationFailedException ex) {
if (account.auth_type == AUTH_TYPE_GMAIL) {
account.password = refreshToken(context, "com.google", account.user, account.password);
DB.getInstance(context).account().setAccountPassword(account.id, account.password);
istore.connect(account.host, account.port, account.user, account.password);
} else
throw ex;
}
// https://www.ietf.org/rfc/rfc2971.txt
if (istore.hasCapability("ID"))
try {
Map<String, String> id = new LinkedHashMap<>();
id.put("name", context.getString(R.string.app_name));
id.put("version", BuildConfig.VERSION_NAME);
Map<String, String> sid = istore.id(id);
if (sid != null) {
Map<String, String> 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 String refreshToken(Context context, String type, String name, String current)
throws AuthenticatorException, OperationCanceledException, IOException {
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(type);
for (Account account : accounts)
if (name.equals(account.name)) {
Log.i("Refreshing token");
am.invalidateAuthToken(type, current);
String refreshed = am.blockingGetAuthToken(account, getAuthTokenType(type), true);
if (refreshed == null)
throw new OperationCanceledException("no token");
Log.i("Refreshed token");
return refreshed;
}
return current;
}
static String getAuthTokenType(String type) {
if ("com.google".equals(type))
return "oauth2:https://mail.google.com/";
return null;
}
}

View File

@ -2125,13 +2125,13 @@ class Core {
}
static class State {
private Helper.NetworkState networkState;
private ConnectionHelper.NetworkState networkState;
private Thread thread;
private Semaphore semaphore = new Semaphore(0);
private boolean running = true;
private boolean recoverable = true;
State(Helper.NetworkState networkState) {
State(ConnectionHelper.NetworkState networkState) {
this.networkState = networkState;
}
@ -2139,7 +2139,7 @@ class Core {
this(parent.networkState);
}
Helper.NetworkState getNetworkState() {
ConnectionHelper.NetworkState getNetworkState() {
return networkState;
}

View File

@ -447,8 +447,8 @@ public class EmailProvider {
int getAuthType() {
if ("com.google".equals(type))
return Helper.AUTH_TYPE_GMAIL;
return Helper.AUTH_TYPE_PASSWORD;
return ConnectionHelper.AUTH_TYPE_GMAIL;
return ConnectionHelper.AUTH_TYPE_PASSWORD;
}
@Override

View File

@ -150,7 +150,7 @@ public class FragmentAccount extends FragmentBase {
private long id = -1;
private boolean saving = false;
private int auth_type = Helper.AUTH_TYPE_PASSWORD;
private int auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
private int color = Color.TRANSPARENT;
@Override
@ -251,7 +251,7 @@ public class FragmentAccount extends FragmentBase {
return;
adapterView.setTag(position);
auth_type = Helper.AUTH_TYPE_PASSWORD;
auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
etHost.setText(provider.imap_host);
etPort.setText(provider.imap_host == null ? null : Integer.toString(provider.imap_port));
@ -314,8 +314,8 @@ public class FragmentAccount extends FragmentBase {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String user = etUser.getText().toString();
if (auth_type != Helper.AUTH_TYPE_PASSWORD && !user.equals(etUser.getTag())) {
auth_type = Helper.AUTH_TYPE_PASSWORD;
if (auth_type != ConnectionHelper.AUTH_TYPE_PASSWORD && !user.equals(etUser.getTag())) {
auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
tilPassword.getEditText().setText(null);
tilPassword.setEnabled(true);
tilPassword.setPasswordVisibilityToggleEnabled(true);
@ -586,8 +586,8 @@ public class FragmentAccount extends FragmentBase {
try {
istore.connect(host, Integer.parseInt(port), user, password);
} catch (AuthenticationFailedException ex) {
if (auth_type == Helper.AUTH_TYPE_GMAIL) {
password = Helper.refreshToken(context, "com.google", user, password);
if (auth_type == ConnectionHelper.AUTH_TYPE_GMAIL) {
password = ConnectionHelper.refreshToken(context, "com.google", user, password);
istore.connect(host, Integer.parseInt(port), user, password);
} else
throw ex;
@ -884,8 +884,8 @@ public class FragmentAccount extends FragmentBase {
try {
istore.connect(host, Integer.parseInt(port), user, password);
} catch (AuthenticationFailedException ex) {
if (auth_type == Helper.AUTH_TYPE_GMAIL) {
password = Helper.refreshToken(context, "com.google", user, password);
if (auth_type == ConnectionHelper.AUTH_TYPE_GMAIL) {
password = ConnectionHelper.refreshToken(context, "com.google", user, password);
istore.connect(host, Integer.parseInt(port), user, password);
} else
throw ex;
@ -1129,7 +1129,7 @@ public class FragmentAccount extends FragmentBase {
spProvider.setAdapter(aaProvider);
if (savedInstanceState == null) {
auth_type = (account == null ? Helper.AUTH_TYPE_PASSWORD : account.auth_type);
auth_type = (account == null ? ConnectionHelper.AUTH_TYPE_PASSWORD : account.auth_type);
if (account != null) {
boolean found = false;
@ -1154,7 +1154,7 @@ public class FragmentAccount extends FragmentBase {
rgEncryption.check(account != null && account.starttls ? R.id.radio_starttls : R.id.radio_ssl);
cbInsecure.setChecked(account == null ? false : account.insecure);
etUser.setTag(account == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user);
etUser.setTag(account == null || auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD ? null : account.user);
etUser.setText(account == null ? null : account.user);
tilPassword.getEditText().setText(account == null ? null : account.password);
etRealm.setText(account == null ? null : account.realm);
@ -1202,8 +1202,8 @@ public class FragmentAccount extends FragmentBase {
Helper.setViewsEnabled(view, true);
tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
setColor(color);
cbPrimary.setEnabled(cbSynchronize.isChecked());
@ -1350,7 +1350,7 @@ public class FragmentAccount extends FragmentBase {
am.getAuthToken(
account,
Helper.getAuthTokenType(type),
ConnectionHelper.getAuthTokenType(type),
new Bundle(),
getActivity(),
new AccountManagerCallback<Bundle>() {
@ -1361,7 +1361,7 @@ public class FragmentAccount extends FragmentBase {
String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
Log.i("Got token");
auth_type = Helper.AUTH_TYPE_GMAIL;
auth_type = ConnectionHelper.AUTH_TYPE_GMAIL;
etUser.setTag(account.name);
etUser.setText(account.name);
etUser.setTag(account.name);
@ -1379,9 +1379,9 @@ public class FragmentAccount extends FragmentBase {
} finally {
btnAuthorize.setEnabled(true);
etUser.setEnabled(true);
tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setPasswordVisibilityToggleEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
tilPassword.setPasswordVisibilityToggleEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
btnCheck.setEnabled(true);
btnSave.setEnabled(true);
new Handler().postDelayed(new Runnable() {

View File

@ -746,7 +746,7 @@ public class FragmentCompose extends FragmentBase {
};
private void checkInternet() {
boolean suitable = Helper.getNetworkState(getContext()).isSuitable();
boolean suitable = ConnectionHelper.getNetworkState(getContext()).isSuitable();
Boolean content = (Boolean) tvNoInternet.getTag();
tvNoInternet.setVisibility(!suitable && content != null && !content ? View.VISIBLE : View.GONE);
@ -1377,7 +1377,8 @@ public class FragmentCompose extends FragmentBase {
(message.identity == null ? null : db.identity().getIdentity(message.identity));
// Build message
Properties props = MessageHelper.getSessionProperties(Helper.AUTH_TYPE_PASSWORD, null, false);
Properties props = MessageHelper.getSessionProperties(
ConnectionHelper.AUTH_TYPE_PASSWORD, null, false);
Session isession = Session.getInstance(props, null);
MimeMessage imessage = new MimeMessage(isession);
MessageHelper.build(context, message, attachments, identity, imessage);

View File

@ -332,7 +332,7 @@ public class FragmentFolders extends FragmentBase {
protected Void onExecute(Context context, Bundle args) {
long aid = args.getLong("account");
if (!Helper.getNetworkState(context).isSuitable())
if (!ConnectionHelper.getNetworkState(context).isSuitable())
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
boolean now = true;

View File

@ -123,7 +123,7 @@ public class FragmentIdentity extends FragmentBase {
private long id = -1;
private long account = -1;
private boolean saving = false;
private int auth_type = Helper.AUTH_TYPE_PASSWORD;
private int auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
private int color = Color.TRANSPARENT;
private String signature = null;
@ -240,12 +240,12 @@ public class FragmentIdentity extends FragmentBase {
// Copy account credentials
etEmail.setText(account.user);
etUser.setTag(auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user);
etUser.setTag(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD ? null : account.user);
etUser.setText(account.user);
tilPassword.getEditText().setText(account.password);
etRealm.setText(account.realm);
tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
}
@Override
@ -261,8 +261,8 @@ public class FragmentIdentity extends FragmentBase {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String user = etUser.getText().toString();
if (auth_type != Helper.AUTH_TYPE_PASSWORD && !user.equals(etUser.getTag())) {
auth_type = Helper.AUTH_TYPE_PASSWORD;
if (auth_type != ConnectionHelper.AUTH_TYPE_PASSWORD && !user.equals(etUser.getTag())) {
auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
tilPassword.getEditText().setText(null);
tilPassword.setEnabled(true);
etRealm.setEnabled(true);
@ -647,8 +647,8 @@ public class FragmentIdentity extends FragmentBase {
try {
itransport.connect(host, Integer.parseInt(port), user, password);
} catch (AuthenticationFailedException ex) {
if (auth_type == Helper.AUTH_TYPE_GMAIL) {
password = Helper.refreshToken(context, "com.google", user, password);
if (auth_type == ConnectionHelper.AUTH_TYPE_GMAIL) {
password = ConnectionHelper.refreshToken(context, "com.google", user, password);
itransport.connect(host, Integer.parseInt(port), user, password);
} else
throw ex;
@ -764,7 +764,7 @@ public class FragmentIdentity extends FragmentBase {
@Override
protected void onExecuted(Bundle args, final EntityIdentity identity) {
if (savedInstanceState == null) {
auth_type = (identity == null ? Helper.AUTH_TYPE_PASSWORD : identity.auth_type);
auth_type = (identity == null ? ConnectionHelper.AUTH_TYPE_PASSWORD : identity.auth_type);
etName.setText(identity == null ? null : identity.name);
etEmail.setText(identity == null ? null : identity.email);
@ -779,7 +779,7 @@ public class FragmentIdentity extends FragmentBase {
rgEncryption.check(identity != null && identity.starttls ? R.id.radio_starttls : R.id.radio_ssl);
cbInsecure.setChecked(identity == null ? false : identity.insecure);
etPort.setText(identity == null ? null : Long.toString(identity.port));
etUser.setTag(identity == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : identity.user);
etUser.setTag(identity == null || auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD ? null : identity.user);
etUser.setText(identity == null ? null : identity.user);
tilPassword.getEditText().setText(identity == null ? null : identity.password);
etRealm.setText(identity == null ? null : identity.realm);
@ -825,8 +825,8 @@ public class FragmentIdentity extends FragmentBase {
Helper.setViewsEnabled(view, true);
tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
etRealm.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
setColor(color);
@ -847,7 +847,7 @@ public class FragmentIdentity extends FragmentBase {
EntityAccount unselected = new EntityAccount();
unselected.id = -1L;
unselected.auth_type = Helper.AUTH_TYPE_PASSWORD;
unselected.auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
unselected.name = getString(R.string.title_select);
unselected.primary = false;
accounts.add(0, unselected);
@ -885,7 +885,7 @@ public class FragmentIdentity extends FragmentBase {
spAccount.setTag(pos);
spAccount.setSelection(pos);
// OAuth token could be updated
if (pos > 0 && accounts.get(pos).auth_type != Helper.AUTH_TYPE_PASSWORD)
if (pos > 0 && accounts.get(pos).auth_type != ConnectionHelper.AUTH_TYPE_PASSWORD)
tilPassword.getEditText().setText(accounts.get(pos).password);
break;
}

View File

@ -803,7 +803,7 @@ public class FragmentMessages extends FragmentBase {
protected Void onExecute(Context context, Bundle args) {
long fid = args.getLong("folder");
if (!Helper.getNetworkState(context).isSuitable())
if (!ConnectionHelper.getNetworkState(context).isSuitable())
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
boolean now = true;
@ -2720,7 +2720,7 @@ public class FragmentMessages extends FragmentBase {
if (download == 0)
download = Long.MAX_VALUE;
boolean unmetered = Helper.getNetworkState(getContext()).isUnmetered();
boolean unmetered = ConnectionHelper.getNetworkState(getContext()).isUnmetered();
int count = 0;
int unseen = 0;

View File

@ -222,7 +222,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre
@Override
public void run() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
Helper.NetworkState networkState = Helper.getNetworkState(getContext());
ConnectionHelper.NetworkState networkState = ConnectionHelper.getNetworkState(getContext());
tvConnectionType.setText(networkState.isUnmetered() ? R.string.title_legend_unmetered : R.string.title_legend_metered);
tvConnectionType.setVisibility(networkState.isConnected() ? View.VISIBLE : View.GONE);

View File

@ -91,7 +91,7 @@ public class FragmentQuickSetup extends FragmentBase {
private Button btnSave;
private Group grpSetup;
private int auth_type = Helper.AUTH_TYPE_PASSWORD;
private int auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
@Override
@Nullable
@ -138,8 +138,8 @@ public class FragmentQuickSetup extends FragmentBase {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (auth_type != Helper.AUTH_TYPE_PASSWORD) {
auth_type = Helper.AUTH_TYPE_PASSWORD;
if (auth_type != ConnectionHelper.AUTH_TYPE_PASSWORD) {
auth_type = ConnectionHelper.AUTH_TYPE_PASSWORD;
tilPassword.getEditText().setText(null);
tilPassword.setEnabled(true);
tilPassword.setPasswordVisibilityToggleEnabled(true);
@ -498,7 +498,7 @@ public class FragmentQuickSetup extends FragmentBase {
am.getAuthToken(
account,
Helper.getAuthTokenType(type),
ConnectionHelper.getAuthTokenType(type),
new Bundle(),
getActivity(),
new AccountManagerCallback<Bundle>() {
@ -511,7 +511,7 @@ public class FragmentQuickSetup extends FragmentBase {
etEmail.setText(account.name);
tilPassword.getEditText().setText(token);
auth_type = Helper.AUTH_TYPE_GMAIL;
auth_type = ConnectionHelper.AUTH_TYPE_GMAIL;
} catch (Throwable ex) {
Log.e(ex);
if (ex instanceof OperationCanceledException ||
@ -523,8 +523,8 @@ public class FragmentQuickSetup extends FragmentBase {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
} finally {
etEmail.setEnabled(true);
tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setPasswordVisibilityToggleEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
tilPassword.setEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
tilPassword.setPasswordVisibilityToggleEnabled(auth_type == ConnectionHelper.AUTH_TYPE_PASSWORD);
btnAuthorize.setEnabled(true);
btnCheck.setEnabled(true);
new Handler().postDelayed(new Runnable() {

View File

@ -19,10 +19,6 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.usage.UsageStatsManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
@ -40,14 +36,11 @@ import android.graphics.Point;
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;
import android.os.Parcel;
import android.os.PowerManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.view.Display;
@ -71,10 +64,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager;
import com.android.billingclient.api.BillingClient;
import com.bugsnag.android.BreadcrumbType;
import com.bugsnag.android.Bugsnag;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.util.MailConnectException;
import org.json.JSONException;
@ -103,11 +93,8 @@ import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -116,10 +103,8 @@ import java.util.Set;
import java.util.concurrent.ThreadFactory;
import javax.mail.Address;
import javax.mail.AuthenticationFailedException;
import javax.mail.FolderClosedException;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.net.ssl.HttpsURLConnection;
@ -133,9 +118,6 @@ public class Helper {
static final int NOTIFICATION_SEND = 2;
static final int NOTIFICATION_EXTERNAL = 3;
static final int AUTH_TYPE_PASSWORD = 1;
static final int AUTH_TYPE_GMAIL = 2;
static final float LOW_LIGHT = 0.6f;
static final String FAQ_URI = "https://github.com/M66B/open-source-email/blob/master/FAQ.md";
@ -149,42 +131,6 @@ public class Helper {
}
};
// 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 boolean hasPermission(Context context, String name) {
return (ContextCompat.checkSelfPermission(context, name) == PackageManager.PERMISSION_GRANTED);
}
@ -825,236 +771,6 @@ public class Helper {
return filename.substring(index + 1);
}
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 rlah = prefs.getBoolean("rlah", true);
boolean roaming = prefs.getBoolean("roaming", 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.getActiveNetworkInfo();
if (ani != null)
state.roaming = ani.isRoaming();
} else {
Network active = 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.getActiveNetworkInfo();
if (ani == null || !ani.isConnected())
return null;
return cm.isActiveNetworkMetered();
}
Network active = 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();
if (networks != null)
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, AuthenticatorException, OperationCanceledException, IOException {
try {
istore.connect(account.host, account.port, account.user, account.password);
} catch (AuthenticationFailedException ex) {
if (account.auth_type == AUTH_TYPE_GMAIL) {
account.password = refreshToken(context, "com.google", account.user, account.password);
DB.getInstance(context).account().setAccountPassword(account.id, account.password);
istore.connect(account.host, account.port, account.user, account.password);
} else
throw ex;
}
// https://www.ietf.org/rfc/rfc2971.txt
if (istore.hasCapability("ID"))
try {
Map<String, String> id = new LinkedHashMap<>();
id.put("name", context.getString(R.string.app_name));
id.put("version", BuildConfig.VERSION_NAME);
Map<String, String> sid = istore.id(id);
if (sid != null) {
Map<String, String> 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 String refreshToken(Context context, String type, String name, String current)
throws AuthenticatorException, OperationCanceledException, IOException {
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(type);
for (Account account : accounts)
if (name.equals(account.name)) {
Log.i("Refreshing token");
am.invalidateAuthToken(type, current);
String refreshed = am.blockingGetAuthToken(account, getAuthTokenType(type), true);
if (refreshed == null)
throw new OperationCanceledException("no token");
Log.i("Refreshed token");
return refreshed;
}
return current;
}
static String getAuthTokenType(String type) {
if ("com.google".equals(type))
return "oauth2:https://mail.google.com/";
return null;
}
static boolean isPlayStoreInstall(Context context) {
return BuildConfig.PLAY_STORE_RELEASE;
}

View File

@ -190,7 +190,7 @@ public class MessageHelper {
// https://javaee.github.io/javamail/OAuth2
Log.i("Auth type=" + auth_type);
if (auth_type == Helper.AUTH_TYPE_GMAIL) {
if (auth_type == ConnectionHelper.AUTH_TYPE_GMAIL) {
props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
props.put("mail.imap.auth.mechanisms", "XOAUTH2");
props.put("mail.smtps.auth.mechanisms", "XOAUTH2");

View File

@ -161,7 +161,7 @@ public class ServiceSend extends LifecycleService {
}
private void check() {
if (!Helper.getNetworkState(ServiceSend.this).isSuitable())
if (!ConnectionHelper.getNetworkState(ServiceSend.this).isSuitable())
return;
if (thread != null && thread.isAlive())
@ -230,7 +230,7 @@ public class ServiceSend extends LifecycleService {
Log.i(outbox.name + " end op=" + op.id + "/" + op.name);
}
if (!Helper.getNetworkState(ServiceSend.this).isSuitable())
if (!ConnectionHelper.getNetworkState(ServiceSend.this).isSuitable())
break;
}
@ -326,9 +326,9 @@ public class ServiceSend extends LifecycleService {
try {
itransport.connect(ident.host, ident.port, ident.user, ident.password);
} catch (AuthenticationFailedException ex) {
if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) {
if (ident.auth_type == ConnectionHelper.AUTH_TYPE_GMAIL) {
EntityAccount account = db.account().getAccount(ident.account);
ident.password = Helper.refreshToken(this, "com.google", ident.user, account.password);
ident.password = ConnectionHelper.refreshToken(this, "com.google", ident.user, account.password);
DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password);
itransport.connect(ident.host, ident.port, ident.user, ident.password);
} else

View File

@ -89,7 +89,7 @@ import me.leolin.shortcutbadger.ShortcutBadger;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
public class ServiceSynchronize extends LifecycleService {
private Helper.NetworkState networkState = new Helper.NetworkState();
private ConnectionHelper.NetworkState networkState = new ConnectionHelper.NetworkState();
private Core.State state;
private boolean oneshot = false;
private boolean started = false;
@ -641,7 +641,7 @@ public class ServiceSynchronize extends LifecycleService {
db.account().setAccountState(account.id, "connecting");
try {
Helper.connect(this, istore, account);
ConnectionHelper.connect(this, istore, account);
} catch (Throwable ex) {
// Report account connection error
if (account.last_connected != null) {
@ -1160,7 +1160,7 @@ public class ServiceSynchronize extends LifecycleService {
ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
networkState.update(Helper.getNetworkState(ServiceSynchronize.this));
networkState.update(ConnectionHelper.getNetworkState(ServiceSynchronize.this));
synchronized (ServiceSynchronize.this) {
try {
@ -1215,7 +1215,7 @@ public class ServiceSynchronize extends LifecycleService {
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
networkState.update(Helper.getNetworkState(ServiceSynchronize.this));
networkState.update(ConnectionHelper.getNetworkState(ServiceSynchronize.this));
synchronized (ServiceSynchronize.this) {
try {
@ -1232,7 +1232,7 @@ public class ServiceSynchronize extends LifecycleService {
@Override
public void onLost(Network network) {
networkState.update(Helper.getNetworkState(ServiceSynchronize.this));
networkState.update(ConnectionHelper.getNetworkState(ServiceSynchronize.this));
synchronized (ServiceSynchronize.this) {
try {