summaryrefslogtreecommitdiff
path: root/app/src/main/java/se
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/java/se
parent7820e8c0819a10c5b4729678607681fcfe30cbae (diff)
parent685da193ea29f3e7a8a42d55747dfb2f956f23b6 (diff)
Merge branch 'pluggableTransports2'
Diffstat (limited to 'app/src/main/java/se')
-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
21 files changed, 1234 insertions, 571 deletions
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);
+ }
+
+}