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