diff options
Diffstat (limited to 'app')
10 files changed, 107 insertions, 154 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 index e4d8ca8b..ee4aea74 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java @@ -19,7 +19,6 @@ 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; @@ -29,23 +28,21 @@ 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.lang.ref.WeakReference; import java.util.List; import java.util.Observable; import java.util.Observer; +import de.blinkt.openvpn.core.VpnStatus; 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.base.views.LocationIndicator; import se.leap.bitmaskclient.eip.EipCommand; import se.leap.bitmaskclient.eip.EipStatus; @@ -55,21 +52,23 @@ 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_BRIDGES; -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 { +interface LocationListSelectionListener { + void onLocationSelected(String name); +} + +public class GatewaySelectionFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer, LocationListSelectionListener { private static final String TAG = GatewaySelectionFragment.class.getSimpleName(); private RecyclerView recyclerView; private LocationListAdapter locationListAdapter; - private IconSwitchEntry autoSelectionSwitch; - private AppCompatButton vpnButton; + private AppCompatTextView currentLocationDescription; + private AppCompatTextView currentLocation; + private AppCompatButton autoSelectionButton; private GatewaysManager gatewaysManager; private SharedPreferences preferences; private EipStatus eipStatus; @@ -97,8 +96,8 @@ public class GatewaySelectionFragment extends Fragment implements SharedPreferen public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initRecyclerView(); - initAutoSelectionSwitch(); - initVpnButton(); + initAutoSelectionButton(); + initCurrentLocationInfoPanel(); eipStatus.addObserver(this); preferences.registerOnSharedPreferenceChangeListener(this); } @@ -113,93 +112,94 @@ public class GatewaySelectionFragment extends Fragment implements SharedPreferen private void initRecyclerView() { - recyclerView = (RecyclerView) getActivity().findViewById(R.id.gatewaySelection_list); + recyclerView = getActivity().findViewById(R.id.gatewaySelection_list); recyclerView.setHasFixedSize(true); LinearLayoutManager layoutManager = new LinearLayoutManager(this.getContext()); recyclerView.setLayoutManager(layoutManager); - locationListAdapter = new LocationListAdapter(gatewaysManager.getGatewayLocations()); + locationListAdapter = new LocationListAdapter(gatewaysManager.getGatewayLocations(), this); recyclerView.setAdapter(locationListAdapter); - recyclerView.setVisibility(getPreferredCity(getContext()) == null ? INVISIBLE : VISIBLE); + recyclerView.setVisibility(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 initAutoSelectionButton() { + autoSelectionButton = getActivity().findViewById(R.id.automatic_gateway_selection_btn); + autoSelectionButton.setOnClickListener(v -> { + startEipService(null); }); } - private void initVpnButton() { - vpnButton = getActivity().findViewById(R.id.vpn_button); - setVpnButtonState(); - vpnButton.setOnClickListener(v -> { + private void initCurrentLocationInfoPanel() { + currentLocationDescription = getActivity().findViewById(R.id.current_location_description); + currentLocation = getActivity().findViewById(R.id.current_location); + setLocationDescription(EipStatus.getInstance(), PreferenceHelper.getPreferredCity(getContext())); + } + + private void setLocationDescription(EipStatus eipStatus, String preferredCity) { + if (eipStatus.isConnected()) { + currentLocationDescription.setText(preferredCity == null ? + R.string.gateway_selection_automatic_location : + R.string.gateway_selection_manual_location); + currentLocation.setText(VpnStatus.getLastConnectedVpnName()); + currentLocation.setVisibility(VISIBLE); + } else if (preferredCity == null) { + currentLocationDescription.setText(R.string.gateway_selection_automatic_not_connected); + currentLocation.setVisibility(INVISIBLE); + } else { + currentLocationDescription.setText(R.string.gateway_selection_manual_not_connected); + currentLocation.setText(preferredCity); + currentLocation.setVisibility(VISIBLE); + } + } + + protected void startEipService(String preferredCity) { + new Thread(() -> { + PreferenceHelper.setPreferredCity(getContext(), preferredCity); 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()); + }).start(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (USE_BRIDGES.equals(key)) { locationListAdapter.updateData(gatewaysManager.getGatewayLocations()); - setVpnButtonState(); - } else if (PREFERRED_CITY.equals(key)) { - setVpnButtonState(); } } @Override + public void onLocationSelected(String name) { + startEipService(name); + } + + @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); + activity.runOnUiThread(() -> setLocationDescription((EipStatus) o, PreferenceHelper.getPreferredCity(getContext()))); } } - } + static class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapter.ViewHolder> { private static final String TAG = LocationListAdapter.class.getSimpleName(); private List<Location> values; - private Location selectedLocation = null; + private final WeakReference<LocationListSelectionListener> callback; static class ViewHolder extends RecyclerView.ViewHolder { public AppCompatTextView locationLabel; public LocationIndicator locationIndicator; - public AppCompatImageView checkedIcon; public View layout; public ViewHolder(View v) { super(v); layout = v; - locationLabel = (AppCompatTextView) v.findViewById(R.id.location); - locationIndicator = (LocationIndicator) v.findViewById(R.id.quality); - checkedIcon = (AppCompatImageView) v.findViewById(R.id.checked_icon); + locationLabel = v.findViewById(R.id.location); + locationIndicator = v.findViewById(R.id.quality); } } @@ -213,20 +213,14 @@ public class GatewaySelectionFragment extends Fragment implements SharedPreferen 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) { + public LocationListAdapter(List<Location> data, LocationListSelectionListener selectionListener) { values = data; + callback = new WeakReference<>(selectionListener); } @NonNull @@ -246,42 +240,12 @@ public class GatewaySelectionFragment extends Fragment implements SharedPreferen 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; + LocationListSelectionListener listener = callback.get(); + if (listener != null) { + listener.onLocationSelected(location.name); } - 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.locationIndicator.setLoad(GatewaysManager.Load.getLoadByValue(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"; - } } 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 86f8471c..da48effc 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 @@ -124,8 +124,10 @@ public class MainActivityErrorDialog extends DialogFragment { builder.setNegativeButton(R.string.cancel, (dialog, id) -> {}); if (getPreferredCity(applicationContext) != null) { builder.setPositiveButton(R.string.warning_option_try_best, (dialog, which) -> { - setPreferredCity(applicationContext, null); - EipCommand.startVPN(applicationContext, false); + new Thread(() -> { + setPreferredCity(applicationContext, null); + EipCommand.startVPN(applicationContext, false); + }).start(); }); } else if (provider.supportsPluggableTransports()) { if (getUseBridges(applicationContext)) { 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 a13692a5..1f4d0b17 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 @@ -425,7 +425,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen } manualGatewaySelection = drawerView.findViewById(R.id.manualGatewaySelection); String preferredGateway = getPreferredCity(getContext()); - String subtitle = preferredGateway != null ? preferredGateway : getString(R.string.gateway_selection_best_location); + String subtitle = preferredGateway != null ? preferredGateway : getString(R.string.gateway_selection_recommended_location); manualGatewaySelection.setSubtitle(subtitle); boolean show = ProviderObservable.getInstance().getCurrentProvider().hasGatewaysInDifferentLocations(); manualGatewaySelection.setVisibility(show ? VISIBLE : GONE); 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 index ae7818ba..8e032d18 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/models/Location.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Location.java @@ -6,13 +6,11 @@ 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) { + public Location(@NonNull String name, double averageLoad, int numberOfGateways) { this.name = name; this.averageLoad = averageLoad; this.numberOfGateways = numberOfGateways; - this.selected = selected; } @Override @@ -24,7 +22,6 @@ public class Location { 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); } @@ -36,7 +33,6 @@ public class Location { 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/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java index 06fb25e9..93284968 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 @@ -3,6 +3,7 @@ package se.leap.bitmaskclient.base.utils; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; @@ -229,8 +230,9 @@ public class PreferenceHelper { return getString(context, PREFERRED_CITY, null); } + @WorkerThread public static void setPreferredCity(Context context, String city) { - putString(context, PREFERRED_CITY, city); + putStringSync(context, PREFERRED_CITY, city); } public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) { @@ -277,6 +279,12 @@ public class PreferenceHelper { return preferences.getString(key, defValue); } + @WorkerThread + public static void putStringSync(Context context, String key, String value) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putString(key, value).commit(); + } + public static void putString(Context context, String key, String value) { SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); preferences.edit().putString(key, value).apply(); 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 24e9c323..0819e9b6 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java @@ -93,10 +93,10 @@ public class GatewaysManager { private static final String TAG = GatewaysManager.class.getSimpleName(); - private Context context; - private LinkedHashMap<String, Gateway> gateways = new LinkedHashMap<>(); - private Type listType = new TypeToken<ArrayList<Gateway>>() {}.getType(); - private ArrayList<Gateway> presortedList = new ArrayList<>(); + private final Context context; + private final LinkedHashMap<String, Gateway> gateways = new LinkedHashMap<>(); + private final Type listType = new TypeToken<ArrayList<Gateway>>() {}.getType(); + private final ArrayList<Gateway> presortedList = new ArrayList<>(); public GatewaysManager(Context context) { this.context = context; @@ -128,24 +128,30 @@ public class GatewaysManager { int n = 0; Gateway gateway; while ((gateway = select(n, null)) != null) { + String name = gateway.getName(); + if (name == null) { + Log.e(TAG, "Gateway without location name found. This should never happen. Provider misconfigured?"); + continue; + } + if (!locationNames.containsKey(gateway.getName())) { locationNames.put(gateway.getName(), locations.size()); // fake values for now Random rand = new Random(); double averageLoad = rand.nextDouble(); //location.averageLoad; - + Log.d(TAG, "getGatewayLocations - new averageLoad (" + gateway.getName() + "): " + averageLoad); Location location = new Location( gateway.getName(), averageLoad /*gateway.getFullness()*/, - 1, - gateway.getName().equals(selectedCity)); + 1); 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); + Log.d(TAG, "getGatewayLocations - updated averageLoad: (" + gateway.getName() + "): " + location.averageLoad); location.numberOfGateways += 1; locations.set(index, location); } diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml index 1f1df7f2..95e6169b 100644 --- a/app/src/main/res/layout/f_drawer_main.xml +++ b/app/src/main/res/layout/f_drawer_main.xml @@ -101,7 +101,7 @@ <se.leap.bitmaskclient.base.views.IconTextEntry android:id="@+id/manualGatewaySelection" app:text="@string/gateway_selection_title" - app:subtitle="@string/gateway_selection_best_location" + app:subtitle="@string/gateway_selection_recommended_location" app:icon="@drawable/ic_map_marker_star_black_36dp" android:layout_height="wrap_content" android:layout_width="wrap_content" diff --git a/app/src/main/res/layout/f_gateway_selection.xml b/app/src/main/res/layout/f_gateway_selection.xml index f96b9c08..5a384163 100644 --- a/app/src/main/res/layout/f_gateway_selection.xml +++ b/app/src/main/res/layout/f_gateway_selection.xml @@ -11,8 +11,9 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" + android:visibility="visible" tools:visibility="visible" + android:layout_alignParentTop="true" > <androidx.appcompat.widget.AppCompatTextView android:id="@+id/current_location_description" @@ -25,7 +26,8 @@ android:paddingEnd="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin" - android:text="@string/vpn_securely_routed" /> + + android:text="@string/gateway_selection_automatic_location" /> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/current_location" @@ -43,26 +45,9 @@ </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_below="@id/current_location_container" android:layout_above="@+id/vpn_button_container" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -87,12 +72,13 @@ 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:id="@+id/automatic_gateway_selection_btn" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="end" - android:text="@string/vpn.button.turn.on" /> + android:text="@string/gateway_selection_recommended_location" + /> </LinearLayout> 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 index 36075424..1d7054a4 100644 --- a/app/src/main/res/layout/v_select_text_list_item.xml +++ b/app/src/main/res/layout/v_select_text_list_item.xml @@ -6,25 +6,13 @@ 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" - /> <se.leap.bitmaskclient.base.views.LocationIndicator android:id="@+id/quality" android:layout_width="56dp" android:layout_height="match_parent" - android:layout_toLeftOf="@+id/checked_icon" - android:layout_toStartOf="@+id/checked_icon" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" tools:visibility="visible" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6bdafa8..7c0c46f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -152,9 +152,12 @@ <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_recommended_location">Use recommended location</string> <string name="gateway_selection_automatic">Automatic</string> - <string name="gateway_selection_current_location">Your traffic is currently routed through: </string> + <string name="gateway_selection_manual_location">Your traffic is currently routed through: </string> + <string name="gateway_selection_manual_not_connected">Your traffic will be routed through: </string> + <string name="gateway_selection_automatic_location">Your traffic is automatically routed through the best location:</string> + <string name="gateway_selection_automatic_not_connected">Your traffic will be automatically routed through the best location.</string> <string name="finding_best_connection">Finding best connection…</string> <string name="reconnecting">Reconnecting…</string> <string name="tor_starting">Starting bridges for censorship circumvention…</string> |