summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcyberta <cyberta@riseup.net>2021-05-18 16:13:44 +0000
committercyberta <cyberta@riseup.net>2021-05-18 16:13:44 +0000
commit15490dae1ac8670d1288367cb2ac8fd43c48a045 (patch)
tree7ee5ae99b504c5a737471f6d1d9ee331008dfc20
parent48bd56b48c96cc62557675a82ac0ca1865f9aa8e (diff)
parent4b8ea1252cddfd54278676a8b2f64eb937f92c2d (diff)
Merge branch 'gateway_selector' into 'master'
Gateway selector See merge request leap/bitmask_android!132
-rw-r--r--CHANGELOG1
-rw-r--r--app/build.gradle8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java293
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java10
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java28
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Location.java42
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java31
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java37
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java102
-rw-r--r--app/src/main/res/drawable-hdpi/ic_map_marker_star_black_36dp.pngbin0 -> 919 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_map_marker_star_black_36dp.pngbin0 -> 645 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_map_marker_star_black_36dp.pngbin0 -> 1182 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_map_marker_star_black_36dp.pngbin0 -> 1809 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_map_marker_star_black_36dp.pngbin0 -> 2284 bytes
-rw-r--r--app/src/main/res/layout-xlarge/v_switch_list_item.xml93
-rw-r--r--app/src/main/res/layout/f_drawer_main.xml10
-rw-r--r--app/src/main/res/layout/f_gateway_selection.xml99
-rw-r--r--app/src/main/res/layout/v_select_text_list_item.xml63
-rw-r--r--app/src/main/res/layout/v_switch_list_item.xml97
-rw-r--r--app/src/main/res/values/strings.xml6
-rw-r--r--app/src/main/res/values/untranslatable.xml4
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java159
-rw-r--r--app/src/test/resources/v4/riseup_eipservice_for_geoip_v4.json169
-rw-r--r--app/src/test/resources/v4/riseup_geoip_v1.json14
-rw-r--r--app/src/test/resources/v4/riseup_geoip_v4.json41
30 files changed, 1241 insertions, 107 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 1dfbebc1..94fe422b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,7 @@
features:
- new languages: Burmese, Uyghur
- more translations, <3 to all translators!
+- initial gateway selector implementation: get the best connection from your chosen location
bugfixes:
- reduce crash rate
diff --git a/app/build.gradle b/app/build.gradle
index 4ff533e7..76a34feb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,8 +13,8 @@ android {
defaultConfig {
applicationId "se.leap.bitmaskclient"
- versionCode 152
- versionName "1.0.8RC1"
+ versionCode 153
+ versionName "1.0.8RC2"
minSdkVersion 16
targetSdkVersion 30
vectorDrawables.useSupportLibrary = true
@@ -34,6 +34,8 @@ android {
buildConfigField 'int', 'donation_reminder_duration', '30'
//skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead
buildConfigField 'boolean', 'priotize_anonymous_usage', 'false'
+ //allow manual gateway selection
+ buildConfigField 'boolean', 'allow_manual_gateway_selection', 'false'
// static update url pointing to the latest stable release apk
buildConfigField "String", "update_apk_url", '"https://dl.bitmask.net/client/android/Bitmask-Android-latest.apk"'
@@ -113,6 +115,8 @@ android {
//skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead
buildConfigField 'boolean', 'priotize_anonymous_usage', 'true'
+ //allow manual gateway selection
+ buildConfigField 'boolean', 'allow_manual_gateway_selection', 'true'
//Build Config Fields for automatic apk update checks
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
index cf64905a..9d689e5d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
@@ -30,6 +30,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.providersetup.ProviderListActivity;
import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.base.models.FeatureVersionCode;
@@ -157,6 +158,11 @@ public class StartActivity extends Activity{
}
}
+ // always check if manual gateway selection feature switch has been disabled
+ if (!BuildConfig.allow_manual_gateway_selection && PreferenceHelper.getPreferredCity(this) != null) {
+ PreferenceHelper.setPreferredCity(this, null);
+ }
+
// ensure all upgrades have passed before storing new information
storeAppVersion();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
new file mode 100644
index 00000000..84024962
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
@@ -0,0 +1,293 @@
+/**
+ * Copyright (c) 2021 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.base.fragments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatButton;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+import java.util.Observable;
+import java.util.Observer;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.MainActivity;
+import se.leap.bitmaskclient.base.models.Location;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.base.views.IconSwitchEntry;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.eip.GatewaysManager;
+
+import static android.content.Context.MODE_PRIVATE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
+import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setPreferredCity;
+
+public class GatewaySelectionFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer {
+
+ private static final String TAG = GatewaySelectionFragment.class.getSimpleName();
+
+
+ private RecyclerView recyclerView;
+ private LocationListAdapter locationListAdapter;
+ private IconSwitchEntry autoSelectionSwitch;
+ private AppCompatButton vpnButton;
+ private GatewaysManager gatewaysManager;
+ private SharedPreferences preferences;
+ private EipStatus eipStatus;
+
+ public GatewaySelectionFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ gatewaysManager = new GatewaysManager(getContext());
+ preferences = getContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ eipStatus = EipStatus.getInstance();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.f_gateway_selection, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ initRecyclerView();
+ initAutoSelectionSwitch();
+ initVpnButton();
+ eipStatus.addObserver(this);
+ preferences.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ preferences.unregisterOnSharedPreferenceChangeListener(this);
+ eipStatus.deleteObserver(this);
+ }
+
+
+
+ private void initRecyclerView() {
+ recyclerView = (RecyclerView) getActivity().findViewById(R.id.gatewaySelection_list);
+ recyclerView.setHasFixedSize(true);
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this.getContext());
+ recyclerView.setLayoutManager(layoutManager);
+ locationListAdapter = new LocationListAdapter(gatewaysManager.getGatewayLocations());
+ recyclerView.setAdapter(locationListAdapter);
+ recyclerView.setVisibility(getPreferredCity(getContext()) == null ? INVISIBLE : VISIBLE);
+ }
+
+ private void initAutoSelectionSwitch() {
+ autoSelectionSwitch = getActivity().findViewById(R.id.automatic_gateway_switch);
+ autoSelectionSwitch.setSingleLine(false);
+ autoSelectionSwitch.setSubtitle(getString(R.string.gateway_selection_warning, getString(R.string.app_name)));
+ autoSelectionSwitch.setChecked(getPreferredCity(getContext()) == null);
+ autoSelectionSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ recyclerView.setVisibility(!isChecked ? VISIBLE : View.GONE);
+ Log.d(TAG, "autoselection enabled: " + isChecked);
+ if (isChecked) {
+ PreferenceHelper.setPreferredCity(getContext(), null);
+ locationListAdapter.resetSelection();
+ }
+ setVpnButtonState();
+ });
+ }
+
+ private void initVpnButton() {
+ vpnButton = getActivity().findViewById(R.id.vpn_button);
+ setVpnButtonState();
+ vpnButton.setOnClickListener(v -> {
+ EipCommand.startVPN(getContext(), false);
+ Intent intent = new Intent(getContext(), MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setAction(ACTION_SHOW_VPN_FRAGMENT);
+ startActivity(intent);
+ });
+ }
+
+ private void setVpnButtonState() {
+ if (eipStatus.isDisconnected()) {
+ vpnButton.setText(R.string.vpn_button_turn_on);
+ } else {
+ vpnButton.setText(R.string.reconnect);
+ }
+ vpnButton.setEnabled(
+ (locationListAdapter.selectedLocation != null && locationListAdapter.selectedLocation.selected) ||
+ autoSelectionSwitch.isChecked());
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (USE_PLUGGABLE_TRANSPORTS.equals(key)) {
+ locationListAdapter.updateData(gatewaysManager.getGatewayLocations());
+ setVpnButtonState();
+ } else if (PREFERRED_CITY.equals(key)) {
+ setVpnButtonState();
+ }
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o instanceof EipStatus) {
+ eipStatus = (EipStatus) o;
+ Activity activity = getActivity();
+ if (activity != null) {
+ activity.runOnUiThread(this::setVpnButtonState);
+ }
+ }
+
+ }
+
+ static class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapter.ViewHolder> {
+ private static final String TAG = LocationListAdapter.class.getSimpleName();
+ private List<Location> values;
+ private Location selectedLocation = null;
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ public AppCompatTextView locationLabel;
+ public AppCompatTextView qualityLabel;
+ public AppCompatImageView checkedIcon;
+ public View layout;
+
+ public ViewHolder(View v) {
+ super(v);
+ layout = v;
+ locationLabel = (AppCompatTextView) v.findViewById(R.id.location);
+ qualityLabel = (AppCompatTextView) v.findViewById(R.id.quality);
+ checkedIcon = (AppCompatImageView) v.findViewById(R.id.checked_icon);
+ }
+ }
+
+ public void add(int position, Location item) {
+ values.add(position, item);
+ notifyItemInserted(position);
+ }
+
+ public void remove(int position) {
+ values.remove(position);
+ notifyItemRemoved(position);
+ }
+
+ public void resetSelection() {
+ if (selectedLocation != null) {
+ selectedLocation.selected = false;
+ notifyDataSetChanged();
+ }
+ }
+
+ public void updateData(List<Location> data) {
+ values = data;
+ notifyDataSetChanged();
+ }
+
+ public LocationListAdapter(List<Location> data) {
+ values = data;
+ }
+
+ @NonNull
+ @Override
+ public LocationListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(
+ parent.getContext());
+ View v = inflater.inflate(R.layout.v_select_text_list_item, parent, false);
+ return new ViewHolder(v);
+ }
+
+ // Replace the contents of a view (invoked by the layout manager)
+ @Override
+ public void onBindViewHolder(ViewHolder holder, final int position) {
+ final Location location = values.get(position);
+ holder.locationLabel.setText(location.name);
+ holder.layout.setOnClickListener(v -> {
+ Log.d(TAG, "view at position clicked: " + position);
+ if (selectedLocation == null) {
+ selectedLocation = location;
+ selectedLocation.selected = true;
+ } else if (selectedLocation.name.equals(location.name)){
+ selectedLocation.selected = !selectedLocation.selected;
+ } else {
+ selectedLocation.selected = false;
+ selectedLocation = location;
+ selectedLocation.selected = true;
+ }
+ setPreferredCity(holder.layout.getContext(), selectedLocation.selected ? selectedLocation.name : null);
+ holder.checkedIcon.setVisibility(selectedLocation.selected ? VISIBLE : INVISIBLE);
+ notifyDataSetChanged();
+ });
+ Drawable checkIcon = DrawableCompat.wrap(holder.layout.getContext().getResources().getDrawable(R.drawable.ic_check_bold)).mutate();
+ DrawableCompat.setTint(checkIcon, ContextCompat.getColor(holder.layout.getContext(), R.color.colorSuccess));
+ holder.checkedIcon.setImageDrawable(checkIcon);
+ holder.checkedIcon.setVisibility(location.selected ? VISIBLE : INVISIBLE);
+ holder.qualityLabel.setText(getQualityString(location.averageLoad));
+ if (location.selected) {
+ selectedLocation = location;
+ }
+ }
+
+ public String getQualityString(double quality) {
+ if (quality == 0) {
+ return "";
+ } else if (quality < 0.25) {
+ return "BAD";
+ } else if (quality < 0.6) {
+ return "AVERAGE";
+ } else if (quality < 0.8){
+ return "GOOD";
+ } else {
+ return "EXCELLENT";
+ }
+ }
+
+
+ @Override
+ public int getItemCount() {
+ return values.size();
+ }
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
index f036b411..4034bd04 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
@@ -18,6 +18,7 @@ package se.leap.bitmaskclient.base.fragments;
import android.app.Dialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,6 +33,8 @@ import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setPreferredCity;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.R.string.warning_option_try_ovpn;
import static se.leap.bitmaskclient.R.string.warning_option_try_pt;
@@ -120,7 +123,12 @@ public class MainActivityErrorDialog extends DialogFragment {
break;
case NO_MORE_GATEWAYS:
builder.setNegativeButton(R.string.cancel, (dialog, id) -> {});
- if (provider.supportsPluggableTransports()) {
+ if (getPreferredCity(applicationContext) != null) {
+ builder.setPositiveButton(R.string.warning_option_try_best, (dialog, which) -> {
+ setPreferredCity(applicationContext, null);
+ EipCommand.startVPN(applicationContext, false);
+ });
+ } else if (provider.supportsPluggableTransports()) {
if (getUsePluggableTransports(applicationContext)) {
builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> {
usePluggableTransports(applicationContext, false);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
index 2ce0e597..5cae1591 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
@@ -50,6 +50,7 @@ import java.util.Observer;
import java.util.Set;
import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.base.FragmentManagerEnhanced;
import se.leap.bitmaskclient.base.MainActivity;
import se.leap.bitmaskclient.base.models.Provider;
@@ -70,6 +71,7 @@ import static android.view.View.VISIBLE;
import static se.leap.bitmaskclient.base.BitmaskApp.getRefWatcher;
import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL;
import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION;
+import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
@@ -79,6 +81,7 @@ 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.log_fragment_title;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSaveBattery;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getShowAlwaysOnDialog;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
@@ -114,6 +117,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
private IconSwitchEntry saveBattery;
private IconTextEntry tethering;
private IconSwitchEntry firewall;
+ private IconTextEntry manualGatewaySelection;
private View experimentalFeatureFooter;
private boolean userLearnedDrawer;
@@ -255,6 +259,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
initSaveBatteryEntry();
initAlwaysOnVpnEntry();
initExcludeAppsEntry();
+ initManualGatewayEntry();
initShowExperimentalHint();
initTetheringEntry();
initFirewallEntry();
@@ -412,6 +417,26 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
});
}
+ private void initManualGatewayEntry() {
+ if (!BuildConfig.allow_manual_gateway_selection) {
+ return;
+ }
+ manualGatewaySelection = drawerView.findViewById(R.id.manualGatewaySelection);
+ String preferredGateway = getPreferredCity(getContext());
+ String subtitle = preferredGateway != null ? preferredGateway : getString(R.string.gateway_selection_best_location);
+ manualGatewaySelection.setSubtitle(subtitle);
+ boolean show = ProviderObservable.getInstance().getCurrentProvider().hasGatewaysInDifferentLocations();
+ manualGatewaySelection.setVisibility(show ? VISIBLE : GONE);
+
+ manualGatewaySelection.setOnClickListener(v -> {
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ closeDrawer();
+ Fragment fragment = new GatewaySelectionFragment();
+ setActionBarTitle(R.string.gateway_selection_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
private void initTetheringEntry() {
tethering = drawerView.findViewById(R.id.tethering);
boolean show = showExperimentalFeatures(getContext());
@@ -627,6 +652,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
account.setText(currentProvider.getName());
initUseBridgesEntry();
+ initManualGatewayEntry();
}
private void updateExcludeAppsSubtitle(IconTextEntry excludeApps, int number) {
@@ -651,6 +677,8 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
initUseBridgesEntry();
} else if (key.equals(USE_IPv6_FIREWALL)) {
initFirewallEntry();
+ } else if (key.equals(PREFERRED_CITY)) {
+ initManualGatewayEntry();
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index a0d295bd..3edfbb3d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -40,6 +40,7 @@ public interface Constants {
String USE_IPv6_FIREWALL = "use_ipv6_firewall";
String RESTART_ON_UPDATE = "restart_on_update";
String LAST_UPDATE_CHECK = "last_update_check";
+ String PREFERRED_CITY = "preferred_city";
//////////////////////////////////////////////
@@ -173,4 +174,7 @@ public interface Constants {
String OPENVPN_CONFIGURATION = "openvpn_configuration";
String GATEWAYS = "gateways";
String HOST = "host";
+ String SORTED_GATEWAYS = "sortedGateways";
+ String FULLNESS = "fullness";
+ String OVERLOAD = "overload";
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Location.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Location.java
new file mode 100644
index 00000000..ae7818ba
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Location.java
@@ -0,0 +1,42 @@
+package se.leap.bitmaskclient.base.models;
+
+import androidx.annotation.NonNull;
+
+public class Location {
+ @NonNull public String name;
+ public double averageLoad;
+ public int numberOfGateways;
+ public boolean selected;
+
+ public Location(@NonNull String name, double averageLoad, int numberOfGateways, boolean selected) {
+ this.name = name;
+ this.averageLoad = averageLoad;
+ this.numberOfGateways = numberOfGateways;
+ this.selected = selected;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Location)) return false;
+
+ Location location = (Location) o;
+
+ if (Double.compare(location.averageLoad, averageLoad) != 0) return false;
+ if (numberOfGateways != location.numberOfGateways) return false;
+ if (selected != location.selected) return false;
+ return name.equals(location.name);
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ result = name.hashCode();
+ temp = Double.doubleToLongBits(averageLoad);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ result = 31 * result + numberOfGateways;
+ result = 31 * result + (selected ? 1 : 0);
+ return result;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
index 97f1019b..5d8b4e5d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
@@ -32,6 +32,7 @@ import java.util.Locale;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGISTERED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
@@ -333,6 +334,14 @@ public final class Provider implements Parcelable {
&& !getEipServiceJson().has(ERRORS);
}
+ public boolean hasGatewaysInDifferentLocations() {
+ try {
+ return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1;
+ } catch (NullPointerException | JSONException e) {
+ return false;
+ }
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index d31c7a20..a3d1314e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
@@ -24,6 +24,7 @@ import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_SHARED_PREFS_B
import static se.leap.bitmaskclient.base.models.Constants.EXCLUDED_APPS;
import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK;
import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE;
+import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
@@ -203,6 +204,14 @@ public class PreferenceHelper {
return getBoolean(context, ALWAYS_ON_SHOW_DIALOG, true);
}
+ public static String getPreferredCity(Context context) {
+ return getString(context, PREFERRED_CITY, null);
+ }
+
+ public static void setPreferredCity(Context context, String city) {
+ putString(context, PREFERRED_CITY, city);
+ }
+
public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) {
JSONObject result = new JSONObject();
try {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java
index 1160986e..b6d72ab6 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java
@@ -1,3 +1,19 @@
+/**
+ * Copyright (c) 2021 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.base.views;
import android.annotation.TargetApi;
@@ -51,7 +67,7 @@ public class IconSwitchEntry extends LinearLayout {
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);
+ textView = rootview.findViewById(R.id.title);
subtitleView = rootview.findViewById(R.id.subtitle);
iconView = rootview.findViewById(R.id.material_icon);
switchView = rootview.findViewById(R.id.option_switch);
@@ -88,6 +104,15 @@ public class IconSwitchEntry extends LinearLayout {
textView.setText(id);
}
+ public void setSubtitle(CharSequence text) {
+ subtitleView.setText(text);
+ }
+
+ public void setSingleLine(boolean singleLine) {
+ textView.setSingleLine(singleLine);
+ subtitleView.setSingleLine(singleLine);
+ }
+
public void showSubtitle(boolean show) {
subtitleView.setVisibility(show ? VISIBLE : GONE);
}
@@ -106,6 +131,10 @@ public class IconSwitchEntry extends LinearLayout {
switchView.setOnCheckedChangeListener(checkedChangeListener);
}
+ public boolean isChecked() {
+ return switchView.isChecked();
+ }
+
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
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 74226250..d632c09e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -84,6 +84,7 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.ensureNotOnMainThread;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_PROFILE;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE;
@@ -317,7 +318,12 @@ public final class EIP extends JobIntentService implements Observer {
Connection.TransportType transportType = getUsePluggableTransports(this) ? OBFS4 : OPENVPN;
if (gateway == null ||
(profile = gateway.getProfile(transportType)) == null) {
- setErrorResult(result, NO_MORE_GATEWAYS.toString(), getStringResourceForNoMoreGateways(), getString(R.string.app_name));
+ String preferredLocation = getPreferredCity(getApplicationContext());
+ if (preferredLocation != null) {
+ setErrorResult(result, NO_MORE_GATEWAYS.toString(), getStringResourceForNoMoreGateways(), getString(R.string.app_name), preferredLocation);
+ } else {
+ setErrorResult(result, NO_MORE_GATEWAYS.toString(), getStringResourceForNoMoreGateways(), getString(R.string.app_name));
+ }
return;
}
@@ -527,7 +533,10 @@ public final class EIP extends JobIntentService implements Observer {
private @StringRes int getStringResourceForNoMoreGateways() {
- if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
+ boolean isManualGatewaySelection = PreferenceHelper.getLastConnectedVpnProfile(getApplicationContext()) != null;
+ if (isManualGatewaySelection) {
+ return R.string.warning_no_more_gateways_manual_gw_selection;
+ } else if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
if (PreferenceHelper.getUsePluggableTransports(getApplicationContext())) {
return R.string.warning_no_more_gateways_use_ovpn;
} else {
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 6b44856e..78b33355 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
@@ -17,6 +17,7 @@
package se.leap.bitmaskclient.eip;
import android.content.Context;
+
import androidx.annotation.NonNull;
import com.google.gson.Gson;
@@ -34,12 +35,14 @@ import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import static se.leap.bitmaskclient.base.models.Constants.FULLNESS;
import static se.leap.bitmaskclient.base.models.Constants.HOST;
import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS;
import static se.leap.bitmaskclient.base.models.Constants.LOCATION;
import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS;
import static se.leap.bitmaskclient.base.models.Constants.NAME;
import static se.leap.bitmaskclient.base.models.Constants.OPENVPN_CONFIGURATION;
+import static se.leap.bitmaskclient.base.models.Constants.OVERLOAD;
import static se.leap.bitmaskclient.base.models.Constants.TIMEZONE;
import static se.leap.bitmaskclient.base.models.Constants.VERSION;
@@ -59,7 +62,9 @@ public class Gateway {
private JSONObject generalConfiguration;
private JSONObject secrets;
private JSONObject gateway;
+ private JSONObject load;
+ // the location of a gateway is its name
private String name;
private int timezone;
private int apiVersion;
@@ -71,9 +76,15 @@ public class Gateway {
*/
public Gateway(JSONObject eipDefinition, JSONObject secrets, JSONObject gateway, Context context)
throws ConfigParser.ConfigParseError, JSONException, IOException {
+ this(eipDefinition, secrets, gateway, null, context);
+ }
+
+ public Gateway(JSONObject eipDefinition, JSONObject secrets, JSONObject gateway, JSONObject load, Context context)
+ throws ConfigParser.ConfigParseError, JSONException, IOException {
this.gateway = gateway;
this.secrets = secrets;
+ this.load = load;
generalConfiguration = getGeneralConfiguration(eipDefinition);
timezone = getTimezone(eipDefinition);
@@ -82,6 +93,10 @@ public class Gateway {
vpnProfiles = createVPNProfiles(context);
}
+ public void updateLoad(JSONObject load) {
+ this.load = load;
+ }
+
private void addProfileInfos(Context context, HashMap<Connection.TransportType, VpnProfile> profiles) {
Set<String> excludedAppsVpn = PreferenceHelper.getExcludedApps(context);
for (VpnProfile profile : profiles.values()) {
@@ -133,6 +148,26 @@ public class Gateway {
}
}
+ public boolean hasLoadInfo() {
+ return load != null;
+ }
+
+ public double getFullness() {
+ try {
+ return load.getDouble(FULLNESS);
+ } catch (JSONException | NullPointerException e) {
+ return 0;
+ }
+ }
+
+ public boolean isOverloaded() {
+ try {
+ return load.getBoolean(OVERLOAD);
+ } catch (JSONException | NullPointerException e) {
+ return false;
+ }
+ }
+
/**
* Create and attach the VpnProfile to our gateway object
*/
@@ -156,7 +191,7 @@ public class Gateway {
return vpnProfiles.get(transportType);
}
- public boolean suppoortsTransport(Connection.TransportType transportType) {
+ public boolean supportsTransport(Connection.TransportType transportType) {
return vpnProfiles.get(transportType) != 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 a5d4c416..77ecfc5f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
@@ -17,6 +17,9 @@
package se.leap.bitmaskclient.eip;
import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@@ -28,6 +31,8 @@ import org.json.JSONObject;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -35,14 +40,19 @@ import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.connection.Connection;
+import se.leap.bitmaskclient.base.models.Location;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
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.base.models.Constants.GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.HOST;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.models.Constants.SORTED_GATEWAYS;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
/**
@@ -67,24 +77,56 @@ public class GatewaysManager {
* @return the n closest Gateway
*/
public Gateway select(int nClosest) {
- Connection.TransportType transportType = getUsePluggableTransports(context) ? OBFS4 : OPENVPN;
+ String selectedCity = getPreferredCity(context);
+ return select(nClosest, selectedCity);
+ }
+ public Gateway select(int nClosest, String city) {
+ Connection.TransportType transportType = getUsePluggableTransports(context) ? OBFS4 : OPENVPN;
if (presortedList.size() > 0) {
- return getGatewayFromPresortedList(nClosest, transportType);
+ return getGatewayFromPresortedList(nClosest, transportType, city);
}
- return getGatewayFromTimezoneCalculation(nClosest, transportType);
+ return getGatewayFromTimezoneCalculation(nClosest, transportType, city);
}
+ public List<Location> getGatewayLocations() {
+ String selectedCity = PreferenceHelper.getPreferredCity(context);
+ HashMap<String, Integer> locationNames = new HashMap<>();
+ ArrayList<Location> locations = new ArrayList<>();
+ int n = 0;
+ Gateway gateway;
+ while ((gateway = select(n, null)) != null) {
+ if (!locationNames.containsKey(gateway.getName())) {
+ locationNames.put(gateway.getName(), locations.size());
+ Location location = new Location(
+ gateway.getName(),
+ gateway.getFullness(),
+ 1,
+ gateway.getName().equals(selectedCity));
+ locations.add(location);
+ } else {
+ int index = locationNames.get(gateway.getName());
+ Location location = locations.get(index);
+ location.averageLoad = (location.numberOfGateways * location.averageLoad + gateway.getFullness()) / (location.numberOfGateways + 1);
+ location.numberOfGateways += 1;
+ locations.set(index, location);
+ }
+ n++;
+ }
+
+ return locations;
+ }
- private Gateway getGatewayFromTimezoneCalculation(int nClosest, Connection.TransportType transportType) {
+ private Gateway getGatewayFromTimezoneCalculation(int nClosest, Connection.TransportType transportType, @Nullable String city) {
List<Gateway> list = new ArrayList<>(gateways.values());
GatewaySelector gatewaySelector = new GatewaySelector(list);
Gateway gateway;
int found = 0;
int i = 0;
while ((gateway = gatewaySelector.select(i)) != null) {
- if (gateway.suppoortsTransport(transportType)) {
+ if ((city == null && gateway.supportsTransport(transportType)) ||
+ (gateway.getName().equals(city) && gateway.supportsTransport(transportType))) {
if (found == nClosest) {
return gateway;
}
@@ -95,10 +137,11 @@ public class GatewaysManager {
return null;
}
- private Gateway getGatewayFromPresortedList(int nClosest, Connection.TransportType transportType) {
+ private Gateway getGatewayFromPresortedList(int nClosest, Connection.TransportType transportType, @Nullable String city) {
int found = 0;
for (Gateway gateway : presortedList) {
- if (gateway.suppoortsTransport(transportType)) {
+ if ((city == null && gateway.supportsTransport(transportType)) ||
+ (gateway.getName().equals(city) && gateway.supportsTransport(transportType))) {
if (found == nClosest) {
return gateway;
}
@@ -125,7 +168,7 @@ public class GatewaysManager {
Connection.TransportType transportType = profile.mUsePluggableTransports ? OBFS4 : OPENVPN;
int nClosest = 0;
for (Gateway gateway : presortedList) {
- if (gateway.suppoortsTransport(transportType)) {
+ if (gateway.supportsTransport(transportType)) {
if (profile.equals(gateway.getProfile(transportType))) {
return nClosest;
}
@@ -142,7 +185,7 @@ public class GatewaysManager {
int nClosest = 0;
int i = 0;
while ((gateway = gatewaySelector.select(i)) != null) {
- if (gateway.suppoortsTransport(transportType)) {
+ if (gateway.supportsTransport(transportType)) {
if (profile.equals(gateway.getProfile(transportType))) {
return nClosest;
}
@@ -206,7 +249,7 @@ public class GatewaysManager {
}
}
- private void parseGatewaysFromGeoIpServiceJson(Provider provider) {
+ private void parseSimpleGatewayList(Provider provider) {
try {
JSONObject geoIpJson = provider.getGeoIpJson();
JSONArray gatewaylist = geoIpJson.getJSONArray(GATEWAYS);
@@ -222,10 +265,40 @@ public class GatewaysManager {
}
}
} catch (NullPointerException | JSONException npe) {
- npe.printStackTrace();
+ Log.d(TAG, "No valid geoip json found: " + npe.getLocalizedMessage());
}
}
+ private boolean hasSortedGatewaysWithLoad(@Nullable Provider provider) {
+ if (provider == null) {
+ return false;
+ }
+ JSONObject geoIpJson = provider.getGeoIpJson();
+ return geoIpJson.has(SORTED_GATEWAYS);
+ }
+
+ private void parseGatewaysWithLoad(Provider provider) {
+ try {
+ JSONObject geoIpJson = provider.getGeoIpJson();
+ JSONArray gatewaylist = geoIpJson.getJSONArray(SORTED_GATEWAYS);
+ for (int i = 0; i < gatewaylist.length(); i++) {
+ try {
+ JSONObject load = gatewaylist.getJSONObject(i);
+ String hostName = load.getString(HOST);
+ if (gateways.containsKey(hostName)) {
+ Gateway gateway = gateways.get(hostName);
+ gateway.updateLoad(load);
+ presortedList.add(gateway);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (NullPointerException | JSONException npe) {
+ npe.printStackTrace();
+ }
+ }
+
private JSONObject secretsConfigurationFromCurrentProvider() {
JSONObject result = new JSONObject();
Provider provider = ProviderObservable.getInstance().getCurrentProvider();
@@ -247,6 +320,11 @@ public class GatewaysManager {
private void configureFromCurrentProvider() {
Provider provider = ProviderObservable.getInstance().getCurrentProvider();
parseDefaultGateways(provider);
- parseGatewaysFromGeoIpServiceJson(provider);
+ if (hasSortedGatewaysWithLoad(provider)) {
+ parseGatewaysWithLoad(provider);
+ } else {
+ parseSimpleGatewayList(provider);
+ }
+
}
}
diff --git a/app/src/main/res/drawable-hdpi/ic_map_marker_star_black_36dp.png b/app/src/main/res/drawable-hdpi/ic_map_marker_star_black_36dp.png
new file mode 100644
index 00000000..0d395564
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_map_marker_star_black_36dp.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_map_marker_star_black_36dp.png b/app/src/main/res/drawable-mdpi/ic_map_marker_star_black_36dp.png
new file mode 100644
index 00000000..3065f57e
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_map_marker_star_black_36dp.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_map_marker_star_black_36dp.png b/app/src/main/res/drawable-xhdpi/ic_map_marker_star_black_36dp.png
new file mode 100644
index 00000000..57016c8c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_map_marker_star_black_36dp.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_map_marker_star_black_36dp.png b/app/src/main/res/drawable-xxhdpi/ic_map_marker_star_black_36dp.png
new file mode 100644
index 00000000..3f0cdaba
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_map_marker_star_black_36dp.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_map_marker_star_black_36dp.png b/app/src/main/res/drawable-xxxhdpi/ic_map_marker_star_black_36dp.png
new file mode 100644
index 00000000..a4afda4d
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_map_marker_star_black_36dp.png
Binary files differ
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 3fa2ac7d..02e52d6d 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
@@ -1,7 +1,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_container"
- android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:layout_height="wrap_content"
android:layout_width="match_parent"
>
@@ -10,68 +10,75 @@
android:layout_width="?android:attr/listPreferredItemHeight"
android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_gravity="center"
+ android:layout_alignTop="@+id/textContainer"
+ android:layout_alignBottom="@+id/textContainer"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- tools:src="@drawable/ic_add_circle_outline_grey600_24dp"
- />
+ tools:src="@drawable/ic_add_circle_outline_grey600_24dp" />
- <TextView
- android:id="@android:id/text1"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:textAppearance="?android:attr/textAppearanceListItem"
+ <LinearLayout
+ android:id="@+id/textContainer"
+ android:orientation="vertical"
android:gravity="center_vertical"
- android:maxLines="1"
- android:ellipsize="end"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:paddingRight="?android:attr/listPreferredItemPaddingRight"
- android:minHeight="?android:attr/listPreferredItemHeight"
- tools:text="TEST"
+ android:layout_toStartOf="@+id/option_switch"
+ android:layout_toLeftOf="@+id/option_switch"
android:layout_toEndOf="@id/material_icon"
android:layout_toRightOf="@+id/material_icon"
- android:layout_above="@+id/subtitle"
- />
-
- <TextView
- android:id="@+id/subtitle"
+ android:minHeight="?android:attr/listPreferredItemHeight"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:maxLines="1"
- android:ellipsize="end"
- 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:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ tools:text=".,m.,msdflksdjflksjdflkjsdflksdlsdflkj lskjdf lkjsdf lkjsdf fsdls" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"
+ tools:text="ldflkjdfglkjdfglksjdflksjdf lksjddf lkjsdfl kjlkjsdf lkjsdfl kjsdlfkj lkj sdflk lkjsdlfdkjsdlfkj "
+ tools:visibility="visible" />
+ </LinearLayout>
+
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/option_switch"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAppearance="?android:attr/textAppearanceListItem"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignTop="@id/textContainer"
+ android:layout_alignBottom="@id/textContainer"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:checked="false"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
- android:background="?android:attr/activatedBackgroundIndicator"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:checked="false"
+ android:textAppearance="?android:attr/textAppearanceListItem"
tools:text="" />
+
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray"
- android:layout_alignParentBottom="true"
+ android:layout_alignBottom="@id/textContainer"
/>
</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 d2729998..1f1df7f2 100644
--- a/app/src/main/res/layout/f_drawer_main.xml
+++ b/app/src/main/res/layout/f_drawer_main.xml
@@ -98,6 +98,16 @@
android:visibility="gone"
/>
+ <se.leap.bitmaskclient.base.views.IconTextEntry
+ android:id="@+id/manualGatewaySelection"
+ app:text="@string/gateway_selection_title"
+ app:subtitle="@string/gateway_selection_best_location"
+ app:icon="@drawable/ic_map_marker_star_black_36dp"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:visibility="gone"
+ />
+
<TextView
android:id="@+id/show_experimental_features"
android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/f_gateway_selection.xml b/app/src/main/res/layout/f_gateway_selection.xml
new file mode 100644
index 00000000..f96b9c08
--- /dev/null
+++ b/app/src/main/res/layout/f_gateway_selection.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:context=".base.fragments.GatewaySelectionFragment">
+
+ <LinearLayout
+ android:id="@+id/current_location_container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/current_location_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingStart="@dimen/activity_horizontal_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingEnd="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:text="@string/vpn_securely_routed" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/current_location"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingStart="@dimen/activity_horizontal_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingEnd="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ tools:text="Paris" />
+
+ </LinearLayout>
+
+
+
+ <se.leap.bitmaskclient.base.views.IconSwitchEntry
+ android:id="@+id/automatic_gateway_switch"
+ android:layout_below="@id/current_location_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding"
+ android:paddingStart="@dimen/stdpadding"
+ android:paddingLeft="@dimen/stdpadding"
+ android:paddingEnd="@dimen/stdpadding"
+ android:paddingRight="@dimen/stdpadding"
+ app:text="@string/gateway_selection_automatic"
+ app:subtitle="@string/gateway_selection_warning"
+ app:icon="@drawable/ic_map_marker_star_black_36dp"
+ />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/gatewaySelection_list"
+ android:layout_below="@id/automatic_gateway_switch"
+ android:layout_above="@+id/vpn_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/stdpadding"
+ android:paddingLeft="@dimen/stdpadding"
+ android:paddingRight="@dimen/stdpadding"
+ android:paddingEnd="@dimen/stdpadding"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:visibility="gone"
+ tools:visibility="visible"
+ android:scrollbars="vertical"
+ >
+
+ </androidx.recyclerview.widget.RecyclerView>
+
+ <LinearLayout
+ android:id="@+id/vpn_button_container"
+ android:orientation="horizontal"
+ android:gravity="end"
+ android:layout_alignParentBottom="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/activity_margin"
+ >
+ <androidx.appcompat.widget.AppCompatButton
+ android:id="@+id/vpn_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:text="@string/vpn.button.turn.on" />
+ </LinearLayout>
+
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/v_select_text_list_item.xml b/app/src/main/res/layout/v_select_text_list_item.xml
new file mode 100644
index 00000000..07187016
--- /dev/null
+++ b/app/src/main/res/layout/v_select_text_list_item.xml
@@ -0,0 +1,63 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/item_container"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- views are composed right to left -->
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/checked_icon"
+ android:layout_width="?android:attr/listPreferredItemHeightSmall"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:layout_gravity="center"
+ android:padding="10dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ tools:src="@drawable/ic_check_bold"
+ android:visibility="visible"
+ tools:visibility="visible"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/quality"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/standard_margin"
+ android:paddingLeft="@dimen/standard_margin"
+ android:paddingEnd="@dimen/standard_margin"
+ android:paddingRight="@dimen/standard_margin"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:layout_toLeftOf="@id/checked_icon"
+ android:layout_toStartOf="@id/checked_icon"
+ tools:text="GOOD"
+ />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/location"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_toStartOf="@id/quality"
+ android:layout_toLeftOf="@id/quality"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeightLarge"
+ android:paddingStart="@dimen/standard_margin"
+ android:paddingLeft="@dimen/standard_margin"
+ android:paddingEnd="@dimen/standard_margin"
+ android:paddingRight="@dimen/standard_margin"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textStyle="bold"
+ tools:text="Paris" />
+
+ <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 b595e7ce..1686a99d 100644
--- a/app/src/main/res/layout/v_switch_list_item.xml
+++ b/app/src/main/res/layout/v_switch_list_item.xml
@@ -1,7 +1,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_container"
- android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:layout_height="wrap_content"
android:layout_width="match_parent"
>
@@ -10,69 +10,76 @@
android:layout_width="?android:attr/listPreferredItemHeightSmall"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:layout_gravity="center"
+ android:layout_alignTop="@+id/textContainer"
+ android:layout_alignBottom="@+id/textContainer"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- tools:src="@drawable/ic_add_circle_outline_grey600_24dp"
- />
+ tools:src="@drawable/ic_add_circle_outline_grey600_24dp" />
- <TextView
- android:id="@android:id/text1"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ <LinearLayout
+ android:id="@+id/textContainer"
+ android:orientation="vertical"
android:gravity="center_vertical"
- android:maxLines="1"
- android:ellipsize="end"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:paddingRight="?android:attr/listPreferredItemPaddingRight"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- tools:text="TEST"
+ android:layout_toStartOf="@+id/option_switch"
+ android:layout_toLeftOf="@+id/option_switch"
android:layout_toEndOf="@id/material_icon"
android:layout_toRightOf="@+id/material_icon"
- android:layout_above="@+id/subtitle"
- />
-
- <TextView
- android:id="@+id/subtitle"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:maxLines="1"
- android:ellipsize="end"
- 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:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ tools:text=".,m.,msdflksdjflksjdflkjsdflksdlsdflkj lskjdf lkjsdf lkjsdf fsdls" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="2dp"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"
+ android:singleLine="true"
+ android:ellipsize="end"
+ tools:text="sdlfkjsdf lksdjdf lkj sdldfk jlkj sdf lkj lskdjf sedflkjsdlfjk"
+ tools:visibility="visible" />
+ </LinearLayout>
+
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/option_switch"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignTop="@id/textContainer"
+ android:layout_alignBottom="@id/textContainer"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:checked="false"
android:gravity="center_vertical"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
- android:background="?android:attr/activatedBackgroundIndicator"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:checked="false"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
tools:text="" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray"
- android:layout_alignParentBottom="true"
+ android:layout_alignBottom="@id/textContainer"
/>
</RelativeLayout> \ 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 000effc7..e883b974 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -149,4 +149,10 @@
<string name="version_update_error_pgp_verification">PGP verification error. Ignoring download.</string>
<string name="version_update_error">Update failed.</string>
<string name="version_update_error_permissions">No permissions to install app.</string>
+ <string name="gateway_selection_title">Select location</string>
+ <string name="gateway_selection_warning">%s will find the best connection for you.</string>
+ <string name="gateway_selection_best_location">Location with best connection</string>
+ <string name="gateway_selection_automatic">Automatic</string>
+ <string name="gateway_selection_current_location">Your traffic is currently routed through: </string>
+
</resources>
diff --git a/app/src/main/res/values/untranslatable.xml b/app/src/main/res/values/untranslatable.xml
index 9c6afadd..a08857c1 100644
--- a/app/src/main/res/values/untranslatable.xml
+++ b/app/src/main/res/values/untranslatable.xml
@@ -44,5 +44,9 @@
<string name="circleImageView" translatable="false">CircleImageView</string>
<string name="copyright_circleImageView" translatable="false">Copyright 2014 - 2020 Henning Dodenhof. Licensed under the Apache License, Version 2.0 </string>
+ <!-- gateway selector, move to strings.xml, once the wording is clear -->
+ <string name="warning_no_more_gateways_manual_gw_selection" translatable="false">%1$s could not connect to %2$s. Do you want to try to connect automatically with best location?</string>
+ <string name="warning_option_try_best" translatable="false">Try best location</string>
+
</resources> \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
index 6056f764..81fcd7d5 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -16,9 +16,11 @@ import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.IOException;
+import java.util.List;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
+import se.leap.bitmaskclient.base.models.Location;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.testutils.MockHelper;
@@ -233,6 +235,163 @@ public class GatewaysManagerTest {
}
+ @Test
+ public void testSelectN_selectFromCity_returnsGatewaysInPresortedOrder() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
+
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+ assertEquals("mouette.riseup.net", gatewaysManager.select(0).getHost());
+ assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).getHost());
+ assertEquals("zarapito.riseup.net", gatewaysManager.select(2).getHost());
+ }
+
+ @Test
+ public void testSelectN_selectFromCityWithGeoIpServiceV1_returnsGatewaysInPresortedOrder() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v1.json");
+
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+ assertEquals("mouette.riseup.net", gatewaysManager.select(0).getHost());
+ assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).getHost());
+ assertEquals("zarapito.riseup.net", gatewaysManager.select(2).getHost());
+ }
+
+ @Test
+ public void testSelectN_selectFromCityWithTimezoneCalculation_returnsRandomizedGatewaysOfSelectedCity() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", null);
+
+ provider.setGeoIpJson(new JSONObject());
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+ assertEquals("Paris", gatewaysManager.select(0).getName());
+ assertEquals("Paris", gatewaysManager.select(1).getName());
+ assertEquals("Paris", gatewaysManager.select(2).getName());
+ assertEquals(null, gatewaysManager.select(3));
+ }
+
+ @Test
+ public void testSelectN_selectNAndCity_returnsGatewaysInPresortedOrder() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
+
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+ assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").getHost());
+ assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").getHost());
+ assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").getHost());
+ }
+
+ @Test
+ public void testSelectN_selectNAndCityWithGeoIpServiceV1_returnsGatewaysInPresortedOrder() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v1.json");
+
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+ assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").getHost());
+ assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").getHost());
+ assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").getHost());
+ }
+
+ @Test
+ public void testSelectN_selectNAndCityWithTimezoneCalculation_returnsRandomizedGatewaysOfSelectedCity() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", null);
+
+ provider.setGeoIpJson(new JSONObject());
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+
+ assertEquals("Paris", gatewaysManager.select(0, "Paris").getName());
+ assertEquals("Paris", gatewaysManager.select(1, "Paris").getName());
+ assertEquals("Paris", gatewaysManager.select(2, "Paris").getName());
+ assertEquals(null, gatewaysManager.select(3, "Paris"));
+ }
+
+ @Test
+ public void testSelectN_selectFromCityWithTimezoneCalculationCityNotExisting_returnsNull() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
+
+ provider.setGeoIpJson(new JSONObject());
+ MockHelper.mockProviderObserver(provider);
+ //use openvpn, not pluggable transports
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+ assertNull(gatewaysManager.select(0, "Stockholm"));
+ }
+
+ @Test
+ public void testGetLocations_openvpn() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
+
+ MockHelper.mockProviderObserver(provider);
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+ List<Location> locations = gatewaysManager.getGatewayLocations();
+
+ assertEquals(3, locations.size());
+ for (Location location : locations) {
+ if ("Paris".equals(location.name)) {
+ assertEquals(3, location.numberOfGateways);
+ // manually calculate average load of paris gateways in "v4/riseup_geoip_v4.json"
+ double averageLoad = (0.3 + 0.36 + 0.92) / 3.0;
+ assertEquals(averageLoad, location.averageLoad);
+ }
+ }
+ }
+
+ @Test
+ public void testGetLocations_obfs4() {
+ Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
+
+ MockHelper.mockProviderObserver(provider);
+ mockStatic(PreferenceHelper.class);
+ when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(true);
+ GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
+ List<Location> locations = gatewaysManager.getGatewayLocations();
+
+ assertEquals(2, locations.size());
+ for (Location location : locations) {
+ if ("Montreal".equals(location.name)) {
+ assertEquals(1, location.numberOfGateways);
+ assertEquals(0.59, location.averageLoad);
+ }
+ if ("Paris".equals(location.name)) {
+ // checks that only gateways supporting obfs4 are taken into account
+ assertEquals(1, location.numberOfGateways);
+ assertEquals(0.36, location.averageLoad);
+ }
+ }
+
+ }
+
+
private String getJsonStringFor(String filename) throws IOException {
return TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename));
}
diff --git a/app/src/test/resources/v4/riseup_eipservice_for_geoip_v4.json b/app/src/test/resources/v4/riseup_eipservice_for_geoip_v4.json
new file mode 100644
index 00000000..76fbea52
--- /dev/null
+++ b/app/src/test/resources/v4/riseup_eipservice_for_geoip_v4.json
@@ -0,0 +1,169 @@
+{
+ "gateways": [
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "transport":[
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "443"
+ ]
+ }
+ ],
+ "user_ips": false
+ },
+ "host": "zarapito.riseup.net",
+ "ip_address": "212.129.62.247",
+ "location": "paris"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "23042"
+ ],
+ "options": {
+ "cert": "48F/JNm/LKYt7zkDmPHXcw5f3Jqgwg/3OBRrqW14Yj87ATZ4KyAZRV7np4RhCXGSJHgoCQ",
+ "iatMode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "443"
+ ]
+ }
+ ],
+ "user_ips": false
+ },
+ "host": "hoatzin.riseup.net",
+ "ip_address": "212.83.143.67",
+ "location": "paris"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "transport":[
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "443"
+ ]
+ }
+ ],
+ "user_ips": false
+ },
+ "host": "mouette.riseup.net",
+ "ip_address": "163.172.126.44",
+ "location": "paris"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "transport":[
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "443"
+ ]
+ }
+ ],
+ "user_ips": false
+ },
+ "host": "redshank.riseup.net",
+ "ip_address": "212.83.165.160",
+ "location": "amsterdam"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "23042"
+ ],
+ "options": {
+ "cert": "48F/JNm/LKYt7zkDmPHXcw5f3Jqgwg/3OBRrqW14Yj87ATZ4KyAZRV7np4RhCXGSJHgoCQ",
+ "iatMode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "443"
+ ]
+ }
+ ],
+ "user_ips": false
+ },
+ "host": "yal.riseup.net",
+ "ip_address": "199.58.83.10",
+ "location": "montreal"
+ }
+ ],
+ "locations": {
+ "amsterdam": {
+ "country_code": "NL",
+ "hemisphere": "N",
+ "name": "Amsterdam",
+ "timezone": "+2"
+ },
+ "montreal": {
+ "country_code": "CA",
+ "hemisphere": "N",
+ "name": "Montreal",
+ "timezone": "-5"
+ },
+ "paris": {
+ "country_code": "FR",
+ "hemisphere": "N",
+ "name": "Paris",
+ "timezone": "+2"
+ }
+ },
+ "openvpn_configuration": {
+ "auth": "SHA1",
+ "cipher": "AES-128-CBC",
+ "keepalive": "10 30",
+ "tls-cipher": "DHE-RSA-AES128-SHA",
+ "tun-ipv6": true
+ },
+ "serial": 3,
+ "version": 3
+}
diff --git a/app/src/test/resources/v4/riseup_geoip_v1.json b/app/src/test/resources/v4/riseup_geoip_v1.json
new file mode 100644
index 00000000..4e3bda2a
--- /dev/null
+++ b/app/src/test/resources/v4/riseup_geoip_v1.json
@@ -0,0 +1,14 @@
+{
+ "ip":"51.158.144.32",
+ "cc":"FR",
+ "city":"Paris",
+ "lat":48.8628,
+ "lon":2.3292,
+ "gateways":[
+ "mouette.riseup.net",
+ "hoatzin.riseup.net",
+ "yal.riseup.net",
+ "redshank.riseup.net",
+ "zarapito.riseup.net"
+ ]
+} \ No newline at end of file
diff --git a/app/src/test/resources/v4/riseup_geoip_v4.json b/app/src/test/resources/v4/riseup_geoip_v4.json
new file mode 100644
index 00000000..c95a9e6d
--- /dev/null
+++ b/app/src/test/resources/v4/riseup_geoip_v4.json
@@ -0,0 +1,41 @@
+{
+ "ip":"51.158.144.32",
+ "cc":"FR",
+ "city":"Paris",
+ "lat":48.8628,
+ "lon":2.3292,
+ "gateways":[
+ "mouette.riseup.net",
+ "hoatzin.riseup.net",
+ "yal.riseup.net",
+ "redshank.riseup.net",
+ "zarapito.riseup.net"
+ ],
+ "sortedGateways": [
+ {
+ "host": "mouette.riseup.net",
+ "fullness": 0.3,
+ "overload": false
+ },
+ {
+ "host": "hoatzin.riseup.net",
+ "fullness": 0.36,
+ "overload": false
+ },
+ {
+ "host": "yal.riseup.net",
+ "fullness": 0.59,
+ "overload": false
+ },
+ {
+ "host": "redshank.riseup.net",
+ "fullness": 0.8,
+ "overload": false
+ },
+ {
+ "host": "zarapito.riseup.net",
+ "fullness": 0.92,
+ "overload": true
+ }
+ ]
+} \ No newline at end of file