summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap/bitmaskclient/base
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/base')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java98
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java104
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java58
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java372
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java54
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java236
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java674
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java67
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java76
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java120
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java608
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java335
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java587
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java174
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java258
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java168
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java48
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java593
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java39
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java91
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java230
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java29
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java46
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java102
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java21
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java78
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java330
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java273
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java17
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java86
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java116
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java106
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java96
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java109
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java99
36 files changed, 6504 insertions, 0 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
new file mode 100644
index 00000000..4b6fea72
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2020 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.base;
+
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.multidex.MultiDexApplication;
+
+import com.squareup.leakcanary.LeakCanary;
+import com.squareup.leakcanary.RefWatcher;
+
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver;
+import se.leap.bitmaskclient.eip.EipSetupObserver;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.tethering.TetheringStateManager;
+import se.leap.bitmaskclient.base.utils.PRNGFixes;
+
+import static android.content.Intent.CATEGORY_DEFAULT;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD;
+import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE;
+import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSavedProviderFromSharedPreferences;
+
+/**
+ * Created by cyberta on 24.10.17.
+ */
+
+public class BitmaskApp extends MultiDexApplication {
+
+ private final static String TAG = BitmaskApp.class.getSimpleName();
+ private RefWatcher refWatcher;
+ private ProviderObservable providerObservable;
+ private DownloadBroadcastReceiver downloadBroadcastReceiver;
+
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (LeakCanary.isInAnalyzerProcess(this)) {
+ // This process is dedicated to LeakCanary for heap analysis.
+ // You should not init your app in this process.
+ return;
+ }
+ refWatcher = LeakCanary.install(this);
+ // Normal app init code...*/
+ PRNGFixes.apply();
+ SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ providerObservable = ProviderObservable.getInstance();
+ providerObservable.updateProvider(getSavedProviderFromSharedPreferences(preferences));
+ EipSetupObserver.init(this, preferences);
+ AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
+ TetheringStateManager.getInstance().init(this);
+ if (BuildConfig.FLAVOR.contains("Fatweb")) {
+ downloadBroadcastReceiver = new DownloadBroadcastReceiver();
+ IntentFilter intentFilter = new IntentFilter(BROADCAST_DOWNLOAD_SERVICE_EVENT);
+ intentFilter.addAction(ACTION_DOWNLOAD);
+ intentFilter.addAction(CHECK_VERSION_FILE);
+ intentFilter.addAction(DOWNLOAD_UPDATE);
+ intentFilter.addCategory(CATEGORY_DEFAULT);
+ LocalBroadcastManager.getInstance(this.getApplicationContext()).registerReceiver(downloadBroadcastReceiver, intentFilter);
+ }
+ }
+
+ /**
+ * Use this method to get a RefWatcher object that checks for memory leaks in the given context.
+ * Call refWatcher.watch(this) to check if all references get garbage collected.
+ * @param context
+ * @return the RefWatcher object
+ */
+ public static RefWatcher getRefWatcher(Context context) {
+ BitmaskApp application = (BitmaskApp) context.getApplicationContext();
+ return application.refWatcher;
+ }
+
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java
new file mode 100644
index 00000000..4a8b1236
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java
@@ -0,0 +1,104 @@
+package se.leap.bitmaskclient.base;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Build;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+
+
+@TargetApi(Build.VERSION_CODES.N)
+public class BitmaskTileService extends TileService implements Observer {
+
+ @SuppressLint("Override")
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public void onClick() {
+ super.onClick();
+ Provider provider = ProviderObservable.getInstance().getCurrentProvider();
+ if (provider.isConfigured()) {
+ if (!isLocked()) {
+ onTileTap();
+ } else {
+ unlockAndRun(this::onTileTap);
+ }
+ } else {
+ Intent intent = new Intent(getApplicationContext(), StartActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ }
+
+ private void onTileTap() {
+ EipStatus eipStatus = EipStatus.getInstance();
+ if (eipStatus.isConnecting() || eipStatus.isBlocking() || eipStatus.isConnected() || eipStatus.isReconnecting()) {
+ EipCommand.stopVPN(getApplicationContext());
+ } else {
+ EipCommand.startVPN(getApplicationContext(), false);
+ }
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public void onTileAdded() {
+ }
+
+ @Override
+ public void onStartListening() {
+ super.onStartListening();
+ EipStatus.getInstance().addObserver(this);
+ update(EipStatus.getInstance(), null);
+ }
+
+ @Override
+ public void onStopListening() {
+ super.onStopListening();
+ EipStatus.getInstance().deleteObserver(this);
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ Tile t = getQsTile();
+
+ if (o instanceof EipStatus) {
+ EipStatus status = (EipStatus) o;
+ Icon icon;
+ String title;
+ if (status.isConnecting() || status.isReconnecting()) {
+ icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_connecting);
+ title = getResources().getString(R.string.cancel);
+ t.setState(Tile.STATE_ACTIVE);
+ } else if (status.isConnected()) {
+ icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_connected);
+ title = String.format(getString(R.string.qs_disconnect), getString(R.string.app_name));
+ t.setState(Tile.STATE_ACTIVE);
+ } else if (status.isBlocking()) {
+ icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_blocking);
+ title = getString(R.string.vpn_button_turn_off_blocking);
+ t.setState(Tile.STATE_ACTIVE);
+ } else {
+ icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_disconnected);
+ title = String.format(getString(R.string.qs_enable_vpn), getString(R.string.app_name));
+ t.setState(Tile.STATE_INACTIVE);
+ }
+
+
+ t.setIcon(icon);
+ t.setLabel(title);
+
+ t.updateTile();
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java b/app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java
new file mode 100644
index 00000000..bc01dcec
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
+public class FragmentManagerEnhanced {
+
+ private FragmentManager genericFragmentManager;
+
+ public FragmentManagerEnhanced(FragmentManager genericFragmentManager) {
+ this.genericFragmentManager = genericFragmentManager;
+ }
+
+ public FragmentTransaction removePreviousFragment(String tag) {
+ FragmentTransaction transaction = genericFragmentManager.beginTransaction();
+ Fragment previousFragment = genericFragmentManager.findFragmentByTag(tag);
+ if (previousFragment != null) {
+ transaction.remove(previousFragment);
+ }
+
+ return transaction;
+ }
+
+ public void replace(int containerViewId, Fragment fragment, String tag) {
+ try {
+ if (genericFragmentManager.findFragmentByTag(tag) != null) {
+ FragmentTransaction transaction = genericFragmentManager.beginTransaction();
+ transaction.replace(containerViewId, fragment, tag).commit();
+ } else {
+ genericFragmentManager.beginTransaction().add(containerViewId, fragment, tag).commit();
+ }
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public Fragment findFragmentByTag(String tag) {
+ return genericFragmentManager.findFragmentByTag(tag);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
new file mode 100644
index 00000000..1b7de10e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
@@ -0,0 +1,372 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base;
+
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.drawer.NavigationDrawerFragment;
+import se.leap.bitmaskclient.eip.EIP;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.eip.EipSetupListener;
+import se.leap.bitmaskclient.eip.EipSetupObserver;
+import se.leap.bitmaskclient.base.fragments.EipFragment;
+import se.leap.bitmaskclient.base.fragments.ExcludeAppsFragment;
+import se.leap.bitmaskclient.base.fragments.LogFragment;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
+import se.leap.bitmaskclient.providersetup.activities.LoginActivity;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.base.fragments.MainActivityErrorDialog;
+
+import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN;
+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.EIP_ACTION_PREPARE_VPN;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_REQUEST;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
+import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
+import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences;
+
+
+public class MainActivity extends AppCompatActivity implements EipSetupListener, Observer, ExcludeAppsFragment.ExcludedAppsCallback {
+
+ public final static String TAG = MainActivity.class.getSimpleName();
+
+ private Provider provider;
+ private SharedPreferences preferences;
+ private NavigationDrawerFragment navigationDrawerFragment;
+
+ public final static String ACTION_SHOW_VPN_FRAGMENT = "action_show_vpn_fragment";
+ public final static String ACTION_SHOW_LOG_FRAGMENT = "action_show_log_fragment";
+
+ /**
+ * Fragment managing the behaviors, interactions and presentation of the navigation drawer.
+ */
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.a_main);
+ setSupportActionBar(findViewById(R.id.toolbar));
+
+ navigationDrawerFragment = (NavigationDrawerFragment)
+ getSupportFragmentManager().findFragmentById(R.id.navigation_drawer);
+
+ preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ provider = ProviderObservable.getInstance().getCurrentProvider();
+
+ EipSetupObserver.addListener(this);
+ // Set up the drawer.
+ navigationDrawerFragment.setUp(R.id.navigation_drawer, findViewById(R.id.drawer_layout));
+ handleIntentAction(getIntent());
+ }
+
+ @Override
+ public void onBackPressed() {
+ FragmentManagerEnhanced fragmentManagerEnhanced = new FragmentManagerEnhanced(getSupportFragmentManager());
+ Fragment fragment = fragmentManagerEnhanced.findFragmentByTag(MainActivity.TAG);
+ if (fragment == null || !(fragment instanceof EipFragment)) {
+ Fragment eipFragment = new EipFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(PROVIDER_KEY, provider);
+ eipFragment.setArguments(bundle);
+ fragmentManagerEnhanced.replace(R.id.main_container, eipFragment, MainActivity.TAG);
+ hideActionBarSubTitle();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ handleIntentAction(intent);
+ }
+
+ private void handleIntentAction(Intent intent) {
+ if (intent == null || intent.getAction() == null) {
+ return;
+ }
+
+ Fragment fragment = null;
+ switch (intent.getAction()) {
+ case ACTION_SHOW_VPN_FRAGMENT:
+ fragment = new EipFragment();
+ Bundle bundle = new Bundle();
+ if (intent.hasExtra(ASK_TO_CANCEL_VPN)) {
+ bundle.putBoolean(ASK_TO_CANCEL_VPN, true);
+ }
+ bundle.putParcelable(PROVIDER_KEY, provider);
+ fragment.setArguments(bundle);
+ hideActionBarSubTitle();
+ break;
+ case ACTION_SHOW_LOG_FRAGMENT:
+ fragment = new LogFragment();
+ setActionBarTitle(R.string.log_fragment_title);
+ break;
+ default:
+ break;
+ }
+ // on layout change / recreation of the activity, we don't want create new Fragments
+ // instead the fragments themselves care about recreation and state restoration
+ intent.setAction(null);
+
+ if (fragment != null) {
+ new FragmentManagerEnhanced(getSupportFragmentManager())
+ .replace(R.id.main_container, fragment, MainActivity.TAG);
+ }
+ }
+
+ private void hideActionBarSubTitle() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setSubtitle(null);
+ }
+ }
+ private void setActionBarTitle(@StringRes int stringId) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setSubtitle(stringId);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (data == null) {
+ return;
+ }
+
+ if (resultCode == RESULT_OK && data.hasExtra(Provider.KEY)) {
+ provider = data.getParcelableExtra(Provider.KEY);
+
+ if (provider == null) {
+ return;
+ }
+
+ storeProviderInPreferences(preferences, provider);
+ ProviderObservable.getInstance().updateProvider(provider);
+ if (!provider.supportsPluggableTransports()) {
+ PreferenceHelper.usePluggableTransports(this, false);
+ }
+ navigationDrawerFragment.refresh();
+
+ switch (requestCode) {
+ case REQUEST_CODE_SWITCH_PROVIDER:
+ EipCommand.stopVPN(this.getApplicationContext());
+ break;
+ case REQUEST_CODE_CONFIGURE_LEAP:
+ Log.d(TAG, "REQUEST_CODE_CONFIGURE_LEAP - onActivityResult - MainActivity");
+ break;
+ case REQUEST_CODE_LOG_IN:
+ EipCommand.startVPN(this.getApplicationContext(), true);
+ break;
+ }
+ }
+
+ // on switch provider we need to set the EIP fragment
+ Fragment fragment = new EipFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(PROVIDER_KEY, provider);
+ fragment.setArguments(arguments);
+ new FragmentManagerEnhanced(getSupportFragmentManager())
+ .replace(R.id.main_container, fragment, MainActivity.TAG);
+ hideActionBarSubTitle();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ EipSetupObserver.removeListener(this);
+ }
+
+ @Override
+ public void handleEipEvent(Intent intent) {
+ int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED);
+ Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY);
+ if (resultData == null) {
+ resultData = Bundle.EMPTY;
+ }
+ String request = resultData.getString(EIP_REQUEST);
+
+ if (request == null) {
+ return;
+ }
+
+ switch (request) {
+ case EIP_ACTION_START:
+ if (resultCode == RESULT_CANCELED) {
+ String error = resultData.getString(ERRORS);
+ if (isInternalErrorHandling(error)) {
+ return;
+ }
+
+ if (LeapSRPSession.loggedIn() || provider.allowsAnonymous()) {
+ showMainActivityErrorDialog(error);
+ } else if (isInvalidCertificateForLoginOnlyProvider(error)) {
+ askUserToLogIn(getString(vpn_certificate_user_message));
+ }
+ }
+ break;
+ case EIP_ACTION_PREPARE_VPN:
+ if (resultCode == RESULT_CANCELED) {
+ showMainActivityErrorDialog(getString(R.string.vpn_error_establish), ERROR_VPN_PREPARE);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void handleProviderApiEvent(Intent intent) {
+ int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED);
+
+ switch (resultCode) {
+ case INCORRECTLY_DOWNLOADED_EIP_SERVICE:
+ // TODO CATCH ME IF YOU CAN - WHAT DO WE WANT TO DO?
+ break;
+ case INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE:
+ if (LeapSRPSession.loggedIn() || provider.allowsAnonymous()) {
+ showMainActivityErrorDialog(getString(downloading_vpn_certificate_failed));
+ } else {
+ askUserToLogIn(getString(vpn_certificate_user_message));
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o instanceof ProviderObservable) {
+ this.provider = ((ProviderObservable) o).getCurrentProvider();
+ }
+ }
+
+ /**
+ * Shows an error dialog
+ */
+ public void showMainActivityErrorDialog(String reasonToFail) {
+ try {
+
+ FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced(
+ this.getSupportFragmentManager()).removePreviousFragment(
+ MainActivityErrorDialog.TAG);
+ DialogFragment newFragment;
+ try {
+ JSONObject errorJson = new JSONObject(reasonToFail);
+ newFragment = MainActivityErrorDialog.newInstance(provider, errorJson);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail);
+ }
+ newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG);
+ } catch (IllegalStateException | NullPointerException e) {
+ e.printStackTrace();
+ Log.w(TAG, "error dialog leaked!");
+ }
+ }
+
+ /**
+ * Shows an error dialog
+ */
+ public void showMainActivityErrorDialog(String reasonToFail, EIP.EIPErrors error) {
+ try {
+ FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced(
+ this.getSupportFragmentManager()).removePreviousFragment(
+ MainActivityErrorDialog.TAG);
+ DialogFragment newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail, error);
+ newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG);
+ } catch (IllegalStateException | NullPointerException e) {
+ e.printStackTrace();
+ Log.w(TAG, "error dialog leaked!");
+ }
+ }
+
+ /**
+ *
+ * @param errorJsonString
+ * @return true if errorJson is a valid json and contains only ERRORID but
+ * not an ERRORS field containing an error message
+ */
+ public boolean isInternalErrorHandling(String errorJsonString) {
+ try {
+ JSONObject errorJson = new JSONObject(errorJsonString);
+ return !errorJson.has(ERRORS) && errorJson.has(ERRORID);
+ } catch (JSONException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public boolean isInvalidCertificateForLoginOnlyProvider(String errorJsonString) {
+ try {
+ JSONObject errorJson = new JSONObject(errorJsonString);
+ return ERROR_INVALID_VPN_CERTIFICATE.toString().equals(errorJson.getString(ERRORID)) &&
+ !LeapSRPSession.loggedIn() &&
+ !provider.allowsAnonymous();
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ private void askUserToLogIn(String userMessage) {
+ Intent intent = new Intent(this, LoginActivity.class);
+ intent.putExtra(PROVIDER_KEY, provider);
+ if (userMessage != null) {
+ intent.putExtra(USER_MESSAGE, userMessage);
+ }
+ startActivityForResult(intent, REQUEST_CODE_LOG_IN);
+ }
+
+
+ @Override
+ public void onAppsExcluded(int number) {
+ navigationDrawerFragment.onAppsExcluded(number);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java b/app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java
new file mode 100644
index 00000000..df1d3e5a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java
@@ -0,0 +1,54 @@
+package se.leap.bitmaskclient.base;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import de.blinkt.openvpn.core.VpnStatus;
+
+import static android.content.Intent.ACTION_BOOT_COMPLETED;
+import static se.leap.bitmaskclient.base.models.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+
+public class OnBootReceiver extends BroadcastReceiver {
+
+ SharedPreferences preferences;
+
+ // Debug: am broadcast -a android.intent.action.BOOT_COMPLETED
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ //Lint complains if we're not checking the intent action
+ if (intent == null || !ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ return;
+ }
+ preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE);
+ boolean providerConfigured = !preferences.getString(PROVIDER_VPN_CERTIFICATE, "").isEmpty();
+ boolean startOnBoot = preferences.getBoolean(EIP_RESTART_ON_BOOT, false);
+ boolean isAlwaysOnConfigured = VpnStatus.isAlwaysOn();
+ Log.d("OpenVPN", "OpenVPN onBoot intent received. Provider configured? " + providerConfigured + " Start on boot? " + startOnBoot + " isAlwaysOn feature configured: " + isAlwaysOnConfigured);
+ if (providerConfigured) {
+ if (isAlwaysOnConfigured) {
+ //exit because the app is already setting up the vpn
+ return;
+ }
+ if (startOnBoot) {
+ Log.d("OpenVpn", "start StartActivity!");
+ Intent startActivityIntent = new Intent(context.getApplicationContext(), StartActivity.class);
+ startActivityIntent.putExtra(EIP_RESTART_ON_BOOT, true);
+ startActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(startActivityIntent);
+ }
+ } else {
+ if (isAlwaysOnConfigured) {
+ Intent dashboardIntent = new Intent(context.getApplicationContext(), StartActivity.class);
+ dashboardIntent.putExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE, true);
+ dashboardIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(dashboardIntent);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
new file mode 100644
index 00000000..cf64905a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
@@ -0,0 +1,236 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.providersetup.ProviderListActivity;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.base.models.FeatureVersionCode;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+
+import static se.leap.bitmaskclient.base.models.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
+import static se.leap.bitmaskclient.base.models.Constants.PREFERENCES_APP_VERSION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences;
+
+/**
+ * Activity shown at startup. Evaluates if App is started for the first time or has been upgraded
+ * and acts and calls another activity accordingly.
+ *
+ */
+public class StartActivity extends Activity{
+ public static final String TAG = StartActivity.class.getSimpleName();
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIRST, NORMAL, UPGRADE})
+ private @interface StartupMode {}
+ private static final int FIRST = 0;
+ private static final int NORMAL = 1;
+ private static final int UPGRADE = 2;
+
+ private int versionCode;
+ private int previousVersionCode;
+
+ private SharedPreferences preferences;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+
+ Log.d(TAG, "Started");
+
+ switch (checkAppStart()) {
+ case NORMAL:
+ break;
+
+ case FIRST:
+ storeAppVersion();
+ // TODO start ProfileCreation & replace below code
+ // (new Intent(getActivity(), ProviderListActivity.class), Constants.REQUEST_CODE_SWITCH_PROVIDER);
+ break;
+
+ case UPGRADE:
+ executeUpgrade();
+ // TODO show donation dialog
+ break;
+ }
+
+ // initialize app necessities
+ VpnStatus.initLogCache(getApplicationContext().getCacheDir());
+
+ prepareEIP();
+
+ }
+
+ /**
+ * check if normal start, first run, up or downgrade
+ * @return @StartupMode
+ */
+ @StartupMode
+ private int checkAppStart() {
+ try {
+ versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
+ previousVersionCode = preferences.getInt(PREFERENCES_APP_VERSION, -1);
+
+ // versions do match -> normal start
+ if (versionCode == previousVersionCode) {
+ Log.d(TAG, "App start was: NORMAL START");
+ return NORMAL;
+ }
+
+ // no previous app version -> first start
+ if (previousVersionCode == -1 ) {
+ Log.d(TAG, "FIRST START");
+ return FIRST;
+ }
+
+ // version has increased -> upgrade
+ if (versionCode > previousVersionCode) {
+ Log.d(TAG, "UPGRADE");
+ return UPGRADE;
+ }
+
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "Splash screen didn't find any " + getPackageName() + " package");
+ }
+
+ return NORMAL;
+ }
+
+ /**
+ * execute necessary upgrades for version change
+ */
+ private void executeUpgrade() {
+ if (hasNewFeature(FeatureVersionCode.RENAMED_EIP_IN_PREFERENCES)) {
+ String eipJson = preferences.getString(PROVIDER_KEY, null);
+ if (eipJson != null) {
+ preferences.edit().putString(PROVIDER_EIP_DEFINITION, eipJson).
+ remove(PROVIDER_KEY).apply();
+ }
+ }
+
+ if (hasNewFeature(FeatureVersionCode.GEOIP_SERVICE)) {
+ // deletion of current configured provider so that the geoip url will picked out
+ // from the preseeded *.url file / geoipUrl buildconfigfield (build.gradle) during
+ // next setup
+ Provider provider = ProviderObservable.getInstance().getCurrentProvider();
+ if (provider != null && !provider.isDefault()) {
+ PreferenceHelper.deleteProviderDetailsFromPreferences(preferences, provider.getDomain());
+ ProviderObservable.getInstance().updateProvider(null);
+ }
+ }
+
+ // ensure all upgrades have passed before storing new information
+ storeAppVersion();
+ }
+
+ /**
+ * check if an upgrade passed or moved to given milestone
+ * @param featureVersionCode Version code of the Milestone FeatureVersionCode.MILE_STONE
+ * @return true if milestone is reached - false otherwise
+ */
+ private boolean hasNewFeature(int featureVersionCode) {
+ return previousVersionCode < featureVersionCode && versionCode >= featureVersionCode;
+ }
+
+ private void storeAppVersion() {
+ preferences.edit().putInt(PREFERENCES_APP_VERSION, versionCode).apply();
+ }
+
+ private void prepareEIP() {
+ boolean providerExists = ProviderObservable.getInstance().getCurrentProvider() != null;
+ if (providerExists) {
+ Provider provider = ProviderObservable.getInstance().getCurrentProvider();
+ if(!provider.isConfigured()) {
+ configureLeapProvider();
+ } else {
+ Log.d(TAG, "vpn provider is configured");
+ if (getIntent() != null && getIntent().getBooleanExtra(EIP_RESTART_ON_BOOT, false)) {
+ EipCommand.startVPN(this.getApplicationContext(), true);
+ finish();
+ } else if (PreferenceHelper.getRestartOnUpdate(this.getApplicationContext())) {
+ PreferenceHelper.restartOnUpdate(this.getApplicationContext(), false);
+ EipCommand.startVPN(this.getApplicationContext(), false);
+ showMainActivity();
+ finish();
+ } else {
+ showMainActivity();
+ }
+ }
+ } else {
+ configureLeapProvider();
+ }
+ }
+
+ private void configureLeapProvider() {
+ if (getIntent().hasExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) {
+ getIntent().removeExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE);
+ }
+ if (isDefaultBitmask()) {
+ startActivityForResult(new Intent(this, ProviderListActivity.class), REQUEST_CODE_CONFIGURE_LEAP);
+ } else { // custom branded app
+ startActivityForResult(new Intent(this, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) {
+ if (resultCode == RESULT_OK && data != null && data.hasExtra(Provider.KEY)) {
+ Provider provider = data.getParcelableExtra(Provider.KEY);
+ storeProviderInPreferences(preferences, provider);
+ ProviderObservable.getInstance().updateProvider(provider);
+ EipCommand.startVPN(this.getApplicationContext(), false);
+ showMainActivity();
+ } else if (resultCode == RESULT_CANCELED) {
+ finish();
+ }
+ }
+ }
+
+ private void showMainActivity() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setAction(ACTION_SHOW_VPN_FRAGMENT);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java
new file mode 100644
index 00000000..e70c87f9
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java
@@ -0,0 +1,674 @@
+/**
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.drawer;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.ActionBarDrawerToggle;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.Observable;
+import java.util.Observer;
+import java.util.Set;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.base.fragments.EipFragment;
+import se.leap.bitmaskclient.base.FragmentManagerEnhanced;
+import se.leap.bitmaskclient.base.MainActivity;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderListActivity;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.firewall.FirewallManager;
+import se.leap.bitmaskclient.base.fragments.AboutFragment;
+import se.leap.bitmaskclient.base.fragments.AlwaysOnDialog;
+import se.leap.bitmaskclient.base.fragments.ExcludeAppsFragment;
+import se.leap.bitmaskclient.base.fragments.LogFragment;
+import se.leap.bitmaskclient.base.fragments.TetheringDialog;
+import se.leap.bitmaskclient.tethering.TetheringObservable;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.base.views.IconSwitchEntry;
+import se.leap.bitmaskclient.base.views.IconTextEntry;
+
+import static android.content.Context.MODE_PRIVATE;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static se.leap.bitmaskclient.base.BitmaskApp.getRefWatcher;
+import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL;
+import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
+import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS;
+import static se.leap.bitmaskclient.R.string.about_fragment_title;
+import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title;
+import static se.leap.bitmaskclient.R.string.log_fragment_title;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSaveBattery;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getShowAlwaysOnDialog;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveBattery;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.showExperimentalFeatures;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports;
+
+/**
+ * Fragment used for managing interactions for and presentation of a navigation drawer.
+ * See the <a href="https://developer.android.com/design/patterns/navigation-drawer.html#Interaction">
+ * design guidelines</a> for a complete explanation of the behaviors implemented here.
+ */
+public class NavigationDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer {
+
+ /**
+ * Per the design guidelines, you should show the drawer on launch until the user manually
+ * expands it. This shared preference tracks this.
+ */
+ private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned";
+ private static final String TAG = NavigationDrawerFragment.class.getName();
+ public static final int TWO_SECONDS = 2000;
+
+ /**
+ * Helper component that ties the action bar to the navigation drawer.
+ */
+ private ActionBarDrawerToggle drawerToggle;
+
+ private DrawerLayout drawerLayout;
+ private View drawerView;
+ private View fragmentContainerView;
+ private Toolbar toolbar;
+ private IconTextEntry account;
+ private IconSwitchEntry saveBattery;
+ private IconTextEntry tethering;
+ private IconSwitchEntry firewall;
+ private View experimentalFeatureFooter;
+
+ private boolean userLearnedDrawer;
+ private volatile boolean wasPaused;
+ private volatile boolean shouldCloseOnResume;
+
+ private SharedPreferences preferences;
+
+ private final static String KEY_SHOW_SAVE_BATTERY_ALERT = "KEY_SHOW_SAVE_BATTERY_ALERT";
+ private volatile boolean showSaveBattery = false;
+ AlertDialog alertDialog;
+ private FirewallManager firewallManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Reads in the flag indicating whether or not the user has demonstrated awareness of the
+ // drawer. See PREF_USER_LEARNED_DRAWER for details.
+ preferences = getContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ userLearnedDrawer = preferences.getBoolean(PREF_USER_LEARNED_DRAWER, false);
+ preferences.registerOnSharedPreferenceChangeListener(this);
+ firewallManager = new FirewallManager(getContext().getApplicationContext(), false);
+
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Indicates that this fragment would like to influence the set of actions in the action bar.
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ drawerView = inflater.inflate(R.layout.f_drawer_main, container, false);
+ restoreFromSavedInstance(savedInstanceState);
+ TetheringObservable.getInstance().addObserver(this);
+ EipStatus.getInstance().addObserver(this);
+ return drawerView;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ TetheringObservable.getInstance().deleteObserver(this);
+ EipStatus.getInstance().deleteObserver(this);
+ }
+
+ public boolean isDrawerOpen() {
+ return drawerLayout != null && drawerLayout.isDrawerOpen(fragmentContainerView);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ wasPaused = false;
+ if (shouldCloseOnResume) {
+ closeDrawerWithDelay();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ wasPaused = true;
+ }
+
+
+
+ /**
+ * Users of this fragment must call this method to set up the navigation drawer interactions.
+ *
+ * @param fragmentId The android:id of this fragment in its activity's layout.
+ * @param drawerLayout The DrawerLayout containing this fragment's UI.
+ */
+ public void setUp(int fragmentId, DrawerLayout drawerLayout) {
+ final AppCompatActivity activity = (AppCompatActivity) getActivity();
+ fragmentContainerView = activity.findViewById(fragmentId);
+ this.drawerLayout = drawerLayout;
+ // set a custom shadow that overlays the main content when the drawer opens
+ this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+ toolbar = this.drawerLayout.findViewById(R.id.toolbar);
+
+ setupActionBar();
+ setupEntries();
+ setupActionBarDrawerToggle(activity);
+
+ if (!userLearnedDrawer) {
+ openNavigationDrawerForFirstTimeUsers();
+ }
+
+ // Defer code dependent on restoration of previous instance state.
+ this.drawerLayout.post(() -> drawerToggle.syncState());
+ this.drawerLayout.addDrawerListener(drawerToggle);
+ }
+
+ private void setupActionBarDrawerToggle(final AppCompatActivity activity) {
+ // ActionBarDrawerToggle ties together the the proper interactions
+ // between the navigation drawer and the action bar app icon.
+ drawerToggle = new ActionBarDrawerToggle(
+ activity,
+ drawerLayout,
+ toolbar,
+ R.string.navigation_drawer_open,
+ R.string.navigation_drawer_close
+ ) {
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+ if (!isAdded()) {
+ return;
+ }
+ activity.invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!userLearnedDrawer) {
+ // The user manually opened the drawer; store this flag to prevent auto-showing
+ // the navigation drawer automatically in the future.
+ userLearnedDrawer = true;
+ preferences.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply();
+ }
+ activity.invalidateOptionsMenu();
+ }
+ };
+ }
+
+ private void setupEntries() {
+ initAccountEntry();
+ initSwitchProviderEntry();
+ initUseBridgesEntry();
+ initSaveBatteryEntry();
+ initAlwaysOnVpnEntry();
+ initExcludeAppsEntry();
+ initShowExperimentalHint();
+ initTetheringEntry();
+ initFirewallEntry();
+ initExperimentalFeatureFooter();
+ initDonateEntry();
+ initLogEntry();
+ initAboutEntry();
+ }
+
+ private void initAccountEntry() {
+ account = drawerView.findViewById(R.id.account);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
+ account.setText(currentProvider.getName());
+ account.setOnClickListener((buttonView) -> {
+ Fragment fragment = new EipFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(PROVIDER_KEY, currentProvider);
+ fragment.setArguments(arguments);
+ hideActionBarSubTitle();
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ closeDrawer();
+ });
+ }
+
+ private void initSwitchProviderEntry() {
+ if (isDefaultBitmask()) {
+ IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider);
+ switchProvider.setVisibility(VISIBLE);
+ switchProvider.setOnClickListener(v ->
+ getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER));
+ }
+ }
+
+ private void initUseBridgesEntry() {
+ IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch);
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
+ useBridges.setVisibility(VISIBLE);
+ useBridges.setChecked(getUsePluggableTransports(getContext()));
+ useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
+ usePluggableTransports(getContext(), isChecked);
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), false);
+ closeDrawer();
+ }
+ });
+
+
+ } else {
+ useBridges.setVisibility(GONE);
+ }
+ }
+
+ private void initSaveBatteryEntry() {
+ saveBattery = drawerView.findViewById(R.id.battery_switch);
+ saveBattery.showSubtitle(false);
+ saveBattery.setChecked(getSaveBattery(getContext()));
+ saveBattery.setOnCheckedChangeListener(((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
+ if (isChecked) {
+ showSaveBatteryAlert();
+ } else {
+ saveBattery(getContext(), false);
+ }
+ }));
+ boolean enableEntry = !TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning();
+ enableSaveBatteryEntry(enableEntry);
+ }
+
+ private void enableSaveBatteryEntry(boolean enabled) {
+ if (saveBattery.isEnabled() == enabled) {
+ return;
+ }
+ saveBattery.setEnabled(enabled);
+ saveBattery.showSubtitle(!enabled);
+ }
+
+ private void initAlwaysOnVpnEntry() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ IconTextEntry alwaysOnVpn = drawerView.findViewById(R.id.always_on_vpn);
+ alwaysOnVpn.setVisibility(VISIBLE);
+ alwaysOnVpn.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ if (getShowAlwaysOnDialog(getContext())) {
+ showAlwaysOnDialog();
+ } else {
+ Intent intent = new Intent("android.net.vpn.SETTINGS");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ });
+ }
+ }
+
+ private void initExcludeAppsEntry() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps);
+ excludeApps.setVisibility(VISIBLE);
+ Set<String> apps = PreferenceHelper.getExcludedApps(this.getContext());
+ if (apps != null) {
+ updateExcludeAppsSubtitle(excludeApps, apps.size());
+ }
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ excludeApps.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new ExcludeAppsFragment();
+ setActionBarTitle(exclude_apps_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+ }
+
+ private void initShowExperimentalHint() {
+ TextView textView = drawerLayout.findViewById(R.id.show_experimental_features);
+ textView.setText(showExperimentalFeatures(getContext()) ? R.string.hide_experimental : R.string.show_experimental);
+ textView.setOnClickListener(v -> {
+ boolean shown = showExperimentalFeatures(getContext());
+ if (shown) {
+ tethering.setVisibility(GONE);
+ firewall.setVisibility(GONE);
+ experimentalFeatureFooter.setVisibility(GONE);
+ ((TextView) v).setText(R.string.show_experimental);
+ } else {
+ tethering.setVisibility(VISIBLE);
+ firewall.setVisibility(VISIBLE);
+ experimentalFeatureFooter.setVisibility(VISIBLE);
+ ((TextView) v).setText(R.string.hide_experimental);
+ }
+ PreferenceHelper.setShowExperimentalFeatures(getContext(), !shown);
+ });
+ }
+
+ private void initFirewallEntry() {
+ firewall = drawerView.findViewById(R.id.enableIPv6Firewall);
+ boolean show = showExperimentalFeatures(getContext());
+ firewall.setVisibility(show ? VISIBLE : GONE);
+ firewall.setChecked(PreferenceHelper.useIpv6Firewall(getContext()));
+ firewall.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
+ PreferenceHelper.setUseIPv6Firewall(getContext(), isChecked);
+ if (VpnStatus.isVPNActive()) {
+ if (isChecked) {
+ firewallManager.startIPv6Firewall();
+ } else {
+ firewallManager.stopIPv6Firewall();
+ }
+ }
+ });
+ }
+
+ private void initTetheringEntry() {
+ tethering = drawerView.findViewById(R.id.tethering);
+ boolean show = showExperimentalFeatures(getContext());
+ tethering.setVisibility(show ? VISIBLE : GONE);
+ tethering.setOnClickListener((buttonView) -> {
+ showTetheringAlert();
+ });
+ }
+
+ private void initExperimentalFeatureFooter() {
+ experimentalFeatureFooter = drawerView.findViewById(R.id.experimental_features_footer);
+ boolean show = showExperimentalFeatures(getContext());
+ experimentalFeatureFooter.setVisibility(show ? VISIBLE : GONE);
+ }
+
+ private void initDonateEntry() {
+ if (ENABLE_DONATION) {
+ IconTextEntry donate = drawerView.findViewById(R.id.donate);
+ donate.setVisibility(VISIBLE);
+ donate.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL));
+ startActivity(browserIntent);
+
+ });
+ }
+ }
+
+ private void initLogEntry() {
+ IconTextEntry log = drawerView.findViewById(R.id.log);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ log.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new LogFragment();
+ setActionBarTitle(log_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
+ private void initAboutEntry() {
+ IconTextEntry about = drawerView.findViewById(R.id.about);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ about.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new AboutFragment();
+ setActionBarTitle(about_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
+ private void closeDrawer() {
+ if (drawerLayout != null) {
+ drawerLayout.closeDrawer(fragmentContainerView);
+ }
+ }
+
+ private ActionBar setupActionBar() {
+ AppCompatActivity activity = (AppCompatActivity) getActivity();
+ activity.setSupportActionBar(toolbar);
+ final ActionBar actionBar = activity.getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(true);
+ return actionBar;
+ }
+
+ private void openNavigationDrawerForFirstTimeUsers() {
+ if (userLearnedDrawer) {
+ return;
+ }
+
+ drawerLayout.openDrawer(fragmentContainerView, false);
+ closeDrawerWithDelay();
+ }
+
+ @NonNull
+ private void closeDrawerWithDelay() {
+ final Handler navigationDrawerHandler = new Handler();
+ navigationDrawerHandler.postDelayed(() -> {
+ if (!wasPaused) {
+ drawerLayout.closeDrawer(fragmentContainerView, true);
+ } else {
+ shouldCloseOnResume = true;
+ }
+
+ }, TWO_SECONDS);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (showSaveBattery) {
+ outState.putBoolean(KEY_SHOW_SAVE_BATTERY_ALERT, true);
+ alertDialog.dismiss();
+ }
+ }
+
+ private void restoreFromSavedInstance(Bundle savedInstanceState) {
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_SAVE_BATTERY_ALERT)) {
+ showSaveBatteryAlert();
+ }
+ }
+
+ private void showSaveBatteryAlert() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ try {
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
+ showSaveBattery = true;
+ alertDialog = alertBuilder
+ .setTitle(activity.getString(R.string.save_battery))
+ .setMessage(activity.getString(R.string.save_battery_message))
+ .setPositiveButton((android.R.string.yes), (dialog, which) -> {
+ saveBattery(getContext(), true);
+ })
+ .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false))
+ .setOnDismissListener(dialog -> showSaveBattery = false)
+ .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void showTetheringAlert() {
+ try {
+
+ FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced(
+ getActivity().getSupportFragmentManager()).removePreviousFragment(
+ TetheringDialog.TAG);
+ DialogFragment newFragment = new TetheringDialog();
+ newFragment.show(fragmentTransaction, TetheringDialog.TAG);
+ } catch (IllegalStateException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void showAlwaysOnDialog() {
+ try {
+
+ FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced(
+ getActivity().getSupportFragmentManager()).removePreviousFragment(
+ AlwaysOnDialog.TAG);
+ DialogFragment newFragment = new AlwaysOnDialog();
+ newFragment.show(fragmentTransaction, AlwaysOnDialog.TAG);
+ } catch (IllegalStateException | NullPointerException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ // Forward the new configuration the drawer toggle component.
+ drawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (drawerLayout != null && isDrawerOpen()) {
+ showGlobalContextActionBar();
+ }
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ getRefWatcher(getActivity()).watch(this);
+ preferences.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ /**
+ * Per the navigation drawer design guidelines, updates the action bar to show the global app
+ * 'context', rather than just what's in the current screen.
+ */
+ private void showGlobalContextActionBar() {
+ ActionBar actionBar = getActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setTitle(R.string.app_name);
+ }
+
+ private ActionBar getActionBar() {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ }
+
+ private void setActionBarTitle(@StringRes int resId) {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setSubtitle(resId);
+ }
+ }
+
+ private void hideActionBarSubTitle() {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setSubtitle(null);
+ }
+ }
+
+ public void refresh() {
+ Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
+ account.setText(currentProvider.getName());
+ initUseBridgesEntry();
+ }
+
+ private void updateExcludeAppsSubtitle(IconTextEntry excludeApps, int number) {
+ if (number > 0) {
+ excludeApps.setSubtitle(getContext().getResources().getQuantityString(R.plurals.subtitle_exclude_apps, number, number));
+ excludeApps.setSubtitleColor(R.color.colorError);
+ } else {
+ excludeApps.hideSubtitle();
+ }
+ }
+
+ public void onAppsExcluded(int number) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps);
+ updateExcludeAppsSubtitle(excludeApps, number);
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.equals(USE_PLUGGABLE_TRANSPORTS)) {
+ initUseBridgesEntry();
+ } else if (key.equals(USE_IPv6_FIREWALL)) {
+ initFirewallEntry();
+ }
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o instanceof TetheringObservable || o instanceof EipStatus) {
+ try {
+ getActivity().runOnUiThread(() ->
+ enableSaveBatteryEntry(!TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning()));
+ } catch (NullPointerException npe) {
+ // eat me
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java
new file mode 100644
index 00000000..d901ba68
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java
@@ -0,0 +1,67 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.R;
+
+import static android.view.View.VISIBLE;
+
+public class AboutFragment extends Fragment {
+
+ final public static String TAG = AboutFragment.class.getSimpleName();
+ final public static int VIEWED = 0;
+
+ @InjectView(R.id.version)
+ TextView versionTextView;
+
+ @InjectView(R.id.terms_of_service)
+ TextView termsOfService;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.f_about, container, false);
+ ButterKnife.inject(this, view);
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ String version;
+ String name = "Bitmask";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(
+ getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app_name);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+ versionTextView.setText(getString(R.string.version_info, name, version));
+
+ if (BuildConfig.FLAVOR_branding.equals("custom") && hasTermsOfServiceResource()) {
+ termsOfService.setText(getString(getTermsOfServiceResource()));
+ termsOfService.setVisibility(VISIBLE);
+ }
+ }
+
+ private boolean hasTermsOfServiceResource() {
+ return getTermsOfServiceResource() != 0;
+ }
+
+ private int getTermsOfServiceResource() {
+ return this.getContext().getResources().getIdentifier("terms_of_service", "string", this.getContext().getPackageName());
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java
new file mode 100644
index 00000000..a8034e1a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java
@@ -0,0 +1,76 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import android.app.Dialog;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import androidx.appcompat.widget.AppCompatTextView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.views.IconTextView;
+
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveShowAlwaysOnDialog;
+
+
+/**
+ * Created by cyberta on 25.02.18.
+ */
+
+
+
+public class AlwaysOnDialog extends AppCompatDialogFragment {
+
+ public final static String TAG = AlwaysOnDialog.class.getName();
+
+ @InjectView(R.id.do_not_show_again)
+ CheckBox doNotShowAgainCheckBox;
+
+ @InjectView(R.id.user_message)
+ IconTextView userMessage;
+
+ @InjectView(R.id.block_vpn_user_message)
+ AppCompatTextView blockVpnUserMessage;
+
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.d_checkbox_confirm, null);
+ ButterKnife.inject(this, view);
+
+ userMessage.setIcon(R.drawable.ic_settings);
+ userMessage.setText(getString(R.string.always_on_vpn_user_message));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ blockVpnUserMessage.setVisibility(View.VISIBLE);
+ }
+
+ builder.setView(view)
+ .setPositiveButton(android.R.string.ok, (dialog, id) -> {
+ if (doNotShowAgainCheckBox.isChecked()) {
+ saveShowAlwaysOnDialog(getContext(), false);
+ }
+ Intent intent = new Intent("android.net.vpn.SETTINGS");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ })
+ .setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel());
+ return builder.create();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java
new file mode 100644
index 00000000..0277933c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java
@@ -0,0 +1,120 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+
+import java.text.ParseException;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.utils.DateHelper;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+
+import static se.leap.bitmaskclient.base.models.Constants.DONATION_REMINDER_DURATION;
+import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL;
+import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION;
+import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION_REMINDER;
+import static se.leap.bitmaskclient.base.models.Constants.FIRST_TIME_USER_DATE;
+import static se.leap.bitmaskclient.base.models.Constants.LAST_DONATION_REMINDER_DATE;
+
+public class DonationReminderDialog extends AppCompatDialogFragment {
+
+ public final static String TAG = DonationReminderDialog.class.getName();
+ private static boolean isShown = false;
+
+ @InjectView(R.id.btnDonate)
+ Button btnDonate;
+
+ @InjectView(R.id.btnLater)
+ Button btnLater;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.donation_reminder_dialog, null);
+ ButterKnife.inject(this, view);
+ isShown = true;
+
+ builder.setView(view);
+ btnDonate.setOnClickListener(v -> {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL));
+ try {
+ startActivity(browserIntent);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ }
+ PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE,
+ DateHelper.getCurrentDateString());
+ dismiss();
+ });
+ btnLater.setOnClickListener(v -> {
+ PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE,
+ DateHelper.getCurrentDateString());
+ dismiss();
+ });
+
+ return builder.create();
+ }
+
+ public static boolean isCallable(Context context) {
+ if (isShown) {
+ return false;
+ }
+
+ if (!ENABLE_DONATION || !ENABLE_DONATION_REMINDER) {
+ return false;
+ }
+
+ if (context == null) {
+ Log.e(TAG, "context is null!");
+ return false;
+ }
+
+ String firstTimeUserDate = PreferenceHelper.getString(context, FIRST_TIME_USER_DATE, null);
+ if (firstTimeUserDate == null) {
+ PreferenceHelper.putString(context, FIRST_TIME_USER_DATE, DateHelper.getCurrentDateString());
+ return false;
+ }
+
+ try {
+ long diffDays;
+
+ diffDays = DateHelper.getDateDiffToCurrentDateInDays(firstTimeUserDate);
+ if (diffDays < 1) {
+ return false;
+ }
+
+ String lastDonationReminderDate = PreferenceHelper.getString(context, LAST_DONATION_REMINDER_DATE, null);
+ if (lastDonationReminderDate == null) {
+ return true;
+ }
+ diffDays = DateHelper.getDateDiffToCurrentDateInDays(lastDonationReminderDate);
+ return diffDays >= DONATION_REMINDER_DURATION;
+
+ } catch (ParseException e) {
+ e.printStackTrace();
+ Log.e(TAG, e.getMessage());
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
new file mode 100644
index 00000000..9544fb1e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
@@ -0,0 +1,608 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.fragments;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Vibrator;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.widget.AppCompatButton;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import butterknife.OnClick;
+import de.blinkt.openvpn.core.IOpenVPNServiceInternal;
+import de.blinkt.openvpn.core.OpenVPNService;
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.providersetup.ProviderListActivity;
+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.views.VpnStateImage;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity;
+import se.leap.bitmaskclient.providersetup.activities.LoginActivity;
+import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
+import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
+import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx;
+import static se.leap.bitmaskclient.eip.EipSetupObserver.connectionRetry;
+import static se.leap.bitmaskclient.eip.EipSetupObserver.gatewayOrder;
+import static se.leap.bitmaskclient.eip.EipSetupObserver.reconnectingWithDifferentGateway;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
+
+public class EipFragment extends Fragment implements Observer {
+
+ public final static String TAG = EipFragment.class.getSimpleName();
+
+
+ private SharedPreferences preferences;
+ private Provider provider;
+
+ @InjectView(R.id.background)
+ AppCompatImageView background;
+
+ @InjectView(R.id.vpn_state_image)
+ VpnStateImage vpnStateImage;
+
+ @InjectView(R.id.vpn_main_button)
+ AppCompatButton mainButton;
+
+ @InjectView(R.id.routed_text)
+ AppCompatTextView routedText;
+
+ @InjectView(R.id.vpn_route)
+ AppCompatTextView vpnRoute;
+
+
+
+ private EipStatus eipStatus;
+
+ //---saved Instance -------
+ private final String KEY_SHOW_PENDING_START_CANCELLATION = "KEY_SHOW_PENDING_START_CANCELLATION";
+ private final String KEY_SHOW_ASK_TO_STOP_EIP = "KEY_SHOW_ASK_TO_STOP_EIP";
+ private boolean showPendingStartCancellation = false;
+ private boolean showAskToStopEip = false;
+ //------------------------
+ AlertDialog alertDialog;
+
+ private IOpenVPNServiceInternal mService;
+ private ServiceConnection openVpnConnection;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ Bundle arguments = getArguments();
+ Activity activity = getActivity();
+ if (activity != null) {
+ if (arguments != null) {
+ provider = arguments.getParcelable(PROVIDER_KEY);
+ if (provider == null) {
+ handleNoProvider(activity);
+ } else {
+ Log.d(TAG, provider.getName() + " configured as provider");
+ }
+ } else {
+ handleNoProvider(activity);
+ }
+ }
+ }
+
+ private void handleNoProvider(Activity activity) {
+ if (isDefaultBitmask()) {
+ activity.startActivityForResult(new Intent(activity, ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER);
+ } else {
+ Log.e(TAG, "no provider given - try to reconfigure custom provider");
+ startActivityForResult(new Intent(activity, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP);
+
+ }
+
+ }
+
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ openVpnConnection = new EipFragmentServiceConnection();
+ eipStatus = EipStatus.getInstance();
+ Activity activity = getActivity();
+ if (activity != null) {
+ preferences = getActivity().getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE);
+ } else {
+ Log.e(TAG, "activity is null in onCreate - no preferences set!");
+ }
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ eipStatus.addObserver(this);
+ View view = inflater.inflate(R.layout.f_eip, container, false);
+ ButterKnife.inject(this, view);
+
+ Bundle arguments = getArguments();
+ if (arguments != null && arguments.containsKey(ASK_TO_CANCEL_VPN) && arguments.getBoolean(ASK_TO_CANCEL_VPN)) {
+ arguments.remove(ASK_TO_CANCEL_VPN);
+ setArguments(arguments);
+ askToStopEIP();
+ }
+ restoreFromSavedInstance(savedInstanceState);
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (DonationReminderDialog.isCallable(getContext())) {
+ showDonationReminderDialog();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ //FIXME: avoid race conditions while checking certificate an logging in at about the same time
+ //eipCommand(Constants.EIP_ACTION_CHECK_CERT_VALIDITY);
+ bindOpenVpnService();
+ handleNewState();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ Activity activity = getActivity();
+ if (activity != null) {
+ getActivity().unbindService(openVpnConnection);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (showAskToStopEip) {
+ outState.putBoolean(KEY_SHOW_ASK_TO_STOP_EIP, true);
+ alertDialog.dismiss();
+ } else if (showPendingStartCancellation) {
+ outState.putBoolean(KEY_SHOW_PENDING_START_CANCELLATION, true);
+ alertDialog.dismiss();
+ }
+ }
+
+ private void restoreFromSavedInstance(Bundle savedInstanceState) {
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_PENDING_START_CANCELLATION)) {
+ showPendingStartCancellation = true;
+ askPendingStartCancellation();
+ } else if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_ASK_TO_STOP_EIP)) {
+ showAskToStopEip = true;
+ askToStopEIP();
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ eipStatus.deleteObserver(this);
+ }
+
+ private void saveStatus(boolean restartOnBoot) {
+ preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, restartOnBoot).apply();
+ }
+
+ @OnClick(R.id.vpn_main_button)
+ void onButtonClick() {
+ handleIcon();
+ }
+
+ @OnClick(R.id.vpn_state_image)
+ void onVpnStateImageClick() {
+ handleIcon();
+ }
+
+ void handleIcon() {
+ if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting())
+ handleSwitchOff();
+ else
+ handleSwitchOn();
+ }
+
+ private void handleSwitchOn() {
+ Context context = getContext();
+ if (context == null) {
+ Log.e(TAG, "context is null when switch turning on");
+ return;
+ }
+
+ if (canStartEIP()) {
+ startEipFromScratch();
+ } else if (canLogInToStartEIP()) {
+ askUserToLogIn(getString(vpn_certificate_user_message));
+ } else {
+ // provider has no VpnCertificate but user is logged in
+ updateInvalidVpnCertificate();
+ }
+ }
+
+ private boolean canStartEIP() {
+ boolean certificateExists = provider.hasVpnCertificate();
+ boolean isAllowedAnon = provider.allowsAnonymous();
+ return (isAllowedAnon || certificateExists) && !eipStatus.isConnected() && !eipStatus.isConnecting();
+ }
+
+ private boolean canLogInToStartEIP() {
+ boolean isAllowedRegistered = provider.allowsRegistered();
+ boolean isLoggedIn = LeapSRPSession.loggedIn();
+ return isAllowedRegistered && !isLoggedIn && !eipStatus.isConnecting() && !eipStatus.isConnected();
+ }
+
+ private void handleSwitchOff() {
+ if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting()) {
+ askPendingStartCancellation();
+ } else if (eipStatus.isConnected()) {
+ askToStopEIP();
+ }
+ }
+
+ private void setMainButtonEnabled(boolean enabled) {
+ mainButton.setEnabled(enabled);
+ vpnStateImage.setEnabled(enabled);
+ }
+
+ public void startEipFromScratch() {
+ saveStatus(true);
+ Context context = getContext();
+ if (context == null) {
+ Log.e(TAG, "context is null when trying to start VPN");
+ return;
+ }
+ if (!provider.getGeoipUrl().isDefault() && provider.shouldUpdateGeoIpJson()) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(EIP_ACTION_START, true);
+ bundle.putBoolean(EIP_EARLY_ROUTES, false);
+ ProviderAPICommand.execute(getContext().getApplicationContext(), DOWNLOAD_GEOIP_JSON, bundle, provider);
+ } else {
+ EipCommand.startVPN(context.getApplicationContext(), false);
+ }
+ vpnStateImage.showProgress();
+ routedText.setVisibility(GONE);
+ vpnRoute.setVisibility(GONE);
+ colorBackgroundALittle();
+ }
+
+ protected void stopEipIfPossible() {
+ Context context = getContext();
+ if (context == null) {
+ Log.e(TAG, "context is null when trying to stop EIP");
+ return;
+ }
+ EipCommand.stopVPN(context.getApplicationContext());
+ }
+
+ private void askPendingStartCancellation() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.e(TAG, "activity is null when asking to cancel");
+ return;
+ }
+
+ try {
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
+ showPendingStartCancellation = true;
+ alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title))
+ .setMessage(activity.getString(R.string.eip_cancel_connect_text))
+ .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible())
+ .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> {
+ }).setOnDismissListener(dialog -> showPendingStartCancellation = false).show();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ protected void askToStopEIP() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.e(TAG, "activity is null when asking to stop EIP");
+ return;
+ }
+ try {
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(activity);
+ showAskToStopEip = true;
+ alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title))
+ .setMessage(activity.getString(R.string.eip_warning_browser_inconsistency))
+ .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible())
+ .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> {
+ }).setOnDismissListener(dialog -> showAskToStopEip = false).show();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void update(Observable observable, Object data) {
+ if (observable instanceof EipStatus) {
+ eipStatus = (EipStatus) observable;
+ Activity activity = getActivity();
+ if (activity != null) {
+ activity.runOnUiThread(this::handleNewState);
+ } else {
+ Log.e("EipFragment", "activity is null");
+ }
+ } else if (observable instanceof ProviderObservable) {
+ provider = ((ProviderObservable) observable).getCurrentProvider();
+ }
+ }
+
+ private void handleNewState() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.e(TAG, "activity is null while trying to handle new state");
+ return;
+ }
+
+ //Log.d(TAG, "eip fragment eipStatus state: " + eipStatus.getState() + " - level: " + eipStatus.getLevel() + " - is reconnecting: " + eipStatus.isReconnecting());
+
+
+ if (eipStatus.isConnecting() ) {
+ setMainButtonEnabled(true);
+ showConnectingLayout(activity);
+ if (eipStatus.isReconnecting()) {
+ //Log.d(TAG, "eip show reconnecting toast!");
+ //showReconnectToast(activity);
+ }
+ } else if (eipStatus.isConnected() ) {
+ mainButton.setText(activity.getString(R.string.vpn_button_turn_off));
+ setMainButtonEnabled(true);
+ vpnStateImage.setStateIcon(R.drawable.vpn_connected);
+ vpnStateImage.stopProgress(false);
+ routedText.setText(R.string.vpn_securely_routed);
+ routedText.setVisibility(VISIBLE);
+ vpnRoute.setVisibility(VISIBLE);
+ setVpnRouteText();
+ colorBackground();
+ } else if(isOpenVpnRunningWithoutNetwork()){
+ mainButton.setText(activity.getString(R.string.vpn_button_turn_off));
+ setMainButtonEnabled(true);
+ vpnStateImage.setStateIcon(R.drawable.vpn_disconnected);
+ vpnStateImage.stopProgress(false);
+ routedText.setText(R.string.vpn_securely_routed_no_internet);
+ routedText.setVisibility(VISIBLE);
+ vpnRoute.setVisibility(VISIBLE);
+ setVpnRouteText();
+ colorBackgroundALittle();
+ } else if (eipStatus.isDisconnected() && reconnectingWithDifferentGateway()) {
+ showConnectingLayout(activity);
+ // showRetryToast(activity);
+ } else if (eipStatus.isDisconnecting()) {
+ setMainButtonEnabled(false);
+ showDisconnectingLayout(activity);
+ } else if (eipStatus.isBlocking()) {
+ setMainButtonEnabled(true);
+ vpnStateImage.setStateIcon(R.drawable.vpn_blocking);
+ vpnStateImage.stopProgress(false);
+ routedText.setText(getString(R.string.void_vpn_establish, getString(R.string.app_name)));
+ routedText.setVisibility(VISIBLE);
+ vpnRoute.setVisibility(GONE);
+ colorBackgroundALittle();
+ } else {
+ mainButton.setText(activity.getString(R.string.vpn_button_turn_on));
+ setMainButtonEnabled(true);
+ vpnStateImage.setStateIcon(R.drawable.vpn_disconnected);
+ vpnStateImage.stopProgress(false);
+ routedText.setVisibility(GONE);
+ vpnRoute.setVisibility(GONE);
+ greyscaleBackground();
+ }
+ }
+
+ private void showToast(Activity activity, String message, boolean vibrateLong) {
+ LayoutInflater inflater = getLayoutInflater();
+ View layout = inflater.inflate(R.layout.custom_toast,
+ activity.findViewById(R.id.custom_toast_container));
+
+ TextView text = layout.findViewById(R.id.text);
+ text.setText(message);
+
+ Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
+ if (vibrateLong) {
+ v.vibrate(100);
+ v.vibrate(200);
+ } else {
+ v.vibrate(100);
+ }
+
+ Toast toast = new Toast(activity.getApplicationContext());
+ toast.setGravity(Gravity.BOTTOM, 0, convertDimensionToPx(this.getContext(), R.dimen.stdpadding));
+ toast.setDuration(Toast.LENGTH_LONG);
+ toast.setView(layout);
+ toast.show();
+ }
+ private void showReconnectToast(Activity activity) {
+ String message = (String.format("Retry %d of %d before the next closest gateway will be selected.", connectionRetry()+1, 5));
+ showToast(activity, message, false);
+ }
+
+ private void showRetryToast(Activity activity) {
+ int nClosestGateway = gatewayOrder();
+ String message = String.format("Server number " + nClosestGateway + " not reachable. Trying next gateway.");
+ showToast(activity, message, true );
+ }
+
+ private void showConnectingLayout(Context activity) {
+ showConnectionTransitionLayout(activity, true);
+ }
+
+ private void showDisconnectingLayout(Activity activity) {
+ showConnectionTransitionLayout(activity, false);
+ }
+
+ private void showConnectionTransitionLayout(Context activity, boolean isConnecting) {
+ mainButton.setText(activity.getString(android.R.string.cancel));
+ vpnStateImage.setStateIcon(R.drawable.vpn_connecting);
+ vpnStateImage.showProgress();
+ routedText.setVisibility(GONE);
+ vpnRoute.setVisibility(GONE);
+ if (isConnecting) {
+ colorBackgroundALittle();
+ } else {
+ greyscaleBackground();
+ }
+ }
+
+ private boolean isOpenVpnRunningWithoutNetwork() {
+ boolean isRunning = false;
+ try {
+ isRunning = eipStatus.getLevel() == LEVEL_NONETWORK &&
+ mService.isVpnRunning();
+ } catch (Exception e) {
+ //eat me
+ e.printStackTrace();
+ }
+
+ return isRunning;
+ }
+
+ private void bindOpenVpnService() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.e(TAG, "activity is null when binding OpenVpn");
+ return;
+ }
+
+ Intent intent = new Intent(activity, OpenVPNService.class);
+ intent.setAction(OpenVPNService.START_SERVICE);
+ activity.bindService(intent, openVpnConnection, Context.BIND_AUTO_CREATE);
+
+ }
+
+ private void greyscaleBackground() {
+ ColorMatrix matrix = new ColorMatrix();
+ matrix.setSaturation(0);
+ ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix);
+ background.setColorFilter(cf);
+ background.setImageAlpha(255);
+ }
+
+ private void colorBackgroundALittle() {
+ background.setColorFilter(null);
+ background.setImageAlpha(144);
+ }
+
+ private void colorBackground() {
+ background.setColorFilter(null);
+ background.setImageAlpha(210);
+ }
+
+ private void updateInvalidVpnCertificate() {
+ ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider);
+ }
+
+ private void askUserToLogIn(String userMessage) {
+ Intent intent = new Intent(getContext(), LoginActivity.class);
+ intent.putExtra(PROVIDER_KEY, provider);
+
+ if(userMessage != null) {
+ intent.putExtra(USER_MESSAGE, userMessage);
+ }
+
+ Activity activity = getActivity();
+ if (activity != null) {
+ activity.startActivityForResult(intent, REQUEST_CODE_LOG_IN);
+ }
+ }
+
+ private void setVpnRouteText() {
+ String vpnRouteString = provider.getName();
+ String profileName = VpnStatus.getLastConnectedVpnName();
+ if (!TextUtils.isEmpty(profileName)) {
+ vpnRouteString += " (" + profileName + ")";
+ }
+ vpnRoute.setText(vpnRouteString);
+ }
+
+ private class EipFragmentServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName className,
+ IBinder service) {
+ mService = IOpenVPNServiceInternal.Stub.asInterface(service);
+ handleNewState();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ mService = null;
+ }
+ }
+
+ public void showDonationReminderDialog() {
+ try {
+ FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced(
+ getActivity().getSupportFragmentManager()).removePreviousFragment(
+ DonationReminderDialog.TAG);
+ DialogFragment newFragment = new DonationReminderDialog();
+ newFragment.setCancelable(false);
+ newFragment.show(fragmentTransaction, DonationReminderDialog.TAG);
+ } catch (IllegalStateException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java
new file mode 100644
index 00000000..18000171
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package se.leap.bitmaskclient.base.fragments;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.CompoundButton;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SearchView;
+import android.widget.TextView;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.Vector;
+
+import de.blinkt.openvpn.VpnProfile;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+
+/**
+ * Created by arne on 16.11.14.
+ */
+public class ExcludeAppsFragment extends Fragment implements AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener {
+ private ListView mListView;
+ private VpnProfile mProfile;
+ private PackageAdapter mListAdapter;
+
+ private Set<String> apps;
+
+ public interface ExcludedAppsCallback {
+ void onAppsExcluded(int number);
+ }
+
+ private ExcludedAppsCallback callback;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof ExcludedAppsCallback) {
+ callback = (ExcludedAppsCallback) context;
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ AppViewHolder avh = (AppViewHolder) view.getTag();
+ avh.checkBox.toggle();
+ }
+
+ @Override
+ public void onClick(View v) {
+
+ }
+
+ static class AppViewHolder {
+ public ApplicationInfo mInfo;
+ public View rootView;
+ public TextView appName;
+ public ImageView appIcon;
+ //public TextView appSize;
+ //public TextView disabled;
+ public CompoundButton checkBox;
+
+ static public AppViewHolder createOrRecycle(LayoutInflater inflater, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.allowed_application_layout, parent, false);
+
+ // Creates a ViewHolder and store references to the two children views
+ // we want to bind data to.
+ AppViewHolder holder = new AppViewHolder();
+ holder.rootView = convertView;
+ holder.appName = convertView.findViewById(R.id.app_name);
+ holder.appIcon = convertView.findViewById(R.id.app_icon);
+ holder.checkBox = convertView.findViewById(R.id.app_selected);
+ convertView.setTag(holder);
+
+ return holder;
+ } else {
+ // Get the ViewHolder back to get fast access to the TextView
+ // and the ImageView.
+ return (AppViewHolder) convertView.getTag();
+ }
+ }
+
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ String packageName = (String) buttonView.getTag();
+
+ if (isChecked) {
+ Log.d("openvpn", "adding to allowed apps" + packageName);
+ apps.add(packageName);
+
+ } else {
+ Log.d("openvpn", "removing from allowed apps" + packageName);
+ apps.remove(packageName);
+ }
+
+ if (callback != null) {
+ callback.onAppsExcluded(apps.size());
+ }
+ }
+
+ class PackageAdapter extends BaseAdapter implements Filterable {
+ private Vector<ApplicationInfo> mPackages;
+ private final LayoutInflater mInflater;
+ private final PackageManager mPm;
+ private ItemFilter mFilter = new ItemFilter();
+ private Vector<ApplicationInfo> mFilteredData;
+
+
+ private class ItemFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+
+ String filterString = constraint.toString().toLowerCase(Locale.getDefault());
+
+ FilterResults results = new FilterResults();
+
+
+ int count = mPackages.size();
+ final Vector<ApplicationInfo> nlist = new Vector<>(count);
+
+ for (int i = 0; i < count; i++) {
+ ApplicationInfo pInfo = mPackages.get(i);
+ CharSequence appName = pInfo.loadLabel(mPm);
+
+ if (TextUtils.isEmpty(appName))
+ appName = pInfo.packageName;
+
+ if (appName instanceof String) {
+ if (((String) appName).toLowerCase(Locale.getDefault()).contains(filterString))
+ nlist.add(pInfo);
+ } else {
+ if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString))
+ nlist.add(pInfo);
+ }
+ }
+ results.values = nlist;
+ results.count = nlist.size();
+
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ mFilteredData = (Vector<ApplicationInfo>) results.values;
+ notifyDataSetChanged();
+ }
+
+ }
+
+
+ PackageAdapter(Context c, VpnProfile vp) {
+ mPm = c.getPackageManager();
+ mProfile = vp;
+ mInflater = LayoutInflater.from(c);
+
+ mPackages = new Vector<>();
+ mFilteredData = mPackages;
+ }
+
+ private void populateList(Activity c) {
+ List<ApplicationInfo> installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA);
+
+ // Remove apps not using Internet
+
+ int androidSystemUid = 0;
+ ApplicationInfo system = null;
+ Vector<ApplicationInfo> apps = new Vector<ApplicationInfo>();
+
+ try {
+ system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA);
+ androidSystemUid = system.uid;
+ apps.add(system);
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+
+
+ for (ApplicationInfo app : installedPackages) {
+
+ if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED &&
+ app.uid != androidSystemUid) {
+
+ apps.add(app);
+ }
+ }
+
+ Collections.sort(apps, new ApplicationInfo.DisplayNameComparator(mPm));
+ mPackages = apps;
+ mFilteredData = apps;
+ c.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ notifyDataSetChanged();
+ }
+ });
+ }
+
+ @Override
+ public int getCount() {
+ return mFilteredData.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mFilteredData.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mFilteredData.get(position).packageName.hashCode();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ AppViewHolder viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent);
+
+ viewHolder.mInfo = mFilteredData.get(position);
+ final ApplicationInfo mInfo = mFilteredData.get(position);
+
+
+ CharSequence appName = mInfo.loadLabel(mPm);
+
+ if (TextUtils.isEmpty(appName))
+ appName = mInfo.packageName;
+ viewHolder.appName.setText(appName);
+ viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm));
+ viewHolder.checkBox.setTag(mInfo.packageName);
+ viewHolder.checkBox.setOnCheckedChangeListener(ExcludeAppsFragment.this);
+ viewHolder.checkBox.setChecked(apps.contains(mInfo.packageName));
+
+ return viewHolder.rootView;
+ }
+
+ @Override
+ public Filter getFilter() {
+ return mFilter;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ PreferenceHelper.setExcludedApps(this.getActivity().getApplicationContext(), apps);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ apps = PreferenceHelper.getExcludedApps(this.getContext());
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.allowed_apps, menu);
+
+ SearchView searchView = (SearchView) menu.findItem( R.id.app_search_widget ).getActionView();
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ mListView.setFilterText(query);
+ mListView.setTextFilterEnabled(true);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ mListView.setFilterText(newText);
+ if (TextUtils.isEmpty(newText))
+ mListView.setTextFilterEnabled(false);
+ else
+ mListView.setTextFilterEnabled(true);
+
+ return true;
+ }
+ });
+ searchView.setOnCloseListener(() -> {
+ mListView.clearTextFilter();
+ mListAdapter.getFilter().filter("");
+ return false;
+ });
+
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.allowed_vpn_apps, container, false);
+
+ mListView = v.findViewById(android.R.id.list);
+
+ mListAdapter = new PackageAdapter(getActivity(), mProfile);
+ mListView.setAdapter(mListAdapter);
+ mListView.setOnItemClickListener(this);
+
+ mListView.setEmptyView(v.findViewById(R.id.loading_container));
+
+ new Thread(() -> mListAdapter.populateList(getActivity())).start();
+
+ return v;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
new file mode 100644
index 00000000..d788b9e6
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package se.leap.bitmaskclient.base.fragments;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.ListFragment;
+import android.text.SpannableString;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Vector;
+
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ConnectionStatus;
+import de.blinkt.openvpn.core.LogItem;
+import de.blinkt.openvpn.core.OpenVPNManagement;
+import de.blinkt.openvpn.core.OpenVPNService;
+import de.blinkt.openvpn.core.Preferences;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.VpnStatus.LogListener;
+import de.blinkt.openvpn.core.VpnStatus.StateListener;
+import se.leap.bitmaskclient.base.models.Constants;
+import se.leap.bitmaskclient.R;
+
+import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount;
+
+public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener {
+ public static final String TAG = LogFragment.class.getSimpleName();
+ private static final String LOGTIMEFORMAT = "logtimeformat";
+ private static final String VERBOSITYLEVEL = "verbositylevel";
+
+
+
+ private SeekBar mLogLevelSlider;
+ private LinearLayout mOptionsLayout;
+ private RadioGroup mTimeRadioGroup;
+ private TextView mUpStatus;
+ private TextView mDownStatus;
+ private TextView mConnectStatus;
+ private boolean mShowOptionsLayout;
+ private CheckBox mClearLogCheckBox;
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ ladapter.setLogLevel(progress + 1);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ switch (checkedId) {
+ case R.id.radioISO:
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO);
+ break;
+ case R.id.radioNone:
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE);
+ break;
+ case R.id.radioShort:
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT);
+ break;
+
+ }
+ }
+
+ @Override
+ public void updateByteCount(long in, long out, long diffIn, long diffOut) {
+ //%2$s/s %1$s - ↑%4$s/s %3$s
+ Resources res = getActivity().getResources();
+ final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res));
+ final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res));
+
+ if (mUpStatus != null && mDownStatus != null) {
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mUpStatus.setText(up);
+ mDownStatus.setText(down);
+ }
+ });
+ }
+ }
+
+ }
+
+
+ class LogWindowListAdapter implements ListAdapter, LogListener, Callback {
+
+ private static final int MESSAGE_NEWLOG = 0;
+
+ private static final int MESSAGE_CLEARLOG = 1;
+
+ private static final int MESSAGE_NEWTS = 2;
+ private static final int MESSAGE_NEWLOGLEVEL = 3;
+
+ public static final int TIME_FORMAT_NONE = 0;
+ public static final int TIME_FORMAT_SHORT = 1;
+ public static final int TIME_FORMAT_ISO = 2;
+ private static final int MAX_STORED_LOG_ENTRIES = 1000;
+
+ private Vector<LogItem> allEntries = new Vector<>();
+
+ private Vector<LogItem> currentLevelEntries = new Vector<LogItem>();
+
+ private Handler mHandler;
+
+ private Vector<DataSetObserver> observers = new Vector<DataSetObserver>();
+
+ private int mTimeFormat = 0;
+ private int mLogLevel = 3;
+
+
+ public LogWindowListAdapter() {
+ initLogBuffer();
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ VpnStatus.addLogListener(this);
+ }
+
+
+ private void initLogBuffer() {
+ allEntries.clear();
+ Collections.addAll(allEntries, VpnStatus.getlogbuffer());
+ initCurrentMessages();
+ }
+
+ String getLogStr() {
+ String str = "";
+ for (LogItem entry : allEntries) {
+ str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n';
+ }
+ return str;
+ }
+
+
+ private void shareLog() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr());
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, "Send Logfile"));
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ observers.add(observer);
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ observers.remove(observer);
+ }
+
+ @Override
+ public int getCount() {
+ return currentLevelEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return currentLevelEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return ((Object) currentLevelEntries.get(position)).hashCode();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView v;
+ if (convertView == null)
+ v = new TextView(getActivity());
+ else
+ v = (TextView) convertView;
+
+ LogItem le = currentLevelEntries.get(position);
+ String msg = le.getString(getActivity());
+ String time = getTime(le, mTimeFormat);
+ msg = time + msg;
+
+ int spanStart = time.length();
+
+ SpannableString t = new SpannableString(msg);
+
+ v.setText(t);
+ return v;
+ }
+
+ private String getTime(LogItem le, int time) {
+ if (time != TIME_FORMAT_NONE) {
+ Date d = new Date(le.getLogtime());
+ java.text.DateFormat timeformat;
+ if (time == TIME_FORMAT_ISO)
+ timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ else
+ timeformat = DateFormat.getTimeFormat(getActivity());
+
+ return timeformat.format(d) + " ";
+
+ } else {
+ return "";
+ }
+
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return currentLevelEntries.isEmpty();
+
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public void newLog(LogItem logMessage) {
+ Message msg = Message.obtain();
+ assert (msg != null);
+ msg.what = MESSAGE_NEWLOG;
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("logmessage", logMessage);
+ msg.setData(bundle);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ // We have been called
+ if (msg.what == MESSAGE_NEWLOG) {
+
+ LogItem logMessage = msg.getData().getParcelable("logmessage");
+ if (addLogMessage(logMessage))
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+ } else if (msg.what == MESSAGE_CLEARLOG) {
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ initLogBuffer();
+ } else if (msg.what == MESSAGE_NEWTS) {
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ } else if (msg.what == MESSAGE_NEWLOGLEVEL) {
+ initCurrentMessages();
+
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+
+ }
+
+ return true;
+ }
+
+ private void initCurrentMessages() {
+ currentLevelEntries.clear();
+ for (LogItem li : allEntries) {
+ if (li.getVerbosityLevel() <= mLogLevel ||
+ mLogLevel == VpnProfile.MAXLOGLEVEL)
+ currentLevelEntries.add(li);
+ }
+ }
+
+ /**
+ * @param logmessage
+ * @return True if the current entries have changed
+ */
+ private boolean addLogMessage(LogItem logmessage) {
+ allEntries.add(logmessage);
+
+ if (allEntries.size() > MAX_STORED_LOG_ENTRIES) {
+ Vector<LogItem> oldAllEntries = allEntries;
+ allEntries = new Vector<LogItem>(allEntries.size());
+ for (int i = 50; i < oldAllEntries.size(); i++) {
+ allEntries.add(oldAllEntries.elementAt(i));
+ }
+ initCurrentMessages();
+ return true;
+ } else {
+ if (logmessage.getVerbosityLevel() <= mLogLevel) {
+ currentLevelEntries.add(logmessage);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ void clearLog() {
+ // Actually is probably called from GUI Thread as result of the user
+ // pressing a button. But better safe than sorry
+ VpnStatus.clearLog();
+ VpnStatus.logInfo(R.string.logCleared);
+ mHandler.sendEmptyMessage(MESSAGE_CLEARLOG);
+ }
+
+
+ public void setTimeFormat(int newTimeFormat) {
+ mTimeFormat = newTimeFormat;
+ mHandler.sendEmptyMessage(MESSAGE_NEWTS);
+ }
+
+ public void setLogLevel(int logLevel) {
+ mLogLevel = logLevel;
+ mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL);
+ }
+
+ }
+
+
+ private LogWindowListAdapter ladapter;
+ private TextView mSpeedView;
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.clearlog) {
+ ladapter.clearLog();
+ return true;
+ } else if (item.getItemId() == R.id.send) {
+ ladapter.shareLog();
+ } else if (item.getItemId() == R.id.toggle_time) {
+ showHideOptionsPanel();
+ }
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ private void showHideOptionsPanel() {
+ boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE);
+
+ ObjectAnimator anim;
+ if (optionsVisible) {
+ anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f);
+ anim.addListener(collapseListener);
+
+ } else {
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f);
+ //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f);
+
+ }
+
+ //anim.setInterpolator(new AccelerateInterpolator(1.0f));
+ //anim.setDuration(300);
+ //mOptionsLayout.startAnimation(anim);
+ anim.start();
+
+ }
+
+ AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mOptionsLayout.setVisibility(View.GONE);
+ }
+
+ };
+
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.f_log, menu);
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible))
+ menu.removeItem(R.id.toggle_time);
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Intent intent = new Intent(getActivity(), OpenVPNService.class);
+ intent.setAction(OpenVPNService.START_SERVICE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ VpnStatus.addStateListener(this);
+ VpnStatus.addByteCountListener(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ VpnStatus.removeStateListener(this);
+ VpnStatus.removeByteCountListener(this);
+
+ getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat)
+ .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply();
+
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ ListView lv = getListView();
+
+ lv.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ ClipboardManager clipboard = (ClipboardManager)
+ getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Log Entry", ((TextView) view).getText());
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ });
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.f_log, container, false);
+
+ setHasOptionsMenu(true);
+
+ ladapter = new LogWindowListAdapter();
+ ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1);
+ int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1);
+ ladapter.setLogLevel(logLevel);
+
+ setListAdapter(ladapter);
+
+ mTimeRadioGroup = v.findViewById(R.id.timeFormatRadioGroup);
+ mTimeRadioGroup.setOnCheckedChangeListener(this);
+
+ if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) {
+ mTimeRadioGroup.check(R.id.radioISO);
+ } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) {
+ mTimeRadioGroup.check(R.id.radioNone);
+ } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) {
+ mTimeRadioGroup.check(R.id.radioShort);
+ }
+
+ mClearLogCheckBox = v.findViewById(R.id.clearlogconnect);
+ mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(Constants.CLEARLOG, true));
+ mClearLogCheckBox.setOnCheckedChangeListener((buttonView, isChecked) ->
+ Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(Constants.CLEARLOG, isChecked).apply());
+
+ mSpeedView = v.findViewById(R.id.speed);
+
+ mOptionsLayout = v.findViewById(R.id.logOptionsLayout);
+ mLogLevelSlider = v.findViewById(R.id.LogLevelSlider);
+ mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1);
+ mLogLevelSlider.setProgress(logLevel - 1);
+
+ mLogLevelSlider.setOnSeekBarChangeListener(this);
+
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible))
+ mOptionsLayout.setVisibility(View.VISIBLE);
+
+ mUpStatus = v.findViewById(R.id.speedUp);
+ mDownStatus = v.findViewById(R.id.speedDown);
+ mConnectStatus = v.findViewById(R.id.speedStatus);
+ if (mShowOptionsLayout)
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // Scroll to the end of the list end
+ //getListView().setSelection(getListView().getAdapter().getCount()-1);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) {
+ mShowOptionsLayout = true;
+ if (mOptionsLayout != null)
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ }
+
+
+ @Override
+ public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) {
+ if (isAdded()) {
+ final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity());
+
+ getActivity().runOnUiThread(() -> {
+ if (isAdded()) {
+ if (mSpeedView != null) {
+ mSpeedView.setText(cleanLogMessage);
+ }
+ if (mConnectStatus != null)
+ mConnectStatus.setText(cleanLogMessage);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setConnectedVPN(String uuid) {
+ }
+
+
+ @Override
+ public void onDestroy() {
+ VpnStatus.removeLogListener(ladapter);
+ super.onDestroy();
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
new file mode 100644
index 00000000..4b307f23
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
@@ -0,0 +1,174 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.fragments;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.appcompat.app.AlertDialog;
+
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.eip.EIP;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.R.string.warning_option_try_ovpn;
+import static se.leap.bitmaskclient.R.string.warning_option_try_pt;
+import static se.leap.bitmaskclient.eip.EIP.EIPErrors.UNKNOWN;
+import static se.leap.bitmaskclient.eip.EIP.EIPErrors.valueOf;
+import static se.leap.bitmaskclient.eip.EIP.ERRORS;
+import static se.leap.bitmaskclient.eip.EIP.ERRORID;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports;
+
+/**
+ * Implements an error dialog for the main activity.
+ *
+ * @author fupduck
+ * @author cyberta
+ */
+public class MainActivityErrorDialog extends DialogFragment {
+
+ final public static String TAG = "downloaded_failed_dialog";
+ final private static String KEY_REASON_TO_FAIL = "key reason to fail";
+ final private static String KEY_PROVIDER = "key provider";
+ private String reasonToFail;
+ private EIP.EIPErrors downloadError = UNKNOWN;
+
+ private Provider provider;
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(Provider provider, String reasonToFail) {
+ return newInstance(provider, reasonToFail, UNKNOWN);
+ }
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(Provider provider, String reasonToFail, EIP.EIPErrors error) {
+ MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog();
+ dialogFragment.reasonToFail = reasonToFail;
+ dialogFragment.provider = provider;
+ dialogFragment.downloadError = error;
+ return dialogFragment;
+ }
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(Provider provider, JSONObject errorJson) {
+ MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog();
+ dialogFragment.provider = provider;
+ try {
+ if (errorJson.has(ERRORS)) {
+ dialogFragment.reasonToFail = errorJson.getString(ERRORS);
+ } else {
+ //default error msg
+ dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message);
+ }
+
+ if (errorJson.has(ERRORID)) {
+ dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message);
+ }
+ return dialogFragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ restoreFromSavedInstance(savedInstanceState);
+ }
+
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ Context applicationContext = getContext().getApplicationContext();
+ builder.setMessage(reasonToFail)
+ .setNegativeButton(R.string.cancel, (dialog, id) -> {
+ });
+ switch (downloadError) {
+ case ERROR_INVALID_VPN_CERTIFICATE:
+ builder.setPositiveButton(R.string.update_certificate, (dialog, which) ->
+ ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider));
+ break;
+ case NO_MORE_GATEWAYS:
+ if (provider.supportsPluggableTransports()) {
+ if (getUsePluggableTransports(applicationContext)) {
+ builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> {
+ usePluggableTransports(applicationContext, false);
+ EipCommand.startVPN(applicationContext, false);
+ }));
+ } else {
+ builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> {
+ usePluggableTransports(applicationContext, true);
+ EipCommand.startVPN(applicationContext, false);
+ }));
+ }
+ } else {
+ builder.setPositiveButton(R.string.retry, (dialog, which) -> {
+ EipCommand.startVPN(applicationContext, false);
+ });
+ }
+ break;
+ case ERROR_VPN_PREPARE:
+ builder.setPositiveButton(R.string.retry, (dialog, which) -> {
+ EipCommand.startVPN(applicationContext, false);
+ });
+ break;
+ default:
+ break;
+ }
+
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(KEY_REASON_TO_FAIL, reasonToFail);
+ outState.putParcelable(KEY_PROVIDER, provider);
+ }
+
+ private void restoreFromSavedInstance(Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+ if (savedInstanceState.containsKey(KEY_PROVIDER)) {
+ this.provider = savedInstanceState.getParcelable(KEY_PROVIDER);
+ }
+ if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) {
+ this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL);
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java
new file mode 100644
index 00000000..8593e25c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java
@@ -0,0 +1,258 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.Settings;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Observable;
+import java.util.Observer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.firewall.FirewallManager;
+import se.leap.bitmaskclient.tethering.TetheringObservable;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.base.views.IconCheckboxEntry;
+
+/**
+ * Copyright (c) 2020 LEAP Encryption Access Project and contributers
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+public class TetheringDialog extends AppCompatDialogFragment implements Observer {
+
+ public final static String TAG = TetheringDialog.class.getName();
+
+ @InjectView(R.id.tvTitle)
+ AppCompatTextView title;
+
+ @InjectView(R.id.user_message)
+ AppCompatTextView userMessage;
+
+ @InjectView(R.id.selection_list_view)
+ RecyclerView selectionListView;
+ DialogListAdapter adapter;
+ private DialogListAdapter.ViewModel[] dataset;
+
+ public static class DialogListAdapter extends RecyclerView.Adapter<DialogListAdapter.ViewHolder> {
+
+ interface OnItemClickListener {
+ void onItemClick(ViewModel item);
+ }
+
+ private ViewModel[] dataSet;
+ private OnItemClickListener clickListener;
+
+ DialogListAdapter(ViewModel[] dataSet, OnItemClickListener clickListener) {
+ this.dataSet = dataSet;
+ this.clickListener = clickListener;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ IconCheckboxEntry v = new IconCheckboxEntry(viewGroup.getContext());
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
+ viewHolder.bind(dataSet[i], clickListener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return dataSet.length;
+ }
+
+ public static class ViewModel {
+
+ public Drawable image;
+ public String text;
+ public boolean checked;
+ public boolean enabled;
+
+ ViewModel(Drawable image, String text, boolean checked, boolean enabled) {
+ this.image = image;
+ this.text = text;
+ this.checked = checked;
+ this.enabled = enabled;
+ }
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ ViewHolder(IconCheckboxEntry v) {
+ super(v);
+ }
+
+ public void bind(ViewModel model, OnItemClickListener onClickListener) {
+ ((IconCheckboxEntry) this.itemView).bind(model);
+ this.itemView.setOnClickListener(v -> {
+ model.checked = !model.checked;
+ ((IconCheckboxEntry) itemView).setChecked(model.checked);
+ onClickListener.onItemClick(model);
+ });
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.d_list_selection, null);
+ ButterKnife.inject(this, view);
+
+ title.setText(R.string.tethering);
+ userMessage.setMovementMethod(LinkMovementMethod.getInstance());
+ userMessage.setLinkTextColor(getContext().getResources().getColor(R.color.colorPrimary));
+ userMessage.setText(createUserMessage());
+
+ initDataset();
+ adapter = new DialogListAdapter(dataset, this::onItemClick);
+ selectionListView.setAdapter(adapter);
+ selectionListView.setLayoutManager(new LinearLayoutManager(getActivity()));
+
+
+ builder.setView(view)
+ .setPositiveButton(android.R.string.ok, (dialog, id) -> {
+ PreferenceHelper.allowWifiTethering(getContext(), dataset[0].checked);
+ PreferenceHelper.allowUsbTethering(getContext(), dataset[1].checked);
+ PreferenceHelper.allowBluetoothTethering(getContext(), dataset[2].checked);
+ TetheringObservable.allowVpnWifiTethering(dataset[0].checked);
+ TetheringObservable.allowVpnUsbTethering(dataset[1].checked);
+ TetheringObservable.allowVpnBluetoothTethering(dataset[2].checked);
+ FirewallManager firewallManager = new FirewallManager(getContext().getApplicationContext(), false);
+ if (VpnStatus.isVPNActive()) {
+ if (TetheringObservable.getInstance().getTetheringState().hasAnyDeviceTetheringEnabled() &&
+ TetheringObservable.getInstance().getTetheringState().hasAnyVpnTetheringAllowed()) {
+ firewallManager.startTethering();
+ } else {
+ firewallManager.stopTethering();
+ }
+ }
+ }).setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel());
+ return builder.create();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ dataset[0].enabled = TetheringObservable.getInstance().isWifiTetheringEnabled();
+ dataset[1].enabled = TetheringObservable.getInstance().isUsbTetheringEnabled();
+ dataset[2].enabled = TetheringObservable.getInstance().isBluetoothTetheringEnabled();
+ adapter.notifyDataSetChanged();
+ TetheringObservable.getInstance().addObserver(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ TetheringObservable.getInstance().deleteObserver(this);
+ }
+
+ public void onItemClick(DialogListAdapter.ViewModel item) {
+
+ }
+
+ private CharSequence createUserMessage() {
+ String tetheringMessage = getString(R.string.tethering_message);
+ String systemSettingsMessage = getString(R.string.tethering_enabled_message);
+ Pattern pattern = Pattern.compile("([\\w .]*)(<b>)+([\\w ]*)(</b>)([\\w .]*)");
+ Matcher matcher = pattern.matcher(systemSettingsMessage);
+ int startIndex = 0;
+ int endIndex = 0;
+ if (matcher.matches()) {
+ startIndex = matcher.start(2);
+ endIndex = startIndex + matcher.group(3).length();
+ }
+ systemSettingsMessage = systemSettingsMessage.replace("<b>", "").replace("</b>", "");
+ String wholeMessage = systemSettingsMessage + "\n\n" + tetheringMessage;
+ Spannable spannable = new SpannableString(wholeMessage);
+ spannable.setSpan(new ClickableSpan() {
+ @Override
+ public void onClick(@NonNull View widget) {
+ try {
+ final Intent intent = new Intent(Intent.ACTION_MAIN, null);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ final ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.TetherSettings");
+ intent.setComponent(cn);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
+ startActivity(intent);
+ }
+
+ }
+ }, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ return spannable;
+ }
+
+ private void initDataset() {
+ dataset = new DialogListAdapter.ViewModel[] {
+ new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_wifi),
+ getContext().getString(R.string.tethering_wifi),
+ PreferenceHelper.isWifiTetheringAllowed(getContext()),
+ TetheringObservable.getInstance().isWifiTetheringEnabled()),
+ new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_usb),
+ getContext().getString(R.string.tethering_usb),
+ PreferenceHelper.isUsbTetheringAllowed(getContext()),
+ TetheringObservable.getInstance().isUsbTetheringEnabled()),
+ new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_bluetooth),
+ getContext().getString(R.string.tethering_bluetooth),
+ PreferenceHelper.isBluetoothTetheringAllowed(getContext()),
+ TetheringObservable.getInstance().isUsbTetheringEnabled())
+ };
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o instanceof TetheringObservable) {
+ TetheringObservable observable = (TetheringObservable) o;
+ Log.d(TAG, "TetheringObservable is updated");
+ dataset[0].enabled = observable.isWifiTetheringEnabled();
+ dataset[1].enabled = observable.isUsbTetheringEnabled();
+ dataset[2].enabled = observable.isBluetoothTetheringEnabled();
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
new file mode 100644
index 00000000..d649aaf5
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) 2020 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.models;
+
+import android.text.TextUtils;
+
+import se.leap.bitmaskclient.BuildConfig;
+
+public interface Constants {
+
+ //////////////////////////////////////////////
+ // PREFERENCES CONSTANTS
+ /////////////////////////////////////////////
+
+ String SHARED_PREFERENCES = "LEAPPreferences";
+ String PREFERENCES_APP_VERSION = "bitmask version";
+ String ALWAYS_ON_SHOW_DIALOG = "DIALOG.ALWAYS_ON_SHOW_DIALOG";
+ String CLEARLOG = "clearlogconnect";
+ String LAST_USED_PROFILE = "last_used_profile";
+ String EXCLUDED_APPS = "excluded_apps";
+ String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports";
+ String ALLOW_TETHERING_BLUETOOTH = "tethering_bluetooth";
+ String ALLOW_TETHERING_WIFI = "tethering_wifi";
+ String ALLOW_TETHERING_USB = "tethering_usb";
+ String SHOW_EXPERIMENTAL = "show_experimental";
+ String USE_IPv6_FIREWALL = "use_ipv6_firewall";
+ String RESTART_ON_UPDATE = "restart_on_update";
+ String LAST_UPDATE_CHECK = "last_update_check";
+
+
+ //////////////////////////////////////////////
+ // REQUEST CODE CONSTANTS
+ /////////////////////////////////////////////
+
+ String REQUEST_CODE_KEY = "request_code";
+ int REQUEST_CODE_CONFIGURE_LEAP = 0;
+ int REQUEST_CODE_SWITCH_PROVIDER = 1;
+ int REQUEST_CODE_LOG_IN = 2;
+ int REQUEST_CODE_ADD_PROVIDER = 3;
+ int REQUEST_CODE_REQUEST_UPDATE = 4;
+
+
+ //////////////////////////////////////////////
+ // APP CONSTANTS
+ /////////////////////////////////////////////
+
+ String APP_ACTION_QUIT = "quit";
+ String APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE = "configure always-on profile";
+ String DEFAULT_BITMASK = "normal";
+ String CUSTOM_BITMASK = "custom";
+ String DANGER_ON = "danger_on";
+
+
+ String ASK_TO_CANCEL_VPN = "ask_to_cancel_vpn";
+
+
+ //////////////////////////////////////////////
+ // EIP CONSTANTS
+ /////////////////////////////////////////////
+
+ String EIP_ACTION_CHECK_CERT_VALIDITY = "EIP.CHECK_CERT_VALIDITY";
+ String EIP_ACTION_START = "se.leap.bitmaskclient.EIP.START";
+ String EIP_ACTION_STOP = "se.leap.bitmaskclient.EIP.STOP";
+ String EIP_ACTION_IS_RUNNING = "se.leap.bitmaskclient.EIP.IS_RUNNING";
+ String EIP_ACTION_START_ALWAYS_ON_VPN = "se.leap.bitmaskclient.START_ALWAYS_ON_VPN";
+ String EIP_ACTION_START_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_START_BLOCKING_VPN";
+ String EIP_ACTION_STOP_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_STOP_BLOCKING_VPN";
+ String EIP_ACTION_PREPARE_VPN = "se.leap.bitmaskclient.EIP_ACTION_PREPARE_VPN";
+ String EIP_ACTION_CONFIGURE_TETHERING = "se.leap.bitmaskclient.EIP_ACTION_CONFIGURE_TETHERING";
+
+ String EIP_RECEIVER = "EIP.RECEIVER";
+ String EIP_REQUEST = "EIP.REQUEST";
+ String EIP_RESTART_ON_BOOT = "EIP.RESTART_ON_BOOT";
+ String EIP_IS_ALWAYS_ON = "EIP.EIP_IS_ALWAYS_ON";
+ String EIP_EARLY_ROUTES = "EIP.EARLY_ROUTES";
+ String EIP_N_CLOSEST_GATEWAY = "EIP.N_CLOSEST_GATEWAY";
+
+
+ //////////////////////////////////////////////
+ // PROVIDER CONSTANTS
+ /////////////////////////////////////////////
+
+ String PROVIDER_ALLOW_ANONYMOUS = "allow_anonymous";
+ String PROVIDER_ALLOWED_REGISTERED = "allow_registration";
+ String PROVIDER_VPN_CERTIFICATE = "cert";
+ String PROVIDER_PRIVATE_KEY = "Constants.PROVIDER_PRIVATE_KEY";
+ String PROVIDER_KEY = "Constants.PROVIDER_KEY";
+ String PROVIDER_CONFIGURED = "Constants.PROVIDER_CONFIGURED";
+ String PROVIDER_EIP_DEFINITION = "Constants.EIP_DEFINITION";
+ String PROVIDER_PROFILE_UUID = "Constants.PROVIDER_PROFILE_UUID";
+ String PROVIDER_PROFILE = "Constants.PROVIDER_PROFILE";
+
+ //////////////////////////////////////////////
+ // CREDENTIAL CONSTANTS
+ /////////////////////////////////////////////
+
+ String CREDENTIALS_USERNAME = "username";
+ String CREDENTIALS_PASSWORD = "password";
+
+ enum CREDENTIAL_ERRORS {
+ USERNAME_MISSING,
+ PASSWORD_INVALID_LENGTH,
+ RISEUP_WARNING
+ }
+
+ //////////////////////////////////////////////
+ // BROADCAST CONSTANTS
+ /////////////////////////////////////////////
+
+ String BROADCAST_EIP_EVENT = "BROADCAST.EIP_EVENT";
+ String BROADCAST_PROVIDER_API_EVENT = "BROADCAST.PROVIDER_API_EVENT";
+ String BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT = "BROADCAST.GATEWAY_SETUP_WATCHER_EVENT";
+ String BROADCAST_RESULT_CODE = "BROADCAST.RESULT_CODE";
+ String BROADCAST_RESULT_KEY = "BROADCAST.RESULT_KEY";
+ String BROADCAST_DOWNLOAD_SERVICE_EVENT = "BROADCAST.DOWNLOAD_SERVICE_EVENT";
+
+
+ //////////////////////////////////////////////
+ // ICS-OPENVPN CONSTANTS
+ /////////////////////////////////////////////
+ String DEFAULT_SHARED_PREFS_BATTERY_SAVER = "screenoff";
+
+ //////////////////////////////////////////////
+ // CUSTOM CONSTANTS
+ /////////////////////////////////////////////
+ boolean ENABLE_DONATION = BuildConfig.enable_donation;
+ boolean ENABLE_DONATION_REMINDER = BuildConfig.enable_donation_reminder;
+ int DONATION_REMINDER_DURATION = BuildConfig.donation_reminder_duration;
+ String DONATION_URL = TextUtils.isEmpty(BuildConfig.donation_url) ?
+ BuildConfig.default_donation_url : BuildConfig.donation_url;
+ String LAST_DONATION_REMINDER_DATE = "last_donation_reminder_date";
+ String FIRST_TIME_USER_DATE = "first_time_user_date";
+
+
+ //////////////////////////////////////////////
+ // JSON KEYS
+ /////////////////////////////////////////////
+ String IP_ADDRESS = "ip_address";
+ String REMOTE = "remote";
+ String PORTS = "ports";
+ String PROTOCOLS = "protocols";
+ String CAPABILITIES = "capabilities";
+ String TRANSPORT = "transport";
+ String TYPE = "type";
+ String OPTIONS = "options";
+ String VERSION = "version";
+ String NAME = "name";
+ String TIMEZONE = "timezone";
+ String LOCATIONS = "locations";
+ String LOCATION = "location";
+ String OPENVPN_CONFIGURATION = "openvpn_configuration";
+ String GATEWAYS = "gateways";
+ String HOST = "host";
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java
new file mode 100644
index 00000000..4bb7e4ee
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java
@@ -0,0 +1,48 @@
+package se.leap.bitmaskclient.base.models;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class DefaultedURL {
+ private URL DEFAULT_URL;
+ private String default_url = "https://example.net";
+
+ private URL url;
+
+ DefaultedURL() {
+ try {
+ DEFAULT_URL = new URL(default_url);
+ url = DEFAULT_URL;
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public boolean isDefault() { return url.equals(DEFAULT_URL); }
+
+ public void setUrl(URL url) {
+ this.url = url;
+ }
+
+ public String getDomain() {
+ return url.getHost();
+ }
+
+ public URL getUrl() {
+ return url;
+ }
+
+ @Override
+ public String toString() {
+ return url.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof DefaultedURL) {
+ return url.equals(((DefaultedURL) o).getUrl());
+ }
+ return false;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
new file mode 100644
index 00000000..7b3f1888
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
@@ -0,0 +1,6 @@
+package se.leap.bitmaskclient.base.models;
+
+public interface FeatureVersionCode {
+ int RENAMED_EIP_IN_PREFERENCES = 132;
+ int GEOIP_SERVICE = 148;
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
new file mode 100644
index 00000000..97f1019b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
@@ -0,0 +1,593 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.models;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.gson.Gson;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGISTERED;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
+import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
+import static se.leap.bitmaskclient.base.models.Constants.TYPE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+
+/**
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ * @author Parménides GV <parmegv@sdf.org>
+ */
+public final class Provider implements Parcelable {
+
+ private static long EIP_SERVICE_TIMEOUT = 1000 * 60 * 60 * 24 * 3;
+ private static long GEOIP_SERVICE_TIMEOUT = 1000 * 60 * 60;
+ private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json
+ private JSONObject eipServiceJson = new JSONObject();
+ private JSONObject geoIpJson = new JSONObject();
+ private DefaultedURL mainUrl = new DefaultedURL();
+ private DefaultedURL apiUrl = new DefaultedURL();
+ private DefaultedURL geoipUrl = new DefaultedURL();
+ private String providerIp = "";
+ private String providerApiIp = "";
+ private String certificatePin = "";
+ private String certificatePinEncoding = "";
+ private String caCert = "";
+ private String apiVersion = "";
+ private String privateKey = "";
+ private String vpnCertificate = "";
+ private long lastEipServiceUpdate = 0L;
+ private long lastGeoIpUpdate = 0L;
+
+ private boolean allowAnonymous;
+ private boolean allowRegistered;
+
+ final public static String
+ API_URL = "api_uri",
+ API_VERSION = "api_version",
+ ALLOW_REGISTRATION = "allow_registration",
+ API_RETURN_SERIAL = "serial",
+ SERVICE = "service",
+ KEY = "provider",
+ CA_CERT = "ca_cert",
+ CA_CERT_URI = "ca_cert_uri",
+ CA_CERT_FINGERPRINT = "ca_cert_fingerprint",
+ NAME = "name",
+ DESCRIPTION = "description",
+ DOMAIN = "domain",
+ MAIN_URL = "main_url",
+ PROVIDER_IP = "provider_ip",
+ PROVIDER_API_IP = "provider_api_ip",
+ GEOIP_URL = "geoip_url";
+
+ private static final String API_TERM_NAME = "name";
+
+ public Provider() { }
+
+ public Provider(String mainUrl) {
+ this(mainUrl, null);
+ }
+
+ public Provider(String mainUrl, String geoipUrl) {
+ try {
+ this.mainUrl.setUrl(new URL(mainUrl));
+ } catch (MalformedURLException e) {
+ this.mainUrl = new DefaultedURL();
+ }
+ setGeoipUrl(geoipUrl);
+ }
+
+ public Provider(String mainUrl, String providerIp, String providerApiIp) {
+ this(mainUrl, null, providerIp, providerApiIp);
+ }
+
+ public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp) {
+ try {
+ this.mainUrl.setUrl(new URL(mainUrl));
+ if (providerIp != null) {
+ this.providerIp = providerIp;
+ }
+ if (providerApiIp != null) {
+ this.providerApiIp = providerApiIp;
+ }
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ return;
+ }
+ setGeoipUrl(geoipUrl);
+ }
+
+
+ public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp, String caCert, String definition) {
+ this(mainUrl, geoipUrl, providerIp, providerApiIp);
+ if (caCert != null) {
+ this.caCert = caCert;
+ }
+ if (definition != null) {
+ try {
+ define(new JSONObject(definition));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public static final Parcelable.Creator<Provider> CREATOR
+ = new Parcelable.Creator<Provider>() {
+ public Provider createFromParcel(Parcel in) {
+ return new Provider(in);
+ }
+
+ public Provider[] newArray(int size) {
+ return new Provider[size];
+ }
+ };
+
+ public boolean isConfigured() {
+ return !mainUrl.isDefault() &&
+ !apiUrl.isDefault() &&
+ hasCaCert() &&
+ hasDefinition() &&
+ hasVpnCertificate() &&
+ hasEIP() &&
+ hasPrivateKey();
+ }
+
+ public boolean supportsPluggableTransports() {
+ try {
+ JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS);
+ for (int i = 0; i < gatewayJsons.length(); i++) {
+ JSONArray transports = gatewayJsons.getJSONObject(i).
+ getJSONObject(CAPABILITIES).
+ getJSONArray(TRANSPORT);
+ for (int j = 0; j < transports.length(); j++) {
+ if (OBFS4.toString().equals(transports.getJSONObject(j).getString(TYPE))) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ return false;
+ }
+
+ public String getIpForHostname(String host) {
+ if (host != null) {
+ if (host.equals(mainUrl.getUrl().getHost())) {
+ return providerIp;
+ } else if (host.equals(apiUrl.getUrl().getHost())) {
+ return providerApiIp;
+ }
+ }
+ return "";
+ }
+
+ public String getProviderApiIp() {
+ return this.providerApiIp;
+ }
+
+ public void setProviderApiIp(String providerApiIp) {
+ if (providerApiIp == null) return;
+ this.providerApiIp = providerApiIp;
+ }
+
+ public void setProviderIp(String providerIp) {
+ if (providerIp == null) return;
+ this.providerIp = providerIp;
+ }
+
+ public String getProviderIp() {
+ return this.providerIp;
+ }
+
+ public void setMainUrl(URL url) {
+ mainUrl.setUrl(url);
+ }
+
+ public void setMainUrl(String url) {
+ try {
+ mainUrl.setUrl(new URL(url));
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public boolean define(JSONObject providerJson) {
+ definition = providerJson;
+ return parseDefinition(definition);
+ }
+
+ public JSONObject getDefinition() {
+ return definition;
+ }
+
+ public String getDefinitionString() {
+ return getDefinition().toString();
+ }
+
+ public String getDomain() {
+ return mainUrl.getDomain();
+ }
+
+ public String getMainUrlString() {
+ return getMainUrl().toString();
+ }
+
+ public DefaultedURL getMainUrl() {
+ return mainUrl;
+ }
+
+ protected DefaultedURL getApiUrl() {
+ return apiUrl;
+ }
+
+ public DefaultedURL getGeoipUrl() {
+ return geoipUrl;
+ }
+
+ public void setGeoipUrl(String url) {
+ try {
+ this.geoipUrl.setUrl(new URL(url));
+ } catch (MalformedURLException e) {
+ this.geoipUrl = new DefaultedURL();
+ }
+ }
+
+ public String getApiUrlWithVersion() {
+ return getApiUrlString() + "/" + getApiVersion();
+ }
+
+
+ public String getApiUrlString() {
+ return getApiUrl().toString();
+ }
+
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ public boolean hasCaCert() {
+ return caCert != null && !caCert.isEmpty();
+ }
+
+ public boolean hasDefinition() {
+ return definition != null && definition.length() > 0;
+ }
+
+ public boolean hasGeoIpJson() {
+ return geoIpJson != null && geoIpJson.length() > 0;
+ }
+
+
+ public String getCaCert() {
+ return caCert;
+ }
+
+ public String getName() {
+ // Should we pass the locale in, or query the system here?
+ String lang = Locale.getDefault().getLanguage();
+ String name = "";
+ try {
+ if (definition != null)
+ name = definition.getJSONObject(API_TERM_NAME).getString(lang);
+ else throw new JSONException("Provider not defined");
+ } catch (JSONException e) {
+ try {
+ name = definition.getJSONObject(API_TERM_NAME).getString("en");
+ } catch (JSONException e2) {
+ if (mainUrl != null) {
+ String host = mainUrl.getDomain();
+ name = host.substring(0, host.indexOf("."));
+ }
+ }
+ }
+
+ return name;
+ }
+
+ public String getDescription() {
+ String lang = Locale.getDefault().getLanguage();
+ String desc = null;
+ try {
+ desc = definition.getJSONObject("description").getString(lang);
+ } catch (JSONException e) {
+ // TODO: handle exception!!
+ try {
+ desc = definition.getJSONObject("description").getString(definition.getString("default_language"));
+ } catch (JSONException e2) {
+ // TODO: i can't believe you're doing it again!
+ }
+ }
+
+ return desc;
+ }
+
+ public boolean hasEIP() {
+ return getEipServiceJson() != null && getEipServiceJson().length() > 0
+ && !getEipServiceJson().has(ERRORS);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(getMainUrlString());
+ parcel.writeString(getProviderIp());
+ parcel.writeString(getProviderApiIp());
+ parcel.writeString(getGeoipUrl().toString());
+ parcel.writeString(getDefinitionString());
+ parcel.writeString(getCaCert());
+ parcel.writeString(getEipServiceJsonString());
+ parcel.writeString(getGeoIpJsonString());
+ parcel.writeString(getPrivateKey());
+ parcel.writeString(getVpnCertificate());
+ parcel.writeLong(lastEipServiceUpdate);
+ parcel.writeLong(lastGeoIpUpdate);
+ }
+
+
+ //TODO: write a test for marshalling!
+ private Provider(Parcel in) {
+ try {
+ mainUrl.setUrl(new URL(in.readString()));
+ String tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ providerIp = tmpString;
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ providerApiIp = tmpString;
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ geoipUrl.setUrl(new URL(tmpString));
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ definition = new JSONObject((tmpString));
+ parseDefinition(definition);
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ this.caCert = tmpString;
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ this.setEipServiceJson(new JSONObject(tmpString));
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ this.setGeoIpJson(new JSONObject(tmpString));
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ this.setPrivateKey(tmpString);
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
+ this.setVpnCertificate(tmpString);
+ }
+ this.lastEipServiceUpdate = in.readLong();
+ this.lastGeoIpUpdate = in.readLong();
+ } catch (MalformedURLException | JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Provider) {
+ Provider p = (Provider) o;
+ return p.getDomain().equals(getDomain()) &&
+ definition.toString().equals(p.getDefinition().toString()) &&
+ eipServiceJson.toString().equals(p.getEipServiceJsonString()) &&
+ geoIpJson.toString().equals(p.getGeoIpJsonString()) &&
+ providerIp.equals(p.getProviderIp()) &&
+ providerApiIp.equals(p.getProviderApiIp()) &&
+ apiUrl.equals(p.getApiUrl()) &&
+ geoipUrl.equals(p.getGeoipUrl()) &&
+ certificatePin.equals(p.getCertificatePin()) &&
+ certificatePinEncoding.equals(p.getCertificatePinEncoding()) &&
+ caCert.equals(p.getCaCert()) &&
+ apiVersion.equals(p.getApiVersion()) &&
+ privateKey.equals(p.getPrivateKey()) &&
+ vpnCertificate.equals(p.getVpnCertificate()) &&
+ allowAnonymous == p.allowsAnonymous() &&
+ allowRegistered == p.allowsRegistered();
+ } else return false;
+ }
+
+
+ public JSONObject toJson() {
+ JSONObject json = new JSONObject();
+ try {
+ json.put(Provider.MAIN_URL, mainUrl);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return json;
+ }
+
+ @Override
+ public int hashCode() {
+ return getDomain().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return new Gson().toJson(this);
+ }
+
+ private boolean parseDefinition(JSONObject definition) {
+ try {
+ String pin = definition.getString(CA_CERT_FINGERPRINT);
+ this.certificatePin = pin.split(":")[1].trim();
+ this.certificatePinEncoding = pin.split(":")[0].trim();
+ this.apiUrl.setUrl(new URL(definition.getString(API_URL)));
+ this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS);
+ this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED);
+ this.apiVersion = getDefinition().getString(Provider.API_VERSION);
+ return true;
+ } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
+ return false;
+ }
+ }
+
+ public void setCaCert(String cert) {
+ this.caCert = cert;
+ }
+
+ public boolean allowsAnonymous() {
+ return allowAnonymous;
+ }
+
+ public boolean allowsRegistered() {
+ return allowRegistered;
+ }
+
+ public void setLastEipServiceUpdate(long timestamp) {
+ lastEipServiceUpdate = timestamp;
+ }
+
+ public boolean shouldUpdateEipServiceJson() {
+ return System.currentTimeMillis() - lastEipServiceUpdate >= EIP_SERVICE_TIMEOUT;
+ }
+
+
+ public void setLastGeoIpUpdate(long timestamp) {
+ lastGeoIpUpdate = timestamp;
+ }
+
+ public boolean shouldUpdateGeoIpJson() {
+ return System.currentTimeMillis() - lastGeoIpUpdate >= GEOIP_SERVICE_TIMEOUT;
+ }
+
+
+ public boolean setEipServiceJson(JSONObject eipServiceJson) {
+ if (eipServiceJson.has(ERRORS)) {
+ return false;
+ }
+ this.eipServiceJson = eipServiceJson;
+ return true;
+ }
+
+ public boolean setGeoIpJson(JSONObject geoIpJson) {
+ if (geoIpJson.has(ERRORS)) {
+ return false;
+ }
+ this.geoIpJson = geoIpJson;
+ return true;
+ }
+
+ public JSONObject getEipServiceJson() {
+ return eipServiceJson;
+ }
+
+ public JSONObject getGeoIpJson() {
+ return geoIpJson;
+ }
+
+ public String getGeoIpJsonString() {
+ return geoIpJson.toString();
+ }
+
+ public String getEipServiceJsonString() {
+ return getEipServiceJson().toString();
+ }
+
+ public boolean isDefault() {
+ return getMainUrl().isDefault() &&
+ getApiUrl().isDefault() &&
+ getGeoipUrl().isDefault() &&
+ certificatePin.isEmpty() &&
+ certificatePinEncoding.isEmpty() &&
+ caCert.isEmpty();
+ }
+
+ public String getPrivateKey() {
+ return privateKey;
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ public boolean hasPrivateKey() {
+ return privateKey != null && privateKey.length() > 0;
+ }
+
+ public String getVpnCertificate() {
+ return vpnCertificate;
+ }
+
+ public void setVpnCertificate(String vpnCertificate) {
+ this.vpnCertificate = vpnCertificate;
+ }
+
+ public boolean hasVpnCertificate() {
+ return getVpnCertificate() != null && getVpnCertificate().length() >0 ;
+ }
+
+ public String getCertificatePin() {
+ return certificatePin;
+ }
+
+ public String getCertificatePinEncoding() {
+ return certificatePinEncoding;
+ }
+
+ public String getCaCertFingerprint() {
+ return getCertificatePinEncoding() + ":" + getCertificatePin();
+ }
+
+ /**
+ * resets everything except the main url, the providerIp and the geoip
+ * service url (currently preseeded)
+ */
+ public void reset() {
+ definition = new JSONObject();
+ eipServiceJson = new JSONObject();
+ geoIpJson = new JSONObject();
+ apiUrl = new DefaultedURL();
+ certificatePin = "";
+ certificatePinEncoding = "";
+ caCert = "";
+ apiVersion = "";
+ privateKey = "";
+ vpnCertificate = "";
+ allowRegistered = false;
+ allowAnonymous = false;
+ lastGeoIpUpdate = 0L;
+ lastEipServiceUpdate = 0L;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java b/app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java
new file mode 100644
index 00000000..19555504
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java
@@ -0,0 +1,39 @@
+package se.leap.bitmaskclient.base.models;
+
+import java.util.Observable;
+
+/**
+ * Created by cyberta on 05.12.18.
+ */
+public class ProviderObservable extends Observable {
+ private static ProviderObservable instance;
+ private Provider currentProvider;
+ private Provider providerForDns;
+
+ public static ProviderObservable getInstance() {
+ if (instance == null) {
+ instance = new ProviderObservable();
+ }
+ return instance;
+ }
+
+ public synchronized void updateProvider(Provider provider) {
+ instance.currentProvider = provider;
+ instance.providerForDns = null;
+ instance.setChanged();
+ instance.notifyObservers();
+ }
+
+ public Provider getCurrentProvider() {
+ return instance.currentProvider;
+ }
+
+ public void setProviderForDns(Provider provider) {
+ this.providerForDns = provider;
+ }
+
+ public Provider getProviderForDns() {
+ return instance.providerForDns;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java
new file mode 100644
index 00000000..affceacf
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.base.utils;
+
+import androidx.annotation.WorkerThread;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+public class Cmd {
+
+ private static final String TAG = Cmd.class.getSimpleName();
+
+ @WorkerThread
+ public static int runBlockingCmd(String[] cmds, StringBuilder log) throws Exception {
+ return runCmd(cmds, log, true);
+ }
+
+ @WorkerThread
+ private static int runCmd(String[] cmds, StringBuilder log,
+ boolean waitFor) throws Exception {
+
+ int exitCode = -1;
+ Process proc = Runtime.getRuntime().exec("sh");
+ OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream());
+
+ try {
+ for (String cmd : cmds) {
+ out.write(cmd);
+ out.write("\n");
+ }
+
+ out.flush();
+ out.write("exit\n");
+ out.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ out.close();
+ }
+
+ if (waitFor) {
+ // Consume the "stdout"
+ InputStreamReader reader = new InputStreamReader(proc.getInputStream());
+ readToLogString(reader, log);
+
+ // Consume the "stderr"
+ reader = new InputStreamReader(proc.getErrorStream());
+ readToLogString(reader, log);
+
+ try {
+ exitCode = proc.waitFor();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return exitCode;
+ }
+
+ private static void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException {
+ final char buf[] = new char[10];
+ int read = 0;
+ try {
+ while ((read = reader.read(buf)) != -1) {
+ if (log != null)
+ log.append(buf, 0, read);
+ }
+ } catch (IOException e) {
+ reader.close();
+ throw new IOException(e);
+ }
+ reader.close();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
new file mode 100644
index 00000000..4248072a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.utils;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Looper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.spongycastle.util.encoders.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Calendar;
+
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.providersetup.ProviderAPI;
+import se.leap.bitmaskclient.R;
+
+import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_BITMASK;
+
+/**
+ * Stores constants, and implements auxiliary methods used across all Bitmask Android classes.
+ * Wraps BuildConfigFields for to support easier unit testing
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ */
+public class ConfigHelper {
+ final public static String NG_1024 =
+ "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3";
+ final public static BigInteger G = new BigInteger("2");
+
+ public static boolean checkErroneousDownload(String downloadedString) {
+ try {
+ if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS) || new JSONObject(downloadedString).has(ProviderAPI.BACKEND_ERROR_KEY)) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (NullPointerException | JSONException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Treat the input as the MSB representation of a number,
+ * and lop off leading zero elements. For efficiency, the
+ * input is simply returned if no leading zeroes are found.
+ *
+ * @param in array to be trimmed
+ */
+ public static byte[] trim(byte[] in) {
+ if (in.length == 0 || in[0] != 0)
+ return in;
+
+ int len = in.length;
+ int i = 1;
+ while (in[i] == 0 && i < len)
+ ++i;
+ byte[] ret = new byte[len - i];
+ System.arraycopy(in, i, ret, 0, len - i);
+ return ret;
+ }
+
+ public static X509Certificate parseX509CertificateFromString(String certificateString) {
+ java.security.cert.Certificate certificate = null;
+ CertificateFactory cf;
+ try {
+ cf = CertificateFactory.getInstance("X.509");
+
+ certificateString = certificateString.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim();
+ byte[] cert_bytes = Base64.decode(certificateString);
+ InputStream caInput = new ByteArrayInputStream(cert_bytes);
+ try {
+ certificate = cf.generateCertificate(caInput);
+ System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN());
+ } finally {
+ caInput.close();
+ }
+ } catch (NullPointerException | CertificateException | IOException | IllegalArgumentException e) {
+ return null;
+ }
+ return (X509Certificate) certificate;
+ }
+
+ public static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
+ RSAPrivateKey key;
+ try {
+ KeyFactory kf;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ kf = KeyFactory.getInstance("RSA", "BC");
+ } else {
+ kf = KeyFactory.getInstance("RSA");
+ }
+ rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
+ key = (RSAPrivateKey) kf.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ return null;
+ } catch (NoSuchProviderException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ return key;
+ }
+
+ private static String byteArrayToHex(byte[] input) {
+ int readBytes = input.length;
+ StringBuffer hexData = new StringBuffer();
+ int onebyte;
+ for (int i = 0; i < readBytes; i++) {
+ onebyte = ((0x000000ff & input[i]) | 0xffffff00);
+ hexData.append(Integer.toHexString(onebyte).substring(6));
+ }
+ return hexData.toString();
+ }
+
+ /**
+ * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate
+ *
+ * @param certificate
+ * @param encoding
+ * @return
+ * @throws NoSuchAlgorithmException
+ * @throws CertificateEncodingException
+ */
+ @NonNull
+ public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ {
+ byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded());
+ return byteArrayToHex(byteArray);
+ }
+
+ public static void ensureNotOnMainThread(@NonNull Context context) throws IllegalStateException{
+ Looper looper = Looper.myLooper();
+ if (looper != null && looper == context.getMainLooper()) {
+ throw new IllegalStateException(
+ "calling this from your main thread can lead to deadlock");
+ }
+ }
+
+ public static boolean isDefaultBitmask() {
+ return BuildConfig.FLAVOR_branding.equals(DEFAULT_BITMASK);
+ }
+
+ public static boolean preferAnonymousUsage() {
+ return BuildConfig.priotize_anonymous_usage;
+ }
+
+ public static int getCurrentTimezone() {
+ return Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 3600000;
+ }
+
+ public static String getProviderFormattedString(Resources resources, @StringRes int resourceId) {
+ String appName = resources.getString(R.string.app_name);
+ return resources.getString(resourceId, appName);
+ }
+
+ public static boolean stringEqual(@Nullable String string1, @Nullable String string2) {
+ return (string1 == null && string2 == null) ||
+ (string1 != null && string1.equals(string2));
+ }
+
+ public static String getApkFileName() {
+ try {
+ return BuildConfig.update_apk_url.substring(BuildConfig.update_apk_url.lastIndexOf("/"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getVersionFileName() {
+ try {
+ return BuildConfig.version_file_url.substring(BuildConfig.version_file_url.lastIndexOf("/"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getSignatureFileName() {
+ try {
+ return BuildConfig.signature_url.substring(BuildConfig.signature_url.lastIndexOf("/"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java
new file mode 100644
index 00000000..0476bf12
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java
@@ -0,0 +1,29 @@
+package se.leap.bitmaskclient.base.utils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Contains helper methods related to date manipulation.
+ *
+ * @author Janak
+ */
+public class DateHelper {
+ private static final String DATE_PATTERN = "dd/MM/yyyy";
+ private static final int ONE_DAY = 86400000; //1000*60*60*24
+
+ public static long getDateDiffToCurrentDateInDays(String startDate) throws ParseException {
+ SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US);
+ Date lastDate = sdf.parse(startDate);
+ Date currentDate = new Date();
+ return (currentDate.getTime() - lastDate.getTime()) / ONE_DAY;
+ }
+
+ public static String getCurrentDateString() {
+ SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US);
+ Date lastDate = new Date();
+ return sdf.format(lastDate);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
new file mode 100644
index 00000000..eb1c255c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
@@ -0,0 +1,46 @@
+package se.leap.bitmaskclient.base.utils;
+
+import android.content.Context;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Created by cyberta on 18.03.18.
+ */
+
+public class FileHelper {
+ public static File createFile(File dir, String fileName) {
+ return new File(dir, fileName);
+ }
+
+ public static void persistFile(File file, String content) throws IOException {
+ FileWriter writer = new FileWriter(file);
+ writer.write(content);
+ writer.close();
+ }
+
+ public static String readPublicKey(Context context) {
+ {
+ InputStream inputStream;
+ try {
+ inputStream = context.getAssets().open("public.pgp");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ reader.close();
+ return sb.toString();
+ } catch (IOException errabi) {
+ return null;
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java
new file mode 100644
index 00000000..377617a4
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java
@@ -0,0 +1,102 @@
+package se.leap.bitmaskclient.base.utils;
+
+/*
+ * Copyright (C) 2006-2008 Alfresco Software Limited.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ * As a special exception to the terms and conditions of version 2.0 of
+ * the GPL, you may redistribute this Program in connection with Free/Libre
+ * and Open Source Software ("FLOSS") applications as described in Alfresco's
+ * FLOSS exception. You should have recieved a copy of the text describing
+ * the FLOSS exception, and it is also available here:
+ * http://www.alfresco.com/legal/licensing"
+ */
+
+import java.util.StringTokenizer;
+
+/**
+ * TCP/IP Address Utility Class
+ *
+ * @author gkspencer
+ */
+public class IPAddress {
+
+
+ /**
+ * Convert a TCP/IP address string into a byte array
+ *
+ * @param addr String
+ * @return byte[]
+ */
+ public static byte[] asBytes(String addr) {
+
+ // Convert the TCP/IP address string to an integer value
+ int ipInt = parseNumericAddress(addr);
+ if (ipInt == 0)
+ return null;
+
+ // Convert to bytes
+ byte[] ipByts = new byte[4];
+
+ ipByts[3] = (byte) (ipInt & 0xFF);
+ ipByts[2] = (byte) ((ipInt >> 8) & 0xFF);
+ ipByts[1] = (byte) ((ipInt >> 16) & 0xFF);
+ ipByts[0] = (byte) ((ipInt >> 24) & 0xFF);
+
+ // Return the TCP/IP bytes
+ return ipByts;
+ }
+ /**
+ * Check if the specified address is a valid numeric TCP/IP address and return as an integer value
+ *
+ * @param ipaddr String
+ * @return int
+ */
+ private static int parseNumericAddress(String ipaddr) {
+
+ // Check if the string is valid
+ if (ipaddr == null || ipaddr.length() < 7 || ipaddr.length() > 15)
+ return 0;
+
+ // Check the address string, should be n.n.n.n format
+ StringTokenizer token = new StringTokenizer(ipaddr,".");
+ if (token.countTokens() != 4)
+ return 0;
+
+ int ipInt = 0;
+ while (token.hasMoreTokens()) {
+
+ // Get the current token and convert to an integer value
+ String ipNum = token.nextToken();
+
+ try {
+ // Validate the current address part
+ int ipVal = Integer.valueOf(ipNum).intValue();
+ if (ipVal < 0 || ipVal > 255)
+ return 0;
+
+ // Add to the integer address
+ ipInt = (ipInt << 8) + ipVal;
+ }
+ catch (NumberFormatException ex) {
+ return 0;
+ }
+ }
+
+ // Return the integer address
+ return ipInt;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java
new file mode 100644
index 00000000..77189dff
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java
@@ -0,0 +1,21 @@
+package se.leap.bitmaskclient.base.utils;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+/**
+ * Created by cyberta on 18.03.18.
+ */
+
+public class InputStreamHelper {
+ //allows us to mock FileInputStream
+ public static InputStream getInputStreamFrom(String filePath) throws FileNotFoundException {
+ return new FileInputStream(filePath);
+ }
+
+ public static String loadInputStreamAsString(InputStream is) {
+ java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java
new file mode 100644
index 00000000..b0b28993
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java
@@ -0,0 +1,78 @@
+package se.leap.bitmaskclient.base.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+/**
+ * Created by cyberta on 18.03.18.
+ */
+
+public class KeyStoreHelper {
+ private static KeyStore trustedKeystore;
+
+ /**
+ * Adds a new X509 certificate given its input stream and its provider name
+ *
+ * @param provider used to store the certificate in the keystore
+ * @param inputStream from which X509 certificate must be generated.
+ */
+ public static void addTrustedCertificate(String provider, InputStream inputStream) {
+ CertificateFactory cf;
+ try {
+ cf = CertificateFactory.getInstance("X.509");
+ X509Certificate cert =
+ (X509Certificate) cf.generateCertificate(inputStream);
+ trustedKeystore.setCertificateEntry(provider, cert);
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds a new X509 certificate given in its string from and using its provider name
+ *
+ * @param provider used to store the certificate in the keystore
+ * @param certificate
+ */
+ public static void addTrustedCertificate(String provider, String certificate) {
+
+ try {
+ X509Certificate cert = ConfigHelper.parseX509CertificateFromString(certificate);
+ if (trustedKeystore == null) {
+ trustedKeystore = KeyStore.getInstance("BKS");
+ trustedKeystore.load(null);
+ }
+ trustedKeystore.setCertificateEntry(provider, cert);
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @return class wide keystore
+ */
+ public static KeyStore getKeystore() {
+ return trustedKeystore;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java
new file mode 100644
index 00000000..41b8cf35
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java
@@ -0,0 +1,330 @@
+package se.leap.bitmaskclient.base.utils;
+
+/*
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will Google be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, as long as the origin is not misrepresented.
+ *
+ * Source: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html
+ */
+
+import android.os.*;
+import android.os.Process;
+import android.util.*;
+
+import java.io.*;
+import java.security.*;
+import java.security.Provider;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ * <p/>
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
+ getBuildFingerprintAndDeviceSerial();
+
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
+ private PRNGFixes() {
+ }
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom()
+ throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders =
+ Security.getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class.equals(
+ secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng1.getProvider().getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng2.getProvider().getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through
+ * all requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG",
+ 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG
+ * ({@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException(
+ "Failed to read from " + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(
+ new FileInputStream(URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut =
+ new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
new file mode 100644
index 00000000..d31c7a20
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
@@ -0,0 +1,273 @@
+package se.leap.bitmaskclient.base.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Set;
+
+import de.blinkt.openvpn.VpnProfile;
+import se.leap.bitmaskclient.base.models.Provider;
+
+import static android.content.Context.MODE_PRIVATE;
+import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_BLUETOOTH;
+import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_USB;
+import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_WIFI;
+import static se.leap.bitmaskclient.base.models.Constants.ALWAYS_ON_SHOW_DIALOG;
+import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER;
+import static se.leap.bitmaskclient.base.models.Constants.EXCLUDED_APPS;
+import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK;
+import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.models.Constants.RESTART_ON_UPDATE;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.models.Constants.SHOW_EXPERIMENTAL;
+import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
+import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS;
+
+/**
+ * Created by cyberta on 18.03.18.
+ */
+
+public class PreferenceHelper {
+
+ public static Provider getSavedProviderFromSharedPreferences(@NonNull SharedPreferences preferences) {
+ Provider provider = new Provider();
+ try {
+ provider.setMainUrl(new URL(preferences.getString(Provider.MAIN_URL, "")));
+ provider.setProviderIp(preferences.getString(Provider.PROVIDER_IP, ""));
+ provider.setProviderApiIp(preferences.getString(Provider.PROVIDER_API_IP, ""));
+ provider.setGeoipUrl(preferences.getString(Provider.GEOIP_URL, ""));
+ provider.define(new JSONObject(preferences.getString(Provider.KEY, "")));
+ provider.setCaCert(preferences.getString(Provider.CA_CERT, ""));
+ provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, ""));
+ provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, ""));
+ provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, "")));
+ } catch (MalformedURLException | JSONException e) {
+ e.printStackTrace();
+ }
+
+ return provider;
+ }
+
+ public static String getFromPersistedProvider(String toFetch, String providerDomain, SharedPreferences preferences) {
+ return preferences.getString(toFetch + "." + providerDomain, "");
+ }
+
+ // TODO: replace commit with apply after refactoring EIP
+ //FIXME: don't save private keys in shared preferences! use the keystore
+ public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider) {
+ preferences.edit().putBoolean(PROVIDER_CONFIGURED, true).
+ putString(Provider.PROVIDER_IP, provider.getProviderIp()).
+ putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()).
+ putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()).
+ putString(Provider.MAIN_URL, provider.getMainUrlString()).
+ putString(Provider.KEY, provider.getDefinitionString()).
+ putString(Provider.CA_CERT, provider.getCaCert()).
+ putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()).
+ putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()).
+ putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()).
+ commit();
+
+ String providerDomain = provider.getDomain();
+ preferences.edit().putBoolean(PROVIDER_CONFIGURED, true).
+ putString(Provider.PROVIDER_IP + "." + providerDomain, provider.getProviderIp()).
+ putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()).
+ putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()).
+ putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()).
+ putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()).
+ putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()).
+ putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()).
+ apply();
+ }
+
+ /**
+ * Sets the profile that is connected (to connect if the service restarts)
+ */
+ public static void setLastUsedVpnProfile(Context context, VpnProfile connectedProfile) {
+ SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ SharedPreferences.Editor prefsedit = prefs.edit();
+ prefsedit.putString(LAST_USED_PROFILE, connectedProfile.toJson());
+ prefsedit.apply();
+ }
+
+ /**
+ * Returns the profile that was last connected (to connect if the service restarts)
+ */
+ public static VpnProfile getLastConnectedVpnProfile(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ String lastConnectedProfileJson = preferences.getString(LAST_USED_PROFILE, null);
+ return VpnProfile.fromJson(lastConnectedProfileJson);
+ }
+
+ public static void deleteProviderDetailsFromPreferences(@NonNull SharedPreferences preferences, String providerDomain) {
+ preferences.edit().
+ remove(Provider.KEY + "." + providerDomain).
+ remove(Provider.CA_CERT + "." + providerDomain).
+ remove(Provider.PROVIDER_IP + "." + providerDomain).
+ remove(Provider.PROVIDER_API_IP + "." + providerDomain).
+ remove(Provider.MAIN_URL + "." + providerDomain).
+ remove(Provider.GEOIP_URL + "." + providerDomain).
+ remove(PROVIDER_EIP_DEFINITION + "." + providerDomain).
+ remove(PROVIDER_PRIVATE_KEY + "." + providerDomain).
+ remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain).
+ apply();
+ }
+
+ public static void setLastAppUpdateCheck(Context context) {
+ putLong(context, LAST_UPDATE_CHECK, System.currentTimeMillis());
+ }
+
+ public static long getLastAppUpdateCheck(Context context) {
+ return getLong(context, LAST_UPDATE_CHECK, 0);
+ }
+
+ public static void restartOnUpdate(Context context, boolean isEnabled) {
+ putBoolean(context, RESTART_ON_UPDATE, isEnabled);
+ }
+
+ public static boolean getRestartOnUpdate(Context context) {
+ return getBoolean(context, RESTART_ON_UPDATE, false);
+ }
+
+ public static boolean getUsePluggableTransports(Context context) {
+ return getBoolean(context, USE_PLUGGABLE_TRANSPORTS, false);
+ }
+
+ public static void usePluggableTransports(Context context, boolean isEnabled) {
+ putBoolean(context, USE_PLUGGABLE_TRANSPORTS, isEnabled);
+ }
+
+ public static void saveBattery(Context context, boolean isEnabled) {
+ putBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, isEnabled);
+ }
+
+ public static boolean getSaveBattery(Context context) {
+ return getBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, false);
+ }
+
+ public static void allowUsbTethering(Context context, boolean isEnabled) {
+ putBoolean(context, ALLOW_TETHERING_USB, isEnabled);
+ }
+
+ public static boolean isUsbTetheringAllowed(Context context) {
+ return getBoolean(context, ALLOW_TETHERING_USB, false);
+ }
+
+ public static void allowWifiTethering(Context context, boolean isEnabled) {
+ putBoolean(context, ALLOW_TETHERING_WIFI, isEnabled);
+ }
+
+ public static boolean isWifiTetheringAllowed(Context context) {
+ return getBoolean(context, ALLOW_TETHERING_WIFI, false);
+ }
+
+ public static void allowBluetoothTethering(Context context, boolean isEnabled) {
+ putBoolean(context, ALLOW_TETHERING_BLUETOOTH, isEnabled);
+ }
+
+ public static boolean isBluetoothTetheringAllowed(Context context) {
+ return getBoolean(context, ALLOW_TETHERING_BLUETOOTH, false);
+ }
+
+ public static void setShowExperimentalFeatures(Context context, boolean show) {
+ putBoolean(context, SHOW_EXPERIMENTAL, show);
+ }
+
+ public static boolean showExperimentalFeatures(Context context) {
+ return getBoolean(context, SHOW_EXPERIMENTAL, false);
+ }
+
+ public static void setUseIPv6Firewall(Context context, boolean useFirewall) {
+ putBoolean(context, USE_IPv6_FIREWALL, useFirewall);
+ }
+
+ public static boolean useIpv6Firewall(Context context) {
+ return getBoolean(context, USE_IPv6_FIREWALL, false);
+ }
+
+ public static void saveShowAlwaysOnDialog(Context context, boolean showAlwaysOnDialog) {
+ putBoolean(context, ALWAYS_ON_SHOW_DIALOG, showAlwaysOnDialog);
+ }
+
+ public static boolean getShowAlwaysOnDialog(Context context) {
+ return getBoolean(context, ALWAYS_ON_SHOW_DIALOG, true);
+ }
+
+ public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) {
+ JSONObject result = new JSONObject();
+ try {
+ String eipDefinitionString = preferences.getString(PROVIDER_EIP_DEFINITION, "");
+ if (!eipDefinitionString.isEmpty()) {
+ result = new JSONObject(eipDefinitionString);
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return result;
+ }
+
+ public static void setExcludedApps(Context context, Set<String> apps) {
+ SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ SharedPreferences.Editor prefsedit = prefs.edit();
+ prefsedit.putStringSet(EXCLUDED_APPS, apps);
+ prefsedit.apply();
+ }
+
+ public static Set<String> getExcludedApps(Context context) {
+ if (context == null) {
+ return null;
+ }
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ return preferences.getStringSet(EXCLUDED_APPS, new HashSet<>());
+ }
+
+ public static long getLong(Context context, String key, long defValue) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ return preferences.getLong(key, defValue);
+ }
+
+ public static void putLong(Context context, String key, long value) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ preferences.edit().putLong(key, value).apply();
+ }
+
+ public static String getString(Context context, String key, String defValue) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ return preferences.getString(key, defValue);
+ }
+
+ public static void putString(Context context, String key, String value) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ preferences.edit().putString(key, value).apply();
+ }
+
+ public static Boolean getBoolean(Context context, String key, Boolean defValue) {
+ if (context == null) {
+ return false;
+ }
+
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ return preferences.getBoolean(key, defValue);
+ }
+
+ public static void putBoolean(Context context, String key, Boolean value) {
+ if (context == null) {
+ return;
+ }
+
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ preferences.edit().putBoolean(key, value).apply();
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java
new file mode 100644
index 00000000..23ca40e5
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java
@@ -0,0 +1,17 @@
+package se.leap.bitmaskclient.base.utils;
+
+import android.content.Context;
+
+import androidx.annotation.DimenRes;
+
+/**
+ * Created by cyberta on 29.06.18.
+ */
+
+public class ViewHelper {
+
+ public static int convertDimensionToPx(Context context, @DimenRes int dimension) {
+ return context.getResources().getDimensionPixelSize(dimension);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java
new file mode 100644
index 00000000..fdbd7dbd
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java
@@ -0,0 +1,86 @@
+package se.leap.bitmaskclient.base.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.appcompat.widget.AppCompatImageView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.fragments.TetheringDialog;
+
+
+public class IconCheckboxEntry extends LinearLayout {
+
+ @InjectView(android.R.id.text1)
+ TextView textView;
+
+ @InjectView(R.id.material_icon)
+ AppCompatImageView iconView;
+
+ @InjectView(R.id.checked_icon)
+ AppCompatImageView checkedIcon;
+
+ public IconCheckboxEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconCheckboxEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_icon_select_text_list_item, this, true);
+ ButterKnife.inject(this, rootview);
+
+
+
+ }
+
+ public void bind(TetheringDialog.DialogListAdapter.ViewModel model) {
+ this.setEnabled(model.enabled);
+ textView.setText(model.text);
+ textView.setEnabled(model.enabled);
+
+ Drawable checkIcon = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_check_bold)).mutate();
+ if (model.enabled) {
+ DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorSuccess));
+ } else {
+ DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorDisabled));
+ }
+
+ iconView.setImageDrawable(model.image);
+ checkedIcon.setImageDrawable(checkIcon);
+ setChecked(model.checked);
+ }
+
+ public void setChecked(boolean checked) {
+ checkedIcon.setVisibility(checked ? VISIBLE : GONE);
+ checkedIcon.setContentDescription(checked ? "selected" : "unselected");
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java
new file mode 100644
index 00000000..1160986e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java
@@ -0,0 +1,116 @@
+package se.leap.bitmaskclient.base.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.appcompat.widget.SwitchCompat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import se.leap.bitmaskclient.R;
+
+public class IconSwitchEntry extends LinearLayout {
+
+ private TextView textView;
+ private TextView subtitleView;
+ private AppCompatImageView iconView;
+ private SwitchCompat switchView;
+ private CompoundButton.OnCheckedChangeListener checkedChangeListener;
+
+ public IconSwitchEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconSwitchEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconSwitchEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconSwitchEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_switch_list_item, this, true);
+ textView = rootview.findViewById(android.R.id.text1);
+ subtitleView = rootview.findViewById(R.id.subtitle);
+ iconView = rootview.findViewById(R.id.material_icon);
+ switchView = rootview.findViewById(R.id.option_switch);
+
+ if (attrs != null) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconSwitchEntry);
+
+ String entryText = typedArray.getString(R.styleable.IconTextEntry_text);
+ if (entryText != null) {
+ textView.setText(entryText);
+ }
+
+ String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle);
+ if (subtitle != null) {
+ subtitleView.setText(subtitle);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon);
+ if (drawable != null) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ typedArray.recycle();
+ }
+ }
+
+ public void setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener listener) {
+ checkedChangeListener = listener;
+ switchView.setOnCheckedChangeListener(checkedChangeListener);
+ }
+
+ public void setText(@StringRes int id) {
+ textView.setText(id);
+ }
+
+ public void showSubtitle(boolean show) {
+ subtitleView.setVisibility(show ? VISIBLE : GONE);
+ }
+
+ public void setIcon(@DrawableRes int id) {
+ iconView.setImageResource(id);
+ }
+
+ public void setChecked(boolean isChecked) {
+ switchView.setChecked(isChecked);
+ }
+
+ public void setCheckedQuietly(boolean isChecked) {
+ switchView.setOnCheckedChangeListener(null);
+ switchView.setChecked(isChecked);
+ switchView.setOnCheckedChangeListener(checkedChangeListener);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ switchView.setVisibility(enabled ? VISIBLE : GONE);
+ textView.setTextColor(getResources().getColor(enabled ? android.R.color.black : R.color.colorDisabled));
+ iconView.setImageAlpha(enabled ? 255 : 128);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
new file mode 100644
index 00000000..6b9bd760
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
@@ -0,0 +1,106 @@
+package se.leap.bitmaskclient.base.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import se.leap.bitmaskclient.R;
+
+
+public class IconTextEntry extends LinearLayout {
+
+ private TextView textView;
+ private ImageView iconView;
+ private TextView subtitleView;
+
+ public IconTextEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconTextEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconTextEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconTextEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_icon_text_list_item, this, true);
+ textView = rootview.findViewById(android.R.id.text1);
+ subtitleView = rootview.findViewById(R.id.subtitle);
+ iconView = rootview.findViewById(R.id.material_icon);
+
+ if (attrs != null) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconTextEntry);
+
+ String entryText = typedArray.getString(R.styleable.IconTextEntry_text);
+ if (entryText != null) {
+ textView.setText(entryText);
+ }
+
+ String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle);
+ if (subtitle != null) {
+ subtitleView.setText(subtitle);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon);
+ if (drawable != null) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ typedArray.recycle();
+ }
+
+
+ }
+
+ public void setText(@StringRes int id) {
+ textView.setText(id);
+ }
+
+ public void setSubtitle(String text) {
+ subtitleView.setText(text);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ public void hideSubtitle() {
+ subtitleView.setVisibility(GONE);
+ }
+
+ public void setSubtitleColor(@ColorRes int color) {
+ subtitleView.setTextColor(getContext().getResources().getColor(color));
+ }
+
+ public void setText(CharSequence text) {
+ textView.setText(text);
+ }
+
+ public void setIcon(@DrawableRes int id) {
+ iconView.setImageResource(id);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java
new file mode 100644
index 00000000..1f64e483
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java
@@ -0,0 +1,96 @@
+package se.leap.bitmaskclient.base.views;
+
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import androidx.appcompat.widget.AppCompatTextView;
+import android.text.Spannable;
+import android.text.style.ImageSpan;
+import android.util.AttributeSet;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IconTextView extends AppCompatTextView {
+
+ private int imageResource = 0;
+ /**
+ * Regex pattern that looks for embedded images of the format: [img src=imageName/]
+ */
+ public static final String PATTERN = "\\Q[img src]\\E";
+
+ public IconTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public IconTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public IconTextView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setText(CharSequence text, BufferType type) {
+ final Spannable spannable = getTextWithImages(getContext(), text, getLineHeight(), getCurrentTextColor());
+ super.setText(spannable, BufferType.SPANNABLE);
+ }
+
+ public void setIcon(int imageResource) {
+ this.imageResource = imageResource;
+ }
+
+ private Spannable getTextWithImages(Context context, CharSequence text, int lineHeight, int colour) {
+ final Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
+ addImages(context, spannable, lineHeight, colour);
+ return spannable;
+ }
+
+ private void addImages(Context context, Spannable spannable, int lineHeight, int colour) {
+ final Pattern refImg = Pattern.compile(PATTERN);
+
+ final Matcher matcher = refImg.matcher(spannable);
+ while (matcher.find()) {
+ boolean set = true;
+ for (ImageSpan span : spannable.getSpans(matcher.start(), matcher.end(), ImageSpan.class)) {
+ if (spannable.getSpanStart(span) >= matcher.start()
+ && spannable.getSpanEnd(span) <= matcher.end()) {
+ spannable.removeSpan(span);
+ } else {
+ set = false;
+ break;
+ }
+ }
+ if (set && imageResource != 0) {
+ spannable.setSpan(makeImageSpan(context, imageResource, lineHeight, colour),
+ matcher.start(),
+ matcher.end(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ }
+ }
+ }
+
+ /**
+ * Create an ImageSpan for the given icon drawable. This also sets the image size and colour.
+ * Works best with a white, square icon because of the colouring and resizing.
+ *
+ * @param context The Android Context.
+ * @param drawableResId A drawable resource Id.
+ * @param size The desired size (i.e. width and height) of the image icon in pixels.
+ * Use the lineHeight of the TextView to make the image inline with the
+ * surrounding text.
+ * @param colour The colour (careful: NOT a resource Id) to apply to the image.
+ * @return An ImageSpan, aligned with the bottom of the text.
+ */
+ private ImageSpan makeImageSpan(Context context, int drawableResId, int size, int colour) {
+ final Drawable drawable = context.getResources().getDrawable(drawableResId);
+ drawable.mutate();
+ drawable.setColorFilter(colour, PorterDuff.Mode.MULTIPLY);
+ drawable.setBounds(0, 0, size, size);
+ return new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java b/app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java
new file mode 100644
index 00000000..811a54a2
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java
@@ -0,0 +1,109 @@
+package se.leap.bitmaskclient.base.views;
+
+import android.content.Context;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.appcompat.widget.AppCompatTextView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+import se.leap.bitmaskclient.R;
+
+import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx;
+
+/**
+ * Created by cyberta on 29.06.18.
+ */
+
+public class ProviderHeaderView extends RelativeLayout {
+ private int stdPadding;
+ private int compactPadding;
+ private int stdImageSize;
+ private int compactImageSize;
+
+ AppCompatImageView providerHeaderLogo;
+ AppCompatTextView providerHeaderText;
+
+ public ProviderHeaderView(Context context) {
+ super(context);
+ initLayout(context);
+ }
+
+ public ProviderHeaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context);
+ }
+
+ public ProviderHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context);
+ }
+
+ @RequiresApi(21)
+ public ProviderHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context);
+ }
+
+
+ void initLayout(Context context) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_provider_header, this, true);
+ providerHeaderLogo = rootview.findViewById(R.id.provider_header_logo);
+ providerHeaderText = rootview.findViewById(R.id.provider_header_text);
+
+ stdPadding = convertDimensionToPx(context, R.dimen.stdpadding);
+ compactPadding = convertDimensionToPx(context, R.dimen.compact_padding);
+ stdImageSize = convertDimensionToPx(context, R.dimen.bitmask_logo);
+ compactImageSize = convertDimensionToPx(context, R.dimen.bitmask_logo_compact);
+ }
+
+ public void setTitle(String title) {
+ providerHeaderText.setText(title);
+ }
+
+ public void setTitle(@StringRes int stringRes) {
+ providerHeaderText.setText(stringRes);
+ }
+
+ public void setLogo(@DrawableRes int drawableRes) {
+ providerHeaderLogo.setImageResource(drawableRes);
+ }
+
+ public void showCompactLayout() {
+ LayoutParams logoLayoutParams = (LayoutParams) providerHeaderLogo.getLayoutParams();
+ logoLayoutParams.width = compactImageSize;
+ logoLayoutParams.height = compactImageSize;
+ providerHeaderLogo.setLayoutParams(logoLayoutParams);
+
+ LayoutParams textLayoutParams = (LayoutParams) providerHeaderText.getLayoutParams();
+ textLayoutParams.addRule(RIGHT_OF, R.id.provider_header_logo);
+ textLayoutParams.addRule(BELOW, 0);
+ textLayoutParams.addRule(ALIGN_TOP, R.id.provider_header_logo);
+ textLayoutParams.setMargins(compactPadding, compactPadding, compactPadding, compactPadding);
+
+ providerHeaderText.setLayoutParams(textLayoutParams);
+ providerHeaderText.setMaxLines(2);
+ }
+
+ public void showStandardLayout() {
+ LayoutParams logoLayoutParams = (LayoutParams) providerHeaderLogo.getLayoutParams();
+ logoLayoutParams.width = stdImageSize;
+ logoLayoutParams.height = stdImageSize;
+ providerHeaderLogo.setLayoutParams(logoLayoutParams);
+
+ LayoutParams textLayoutParams = (LayoutParams) providerHeaderText.getLayoutParams();
+ textLayoutParams.addRule(RIGHT_OF, 0);
+ textLayoutParams.addRule(BELOW, R.id.provider_header_logo);
+ textLayoutParams.addRule(ALIGN_TOP, 0);
+ textLayoutParams.setMargins(stdPadding, stdPadding, stdPadding, stdPadding);
+ providerHeaderText.setLayoutParams(textLayoutParams);
+ providerHeaderText.setMaxLines(1);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java b/app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java
new file mode 100644
index 00000000..2f8a4448
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.base.views;
+
+import android.content.Context;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.appcompat.widget.AppCompatImageView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.ProgressBar;
+
+import se.leap.bitmaskclient.R;
+
+/**
+ * Created by cyberta on 12.02.18.
+ */
+
+
+public class VpnStateImage extends ConstraintLayout {
+
+ ProgressBar progressBar;
+ AppCompatImageView stateIcon;
+
+ public VpnStateImage(Context context) {
+ super(context);
+ initLayout(context);
+ }
+
+ public VpnStateImage(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context);
+ }
+
+ public VpnStateImage(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context);
+ }
+
+ void initLayout(Context context) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_main_button, this, true);
+ stateIcon = rootview.findViewById(R.id.vpn_state_key);
+ progressBar = rootview.findViewById(R.id.progressBar);
+ progressBar.setIndeterminate(true);
+ }
+
+ public void showProgress() {
+ progressBar.setVisibility(VISIBLE);
+ }
+
+
+ public void stopProgress(boolean animated) {
+ if (!animated) {
+ progressBar.setVisibility(GONE);
+ return;
+ }
+
+ AlphaAnimation fadeOutAnimation = new AlphaAnimation(1.0f, 0.0f);
+ fadeOutAnimation.setDuration(1000);
+ fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ progressBar.setVisibility(GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ progressBar.startAnimation(fadeOutAnimation);
+ }
+
+ public void setStateIcon(int resource) {
+ stateIcon.setImageResource(resource);
+ }
+
+
+}