summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java63
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/ProgressSpinner.java53
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java52
-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/ConfigWizardBaseActivity.java48
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java138
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupInterface.java20
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java46
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java163
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java138
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java81
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModelFactory.java28
14 files changed, 860 insertions, 52 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
index 94000a0f..4748b22e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
@@ -56,6 +56,7 @@ import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.providersetup.ProviderListActivity;
import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity;
+import se.leap.bitmaskclient.providersetup.activities.SetupActivity;
/**
* Activity shown at startup. Evaluates if App is started for the first time or has been upgraded
@@ -249,7 +250,7 @@ public class StartActivity extends Activity{
getIntent().removeExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE);
}
if (isDefaultBitmask()) {
- startActivityForResult(new Intent(this, ProviderListActivity.class), REQUEST_CODE_CONFIGURE_LEAP);
+ startActivityForResult(new Intent(this, SetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP);
} else { // custom branded app
startActivityForResult(new Intent(this, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java
index 51bcb2b1..efc6d093 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java
@@ -1,5 +1,10 @@
package se.leap.bitmaskclient.base.utils;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.Notification;
import android.content.Context;
@@ -16,10 +21,12 @@ import android.view.WindowManager;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
+import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
@@ -146,4 +153,60 @@ public class ViewHelper {
bar.setTitle(spannableTitle);
}
+ public interface AnimationInterface {
+ void onAnimationEnd();
+ }
+
+ public static void animateContainerVisibility(View container, boolean isExpanded) {
+ animateContainerVisibility(container, isExpanded, null);
+ }
+
+ public static void animateContainerVisibility(View container, boolean isExpanded, AnimationInterface animationInterface) {
+
+ int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+
+ container.measure(widthMeasureSpec, heightMeasureSpec);
+ int measuredHeight = container.getMeasuredHeight();
+
+ int targetHeight = isExpanded ? 0 : measuredHeight; // Get the actual content height of the view
+ int initialHeight = isExpanded ? measuredHeight : 0;
+
+ ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight);
+ animator.setDuration(250); // Set the duration of the animation in milliseconds
+
+ animator.addUpdateListener(animation -> {
+ container.getLayoutParams().height = (int) animation.getAnimatedValue();
+ container.requestLayout();
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ if (initialHeight == 0 && container.getVisibility() == GONE) {
+ container.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ if (targetHeight == 0) {
+ container.setVisibility(GONE);
+ }
+ if (animationInterface != null) {
+ animationInterface.onAnimationEnd();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {
+ container.setVisibility(targetHeight == 0 ? GONE : VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animation) {}
+ });
+
+ animator.start();
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/ProgressSpinner.java b/app/src/main/java/se/leap/bitmaskclient/base/views/ProgressSpinner.java
new file mode 100644
index 00000000..b0b81624
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/ProgressSpinner.java
@@ -0,0 +1,53 @@
+package se.leap.bitmaskclient.base.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.RelativeLayout;
+
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.content.ContextCompat;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.databinding.VProgressSpinnerBinding;
+
+public class ProgressSpinner extends RelativeLayout {
+
+ private static final String TAG = ProgressSpinner.class.getSimpleName();
+
+ AppCompatImageView spinnerView;
+ AppCompatTextView textView;
+
+ public ProgressSpinner(Context context) {
+ super(context);
+ initLayout(context);
+ }
+
+ public ProgressSpinner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context);
+ }
+
+ public ProgressSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context);
+ }
+
+
+ public ProgressSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context);
+ }
+
+ private void initLayout(Context context) {
+ VProgressSpinnerBinding binding = VProgressSpinnerBinding.inflate(LayoutInflater.from(context), this, true);
+ spinnerView = binding.spinnerView;
+ textView = binding.tvProgress;
+ }
+
+ public void update(int progress) {
+ textView.setText(textView.getContext().getString(R.string.percentage, progress));
+ }
+}
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/SetupViewPagerAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
new file mode 100644
index 00000000..d32a4eb6
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
@@ -0,0 +1,52 @@
+package se.leap.bitmaskclient.providersetup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+import se.leap.bitmaskclient.providersetup.fragments.CircumventionSetupFragment;
+import se.leap.bitmaskclient.providersetup.fragments.ConfigureProviderFragment;
+import se.leap.bitmaskclient.providersetup.fragments.ProviderSelectionFragment;
+
+public class SetupViewPagerAdapter extends FragmentStateAdapter {
+
+
+ public SetupViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
+ super(fragmentActivity);
+ }
+
+ public SetupViewPagerAdapter(@NonNull Fragment fragment) {
+ super(fragment);
+ }
+
+ public SetupViewPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
+ super(fragmentManager, lifecycle);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case 0:
+ return ProviderSelectionFragment.newInstance();
+ case 1:
+ return CircumventionSetupFragment.newInstance();
+ case 2:
+ return ConfigureProviderFragment.newInstance(position);
+ default:
+ return ProviderSelectionFragment.newInstance();
+ }
+ }
+
+
+
+ @Override
+ public int getItemCount() {
+ return 4;
+ }
+
+
+}
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/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
index caba1436..fdc482fd 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
@@ -15,7 +15,6 @@ 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;
@@ -42,6 +41,7 @@ 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;
/**
@@ -431,50 +431,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..f62f959d
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java
@@ -0,0 +1,138 @@
+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 androidx.annotation.ColorInt;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.viewpager2.widget.ViewPager2;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.views.ActionBarTitle;
+import se.leap.bitmaskclient.databinding.ActivitySetupBinding;
+import se.leap.bitmaskclient.providersetup.SetupViewPagerAdapter;
+
+public class SetupActivity extends AppCompatActivity implements SetupInterface {
+
+ ActivitySetupBinding binding;
+ Provider provider;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivitySetupBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ SetupViewPagerAdapter adapter = new SetupViewPagerAdapter(getSupportFragmentManager(), getLifecycle());
+ View[] indicatorViews = {
+ binding.indicator1,
+ binding.indicator2,
+ binding.indicator3,
+ binding.indicator4,
+ binding.indicator5
+ };
+ binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ for (int i = 0; i < indicatorViews.length; i++) {
+ indicatorViews[i].setBackgroundColor(ContextCompat.getColor(
+ SetupActivity.this,
+ i == position ? R.color.colorPrimaryDark : R.color.colorDisabled));
+ }
+ }
+ });
+ binding.viewPager.setAdapter(adapter);
+ binding.viewPager.setUserInputEnabled(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);
+ });
+ setupActionBar();
+ }
+
+ 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));
+ actionBarTitle.showSubtitle(false);
+
+ @ColorInt int titleColor = ContextCompat.getColor(context, R.color.colorActionBarTitleFont);
+ actionBarTitle.setTitleTextColor(titleColor);
+
+ actionBarTitle.setCentered(BuildConfig.actionbar_center_title);
+ 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);
+ }
+ }
+
+ public int getCurrentFragmentPosition() {
+ return binding.viewPager.getCurrentItem();
+ }
+
+
+ @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 setNavigationButtonHidden(boolean isHidden) {
+ binding.setupNextButton.setVisibility(isHidden ? GONE : VISIBLE);
+ }
+
+ @Override
+ public void onCanceled() {
+ binding.viewPager.setCurrentItem(0);
+ }
+
+ @Override
+ public void onProviderSelected(Provider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public Provider getSelectedProvider() {
+ return provider;
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupInterface.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupInterface.java
new file mode 100644
index 00000000..1438ee5d
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupInterface.java
@@ -0,0 +1,20 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import androidx.viewpager2.widget.ViewPager2;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+public interface SetupInterface {
+
+ void onSetupStepValidationChanged(boolean isValid);
+ void registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback);
+ void removeOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback);
+ void setNavigationButtonHidden(boolean isHidden);
+ void onCanceled();
+
+ void onProviderSelected(Provider provider);
+
+ Provider getSelectedProvider();
+
+}
+
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..606de943
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java
@@ -0,0 +1,46 @@
+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 androidx.fragment.app.Fragment;
+
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.databinding.FCircumventionSetupBinding;
+
+public class CircumventionSetupFragment extends Fragment {
+
+ public static CircumventionSetupFragment newInstance() {
+ return new CircumventionSetupFragment();
+ }
+
+ @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(getContext(), true);
+ PreferenceHelper.useSnowflake(getContext(), true);
+ binding.tvCircumventionDetailDescription.setVisibility(View.VISIBLE);
+ binding.rbCircumvention.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
+ binding.rbPlainVpn.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
+ return;
+ }
+
+ PreferenceHelper.useBridges(getContext(), false);
+ PreferenceHelper.useSnowflake(getContext(), false);
+ binding.tvCircumventionDetailDescription.setVisibility(View.GONE);
+ binding.rbPlainVpn.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
+ binding.rbCircumvention.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
+ });
+ binding.circumventionRadioGroup.check(binding.rbPlainVpn.getId());
+ return binding.getRoot();
+ }
+} \ 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..ceed2c3c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java
@@ -0,0 +1,163 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+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.base.utils.ViewHelper.animateContainerVisibility;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
+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.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 androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager2.widget.ViewPager2;
+
+import java.util.List;
+import java.util.Observable;
+import java.util.Observer;
+
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.databinding.FConfigureProviderBinding;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.providersetup.TorLogAdapter;
+import se.leap.bitmaskclient.providersetup.activities.SetupInterface;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+public class ConfigureProviderFragment extends Fragment implements Observer {
+
+ public static ConfigureProviderFragment newInstance(int position) {
+ return new ConfigureProviderFragment(position);
+ }
+
+ FConfigureProviderBinding binding;
+ private SetupInterface setupInterface;
+ private boolean isExpanded = false;
+ private final int position;
+ private ViewPager2.OnPageChangeCallback viewPagerCallback;
+ private TorLogAdapter torLogAdapter;
+
+
+ public ConfigureProviderFragment(int position) {
+ this.position = position;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ binding = FConfigureProviderBinding.inflate(inflater, container, false);
+ binding.detailContainer.setVisibility(PreferenceHelper.getUseSnowflake(getContext()) ? VISIBLE : GONE);
+ binding.detailHeaderContainer.setOnClickListener(v -> {
+ binding.ivExpand.animate().setDuration(250).rotation(isExpanded ? 0 : 270);
+ showConnectionDetails();
+ animateContainerVisibility(binding.expandableDetailContainer, isExpanded);
+ isExpanded = !isExpanded;
+ });
+
+ binding.ivExpand.animate().setDuration(0).rotation(270);
+ return binding.getRoot();
+ }
+
+
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ setupInterface = (SetupInterface) getActivity();
+ viewPagerCallback = new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ if (position == ConfigureProviderFragment.this.position) {
+ binding.detailContainer.setVisibility(PreferenceHelper.getUseSnowflake(getContext()) ? VISIBLE : GONE);
+ setupInterface.setNavigationButtonHidden(true);
+ ProviderAPICommand.execute(context, SET_UP_PROVIDER, setupInterface.getSelectedProvider());
+ }
+ }
+ };
+ setupInterface.registerOnPageChangeCallback(viewPagerCallback);
+ TorStatusObservable.getInstance().addObserver(this);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ TorStatusObservable.getInstance().deleteObserver(this);
+ setupInterface.removeOnPageChangeCallback(viewPagerCallback);
+ setupInterface = null;
+ }
+
+ protected void showConnectionDetails() {
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this.getContext());
+ binding.connectionDetailLogs.setLayoutManager(layoutManager);
+ torLogAdapter = new TorLogAdapter(getLastLogs());
+ binding.connectionDetailLogs.setAdapter(torLogAdapter);
+
+ 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) {
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ if (TorStatusObservable.getStatus() != TorStatusObservable.TorStatus.OFF) {
+ if (binding.connectionDetailContainer.getVisibility() == GONE) {
+ showConnectionDetails();
+ } else {
+ setLogs(getLastTorLog(), getLastSnowflakeLog(), getLastLogs());
+ }
+ }
+ 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);
+ }
+} \ 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..45ba73dc
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java
@@ -0,0 +1,138 @@
+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.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.ArrayList;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.databinding.FProviderSelectionBinding;
+import se.leap.bitmaskclient.providersetup.activities.SetupInterface;
+import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel;
+import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModelFactory;
+
+public class ProviderSelectionFragment extends Fragment {
+
+ private ProviderSelectionViewModel viewModel;
+ private ArrayList<RadioButton> radioButtons;
+ private SetupInterface setupCallback;
+
+ public static ProviderSelectionFragment newInstance() {
+ return new ProviderSelectionFragment();
+ }
+
+ @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) {
+ FProviderSelectionBinding binding = FProviderSelectionBinding.inflate(inflater, container, false);
+
+ radioButtons = new ArrayList<>();
+ for (int i = 0; i < viewModel.size(); i++) {
+ Provider provider = viewModel.getProvider(i);
+ RadioButton radioButton = new RadioButton(binding.getRoot().getContext());
+ radioButton.setText(provider.getDomain());
+ 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());
+ 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());
+ if (setupCallback != null) {
+ setupCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
+ if (checkedId != ADD_PROVIDER) {
+ setupCallback.onProviderSelected(viewModel.getProvider(checkedId));
+ } else if (viewModel.isValidConfig()) {
+ setupCallback.onProviderSelected(new Provider(binding.editCustomProvider.getText().toString()));
+ }
+ }
+ });
+ binding.providerRadioGroup.check(viewModel.getSelected());
+
+ 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 (setupCallback == null) return;
+ setupCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
+ if (viewModel.isValidConfig()) {
+ setupCallback.onProviderSelected(new Provider(s.toString()));
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getActivity() instanceof SetupInterface) {
+ setupCallback = (SetupInterface) getActivity();
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ setupCallback = null;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ radioButtons = null;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setupCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
+ }
+} \ 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..e3880181
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java
@@ -0,0 +1,81 @@
+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 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;
+ }
+} \ 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