summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
authorNorbel Ambanumben <nambanumben@riseup.net>2024-10-11 11:54:39 +0000
committercyberta <cyberta@riseup.net>2024-10-11 11:54:39 +0000
commit55219121b127a000b2d410a841b6441a25adb1f3 (patch)
treede5e801f2df0cccf393a051976a12a7a69fcc442 /app/src/main/java
parent236ce1209278fae174713d605300b6ed3f16febb (diff)
feat: add invite code to UI.
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java105
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java12
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java56
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java43
5 files changed, 212 insertions, 9 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