diff options
Diffstat (limited to 'app/src/main/java')
12 files changed, 567 insertions, 17 deletions
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); + } + } } |