From 0fa7ae499185fefa732a7bc02a8e22ea5da92ec7 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 2 Aug 2023 12:34:45 +0200 Subject: * Implenting permissionn fragments * refactoring fragments, use of a base fragment to deduplicate code * improve SetupViewPagerAdapter by implementing a factory that hands out the reuired fragments in the correct order * very basic setup success fragment ("You're all set!") --- app/build.gradle | 3 +- .../providersetup/SetupViewPagerAdapter.java | 56 +++++++------ .../providersetup/activities/SetupActivity.java | 82 ++++++++++++++----- .../activities/SetupActivityCallback.java | 4 +- .../providersetup/fragments/BaseSetupFragment.java | 63 +++++++++++++++ .../fragments/CircumventionSetupFragment.java | 18 ++++- .../fragments/ConfigureProviderFragment.java | 68 ++++++++-------- .../fragments/EmptyPermissionSetupFragment.java | 92 ++++++++++++++++++++++ .../fragments/NotificationSetupFragment.java | 37 +++++++++ .../fragments/ProviderSelectionFragment.java | 63 ++++++--------- .../fragments/SetupFragmentFactory.java | 61 ++++++++++++++ .../fragments/SetupSuccessFragment.java | 40 ++++++++++ .../fragments/VpnPermissionSetupFragment.java | 44 +++++++++++ app/src/main/res/layout/activity_setup.xml | 33 ++++++-- .../main/res/layout/f_empty_permission_setup.xml | 6 ++ app/src/main/res/layout/f_notification_setup.xml | 34 ++++++++ app/src/main/res/layout/f_setup_success.xml | 73 +++++++++++++++++ app/src/main/res/layout/f_vpn_permission_setup.xml | 34 ++++++++ app/src/main/res/values/strings.xml | 6 ++ 19 files changed, 688 insertions(+), 129 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/BaseSetupFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupSuccessFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java create mode 100644 app/src/main/res/layout/f_empty_permission_setup.xml create mode 100644 app/src/main/res/layout/f_notification_setup.xml create mode 100644 app/src/main/res/layout/f_setup_success.xml create mode 100644 app/src/main/res/layout/f_vpn_permission_setup.xml (limited to 'app') diff --git a/app/build.gradle b/app/build.gradle index 367f69c8..0b5165b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -433,12 +433,11 @@ dependencies { implementation 'androidx.legacy:legacy-support-core-utils:1.0.0' implementation 'androidx.annotation:annotation:1.4.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.fragment:fragment:1.5.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation 'de.hdodenhof:circleimageview:3.1.0' diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java index d32a4eb6..3f585c35 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java @@ -1,51 +1,57 @@ 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.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; +import java.util.ArrayList; -public class SetupViewPagerAdapter extends FragmentStateAdapter { +import se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory; +public class SetupViewPagerAdapter extends FragmentStateAdapter { - public SetupViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) { - super(fragmentActivity); - } + private SetupFragmentFactory setupFragmentFactory; - public SetupViewPagerAdapter(@NonNull Fragment fragment) { - super(fragment); + private SetupViewPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) { + super(fragmentManager, lifecycle); } - public 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 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) { - switch (position) { - case 0: - return ProviderSelectionFragment.newInstance(); - case 1: - return CircumventionSetupFragment.newInstance(); - case 2: - return ConfigureProviderFragment.newInstance(position); - default: - return ProviderSelectionFragment.newInstance(); - } + return setupFragmentFactory.createFragment(position); } - - @Override public int getItemCount() { - return 4; + return setupFragmentFactory.getItemCount(); } diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java index 33e9cbbd..3e91795b 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java @@ -4,6 +4,7 @@ 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.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF; import androidx.annotation.ColorInt; @@ -12,16 +13,22 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.viewpager2.widget.ViewPager2; +import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.VpnService; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; import android.widget.Toast; +import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; import se.leap.bitmaskclient.BuildConfig; import se.leap.bitmaskclient.R; @@ -45,22 +52,39 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal 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 - }; + ArrayList 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); + } + } + } + + SetupViewPagerAdapter adapter = new SetupViewPagerAdapter(getSupportFragmentManager(), getLifecycle(), requestVpnPermission, showNotificationPermissionFragments); + 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)); + 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)); } } }); @@ -87,6 +111,20 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal } }); setupActionBar(); + + } + + private void addIndicatorView(ArrayList 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() { @@ -115,11 +153,6 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal } } - public int getCurrentFragmentPosition() { - return binding.viewPager.getCurrentItem(); - } - - @Override public void onSetupStepValidationChanged(boolean isValid) { binding.setupNextButton.setEnabled(isValid); @@ -170,4 +203,17 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal return provider; } + @Override + public int getCurrentPosition() { + return binding.viewPager.getCurrentItem(); + } + + @Override + public void onSetupFinished() { + Intent intent = getIntent(); + intent.putExtra(Provider.KEY, provider); + setResult(RESULT_OK, intent); + finish(); + } + } \ 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 index 8fe4118d..3906d73a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivityCallback.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivityCallback.java @@ -14,7 +14,6 @@ public interface SetupActivityCallback { void removeCancelCallback(CancelCallback cancelCallback); - void setNavigationButtonHidden(boolean isHidden); void setCancelButtonHidden(boolean isHidden); @@ -25,5 +24,8 @@ public interface SetupActivityCallback { Provider getSelectedProvider(); + int getCurrentPosition(); + + void onSetupFinished(); } 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..8012fe76 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/BaseSetupFragment.java @@ -0,0 +1,63 @@ +package se.leap.bitmaskclient.providersetup.fragments; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.widget.ViewPager2; + +import se.leap.bitmaskclient.providersetup.activities.SetupActivityCallback; + +public class BaseSetupFragment extends Fragment { + + SetupActivityCallback setupActivityCallback; + private boolean callFragmentSelected = false; + 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 final int position; + + public BaseSetupFragment(int position) { + this.position = position; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getActivity() instanceof SetupActivityCallback) { + setupActivityCallback = (SetupActivityCallback) getActivity(); + setupActivityCallback.registerOnPageChangeCallback(viewPagerCallback); + if (setupActivityCallback.getCurrentPosition() == position) { + handleCallFragmentSelected(); + } + } else { + throw new IllegalStateException("These setup fragments are closely coupled to SetupActivityCallback interface. Activities instantiating them are required to implement the interface"); + } + } + + private void handleCallFragmentSelected() { + if (!callFragmentSelected) { + callFragmentSelected = true; + onFragmentSelected(); + } + } + + @Override + public void onDetach() { + super.onDetach(); + setupActivityCallback.removeOnPageChangeCallback(viewPagerCallback); + setupActivityCallback = null; + } + + 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 index 606de943..1bc12f57 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java @@ -8,15 +8,18 @@ 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 class CircumventionSetupFragment extends BaseSetupFragment { - public static CircumventionSetupFragment newInstance() { - return new CircumventionSetupFragment(); + private CircumventionSetupFragment(int position) { + super(position); + } + + public static CircumventionSetupFragment newInstance(int position) { + return new CircumventionSetupFragment(position); } @Override @@ -43,4 +46,11 @@ public class CircumventionSetupFragment extends Fragment { binding.circumventionRadioGroup.check(binding.rbPlainVpn.getId()); 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 index 42d516a0..41e7cb8e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java @@ -8,6 +8,8 @@ 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.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.PROVIDER_OK; import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER; import static se.leap.bitmaskclient.tor.TorStatusObservable.getBootstrapProgress; @@ -25,10 +27,8 @@ 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; @@ -42,10 +42,9 @@ 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.providersetup.activities.SetupActivityCallback; import se.leap.bitmaskclient.tor.TorStatusObservable; -public class ConfigureProviderFragment extends Fragment implements Observer, CancelCallback, EipSetupListener { +public class ConfigureProviderFragment extends BaseSetupFragment implements Observer, CancelCallback, EipSetupListener { private static final String TAG = ConfigureProviderFragment.class.getSimpleName(); @@ -54,15 +53,13 @@ public class ConfigureProviderFragment extends Fragment implements Observer, Can } FConfigureProviderBinding binding; - private SetupActivityCallback setupActivityCallback; private boolean isExpanded = false; - private final int position; - private ViewPager2.OnPageChangeCallback viewPagerCallback; + private boolean ignoreProviderAPIUpdates = false; private TorLogAdapter torLogAdapter; - public ConfigureProviderFragment(int position) { - this.position = position; + private ConfigureProviderFragment(int position) { + super(position); } @Override @@ -98,40 +95,30 @@ public class ConfigureProviderFragment extends Fragment implements Observer, Can binding = null; } + @Override + public void onFragmentSelected() { + super.onFragmentSelected(); + ignoreProviderAPIUpdates = false; + binding.detailContainer.setVisibility(PreferenceHelper.getUseSnowflake(getContext()) ? VISIBLE : GONE); + setupActivityCallback.setNavigationButtonHidden(true); + setupActivityCallback.setCancelButtonHidden(false); + ProviderAPICommand.execute(getContext(), SET_UP_PROVIDER, setupActivityCallback.getSelectedProvider()); + } + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - if (getActivity() instanceof SetupActivityCallback) { - setupActivityCallback = (SetupActivityCallback) 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); - setupActivityCallback.setNavigationButtonHidden(true); - setupActivityCallback.setCancelButtonHidden(false); - ProviderAPICommand.execute(context, SET_UP_PROVIDER, setupActivityCallback.getSelectedProvider()); - } - } - }; - setupActivityCallback.registerOnPageChangeCallback(viewPagerCallback); - setupActivityCallback.registerCancelCallback(this); - } + setupActivityCallback.registerCancelCallback(this); TorStatusObservable.getInstance().addObserver(this); EipSetupObserver.addListener(this); } @Override public void onDetach() { - super.onDetach(); + setupActivityCallback.removeCancelCallback(this); TorStatusObservable.getInstance().deleteObserver(this); - if (setupActivityCallback != null) { - setupActivityCallback.removeOnPageChangeCallback(viewPagerCallback); - setupActivityCallback.removeCancelCallback(this); - setupActivityCallback = null; - } EipSetupObserver.removeListener(this); + super.onDetach(); } protected void showConnectionDetails() { @@ -185,7 +172,7 @@ public class ConfigureProviderFragment extends Fragment implements Observer, Can @Override public void onCanceled() { - + ignoreProviderAPIUpdates = true; } @Override @@ -198,10 +185,23 @@ public class ConfigureProviderFragment extends Fragment implements Observer, Can if (resultData == null) { resultData = Bundle.EMPTY; } + Provider provider = resultData.getParcelable(PROVIDER_KEY); + if (ignoreProviderAPIUpdates || + provider == null || + !setupActivityCallback.getSelectedProvider().getDomain().equals(provider.getDomain())) { + return; + } if (resultCode == PROVIDER_OK) { - Provider provider = resultData.getParcelable(PROVIDER_KEY); + setupActivityCallback.onProviderSelected(provider); + if (provider.allowsAnonymous()) { + ProviderAPICommand.execute(this.getContext(), DOWNLOAD_VPN_CERTIFICATE, provider); + } else { + // TODO: implement error message that this client only supports anonymous usage + } + } else if (resultCode == CORRECTLY_DOWNLOADED_VPN_CERTIFICATE) { setupActivityCallback.onProviderSelected(provider); setupActivityCallback.onConfigurationSuccess(); } } + } \ 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..18751ee5 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java @@ -0,0 +1,92 @@ +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.FVpnPermissionSetupBinding; + +public class EmptyPermissionSetupFragment extends BaseSetupFragment { + + private String notificationPermissionAction = null; + private Intent vpnPermissionIntent = null; + + private final ActivityResultLauncher 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 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 + } + } + ); + + private EmptyPermissionSetupFragment(int position, String permissionAction) { + super(position); + this.notificationPermissionAction = permissionAction; + } + + private EmptyPermissionSetupFragment(int position, Intent vpnPermissionIntent) { + super(position); + this.vpnPermissionIntent = vpnPermissionIntent; + } + + public static EmptyPermissionSetupFragment newInstance(int position, Intent vpnPermissionIntent) { + return new EmptyPermissionSetupFragment(position, vpnPermissionIntent); + } + + public static EmptyPermissionSetupFragment newInstance(int position, String notificationPermissionAction) { + return new EmptyPermissionSetupFragment(position, notificationPermissionAction); + } + + @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(); + 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..72374bb9 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java @@ -0,0 +1,37 @@ +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 { + + private NotificationSetupFragment(int position) { + super(position); + } + + public static NotificationSetupFragment newInstance(int position) { + return new NotificationSetupFragment(position); + } + + @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 index 6ebb149c..7f80a99d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java @@ -14,9 +14,7 @@ import android.widget.RadioButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; -import androidx.viewpager2.widget.ViewPager2; import java.util.ArrayList; @@ -25,33 +23,22 @@ import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.base.models.ProviderObservable; import se.leap.bitmaskclient.databinding.FProviderSelectionBinding; import se.leap.bitmaskclient.providersetup.activities.CancelCallback; -import se.leap.bitmaskclient.providersetup.activities.SetupActivityCallback; import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel; import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModelFactory; -public class ProviderSelectionFragment extends Fragment implements CancelCallback { +public class ProviderSelectionFragment extends BaseSetupFragment implements CancelCallback { private ProviderSelectionViewModel viewModel; private ArrayList radioButtons; - private SetupActivityCallback setupCallback; private FProviderSelectionBinding binding; - private final ViewPager2.OnPageChangeCallback onPageChangeCallback = new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - if (position == 0) { - if (setupCallback != null) { - setupCallback.setCancelButtonHidden(!ProviderObservable.getInstance().getCurrentProvider().isConfigured()); - setupCallback.setNavigationButtonHidden(false); - } - } - } - }; + private ProviderSelectionFragment(int position) { + super(position); + } - public static ProviderSelectionFragment newInstance() { - return new ProviderSelectionFragment(); + public static ProviderSelectionFragment newInstance(int position) { + return new ProviderSelectionFragment(position); } @Override @@ -92,13 +79,11 @@ public class ProviderSelectionFragment extends Fragment implements CancelCallbac } 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())); - } + 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.providerRadioGroup.check(viewModel.getSelected()); @@ -110,10 +95,9 @@ public class ProviderSelectionFragment extends Fragment implements CancelCallbac @Override public void onTextChanged(CharSequence s, int start, int before, int count) { viewModel.setCustomUrl(s.toString()); - if (setupCallback == null) return; - setupCallback.onSetupStepValidationChanged(viewModel.isValidConfig()); + setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig()); if (viewModel.isValidConfig()) { - setupCallback.onProviderSelected(new Provider(s.toString())); + setupActivityCallback.onProviderSelected(new Provider(s.toString())); } } @@ -123,24 +107,23 @@ public class ProviderSelectionFragment extends Fragment implements CancelCallbac return binding.getRoot(); } + @Override + public void onFragmentSelected() { + super.onFragmentSelected(); + setupActivityCallback.setCancelButtonHidden(!ProviderObservable.getInstance().getCurrentProvider().isConfigured()); + setupActivityCallback.setNavigationButtonHidden(false); + } + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - if (getActivity() instanceof SetupActivityCallback) { - setupCallback = (SetupActivityCallback) getActivity(); - setupCallback.registerOnPageChangeCallback(onPageChangeCallback); - setupCallback.registerCancelCallback(this); - } + setupActivityCallback.registerCancelCallback(this); } @Override public void onDetach() { + setupActivityCallback.removeCancelCallback(this); super.onDetach(); - if (setupCallback != null) { - setupCallback.removeOnPageChangeCallback(onPageChangeCallback); - setupCallback.removeCancelCallback(this); - } - setupCallback = null; } @Override @@ -158,7 +141,7 @@ public class ProviderSelectionFragment extends Fragment implements CancelCallbac @Override public void onResume() { super.onResume(); - setupCallback.onSetupStepValidationChanged(viewModel.isValidConfig()); + setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig()); } @Override 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..fd76a841 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java @@ -0,0 +1,61 @@ +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 fragmentTypes; + + public SetupFragmentFactory(@NonNull ArrayList 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(); + } +} 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..3e241012 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupSuccessFragment.java @@ -0,0 +1,40 @@ +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.FSetupSuccessBinding; + +public class SetupSuccessFragment extends BaseSetupFragment { + + + private SetupSuccessFragment(int position) { + super(position); + } + + public static SetupSuccessFragment newInstance(int position) { + return new SetupSuccessFragment(position); + } + + @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()); + 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..a6af0b36 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java @@ -0,0 +1,44 @@ +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 { + + + private VpnPermissionSetupFragment(int position) { + super(position); + } + + public static VpnPermissionSetupFragment newInstance(int position) { + return new VpnPermissionSetupFragment(position); + } + + @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 onAttach(@NonNull Context context) { + super.onAttach(context); + } + + @Override + public void onFragmentSelected() { + super.onFragmentSelected(); + setupActivityCallback.setNavigationButtonHidden(false); + setupActivityCallback.setCancelButtonHidden(true); + } + +} \ 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 index 770b8ab3..bb1682d7 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -47,7 +47,6 @@ android:layout_margin="4dp" > @@ -60,7 +59,6 @@ android:layout_margin="4dp" > @@ -73,7 +71,6 @@ android:layout_margin="4dp" > @@ -84,9 +81,9 @@ app:cardCornerRadius="6dp" app:cardElevation="0dp" android:layout_margin="4dp" + android:visibility="gone" > @@ -97,9 +94,35 @@ app:cardCornerRadius="6dp" app:cardElevation="0dp" android:layout_margin="4dp" + android:visibility="gone" + > + + + + + + diff --git a/app/src/main/res/layout/f_empty_permission_setup.xml b/app/src/main/res/layout/f_empty_permission_setup.xml new file mode 100644 index 00000000..0fbb55e4 --- /dev/null +++ b/app/src/main/res/layout/f_empty_permission_setup.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/f_notification_setup.xml b/app/src/main/res/layout/f_notification_setup.xml new file mode 100644 index 00000000..d9c7d1a3 --- /dev/null +++ b/app/src/main/res/layout/f_notification_setup.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/f_setup_success.xml b/app/src/main/res/layout/f_setup_success.xml new file mode 100644 index 00000000..f48d3487 --- /dev/null +++ b/app/src/main/res/layout/f_setup_success.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/f_vpn_permission_setup.xml b/app/src/main/res/layout/f_vpn_permission_setup.xml new file mode 100644 index 00000000..99dd531b --- /dev/null +++ b/app/src/main/res/layout/f_vpn_permission_setup.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afb073f1..90d06333 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,5 +230,11 @@ Snowflake SOCKS error Snowflake proxy rendezvous successful Sending data via Snowflake + Upcoming Connection Request + In the next panel Android will remind you that it’s essential to trust your VPN provider. Bitmask only partners with providers that adhere to strict privacy best practices for VPNs and have a verifiable history of protecting user’s data and identities. + Upcoming Notifications Request + In the next panel Android will ask if you want to allow notifications. This will ensure a stable background connection and enable you to see your data usage from within Android’s notification center. + You\'re all set! + Click the button below to connect -- cgit v1.2.3