From ff5188d7de1df36c5d71d309a08d290560e9d337 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 22 Mar 2021 18:49:19 +0100 Subject: initial draft for gateway selection on Android --- .../base/fragments/GatewaySelectionFragment.java | 286 +++++++++++++++++++++ .../base/fragments/NavigationDrawerFragment.java | 24 ++ .../leap/bitmaskclient/base/models/Location.java | 42 +++ .../leap/bitmaskclient/base/models/Provider.java | 9 + .../bitmaskclient/base/views/IconSwitchEntry.java | 27 +- .../java/se/leap/bitmaskclient/eip/Gateway.java | 22 ++ .../se/leap/bitmaskclient/eip/GatewaysManager.java | 41 ++- 7 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/models/Location.java (limited to 'app/src/main/java') 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 . + */ +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 { + private static final String TAG = LocationListAdapter.class.getSimpleName(); + private List 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 data) { + values = data; + notifyDataSetChanged(); + } + + public LocationListAdapter(List 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 . + */ 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 getGatewayLocations() { + String selectedCity = PreferenceHelper.getPreferredCity(context); + HashMap locationNames = new HashMap<>(); + ArrayList 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 list = new ArrayList<>(gateways.values()); -- cgit v1.2.3