summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap/bitmaskclient
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java42
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java20
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipFragment.java94
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/MainActivity.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java35
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java30
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderManager.java53
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/StartActivity.java9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java96
14 files changed, 306 insertions, 144 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index ec9678d8..aaff9ebc 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -29,6 +29,8 @@ import org.spongycastle.util.encoders.Base64;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -49,9 +51,11 @@ import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import static android.R.attr.name;
import static se.leap.bitmaskclient.Constants.ALWAYS_ON_SHOW_DIALOG;
@@ -79,7 +83,7 @@ public class ConfigHelper {
public static boolean checkErroneousDownload(String downloadedString) {
try {
- if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS)) {
+ if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS) || new JSONObject(downloadedString).has(ProviderAPI.BACKEND_ERROR_KEY)) {
return true;
} else {
return false;
@@ -130,36 +134,14 @@ public class ConfigHelper {
return (X509Certificate) certificate;
}
+ public static String loadInputStreamAsString(java.io.InputStream is) {
+ java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
- public static String loadInputStreamAsString(InputStream inputStream) {
- BufferedReader in = null;
- try {
- StringBuilder buf = new StringBuilder();
- in = new BufferedReader(new InputStreamReader(inputStream));
-
- String str;
- boolean isFirst = true;
- while ( (str = in.readLine()) != null ) {
- if (isFirst)
- isFirst = false;
- else
- buf.append('\n');
- buf.append(str);
- }
- return buf.toString();
- } catch (IOException e) {
- Log.e(TAG, "Error opening asset " + name);
- } finally {
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- Log.e(TAG, "Error closing asset " + name);
- }
- }
- }
-
- return null;
+ //allows us to mock FileInputStream
+ public static InputStream getInputStreamFrom(String filePath) throws FileNotFoundException {
+ return new FileInputStream(filePath);
}
protected static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java
index ea328216..f0e2de85 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java
@@ -1,16 +1,19 @@
package se.leap.bitmaskclient;
import android.content.SharedPreferences;
+import android.graphics.PorterDuff;
+import android.os.Build;
import android.os.Bundle;
-import android.os.PersistableBundle;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
+import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatImageView;
import android.support.v7.widget.AppCompatTextView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
+import android.widget.ProgressBar;
import butterknife.InjectView;
@@ -38,6 +41,9 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
@InjectView(R.id.loading_screen)
protected LinearLayout loadingScreen;
+ @InjectView(R.id.progressbar)
+ protected ProgressBar progressBar;
+
@InjectView(R.id.progressbar_description)
protected AppCompatTextView progressbarText;
@@ -59,6 +65,7 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
super.setContentView(view);
if (provider != null)
setProviderHeaderText(provider.getName());
+ setProgressbarColorForPreLollipop();
}
@Override
@@ -66,6 +73,7 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
super.setContentView(layoutResID);
if (provider != null)
setProviderHeaderText(provider.getName());
+ setProgressbarColorForPreLollipop();
}
@Override
@@ -73,8 +81,18 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
super.setContentView(view, params);
if (provider != null)
setProviderHeaderText(provider.getName());
+ setProgressbarColorForPreLollipop();
}
+ private void setProgressbarColorForPreLollipop() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ progressBar.getIndeterminateDrawable().setColorFilter(
+ ContextCompat.getColor(this, R.color.colorPrimary),
+ PorterDuff.Mode.SRC_IN);
+ }
+ }
+
+
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
diff --git a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java
index 7b84657c..34120859 100644
--- a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java
@@ -29,8 +29,10 @@ import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
+import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.AppCompatImageView;
+import android.support.v7.widget.AppCompatTextView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -44,30 +46,35 @@ import java.util.Observer;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
+import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.IOpenVPNServiceInternal;
import de.blinkt.openvpn.core.OpenVPNService;
+import de.blinkt.openvpn.core.ProfileManager;
import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.eip.EipStatus;
-import se.leap.bitmaskclient.eip.VoidVpnService;
import se.leap.bitmaskclient.views.VpnStateImage;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK;
+import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT;
+import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE;
+import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP;
+import static se.leap.bitmaskclient.Constants.EIP_REQUEST;
import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT;
import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.Constants.REQUEST_CODE_LOG_IN;
import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.ProviderCredentialsBaseActivity.USER_MESSAGE;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
public class EipFragment extends Fragment implements Observer {
public final static String TAG = EipFragment.class.getSimpleName();
- public static final String START_EIP_ON_BOOT = "start on boot";
public static final String ASK_TO_CANCEL_VPN = "ask_to_cancel_vpn";
@@ -84,10 +91,10 @@ public class EipFragment extends Fragment implements Observer {
Button mainButton;
@InjectView(R.id.routed_text)
- TextView routedText;
+ AppCompatTextView routedText;
@InjectView(R.id.vpn_route)
- TextView vpnRoute;
+ AppCompatTextView vpnRoute;
private EipStatus eipStatus;
@@ -100,23 +107,7 @@ public class EipFragment extends Fragment implements Observer {
AlertDialog alertDialog;
private IOpenVPNServiceInternal mService;
- private ServiceConnection openVpnConnection = new ServiceConnection() {
-
-
-
- @Override
- public void onServiceConnected(ComponentName className,
- IBinder service) {
-
- mService = IOpenVPNServiceInternal.Stub.asInterface(service);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName arg0) {
- mService = null;
- }
-
- };
+ private ServiceConnection openVpnConnection;
@Override
public void onAttach(Context context) {
@@ -141,6 +132,7 @@ public class EipFragment extends Fragment implements Observer {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ openVpnConnection = new EipFragmentServiceConnection();
eipStatus = EipStatus.getInstance();
Activity activity = getActivity();
if (activity != null) {
@@ -171,8 +163,8 @@ public class EipFragment extends Fragment implements Observer {
super.onResume();
//FIXME: avoid race conditions while checking certificate an logging in at about the same time
//eipCommand(Constants.EIP_ACTION_CHECK_CERT_VALIDITY);
- handleNewState();
bindOpenVpnService();
+ handleNewState();
}
@Override
@@ -230,7 +222,7 @@ public class EipFragment extends Fragment implements Observer {
}
void handleIcon() {
- if (eipStatus.isConnected() || eipStatus.isConnecting())
+ if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting())
handleSwitchOff();
else
handleSwitchOn();
@@ -266,7 +258,7 @@ public class EipFragment extends Fragment implements Observer {
}
private void handleSwitchOff() {
- if (eipStatus.isConnecting()) {
+ if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting()) {
askPendingStartCancellation();
} else if (eipStatus.isConnected()) {
askToStopEIP();
@@ -290,7 +282,20 @@ public class EipFragment extends Fragment implements Observer {
protected void stopEipIfPossible() {
Context context = getContext();
if (context != null) {
- EipCommand.stopVPN(getContext());
+ if (isOpenVpnRunningWithoutNetwork()) {
+ // TODO move to EIP
+ // TODO see stopEIP function
+ Bundle resultData = new Bundle();
+ resultData.putString(EIP_REQUEST, EIP_ACTION_STOP);
+ Intent intentUpdate = new Intent(BROADCAST_EIP_EVENT);
+ intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
+ intentUpdate.putExtra(BROADCAST_RESULT_CODE, Activity.RESULT_OK);
+ intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData);
+ Log.d(TAG, "sending broadcast");
+ LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intentUpdate);
+ } else {
+ EipCommand.stopVPN(getContext());
+ }
} else {
Log.e(TAG, "context is null when trying to stop EIP");
}
@@ -386,14 +391,24 @@ public class EipFragment extends Fragment implements Observer {
routedText.setVisibility(GONE);
vpnRoute.setVisibility(GONE);
colorBackgroundALittle();
- } else if (eipStatus.isConnected() || isOpenVpnRunningWithoutNetwork()) {
+ } else if (eipStatus.isConnected() ) {
mainButton.setText(activity.getString(R.string.vpn_button_turn_off));
vpnStateImage.setStateIcon(R.drawable.vpn_connected);
vpnStateImage.stopProgress(true);
+ routedText.setText(R.string.vpn_securely_routed);
routedText.setVisibility(VISIBLE);
vpnRoute.setVisibility(VISIBLE);
- vpnRoute.setText(ConfigHelper.getProviderName(preferences));
+ setVpnRouteText();
colorBackground();
+ } else if(isOpenVpnRunningWithoutNetwork()){
+ mainButton.setText(activity.getString(R.string.vpn_button_turn_off));
+ vpnStateImage.setStateIcon(R.drawable.vpn_disconnected);
+ vpnStateImage.stopProgress(true);
+ routedText.setText(R.string.vpn_securely_routed_no_internet);
+ routedText.setVisibility(VISIBLE);
+ vpnRoute.setVisibility(VISIBLE);
+ setVpnRouteText();
+ colorBackgroundALittle();
} else {
mainButton.setText(activity.getString(R.string.vpn_button_turn_on));
vpnStateImage.setStateIcon(R.drawable.vpn_disconnected);
@@ -465,4 +480,27 @@ public class EipFragment extends Fragment implements Observer {
activity.startActivityForResult(intent, REQUEST_CODE_LOG_IN);
}
}
+
+ private void setVpnRouteText() {
+ String vpnRouteString = provider.getName();
+ VpnProfile vpnProfile = ProfileManager.getLastConnectedVpn();
+ if (vpnProfile != null && vpnProfile.mName != null) {
+ vpnRouteString += " (" + vpnProfile.mName + ")";
+ }
+ vpnRoute.setText(vpnRouteString);
+ }
+
+ private class EipFragmentServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName className,
+ IBinder service) {
+ mService = IOpenVPNServiceInternal.Stub.asInterface(service);
+ handleNewState();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ mService = null;
+ }
+ }
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java
index 952f2d1f..19294618 100644
--- a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java
@@ -73,7 +73,7 @@ import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFI
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.ProviderCredentialsBaseActivity.USER_MESSAGE;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed;
import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
@@ -82,12 +82,7 @@ public class MainActivity extends AppCompatActivity implements Observer {
public final static String TAG = MainActivity.class.getSimpleName();
- private static final String KEY_ACTIVITY_STATE = "key state of activity";
- private static final String DEFAULT_UI_STATE = "default state";
- private static final String SHOW_DIALOG_STATE = "show dialog";
- private static final String REASON_TO_FAIL = "reason to fail";
-
- private static Provider provider = new Provider();
+ private Provider provider = new Provider();
private SharedPreferences preferences;
private EipStatus eipStatus;
private NavigationDrawerFragment navigationDrawerFragment;
@@ -213,7 +208,7 @@ public class MainActivity extends AppCompatActivity implements Observer {
break;
}
}
-
+ //TODO: Why do we want this --v? legacy and redundant?
Fragment fragment = new EipFragment();
Bundle arguments = new Bundle();
arguments.putParcelable(PROVIDER_KEY, provider);
diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index 7104143c..fd067bf9 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -90,9 +90,8 @@ public final class Provider implements Parcelable {
}
if (definition != null) {
try {
- this.definition = new JSONObject(definition);
- parseDefinition(this.definition);
- } catch (JSONException | NullPointerException e) {
+ define(new JSONObject(definition));
+ } catch (JSONException e) {
e.printStackTrace();
}
}
@@ -133,26 +132,8 @@ public final class Provider implements Parcelable {
}
public boolean define(JSONObject providerJson) {
- /*
- * fix against "api_uri": "https://calyx.net.malicious.url.net:4430",
- * This method aims to prevent attacks where the provider.json file got manipulated by a third party.
- * The main url should not change.
- */
-
- try {
- String providerApiUrl = providerJson.getString(Provider.API_URL);
- String providerDomain = providerJson.getString(Provider.DOMAIN);
- if (getMainUrlString().contains(providerDomain) && providerApiUrl.contains(providerDomain + ":")) {
- definition = providerJson;
- parseDefinition(definition);
- return true;
- } else {
- return false;
- }
- } catch (JSONException e) {
- e.printStackTrace();
- return false;
- }
+ definition = providerJson;
+ return parseDefinition(definition);
}
public JSONObject getDefinition() {
@@ -297,8 +278,6 @@ public final class Provider implements Parcelable {
try {
json.put(Provider.MAIN_URL, mainUrl);
//TODO: add other fields here?
- //this is used to save custom providers as json. I guess this doesn't work correctly
- //TODO 2: verify that
} catch (JSONException e) {
e.printStackTrace();
}
@@ -345,7 +324,7 @@ public final class Provider implements Parcelable {
}
}
- private void parseDefinition(JSONObject definition) {
+ private boolean parseDefinition(JSONObject definition) {
try {
String pin = definition.getString(CA_CERT_FINGERPRINT);
this.certificatePin = pin.split(":")[1].trim();
@@ -354,8 +333,9 @@ public final class Provider implements Parcelable {
this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS);
this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED);
this.apiVersion = getDefinition().getString(Provider.API_VERSION);
+ return true;
} catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
- e.printStackTrace();
+ return false;
}
}
@@ -446,5 +426,4 @@ public final class Provider implements Parcelable {
allowRegistered = false;
allowAnonymous = false;
}
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
index f5efde05..f1f474d7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -51,6 +51,7 @@ public class ProviderAPI extends IntentService implements ProviderApiManagerBase
ERRORID = "errorId",
BACKEND_ERROR_KEY = "error",
BACKEND_ERROR_MESSAGE = "message",
+ USER_MESSAGE = "userMessage",
DOWNLOAD_SERVICE_JSON = "ProviderAPI.DOWNLOAD_SERVICE_JSON",
PROVIDER_SET_UP = "ProviderAPI.PROVIDER_SET_UP";
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index b93abaeb..2cde431e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
@@ -46,6 +46,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
+import java.util.NoSuchElementException;
import javax.net.ssl.SSLHandshakeException;
@@ -63,19 +64,16 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_KEY;
import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_MESSAGE;
-import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
-import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
-import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
-import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
-import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_SERVICE_JSON;
+import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.FAILED_LOGIN;
import static se.leap.bitmaskclient.ProviderAPI.FAILED_SIGNUP;
-import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.LOGOUT_FAILED;
import static se.leap.bitmaskclient.ProviderAPI.LOG_IN;
import static se.leap.bitmaskclient.ProviderAPI.LOG_OUT;
@@ -90,15 +88,18 @@ import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGIN;
import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGOUT;
import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_SIGNUP;
import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
+import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
import static se.leap.bitmaskclient.R.string.certificate_error;
-import static se.leap.bitmaskclient.R.string.switch_provider_menu_option;
-import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
import static se.leap.bitmaskclient.R.string.error_json_exception_user_message;
import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
import static se.leap.bitmaskclient.R.string.malformed_url;
import static se.leap.bitmaskclient.R.string.server_unreachable_message;
import static se.leap.bitmaskclient.R.string.service_is_down_error;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
@@ -290,7 +291,7 @@ public abstract class ProviderApiManagerBase {
JSONObject stepResult = null;
OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
if (okHttpClient == null) {
- return authFailedNotification(stepResult, username);
+ return backendErrorNotification(stepResult, username);
}
LeapSRPSession client = new LeapSRPSession(username, password);
@@ -302,7 +303,7 @@ public abstract class ProviderApiManagerBase {
Bundle result = new Bundle();
if (api_result.has(ERRORS) || api_result.has(BACKEND_ERROR_KEY))
- result = authFailedNotification(api_result, username);
+ result = backendErrorNotification(api_result, username);
else {
result.putString(CREDENTIALS_USERNAME, username);
result.putString(CREDENTIALS_PASSWORD, password);
@@ -349,7 +350,7 @@ public abstract class ProviderApiManagerBase {
OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
if (okHttpClient == null) {
- return authFailedNotification(stepResult, username);
+ return backendErrorNotification(stepResult, username);
}
LeapSRPSession client = new LeapSRPSession(username, password);
@@ -367,15 +368,15 @@ public abstract class ProviderApiManagerBase {
if (client.verify(M2)) {
result.putBoolean(BROADCAST_RESULT_KEY, true);
} else {
- authFailedNotification(step_result, username);
+ backendErrorNotification(step_result, username);
}
} else {
result.putBoolean(BROADCAST_RESULT_KEY, false);
result.putString(CREDENTIALS_USERNAME, username);
- result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message));
+ result.putString(USER_MESSAGE, resources.getString(R.string.error_srp_math_error_user_message));
}
} catch (JSONException e) {
- result = authFailedNotification(step_result, username);
+ result = backendErrorNotification(step_result, username);
e.printStackTrace();
}
@@ -391,7 +392,7 @@ public abstract class ProviderApiManagerBase {
return true;
}
- private Bundle authFailedNotification(JSONObject result, String username) {
+ private Bundle backendErrorNotification(JSONObject result, String username) {
Bundle userNotificationBundle = new Bundle();
if (result.has(ERRORS)) {
Object baseErrorMessage = result.opt(ERRORS);
@@ -400,14 +401,14 @@ public abstract class ProviderApiManagerBase {
JSONObject errorMessage = result.getJSONObject(ERRORS);
String errorType = errorMessage.keys().next().toString();
String message = errorMessage.get(errorType).toString();
- userNotificationBundle.putString(resources.getString(R.string.user_message), message);
- } catch (JSONException e) {
+ userNotificationBundle.putString(USER_MESSAGE, message);
+ } catch (JSONException | NoSuchElementException | NullPointerException e) {
e.printStackTrace();
}
} else if (baseErrorMessage instanceof String) {
try {
String errorMessage = result.getString(ERRORS);
- userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage);
+ userNotificationBundle.putString(USER_MESSAGE, errorMessage);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -418,7 +419,7 @@ public abstract class ProviderApiManagerBase {
if (result.has(BACKEND_ERROR_MESSAGE)) {
backendErrorMessage = resources.getString(R.string.error) + result.getString(BACKEND_ERROR_MESSAGE);
}
- userNotificationBundle.putString(resources.getString(R.string.user_message), backendErrorMessage);
+ userNotificationBundle.putString(USER_MESSAGE, backendErrorMessage);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -431,7 +432,7 @@ public abstract class ProviderApiManagerBase {
return userNotificationBundle;
}
- void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
+ private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
if (resultData == null || resultData == Bundle.EMPTY) {
resultData = new Bundle();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java
index d41be512..15cd9617 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java
@@ -30,6 +30,7 @@ import org.json.JSONException;
import butterknife.InjectView;
import butterknife.OnClick;
import se.leap.bitmaskclient.Constants.CREDENTIAL_ERRORS;
+import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.userstatus.User;
import static android.view.View.GONE;
@@ -41,9 +42,12 @@ import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY;
import static se.leap.bitmaskclient.Constants.CREDENTIALS_PASSWORD;
import static se.leap.bitmaskclient.Constants.CREDENTIALS_USERNAME;
import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_KEY;
import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.LOG_IN;
import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
/**
* Base Activity for activities concerning a provider interaction
@@ -59,7 +63,6 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
final private static String SHOWING_FORM = "SHOWING_FORM";
final private static String PERFORMING_ACTION = "PERFORMING_ACTION";
- final public static String USER_MESSAGE = "USER_MESSAGE";
final private static String USERNAME_ERROR = "USERNAME_ERROR";
final private static String PASSWORD_ERROR = "PASSWORD_ERROR";
final private static String PASSWORD_VERIFICATION_ERROR = "PASSWORD_VERIFICATION_ERROR";
@@ -183,7 +186,11 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
String username = usernameField.getText().toString();
String providerDomain = provider.getDomain();
if (username.endsWith(providerDomain)) {
- return username.split("@" + providerDomain)[0];
+ try {
+ return username.split("@" + providerDomain)[0];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return "";
+ }
}
return username;
}
@@ -237,9 +244,15 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
@Override
public void afterTextChanged(Editable s) {
if (getUsername().equalsIgnoreCase("")) {
+ s.clear();
usernameError.setError(getString(R.string.username_ask));
} else {
usernameError.setError(null);
+ String suffix = "@" + provider.getDomain();
+ if (!usernameField.getText().toString().endsWith(suffix)) {
+ s.append(suffix);
+ usernameField.setSelection(usernameField.getText().toString().indexOf('@'));
+ }
}
}
});
@@ -344,8 +357,8 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
if (arguments.containsKey(CREDENTIAL_ERRORS.USERNAME_MISSING.toString())) {
usernameError.setError(getString(R.string.username_ask));
}
- if (arguments.containsKey(getString(R.string.user_message))) {
- String userMessageString = arguments.getString(getString(R.string.user_message));
+ if (arguments.containsKey(USER_MESSAGE)) {
+ String userMessageString = arguments.getString(USER_MESSAGE);
try {
userMessageString = new JSONArray(userMessageString).getString(0);
} catch (JSONException e) {
@@ -395,6 +408,10 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
switch (resultCode) {
case ProviderAPI.SUCCESSFUL_SIGNUP:
+ String password = resultData.getString(CREDENTIALS_PASSWORD);
+ String username = resultData.getString(CREDENTIALS_USERNAME);
+ login(username, password);
+ break;
case ProviderAPI.SUCCESSFUL_LOGIN:
downloadVpnCertificate(handledProvider);
break;
@@ -403,12 +420,11 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
handleReceivedErrors((Bundle) intent.getParcelableExtra(BROADCAST_RESULT_KEY));
break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ // error handling takes place in MainActivity
case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
successfullyFinished(handledProvider);
break;
- case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
- // TODO activity.setResult(RESULT_CANCELED);
- break;
}
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java
index e961b0a2..75fffaf7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java
@@ -207,7 +207,8 @@ public abstract class ProviderListBaseActivity extends ConfigWizardBaseActivity
void handleProviderSetUp(Provider handledProvider) {
this.provider = handledProvider;
-
+ adapter.add(provider);
+ adapter.saveProviders();
if (provider.allowsAnonymous()) {
mConfigState.putExtra(SERVICES_RETRIEVED, true);
downloadVpnCertificate();
@@ -368,12 +369,6 @@ public abstract class ProviderListBaseActivity extends ConfigWizardBaseActivity
}
}
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu);
- return true;
- }
-
public class ProviderAPIBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
index ed41be67..97ba3b98 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
@@ -31,6 +31,8 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private File externalFilesDir;
private Set<Provider> defaultProviders;
private Set<Provider> customProviders;
+ private Set<URL> defaultProviderURLs;
+ private Set<URL> customProviderURLs;
private static ProviderManager instance;
@@ -52,11 +54,20 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private void addDefaultProviders(AssetManager assets_manager) {
try {
defaultProviders = providersFromAssets(URLS, assets_manager.list(URLS));
+ defaultProviderURLs = getProviderUrlSetFromProviderSet(defaultProviders);
} catch (IOException e) {
e.printStackTrace();
}
}
+ private Set<URL> getProviderUrlSetFromProviderSet(Set<Provider> providers) {
+ HashSet<URL> providerUrls = new HashSet<>();
+ for (Provider provider : providers) {
+ providerUrls.add(provider.getMainUrl().getUrl());
+ }
+ return providerUrls;
+ }
+
private Set<Provider> providersFromAssets(String directory, String[] relativeFilePaths) {
Set<Provider> providers = new HashSet<>();
@@ -89,13 +100,14 @@ public class ProviderManager implements AdapteeCollection<Provider> {
customProviders = externalFilesDir != null && externalFilesDir.isDirectory() ?
providersFromFiles(externalFilesDir.list()) :
new HashSet<Provider>();
+ customProviderURLs = getProviderUrlSetFromProviderSet(customProviders);
}
private Set<Provider> providersFromFiles(String[] files) {
Set<Provider> providers = new HashSet<>();
try {
for (String file : files) {
- String mainUrl = extractMainUrlFromInputStream(new FileInputStream(externalFilesDir.getAbsolutePath() + "/" + file));
+ String mainUrl = extractMainUrlFromInputStream(ConfigHelper.getInputStreamFrom(externalFilesDir.getAbsolutePath() + "/" + file));
providers.add(new Provider(new URL(mainUrl)));
}
} catch (MalformedURLException | FileNotFoundException e) {
@@ -132,6 +144,8 @@ public class ProviderManager implements AdapteeCollection<Provider> {
allProviders.addAll(defaultProviders);
if(customProviders != null)
allProviders.addAll(customProviders);
+ //add an option to add a custom provider
+ //TODO: refactor me?
allProviders.add(new Provider());
return allProviders;
}
@@ -153,32 +167,59 @@ public class ProviderManager implements AdapteeCollection<Provider> {
@Override
public boolean add(Provider element) {
- return !defaultProviders.contains(element) || customProviders.add(element);
+ return element != null &&
+ !defaultProviderURLs.contains(element.getMainUrl().getUrl()) &&
+ customProviders.add(element) &&
+ customProviderURLs.add(element.getMainUrl().getUrl());
}
@Override
public boolean remove(Object element) {
- return customProviders.remove(element);
+ return element instanceof Provider &&
+ customProviders.remove(element) &&
+ customProviderURLs.remove(((Provider) element).getMainUrl().getUrl());
}
@Override
public boolean addAll(Collection<? extends Provider> elements) {
- return customProviders.addAll(elements);
+ Iterator iterator = elements.iterator();
+ boolean addedAll = true;
+ while (iterator.hasNext()) {
+ Provider p = (Provider) iterator.next();
+ addedAll = customProviders.add(p) &&
+ customProviderURLs.add(p.getMainUrl().getUrl()) &&
+ addedAll;
+ }
+ return addedAll;
}
@Override
public boolean removeAll(Collection<?> elements) {
- if(!elements.getClass().equals(Provider.class))
+ Iterator iterator = elements.iterator();
+ boolean removedAll = true;
+ try {
+ while (iterator.hasNext()) {
+ Provider p = (Provider) iterator.next();
+ removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().getUrl())) ||
+ (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().getUrl()))) &&
+ removedAll;
+ }
+ } catch (ClassCastException e) {
return false;
- return defaultProviders.removeAll(elements) || customProviders.removeAll(elements);
+ }
+
+ return removedAll;
}
@Override
public void clear() {
defaultProviders.clear();
customProviders.clear();
+ customProviderURLs.clear();
+ defaultProviderURLs.clear();
}
+ //FIXME: removed custom providers should be deleted here as well
void saveCustomProvidersToFile() {
try {
for (Provider provider : customProviders) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
index 39717bd8..6bbdeb4f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
@@ -13,12 +13,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import de.blinkt.openvpn.core.VpnStatus;
-import se.leap.bitmaskclient.eip.EIP;
import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.userstatus.User;
import static se.leap.bitmaskclient.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE;
-import static se.leap.bitmaskclient.Constants.EIP_ACTION_START;
import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT;
import static se.leap.bitmaskclient.Constants.PREFERENCES_APP_VERSION;
import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION;
@@ -32,7 +30,7 @@ import static se.leap.bitmaskclient.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
* and acts and calls another activity accordingly.
*
*/
-public class StartActivity extends Activity {
+public class StartActivity extends Activity{
public static final String TAG = StartActivity.class.getSimpleName();
@Retention(RetentionPolicy.SOURCE)
@@ -184,12 +182,9 @@ public class StartActivity extends Activity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (data == null) {
- return;
- }
if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) {
- if (resultCode == RESULT_OK && data.hasExtra(Provider.KEY)) {
+ if (resultCode == RESULT_OK && data != null && data.hasExtra(Provider.KEY)) {
Provider provider = data.getParcelableExtra(Provider.KEY);
ConfigHelper.storeProviderInPreferences(preferences, provider);
EipCommand.startVPN(this, false);
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
index 5cf180d3..665e0ebd 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -184,6 +184,7 @@ public final class EIP extends IntentService {
private void stopEIP() {
// TODO stop eip from here if possible...
+ // TODO then refactor EipFragment.handleSwitchOff
EipStatus eipStatus = EipStatus.getInstance();
int resultCode = RESULT_CANCELED;
if (eipStatus.isConnected() || eipStatus.isConnecting())
diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java b/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java
index 80510b86..4e8e3d79 100644
--- a/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java
@@ -15,6 +15,7 @@ import android.widget.CheckBox;
import butterknife.ButterKnife;
import butterknife.InjectView;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.views.IconTextView;
import static se.leap.bitmaskclient.ConfigHelper.saveShowAlwaysOnDialog;
@@ -31,6 +32,9 @@ public class AlwaysOnDialog extends AppCompatDialogFragment {
@InjectView(R.id.do_not_show_again)
CheckBox doNotShowAgainCheckBox;
+ @InjectView(R.id.user_message)
+ IconTextView userMessage;
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -44,8 +48,9 @@ public class AlwaysOnDialog extends AppCompatDialogFragment {
View view = inflater.inflate(R.layout.checkbox_confirm_dialog, null);
ButterKnife.inject(this, view);
+ userMessage.setIcon(R.drawable.ic_settings);
+ userMessage.setText(getString(R.string.always_on_vpn_user_message));
builder.setView(view)
- .setMessage(R.string.always_on_vpn_user_message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
if (doNotShowAgainCheckBox.isChecked()) {
@@ -61,7 +66,6 @@ public class AlwaysOnDialog extends AppCompatDialogFragment {
dialog.cancel();
}
});
- // Create the AlertDialog object and return it
return builder.create();
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java b/app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java
new file mode 100644
index 00000000..0af33c68
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java
@@ -0,0 +1,96 @@
+package se.leap.bitmaskclient.views;
+
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.AppCompatTextView;
+import android.text.Spannable;
+import android.text.style.ImageSpan;
+import android.util.AttributeSet;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IconTextView extends AppCompatTextView {
+
+ private int imageResource = 0;
+ /**
+ * Regex pattern that looks for embedded images of the format: [img src=imageName/]
+ */
+ public static final String PATTERN = "\\Q[img src]\\E";
+
+ public IconTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public IconTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public IconTextView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setText(CharSequence text, BufferType type) {
+ final Spannable spannable = getTextWithImages(getContext(), text, getLineHeight(), getCurrentTextColor());
+ super.setText(spannable, BufferType.SPANNABLE);
+ }
+
+ public void setIcon(int imageResource) {
+ this.imageResource = imageResource;
+ }
+
+ private Spannable getTextWithImages(Context context, CharSequence text, int lineHeight, int colour) {
+ final Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
+ addImages(context, spannable, lineHeight, colour);
+ return spannable;
+ }
+
+ private void addImages(Context context, Spannable spannable, int lineHeight, int colour) {
+ final Pattern refImg = Pattern.compile(PATTERN);
+
+ final Matcher matcher = refImg.matcher(spannable);
+ while (matcher.find()) {
+ boolean set = true;
+ for (ImageSpan span : spannable.getSpans(matcher.start(), matcher.end(), ImageSpan.class)) {
+ if (spannable.getSpanStart(span) >= matcher.start()
+ && spannable.getSpanEnd(span) <= matcher.end()) {
+ spannable.removeSpan(span);
+ } else {
+ set = false;
+ break;
+ }
+ }
+ if (set && imageResource != 0) {
+ spannable.setSpan(makeImageSpan(context, imageResource, lineHeight, colour),
+ matcher.start(),
+ matcher.end(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ }
+ }
+ }
+
+ /**
+ * Create an ImageSpan for the given icon drawable. This also sets the image size and colour.
+ * Works best with a white, square icon because of the colouring and resizing.
+ *
+ * @param context The Android Context.
+ * @param drawableResId A drawable resource Id.
+ * @param size The desired size (i.e. width and height) of the image icon in pixels.
+ * Use the lineHeight of the TextView to make the image inline with the
+ * surrounding text.
+ * @param colour The colour (careful: NOT a resource Id) to apply to the image.
+ * @return An ImageSpan, aligned with the bottom of the text.
+ */
+ private ImageSpan makeImageSpan(Context context, int drawableResId, int size, int colour) {
+ final Drawable drawable = context.getResources().getDrawable(drawableResId);
+ drawable.mutate();
+ drawable.setColorFilter(colour, PorterDuff.Mode.MULTIPLY);
+ drawable.setBounds(0, 0, size, size);
+ return new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM);
+ }
+
+} \ No newline at end of file