diff options
author | Norbel Ambanumben <nambanumben@riseup.net> | 2024-10-11 11:54:39 +0000 |
---|---|---|
committer | cyberta <cyberta@riseup.net> | 2024-10-11 11:54:39 +0000 |
commit | 55219121b127a000b2d410a841b6441a25adb1f3 (patch) | |
tree | de5e801f2df0cccf393a051976a12a7a69fcc442 /app/src/main | |
parent | 236ce1209278fae174713d605300b6ed3f16febb (diff) |
feat: add invite code to UI.
Diffstat (limited to 'app/src/main')
9 files changed, 292 insertions, 36 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java new file mode 100644 index 00000000..c31912d9 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java @@ -0,0 +1,105 @@ +package se.leap.bitmaskclient.base.models; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.URI; +import java.net.URISyntaxException; + +public class Introducer implements Parcelable { + private String type; + private String address; + private String certificate; + private String fullyQualifiedDomainName; + private boolean kcpEnabled; + + public Introducer(String type, String address, String certificate, String fullyQualifiedDomainName, boolean kcpEnabled) { + this.type = type; + this.address = address; + this.certificate = certificate; + this.fullyQualifiedDomainName = fullyQualifiedDomainName; + this.kcpEnabled = kcpEnabled; + } + + protected Introducer(Parcel in) { + type = in.readString(); + address = in.readString(); + certificate = in.readString(); + fullyQualifiedDomainName = in.readString(); + kcpEnabled = in.readByte() != 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type); + dest.writeString(address); + dest.writeString(certificate); + dest.writeString(fullyQualifiedDomainName); + dest.writeByte((byte) (kcpEnabled ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<Introducer> CREATOR = new Creator<>() { + @Override + public Introducer createFromParcel(Parcel in) { + return new Introducer(in); + } + + @Override + public Introducer[] newArray(int size) { + return new Introducer[size]; + } + }; + + public boolean validate() { + if (!"obfsvpnintro".equals(type)) { + throw new IllegalArgumentException("Unknown type: " + type); + } + if (!address.contains(":") || address.split(":").length != 2) { + throw new IllegalArgumentException("Expected address in format ipaddr:port"); + } + if (certificate.length() != 70) { + throw new IllegalArgumentException("Wrong certificate length: " + certificate.length()); + } + if (!"localhost".equals(fullyQualifiedDomainName) && fullyQualifiedDomainName.split("\\.").length < 2) { + throw new IllegalArgumentException("Expected a FQDN, got: " + fullyQualifiedDomainName); + } + return true; + } + + public static Introducer fromUrl(String introducerUrl) throws URISyntaxException { + URI uri = new URI(introducerUrl); + String fqdn = getQueryParam(uri, "fqdn"); + if (fqdn == null || fqdn.isEmpty()) { + throw new IllegalArgumentException("FQDN not found in the introducer URL"); + } + + boolean kcp = "1".equals(getQueryParam(uri, "kcp")); + + String cert = getQueryParam(uri, "cert"); + if (cert == null || cert.isEmpty()) { + throw new IllegalArgumentException("Cert not found in the introducer URL"); + } + + return new Introducer(uri.getScheme(), uri.getAuthority(), cert, fqdn, kcp); + } + + public String toUrl() { + return String.format("%s://%s?fqdn=%s&kcp=%d&cert=%s", type, address, fullyQualifiedDomainName, kcpEnabled ? 1 : 0, certificate); + } + + private static String getQueryParam(URI uri, String param) { + String[] queryParams = uri.getQuery().split("&"); + for (String queryParam : queryParams) { + String[] keyValue = queryParam.split("="); + if (keyValue.length == 2 && keyValue[0].equals(param)) { + return keyValue[1]; + } + } + return null; + } +}
\ No newline at end of file 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 725c602a..bf1e6b94 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 @@ -86,6 +86,7 @@ public final class Provider implements Parcelable { private long lastGeoIpUpdate = 0L; private long lastMotdUpdate = 0L; private long lastMotdSeen = 0L; + private Introducer introducer = null; private Set<String> lastMotdSeenHashes = new HashSet<>(); private boolean shouldUpdateVpnCertificate; @@ -115,6 +116,11 @@ public final class Provider implements Parcelable { public Provider() { } + public Provider(Introducer introducer) { + this(introducer.toUrl(), null); + this.introducer = introducer; + } + public Provider(String mainUrl) { this(mainUrl, null); } @@ -423,6 +429,7 @@ public final class Provider implements Parcelable { parcel.writeLong(lastMotdSeen); parcel.writeStringList(new ArrayList<>(lastMotdSeenHashes)); parcel.writeInt(shouldUpdateVpnCertificate ? 0 : 1); + parcel.writeParcelable(introducer, 0); } @@ -484,6 +491,7 @@ public final class Provider implements Parcelable { in.readStringList(lastMotdSeenHashes); this.lastMotdSeenHashes = new HashSet<>(lastMotdSeenHashes); this.shouldUpdateVpnCertificate = in.readInt() == 0; + this.introducer = in.readParcelable(Introducer.class.getClassLoader()); } catch (MalformedURLException | JSONException e) { e.printStackTrace(); } @@ -739,6 +747,10 @@ public final class Provider implements Parcelable { return getCertificatePinEncoding() + ":" + getCertificatePin(); } + public boolean hasIntroducer() { + return introducer != null; + } + /** * resets everything except the main url, the providerIp and the geoip * service url (currently preseeded) diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java index 9a0bf7b7..9284edc9 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java @@ -6,7 +6,9 @@ import static androidx.appcompat.app.ActionBar.DISPLAY_SHOW_CUSTOM; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences; +import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.CIRCUMVENTION_SETUP_FRAGMENT; import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.CONFIGURE_PROVIDER_FRAGMENT; +import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.INVITE_CODE_PROVIDER; import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF; import android.Manifest; @@ -145,6 +147,9 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal binding.setupNextButton.setOnClickListener(v -> { int currentPos = binding.viewPager.getCurrentItem(); int newPos = currentPos + 1; + if (newPos == CIRCUMVENTION_SETUP_FRAGMENT && provider.hasIntroducer()) { + newPos = newPos + 1; // skip configuration of `CIRCUMVENTION_SETUP_FRAGMENT` when invite code provider is selected + } if (newPos >= binding.viewPager.getAdapter().getItemCount()) { return; } diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java index f15aaa43..24da4691 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java @@ -1,6 +1,7 @@ package se.leap.bitmaskclient.providersetup.fragments; import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.ADD_PROVIDER; +import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.INVITE_CODE_PROVIDER; import android.graphics.Typeface; import android.os.Bundle; @@ -15,9 +16,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; +import java.net.URISyntaxException; import java.util.ArrayList; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.models.Introducer; import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.base.utils.ViewHelper; import se.leap.bitmaskclient.databinding.FProviderSelectionBinding; @@ -61,13 +64,22 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc binding.providerRadioGroup.addView(radioButton); radioButtons.add(radioButton); } - RadioButton radioButton = new RadioButton(binding.getRoot().getContext()); - radioButton.setText(getText(R.string.add_provider)); - radioButton.setId(ADD_PROVIDER); - binding.providerRadioGroup.addView(radioButton); - radioButtons.add(radioButton); + + RadioButton addProviderRadioButton = new RadioButton(binding.getRoot().getContext()); + addProviderRadioButton.setText(getText(R.string.add_provider)); + addProviderRadioButton.setId(ADD_PROVIDER); + binding.providerRadioGroup.addView(addProviderRadioButton); + radioButtons.add(addProviderRadioButton); + + + RadioButton inviteCodeRadioButton = new RadioButton(binding.getRoot().getContext()); + inviteCodeRadioButton.setText(R.string.enter_invite_code); + inviteCodeRadioButton.setId(INVITE_CODE_PROVIDER); + binding.providerRadioGroup.addView(inviteCodeRadioButton); + radioButtons.add(inviteCodeRadioButton); binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility()); + binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility()); return binding.getRoot(); } @@ -89,11 +101,22 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc } binding.providerDescription.setText(viewModel.getProviderDescription(getContext())); binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility()); + binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility()); + if (viewModel.getCustomUrl() == null || viewModel.getCustomUrl().isEmpty()) { + binding.syntaxCheckResult.setText(""); + binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.color_font_btn)); + binding.editCustomProvider.setHint(viewModel.getHint(getContext())); + } else { + binding.editCustomProvider.setText(""); + } + binding.editCustomProvider.setRawInputType(viewModel.getEditInputType()); + binding.editCustomProvider.setMaxLines(viewModel.getEditInputLines()); + binding.editCustomProvider.setMinLines(viewModel.getEditInputLines()); setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig()); - if (checkedId != ADD_PROVIDER) { + if (checkedId != ADD_PROVIDER && checkedId != INVITE_CODE_PROVIDER) { setupActivityCallback.onProviderSelected(viewModel.getProvider(checkedId)); } else if (viewModel.isValidConfig()) { - setupActivityCallback.onProviderSelected(new Provider(binding.editCustomProvider.getText().toString())); + providerSelected(binding.editCustomProvider.getText().toString(),checkedId); } }); @@ -107,7 +130,12 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc if (viewModel.isCustomProviderSelected()) { setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig()); if (viewModel.isValidConfig()) { - setupActivityCallback.onProviderSelected(new Provider(viewModel.getCustomUrl())); + providerSelected(viewModel.getCustomUrl(),viewModel.getSelected()); + binding.syntaxCheckResult.setText(getString(R.string.validation_status_success)); + binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.green200)); + } else { + binding.syntaxCheckResult.setText(getString(R.string.validation_status_failure)); + binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.red200)); } } } @@ -130,6 +158,18 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc binding.providerRadioGroup.check(viewModel.getSelected()); } + private void providerSelected(String string, int checkedId) { + if (checkedId == INVITE_CODE_PROVIDER) { + try { + setupActivityCallback.onProviderSelected(new Provider(Introducer.fromUrl(string))); + } catch (Exception e) { + // This cannot happen + } + } else { + setupActivityCallback.onProviderSelected(new Provider(string)); + } + } + @Override public void onDestroyView() { setupActivityCallback.removeCancelCallback(this); diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java index 29dab98a..58b43fbd 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java @@ -2,6 +2,7 @@ package se.leap.bitmaskclient.providersetup.fragments.viewmodel; import android.content.Context; import android.content.res.AssetManager; +import android.text.InputType; import android.util.Patterns; import android.view.View; import android.webkit.URLUtil; @@ -11,12 +12,14 @@ import androidx.lifecycle.ViewModel; import java.util.List; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.models.Introducer; import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.providersetup.ProviderManager; public class ProviderSelectionViewModel extends ViewModel { private final ProviderManager providerManager; public static int ADD_PROVIDER = 100100100; + public static int INVITE_CODE_PROVIDER = 200100100; private int selected = 0; private String customUrl; @@ -50,17 +53,29 @@ public class ProviderSelectionViewModel extends ViewModel { if (selected == ADD_PROVIDER) { return customUrl != null && (Patterns.DOMAIN_NAME.matcher(customUrl).matches() || (URLUtil.isNetworkUrl(customUrl) && Patterns.WEB_URL.matcher(customUrl).matches())); } + if (selected == INVITE_CODE_PROVIDER) { + try { + Introducer introducer = Introducer.fromUrl(customUrl); + return introducer.validate(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } return true; } public boolean isCustomProviderSelected() { - return selected == ADD_PROVIDER; + return selected == ADD_PROVIDER || selected == INVITE_CODE_PROVIDER; } public CharSequence getProviderDescription(Context context) { if (selected == ADD_PROVIDER) { return context.getText(R.string.add_provider_description); } + if (selected == INVITE_CODE_PROVIDER) { + return context.getText(R.string.invite_code_provider_description); + } Provider provider = getProvider(selected); if ("riseup.net".equals(provider.getDomain())) { return context.getText(R.string.provider_description_riseup); @@ -74,9 +89,25 @@ public class ProviderSelectionViewModel extends ViewModel { public int getEditProviderVisibility() { if (selected == ADD_PROVIDER) { return View.VISIBLE; + } else if (selected == INVITE_CODE_PROVIDER) { + return View.VISIBLE; } return View.GONE; } + public int getEditInputType() { + if (selected == INVITE_CODE_PROVIDER) { + return InputType.TYPE_TEXT_FLAG_MULTI_LINE; + } + return InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; + } + + public int getEditInputLines() { + if (selected == INVITE_CODE_PROVIDER) { + return 3; + } + return 1; + } + public void setCustomUrl(String url) { customUrl = url; @@ -100,4 +131,14 @@ public class ProviderSelectionViewModel extends ViewModel { } return domain; } + + public CharSequence getHint(Context context) { + if (selected == ADD_PROVIDER) { + return context.getText(R.string.add_provider_prompt); + } + if (selected == INVITE_CODE_PROVIDER) { + return context.getText(R.string.invite_code_provider_prompt); + } + return ""; + } }
\ No newline at end of file diff --git a/app/src/main/res/drawable/qr_code_scanner.xml b/app/src/main/res/drawable/qr_code_scanner.xml new file mode 100644 index 00000000..5ab50c71 --- /dev/null +++ b/app/src/main/res/drawable/qr_code_scanner.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"/> + +</vector> diff --git a/app/src/main/res/layout/f_provider_selection.xml b/app/src/main/res/layout/f_provider_selection.xml index 48d5bdd3..e1b7fc64 100644 --- a/app/src/main/res/layout/f_provider_selection.xml +++ b/app/src/main/res/layout/f_provider_selection.xml @@ -61,35 +61,74 @@ android:layout_height="wrap_content" > </RadioGroup> - <androidx.appcompat.widget.LinearLayoutCompat - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:id="@+id/expandable_detail_container"> - <androidx.appcompat.widget.AppCompatTextView - android:paddingTop="@dimen/stdpadding" - android:id="@+id/provider_description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - tools:text="@string/provider_description_riseup"/> - <androidx.appcompat.widget.AppCompatEditText - android:id="@+id/edit_customProvider" - android:layout_marginVertical="@dimen/stdpadding" - android:paddingHorizontal="@dimen/stdpadding" - android:paddingVertical="@dimen/compact_padding" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/white" - android:hint="https://example.org" - android:inputType="textWebEditText" - android:imeOptions="actionDone" - android:maxLines="1" - android:textAppearance="@style/TextAppearance.AppCompat.Body1" - android:textColorHint="@color/black800_transparent" - /> - </androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat> </androidx.cardview.widget.CardView> + + <androidx.appcompat.widget.LinearLayoutCompat + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="@dimen/activity_margin" + android:paddingBottom="@dimen/list_view_margin_top" + android:background="@color/color_provider_description_background" + android:id="@+id/expandable_detail_container"> + <androidx.appcompat.widget.AppCompatTextView + android:paddingTop="@dimen/stdpadding" + android:id="@+id/provider_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:text="@string/provider_description_riseup"/> + <androidx.appcompat.widget.AppCompatEditText + android:id="@+id/edit_customProvider" + android:layout_marginVertical="@dimen/stdpadding" + android:paddingHorizontal="@dimen/stdpadding" + android:paddingVertical="@dimen/compact_padding" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/white" + tools:hint="https://example.org" + android:imeOptions="actionDone" + android:textAppearance="@style/TextAppearance.AppCompat.Body1" + android:textColorHint="@color/black800_transparent" + /> + <LinearLayout + android:id="@+id/syntax_check" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + <androidx.appcompat.widget.AppCompatTextView + android:paddingTop="@dimen/stdpadding" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/syntax_check"/> + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/syntax_check_result" + android:paddingTop="@dimen/stdpadding" + android:paddingStart="@dimen/stdpadding" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:text="Good"/> + </LinearLayout> + <LinearLayout + android:visibility="gone" + android:id="@+id/qr_scanner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/stdpadding" + android:orientation="horizontal"> + <androidx.appcompat.widget.AppCompatImageView + android:paddingTop="@dimen/stdpadding" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:srcCompat="@drawable/qr_code_scanner"/> + <androidx.appcompat.widget.AppCompatTextView + android:paddingTop="@dimen/stdpadding" + android:paddingStart="@dimen/stdpadding" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/qr_scanner_prompt"/> + </LinearLayout> + </androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat> </ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 84a2d9f0..d81c4021 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,6 +4,7 @@ <color name="colorPrimaryLight">#FF69B4</color> <color name="colorPrimaryDark">#ef0072</color> <color name="colorPrimary_transparent">#0D000000</color> + <color name="color_provider_description_background">#F6F6F6</color> <color name="colorPrimary_transparent_dark">#1F000000</color> <color name="colorBackground">#fffafafa</color> <color name="colorError">#ef9a9a</color> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b90c4e0f..c40f5819 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,6 +212,10 @@ <string name="provider_description_riseup">Riseup provides online communication tools for people and groups working on liberatory social change. We are a project to create democratic alternatives and practice self-determination by controlling our own secure means of communications.</string> <string name="next">Next</string> <string name="add_provider_description">Bitmask connects to trusted providers that are not publicly listed. Enter your provider’s url below.</string> + <string name="add_provider_prompt">Enter the provider’s URL here.</string> + <string name="invite_code_provider_description">Bitmask allows you to connect to providers using a private Invite Code. </string> + <string name="invite_code_provider_prompt">Enter your trusted Invite Code here.</string> + <string name="qr_scanner_prompt">Click to Scan QR Code</string> <string name="provider_description_calyx">Calyx is a non-profit education and research organization devoted to studying, testing, developing and implementing privacy technology and tools to promote free speech, free expression, civic engagement and privacy rights on the internet and in the mobile communications industry.</string> <string name="title_circumvention_setup">Do You Require Censorship Circumvention?</string> <string name="circumvention_setup_description">If you live where the internet is censored you can use our censorship circumvention options to access all internet services. These options will slow down your connection!</string> @@ -239,4 +243,8 @@ <string name="permission_rejected">Permission request rejected.</string> <string name="login_not_supported">The current app version doesn\'t support logins, which you need to update your VPN certificate for this provider.</string> <string name="select_language">Select Language</string> + <string name="syntax_check">Syntax Check:</string> + <string name="validation_status_success">Good</string> + <string name="validation_status_failure">Bad</string> + <string name="enter_invite_code">Enter invite Code</string> </resources> |