From addf8d89962bf3de6d70330f9264d0e4d866613e Mon Sep 17 00:00:00 2001 From: cyberta Date: Sat, 29 Jul 2023 17:15:05 +0200 Subject: update design and UX for provider setup --- app/src/main/AndroidManifest.xml | 47 +-- .../se/leap/bitmaskclient/base/StartActivity.java | 3 +- .../leap/bitmaskclient/base/utils/ViewHelper.java | 63 +++ .../bitmaskclient/base/views/ProgressSpinner.java | 53 +++ .../providersetup/ProviderManager.java | 18 +- .../providersetup/SetupViewPagerAdapter.java | 52 +++ .../bitmaskclient/providersetup/TorLogAdapter.java | 61 +++ .../activities/ConfigWizardBaseActivity.java | 48 +-- .../providersetup/activities/SetupActivity.java | 138 +++++++ .../providersetup/activities/SetupInterface.java | 20 + .../fragments/CircumventionSetupFragment.java | 46 +++ .../fragments/ConfigureProviderFragment.java | 163 ++++++++ .../fragments/ProviderSelectionFragment.java | 138 +++++++ .../viewmodel/ProviderSelectionViewModel.java | 81 ++++ .../ProviderSelectionViewModelFactory.java | 28 ++ .../res/drawable-anydpi-v24/ic_arrow_forward.xml | 9 + app/src/main/res/drawable/ic_arrow_forward.png | Bin 0 -> 462 bytes app/src/main/res/drawable/progress_spinner.xml | 28 ++ app/src/main/res/layout/activity_setup.xml | 118 ++++++ app/src/main/res/layout/f_circumvention_setup.xml | 76 ++++ app/src/main/res/layout/f_configure_provider.xml | 188 +++++++++ app/src/main/res/layout/f_provider_selection.xml | 91 +++++ app/src/main/res/layout/v_progress_spinner.xml | 128 ++++++ app/src/main/res/values/strings.xml | 431 +++++++++++---------- 24 files changed, 1737 insertions(+), 291 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/ProgressSpinner.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/TorLogAdapter.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupInterface.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModelFactory.java create mode 100644 app/src/main/res/drawable-anydpi-v24/ic_arrow_forward.xml create mode 100644 app/src/main/res/drawable/ic_arrow_forward.png create mode 100644 app/src/main/res/drawable/progress_spinner.xml create mode 100644 app/src/main/res/layout/activity_setup.xml create mode 100644 app/src/main/res/layout/f_circumvention_setup.xml create mode 100644 app/src/main/res/layout/f_configure_provider.xml create mode 100644 app/src/main/res/layout/f_provider_selection.xml create mode 100644 app/src/main/res/layout/v_progress_spinner.xml (limited to 'app') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cbc06854..a9971904 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,24 +1,8 @@ - - @@ -26,9 +10,9 @@ - - + + + - - - + android:exported="true" + android:permission="android.permission.RECEIVE_BOOT_COMPLETED"> + + + @@ -97,29 +85,23 @@ - - - - - - @@ -144,7 +126,6 @@ android:name="android.service.quicksettings.ACTIVE_TILE" android:value="false" /> - - + \ No newline at end of file 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 { - private AssetManager assetsManager; + private final AssetManager assetsManager; private File externalFilesDir; private Set defaultProviders; private Set customProviders; @@ -49,6 +49,7 @@ public class ProviderManager implements AdapteeCollection { private Set 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 { 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 { } public List providers() { + return providers(addDummyEntry); + } + + private List providers(boolean addEmptyProvider) { List 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 { + private List 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 data) { + values = data; + if (!postponeUpdate) { + notifyDataSetChanged(); + } + } + + public TorLogAdapter(List 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 { - private List 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 data) { - values = data; - if (!postponeUpdate) { - notifyDataSetChanged(); - } - } - - public TorLogAdapter(List 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 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 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 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 create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(ProviderSelectionViewModel.class)) { + return (T) new ProviderSelectionViewModel(assetManager, externalFilesDir); + } + throw new IllegalArgumentException("Unknown ViewModel class"); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v24/ic_arrow_forward.xml b/app/src/main/res/drawable-anydpi-v24/ic_arrow_forward.xml new file mode 100644 index 00000000..fb6b1ad8 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_arrow_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward.png b/app/src/main/res/drawable/ic_arrow_forward.png new file mode 100644 index 00000000..2b45341c Binary files /dev/null and b/app/src/main/res/drawable/ic_arrow_forward.png differ diff --git a/app/src/main/res/drawable/progress_spinner.xml b/app/src/main/res/drawable/progress_spinner.xml new file mode 100644 index 00000000..ae2c4f69 --- /dev/null +++ b/app/src/main/res/drawable/progress_spinner.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml new file mode 100644 index 00000000..fe302bb1 --- /dev/null +++ b/app/src/main/res/layout/activity_setup.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +