summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap/bitmaskclient/providersetup
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/providersetup')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java42
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java62
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/TorLogAdapter.java61
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CancelCallback.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java52
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java328
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivityCallback.java33
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/BaseSetupFragment.java96
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java58
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java222
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java117
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java35
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java153
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java65
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupSuccessFragment.java44
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java36
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java97
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModelFactory.java28
23 files changed, 1485 insertions, 82 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
index 0e7e1f4a..07ea4691 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
@@ -178,7 +178,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
private ProviderApiManager initApiManager() {
OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources());
- return new ProviderApiManager(PreferenceHelper.getSharedPreferences(this), getResources(), clientGenerator, this);
+ return new ProviderApiManager(getResources(), clientGenerator, this);
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
index fdaef28b..9468f76e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
@@ -101,7 +101,6 @@ import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.ON;
import static se.leap.bitmaskclient.tor.TorStatusObservable.getProxyPort;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.ResultReceiver;
@@ -172,12 +171,10 @@ public abstract class ProviderApiManagerBase {
private final ProviderApiServiceCallback serviceCallback;
- protected SharedPreferences preferences;
protected Resources resources;
OkHttpClientGenerator clientGenerator;
- ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
- this.preferences = preferences;
+ ProviderApiManagerBase(Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
this.resources = resources;
this.serviceCallback = callback;
this.clientGenerator = clientGenerator;
@@ -221,7 +218,7 @@ public abstract class ProviderApiManagerBase {
}
try {
- if (PreferenceHelper.hasSnowflakePrefs(preferences) && !VpnStatus.isVPNActive()) {
+ if (PreferenceHelper.hasSnowflakePrefs() && !VpnStatus.isVPNActive()) {
startTorProxy();
}
} catch (InterruptedException | IllegalStateException e) {
@@ -253,6 +250,7 @@ public abstract class ProviderApiManagerBase {
}
ProviderObservable.getInstance().setProviderForDns(null);
break;
+
case SET_UP_PROVIDER:
ProviderObservable.getInstance().setProviderForDns(provider);
result = setUpProvider(provider, parameters);
@@ -303,7 +301,7 @@ public abstract class ProviderApiManagerBase {
if (result.getBoolean(BROADCAST_RESULT_KEY)) {
Log.d(TAG, "successfully downloaded VPN certificate");
provider.setShouldUpdateVpnCertificate(false);
- PreferenceHelper.storeProviderInPreferences(preferences, provider);
+ PreferenceHelper.storeProviderInPreferences(provider);
ProviderObservable.getInstance().updateProvider(provider);
}
ProviderObservable.getInstance().setProviderForDns(null);
@@ -315,7 +313,7 @@ public abstract class ProviderApiManagerBase {
provider.setMotdJson(motd);
provider.setLastMotdUpdate(System.currentTimeMillis());
}
- PreferenceHelper.storeProviderInPreferences(preferences, provider);
+ PreferenceHelper.storeProviderInPreferences(provider);
ProviderObservable.getInstance().updateProvider(provider);
break;
@@ -359,7 +357,7 @@ public abstract class ProviderApiManagerBase {
protected boolean startTorProxy() throws InterruptedException, IllegalStateException, TimeoutException {
if (EipStatus.getInstance().isDisconnected() &&
- PreferenceHelper.getUseSnowflake(preferences) &&
+ PreferenceHelper.getUseSnowflake() &&
serviceCallback.startTorService()) {
waitForTorCircuits();
if (TorStatusObservable.isCancelled()) {
@@ -385,7 +383,7 @@ public abstract class ProviderApiManagerBase {
void resetProviderDetails(Provider provider) {
provider.reset();
- deleteProviderDetailsFromPreferences(preferences, provider.getDomain());
+ deleteProviderDetailsFromPreferences(provider.getDomain());
}
String formatErrorMessage(final int errorStringId) {
@@ -952,7 +950,7 @@ public abstract class ProviderApiManagerBase {
result = validateCertificateForProvider(result, provider);
- //invalid certificate or no certificate
+ //invalid certificate or no certificate or unable to connect due other connectivity issues
if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) {
return result;
}
@@ -1037,16 +1035,16 @@ public abstract class ProviderApiManagerBase {
}
protected String getPersistedPrivateKey(String providerDomain) {
- return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain, preferences);
+ return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain);
}
protected String getPersistedVPNCertificate(String providerDomain) {
- return getFromPersistedProvider(PROVIDER_VPN_CERTIFICATE, providerDomain, preferences);
+ return getFromPersistedProvider(PROVIDER_VPN_CERTIFICATE, providerDomain);
}
protected JSONObject getPersistedProviderDefinition(String providerDomain) {
try {
- return new JSONObject(getFromPersistedProvider(Provider.KEY, providerDomain, preferences));
+ return new JSONObject(getFromPersistedProvider(Provider.KEY, providerDomain));
} catch (JSONException e) {
e.printStackTrace();
return new JSONObject();
@@ -1054,44 +1052,44 @@ public abstract class ProviderApiManagerBase {
}
protected String getPersistedProviderCA(String providerDomain) {
- return getFromPersistedProvider(CA_CERT, providerDomain, preferences);
+ return getFromPersistedProvider(CA_CERT, providerDomain);
}
protected String getPersistedProviderApiIp(String providerDomain) {
- return getFromPersistedProvider(PROVIDER_API_IP, providerDomain, preferences);
+ return getFromPersistedProvider(PROVIDER_API_IP, providerDomain);
}
protected String getPersistedProviderIp(String providerDomain) {
- return getFromPersistedProvider(PROVIDER_IP, providerDomain, preferences);
+ return getFromPersistedProvider(PROVIDER_IP, providerDomain);
}
protected String getPersistedGeoIp(String providerDomain) {
- return getFromPersistedProvider(GEOIP_URL, providerDomain, preferences);
+ return getFromPersistedProvider(GEOIP_URL, providerDomain);
}
protected JSONObject getPersistedMotd(String providerDomain) {
try {
- return new JSONObject(getFromPersistedProvider(PROVIDER_MOTD, providerDomain, preferences));
+ return new JSONObject(getFromPersistedProvider(PROVIDER_MOTD, providerDomain));
} catch (JSONException e) {
return new JSONObject();
}
}
protected long getPersistedMotdLastSeen(String providerDomain) {
- return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_SEEN, providerDomain, preferences);
+ return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_SEEN, providerDomain);
}
protected long getPersistedMotdLastUpdate(String providerDomain) {
- return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_UPDATED, providerDomain, preferences);
+ return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_UPDATED, providerDomain);
}
protected Set<String> getPersistedMotdHashes(String providerDomain) {
- return getStringSetFromPersistedProvider(PROVIDER_MOTD_HASHES, providerDomain, preferences);
+ return getStringSetFromPersistedProvider(PROVIDER_MOTD_HASHES, providerDomain);
}
protected boolean hasUpdatedProviderDetails(String domain) {
- return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(CA_CERT + "." + domain);
+ return PreferenceHelper.hasKey(Provider.KEY + "." + domain) && PreferenceHelper.hasKey(CA_CERT + "." + domain);
}
/**
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
index 775e174a..0f6c5090 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
@@ -41,7 +41,7 @@ import se.leap.bitmaskclient.base.models.Provider;
*/
public class ProviderManager implements AdapteeCollection<Provider> {
- private AssetManager assetsManager;
+ private final AssetManager assetsManager;
private File externalFilesDir;
private Set<Provider> defaultProviders;
private Set<Provider> customProviders;
@@ -49,6 +49,7 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private Set<String> customProviderURLs;
private static ProviderManager instance;
+ private boolean addDummyEntry = false;
public static ProviderManager getInstance(AssetManager assetsManager, File externalFilesDir) {
if (instance == null)
@@ -62,6 +63,10 @@ public class ProviderManager implements AdapteeCollection<Provider> {
instance = null;
}
+ public void setAddDummyEntry(boolean addDummyEntry) {
+ this.addDummyEntry = addDummyEntry;
+ }
+
private ProviderManager(AssetManager assetManager, File externalFilesDir) {
this.assetsManager = assetManager;
addDefaultProviders(assetManager);
@@ -145,13 +150,18 @@ public class ProviderManager implements AdapteeCollection<Provider> {
}
public List<Provider> providers() {
+ return providers(addDummyEntry);
+ }
+
+ private List<Provider> providers(boolean addEmptyProvider) {
List<Provider> allProviders = new ArrayList<>();
allProviders.addAll(defaultProviders);
if(customProviders != null)
allProviders.addAll(customProviders);
- //add an option to add a custom provider
- //TODO: refactor me?
- allProviders.add(new Provider());
+ if (addEmptyProvider) {
+ //add an option to add a custom provider
+ allProviders.add(new Provider());
+ }
return allProviders;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
index 0d5f903f..338a60e9 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
@@ -143,7 +143,7 @@ public class ProviderSetupFailedDialog extends DialogFragment {
handleTorTimeoutError();
});
builder.setNeutralButton(R.string.retry_unobfuscated, ((dialog, id) -> {
- PreferenceHelper.useSnowflake(getContext(), false);
+ PreferenceHelper.useSnowflake(false);
handleTorTimeoutError();
}));
default:
@@ -189,7 +189,7 @@ public class ProviderSetupFailedDialog extends DialogFragment {
interfaceWithConfigurationWizard = (DownloadFailedDialogInterface) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString()
- + " must implement NoticeDialogListener");
+ + " must implement DownloadFailedDialogInterface");
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
new file mode 100644
index 00000000..39122572
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
@@ -0,0 +1,62 @@
+package se.leap.bitmaskclient.providersetup;
+
+import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.*;
+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.SetupFragmentFactory.PROVIDER_SELECTION_FRAGMENT;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+import java.util.ArrayList;
+
+import se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory;
+
+public class SetupViewPagerAdapter extends FragmentStateAdapter {
+
+ private SetupFragmentFactory setupFragmentFactory;
+
+ private SetupViewPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
+ super(fragmentManager, lifecycle);
+ }
+
+ public SetupViewPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, Intent vpnPermissionRequest, Boolean showNotificationPermission) {
+ this(fragmentManager, lifecycle);
+ ArrayList<Integer> fragments = new ArrayList<>();
+ fragments.add(PROVIDER_SELECTION_FRAGMENT);
+ fragments.add(CIRCUMVENTION_SETUP_FRAGMENT);
+ fragments.add(CONFIGURE_PROVIDER_FRAGMENT);
+ if (vpnPermissionRequest != null) {
+ fragments.add(VPN_PERMISSON_EDUCATIONAL_FRAGMENT);
+ fragments.add(VPN_PERMISSON_FRAGMENT);
+ }
+ if (showNotificationPermission) {
+ fragments.add(NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT);
+ fragments.add(NOTIFICATION_PERMISSON_FRAGMENT);
+ }
+ fragments.add(SUCCESS_FRAGMENT);
+ setupFragmentFactory = new SetupFragmentFactory(fragments, vpnPermissionRequest);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ return setupFragmentFactory.createFragment(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return setupFragmentFactory.getItemCount();
+ }
+
+ public int getFragmentPostion(int fragmentType) {
+ return setupFragmentFactory.getPos(fragmentType);
+ }
+
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/TorLogAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/TorLogAdapter.java
new file mode 100644
index 00000000..3df0fd94
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/TorLogAdapter.java
@@ -0,0 +1,61 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.providersetup.activities.ConfigWizardBaseActivity;
+
+public class TorLogAdapter extends RecyclerView.Adapter<TorLogAdapter.ViewHolder> {
+ private List<String> values;
+ public boolean postponeUpdate;
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ public AppCompatTextView logTextLabel;
+ public View layout;
+
+ public ViewHolder(View v) {
+ super(v);
+ layout = v;
+ logTextLabel = v.findViewById(android.R.id.text1);
+ }
+ }
+
+ public void updateData(List<String> data) {
+ values = data;
+ if (!postponeUpdate) {
+ notifyDataSetChanged();
+ }
+ }
+
+ public TorLogAdapter(List<String> data) {
+ values = data;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(
+ parent.getContext());
+ View v = inflater.inflate(R.layout.v_log_item, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, final int position) {
+ final String log = values.get(position);
+ holder.logTextLabel.setText(log);
+ }
+
+ @Override
+ public int getItemCount() {
+ return values.size();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CancelCallback.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CancelCallback.java
new file mode 100644
index 00000000..a3f387d8
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CancelCallback.java
@@ -0,0 +1,5 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+public interface CancelCallback {
+ void onCanceled();
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
index caba1436..1bf66d7d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
@@ -11,11 +11,9 @@ import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastSnowflakeLog;
import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastTorLog;
import static se.leap.bitmaskclient.tor.TorStatusObservable.getStringForCurrentStatus;
-import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -40,8 +38,8 @@ import java.util.Observer;
import butterknife.BindView;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.Provider;
-import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.base.views.ProviderHeaderView;
+import se.leap.bitmaskclient.providersetup.TorLogAdapter;
import se.leap.bitmaskclient.tor.TorStatusObservable;
/**
@@ -54,7 +52,6 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity imple
private static final String TAG = ConfigWizardBaseActivity.class.getName();
public static final float GUIDE_LINE_COMPACT_DELTA = 0.1f;
- protected SharedPreferences preferences;
@BindView(R.id.header)
ProviderHeaderView providerHeaderView;
@@ -133,7 +130,6 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity imple
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- preferences = PreferenceHelper.getSharedPreferences(this);
provider = getIntent().getParcelableExtra(PROVIDER_KEY);
}
@@ -431,50 +427,4 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity imple
snowflakeState.setText(snowflakeLog);
}
- static class TorLogAdapter extends RecyclerView.Adapter<TorLogAdapter.ViewHolder> {
- private List<String> values;
- private boolean postponeUpdate;
-
- static class ViewHolder extends RecyclerView.ViewHolder {
- public AppCompatTextView logTextLabel;
- public View layout;
-
- public ViewHolder(View v) {
- super(v);
- layout = v;
- logTextLabel = v.findViewById(android.R.id.text1);
- }
- }
-
- public void updateData(List<String> data) {
- values = data;
- if (!postponeUpdate) {
- notifyDataSetChanged();
- }
- }
-
- public TorLogAdapter(List<String> data) {
- values = data;
- }
-
- @NonNull
- @Override
- public TorLogAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- LayoutInflater inflater = LayoutInflater.from(
- parent.getContext());
- View v = inflater.inflate(R.layout.v_log_item, parent, false);
- return new TorLogAdapter.ViewHolder(v);
- }
-
- @Override
- public void onBindViewHolder(TorLogAdapter.ViewHolder holder, final int position) {
- final String log = values.get(position);
- holder.logTextLabel.setText(log);
- }
-
- @Override
- public int getItemCount() {
- return values.size();
- }
- }
}
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
new file mode 100644
index 00000000..27ca6658
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java
@@ -0,0 +1,328 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static androidx.appcompat.app.ActionBar.DISPLAY_SHOW_CUSTOM;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences;
+import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.CONFIGURE_PROVIDER_FRAGMENT;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.net.VpnService;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.viewpager2.widget.ViewPager2;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.FragmentManagerEnhanced;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.ViewHelper;
+import se.leap.bitmaskclient.base.views.ActionBarTitle;
+import se.leap.bitmaskclient.databinding.ActivitySetupBinding;
+import se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog;
+import se.leap.bitmaskclient.providersetup.SetupViewPagerAdapter;
+import se.leap.bitmaskclient.tor.TorServiceCommand;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+public class SetupActivity extends AppCompatActivity implements SetupActivityCallback, ProviderSetupFailedDialog.DownloadFailedDialogInterface {
+
+ public static final String EXTRA_PROVIDER = "EXTRA_PROVIDER";
+ public static final String EXTRA_CURRENT_POSITION = "EXTRA_CURRENT_POSITION";
+ private static final String TAG = SetupActivity.class.getSimpleName();
+ ActivitySetupBinding binding;
+ Provider provider;
+ private int currentPosition = 0;
+
+ private final HashSet<CancelCallback> cancelCallbacks = new HashSet<>();
+ private FragmentManagerEnhanced fragmentManager;
+ SetupViewPagerAdapter adapter;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ provider = savedInstanceState.getParcelable(EXTRA_PROVIDER);
+ currentPosition = savedInstanceState.getInt(EXTRA_CURRENT_POSITION);
+ }
+
+ binding = ActivitySetupBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ fragmentManager = new FragmentManagerEnhanced(getSupportFragmentManager());
+ ArrayList<View> indicatorViews = new ArrayList<>();
+
+ for (int i = 0; i < 4; i++) {
+ addIndicatorView(indicatorViews);
+ }
+
+ Intent requestVpnPermission = VpnService.prepare(this);
+ if (requestVpnPermission != null) {
+ addIndicatorView(indicatorViews);
+ addIndicatorView(indicatorViews);
+ }
+
+ boolean showNotificationPermissionFragments = false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ showNotificationPermissionFragments = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS);
+ if (showNotificationPermissionFragments) {
+ addIndicatorView(indicatorViews);
+ addIndicatorView(indicatorViews);
+ }
+ }
+ }
+
+ adapter = new SetupViewPagerAdapter(getSupportFragmentManager(), getLifecycle(), requestVpnPermission, showNotificationPermissionFragments);
+
+ binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ currentPosition = position;
+ for (int i = 0; i < indicatorViews.size(); i++) {
+ ((ViewGroup) indicatorViews.get(i)).
+ getChildAt(0).
+ setBackgroundColor(ContextCompat.getColor(SetupActivity.this, (i == position) ? R.color.colorPrimaryDark : R.color.colorDisabled));
+ }
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(position == 0 && ProviderObservable.getInstance().getCurrentProvider().isConfigured());
+ }
+ }
+ });
+ binding.viewPager.setAdapter(adapter);
+ binding.viewPager.setUserInputEnabled(false);
+ binding.viewPager.setCurrentItem(currentPosition, false);
+
+ binding.setupNextButton.setOnClickListener(v -> {
+ int currentPos = binding.viewPager.getCurrentItem();
+ int newPos = currentPos + 1;
+ if (newPos >= binding.viewPager.getAdapter().getItemCount()) {
+ Toast.makeText(SetupActivity.this, "SetupFinished \\o/", Toast.LENGTH_LONG).show();
+ return;
+ }
+ binding.viewPager.setCurrentItem(newPos);
+ });
+ binding.setupCancelButton.setOnClickListener(v -> {
+ cancel();
+ });
+ setupActionBar();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (provider != null) {
+ outState.putParcelable(EXTRA_PROVIDER, provider);
+ outState.putInt(EXTRA_CURRENT_POSITION, currentPosition);
+ }
+ }
+
+ private void cancel() {
+ binding.viewPager.setCurrentItem(0, false);
+ if (TorStatusObservable.getStatus() != OFF) {
+ Log.d(TAG, "SHUTDOWN - cancelSettingUpProvider");
+ TorServiceCommand.stopTorServiceAsync(this);
+ }
+ provider = null;
+ for (CancelCallback cancelCallback : cancelCallbacks) {
+ cancelCallback.onCanceled();
+ }
+ }
+
+ private void addIndicatorView(ArrayList<View> indicatorViews) {
+ // FIXME: we have to work around a bug in our usage of CardView, that
+ // doesn't let us programmatically add new indicator views as needed.
+ // for some reason the cardBackgroundColor property is ignored if we add
+ // the card view dynamically
+ View v = binding.indicatorContainer.getChildAt(indicatorViews.size());
+ if (v == null) {
+ throw new IllegalStateException("Too few indicator views in layout hard-coded");
+ }
+ v.setVisibility(VISIBLE);
+ indicatorViews.add(v);
+ }
+
+ private void setupActionBar() {
+ setSupportActionBar(binding.toolbar);
+ final ActionBar actionBar = getSupportActionBar();
+ Context context = actionBar.getThemedContext();
+ actionBar.setDisplayOptions(DISPLAY_SHOW_CUSTOM);
+
+ ActionBarTitle actionBarTitle = new ActionBarTitle(context);
+ actionBarTitle.setTitleCaps(BuildConfig.actionbar_capitalize_title);
+ actionBarTitle.setTitle(getString(R.string.app_name));
+
+ final Drawable upArrow = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_back, getTheme());
+ actionBar.setHomeAsUpIndicator(upArrow);
+
+ actionBar.setDisplayHomeAsUpEnabled(currentPosition == 0 && ProviderObservable.getInstance().getCurrentProvider().isConfigured());
+ ViewHelper.setActivityBarColor(this, R.color.bg_setup_status_bar, R.color.bg_setup_action_bar, R.color.colorActionBarTitleFont);
+ @ColorInt int titleColor = ContextCompat.getColor(context, R.color.colorActionBarTitleFont);
+ actionBarTitle.setTitleTextColor(titleColor);
+
+ actionBarTitle.setCentered(BuildConfig.actionbar_center_title);
+ actionBarTitle.setSingleBoldTitle();
+ if (BuildConfig.actionbar_center_title) {
+ ActionBar.LayoutParams params = new ActionBar.LayoutParams(
+ ActionBar.LayoutParams.WRAP_CONTENT,
+ ActionBar.LayoutParams.MATCH_PARENT,
+ Gravity.CENTER);
+ actionBar.setCustomView(actionBarTitle, params);
+ } else {
+ actionBar.setCustomView(actionBarTitle);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onSetupStepValidationChanged(boolean isValid) {
+ binding.setupNextButton.setEnabled(isValid);
+ }
+
+ @Override
+ public void registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback) {
+ binding.viewPager.registerOnPageChangeCallback(callback);
+ }
+
+ @Override
+ public void removeOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback) {
+ binding.viewPager.unregisterOnPageChangeCallback(callback);
+ }
+
+ @Override
+ public void registerCancelCallback(CancelCallback cancelCallback) {
+ cancelCallbacks.add(cancelCallback);
+ }
+
+ @Override
+ public void removeCancelCallback(CancelCallback cancelCallback) {
+ cancelCallbacks.remove(cancelCallback);
+ }
+
+ @Override
+ public void setNavigationButtonHidden(boolean isHidden) {
+ binding.setupNextButton.setVisibility(isHidden ? GONE : VISIBLE);
+ }
+
+ @Override
+ public void setCancelButtonHidden(boolean isHidden) {
+ binding.setupCancelButton.setVisibility(isHidden ? GONE : VISIBLE);
+ }
+
+ @Override
+ public void onProviderSelected(Provider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public void onConfigurationSuccess() {
+ binding.viewPager.setCurrentItem(binding.viewPager.getCurrentItem() + 1);
+ }
+
+ @Override
+ public Provider getSelectedProvider() {
+ return provider;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return currentPosition;
+ }
+
+ @Override
+ public void onSetupFinished() {
+ Intent intent = getIntent();
+ intent.putExtra(Provider.KEY, provider);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ @Override
+ public void onError(String reasonToFail) {
+ binding.viewPager.setCurrentItem(0, false);
+ try {
+ FragmentTransaction fragmentTransaction = fragmentManager.removePreviousFragment(ProviderSetupFailedDialog.TAG);
+ DialogFragment newFragment;
+ try {
+ JSONObject errorJson = new JSONObject(reasonToFail);
+ newFragment = ProviderSetupFailedDialog.newInstance(provider, errorJson, false);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ newFragment = ProviderSetupFailedDialog.newInstance(provider, reasonToFail);
+ } catch (NullPointerException e) {
+ //reasonToFail was null
+ return;
+ }
+ newFragment.show(fragmentTransaction, ProviderSetupFailedDialog.TAG);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void retrySetUpProvider(@NonNull Provider provider) {
+ onProviderSelected(provider);
+ binding.viewPager.setCurrentItem(adapter.getFragmentPostion(CONFIGURE_PROVIDER_FRAGMENT));
+ }
+
+ @Override
+ public void cancelSettingUpProvider() {
+ cancel();
+ }
+
+ @Override
+ public void updateProviderDetails() {
+ provider.reset();
+ deleteProviderDetailsFromPreferences(provider.getDomain());
+ binding.viewPager.setCurrentItem(adapter.getFragmentPostion(CONFIGURE_PROVIDER_FRAGMENT));
+ }
+
+ @Override
+ public void addAndSelectNewProvider(String url) {
+ // ignore, not implemented anymore in new setup flow
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ adapter = null;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivityCallback.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivityCallback.java
new file mode 100644
index 00000000..2c77dfd0
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivityCallback.java
@@ -0,0 +1,33 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import androidx.viewpager2.widget.ViewPager2;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+public interface SetupActivityCallback {
+
+ void onSetupStepValidationChanged(boolean isValid);
+ void registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback);
+ void removeOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback);
+
+ void registerCancelCallback(CancelCallback cancelCallback);
+
+ void removeCancelCallback(CancelCallback cancelCallback);
+
+ void setNavigationButtonHidden(boolean isHidden);
+
+ void setCancelButtonHidden(boolean isHidden);
+
+ void onProviderSelected(Provider provider);
+
+ void onConfigurationSuccess();
+
+ Provider getSelectedProvider();
+
+ int getCurrentPosition();
+
+ void onSetupFinished();
+
+ void onError(String reasonToFail);
+}
+
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java
index e8249692..30d008d5 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java
@@ -19,6 +19,7 @@ import okhttp3.dnsoverhttps.DnsOverHttps;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.IPAddress;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
public class DnsResolver implements Dns {
OkHttpClient dohHttpClient;
@@ -34,7 +35,7 @@ public class DnsResolver implements Dns {
public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
Log.d("DNS", "trying to resolve DNS for " + hostname);
List<InetAddress> list = null;
- if (preferDoH) {
+ if (preferDoH && !"127.0.0.1".equals(hostname)) {
if ((list = tryLookupDoH(hostname)) == null) {
list = tryLookupSystemDNS(hostname);
}
@@ -71,7 +72,7 @@ public class DnsResolver implements Dns {
private List<InetAddress> tryLookupSystemDNS(@NonNull String hostname) throws RuntimeException, UnknownHostException {
try {
- Log.d("DNS", "trying to resolve " + hostname + "with system DNS");
+ Log.d("DNS", "trying to resolve " + hostname + " with system DNS");
return Dns.SYSTEM.lookup(hostname);
} catch (UnknownHostException e) {
e.printStackTrace();
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
index 97393551..b0dbd49b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
@@ -43,10 +43,12 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
@@ -139,6 +141,8 @@ public class OkHttpClientGenerator {
clientBuilder.dns(new DnsResolver(clientBuilder.build(), true));
sslCompatFactory.initSSLSocketFactory(clientBuilder);
+ clientBuilder.connectTimeout(45L, TimeUnit.SECONDS);
+ clientBuilder.readTimeout(45L, TimeUnit.SECONDS);
return clientBuilder.build();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/BaseSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/BaseSetupFragment.java
new file mode 100644
index 00000000..151361ba
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/BaseSetupFragment.java
@@ -0,0 +1,96 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.widget.ViewPager2;
+
+import se.leap.bitmaskclient.providersetup.activities.SetupActivityCallback;
+
+public class BaseSetupFragment extends Fragment {
+
+ public static String EXTRA_POSITION = "EXTRA_POSITION";
+ private boolean callFragmentSelected = false;
+ SetupActivityCallback setupActivityCallback;
+
+ private final ViewPager2.OnPageChangeCallback viewPagerCallback = new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ if (position == BaseSetupFragment.this.position) {
+ handleCallFragmentSelected();
+ } else {
+ callFragmentSelected = false;
+ }
+ }
+ };
+ private int position;
+
+ public static Bundle initBundle(int pos) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_POSITION, pos);
+ return bundle;
+ }
+
+ public BaseSetupFragment() {
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.position = getArguments().getInt(EXTRA_POSITION);
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getActivity() instanceof SetupActivityCallback) {
+ setupActivityCallback = (SetupActivityCallback) getActivity();
+ } else {
+ throw new IllegalStateException("These setup fragments are closely coupled to SetupActivityCallback interface. Activities instantiating them are required to implement the interface");
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putInt(EXTRA_POSITION, position);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setupActivityCallback.registerOnPageChangeCallback(viewPagerCallback);
+ if (setupActivityCallback.getCurrentPosition() == position) {
+ handleCallFragmentSelected();
+ }
+ }
+
+ private void handleCallFragmentSelected() {
+ if (!callFragmentSelected) {
+ callFragmentSelected = true;
+ onFragmentSelected();
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ setupActivityCallback.removeOnPageChangeCallback(viewPagerCallback);
+ callFragmentSelected = false;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDetach() {
+ setupActivityCallback = null;
+ super.onDetach();
+ }
+
+ public void onFragmentSelected() {
+
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java
new file mode 100644
index 00000000..11fa582b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java
@@ -0,0 +1,58 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.databinding.FCircumventionSetupBinding;
+
+public class CircumventionSetupFragment extends BaseSetupFragment {
+
+ public static CircumventionSetupFragment newInstance(int position) {
+ CircumventionSetupFragment fragment = new CircumventionSetupFragment();
+ fragment.setArguments(initBundle(position));
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ FCircumventionSetupBinding binding = FCircumventionSetupBinding.inflate(inflater, container, false);
+
+ binding.circumventionRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (binding.rbCircumvention.getId() == checkedId) {
+ PreferenceHelper.useBridges(true);
+ PreferenceHelper.useSnowflake(true);
+ binding.tvCircumventionDetailDescription.setVisibility(View.VISIBLE);
+ binding.rbCircumvention.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
+ binding.rbPlainVpn.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
+ return;
+ }
+
+ PreferenceHelper.useBridges(false);
+ PreferenceHelper.useSnowflake(false);
+ binding.tvCircumventionDetailDescription.setVisibility(View.GONE);
+ binding.rbPlainVpn.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
+ binding.rbCircumvention.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
+ });
+
+ int id = (PreferenceHelper.hasSnowflakePrefs() && PreferenceHelper.getUseSnowflake()) ?
+ binding.rbCircumvention.getId() :
+ binding.rbPlainVpn.getId();
+ binding.circumventionRadioGroup.check(id);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ setupActivityCallback.setCancelButtonHidden(false);
+ setupActivityCallback.setNavigationButtonHidden(false);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java
new file mode 100644
index 00000000..3c36065e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java
@@ -0,0 +1,222 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import static android.app.Activity.RESULT_CANCELED;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
+import static se.leap.bitmaskclient.R.string.description_configure_provider;
+import static se.leap.bitmaskclient.R.string.description_configure_provider_circumvention;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseSnowflake;
+import static se.leap.bitmaskclient.base.utils.ViewHelper.animateContainerVisibility;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.MISSING_NETWORK_CONNECTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getBootstrapProgress;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastLogs;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastSnowflakeLog;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastTorLog;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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.base.models.Provider;
+import se.leap.bitmaskclient.databinding.FConfigureProviderBinding;
+import se.leap.bitmaskclient.eip.EipSetupListener;
+import se.leap.bitmaskclient.eip.EipSetupObserver;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.providersetup.TorLogAdapter;
+import se.leap.bitmaskclient.providersetup.activities.CancelCallback;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+public class ConfigureProviderFragment extends BaseSetupFragment implements Observer, CancelCallback, EipSetupListener {
+
+ private static final String TAG = ConfigureProviderFragment.class.getSimpleName();
+
+ FConfigureProviderBinding binding;
+ private boolean isExpanded = false;
+ private boolean ignoreProviderAPIUpdates = false;
+ private TorLogAdapter torLogAdapter;
+
+
+ public static ConfigureProviderFragment newInstance(int position) {
+ ConfigureProviderFragment fragment = new ConfigureProviderFragment();
+ fragment.setArguments(initBundle(position));
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ torLogAdapter = new TorLogAdapter(getLastLogs());
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ binding = FConfigureProviderBinding.inflate(inflater, container, false);
+ binding.detailContainer.setVisibility(getUseSnowflake() ? VISIBLE : GONE);
+ binding.tvCircumventionDescription.setText(getUseSnowflake() ? description_configure_provider_circumvention : description_configure_provider);
+ binding.detailHeaderContainer.setOnClickListener(v -> {
+ binding.ivExpand.animate().setDuration(250).rotation(isExpanded ? -90 : 0);
+ showConnectionDetails();
+ animateContainerVisibility(binding.expandableDetailContainer, isExpanded);
+ isExpanded = !isExpanded;
+ });
+
+ binding.ivExpand.animate().setDuration(0).rotation(270);
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this.getContext());
+ binding.connectionDetailLogs.setLayoutManager(layoutManager);
+ binding.connectionDetailLogs.setAdapter(torLogAdapter);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setupActivityCallback.registerCancelCallback(this);
+ TorStatusObservable.getInstance().addObserver(this);
+ EipSetupObserver.addListener(this);
+ }
+
+ @Override
+ public void onDestroyView() {
+ setupActivityCallback.removeCancelCallback(this);
+ TorStatusObservable.getInstance().deleteObserver(this);
+ EipSetupObserver.removeListener(this);
+ binding = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ ignoreProviderAPIUpdates = false;
+ binding.detailContainer.setVisibility(getUseSnowflake() ? VISIBLE : GONE);
+ binding.tvCircumventionDescription.setText(getUseSnowflake() ? description_configure_provider_circumvention : description_configure_provider);
+ setupActivityCallback.setNavigationButtonHidden(true);
+ setupActivityCallback.setCancelButtonHidden(false);
+ ProviderAPICommand.execute(getContext(), SET_UP_PROVIDER, setupActivityCallback.getSelectedProvider());
+ }
+
+ protected void showConnectionDetails() {
+
+ binding.connectionDetailLogs.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ if (newState != SCROLL_STATE_IDLE) {
+ torLogAdapter.postponeUpdate = true;
+ } else if (newState == SCROLL_STATE_IDLE && getFirstVisibleItemPosion() == 0) {
+ torLogAdapter.postponeUpdate = false;
+ }
+ }
+ });
+
+ binding.snowflakeState.setText(getLastSnowflakeLog());
+ binding.torState.setText(getLastTorLog());
+ }
+
+ private int getFirstVisibleItemPosion() {
+ return ((LinearLayoutManager) binding.connectionDetailLogs.getLayoutManager()).findFirstVisibleItemPosition();
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o instanceof TorStatusObservable) {
+ Activity activity = getActivity();
+ if (activity == null || binding == null) {
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ if (binding == null) {
+ return;
+ }
+ if (TorStatusObservable.getStatus() != TorStatusObservable.TorStatus.OFF) {
+ if (binding.connectionDetailContainer.getVisibility() == GONE) {
+ showConnectionDetails();
+ } else {
+ setLogs(getLastTorLog(), getLastSnowflakeLog(), getLastLogs());
+ }
+ }
+ binding.tvProgressStatus.setText(TorStatusObservable.getStringForCurrentStatus(activity));
+ binding.progressSpinner.update(getBootstrapProgress());
+ });
+ }
+ }
+
+ protected void setLogs(String torLog, String snowflakeLog, List<String> lastLogs) {
+ torLogAdapter.updateData(lastLogs);
+ binding.torState.setText(torLog);
+ binding.snowflakeState.setText(snowflakeLog);
+ }
+
+ @Override
+ public void onCanceled() {
+ ignoreProviderAPIUpdates = true;
+ }
+
+ @Override
+ public void handleEipEvent(Intent intent) {}
+
+ @Override
+ public void handleProviderApiEvent(Intent intent) {
+ int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED);
+ Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY);
+ if (resultData == null) {
+ resultData = Bundle.EMPTY;
+ }
+ Provider provider = resultData.getParcelable(PROVIDER_KEY);
+ if (ignoreProviderAPIUpdates ||
+ provider == null ||
+ !setupActivityCallback.getSelectedProvider().getDomain().equals(provider.getDomain())) {
+ return;
+ }
+
+ switch (resultCode) {
+ case PROVIDER_OK:
+ if (provider.allowsAnonymous()) {
+ ProviderAPICommand.execute(this.getContext(), DOWNLOAD_VPN_CERTIFICATE, provider);
+ } else {
+ // TODO: implement error message that this client only supports anonymous usage
+ }
+ break;
+ case CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ setupActivityCallback.onProviderSelected(provider);
+ setupActivityCallback.onConfigurationSuccess();
+ break;
+ case PROVIDER_NOK:
+ case INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ case MISSING_NETWORK_CONNECTION:
+ case TOR_EXCEPTION:
+ case TOR_TIMEOUT:
+ String reasonToFail = resultData.getString(ERRORS);
+ setupActivityCallback.onError(reasonToFail);
+ break;
+
+ }
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java
new file mode 100644
index 00000000..4226d804
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java
@@ -0,0 +1,117 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.databinding.FEmptyPermissionSetupBinding;
+
+public class EmptyPermissionSetupFragment extends BaseSetupFragment {
+
+ public static String EXTRA_VPN_INTENT = "EXTRA_VPN_INTENT";
+ public static String EXTRA_NOTIFICATION_PERMISSON_ACTION = "EXTRA_NOTIFICATION_PERMISSON_ACTION";
+
+ private String notificationPermissionAction = null;
+ private Intent vpnPermissionIntent = null;
+
+ private final ActivityResultLauncher<String> requestNotificationPermissionLauncher =
+ registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
+ if (isGranted) {
+ if (setupActivityCallback != null) {
+ setupActivityCallback.onConfigurationSuccess();
+ }
+ } else {
+ Toast.makeText(getContext(), "Permission request failed :(", Toast.LENGTH_LONG).show();
+ setupActivityCallback.setNavigationButtonHidden(false);
+ // TODO: implement sth. useful
+ }
+ });
+
+ private final ActivityResultLauncher<Intent> requestVpnPermissionLauncher =
+ registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ if (setupActivityCallback != null) {
+ setupActivityCallback.onConfigurationSuccess();
+ }
+ } else {
+ Toast.makeText(getContext(), "Permission request failed :(", Toast.LENGTH_LONG).show();
+ setupActivityCallback.setNavigationButtonHidden(false);
+ // TODO: implement sth. useful
+ }
+ }
+ );
+
+
+ public static EmptyPermissionSetupFragment newInstance(int position, Intent vpnPermissionIntent) {
+ Bundle bundle = initBundle(position);
+ bundle.putParcelable(EXTRA_VPN_INTENT, vpnPermissionIntent);
+ EmptyPermissionSetupFragment fragment = new EmptyPermissionSetupFragment();
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ public static EmptyPermissionSetupFragment newInstance(int position, String notificationPermissionAction) {
+ Bundle bundle = initBundle(position);
+ bundle.putString(EXTRA_NOTIFICATION_PERMISSON_ACTION, notificationPermissionAction);
+ EmptyPermissionSetupFragment fragment = new EmptyPermissionSetupFragment();
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.vpnPermissionIntent = getArguments().getParcelable(EXTRA_VPN_INTENT);
+ this.notificationPermissionAction = getArguments().getString(EXTRA_NOTIFICATION_PERMISSON_ACTION);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ FEmptyPermissionSetupBinding binding = FEmptyPermissionSetupBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ if (vpnPermissionIntent != null) {
+ outState.putParcelable(EXTRA_VPN_INTENT, vpnPermissionIntent);
+ }
+ if (notificationPermissionAction != null) {
+ outState.putString(EXTRA_NOTIFICATION_PERMISSON_ACTION, notificationPermissionAction);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
+ super.onViewStateRestored(savedInstanceState);
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ if (notificationPermissionAction != null) {
+ requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
+ } else if (vpnPermissionIntent != null) {
+ requestVpnPermissionLauncher.launch(vpnPermissionIntent);
+ }
+
+ setupActivityCallback.setNavigationButtonHidden(true);
+ setupActivityCallback.setCancelButtonHidden(true);
+ }
+
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java
new file mode 100644
index 00000000..a9589336
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java
@@ -0,0 +1,35 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.databinding.FNotificationSetupBinding;
+
+public class NotificationSetupFragment extends BaseSetupFragment {
+
+ public static NotificationSetupFragment newInstance(int position) {
+ NotificationSetupFragment fragment = new NotificationSetupFragment();
+ fragment.setArguments(initBundle(position));
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ FNotificationSetupBinding binding = FNotificationSetupBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ setupActivityCallback.setNavigationButtonHidden(false);
+ setupActivityCallback.setCancelButtonHidden(true);
+ }
+
+} \ No newline at end of file
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
new file mode 100644
index 00000000..e8f37e43
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java
@@ -0,0 +1,153 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.ADD_PROVIDER;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.ArrayList;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.ViewHelper;
+import se.leap.bitmaskclient.databinding.FProviderSelectionBinding;
+import se.leap.bitmaskclient.providersetup.activities.CancelCallback;
+import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel;
+import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModelFactory;
+
+public class ProviderSelectionFragment extends BaseSetupFragment implements CancelCallback {
+
+ private ProviderSelectionViewModel viewModel;
+ private ArrayList<RadioButton> radioButtons;
+
+ private FProviderSelectionBinding binding;
+
+ public static ProviderSelectionFragment newInstance(int position) {
+ ProviderSelectionFragment fragment = new ProviderSelectionFragment();
+ fragment.setArguments(initBundle(position));
+ return fragment;
+ }
+
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ viewModel = new ViewModelProvider(this,
+ new ProviderSelectionViewModelFactory(
+ getContext().getApplicationContext().getAssets(),
+ getContext().getExternalFilesDir(null))).
+ get(ProviderSelectionViewModel.class);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ binding = FProviderSelectionBinding.inflate(inflater, container, false);
+
+ radioButtons = new ArrayList<>();
+ for (int i = 0; i < viewModel.size(); i++) {
+ RadioButton radioButton = new RadioButton(binding.getRoot().getContext());
+ radioButton.setText(viewModel.getProviderName(i));
+ radioButton.setId(i);
+ 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);
+
+ binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility());
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setupActivityCallback.registerCancelCallback(this);
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ setupActivityCallback.setCancelButtonHidden(true);
+ setupActivityCallback.setNavigationButtonHidden(false);
+ binding.providerRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ viewModel.setSelected(checkedId);
+ for (RadioButton rb : radioButtons) {
+ rb.setTypeface(Typeface.DEFAULT, rb.getId() == checkedId ? Typeface.BOLD : Typeface.NORMAL);
+ }
+ binding.providerDescription.setText(viewModel.getProviderDescription(getContext()));
+ binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility());
+ setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
+ if (checkedId != ADD_PROVIDER) {
+ setupActivityCallback.onProviderSelected(viewModel.getProvider(checkedId));
+ } else if (viewModel.isValidConfig()) {
+ setupActivityCallback.onProviderSelected(new Provider(binding.editCustomProvider.getText().toString()));
+ }
+ });
+
+ binding.editCustomProvider.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ viewModel.setCustomUrl(s.toString());
+ if (viewModel.isCustomProviderSelected()) {
+ setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
+ if (viewModel.isValidConfig()) {
+ setupActivityCallback.onProviderSelected(new Provider(s.toString()));
+ }
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+
+ binding.editCustomProvider.setOnFocusChangeListener((v, hasFocus) -> {
+ if (!hasFocus) {
+ ViewHelper.hideKeyboardFrom(getContext(), v);
+ }
+ });
+ binding.providerRadioGroup.check(viewModel.getSelected());
+ }
+
+ @Override
+ public void onDestroyView() {
+ setupActivityCallback.removeCancelCallback(this);
+ binding = null;
+ radioButtons = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
+ }
+
+ @Override
+ public void onCanceled() {
+ binding.providerRadioGroup.check(0);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java
new file mode 100644
index 00000000..eaf3fbfa
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java
@@ -0,0 +1,65 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.Manifest;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import java.util.ArrayList;
+
+public class SetupFragmentFactory {
+ public static final int PROVIDER_SELECTION_FRAGMENT = 0;
+ public static final int CIRCUMVENTION_SETUP_FRAGMENT = 1;
+ public static final int CONFIGURE_PROVIDER_FRAGMENT = 2;
+ public static final int VPN_PERMISSON_EDUCATIONAL_FRAGMENT = 3;
+ public static final int VPN_PERMISSON_FRAGMENT = 4;
+ public static final int NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT = 5;
+ public static final int NOTIFICATION_PERMISSON_FRAGMENT = 6;
+
+ public static final int SUCCESS_FRAGMENT = 7;
+
+ private final Intent vpnPermissionRequest;
+
+ private final ArrayList<Integer> fragmentTypes;
+
+ public SetupFragmentFactory(@NonNull ArrayList<Integer> fragmentTypes, Intent vpnPermissionRequest) {
+ this.fragmentTypes = fragmentTypes;
+ this.vpnPermissionRequest = vpnPermissionRequest;
+ }
+
+ public Fragment createFragment(int position) {
+ if (position < 0 || position >= fragmentTypes.size()) {
+ throw new IllegalStateException("Illegal fragment position");
+ }
+ int type = fragmentTypes.get(position);
+ switch (type) {
+ case PROVIDER_SELECTION_FRAGMENT:
+ return ProviderSelectionFragment.newInstance(position);
+ case CIRCUMVENTION_SETUP_FRAGMENT:
+ return CircumventionSetupFragment.newInstance(position);
+ case CONFIGURE_PROVIDER_FRAGMENT:
+ return ConfigureProviderFragment.newInstance(position);
+ case NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT:
+ return NotificationSetupFragment.newInstance(position);
+ case NOTIFICATION_PERMISSON_FRAGMENT:
+ return EmptyPermissionSetupFragment.newInstance(position, Manifest.permission.POST_NOTIFICATIONS);
+ case VPN_PERMISSON_EDUCATIONAL_FRAGMENT:
+ return VpnPermissionSetupFragment.newInstance(position);
+ case VPN_PERMISSON_FRAGMENT:
+ return EmptyPermissionSetupFragment.newInstance(position, vpnPermissionRequest);
+ case SUCCESS_FRAGMENT:
+ return SetupSuccessFragment.newInstance(position);
+ default:
+ throw new IllegalArgumentException("Unexpected fragment type: " + type);
+ }
+ }
+
+ public int getItemCount() {
+ return fragmentTypes.size();
+ }
+
+ public int getPos(int fragmentType) {
+ return fragmentTypes.indexOf(fragmentType);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupSuccessFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupSuccessFragment.java
new file mode 100644
index 00000000..daf4ed6c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupSuccessFragment.java
@@ -0,0 +1,44 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.databinding.FSetupSuccessBinding;
+
+public class SetupSuccessFragment extends BaseSetupFragment {
+
+ public static SetupSuccessFragment newInstance(int position) {
+ SetupSuccessFragment fragment = new SetupSuccessFragment();
+ fragment.setArguments(initBundle(position));
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ FSetupSuccessBinding binding = FSetupSuccessBinding.inflate(inflater, container, false);
+
+ binding.mainButton.setOnClickListener(v -> {
+ setupActivityCallback.onSetupFinished();
+ binding.mainButton.setEnabled(false);
+ binding.mainButton.setCustomDrawable(R.drawable.button_setup_circle_progress);
+ });
+ binding.mainButton.setCustomDrawable(R.drawable.button_setup_circle_start);
+
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ setupActivityCallback.setNavigationButtonHidden(true);
+ setupActivityCallback.setCancelButtonHidden(true);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java
new file mode 100644
index 00000000..188ba9ac
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java
@@ -0,0 +1,36 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.databinding.FVpnPermissionSetupBinding;
+
+public class VpnPermissionSetupFragment extends BaseSetupFragment {
+
+ public static VpnPermissionSetupFragment newInstance(int position) {
+ VpnPermissionSetupFragment fragment = new VpnPermissionSetupFragment();
+ fragment.setArguments(initBundle(position));
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ FVpnPermissionSetupBinding binding = FVpnPermissionSetupBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ setupActivityCallback.setNavigationButtonHidden(false);
+ setupActivityCallback.setCancelButtonHidden(true);
+ }
+
+} \ No newline at end of file
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
new file mode 100644
index 00000000..aa2fe7cb
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java
@@ -0,0 +1,97 @@
+package se.leap.bitmaskclient.providersetup.fragments.viewmodel;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.util.Patterns;
+import android.view.View;
+import android.webkit.URLUtil;
+
+import androidx.lifecycle.ViewModel;
+
+import java.io.File;
+import java.util.List;
+
+import se.leap.bitmaskclient.R;
+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;
+
+ private int selected = 0;
+ private String customUrl;
+
+ public ProviderSelectionViewModel(AssetManager assetManager, File externalFilesDir) {
+ providerManager = ProviderManager.getInstance(assetManager, externalFilesDir);
+ providerManager.setAddDummyEntry(false);
+ }
+
+ public int size() {
+ return providerManager.size();
+ }
+
+ public List<Provider> providers() {
+ return providerManager.providers();
+ }
+
+ public Provider getProvider(int pos) {
+ return providerManager.get(pos);
+ }
+
+ public void setSelected(int checkedId) {
+ selected = checkedId;
+ }
+
+ public int getSelected() {
+ return selected;
+ }
+
+ public boolean isValidConfig() {
+ if (selected == ADD_PROVIDER) {
+ return URLUtil.isValidUrl(customUrl) && Patterns.WEB_URL.matcher(customUrl).matches();
+ }
+ return true;
+ }
+
+ public boolean isCustomProviderSelected() {
+ return selected == ADD_PROVIDER;
+ }
+
+ public CharSequence getProviderDescription(Context context) {
+ if (selected == ADD_PROVIDER) {
+ return context.getText(R.string.add_provider_description);
+ }
+ Provider provider = getProvider(selected);
+ if ("riseup.net".equals(provider.getDomain())) {
+ return context.getText(R.string.provider_description_riseup);
+ }
+ if ("calyx.net".equals(provider.getDomain())) {
+ return context.getText(R.string.provider_description_calyx);
+ }
+ return provider.getDescription();
+ }
+
+ public int getEditProviderVisibility() {
+ if (selected == ADD_PROVIDER) {
+ return View.VISIBLE;
+ }
+ return View.GONE;
+ }
+
+ public void setCustomUrl(String url) {
+ customUrl = url;
+ }
+
+
+ public String getProviderName(int pos) {
+ String domain = getProvider(pos).getDomain();
+ if ("riseup.net".equals(domain)) {
+ return "Riseup";
+ }
+ if ("calyx.net".equals(domain)) {
+ return "The Calyx Institute";
+ }
+ return domain;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModelFactory.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModelFactory.java
new file mode 100644
index 00000000..a21e4924
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModelFactory.java
@@ -0,0 +1,28 @@
+package se.leap.bitmaskclient.providersetup.fragments.viewmodel;
+
+import android.content.res.AssetManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.io.File;
+
+public class ProviderSelectionViewModelFactory implements ViewModelProvider.Factory {
+ private final AssetManager assetManager;
+ private final File externalFilesDir;
+
+ public ProviderSelectionViewModelFactory(AssetManager assetManager, File externalFilesDir) {
+ this.assetManager = assetManager;
+ this.externalFilesDir = externalFilesDir;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ if (modelClass.isAssignableFrom(ProviderSelectionViewModel.class)) {
+ return (T) new ProviderSelectionViewModel(assetManager, externalFilesDir);
+ }
+ throw new IllegalArgumentException("Unknown ViewModel class");
+ }
+} \ No newline at end of file