diff options
18 files changed, 726 insertions, 92 deletions
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..b14a9e44 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java @@ -0,0 +1,286 @@ +/** + * 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.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 LinearLayoutManager layoutManager; + 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); + 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(); + } + }); + } + + 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); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (USE_PLUGGABLE_TRANSPORTS.equals(key)) { + locationListAdapter.updateData(gatewaysManager.getGatewayLocations()); + } + } + + @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.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/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java index 2ce0e597..cbfe8a71 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 @@ -70,6 +70,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 +80,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 +116,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 +258,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen initSaveBatteryEntry(); initAlwaysOnVpnEntry(); initExcludeAppsEntry(); + initManualGatewayEntry(); initShowExperimentalHint(); initTetheringEntry(); initFirewallEntry(); @@ -412,6 +416,23 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen }); } + private void initManualGatewayEntry() { + 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 +648,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 +673,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/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/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java index 1160986e..628b4c15 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); } 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 763b5449..78b33355 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java @@ -35,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; @@ -146,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 */ 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 b06e894e..5f6910bf 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java @@ -30,6 +30,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; @@ -37,8 +39,10 @@ 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; @@ -72,15 +76,46 @@ 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, selectedCity); + return getGatewayFromPresortedList(nClosest, transportType, city); } - return getGatewayFromTimezoneCalculation(nClosest, transportType, selectedCity); + 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, @Nullable String city) { List<Gateway> list = new ArrayList<>(gateways.values()); 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 Binary files differnew file mode 100644 index 00000000..0d395564 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_map_marker_star_black_36dp.png 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 Binary files differnew file mode 100644 index 00000000..3065f57e --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_map_marker_star_black_36dp.png 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 Binary files differnew file mode 100644 index 00000000..57016c8c --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_map_marker_star_black_36dp.png 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 Binary files differnew file mode 100644 index 00000000..3f0cdaba --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_map_marker_star_black_36dp.png 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 Binary files differnew file mode 100644 index 00000000..a4afda4d --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_map_marker_star_black_36dp.png 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..15dd0425 100644 --- a/app/src/main/res/layout/f_drawer_main.xml +++ b/app/src/main/res/layout/f_drawer_main.xml @@ -98,6 +98,15 @@ 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" + /> + <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> |