summaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorcyBerta <cyberta@riseup.net>2019-10-01 00:25:54 +0200
committercyBerta <cyberta@riseup.net>2019-10-01 00:25:54 +0200
commitfca6c9dcff1b5b5400a61b7411a7f72460fbfbfa (patch)
treec8efd8b964d495467619000fa7435c2a5527efa1 /app/src/main
parent7820e8c0819a10c5b4729678607681fcfe30cbae (diff)
parent685da193ea29f3e7a8a42d55747dfb2f956f23b6 (diff)
Merge branch 'pluggableTransports2'
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/AndroidManifest.xml4
-rw-r--r--app/src/main/java/de/blinkt/openvpn/VpnProfile.java46
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java93
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java15
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java13
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java36
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java16
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java0
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java219
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java59
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Constants.java19
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java246
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java10
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/StartActivity.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java56
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java320
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java29
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java121
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java40
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java203
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java204
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java216
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java65
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java104
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java92
-rw-r--r--app/src/main/res/drawable-hdpi/ic_bridge_36.pngbin0 -> 821 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_bridge_36.pngbin0 -> 774 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_bridge_36.pngbin0 -> 1162 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_bridge_36.pngbin0 -> 1308 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_bridge_36.pngbin0 -> 1760 bytes
-rw-r--r--app/src/main/res/layout-xlarge/v_icon_text_list_item.xml33
-rw-r--r--app/src/main/res/layout-xlarge/v_switch_list_item.xml25
-rw-r--r--app/src/main/res/layout/f_drawer_main.xml156
-rw-r--r--app/src/main/res/layout/v_icon_text_list_item.xml33
-rw-r--r--app/src/main/res/layout/v_switch_list_item.xml26
-rw-r--r--app/src/main/res/values/attrs.xml16
-rw-r--r--app/src/main/res/values/strings.xml5
-rw-r--r--app/src/main/res/values/themes.xml4
45 files changed, 1920 insertions, 697 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a00582cc..9a2b1e43 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -62,10 +62,10 @@
<activity
android:name=".eip.VoidVpnLauncher"
- android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+ android:theme="@style/invisibleTheme" />
<activity
android:name="de.blinkt.openvpn.LaunchVPN"
- android:label="@string/vpn_launch_title" />
+ android:theme="@style/invisibleTheme" />
<activity
android:name=".StartActivity"
android:label="@string/app_name"
diff --git a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
index 7b9003aa..f139fdc9 100644
--- a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
+++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
@@ -53,7 +53,6 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
-import de.blinkt.openvpn.core.Connection;
import de.blinkt.openvpn.core.ExtAuthHelper;
import de.blinkt.openvpn.core.NativeUtils;
import de.blinkt.openvpn.core.OpenVPNService;
@@ -63,9 +62,13 @@ import de.blinkt.openvpn.core.Preferences;
import de.blinkt.openvpn.core.VPNLaunchHelper;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.X509Utils;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.OpenvpnConnection;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE;
public class VpnProfile implements Serializable, Cloneable {
@@ -116,7 +119,7 @@ public class VpnProfile implements Serializable, Cloneable {
public String mTLSAuthFilename;
public String mClientKeyFilename;
public String mCaFilename;
- public boolean mUseLzo = true;
+ public boolean mUseLzo = false;
public String mPKCS12Filename;
public String mPKCS12Password;
public boolean mUseTLSAuth = false;
@@ -171,6 +174,7 @@ public class VpnProfile implements Serializable, Cloneable {
// timestamp when the profile was last used
public long mLastUsed;
public String importedProfileHash;
+ //TODO: cleanup here
/* Options no longer used in new profiles */
public String mServerName = "openvpn.example.com";
public String mServerPort = "1194";
@@ -181,16 +185,17 @@ public class VpnProfile implements Serializable, Cloneable {
// set members to default values
private UUID mUuid;
private int mProfileVersion;
+ public String mGatewayIp;
+ public boolean mUsePluggableTransports;
-
- public VpnProfile(String name) {
+ public VpnProfile(String name, Connection.TransportType transportType) {
mUuid = UUID.randomUUID();
mName = name;
mProfileVersion = CURRENT_PROFILE_VERSION;
mConnections = new Connection[1];
- mConnections[0] = new Connection();
mLastUsed = System.currentTimeMillis();
+ mUsePluggableTransports = transportType == OBFS4;
}
public static String openVpnEscape(String unescaped) {
@@ -292,6 +297,7 @@ public class VpnProfile implements Serializable, Cloneable {
return mName;
}
+ @Deprecated
public void upgradeProfile() {
if (mProfileVersion < 2) {
/* default to the behaviour the OS used */
@@ -314,22 +320,23 @@ public class VpnProfile implements Serializable, Cloneable {
}
if (mProfileVersion < 7) {
for (Connection c : mConnections)
- if (c.mProxyType == null)
- c.mProxyType = Connection.ProxyType.NONE;
+ if (c.getProxyType() == null)
+ c.setProxyType(Connection.ProxyType.NONE);
}
mProfileVersion = CURRENT_PROFILE_VERSION;
}
+ @Deprecated
private void moveOptionsToConnection() {
mConnections = new Connection[1];
- Connection conn = new Connection();
+ Connection conn = mUsePluggableTransports ? new Obfs4Connection() : new OpenvpnConnection();
- conn.mServerName = mServerName;
- conn.mServerPort = mServerPort;
- conn.mUseUdp = mUseUdp;
- conn.mCustomConfiguration = "";
+ conn.setServerName(mServerName);
+ conn.setServerPort(mServerPort);
+ conn.setUseUdp(mUseUdp);
+ conn.setCustomConfiguration("");
mConnections[0] = conn;
@@ -425,7 +432,7 @@ public class VpnProfile implements Serializable, Cloneable {
if (canUsePlainRemotes) {
for (Connection conn : mConnections) {
- if (conn.mEnabled) {
+ if (conn.isEnabled()) {
cfg.append(conn.getConnectionBlock(configForOvpn3));
}
}
@@ -494,7 +501,8 @@ public class VpnProfile implements Serializable, Cloneable {
if (!TextUtils.isEmpty(mCrlFilename))
cfg.append(insertFileData("crl-verify", mCrlFilename));
- if (mUseLzo) {
+ // compression does not work in conjunction with shapeshifter-dispatcher so far
+ if (mUseLzo && !mUsePluggableTransports) {
cfg.append("comp-lzo\n");
}
@@ -586,7 +594,7 @@ public class VpnProfile implements Serializable, Cloneable {
if (mAuthenticationType != TYPE_STATICKEYS) {
if (mCheckRemoteCN) {
if (mRemoteCN == null || mRemoteCN.equals(""))
- cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].mServerName)).append(" name\n");
+ cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].getServerName())).append(" name\n");
else
switch (mX509AuthType) {
@@ -660,7 +668,7 @@ public class VpnProfile implements Serializable, Cloneable {
if (!canUsePlainRemotes) {
cfg.append("# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n");
for (Connection conn : mConnections) {
- if (conn.mEnabled) {
+ if (conn.isEnabled()) {
cfg.append("<connection>\n");
cfg.append(conn.getConnectionBlock(configForOvpn3));
cfg.append("</connection>\n");
@@ -985,7 +993,7 @@ public class VpnProfile implements Serializable, Cloneable {
boolean noRemoteEnabled = true;
for (Connection c : mConnections) {
- if (c.mEnabled)
+ if (c.isEnabled())
noRemoteEnabled = false;
}
@@ -1000,12 +1008,12 @@ public class VpnProfile implements Serializable, Cloneable {
return R.string.openvpn3_pkcs12;
}
for (Connection conn : mConnections) {
- if (conn.mProxyType == Connection.ProxyType.ORBOT || conn.mProxyType == Connection.ProxyType.SOCKS5)
+ if (conn.getProxyType() == Connection.ProxyType.ORBOT || conn.getProxyType() == Connection.ProxyType.SOCKS5)
return R.string.openvpn3_socksproxy;
}
}
for (Connection c : mConnections) {
- if (c.mProxyType == Connection.ProxyType.ORBOT) {
+ if (c.getProxyType() == Connection.ProxyType.ORBOT) {
if (usesExtraProxyOptions())
return R.string.error_orbot_and_proxy_options;
if (!OrbotHelper.checkTorReceier(context))
diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
index 0148bfb7..5ccd83dd 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
@@ -13,9 +13,21 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.OpenvpnConnection;
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
@@ -128,6 +140,7 @@ public class ConfigParser {
private HashMap<String, Vector<Vector<String>>> options = new HashMap<>();
private HashMap<String, Vector<String>> meta = new HashMap<String, Vector<String>>();
private String auth_user_pass_file;
+ private Obfs4Options obfs4Options;
static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) {
String data = VpnProfile.getEmbeddedContent(inlinedata);
@@ -142,9 +155,9 @@ public class ConfigParser {
String data = VpnProfile.getEmbeddedContent(inlinedata);
String[] parts = data.split("\n");
if (parts.length >= 2) {
- c.mProxyAuthUser = parts[0];
- c.mProxyAuthPassword = parts[1];
- c.mUseProxyAuth = true;
+ c.setProxyAuthUser(parts[0]);
+ c.setProxyAuthPassword(parts[1]);
+ c.setUseProxyAuth(true);
}
}
@@ -338,9 +351,9 @@ public class ConfigParser {
// This method is far too long
@SuppressWarnings("ConstantConditions")
- public VpnProfile convertProfile() throws ConfigParseError, IOException {
+ public VpnProfile convertProfile(Connection.TransportType transportType) throws ConfigParseError, IOException {
boolean noauthtypeset = true;
- VpnProfile np = new VpnProfile(CONVERTED_PROFILE);
+ VpnProfile np = new VpnProfile(CONVERTED_PROFILE, transportType);
// Pull, client, tls-client
np.clearDefaults();
@@ -443,6 +456,7 @@ public class ConfigParser {
if (redirectPrivate != null) {
checkRedirectParameters(np, redirectPrivate, false);
}
+
Vector<String> dev = getOption("dev", 1, 1);
Vector<String> devtype = getOption("dev-type", 1, 1);
@@ -468,7 +482,6 @@ public class ConfigParser {
}
}
-
Vector<String> tunmtu = getOption("tun-mtu", 1, 1);
if (tunmtu != null) {
@@ -479,14 +492,12 @@ public class ConfigParser {
}
}
-
Vector<String> mode = getOption("mode", 1, 1);
if (mode != null) {
if (!mode.get(1).equals("p2p"))
throw new ConfigParseError("Invalid mode for --mode specified, need p2p");
}
-
Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2);
if (dhcpoptions != null) {
for (Vector<String> dhcpoption : dhcpoptions) {
@@ -521,8 +532,10 @@ public class ConfigParser {
if (getOption("float", 0, 0) != null)
np.mUseFloat = true;
- if (getOption("comp-lzo", 0, 1) != null)
- np.mUseLzo = true;
+ Vector<String> useLzo = getOption("comp-lzo", 0, 1);
+ if (useLzo != null) {
+ np.mUseLzo = Boolean.valueOf(useLzo.get(1));
+ }
Vector<String> cipher = getOption("cipher", 1, 1);
if (cipher != null)
@@ -532,7 +545,6 @@ public class ConfigParser {
if (auth != null)
np.mAuth = auth.get(1);
-
Vector<String> ca = getOption("ca", 1, 1);
if (ca != null) {
np.mCaFilename = ca.get(1);
@@ -544,6 +556,7 @@ public class ConfigParser {
np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES;
noauthtypeset = false;
}
+
Vector<String> key = getOption("key", 1, 1);
if (key != null)
np.mClientKeyFilename = key.get(1);
@@ -604,8 +617,7 @@ public class ConfigParser {
np.mVerb = verb.get(1);
}
-
- if (getOption("nobind", 0, 0) != null)
+ if (getOption("nobind", 0, 1) != null)
np.mNobind = true;
if (getOption("persist-tun", 0, 0) != null)
@@ -674,8 +686,7 @@ public class ConfigParser {
}
-
- Pair<Connection, Connection[]> conns = parseConnectionOptions(null);
+ Pair<Connection, Connection[]> conns = parseConnectionOptions(null, transportType);
np.mConnections = conns.second;
Vector<Vector<String>> connectionBlocks = getAllOption("connection", 1, 1);
@@ -698,6 +709,7 @@ public class ConfigParser {
connIndex++;
}
}
+
if (getOption("remote-random", 0, 0) != null)
np.mRemoteRandom = true;
@@ -713,8 +725,8 @@ public class ConfigParser {
throw new ConfigParseError(String.format("Unknown protocol %s in proto-force", protoToDisable));
for (Connection conn : np.mConnections)
- if (conn.mUseUdp == disableUDP)
- conn.mEnabled = false;
+ if (conn.isUseUdp() == disableUDP)
+ conn.setEnabled(false);
}
// Parse OpenVPN Access Server extra
@@ -740,20 +752,21 @@ public class ConfigParser {
return TextUtils.join(s, str);
}
+ public void setObfs4Options(Obfs4Options obfs4Options) {
+ this.obfs4Options = obfs4Options;
+ }
+
private Pair<Connection, Connection[]> parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError {
// Parse a connection Block as a new configuration file
-
ConfigParser connectionParser = new ConfigParser();
StringReader reader = new StringReader(connection.substring(VpnProfile.INLINE_TAG.length()));
connectionParser.parseConfig(reader);
- Pair<Connection, Connection[]> conn = connectionParser.parseConnectionOptions(defaultValues);
-
- return conn;
+ return connectionParser.parseConnectionOptions(defaultValues, defaultValues.getTransportType());
}
- private Pair<Connection, Connection[]> parseConnectionOptions(Connection connDefault) throws ConfigParseError {
+ private Pair<Connection, Connection[]> parseConnectionOptions(Connection connDefault, Connection.TransportType transportType) throws ConfigParseError {
Connection conn;
if (connDefault != null)
try {
@@ -763,27 +776,27 @@ public class ConfigParser {
return null;
}
else
- conn = new Connection();
+ conn = transportType == OBFS4 ? new Obfs4Connection(obfs4Options) : new OpenvpnConnection();
Vector<String> port = getOption("port", 1, 1);
if (port != null) {
- conn.mServerPort = port.get(1);
+ conn.setServerPort(port.get(1));
}
Vector<String> rport = getOption("rport", 1, 1);
if (rport != null) {
- conn.mServerPort = rport.get(1);
+ conn.setServerPort(rport.get(1));
}
Vector<String> proto = getOption("proto", 1, 1);
if (proto != null) {
- conn.mUseUdp = isUdpProto(proto.get(1));
+ conn.setUseUdp(isUdpProto(proto.get(1)));
}
Vector<String> connectTimeout = getOption("connect-timeout", 1, 1);
if (connectTimeout != null) {
try {
- conn.mConnectTimeout = Integer.parseInt(connectTimeout.get(1));
+ conn.setConnectTimeout(Integer.parseInt(connectTimeout.get(1)));
} catch (NumberFormatException nfe) {
throw new ConfigParseError(String.format("Argument to connect-timeout (%s) must to be an integer: %s",
connectTimeout.get(1), nfe.getLocalizedMessage()));
@@ -797,16 +810,16 @@ public class ConfigParser {
if (proxy != null) {
if (proxy.get(0).equals("socks-proxy")) {
- conn.mProxyType = Connection.ProxyType.SOCKS5;
+ conn.setProxyType(Connection.ProxyType.SOCKS5);
// socks defaults to 1080, http always sets port
- conn.mProxyPort = "1080";
+ conn.setProxyPort("1080");
} else {
- conn.mProxyType = Connection.ProxyType.HTTP;
+ conn.setProxyType(Connection.ProxyType.HTTP);
}
- conn.mProxyName = proxy.get(1);
+ conn.setProxyName(proxy.get(1));
if (proxy.size() >= 3)
- conn.mProxyPort = proxy.get(2);
+ conn.setProxyPort(proxy.get(2));
}
Vector<String> httpproxyauthhttp = getOption("http-proxy-user-pass", 1, 1);
@@ -817,21 +830,19 @@ public class ConfigParser {
// Parse remote config
Vector<Vector<String>> remotes = getAllOption("remote", 1, 3);
-
-
Vector <String> optionsToRemove = new Vector<>();
// Assume that we need custom options if connectionDefault are set or in the connection specific set
for (Map.Entry<String, Vector<Vector<String>>> option : options.entrySet()) {
if (connDefault != null || connectionOptionsSet.contains(option.getKey())) {
- conn.mCustomConfiguration += getOptionStrings(option.getValue());
+ conn.setCustomConfiguration(conn.getCustomConfiguration() + getOptionStrings(option.getValue()));
optionsToRemove.add(option.getKey());
}
}
for (String o: optionsToRemove)
options.remove(o);
- if (!(conn.mCustomConfiguration == null || "".equals(conn.mCustomConfiguration.trim())))
- conn.mUseCustomConfig = true;
+ if (!(conn.getCustomConfiguration() == null || "".equals(conn.getCustomConfiguration().trim())))
+ conn.setUseCustomConfig(true);
// Make remotes empty to simplify code
if (remotes == null)
@@ -849,11 +860,11 @@ public class ConfigParser {
}
switch (remote.size()) {
case 4:
- connections[i].mUseUdp = isUdpProto(remote.get(3));
+ connections[i].setUseUdp(isUdpProto(remote.get(3)));
case 3:
- connections[i].mServerPort = remote.get(2);
+ connections[i].setServerPort(remote.get(2));
case 2:
- connections[i].mServerName = remote.get(1);
+ connections[i].setServerName(remote.get(1));
}
i++;
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java b/app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java
new file mode 100644
index 00000000..70b4b4ec
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java
@@ -0,0 +1,15 @@
+package de.blinkt.openvpn.core;
+
+import java.io.Serializable;
+
+/**
+ * Created by cyberta on 11.03.19.
+ */
+
+public interface ConnectionInterface {
+
+ String getConnectionBlock(boolean isOpenVPN3);
+ boolean usesExtraProxyOptions();
+ boolean isOnlyRemote();
+ int getTimeout();
+}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
index 6b633c34..a66b7b51 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
@@ -20,6 +20,8 @@ public class NativeUtils {
{
if (isRoboUnitTest())
return "ROBO";
+ else if (isUnitTest())
+ return "JUNIT";
else
return getJNIAPI();
}
@@ -34,7 +36,7 @@ public class NativeUtils {
public static native double[] getOpenSSLSpeed(String algorithm, int testnum);
static {
- if (!isRoboUnitTest()) {
+ if (!isRoboUnitTest() && !isUnitTest()) {
System.loadLibrary("opvpnutil");
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN)
System.loadLibrary("jbcrypto");
@@ -44,4 +46,13 @@ public class NativeUtils {
public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT);
}
+
+ public static boolean isUnitTest() {
+ try {
+ Class.forName("se.leap.bitmaskclient.testutils.MockHelper");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
index 82c4e1df..e446021f 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
@@ -42,8 +42,11 @@ import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.VpnStatus.ByteCountListener;
import de.blinkt.openvpn.core.VpnStatus.StateListener;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.VpnNotificationManager;
+import se.leap.bitmaskclient.pluggableTransports.Shapeshifter;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
@@ -52,6 +55,7 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE;
public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal, VpnNotificationManager.VpnServiceCallback {
+ public static final String TAG = OpenVPNService.class.getSimpleName();
public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE";
public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY";
public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE";
@@ -62,7 +66,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
public final static String ORBOT_PACKAGE_NAME = "org.torproject.android";
private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN";
private static final String RESUME_VPN = "se.leap.bitmaskclient.RESUME_VPN";
- private static final String TAG = OpenVPNService.class.getSimpleName();
private static boolean mNotificationAlwaysVisible = false;
private final Vector<String> mDnslist = new Vector<>();
private final NetworkSpace mRoutes = new NetworkSpace();
@@ -85,6 +88,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
private Toast mlastToast;
private Runnable mOpenVPNThread;
private VpnNotificationManager notificationManager;
+ private Shapeshifter shapeshifter;
private static final int PRIORITY_MIN = -2;
private static final int PRIORITY_DEFAULT = 0;
@@ -242,6 +246,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
if(isVpnRunning()) {
if (getManagement() != null && getManagement().stopVPN(replaceConnection)) {
if (!replaceConnection) {
+ if (shapeshifter != null) {
+ shapeshifter.stop();
+ }
VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
}
return true;
@@ -249,6 +256,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
return false;
} else {
if (!replaceConnection) {
+ if (shapeshifter != null) {
+ shapeshifter.stop();
+ }
VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
return true;
}
@@ -307,6 +317,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START);
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
VpnStatus.getLastCleanLogMessage(this),
VpnStatus.getLastCleanLogMessage(this),
ConnectionStatus.LEVEL_START,
@@ -366,10 +377,26 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
/**
* see change above (l. 292 ff)
*/
+ //TODO: investigate how connections[n] with n>0 get called during vpn setup (on connection refused?)
+ // Do we need to check if there's any obfs4 connection in mProfile.mConnections and start
+ // the dispatcher here? Can we start the dispatcher at a later point of execution, e.g. when
+ // connections[n], n>0 gets choosen?
+
+ Connection connection = mProfile.mConnections[0];
+
+ if (mProfile.mUsePluggableTransports) {
+ Obfs4Connection obfs4Connection = (Obfs4Connection) connection;
+ shapeshifter = new Shapeshifter(obfs4Connection.getDispatcherOptions());
+ if (!shapeshifter.start()) {
+ //TODO: implement useful error handling
+ Log.e(TAG, "Cannot initialize shapeshifter dispatcher for obfs4 connection. Shutting down.");
+ VpnStatus.logError("Cannot initialize shapeshifter dispatcher for obfs4 connection. Shutting down.");
+ }
+ }
+
VpnStatus.logInfo(R.string.building_configration);
VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START);
-
try {
mProfile.writeConfigFile(this);
} catch (IOException e) {
@@ -743,7 +770,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
boolean profileUsesOrBot = false;
for (Connection c : mProfile.mConnections) {
- if (c.mProxyType == Connection.ProxyType.ORBOT)
+ if (c.getProxyType() == Connection.ProxyType.ORBOT)
profileUsesOrBot = true;
}
@@ -951,6 +978,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
// Does not work :(
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
VpnStatus.getLastCleanLogMessage(this),
VpnStatus.getLastCleanLogMessage(this),
level,
@@ -982,6 +1010,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, getResources()));
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
netstat,
null,
LEVEL_CONNECTED,
@@ -1025,6 +1054,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
VpnStatus.updateStateString("NEED", "need " + needed, resid, LEVEL_WAITING_FOR_USER_INPUT);
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
getString(resid),
getString(resid),
LEVEL_WAITING_FOR_USER_INPUT,
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
index 4f7a5bda..91cc66bc 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
@@ -15,9 +15,10 @@ import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
-import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
+
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.R;
import de.blinkt.openvpn.VpnProfile;
@@ -452,10 +453,10 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
if (mProfile.mConnections.length > connectionEntryNumber) {
Connection connection = mProfile.mConnections[connectionEntryNumber];
- proxyType = connection.mProxyType;
- proxyname = connection.mProxyName;
- proxyport = connection.mProxyPort;
- proxyUseAuth = connection.mUseProxyAuth;
+ proxyType = connection.getProxyType();
+ proxyname = connection.getProxyName();
+ proxyport = connection.getProxyPort();
+ proxyUseAuth = connection.isUseProxyAuth();
// Use transient variable to remember http user/password
mCurrentProxyConnection = connection;
@@ -696,8 +697,8 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
} else if (needed.equals("HTTP Proxy")) {
if( mCurrentProxyConnection != null) {
- pw = mCurrentProxyConnection.mProxyAuthPassword;
- username = mCurrentProxyConnection.mProxyAuthUser;
+ pw = mCurrentProxyConnection.getProxyAuthPassword();
+ username = mCurrentProxyConnection.getProxyAuthUser();
}
}
if (pw != null) {
@@ -782,7 +783,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
boolean stopSucceed = stopOpenVPN();
if (stopSucceed) {
mShuttingDown = true;
-
}
return stopSucceed;
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
new file mode 100644
index 00000000..a318e55d
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.core.connection;
+
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+public abstract class Connection implements Serializable, Cloneable {
+ private String mServerName = "openvpn.example.com";
+ private String mServerPort = "1194";
+ private boolean mUseUdp = true;
+ private String mCustomConfiguration = "";
+ private boolean mUseCustomConfig = false;
+ private boolean mEnabled = true;
+ private int mConnectTimeout = 0;
+ private static final int CONNECTION_DEFAULT_TIMEOUT = 120;
+ private ProxyType mProxyType = ProxyType.NONE;
+ private String mProxyName = "proxy.example.com";
+ private String mProxyPort = "8080";
+
+ private boolean mUseProxyAuth;
+ private String mProxyAuthUser = null;
+ private String mProxyAuthPassword = null;
+
+ public enum ProxyType {
+ NONE,
+ HTTP,
+ SOCKS5,
+ ORBOT
+ }
+
+ public enum TransportType {
+ OBFS4("obfs4"),
+ OPENVPN("openvpn");
+
+ String transport;
+
+ TransportType(String transportType) {
+ this.transport = transportType;
+ }
+
+ @Override
+ public String toString() {
+ return transport;
+ }
+ }
+
+
+ private static final long serialVersionUID = 92031902903829089L;
+
+
+ public String getConnectionBlock(boolean isOpenVPN3) {
+ String cfg = "";
+
+ // Server Address
+ cfg += "remote ";
+ cfg += mServerName;
+ cfg += " ";
+ cfg += mServerPort;
+ if (mUseUdp)
+ cfg += " udp\n";
+ else
+ cfg += " tcp-client\n";
+
+ if (mConnectTimeout != 0)
+ cfg += String.format(Locale.US, " connect-timeout %d\n", mConnectTimeout);
+
+ // OpenVPN 2.x manages proxy connection via management interface
+ if ((isOpenVPN3 || usesExtraProxyOptions()) && mProxyType == ProxyType.HTTP)
+ {
+ cfg+=String.format(Locale.US,"http-proxy %s %s\n", mProxyName, mProxyPort);
+ if (mUseProxyAuth)
+ cfg+=String.format(Locale.US, "<http-proxy-user-pass>\n%s\n%s\n</http-proxy-user-pass>\n", mProxyAuthUser, mProxyAuthPassword);
+ }
+ if (usesExtraProxyOptions() && mProxyType == ProxyType.SOCKS5) {
+ cfg+=String.format(Locale.US,"socks-proxy %s %s\n", mProxyName, mProxyPort);
+ }
+
+ if (!TextUtils.isEmpty(mCustomConfiguration) && mUseCustomConfig) {
+ cfg += mCustomConfiguration;
+ cfg += "\n";
+ }
+
+
+ return cfg;
+ }
+
+ public boolean usesExtraProxyOptions() {
+ return (mUseCustomConfig && mCustomConfiguration.contains("http-proxy-option "));
+ }
+
+
+ @Override
+ public Connection clone() throws CloneNotSupportedException {
+ return (Connection) super.clone();
+ }
+
+ public boolean isOnlyRemote() {
+ return TextUtils.isEmpty(mCustomConfiguration) || !mUseCustomConfig;
+ }
+
+ public int getTimeout() {
+ if (mConnectTimeout <= 0)
+ return CONNECTION_DEFAULT_TIMEOUT;
+ else
+ return mConnectTimeout;
+ }
+
+ public String getServerName() {
+ return mServerName;
+ }
+
+ public void setServerName(String mServerName) {
+ this.mServerName = mServerName;
+ }
+
+ public String getServerPort() {
+ return mServerPort;
+ }
+
+ public void setServerPort(String serverPort) {
+ this.mServerPort = serverPort;
+ }
+
+ public boolean isUseUdp() {
+ return mUseUdp;
+ }
+
+ public void setUseUdp(boolean useUdp) {
+ this.mUseUdp = useUdp;
+ }
+
+ public String getCustomConfiguration() {
+ return mCustomConfiguration;
+ }
+
+ public void setCustomConfiguration(String customConfiguration) {
+ this.mCustomConfiguration = customConfiguration;
+ }
+
+ public boolean isUseCustomConfig() {
+ return mUseCustomConfig;
+ }
+
+ public void setUseCustomConfig(boolean useCustomConfig) {
+ this.mUseCustomConfig = useCustomConfig;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.mEnabled = enabled;
+ }
+
+ public int getConnectTimeout() {
+ return mConnectTimeout;
+ }
+
+ public void setConnectTimeout(int connectTimeout) {
+ this.mConnectTimeout = connectTimeout;
+ }
+
+ public ProxyType getProxyType() {
+ return mProxyType;
+ }
+
+ public void setProxyType(ProxyType proxyType) {
+ this.mProxyType = proxyType;
+ }
+
+ public String getProxyName() {
+ return mProxyName;
+ }
+
+ public void setProxyName(String proxyName) {
+ this.mProxyName = proxyName;
+ }
+
+ public String getProxyPort() {
+ return mProxyPort;
+ }
+
+ public void setProxyPort(String proxyPort) {
+ this.mProxyPort = proxyPort;
+ }
+
+ public boolean isUseProxyAuth() {
+ return mUseProxyAuth;
+ }
+
+ public void setUseProxyAuth(boolean useProxyAuth) {
+ this.mUseProxyAuth = useProxyAuth;
+ }
+
+ public String getProxyAuthUser() {
+ return mProxyAuthUser;
+ }
+
+ public void setProxyAuthUser(String proxyAuthUser) {
+ this.mProxyAuthUser = proxyAuthUser;
+ }
+
+ public String getProxyAuthPassword() {
+ return mProxyAuthPassword;
+ }
+
+ public void setProxyAuthPassword(String proxyAuthPassword) {
+ this.mProxyAuthPassword = proxyAuthPassword;
+ }
+
+ public abstract TransportType getTransportType();
+}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
new file mode 100644
index 00000000..a2f86e05
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
@@ -0,0 +1,59 @@
+package de.blinkt.openvpn.core.connection;
+
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_IP;
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_PORT;
+
+/**
+ * Created by cyberta on 08.03.19.
+ */
+
+public class Obfs4Connection extends Connection {
+
+ private static final String TAG = Obfs4Connection.class.getName();
+ private Obfs4Options options;
+
+ public Obfs4Connection(Obfs4Options options) {
+ setUseUdp(false);
+ setServerName(DISPATCHER_IP);
+ setServerPort(DISPATCHER_PORT);
+ setProxyName("");
+ setProxyPort("");
+ setProxyAuthUser(null);
+ setProxyAuthPassword(null);
+ setProxyType(ProxyType.NONE);
+ setUseProxyAuth(false);
+ this.options = options;
+ }
+
+ @Deprecated
+ public Obfs4Connection() {
+ setUseUdp(false);
+ setServerName(DISPATCHER_IP);
+ setServerPort(DISPATCHER_PORT);
+ setProxyName("");
+ setProxyPort("");
+ setProxyAuthUser(null);
+ setProxyAuthPassword(null);
+ setProxyType(ProxyType.NONE);
+ setUseProxyAuth(false); }
+
+ @Override
+ public Connection clone() throws CloneNotSupportedException {
+ Obfs4Connection connection = (Obfs4Connection) super.clone();
+ connection.options = this.options;
+ return connection;
+ }
+
+ @Override
+ public TransportType getTransportType() {
+ return TransportType.OBFS4;
+ }
+
+
+ public Obfs4Options getDispatcherOptions() {
+ return options;
+ }
+
+}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java
new file mode 100644
index 00000000..3a3fd0c3
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java
@@ -0,0 +1,13 @@
+package de.blinkt.openvpn.core.connection;
+
+/**
+ * Created by cyberta on 11.03.19.
+ */
+
+public class OpenvpnConnection extends Connection {
+
+ @Override
+ public TransportType getTransportType() {
+ return TransportType.OPENVPN;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java
index 18338a73..720cd1c4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java
@@ -14,6 +14,7 @@ public interface Constants {
String CLEARLOG = "clearlogconnect";
String LAST_USED_PROFILE = "last_used_profile";
String EXCLUDED_APPS = "excluded_apps";
+ String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports";
//////////////////////////////////////////////
@@ -114,4 +115,22 @@ public interface Constants {
String FIRST_TIME_USER_DATE = "first_time_user_date";
+ //////////////////////////////////////////////
+ // JSON KEYS
+ /////////////////////////////////////////////
+ String IP_ADDRESS = "ip_address";
+ String REMOTE = "remote";
+ String PORTS = "ports";
+ String PROTOCOLS = "protocols";
+ String CAPABILITIES = "capabilities";
+ String TRANSPORT = "transport";
+ String TYPE = "type";
+ String OPTIONS = "options";
+ String VERSION = "version";
+ String NAME = "name";
+ String TIMEZONE = "timezone";
+ String LOCATIONS = "locations";
+ String LOCATION = "location";
+ String OPENVPN_CONFIGURATION = "openvpn_configuration";
+ String GATEWAYS = "gateways";
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java
index 024bfaba..e69de29b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java
@@ -1,246 +0,0 @@
-/**
- * Copyright (c) 2018 LEAP Encryption Access Project and contributers
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package se.leap.bitmaskclient;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.SwitchCompat;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.CompoundButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-/**
- * Created by cyberta on 21.02.18.
- */
-
-public class DrawerSettingsAdapter extends BaseAdapter {
-
- //item types
- public static final int NONE = -1;
- public static final int SWITCH_PROVIDER = 0;
- public static final int LOG = 1;
- public static final int ABOUT = 2;
- public static final int BATTERY_SAVER = 3;
- public static final int ALWAYS_ON = 4;
- public static final int DONATE = 5;
- public static final int SELECT_APPS = 6;
-
- //view types
- public final static int VIEW_SIMPLE_TEXT = 0;
- public final static int VIEW_SWITCH = 1;
-
- public static class DrawerSettingsItem {
- private String description = "";
- private int viewType = VIEW_SIMPLE_TEXT;
- private boolean isChecked = false;
- private int itemType = NONE;
- private CompoundButton.OnCheckedChangeListener callback;
- private Drawable iconResource;
-
- private DrawerSettingsItem(Context context, String description, @DrawableRes int iconResource, int viewType, boolean isChecked, int itemType, CompoundButton.OnCheckedChangeListener callback) {
- this.description = description;
- this.viewType = viewType;
- this.isChecked = isChecked;
- this.itemType = itemType;
- this.callback = callback;
- try {
- this.iconResource = context.getResources().getDrawable(iconResource);
- } catch (RuntimeException e) {
- e.printStackTrace();
- }
- }
-
- public static DrawerSettingsItem getSimpleTextInstance(Context context, String description, @DrawableRes int iconResource, int itemType) {
- return new DrawerSettingsItem(context, description, iconResource, VIEW_SIMPLE_TEXT, false, itemType, null);
- }
-
- public static DrawerSettingsItem getSwitchInstance(Context context, String description, @DrawableRes int iconResource, boolean isChecked, int itemType, CompoundButton.OnCheckedChangeListener callback) {
- return new DrawerSettingsItem(context, description, iconResource, VIEW_SWITCH, isChecked, itemType, callback);
- }
-
- public int getItemType() {
- return itemType;
- }
-
- public void setChecked(boolean checked) {
- isChecked = checked;
- }
-
- public boolean isChecked() {
- return isChecked;
- }
- }
-
- private ArrayList<DrawerSettingsItem> mData = new ArrayList<>();
- private LayoutInflater mInflater;
-
- public DrawerSettingsAdapter(LayoutInflater layoutInflater) {
- mInflater = layoutInflater;
- }
-
- public void addItem(final DrawerSettingsItem item) {
- mData.add(item);
- notifyDataSetChanged();
- }
-
- @Override
- public int getItemViewType(int position) {
- DrawerSettingsItem item = mData.get(position);
- return item.viewType;
- }
-
- @Override
- public int getViewTypeCount() {
- boolean hasSwitchItem = false;
- for (DrawerSettingsItem item : mData) {
- if (item.viewType == VIEW_SWITCH) {
- hasSwitchItem = true;
- break;
- }
- }
- return hasSwitchItem ? 2 : 1;
- }
-
- @Override
- public int getCount() {
- return mData.size();
- }
-
- @Override
- public DrawerSettingsItem getItem(int position) {
- return mData.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
-
- DrawerSettingsItem drawerSettingsItem = mData.get(position);
- ViewHolder holder = null;
- int type = getItemViewType(position);
- if (convertView == null) {
- holder = new ViewHolder();
- switch(type) {
- case VIEW_SIMPLE_TEXT:
- convertView = initTextViewBinding(holder);
- bindSimpleText(drawerSettingsItem, holder);
- break;
- case VIEW_SWITCH:
- convertView = initSwitchBinding(holder);
- bindSwitch(drawerSettingsItem, holder);
- break;
- }
- convertView.setTag(holder);
- } else {
- holder = (ViewHolder)convertView.getTag();
- switch (type) {
- case VIEW_SIMPLE_TEXT:
- if (holder.isSwitchViewHolder()) {
- holder.resetSwitchView();
- convertView = initTextViewBinding(holder);
- }
- bindSimpleText(drawerSettingsItem, holder);
- break;
- case VIEW_SWITCH:
- if (!holder.isSwitchViewHolder()) {
- holder.resetTextView();
- convertView = initSwitchBinding(holder);
- }
- bindSwitch(drawerSettingsItem, holder);
- break;
- }
- convertView.setTag(holder);
- }
- return convertView;
- }
-
- private void bindSimpleText(DrawerSettingsItem drawerSettingsItem, ViewHolder holder) {
- holder.textView.setText(drawerSettingsItem.description);
- if (drawerSettingsItem.iconResource != null) {
- holder.iconView.setImageDrawable(drawerSettingsItem.iconResource);
- }
- }
-
- private void bindSwitch(DrawerSettingsItem drawerSettingsItem, ViewHolder holder) {
- holder.switchView.setChecked(drawerSettingsItem.isChecked);
- holder.textView.setText(drawerSettingsItem.description);
- holder.switchView.setOnCheckedChangeListener(drawerSettingsItem.callback);
- if (drawerSettingsItem.iconResource != null) {
- holder.iconView.setImageDrawable(drawerSettingsItem.iconResource);
- }
- }
-
- @NonNull
- private View initSwitchBinding(ViewHolder holder) {
- View convertView = mInflater.inflate(R.layout.v_switch_list_item, null);
- holder.switchView = convertView.findViewById(R.id.option_switch);
- holder.textView = convertView.findViewById(android.R.id.text1);
- holder.iconView = convertView.findViewById(R.id.material_icon);
- return convertView;
- }
-
- @NonNull
- private View initTextViewBinding(ViewHolder holder) {
- View convertView = mInflater.inflate(R.layout.v_icon_text_list_item, null);
- holder.textView = convertView.findViewById(android.R.id.text1);
- holder.iconView = convertView.findViewById(R.id.material_icon);
- return convertView;
- }
-
- public DrawerSettingsItem getDrawerItem(int elementType) {
- for (DrawerSettingsItem item : mData) {
- if (item.itemType == elementType) {
- return item;
- }
- }
- return null;
- }
-
- static class ViewHolder {
- TextView textView;
- ImageView iconView;
- SwitchCompat switchView;
-
- boolean isSwitchViewHolder() {
- return switchView != null;
- }
-
- void resetSwitchView() {
- switchView.setOnCheckedChangeListener(null);
- switchView = null;
- }
-
- void resetTextView() {
- textView = null;
- }
- }
-}
-
-
-
diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
index a8aa2dfb..7327c416 100644
--- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
@@ -168,6 +168,7 @@ class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListe
if (resultCode == RESULT_CANCELED) {
//setup failed
finishGatewaySetup(false);
+ EipStatus.refresh();
}
break;
default:
diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index c81f5739..067f9b2e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -21,6 +21,7 @@ import android.os.Parcelable;
import com.google.gson.Gson;
+import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -28,8 +29,13 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static se.leap.bitmaskclient.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.Constants.GATEWAYS;
import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOWED_REGISTERED;
import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOW_ANONYMOUS;
+import static se.leap.bitmaskclient.Constants.TRANSPORT;
+import static se.leap.bitmaskclient.Constants.TYPE;
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
/**
@@ -119,6 +125,25 @@ public final class Provider implements Parcelable {
hasPrivateKey();
}
+ public boolean supportsPluggableTransports() {
+ try {
+ JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS);
+ for (int i = 0; i < gatewayJsons.length(); i++) {
+ JSONArray transports = gatewayJsons.getJSONObject(i).
+ getJSONObject(CAPABILITIES).
+ getJSONArray(TRANSPORT);
+ for (int j = 0; j < transports.length(); j++) {
+ if (OBFS4.toString().equals(transports.getJSONObject(j).getString(TYPE))) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
public void setMainUrl(URL url) {
mainUrl.setUrl(url);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index 37adbe93..46782802 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
@@ -49,6 +49,7 @@ import java.util.List;
import java.util.NoSuchElementException;
import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
import okhttp3.OkHttpClient;
import se.leap.bitmaskclient.Constants.CREDENTIAL_ERRORS;
@@ -578,7 +579,7 @@ public abstract class ProviderApiManagerBase {
plainResponseBody = formatErrorMessage(server_unreachable_message);
} catch (MalformedURLException e) {
plainResponseBody = formatErrorMessage(malformed_url);
- } catch (SSLHandshakeException e) {
+ } catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
plainResponseBody = formatErrorMessage(certificate_error);
} catch (ConnectException e) {
plainResponseBody = formatErrorMessage(service_is_down_error);
@@ -750,6 +751,13 @@ public abstract class ProviderApiManagerBase {
return result;
}
+ protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) {
+ String reasonToFail = pickErrorMessage(stringJsonErrorMessage);
+ result.putString(ERRORS, reasonToFail);
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
JSONObject errorJson = new JSONObject();
String errorMessage = getProviderFormattedString(resources, errorMessageId);
diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
index d8aca351..b89363b2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
@@ -162,8 +162,8 @@ public class StartActivity extends Activity{
}
private void prepareEIP() {
- boolean provider_exists = providerInSharedPreferences(preferences);
- if (provider_exists) {
+ boolean providerExists = providerInSharedPreferences(preferences);
+ if (providerExists) {
Provider provider = getSavedProviderFromSharedPreferences(preferences);
if(!provider.isConfigured()) {
configureLeapProvider();
@@ -215,5 +215,4 @@ public class StartActivity extends Activity{
startActivity(intent);
finish();
}
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java
index 9107568c..b276a402 100644
--- a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java
@@ -24,11 +24,17 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
+import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
import android.widget.RemoteViews;
import de.blinkt.openvpn.LaunchVPN;
@@ -43,8 +49,8 @@ import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
import static android.text.TextUtils.isEmpty;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
-import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN;
import static se.leap.bitmaskclient.Constants.ASK_TO_CANCEL_VPN;
+import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN;
import static se.leap.bitmaskclient.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
/**
@@ -83,6 +89,7 @@ public class VpnNotificationManager {
buildVpnNotification(
context.getString(R.string.void_vpn_title),
msg,
+ null,
tickerText,
status,
VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID,
@@ -110,8 +117,11 @@ public class VpnNotificationManager {
* @param status
* @param when
*/
- public void buildOpenVpnNotification(String profileName, final String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) {
+ public void buildOpenVpnNotification(String profileName, boolean isObfuscated, String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) {
String cancelString;
+ CharSequence bigmessage = null;
+ String ghostIcon = new String(Character.toChars(0x1f309));
+
switch (status) {
// show cancel if no connection
case LEVEL_START:
@@ -119,11 +129,28 @@ public class VpnNotificationManager {
case LEVEL_CONNECTING_SERVER_REPLIED:
case LEVEL_CONNECTING_NO_SERVER_REPLY_YET:
cancelString = context.getString(R.string.cancel);
+ if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
+ Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection_try));
+ spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg);
+ }
break;
+
// show disconnect if connection exists
+ case LEVEL_CONNECTED:
+ if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
+ Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection));
+ spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg);
+ }
default:
cancelString = context.getString(R.string.cancel_connection);
}
+
+ if (isObfuscated) {
+ msg = ghostIcon + " " + msg;
+ }
+
NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.
Builder(R.drawable.ic_menu_close_clear_cancel, cancelString, getDisconnectIntent());
String title;
@@ -151,6 +178,7 @@ public class VpnNotificationManager {
buildVpnNotification(
title,
msg,
+ bigmessage,
tickerText,
status,
notificationChannelNewstatusId,
@@ -224,28 +252,30 @@ public class VpnNotificationManager {
return remoteViews;
}
- private void buildVpnNotification(String title, final String msg, String tickerText, ConnectionStatus status, String notificationChannelNewstatusId, int priority, long when, PendingIntent contentIntent, NotificationCompat.Action notificationAction) {
+ private void buildVpnNotification(String title, String message, CharSequence bigMessage, String tickerText, ConnectionStatus status, String notificationChannelNewstatusId, int priority, long when, PendingIntent contentIntent, NotificationCompat.Action notificationAction) {
NotificationCompat.Builder nCompatBuilder = new NotificationCompat.Builder(context, notificationChannelNewstatusId);
int icon = getIconByConnectionStatus(status);
// this is a workaround to avoid confusion between the Android's system vpn notification
// showing a filled out key icon and the bitmask icon indicating a different state.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT &&
- notificationChannelNewstatusId.equals(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID) &&
- status != LEVEL_NONETWORK
- ) {
- // removes the icon from the system status bar
- icon = android.R.color.transparent;
- // adds the icon to the notification in the notification drawer
- nCompatBuilder.setContent(getKitkatCustomRemoteView(status, title, msg));
+ notificationChannelNewstatusId.equals(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID)) {
+ if (status != LEVEL_NONETWORK) {
+ // removes the icon from the system status bar
+ icon = android.R.color.transparent;
+ // adds the icon to the notification in the notification drawer
+ nCompatBuilder.setContent(getKitkatCustomRemoteView(status, title, message));
+ }
} else {
- nCompatBuilder.addAction(notificationAction);
+ nCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().
+ setBigContentTitle(title).
+ bigText(bigMessage));
}
-
+ nCompatBuilder.addAction(notificationAction);
nCompatBuilder.setContentTitle(title);
nCompatBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE);
nCompatBuilder.setLocalOnly(true);
- nCompatBuilder.setContentText(msg);
+ nCompatBuilder.setContentText(message);
nCompatBuilder.setOnlyAlertOnce(true);
nCompatBuilder.setSmallIcon(icon);
nCompatBuilder.setPriority(priority);
diff --git a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java
index a604c536..e3c7ac1b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributers
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@ package se.leap.bitmaskclient.drawer;
import android.app.Activity;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -44,50 +43,41 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
-import se.leap.bitmaskclient.DrawerSettingsAdapter;
-import se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem;
+import de.blinkt.openvpn.core.VpnStatus;
import se.leap.bitmaskclient.EipFragment;
import se.leap.bitmaskclient.FragmentManagerEnhanced;
import se.leap.bitmaskclient.MainActivity;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.ProviderListActivity;
+import se.leap.bitmaskclient.ProviderObservable;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.fragments.AboutFragment;
import se.leap.bitmaskclient.fragments.AlwaysOnDialog;
-import se.leap.bitmaskclient.fragments.LogFragment;
import se.leap.bitmaskclient.fragments.ExcludeAppsFragment;
+import se.leap.bitmaskclient.fragments.LogFragment;
+import se.leap.bitmaskclient.views.IconSwitchEntry;
+import se.leap.bitmaskclient.views.IconTextEntry;
import static android.content.Context.MODE_PRIVATE;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static se.leap.bitmaskclient.BitmaskApp.getRefWatcher;
import static se.leap.bitmaskclient.Constants.DONATION_URL;
import static se.leap.bitmaskclient.Constants.ENABLE_DONATION;
import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.ABOUT;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.ALWAYS_ON;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.BATTERY_SAVER;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.DONATE;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem.getSimpleTextInstance;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem.getSwitchInstance;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.LOG;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.SELECT_APPS;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.SWITCH_PROVIDER;
import static se.leap.bitmaskclient.R.string.about_fragment_title;
import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title;
-import static se.leap.bitmaskclient.R.string.donate_title;
import static se.leap.bitmaskclient.R.string.log_fragment_title;
-import static se.leap.bitmaskclient.R.string.switch_provider_menu_option;
import static se.leap.bitmaskclient.utils.ConfigHelper.isDefaultBitmask;
-import static se.leap.bitmaskclient.utils.PreferenceHelper.getProviderName;
import static se.leap.bitmaskclient.utils.PreferenceHelper.getSaveBattery;
-import static se.leap.bitmaskclient.utils.PreferenceHelper.getSavedProviderFromSharedPreferences;
import static se.leap.bitmaskclient.utils.PreferenceHelper.getShowAlwaysOnDialog;
+import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports;
import static se.leap.bitmaskclient.utils.PreferenceHelper.saveBattery;
+import static se.leap.bitmaskclient.utils.PreferenceHelper.usePluggableTransports;
/**
* Fragment used for managing interactions for and presentation of a navigation drawer.
@@ -112,11 +102,10 @@ public class NavigationDrawerFragment extends Fragment {
private DrawerLayout drawerLayout;
private View drawerView;
- private ListView drawerAccountsListView;
private View fragmentContainerView;
- private ArrayAdapter<String> accountListAdapter;
- private DrawerSettingsAdapter settingsListAdapter;
private Toolbar toolbar;
+ private IconTextEntry account;
+ private IconSwitchEntry saveBattery;
private boolean userLearnedDrawer;
private volatile boolean wasPaused;
@@ -186,14 +175,8 @@ public class NavigationDrawerFragment extends Fragment {
this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
toolbar = this.drawerLayout.findViewById(R.id.toolbar);
- final ActionBar actionBar = setupActionBar();
- setupSettingsListAdapter();
- setupSettingsListView();
- accountListAdapter = new ArrayAdapter<>(actionBar.getThemedContext(),
- R.layout.v_icon_text_list_item,
- android.R.id.text1);
- refreshAccountListAdapter();
- setupAccountsListView();
+ setupActionBar();
+ setupEntries();
setupActionBarDrawerToggle(activity);
if (!userLearnedDrawer) {
@@ -243,40 +226,144 @@ public class NavigationDrawerFragment extends Fragment {
};
}
- private void setupAccountsListView() {
- drawerAccountsListView = drawerView.findViewById(R.id.accountList);
- drawerAccountsListView.setAdapter(accountListAdapter);
- drawerAccountsListView.setOnItemClickListener((parent, view, position, id) -> selectItem(parent, position));
+ private void setupEntries() {
+ initAccountEntry();
+ initSwitchProviderEntry();
+ initUseBridgesEntry();
+ initSaveBatteryEntry();
+ initAlwaysOnVpnEntry();
+ initExcludeAppsEntry();
+ initDonateEntry();
+ initLogEntry();
+ initAboutEntry();
+ }
+
+ private void initAccountEntry() {
+ account = drawerView.findViewById(R.id.account);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
+ account.setText(currentProvider.getName());
+ account.setOnClickListener((buttonView) -> {
+ Fragment fragment = new EipFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(PROVIDER_KEY, currentProvider);
+ fragment.setArguments(arguments);
+ hideActionBarSubTitle();
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ closeDrawer();
+ });
}
- private void setupSettingsListView() {
- ListView drawerSettingsListView = drawerView.findViewById(R.id.settingsList);
- drawerSettingsListView.setOnItemClickListener((parent, view, position, id) -> selectItem(parent, position));
- drawerSettingsListView.setAdapter(settingsListAdapter);
+ private void initSwitchProviderEntry() {
+ if (isDefaultBitmask()) {
+ IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider);
+ switchProvider.setVisibility(VISIBLE);
+ switchProvider.setOnClickListener(v ->
+ getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER));
+ }
}
- private void setupSettingsListAdapter() {
- settingsListAdapter = new DrawerSettingsAdapter(getLayoutInflater());
- if (getContext() != null) {
- settingsListAdapter.addItem(getSwitchInstance(getContext(),
- getString(R.string.save_battery),
- R.drawable.ic_battery_36,
- getSaveBattery(getContext()),
- BATTERY_SAVER,
- (buttonView, newStateIsChecked) -> onSwitchItemSelected(BATTERY_SAVER, newStateIsChecked)));
+ private void initUseBridgesEntry() {
+ IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch);
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
+ useBridges.setVisibility(VISIBLE);
+ useBridges.setChecked(getUsePluggableTransports(getContext()));
+ useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ usePluggableTransports(getContext(), isChecked);
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), true);
+ closeDrawer();
+ }
+ });
+
+
+ } else {
+ useBridges.setVisibility(GONE);
}
+ }
+
+ private void initSaveBatteryEntry() {
+ saveBattery = drawerView.findViewById(R.id.battery_switch);
+ saveBattery.setChecked(getSaveBattery(getContext()));
+ saveBattery.setOnCheckedChangeListener(((buttonView, isChecked) -> {
+ if (isChecked) {
+ showExperimentalFeatureAlert();
+ } else {
+ saveBattery(getContext(), false);
+ }
+ }));
+ }
+
+ private void initAlwaysOnVpnEntry() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(R.string.always_on_vpn), R.drawable.ic_always_on_36, ALWAYS_ON));
+ IconTextEntry alwaysOnVpn = drawerView.findViewById(R.id.always_on_vpn);
+ alwaysOnVpn.setVisibility(VISIBLE);
+ alwaysOnVpn.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ if (getShowAlwaysOnDialog(getContext())) {
+ showAlwaysOnDialog();
+ } else {
+ Intent intent = new Intent("android.net.vpn.SETTINGS");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ });
}
- if (isDefaultBitmask()) {
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(switch_provider_menu_option), R.drawable.ic_switch_provider_36, SWITCH_PROVIDER));
+ }
+
+ private void initExcludeAppsEntry() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps);
+ excludeApps.setVisibility(VISIBLE);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ excludeApps.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new ExcludeAppsFragment();
+ setActionBarTitle(exclude_apps_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
}
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(exclude_apps_fragment_title), R.drawable.ic_shield_remove_grey600_36dp, SELECT_APPS));
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(log_fragment_title), R.drawable.ic_log_36, LOG));
+ }
+
+ private void initDonateEntry() {
if (ENABLE_DONATION) {
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(donate_title), R.drawable.ic_donate_36, DONATE));
+ IconTextEntry donate = drawerView.findViewById(R.id.donate);
+ donate.setVisibility(VISIBLE);
+ donate.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL));
+ startActivity(browserIntent);
+
+ });
+ }
+ }
+
+ private void initLogEntry() {
+ IconTextEntry log = drawerView.findViewById(R.id.log);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ log.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new LogFragment();
+ setActionBarTitle(log_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
+ private void initAboutEntry() {
+ IconTextEntry about = drawerView.findViewById(R.id.about);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ about.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new AboutFragment();
+ setActionBarTitle(about_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
+ private void closeDrawer() {
+ if (drawerLayout != null) {
+ drawerLayout.closeDrawer(fragmentContainerView);
}
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(about_fragment_title), R.drawable.ic_about_36, ABOUT));
}
private ActionBar setupActionBar() {
@@ -324,16 +411,6 @@ public class NavigationDrawerFragment extends Fragment {
}, TWO_SECONDS);
}
- private void selectItem(AdapterView<?> list, int position) {
- if (list != null) {
- ((ListView) list).setItemChecked(position, true);
- }
- if (drawerLayout != null) {
- drawerLayout.closeDrawer(fragmentContainerView);
- }
- onTextItemSelected(list, position);
- }
-
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@@ -361,17 +438,11 @@ public class NavigationDrawerFragment extends Fragment {
.setTitle(activity.getString(R.string.save_battery))
.setMessage(activity.getString(R.string.save_battery_message))
.setPositiveButton((android.R.string.yes), (dialog, which) -> {
- DrawerSettingsItem item = settingsListAdapter.getDrawerItem(BATTERY_SAVER);
- item.setChecked(true);
- settingsListAdapter.notifyDataSetChanged();
- saveBattery(getContext(), item.isChecked());
+ saveBattery(getContext(), true);
})
- .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> disableSwitch(BATTERY_SAVER)).setOnDismissListener(new DialogInterface.OnDismissListener() {
- @Override
- public void onDismiss(DialogInterface dialog) {
- showEnableExperimentalFeature = false;
- }
- }).setOnCancelListener(dialog -> disableSwitch(BATTERY_SAVER)).show();
+ .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false))
+ .setOnDismissListener(dialog -> showEnableExperimentalFeature = false)
+ .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show();
} catch (IllegalStateException e) {
e.printStackTrace();
}
@@ -434,85 +505,6 @@ public class NavigationDrawerFragment extends Fragment {
return ((AppCompatActivity) getActivity()).getSupportActionBar();
}
- private void onSwitchItemSelected(int elementType, boolean newStateIsChecked) {
- switch (elementType) {
- case BATTERY_SAVER:
- if (getSaveBattery(getContext()) == newStateIsChecked) {
- //initial ui setup, ignore
- return;
- }
- if (newStateIsChecked) {
- showExperimentalFeatureAlert();
- } else {
- saveBattery(this.getContext(), false);
- disableSwitch(BATTERY_SAVER);
- }
- break;
- default:
- break;
- }
- }
-
- private void disableSwitch(int elementType) {
- DrawerSettingsItem item = settingsListAdapter.getDrawerItem(elementType);
- item.setChecked(false);
- settingsListAdapter.notifyDataSetChanged();
- }
-
- public void onTextItemSelected(AdapterView<?> parent, int position) {
- // update the main content by replacing fragments
- FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
- Fragment fragment = null;
-
- if (parent == drawerAccountsListView) {
- fragment = new EipFragment();
- Bundle arguments = new Bundle();
- Provider currentProvider = getSavedProviderFromSharedPreferences(preferences);
- arguments.putParcelable(PROVIDER_KEY, currentProvider);
- fragment.setArguments(arguments);
- hideActionBarSubTitle();
- } else {
- DrawerSettingsItem settingsItem = settingsListAdapter.getItem(position);
- switch (settingsItem.getItemType()) {
- case SWITCH_PROVIDER:
- getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER);
- break;
- case LOG:
- fragment = new LogFragment();
- setActionBarTitle(log_fragment_title);
- break;
- case ABOUT:
- fragment = new AboutFragment();
- setActionBarTitle(about_fragment_title);
- break;
- case ALWAYS_ON:
- if (getShowAlwaysOnDialog(getContext())) {
- showAlwaysOnDialog();
- } else {
- Intent intent = new Intent("android.net.vpn.SETTINGS");
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- }
- break;
- case DONATE:
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL));
- startActivity(browserIntent);
- break;
- case SELECT_APPS:
- fragment = new ExcludeAppsFragment();
- setActionBarTitle(exclude_apps_fragment_title);
- break;
- default:
- break;
- }
- }
-
- if (fragment != null) {
- fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
- }
-
- }
-
private void setActionBarTitle(@StringRes int resId) {
ActionBar actionBar = getActionBar();
if (actionBar != null) {
@@ -527,22 +519,10 @@ public class NavigationDrawerFragment extends Fragment {
}
}
-
public void refresh() {
- refreshAccountListAdapter();
- accountListAdapter.notifyDataSetChanged();
- drawerAccountsListView.setAdapter(accountListAdapter);
- }
-
- private void refreshAccountListAdapter() {
- accountListAdapter.clear();
- String providerName = getProviderName(preferences);
- if (providerName == null) {
- //TODO: ADD A header to the ListView containing a useful message.
- //TODO 2: disable switchProvider
- } else {
- accountListAdapter.add(providerName);
- }
+ Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
+ account.setText(currentProvider.getName());
+ initUseBridgesEntry();
}
}
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 a5434871..19c539e8 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -43,16 +43,20 @@ import java.util.Observer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConnectionStatus;
import de.blinkt.openvpn.core.IOpenVPNServiceInternal;
import de.blinkt.openvpn.core.OpenVPNService;
import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.OnBootReceiver;
import se.leap.bitmaskclient.R;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.CATEGORY_DEFAULT;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT;
import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT;
import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE;
@@ -74,6 +78,7 @@ import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.MainActivityErrorDialog.DOWNLOAD_ERRORS.ERROR_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
import static se.leap.bitmaskclient.utils.ConfigHelper.ensureNotOnMainThread;
+import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports;
/**
* EIP is the abstract base class for interacting with and managing the Encrypted
@@ -203,11 +208,11 @@ public final class EIP extends JobIntentService implements Observer {
GatewaysManager gatewaysManager = gatewaysFromPreferences();
Gateway gateway = gatewaysManager.select(nClosestGateway);
- if (gateway != null && gateway.getProfile() != null) {
- launchActiveGateway(gateway, nClosestGateway);
+ if (launchActiveGateway(gateway, nClosestGateway)) {
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_OK);
- } else
+ } else {
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED);
+ }
}
/**
@@ -218,9 +223,7 @@ public final class EIP extends JobIntentService implements Observer {
GatewaysManager gatewaysManager = gatewaysFromPreferences();
Gateway gateway = gatewaysManager.select(0);
- if (gateway != null && gateway.getProfile() != null) {
- launchActiveGateway(gateway, 0);
- } else {
+ if (!launchActiveGateway(gateway, 0)) {
Log.d(TAG, "startEIPAlwaysOnVpn no active profile available!");
}
}
@@ -240,11 +243,19 @@ public final class EIP extends JobIntentService implements Observer {
*
* @param gateway to connect to
*/
- private void launchActiveGateway(@NonNull Gateway gateway, int nClosestGateway) {
+ private boolean launchActiveGateway(Gateway gateway, int nClosestGateway) {
+ VpnProfile profile;
+ Connection.TransportType transportType = getUsePluggableTransports(this) ? OBFS4 : OPENVPN;
+ if (gateway == null ||
+ (profile = gateway.getProfile(transportType)) == null) {
+ return false;
+ }
+
Intent intent = new Intent(BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT);
- intent.putExtra(PROVIDER_PROFILE, gateway.getProfile());
+ intent.putExtra(PROVIDER_PROFILE, profile);
intent.putExtra(Gateway.KEY_N_CLOSEST_GATEWAY, nClosestGateway);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ return true;
}
@@ -277,7 +288,7 @@ public final class EIP extends JobIntentService implements Observer {
* @return GatewaysManager
*/
private GatewaysManager gatewaysFromPreferences() {
- GatewaysManager gatewaysManager = new GatewaysManager(this, preferences);
+ GatewaysManager gatewaysManager = new GatewaysManager(preferences);
gatewaysManager.configureFromPreferences();
return gatewaysManager;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java
index 64904816..69fc483a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java
@@ -78,8 +78,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener {
currentStatus.setLevel(level);
currentStatus.setEipLevel(level);
if (tmp != currentStatus.getLevel() || "RECONNECTING".equals(state)) {
- currentStatus.setChanged();
- currentStatus.notifyObservers();
+ refresh();
}
}
@@ -174,8 +173,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener {
default:
break;
}
- currentStatus.setChanged();
- currentStatus.notifyObservers();
+ refresh();
}
}
}
@@ -286,4 +284,9 @@ public class EipStatus extends Observable implements VpnStatus.StateListener {
return "State: " + state + " Level: " + vpnLevel.toString();
}
+ public static void refresh() {
+ currentStatus.setChanged();
+ currentStatus.notifyObservers();
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
index 09b33845..15ee13c2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
@@ -17,7 +17,8 @@
package se.leap.bitmaskclient.eip;
import android.content.Context;
-import android.content.SharedPreferences;
+
+import android.support.annotation.NonNull;
import com.google.gson.Gson;
@@ -25,14 +26,22 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.io.StringReader;
import java.util.HashSet;
import java.util.Set;
+import java.util.HashMap;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
-import se.leap.bitmaskclient.BitmaskApp;
import se.leap.bitmaskclient.utils.PreferenceHelper;
+import de.blinkt.openvpn.core.connection.Connection;
+
+import static se.leap.bitmaskclient.Constants.IP_ADDRESS;
+import static se.leap.bitmaskclient.Constants.LOCATION;
+import static se.leap.bitmaskclient.Constants.LOCATIONS;
+import static se.leap.bitmaskclient.Constants.NAME;
+import static se.leap.bitmaskclient.Constants.OPENVPN_CONFIGURATION;
+import static se.leap.bitmaskclient.Constants.TIMEZONE;
+import static se.leap.bitmaskclient.Constants.VERSION;
/**
* Gateway provides objects defining gateways and their metadata.
@@ -41,6 +50,7 @@ import se.leap.bitmaskclient.utils.PreferenceHelper;
*
* @author Sean Leonard <meanderingcode@aetherislands.net>
* @author Parménides GV <parmegv@sdf.org>
+ * @author cyberta
*/
public class Gateway {
@@ -51,60 +61,69 @@ public class Gateway {
private JSONObject secrets;
private JSONObject gateway;
- private String mName;
+ private String name;
private int timezone;
- private VpnProfile mVpnProfile;
+ private int apiVersion;
+ private HashMap<Connection.TransportType, VpnProfile> vpnProfiles;
/**
* Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json
* and create a VpnProfile belonging to it.
*/
- public Gateway(JSONObject eip_definition, JSONObject secrets, JSONObject gateway, Context context) {
+ public Gateway(JSONObject eipDefinition, JSONObject secrets, JSONObject gateway, Context context) {
this.gateway = gateway;
this.secrets = secrets;
- generalConfiguration = getGeneralConfiguration(eip_definition);
- timezone = getTimezone(eip_definition);
- mName = locationAsName(eip_definition);
-
- mVpnProfile = createVPNProfile();
- System.out.println("###########" + mName + "###########");
- mVpnProfile.mName = mName;
+ generalConfiguration = getGeneralConfiguration(eipDefinition);
+ timezone = getTimezone(eipDefinition);
+ name = locationAsName(eipDefinition);
+ apiVersion = getApiVersion(eipDefinition);
+ vpnProfiles = createVPNProfiles(context);
+ }
+ private void addProfileInfos(Context context, HashMap<Connection.TransportType, VpnProfile> profiles) {
Set<String> excludedAppsVpn = PreferenceHelper.getExcludedApps(context);
- if (excludedAppsVpn != null) {
- mVpnProfile.mAllowedAppsVpn = new HashSet<>(excludedAppsVpn);
- }
- else {
- mVpnProfile.mAllowedAppsVpn = null;
+ for (VpnProfile profile : profiles.values()) {
+ profile.mName = name;
+ profile.mGatewayIp = gateway.optString(IP_ADDRESS);
+ if (excludedAppsVpn != null) {
+ profile.mAllowedAppsVpn = new HashSet<>(excludedAppsVpn);
+ }
}
-
}
- private JSONObject getGeneralConfiguration(JSONObject eip_definition) {
+ private JSONObject getGeneralConfiguration(JSONObject eipDefinition) {
try {
- return eip_definition.getJSONObject("openvpn_configuration");
+ return eipDefinition.getJSONObject(OPENVPN_CONFIGURATION);
} catch (JSONException e) {
return new JSONObject();
}
}
- private int getTimezone(JSONObject eip_definition) {
- JSONObject location = getLocationInfo(eip_definition);
- return location.optInt("timezone");
+ private int getTimezone(JSONObject eipDefinition) {
+ JSONObject location = getLocationInfo(eipDefinition);
+ return location.optInt(TIMEZONE);
+ }
+
+ private int getApiVersion(JSONObject eipDefinition) {
+ return eipDefinition.optInt(VERSION);
+ }
+
+ public String getRemoteIP() {
+ return gateway.optString(IP_ADDRESS);
}
- private String locationAsName(JSONObject eip_definition) {
- JSONObject location = getLocationInfo(eip_definition);
- return location.optString("name");
+ private String locationAsName(JSONObject eipDefinition) {
+ JSONObject location = getLocationInfo(eipDefinition);
+ return location.optString(NAME);
}
private JSONObject getLocationInfo(JSONObject eipDefinition) {
try {
- JSONObject locations = eipDefinition.getJSONObject("locations");
+ JSONObject locations = eipDefinition.getJSONObject(LOCATIONS);
- return locations.getJSONObject(gateway.getString("location"));
+ return locations.getJSONObject(gateway.getString(LOCATION));
} catch (JSONException e) {
return new JSONObject();
}
@@ -113,32 +132,29 @@ public class Gateway {
/**
* Create and attach the VpnProfile to our gateway object
*/
- private VpnProfile createVPNProfile() {
+ private @NonNull HashMap<Connection.TransportType, VpnProfile> createVPNProfiles(Context context) {
+ HashMap<Connection.TransportType, VpnProfile> profiles = new HashMap<>();
try {
- ConfigParser cp = new ConfigParser();
-
- VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway);
- String configuration = vpnConfigurationGenerator.generate();
-
- cp.parseConfig(new StringReader(configuration));
- return cp.convertProfile();
- } catch (ConfigParser.ConfigParseError e) {
- // FIXME We didn't get a VpnProfile! Error handling! and log level
- e.printStackTrace();
- return null;
- } catch (IOException e) {
+ VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway, apiVersion);
+ profiles = vpnConfigurationGenerator.generateVpnProfiles();
+ addProfileInfos(context, profiles);
+ } catch (ConfigParser.ConfigParseError | IOException | JSONException e) {
// FIXME We didn't get a VpnProfile! Error handling! and log level
e.printStackTrace();
- return null;
}
+ return profiles;
}
public String getName() {
- return mName;
+ return name;
}
- public VpnProfile getProfile() {
- return mVpnProfile;
+ public HashMap<Connection.TransportType, VpnProfile> getProfiles() {
+ return vpnProfiles;
+ }
+
+ public VpnProfile getProfile(Connection.TransportType transportType) {
+ return vpnProfiles.get(transportType);
}
public int getTimezone() {
@@ -150,17 +166,4 @@ public class Gateway {
return new Gson().toJson(this, Gateway.class);
}
- @Override
- public boolean equals(Object obj) {
- return obj instanceof Gateway &&
- (this.mVpnProfile != null &&
- ((Gateway) obj).mVpnProfile != null &&
- this.mVpnProfile.mConnections != null &&
- ((Gateway) obj).mVpnProfile != null &&
- this.mVpnProfile.mConnections.length > 0 &&
- ((Gateway) obj).mVpnProfile.mConnections.length > 0 &&
- this.mVpnProfile.mConnections[0].mServerName != null &&
- this.mVpnProfile.mConnections[0].mServerName.equals(((Gateway) obj).mVpnProfile.mConnections[0].mServerName)) ||
- this.mVpnProfile == null && ((Gateway) obj).mVpnProfile == null;
- }
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
index 2bd666bf..0ba0f207 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
@@ -36,7 +36,7 @@ public class GatewaySelector {
}
}
- Log.e(TAG, "There are less than " + nClosest + " Gateways available.");
+ Log.e(TAG, "There are less than " + (nClosest + 1) + " Gateways available.");
return null;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
index 6bd7b4a3..0847a07e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
@@ -28,11 +28,12 @@ import org.json.JSONObject;
import java.lang.reflect.Type;
import java.util.ArrayList;
-import java.util.List;
+import java.util.LinkedHashMap;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.utils.PreferenceHelper;
+import static se.leap.bitmaskclient.Constants.GATEWAYS;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
@@ -45,11 +46,10 @@ public class GatewaysManager {
private Context context;
private SharedPreferences preferences;
- private List<Gateway> gateways = new ArrayList<>();
+ private LinkedHashMap<String, Gateway> gateways = new LinkedHashMap<>();
private Type listType = new TypeToken<ArrayList<Gateway>>() {}.getType();
- GatewaysManager(Context context, SharedPreferences preferences) {
- this.context = context;
+ GatewaysManager(SharedPreferences preferences) {
this.preferences = preferences;
}
@@ -58,7 +58,7 @@ public class GatewaysManager {
* @return the n closest Gateway
*/
public Gateway select(int nClosest) {
- GatewaySelector gatewaySelector = new GatewaySelector(gateways);
+ GatewaySelector gatewaySelector = new GatewaySelector(new ArrayList<>(gateways.values()));
return gatewaySelector.select(nClosest);
}
@@ -88,37 +88,21 @@ public class GatewaysManager {
*/
void fromEipServiceJson(JSONObject eipDefinition) {
try {
- JSONArray gatewaysDefined = eipDefinition.getJSONArray("gateways");
+ JSONArray gatewaysDefined = eipDefinition.getJSONArray(GATEWAYS);
for (int i = 0; i < gatewaysDefined.length(); i++) {
JSONObject gw = gatewaysDefined.getJSONObject(i);
- if (isOpenVpnGateway(gw)) {
- JSONObject secrets = secretsConfiguration();
- Gateway aux = new Gateway(eipDefinition, secrets, gw, this.context);
- if (!gateways.contains(aux)) {
- addGateway(aux);
- }
+ JSONObject secrets = secretsConfiguration();
+ Gateway aux = new Gateway(eipDefinition, secrets, gw, this.context);
+ if (gateways.get(aux.getRemoteIP()) == null) {
+ addGateway(aux);
}
}
- } catch (JSONException e) {
+ } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
- /**
- * check if a gateway is an OpenVpn gateway
- * @param gateway to check
- * @return true if gateway is an OpenVpn gateway otherwise false
- */
- private boolean isOpenVpnGateway(JSONObject gateway) {
- try {
- String transport = gateway.getJSONObject("capabilities").getJSONArray("transport").toString();
- return transport.contains("openvpn");
- } catch (JSONException e) {
- return false;
- }
- }
-
private JSONObject secretsConfiguration() {
JSONObject result = new JSONObject();
try {
@@ -137,7 +121,7 @@ public class GatewaysManager {
}
private void addGateway(Gateway gateway) {
- gateways.add(gateway);
+ gateways.put(gateway.getRemoteIP(), gateway);
}
/**
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
index 6f0ccf18..d9bf5dd3 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
@@ -20,48 +20,125 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.HashMap;
import java.util.Iterator;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ConfigParser;
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.Provider;
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
+import static se.leap.bitmaskclient.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.Constants.IP_ADDRESS;
+import static se.leap.bitmaskclient.Constants.OPTIONS;
+import static se.leap.bitmaskclient.Constants.PORTS;
+import static se.leap.bitmaskclient.Constants.PROTOCOLS;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.Constants.REMOTE;
+import static se.leap.bitmaskclient.Constants.TRANSPORT;
+import static se.leap.bitmaskclient.Constants.TYPE;
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_IP;
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_PORT;
public class VpnConfigGenerator {
-
- private JSONObject general_configuration;
+ private JSONObject generalConfiguration;
private JSONObject gateway;
private JSONObject secrets;
+ private JSONObject obfs4Transport;
+ private int apiVersion;
+
public final static String TAG = VpnConfigGenerator.class.getSimpleName();
private final String newLine = System.getProperty("line.separator"); // Platform new line
- public VpnConfigGenerator(JSONObject general_configuration, JSONObject secrets, JSONObject gateway) {
- this.general_configuration = general_configuration;
+ public VpnConfigGenerator(JSONObject generalConfiguration, JSONObject secrets, JSONObject gateway, int apiVersion) throws ConfigParser.ConfigParseError {
+ this.generalConfiguration = generalConfiguration;
this.gateway = gateway;
this.secrets = secrets;
+ this.apiVersion = apiVersion;
+ checkCapabilities();
}
- public String generate() {
- return
- generalConfiguration()
- + newLine
- + gatewayConfiguration()
- + newLine
- + secretsConfiguration()
- + newLine
- + androidCustomizations();
+ public void checkCapabilities() throws ConfigParser.ConfigParseError {
+
+ try {
+ if (apiVersion == 3) {
+ JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT);
+ for (int i = 0; i < supportedTransports.length(); i++) {
+ JSONObject transport = supportedTransports.getJSONObject(i);
+ if (transport.getString(TYPE).equals(OBFS4.toString())) {
+ obfs4Transport = transport;
+ break;
+ }
+ }
+ }
+
+ } catch (JSONException e) {
+ throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields");
+ }
+ }
+
+ public HashMap<Connection.TransportType, VpnProfile> generateVpnProfiles() throws
+ ConfigParser.ConfigParseError,
+ NumberFormatException,
+ JSONException,
+ IOException {
+ HashMap<Connection.TransportType, VpnProfile> profiles = new HashMap<>();
+ profiles.put(OPENVPN, createProfile(OPENVPN));
+ if (supportsObfs4()) {
+ profiles.put(OBFS4, createProfile(OBFS4));
+ }
+ return profiles;
+ }
+
+ private boolean supportsObfs4(){
+ return obfs4Transport != null;
+ }
+
+ private String getConfigurationString(Connection.TransportType transportType) {
+ return generalConfiguration()
+ + newLine
+ + gatewayConfiguration(transportType)
+ + newLine
+ + androidCustomizations()
+ + newLine
+ + secretsConfiguration();
+ }
+
+ private VpnProfile createProfile(Connection.TransportType transportType) throws IOException, ConfigParser.ConfigParseError, JSONException {
+ String configuration = getConfigurationString(transportType);
+ ConfigParser icsOpenvpnConfigParser = new ConfigParser();
+ icsOpenvpnConfigParser.parseConfig(new StringReader(configuration));
+ if (transportType == OBFS4) {
+ icsOpenvpnConfigParser.setObfs4Options(getObfs4Options());
+ }
+ return icsOpenvpnConfigParser.convertProfile(transportType);
+ }
+
+ private Obfs4Options getObfs4Options() throws JSONException {
+ JSONObject transportOptions = obfs4Transport.getJSONObject(OPTIONS);
+ String iatMode = transportOptions.getString("iat-mode");
+ String cert = transportOptions.getString("cert");
+ String port = obfs4Transport.getJSONArray(PORTS).getString(0);
+ String ip = gateway.getString(IP_ADDRESS);
+ return new Obfs4Options(ip, port, cert, iatMode);
}
private String generalConfiguration() {
String commonOptions = "";
try {
- Iterator keys = general_configuration.keys();
+ Iterator keys = generalConfiguration.keys();
while (keys.hasNext()) {
String key = keys.next().toString();
commonOptions += key + " ";
- for (String word : String.valueOf(general_configuration.get(key)).split(" "))
+ for (String word : String.valueOf(generalConfiguration.get(key)).split(" "))
commonOptions += word + " ";
commonOptions += newLine;
@@ -76,41 +153,95 @@ public class VpnConfigGenerator {
return commonOptions;
}
- private String gatewayConfiguration() {
+ private String gatewayConfiguration(Connection.TransportType transportType) {
String remotes = "";
- String ipAddressKeyword = "ip_address";
- String remoteKeyword = "remote";
- String portsKeyword = "ports";
- String protocolKeyword = "protocols";
- String capabilitiesKeyword = "capabilities";
-
+ StringBuilder stringBuilder = new StringBuilder();
try {
- String ip_address = gateway.getString(ipAddressKeyword);
- JSONObject capabilities = gateway.getJSONObject(capabilitiesKeyword);
- JSONArray ports = capabilities.getJSONArray(portsKeyword);
- for (int i = 0; i < ports.length(); i++) {
- String port_specific_remotes = "";
- int port = ports.getInt(i);
- JSONArray protocols = capabilities.getJSONArray(protocolKeyword);
- for (int j = 0; j < protocols.length(); j++) {
- String protocol = protocols.optString(j);
- String new_remote = remoteKeyword + " " + ip_address + " " + port + " " + protocol + newLine;
-
- port_specific_remotes += new_remote;
- }
- remotes += port_specific_remotes;
+ String ipAddress = gateway.getString(IP_ADDRESS);
+ JSONObject capabilities = gateway.getJSONObject(CAPABILITIES);
+ switch (apiVersion) {
+ default:
+ case 1:
+ case 2:
+ gatewayConfigApiv1(stringBuilder, ipAddress, capabilities);
+ break;
+ case 3:
+ JSONArray transports = capabilities.getJSONArray(TRANSPORT);
+ gatewayConfigApiv3(transportType, stringBuilder, ipAddress, transports);
+ break;
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
+
+ remotes = stringBuilder.toString();
if (remotes.endsWith(newLine)) {
remotes = remotes.substring(0, remotes.lastIndexOf(newLine));
}
return remotes;
}
+ private void gatewayConfigApiv3(Connection.TransportType transportType, StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException {
+ if (transportType == OBFS4) {
+ obfs4GatewayConfigApiv3(stringBuilder, ipAddress, transports);
+ } else {
+ ovpnGatewayConfigApi3(stringBuilder, ipAddress, transports);
+ }
+ }
+
+ private void gatewayConfigApiv1(StringBuilder stringBuilder, String ipAddress, JSONObject capabilities) throws JSONException {
+ int port;
+ String protocol;
+ JSONArray ports = capabilities.getJSONArray(PORTS);
+ for (int i = 0; i < ports.length(); i++) {
+ port = ports.getInt(i);
+ JSONArray protocols = capabilities.getJSONArray(PROTOCOLS);
+ for (int j = 0; j < protocols.length(); j++) {
+ protocol = protocols.optString(j);
+ String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
+ stringBuilder.append(newRemote);
+ }
+ }
+ }
+
+ private void ovpnGatewayConfigApi3(StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException {
+ String port;
+ String protocol;
+ JSONObject openvpnTransport = getTransport(transports, OPENVPN);
+ JSONArray ports = openvpnTransport.getJSONArray(PORTS);
+ for (int j = 0; j < ports.length(); j++) {
+ port = ports.getString(j);
+ JSONArray protocols = openvpnTransport.getJSONArray(PROTOCOLS);
+ for (int k = 0; k < protocols.length(); k++) {
+ protocol = protocols.optString(k);
+ String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
+ stringBuilder.append(newRemote);
+ }
+ }
+ }
+
+ private JSONObject getTransport(JSONArray transports, Connection.TransportType transportType) throws JSONException {
+ JSONObject selectedTransport = new JSONObject();
+ for (int i = 0; i < transports.length(); i++) {
+ JSONObject transport = transports.getJSONObject(i);
+ if (transport.getString(TYPE).equals(transportType.toString())) {
+ selectedTransport = transport;
+ break;
+ }
+ }
+ return selectedTransport;
+ }
+
+ private void obfs4GatewayConfigApiv3(StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException {
+ JSONObject obfs4Transport = getTransport(transports, OBFS4);
+ String route = "route " + ipAddress + " 255.255.255.255 net_gateway" + newLine;
+ stringBuilder.append(route);
+ String remote = REMOTE + " " + DISPATCHER_IP + " " + DISPATCHER_PORT + " " + obfs4Transport.getJSONArray(PROTOCOLS).getString(0) + newLine;
+ stringBuilder.append(remote);
+ }
+
private String secretsConfiguration() {
try {
String ca =
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java
new file mode 100644
index 00000000..0d6aa61e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java
@@ -0,0 +1,204 @@
+/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */
+/* See LICENSE for licensing information */
+
+package se.leap.bitmaskclient.pluggableTransports;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.TimeoutException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class BinaryInstaller {
+
+ File installFolder;
+ Context context;
+
+ public BinaryInstaller(Context context, File installFolder)
+ {
+ this.installFolder = installFolder;
+
+ this.context = context;
+ }
+
+ public void deleteDirectory(File file) {
+ if( file.exists() ) {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ for(int i=0; i<files.length; i++) {
+ if(files[i].isDirectory()) {
+ deleteDirectory(files[i]);
+ }
+ else {
+ files[i].delete();
+ }
+ }
+ }
+
+ file.delete();
+ }
+ }
+
+ private final static String COMMAND_RM_FORCE = "rm -f ";
+ private final static String MP3_EXT = ".mp3";
+ //
+ /*
+ * Extract the resources from the APK file using ZIP
+ */
+ public File installResource (String basePath, String assetKey, boolean overwrite) throws IOException, FileNotFoundException, TimeoutException
+ {
+
+ InputStream is;
+ File outFile;
+
+ outFile = new File(installFolder, assetKey);
+
+ if (outFile.exists() && (!overwrite)) {
+ Log.d("BINARY_INSTALLER", "Binary already exists! Using " + outFile.getCanonicalPath());
+ return outFile;
+ }
+
+ deleteDirectory(installFolder);
+ installFolder.mkdirs();
+
+ Log.d("BINARY_INSTALLER", "Search asset in " + basePath + "/" + assetKey);
+
+ is = context.getAssets().open(basePath + '/' + assetKey);
+ streamToFile(is,outFile, false, false);
+ setExecutable(outFile);
+
+ Log.d("BINARY_INSTALLER", "Asset copied from " + basePath + "/" + assetKey + " to: " + outFile.getCanonicalPath());
+
+ return outFile;
+ }
+
+
+ private final static int FILE_WRITE_BUFFER_SIZE = 1024*8;
+ /*
+ * Write the inputstream contents to the file
+ */
+ public static boolean streamToFile(InputStream stm, File outFile, boolean append, boolean zip) throws IOException
+
+ {
+ byte[] buffer = new byte[FILE_WRITE_BUFFER_SIZE];
+
+ int bytecount;
+
+ OutputStream stmOut = new FileOutputStream(outFile.getAbsolutePath(), append);
+ ZipInputStream zis = null;
+
+ if (zip)
+ {
+ zis = new ZipInputStream(stm);
+ ZipEntry ze = zis.getNextEntry();
+ stm = zis;
+
+ }
+
+ while ((bytecount = stm.read(buffer)) > 0)
+ {
+
+ stmOut.write(buffer, 0, bytecount);
+
+ }
+
+ stmOut.close();
+ stm.close();
+
+ if (zis != null)
+ zis.close();
+
+
+ return true;
+
+ }
+
+ //copy the file from inputstream to File output - alternative impl
+ public static boolean copyFile (InputStream is, File outputFile)
+ {
+
+ try {
+ if (outputFile.exists())
+ outputFile.delete();
+
+ boolean newFile = outputFile.createNewFile();
+ DataOutputStream out = new DataOutputStream(new FileOutputStream(outputFile));
+ DataInputStream in = new DataInputStream(is);
+
+ int b = -1;
+ byte[] data = new byte[1024];
+
+ while ((b = in.read(data)) != -1) {
+ out.write(data);
+ }
+
+ if (b == -1); //rejoice
+
+ //
+ out.flush();
+ out.close();
+ in.close();
+ // chmod?
+
+ return newFile;
+
+
+ } catch (IOException ex) {
+ Log.e("Binaryinstaller", "error copying binary", ex);
+ return false;
+ }
+
+ }
+
+ /**
+ * Copies a raw resource file, given its ID to the given location
+ * @param ctx context
+ * @param resid resource id
+ * @param file destination file
+ * @param mode file permissions (E.g.: "755")
+ * @throws IOException on error
+ * @throws InterruptedException when interrupted
+ */
+ public static void copyRawFile(Context ctx, int resid, File file, String mode, boolean isZipd) throws IOException, InterruptedException
+ {
+ final String abspath = file.getAbsolutePath();
+ // Write the iptables binary
+ final FileOutputStream out = new FileOutputStream(file);
+ InputStream is = ctx.getResources().openRawResource(resid);
+
+ if (isZipd)
+ {
+ ZipInputStream zis = new ZipInputStream(is);
+ ZipEntry ze = zis.getNextEntry();
+ is = zis;
+ }
+
+ byte buf[] = new byte[1024];
+ int len;
+ while ((len = is.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ out.close();
+ is.close();
+ // Change the permissions
+ Runtime.getRuntime().exec("chmod "+mode+" "+abspath).waitFor();
+ }
+
+
+ private void setExecutable(File fileBin) {
+ fileBin.setReadable(true);
+ fileBin.setExecutable(true);
+ fileBin.setWritable(false);
+ fileBin.setWritable(true, true);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java
new file mode 100644
index 00000000..8e787b57
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java
@@ -0,0 +1,216 @@
+/**
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.pluggableTransports;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.StringTokenizer;
+
+
+/**
+ * Created by cyberta on 22.02.19.
+ */
+
+public class Dispatcher {
+ private static final String ASSET_KEY = "piedispatcher";
+ public static final String DISPATCHER_PORT = "4430";
+ public static final String DISPATCHER_IP = "127.0.0.1";
+ private static final String TAG = Dispatcher.class.getName();
+ private final String remoteIP;
+ private final String remotePort;
+ private final String certificate;
+ private final String iatMode;
+ private File fileDispatcher;
+ private Context context;
+ private Thread dispatcherThread = null;
+ private int dispatcherPid = -1;
+
+ public Dispatcher(Context context, Obfs4Options obfs4Options) {
+ this.context = context.getApplicationContext();
+ this.remoteIP = obfs4Options.remoteIP;
+ this.remotePort = obfs4Options.remotePort;
+ this.certificate = obfs4Options.cert;
+ this.iatMode = obfs4Options.iatMode;
+ }
+
+ @WorkerThread
+ public void initSync() {
+ try {
+ fileDispatcher = installDispatcher();
+
+ // start dispatcher
+ dispatcherThread = new Thread(() -> {
+ try {
+ StringBuilder dispatcherLog = new StringBuilder();
+ String dispatcherCommand = fileDispatcher.getCanonicalPath() +
+ " -transparent" +
+ " -client" +
+ " -state " + context.getFilesDir().getCanonicalPath() + "/state" +
+ " -target " + remoteIP + ":" + remotePort +
+ " -transports obfs4" +
+ " -options \"" + String.format("{\\\"cert\\\": \\\"%s\\\", \\\"iatMode\\\": \\\"%s\\\"}\"", certificate, iatMode) +
+ " -logLevel DEBUG -enableLogging" +
+ " -proxylistenaddr "+ DISPATCHER_IP + ":" + DISPATCHER_PORT;
+
+ Log.d(TAG, "dispatcher command: " + dispatcherCommand);
+ runBlockingCmd(new String[]{dispatcherCommand}, dispatcherLog);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ dispatcherThread.start();
+
+ // get pid of dispatcher, try several times in case the dispatcher
+ // process is not spawned yet
+ StringBuilder log = new StringBuilder();
+ String pidCommand = "ps | grep piedispatcher";
+ for (int i = 0; i < 5; i++) {
+ runBlockingCmd(new String[]{pidCommand}, log);
+ if (!TextUtils.isEmpty(log)) {
+ break;
+ }
+ Thread.sleep(100);
+ }
+
+ String output = log.toString();
+ StringTokenizer st = new StringTokenizer(output, " ");
+ st.nextToken(); // proc owner
+ dispatcherPid = Integer.parseInt(st.nextToken().trim());
+ } catch(Exception e){
+ if (dispatcherThread.isAlive()) {
+ Log.e(TAG, e.getMessage() + ". Shutting down Dispatcher thread.");
+ stop();
+ }
+ }
+ }
+
+ public String getPort() {
+ return DISPATCHER_PORT;
+ }
+
+ public void stop() {
+ Log.d(TAG, "Shutting down Dispatcher thread.");
+ if (dispatcherThread != null && dispatcherThread.isAlive()) {
+ try {
+ killProcess(dispatcherPid);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ dispatcherThread.interrupt();
+ }
+ }
+
+ private void killProcess(int pid) throws Exception {
+ String killPid = "kill -9 " + pid;
+ runCmd(new String[]{killPid}, null, false);
+ }
+
+ public boolean isRunning() {
+ return dispatcherThread != null && dispatcherThread.isAlive();
+ }
+
+ private File installDispatcher(){
+ File fileDispatcher = null;
+ BinaryInstaller bi = new BinaryInstaller(context,context.getFilesDir());
+
+ String arch = System.getProperty("os.arch");
+ if (arch.contains("arm"))
+ arch = "armeabi-v7a";
+ else
+ arch = "x86";
+
+ try {
+ fileDispatcher = bi.installResource(arch, ASSET_KEY, false);
+ } catch (Exception ioe) {
+ Log.d(TAG,"Couldn't install dispatcher: " + ioe);
+ }
+
+ return fileDispatcher;
+ }
+
+ @WorkerThread
+ private void runBlockingCmd(String[] cmds, StringBuilder log) throws Exception {
+ runCmd(cmds, log, true);
+ }
+
+ @WorkerThread
+ private int runCmd(String[] cmds, StringBuilder log,
+ boolean waitFor) throws Exception {
+
+ int exitCode = -1;
+ Process proc = Runtime.getRuntime().exec("sh");
+ OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream());
+
+ try {
+ for (String cmd : cmds) {
+ Log.d(TAG, "executing CMD: " + cmd);
+ out.write(cmd);
+ out.write("\n");
+ }
+
+ out.flush();
+ out.write("exit\n");
+ out.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ out.close();
+ }
+
+ if (waitFor) {
+ // Consume the "stdout"
+ InputStreamReader reader = new InputStreamReader(proc.getInputStream());
+ readToLogString(reader, log);
+
+ // Consume the "stderr"
+ reader = new InputStreamReader(proc.getErrorStream());
+ readToLogString(reader, log);
+
+ try {
+ exitCode = proc.waitFor();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return exitCode;
+ }
+
+ private void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException {
+ final char buf[] = new char[10];
+ int read = 0;
+ try {
+ while ((read = reader.read(buf)) != -1) {
+ if (log != null)
+ log.append(buf, 0, read);
+ }
+ } catch (IOException e) {
+ reader.close();
+ throw new IOException(e);
+ }
+ reader.close();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
new file mode 100644
index 00000000..2f9cb732
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
@@ -0,0 +1,18 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import java.io.Serializable;
+
+public class Obfs4Options implements Serializable {
+ public String cert;
+ public String iatMode;
+ public String remoteIP;
+ public String remotePort;
+
+ public Obfs4Options(String remoteIP, String remotePort, String cert, String iatMode) {
+ this.cert = cert;
+ this.iatMode = iatMode;
+ this.remoteIP = remoteIP;
+ this.remotePort = remotePort;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
new file mode 100644
index 00000000..175e236a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.pluggableTransports;
+
+import android.util.Log;
+
+import shapeshifter.ShapeShifter;
+
+public class Shapeshifter {
+
+ public static final String DISPATCHER_PORT = "4430";
+ public static final String DISPATCHER_IP = "127.0.0.1";
+ private static final String TAG = Shapeshifter.class.getSimpleName();
+
+ ShapeShifter shapeShifter;
+
+ public Shapeshifter(Obfs4Options options) {
+ shapeShifter = new ShapeShifter();
+ shapeShifter.setIatMode(Long.valueOf(options.iatMode));
+ shapeShifter.setSocksAddr(DISPATCHER_IP+":"+DISPATCHER_PORT);
+ shapeShifter.setTarget(options.remoteIP+":"+options.remotePort);
+ shapeShifter.setCert(options.cert);
+ Log.d(TAG, "shapeshifter initialized with: iat - " + shapeShifter.getIatMode() +
+ "; socksAddr - " + shapeShifter.getSocksAddr() +
+ "; target addr - " + shapeShifter.getTarget() +
+ "; cert - " + shapeShifter.getCert());
+ }
+
+ public boolean start() {
+ try {
+ shapeShifter.open();
+ Log.d(TAG, "shapeshifter opened");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public boolean stop() {
+ try {
+ shapeShifter.close();
+ Log.d(TAG, "shapeshifter closed");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
index 9eb4c972..44b2a45d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
@@ -31,6 +31,7 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.Constants.USE_PLUGGABLE_TRANSPORTS;
import static se.leap.bitmaskclient.Constants.EXCLUDED_APPS;
/**
@@ -213,6 +214,22 @@ public class PreferenceHelper {
apply();
}
+ public static boolean getUsePluggableTransports(Context context) {
+ if (context == null) {
+ return false;
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getBoolean(USE_PLUGGABLE_TRANSPORTS, false);
+ }
+
+ public static void usePluggableTransports(Context context, boolean isEnabled) {
+ if (context == null) {
+ return;
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ preferences.edit().putBoolean(USE_PLUGGABLE_TRANSPORTS, isEnabled).apply();
+ }
+
public static void saveBattery(Context context, boolean isEnabled) {
if (context == null) {
return;
@@ -284,5 +301,4 @@ public class PreferenceHelper {
preferences.edit().putString(key, value).apply();
}
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java
new file mode 100644
index 00000000..02347b05
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java
@@ -0,0 +1,104 @@
+package se.leap.bitmaskclient.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.v7.widget.SwitchCompat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import se.leap.bitmaskclient.R;
+
+public class IconSwitchEntry extends LinearLayout {
+
+ private TextView textView;
+ private TextView subtitleView;
+ private ImageView iconView;
+ private SwitchCompat switchView;
+ private CompoundButton.OnCheckedChangeListener checkedChangeListener;
+
+ public IconSwitchEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconSwitchEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconSwitchEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconSwitchEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_switch_list_item, this, true);
+ textView = rootview.findViewById(android.R.id.text1);
+ subtitleView = rootview.findViewById(R.id.subtitle);
+ iconView = rootview.findViewById(R.id.material_icon);
+ switchView = rootview.findViewById(R.id.option_switch);
+
+ if (attrs != null) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconSwitchEntry);
+
+ String entryText = typedArray.getString(R.styleable.IconTextEntry_text);
+ if (entryText != null) {
+ textView.setText(entryText);
+ }
+
+ String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle);
+ if (subtitle != null) {
+ subtitleView.setText(subtitle);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon);
+ if (drawable != null) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ typedArray.recycle();
+ }
+ }
+
+ public void setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener listener) {
+ checkedChangeListener = listener;
+ switchView.setOnCheckedChangeListener(checkedChangeListener);
+ }
+
+ public void setText(@StringRes int id) {
+ textView.setText(id);
+ }
+
+ public void setIcon(@DrawableRes int id) {
+ iconView.setImageResource(id);
+ }
+
+ public void setChecked(boolean isChecked) {
+ switchView.setChecked(isChecked);
+ }
+
+ public void setCheckedQuietly(boolean isChecked) {
+ switchView.setOnCheckedChangeListener(null);
+ switchView.setChecked(isChecked);
+ switchView.setOnCheckedChangeListener(checkedChangeListener);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java
new file mode 100644
index 00000000..0e86f506
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java
@@ -0,0 +1,92 @@
+package se.leap.bitmaskclient.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import se.leap.bitmaskclient.R;
+
+
+public class IconTextEntry extends LinearLayout {
+
+ private TextView textView;
+ private ImageView iconView;
+ private TextView subtitleView;
+
+ public IconTextEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconTextEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconTextEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconTextEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_icon_text_list_item, this, true);
+ textView = rootview.findViewById(android.R.id.text1);
+ subtitleView = rootview.findViewById(R.id.subtitle);
+ iconView = rootview.findViewById(R.id.material_icon);
+
+ if (attrs != null) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconTextEntry);
+
+ String entryText = typedArray.getString(R.styleable.IconTextEntry_text);
+ if (entryText != null) {
+ textView.setText(entryText);
+ }
+
+ String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle);
+ if (subtitle != null) {
+ subtitleView.setText(subtitle);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon);
+ if (drawable != null) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ typedArray.recycle();
+ }
+
+
+ }
+
+ public void setText(@StringRes int id) {
+ textView.setText(id);
+ }
+
+ public void setText(CharSequence text) {
+ textView.setText(text);
+ }
+
+ public void setIcon(@DrawableRes int id) {
+ iconView.setImageResource(id);
+ }
+
+}
diff --git a/app/src/main/res/drawable-hdpi/ic_bridge_36.png b/app/src/main/res/drawable-hdpi/ic_bridge_36.png
new file mode 100644
index 00000000..e3acd2d1
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_bridge_36.png b/app/src/main/res/drawable-mdpi/ic_bridge_36.png
new file mode 100644
index 00000000..6c45a2d8
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xhdpi/ic_bridge_36.png
new file mode 100644
index 00000000..6f89408c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png
new file mode 100644
index 00000000..d00613b8
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png
new file mode 100644
index 00000000..8f531f5a
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml b/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml
index 0192e080..798b47e3 100644
--- a/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml
+++ b/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml
@@ -1,6 +1,6 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_container"
- android:layout_height="wrap_content"
+ android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_width="match_parent"
android:orientation="horizontal"
xmlns:tools="http://schemas.android.com/tools">
@@ -27,6 +27,33 @@
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:minHeight="?android:attr/listPreferredItemHeight"
tools:text="TEST"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
/>
-</LinearLayout>
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="8dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
+</RelativeLayout>
diff --git a/app/src/main/res/layout-xlarge/v_switch_list_item.xml b/app/src/main/res/layout-xlarge/v_switch_list_item.xml
index d692070e..3d81af11 100644
--- a/app/src/main/res/layout-xlarge/v_switch_list_item.xml
+++ b/app/src/main/res/layout-xlarge/v_switch_list_item.xml
@@ -29,6 +29,25 @@
tools:text="TEST"
android:layout_toEndOf="@id/material_icon"
android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
+ />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="4dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
/>
<android.support.v7.widget.SwitchCompat
@@ -45,4 +64,10 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:checked="false"
tools:text="" />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml
index b04d7b87..f6c9b2bb 100644
--- a/app/src/main/res/layout/f_drawer_main.xml
+++ b/app/src/main/res/layout/f_drawer_main.xml
@@ -1,70 +1,134 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
+ android:layout_width="match_parent"
android:background="@color/colorBackground"
tools:context="se.leap.bitmaskclient.drawer.NavigationDrawerFragment"
android:clickable="true"
- android:focusable="true">
+ android:focusable="true"
+ android:fillViewport="true"
+ >
- <FrameLayout
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="150dp">
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ >
- <android.support.v7.widget.AppCompatImageView
- android:id="@+id/background"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:adjustViewBounds="false"
- android:cropToPadding="false"
- android:scaleType="fitXY"
- app:srcCompat="@drawable/background_drawer" />
-
- <android.support.v7.widget.AppCompatImageView
- android:id="@+id/foreground"
+ <FrameLayout
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="centerInside"
- app:srcCompat="@drawable/drawer_logo" />
- </FrameLayout>
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:layout_height="150dp">
- <ListView
- android:id="@+id/accountList"
- android:layout_width="match_parent"
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:adjustViewBounds="false"
+ android:cropToPadding="false"
+ android:scaleType="fitXY"
+ app:srcCompat="@drawable/background_drawer" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/foreground"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerInside"
+ app:srcCompat="@drawable/drawer_logo" />
+ </FrameLayout>
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/account"
android:layout_height="wrap_content"
- android:isScrollContainer="false"
+ android:layout_width="wrap_content"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/switch_provider"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:text="@string/switch_provider_menu_option"
+ app:icon="@drawable/ic_switch_provider_36"
+ android:visibility="gone"
/>
<View
- android:id="@+id/divider"
- android:layout_below="@id/accountList"
android:layout_width="match_parent"
- android:layout_height="1px"
- android:background="@android:color/darker_gray"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:minHeight="20dp"
+ android:background="@color/black800_high_transparent"
/>
- <FrameLayout
+ <se.leap.bitmaskclient.views.IconSwitchEntry
+ android:id="@+id/battery_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/save_battery"
+ app:icon="@drawable/ic_battery_36"
+ />
+
+ <se.leap.bitmaskclient.views.IconSwitchEntry
+ android:id="@+id/bridges_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/nav_drawer_obfuscated_connection"
+ app:subtitle="@string/nav_drawer_subtitle_obfuscated_connection"
+ app:icon="@drawable/ic_bridge_36"
+ android:visibility="gone"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/always_on_vpn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/always_on_vpn"
+ app:subtitle="@string/subtitle_always_on_vpn"
+ app:icon="@drawable/ic_always_on_36"
+ android:visibility="gone"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/exclude_apps"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/exclude_apps_fragment_title"
+ app:icon="@drawable/ic_shield_remove_grey600_36dp"
+ android:visibility="gone"
+ />
+
+ <View
android:layout_width="match_parent"
+ android:layout_height="20dp"
+ android:background="@color/black800_high_transparent"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/donate"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignTop="@id/divider"
- android:layout_alignParentBottom="true"
- >
- <ListView
- android:id="@+id/settingsList"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- />
- </FrameLayout>
+ app:text="@string/donate_title"
+ app:icon="@drawable/ic_donate_36"
+ android:visibility="gone"
+ />
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/log"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/log_fragment_title"
+ app:icon="@drawable/ic_log_36"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/about"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/about_fragment_title"
+ app:icon="@drawable/ic_about_36"
+ />
- </RelativeLayout>
+ </LinearLayout>
-</LinearLayout> \ No newline at end of file
+</ScrollView>
diff --git a/app/src/main/res/layout/v_icon_text_list_item.xml b/app/src/main/res/layout/v_icon_text_list_item.xml
index 0631b2fc..64cc474a 100644
--- a/app/src/main/res/layout/v_icon_text_list_item.xml
+++ b/app/src/main/res/layout/v_icon_text_list_item.xml
@@ -1,6 +1,6 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_container"
- android:layout_height="wrap_content"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:layout_width="match_parent"
android:orientation="horizontal"
xmlns:tools="http://schemas.android.com/tools">
@@ -26,6 +26,33 @@
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
tools:text="TEST"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
/>
-</LinearLayout>
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="4dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
+</RelativeLayout>
diff --git a/app/src/main/res/layout/v_switch_list_item.xml b/app/src/main/res/layout/v_switch_list_item.xml
index 26060a73..967d7a97 100644
--- a/app/src/main/res/layout/v_switch_list_item.xml
+++ b/app/src/main/res/layout/v_switch_list_item.xml
@@ -29,6 +29,25 @@
tools:text="TEST"
android:layout_toEndOf="@id/material_icon"
android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
+ />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="4dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
/>
<android.support.v7.widget.SwitchCompat
@@ -45,4 +64,11 @@
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:checked="false"
tools:text="" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index eb9626bc..d3a88b81 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -1,6 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+ <!--TODO: check that it's not needed and throw it out!-->
<declare-styleable name="foo">
<attr name="textColorError" format="color" />
</declare-styleable>
+
+ <attr name="text" format="string|reference"/>
+ <attr name="icon" format="reference"/>
+ <attr name="subtitle" format="string|reference"/>
+ <declare-styleable name="IconSwitchEntry">
+ <attr name="text"/>
+ <attr name="subtitle" />
+ <attr name="icon"/>
+ </declare-styleable>
+
+ <declare-styleable name="IconTextEntry">
+ <attr name="text"/>
+ <attr name="subtitle" />
+ <attr name="icon"/>
+ </declare-styleable>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e685cff5..27f508d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -103,6 +103,7 @@
<string name="save_battery">Save battery</string>
<string name="save_battery_message">Background data connections will hibernate when your phone is inactive.</string>
<string name="always_on_vpn">Always-on VPN</string>
+ <string name="subtitle_always_on_vpn">Open Android System Settings</string>
<string name="do_not_show_again">Do not show again</string>
<string name="always_on_vpn_user_message">To enable always-on VPN in Android VPN Settings click on the configure icon [img src] and turn the switch on.</string>
<string name="always_on_blocking_vpn_user_message">To protect your privacy optimally, you should also activate the option \"Block connections without VPN\".</string>
@@ -111,6 +112,10 @@
<string name="donate_message">LEAP depends on donations and grants. Please donate today if you value secure communication that is easy for both the end-user and the service provider.</string>
<string name="donate_button_remind_later">Remind me later</string>
<string name="donate_button_donate">Donate</string>
+ <string name="obfuscated_connection">Using an obfuscated connection.</string>
+ <string name="obfuscated_connection_try">Trying an obfuscated connection.</string>
+ <string name="nav_drawer_obfuscated_connection">Using Bridges</string>
+ <string name="nav_drawer_subtitle_obfuscated_connection">Circumvent VPN filtering</string>
<string name="warning_exclude_apps_message">Be careful of excluding apps from VPN. This will reveal your identity and compromise your security.</string>
</resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 51a8ea0e..7e98ccf4 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -20,4 +20,8 @@
<item name="android:windowBackground">@drawable/splash_page</item>
</style>
+ <style name="invisibleTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+
</resources>