summaryrefslogtreecommitdiff
path: root/main/src/ui/java/de
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/ui/java/de')
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java151
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java43
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java847
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java159
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java257
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java37
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java131
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java193
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java244
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java333
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java413
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java216
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java132
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java325
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java190
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java400
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java70
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt262
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java694
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java53
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java128
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt323
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java238
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java227
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java101
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java35
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java137
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java212
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java109
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java63
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java134
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java288
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java636
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java39
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java189
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java164
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java732
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java146
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java79
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java73
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java314
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java207
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java16
43 files changed, 9740 insertions, 0 deletions
diff --git a/main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java b/main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java
new file mode 100644
index 00000000..ce14cc98
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java
@@ -0,0 +1,151 @@
+/*
+ * 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 de.blinkt.openvpn;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+import android.widget.Toast;
+
+import de.blinkt.openvpn.core.ConnectionStatus;
+import de.blinkt.openvpn.core.IOpenVPNServiceInternal;
+import de.blinkt.openvpn.core.OpenVPNService;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.core.VpnStatus;
+
+
+/**
+ * Created by arne on 22.04.16.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class OpenVPNTileService extends TileService implements VpnStatus.StateListener {
+
+ @SuppressLint("Override")
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public void onClick() {
+ super.onClick();
+ final VpnProfile bootProfile = getQSVPN();
+ if (bootProfile == null) {
+ Toast.makeText(this, R.string.novpn_selected, Toast.LENGTH_SHORT).show();
+ } else {
+ if (!isLocked())
+ clickAction(bootProfile);
+ else
+ unlockAndRun(new Runnable() {
+ @Override
+ public void run() {
+ clickAction(bootProfile);
+ }
+ });
+ }
+ }
+
+ private void clickAction(VpnProfile bootProfile) {
+ if (VpnStatus.isVPNActive()) {
+ Intent intent = new Intent(this, OpenVPNService.class);
+ intent.setAction(OpenVPNService.START_SERVICE);
+ bindService(intent, new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder binder) {
+ IOpenVPNServiceInternal service = IOpenVPNServiceInternal.Stub.asInterface(binder);
+
+ if (service != null)
+ try {
+ service.stopVPN(false);
+ } catch (RemoteException e) {
+ VpnStatus.logException(e);
+ }
+
+ unbindService(this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+
+ }
+ }, Context.BIND_AUTO_CREATE);
+ } else
+ launchVPN(bootProfile, this);
+ }
+
+
+ @SuppressLint("Override")
+ @TargetApi(Build.VERSION_CODES.N)
+ void launchVPN(VpnProfile profile, Context context) {
+ Intent startVpnIntent = new Intent(Intent.ACTION_MAIN);
+ startVpnIntent.setClass(context, LaunchVPN.class);
+ startVpnIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString());
+ startVpnIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startVpnIntent.putExtra(LaunchVPN.EXTRA_HIDELOG, true);
+
+ context.startActivity(startVpnIntent);
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public void onTileAdded() {
+ }
+
+ @Override
+ public void onStartListening() {
+ super.onStartListening();
+ VpnStatus.addStateListener(this);
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.N)
+ public VpnProfile getQSVPN() {
+ return ProfileManager.getAlwaysOnVPN(this);
+ }
+
+ @Override
+ public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level) {
+ VpnProfile vpn;
+ Tile t = getQsTile();
+ if (level == ConnectionStatus.LEVEL_AUTH_FAILED || level == ConnectionStatus.LEVEL_NOTCONNECTED) {
+ // No VPN connected, use stadnard VPN
+ vpn = getQSVPN();
+ if (vpn == null) {
+ t.setLabel(getString(R.string.novpn_selected));
+ t.setState(Tile.STATE_UNAVAILABLE);
+ } else {
+ t.setLabel(getString(R.string.qs_connect, vpn.getName()));
+ t.setState(Tile.STATE_INACTIVE);
+ }
+ } else {
+ vpn = ProfileManager.get(getBaseContext(), VpnStatus.getLastConnectedVPNProfile());
+ String name;
+ if (vpn == null)
+ name = "null?!";
+ else
+ name = vpn.getName();
+ t.setLabel(getString(R.string.qs_disconnect, name));
+ t.setState(Tile.STATE_ACTIVE);
+ }
+
+ t.updateTile();
+ }
+
+ @Override
+ public void setConnectedVPN(String uuid) {
+
+ }
+
+ @Override
+ public void onStopListening() {
+ VpnStatus.removeStateListener(this);
+ super.onStopListening();
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java b/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java
new file mode 100644
index 00000000..7258d8d6
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2012-2015 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.activities;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.RestrictionsManager;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.view.Window;
+import de.blinkt.openvpn.api.AppRestrictions;
+
+public class BaseActivity extends Activity {
+ private boolean isAndroidTV() {
+ final UiModeManager uiModeManager = (UiModeManager) getSystemService(Activity.UI_MODE_SERVICE);
+ return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ if (isAndroidTV()) {
+ requestWindowFeature(Window.FEATURE_OPTIONS_PANEL);
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java
new file mode 100644
index 00000000..38b47b5a
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java
@@ -0,0 +1,847 @@
+
+/*
+ * 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 de.blinkt.openvpn.activities;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.OpenableColumns;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Pair;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ConfigParser;
+import de.blinkt.openvpn.core.ConfigParser.ConfigParseError;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.fragments.Utils;
+import de.blinkt.openvpn.views.FileSelectLayout;
+
+import static de.blinkt.openvpn.views.FileSelectLayout.FileSelectCallback;
+
+public class ConfigConverter extends BaseActivity implements FileSelectCallback, View.OnClickListener {
+
+ public static final String IMPORT_PROFILE = "de.blinkt.openvpn.IMPORT_PROFILE";
+ private static final int RESULT_INSTALLPKCS12 = 7;
+ private static final int CHOOSE_FILE_OFFSET = 1000;
+ public static final String VPNPROFILE = "vpnProfile";
+ private static final int PERMISSION_REQUEST_EMBED_FILES = 37231;
+ private static final int PERMISSION_REQUEST_READ_URL = PERMISSION_REQUEST_EMBED_FILES + 1;
+
+ private VpnProfile mResult;
+
+ private transient List<String> mPathsegments;
+
+ private String mAliasName = null;
+
+
+ private Map<Utils.FileType, FileSelectLayout> fileSelectMap = new HashMap<>();
+ private String mEmbeddedPwFile;
+ private Vector<String> mLogEntries = new Vector<>();
+ private Uri mSourceUri;
+ private EditText mProfilename;
+ private AsyncTask<Void, Void, Integer> mImportTask;
+ private LinearLayout mLogLayout;
+ private TextView mProfilenameLabel;
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.fab_save)
+ userActionSaveProfile();
+ if (v.getId() == R.id.permssion_hint && Build.VERSION.SDK_INT == Build.VERSION_CODES.M)
+ doRequestSDCardPermission(PERMISSION_REQUEST_EMBED_FILES);
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private void doRequestSDCardPermission(int requestCode) {
+ requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, requestCode);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ // Permission declined, do nothing
+ if (grantResults.length == 0 || grantResults[0] == PackageManager.PERMISSION_DENIED)
+ return;
+
+ // Reset file select dialogs
+ findViewById(R.id.files_missing_hint).setVisibility(View.GONE);
+ findViewById(R.id.permssion_hint).setVisibility(View.GONE);
+ LinearLayout fileroot = (LinearLayout) findViewById(R.id.config_convert_root);
+ for (int i = 0; i < fileroot.getChildCount(); ) {
+ if (fileroot.getChildAt(i) instanceof FileSelectLayout)
+ fileroot.removeViewAt(i);
+ else
+ i++;
+ }
+
+ if (requestCode == PERMISSION_REQUEST_EMBED_FILES)
+ embedFiles(null);
+
+ else if (requestCode == PERMISSION_REQUEST_READ_URL) {
+ if (mSourceUri != null)
+ doImportUri(mSourceUri);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.cancel) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ } else if (item.getItemId() == R.id.ok) {
+ return userActionSaveProfile();
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ private boolean userActionSaveProfile() {
+ if (mResult == null) {
+ log(R.string.import_config_error);
+ Toast.makeText(this, R.string.import_config_error, Toast.LENGTH_LONG).show();
+ return true;
+ }
+
+ mResult.mName = mProfilename.getText().toString();
+ ProfileManager vpl = ProfileManager.getInstance(this);
+ if (vpl.getProfileByName(mResult.mName) != null) {
+ mProfilename.setError(getString(R.string.duplicate_profile_name));
+ return true;
+ }
+
+ Intent in = installPKCS12();
+
+ if (in != null)
+ startActivityForResult(in, RESULT_INSTALLPKCS12);
+ else
+ saveProfile();
+
+ return true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mResult != null)
+ outState.putSerializable(VPNPROFILE, mResult);
+ outState.putString("mAliasName", mAliasName);
+
+
+ String[] logentries = mLogEntries.toArray(new String[mLogEntries.size()]);
+
+ outState.putStringArray("logentries", logentries);
+
+ int[] fileselects = new int[fileSelectMap.size()];
+ int k = 0;
+ for (Utils.FileType key : fileSelectMap.keySet()) {
+ fileselects[k] = key.getValue();
+ k++;
+ }
+ outState.putIntArray("fileselects", fileselects);
+ outState.putString("pwfile", mEmbeddedPwFile);
+ outState.putParcelable("mSourceUri", mSourceUri);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent result) {
+ if (requestCode == RESULT_INSTALLPKCS12 && resultCode == Activity.RESULT_OK) {
+ showCertDialog();
+ }
+
+ if (resultCode == Activity.RESULT_OK && requestCode >= CHOOSE_FILE_OFFSET) {
+ Utils.FileType type = Utils.FileType.getFileTypeByValue(requestCode - CHOOSE_FILE_OFFSET);
+
+
+ FileSelectLayout fs = fileSelectMap.get(type);
+ fs.parseResponse(result, this);
+
+ String data = fs.getData();
+
+ switch (type) {
+ case USERPW_FILE:
+ mEmbeddedPwFile = data;
+ break;
+ case PKCS12:
+ mResult.mPKCS12Filename = data;
+ break;
+ case TLS_AUTH_FILE:
+ mResult.mTLSAuthFilename = data;
+ break;
+ case CA_CERTIFICATE:
+ mResult.mCaFilename = data;
+ break;
+ case CLIENT_CERTIFICATE:
+ mResult.mClientCertFilename = data;
+ break;
+ case KEYFILE:
+ mResult.mClientKeyFilename = data;
+ break;
+ case CRL_FILE:
+ mResult.mCrlFilename = data;
+ break;
+ default:
+ throw new RuntimeException("Type is wrong somehow?");
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, result);
+ }
+
+ private void saveProfile() {
+ Intent result = new Intent();
+ ProfileManager vpl = ProfileManager.getInstance(this);
+
+ if (!TextUtils.isEmpty(mEmbeddedPwFile))
+ ConfigParser.useEmbbedUserAuth(mResult, mEmbeddedPwFile);
+
+ vpl.addProfile(mResult);
+ vpl.saveProfile(this, mResult);
+ vpl.saveProfileList(this);
+ result.putExtra(VpnProfile.EXTRA_PROFILEUUID, mResult.getUUID().toString());
+ setResult(Activity.RESULT_OK, result);
+ finish();
+ }
+
+ public void showCertDialog() {
+ try {
+ //noinspection WrongConstant
+ KeyChain.choosePrivateKeyAlias(this,
+ new KeyChainAliasCallback() {
+
+ public void alias(String alias) {
+ // Credential alias selected. Remember the alias selection for future use.
+ mResult.mAlias = alias;
+ saveProfile();
+ }
+
+
+ },
+ new String[]{"RSA", "EC"}, // List of acceptable key types. null for any
+ null, // issuer, null for any
+ mResult.mServerName, // host name of server requesting the cert, null if unavailable
+ -1, // port of server requesting the cert, -1 if unavailable
+ mAliasName); // alias to preselect, null if unavailable
+ } catch (ActivityNotFoundException anf) {
+ Builder ab = new AlertDialog.Builder(this);
+ ab.setTitle(R.string.broken_image_cert_title);
+ ab.setMessage(R.string.broken_image_cert);
+ ab.setPositiveButton(android.R.string.ok, null);
+ ab.show();
+ }
+ }
+
+
+ private Intent installPKCS12() {
+
+ if (!((CheckBox) findViewById(R.id.importpkcs12)).isChecked()) {
+ setAuthTypeToEmbeddedPKCS12();
+ return null;
+
+ }
+ String pkcs12datastr = mResult.mPKCS12Filename;
+ if (VpnProfile.isEmbedded(pkcs12datastr)) {
+ Intent inkeyIntent = KeyChain.createInstallIntent();
+
+ pkcs12datastr = VpnProfile.getEmbeddedContent(pkcs12datastr);
+
+
+ byte[] pkcs12data = Base64.decode(pkcs12datastr, Base64.DEFAULT);
+
+
+ inkeyIntent.putExtra(KeyChain.EXTRA_PKCS12, pkcs12data);
+
+ if (mAliasName.equals(""))
+ mAliasName = null;
+
+ if (mAliasName != null) {
+ inkeyIntent.putExtra(KeyChain.EXTRA_NAME, mAliasName);
+ }
+ return inkeyIntent;
+
+ }
+ return null;
+ }
+
+
+ private void setAuthTypeToEmbeddedPKCS12() {
+ if (VpnProfile.isEmbedded(mResult.mPKCS12Filename)) {
+ if (mResult.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE)
+ mResult.mAuthenticationType = VpnProfile.TYPE_USERPASS_PKCS12;
+
+ if (mResult.mAuthenticationType == VpnProfile.TYPE_KEYSTORE)
+ mResult.mAuthenticationType = VpnProfile.TYPE_PKCS12;
+
+ }
+ }
+
+
+ private String getUniqueProfileName(String possibleName) {
+
+ int i = 0;
+
+ ProfileManager vpl = ProfileManager.getInstance(this);
+
+ String newname = possibleName;
+
+ // Default to
+ if (mResult.mName != null && !ConfigParser.CONVERTED_PROFILE.equals(mResult.mName))
+ newname = mResult.mName;
+
+ while (newname == null || vpl.getProfileByName(newname) != null) {
+ i++;
+ if (i == 1)
+ newname = getString(R.string.converted_profile);
+ else
+ newname = getString(R.string.converted_profile_i, i);
+ }
+
+ return newname;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.import_menu, menu);
+ return true;
+ }
+
+ private String embedFile(String filename, Utils.FileType type, boolean onlyFindFileAndNullonNotFound) {
+ if (filename == null)
+ return null;
+
+ // Already embedded, nothing to do
+ if (VpnProfile.isEmbedded(filename))
+ return filename;
+
+ File possibleFile = findFile(filename, type);
+ if (possibleFile == null)
+ if (onlyFindFileAndNullonNotFound)
+ return null;
+ else
+ return filename;
+ else if (onlyFindFileAndNullonNotFound)
+ return possibleFile.getAbsolutePath();
+ else
+ return readFileContent(possibleFile, type == Utils.FileType.PKCS12);
+
+ }
+
+
+ private Pair<Integer, String> getFileDialogInfo(Utils.FileType type) {
+ int titleRes = 0;
+ String value = null;
+ switch (type) {
+ case KEYFILE:
+ titleRes = R.string.client_key_title;
+ if (mResult != null)
+ value = mResult.mClientKeyFilename;
+ break;
+ case CLIENT_CERTIFICATE:
+ titleRes = R.string.client_certificate_title;
+ if (mResult != null)
+ value = mResult.mClientCertFilename;
+ break;
+ case CA_CERTIFICATE:
+ titleRes = R.string.ca_title;
+ if (mResult != null)
+ value = mResult.mCaFilename;
+ break;
+ case TLS_AUTH_FILE:
+ titleRes = R.string.tls_auth_file;
+ if (mResult != null)
+ value = mResult.mTLSAuthFilename;
+ break;
+ case PKCS12:
+ titleRes = R.string.client_pkcs12_title;
+ if (mResult != null)
+ value = mResult.mPKCS12Filename;
+ break;
+
+ case USERPW_FILE:
+ titleRes = R.string.userpw_file;
+ value = mEmbeddedPwFile;
+ break;
+
+ case CRL_FILE:
+ titleRes = R.string.crl_file;
+ value = mResult.mCrlFilename;
+ break;
+ }
+
+ return Pair.create(titleRes, value);
+
+ }
+
+ private File findFile(String filename, Utils.FileType fileType) {
+ File foundfile = findFileRaw(filename);
+
+ if (foundfile == null && filename != null && !filename.equals("")) {
+ log(R.string.import_could_not_open, filename);
+ }
+ fileSelectMap.put(fileType, null);
+
+ return foundfile;
+ }
+
+ private void addMissingFileDialogs()
+ {
+ for (Map.Entry<Utils.FileType, FileSelectLayout> item: fileSelectMap.entrySet()) {
+ if (item.getValue()==null)
+ addFileSelectDialog(item.getKey());
+ }
+ }
+
+ private void addFileSelectDialog(Utils.FileType type) {
+
+ Pair<Integer, String> fileDialogInfo = getFileDialogInfo(type);
+
+ boolean isCert = type == Utils.FileType.CA_CERTIFICATE || type == Utils.FileType.CLIENT_CERTIFICATE;
+ FileSelectLayout fl = new FileSelectLayout(this, getString(fileDialogInfo.first), isCert, false);
+ fileSelectMap.put(type, fl);
+ fl.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ ((LinearLayout) findViewById(R.id.config_convert_root)).addView(fl, 2);
+ findViewById(R.id.files_missing_hint).setVisibility(View.VISIBLE);
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M)
+ checkPermission();
+
+ fl.setData(fileDialogInfo.second, this);
+ int i = getFileLayoutOffset(type);
+ fl.setCaller(this, i, type);
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private void checkPermission() {
+ if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ findViewById(R.id.permssion_hint).setVisibility(View.VISIBLE);
+ findViewById(R.id.permssion_hint).setOnClickListener(this);
+ }
+ }
+
+ private int getFileLayoutOffset(Utils.FileType type) {
+ return CHOOSE_FILE_OFFSET + type.getValue();
+ }
+
+
+ private File findFileRaw(String filename) {
+ if (filename == null || filename.equals(""))
+ return null;
+
+ // Try diffent path relative to /mnt/sdcard
+ File sdcard = Environment.getExternalStorageDirectory();
+ File root = new File("/");
+
+ HashSet<File> dirlist = new HashSet<>();
+
+ for (int i = mPathsegments.size() - 1; i >= 0; i--) {
+ String path = "";
+ for (int j = 0; j <= i; j++) {
+ path += "/" + mPathsegments.get(j);
+ }
+ // Do a little hackish dance for the Android File Importer
+ // /document/primary:ovpn/openvpn-imt.conf
+
+
+ if (path.indexOf(':') != -1 && path.lastIndexOf('/') > path.indexOf(':')) {
+ String possibleDir = path.substring(path.indexOf(':') + 1, path.length());
+ // Unquote chars in the path
+ try {
+ possibleDir = URLDecoder.decode(possibleDir, "UTF-8");
+ } catch (UnsupportedEncodingException ignored) {}
+
+ possibleDir = possibleDir.substring(0, possibleDir.lastIndexOf('/'));
+
+
+
+
+ dirlist.add(new File(sdcard, possibleDir));
+
+ }
+ dirlist.add(new File(path));
+
+
+ }
+ dirlist.add(sdcard);
+ dirlist.add(root);
+
+
+ String[] fileparts = filename.split("/");
+ for (File rootdir : dirlist) {
+ String suffix = "";
+ for (int i = fileparts.length - 1; i >= 0; i--) {
+ if (i == fileparts.length - 1)
+ suffix = fileparts[i];
+ else
+ suffix = fileparts[i] + "/" + suffix;
+
+ File possibleFile = new File(rootdir, suffix);
+ if (possibleFile.canRead())
+ return possibleFile;
+
+ }
+ }
+ return null;
+ }
+
+ String readFileContent(File possibleFile, boolean base64encode) {
+ byte[] filedata;
+ try {
+ filedata = readBytesFromFile(possibleFile);
+ } catch (IOException e) {
+ log(e.getLocalizedMessage());
+ return null;
+ }
+
+ String data;
+ if (base64encode) {
+ data = Base64.encodeToString(filedata, Base64.DEFAULT);
+ } else {
+ data = new String(filedata);
+
+ }
+
+ return VpnProfile.DISPLAYNAME_TAG + possibleFile.getName() + VpnProfile.INLINE_TAG + data;
+
+ }
+
+
+ private byte[] readBytesFromFile(File file) throws IOException {
+ InputStream input = new FileInputStream(file);
+
+ long len = file.length();
+ if (len > VpnProfile.MAX_EMBED_FILE_SIZE)
+ throw new IOException("File size of file to import too large.");
+
+ // Create the byte array to hold the data
+ byte[] bytes = new byte[(int) len];
+
+ // Read in the bytes
+ int offset = 0;
+ int bytesRead;
+ while (offset < bytes.length
+ && (bytesRead = input.read(bytes, offset, bytes.length - offset)) >= 0) {
+ offset += bytesRead;
+ }
+
+ input.close();
+ return bytes;
+ }
+
+ void embedFiles(ConfigParser cp) {
+ // This where I would like to have a c++ style
+ // void embedFile(std::string & option)
+
+ if (mResult.mPKCS12Filename != null) {
+ File pkcs12file = findFileRaw(mResult.mPKCS12Filename);
+ if (pkcs12file != null) {
+ mAliasName = pkcs12file.getName().replace(".p12", "");
+ } else {
+ mAliasName = "Imported PKCS12";
+ }
+ }
+
+
+ mResult.mCaFilename = embedFile(mResult.mCaFilename, Utils.FileType.CA_CERTIFICATE, false);
+ mResult.mClientCertFilename = embedFile(mResult.mClientCertFilename, Utils.FileType.CLIENT_CERTIFICATE, false);
+ mResult.mClientKeyFilename = embedFile(mResult.mClientKeyFilename, Utils.FileType.KEYFILE, false);
+ mResult.mTLSAuthFilename = embedFile(mResult.mTLSAuthFilename, Utils.FileType.TLS_AUTH_FILE, false);
+ mResult.mPKCS12Filename = embedFile(mResult.mPKCS12Filename, Utils.FileType.PKCS12, false);
+ mResult.mCrlFilename = embedFile(mResult.mCrlFilename, Utils.FileType.CRL_FILE, true);
+ if (cp != null) {
+ mEmbeddedPwFile = cp.getAuthUserPassFile();
+ mEmbeddedPwFile = embedFile(cp.getAuthUserPassFile(), Utils.FileType.USERPW_FILE, false);
+ }
+
+ }
+
+ private void updateFileSelectDialogs() {
+ for (Map.Entry<Utils.FileType, FileSelectLayout> fl : fileSelectMap.entrySet()) {
+ fl.getValue().setData(getFileDialogInfo(fl.getKey()).second, this);
+ }
+ }
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.config_converter);
+
+ ImageButton fab_button = (ImageButton) findViewById(R.id.fab_save);
+ if (fab_button != null) {
+ fab_button.setOnClickListener(this);
+ findViewById(R.id.fab_footerspace).setVisibility(View.VISIBLE);
+ }
+
+ mLogLayout = (LinearLayout) findViewById(R.id.config_convert_root);
+
+
+ mProfilename = (EditText) findViewById(R.id.profilename);
+ mProfilenameLabel = (TextView) findViewById(R.id.profilename_label);
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(VPNPROFILE)) {
+ mResult = (VpnProfile) savedInstanceState.getSerializable(VPNPROFILE);
+ mAliasName = savedInstanceState.getString("mAliasName");
+ mEmbeddedPwFile = savedInstanceState.getString("pwfile");
+ mSourceUri = savedInstanceState.getParcelable("mSourceUri");
+ mProfilename.setText(mResult.mName);
+
+ if (savedInstanceState.containsKey("logentries")) {
+ //noinspection ConstantConditions
+ for (String logItem : savedInstanceState.getStringArray("logentries"))
+ log(logItem);
+ }
+ if (savedInstanceState.containsKey("fileselects")) {
+ //noinspection ConstantConditions
+ for (int k : savedInstanceState.getIntArray("fileselects")) {
+ addFileSelectDialog(Utils.FileType.getFileTypeByValue(k));
+ }
+ }
+ return;
+ }
+
+
+ final android.content.Intent intent = getIntent();
+
+ if (intent != null) {
+ doImportIntent(intent);
+
+ // We parsed the intent, relay on saved instance for restoring
+ setIntent(null);
+ }
+
+
+ }
+
+ private void doImportIntent(Intent intent) {
+ final Uri data = intent.getData();
+ if (data != null) {
+ mSourceUri = data;
+ doImportUri(data);
+ }
+ }
+
+ private void doImportUri(Uri data) {
+ //log(R.string.import_experimental);
+ log(R.string.importing_config, data.toString());
+ String possibleName = null;
+ if ((data.getScheme() != null && data.getScheme().equals("file")) ||
+ (data.getLastPathSegment() != null &&
+ (data.getLastPathSegment().endsWith(".ovpn") ||
+ data.getLastPathSegment().endsWith(".conf")))
+ ) {
+ possibleName = data.getLastPathSegment();
+ if (possibleName.lastIndexOf('/') != -1)
+ possibleName = possibleName.substring(possibleName.lastIndexOf('/') + 1);
+
+ }
+
+ mPathsegments = data.getPathSegments();
+
+ Cursor cursor = getContentResolver().query(data, null, null, null, null);
+
+ try {
+
+ if (cursor != null && cursor.moveToFirst()) {
+ int columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+
+ if (columnIndex != -1) {
+ String displayName = cursor.getString(columnIndex);
+ if (displayName != null)
+ possibleName = displayName;
+ }
+ columnIndex = cursor.getColumnIndex("mime_type");
+ if (columnIndex != -1) {
+ log("Mime type: " + cursor.getString(columnIndex));
+ }
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ if (possibleName != null) {
+ possibleName = possibleName.replace(".ovpn", "");
+ possibleName = possibleName.replace(".conf", "");
+ }
+
+ startImportTask(data, possibleName);
+
+
+ }
+
+ private void startImportTask(final Uri data, final String possibleName) {
+ mImportTask = new AsyncTask<Void, Void, Integer>() {
+ private ProgressBar mProgress;
+
+ @Override
+ protected void onPreExecute() {
+ mProgress = new ProgressBar(ConfigConverter.this);
+ addViewToLog(mProgress);
+ }
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ try {
+ InputStream is = getContentResolver().openInputStream(data);
+
+ doImport(is);
+ is.close();
+ if (mResult==null)
+ return -3;
+ } catch (IOException| SecurityException se)
+
+ {
+ log(R.string.import_content_resolve_error + ":" + se.getLocalizedMessage());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ checkMarschmallowFileImportError(data);
+ return -2;
+ }
+
+ return 0;
+ }
+
+ @Override
+ protected void onPostExecute(Integer errorCode) {
+ mLogLayout.removeView(mProgress);
+ addMissingFileDialogs();
+ updateFileSelectDialogs();
+
+ if (errorCode == 0) {
+ displayWarnings();
+ mResult.mName = getUniqueProfileName(possibleName);
+ mProfilename.setVisibility(View.VISIBLE);
+ mProfilenameLabel.setVisibility(View.VISIBLE);
+ mProfilename.setText(mResult.getName());
+
+ log(R.string.import_done);
+ }
+ }
+ }.execute();
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private void checkMarschmallowFileImportError(Uri data) {
+ // Permission already granted, not the source of the error
+ if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
+ return;
+
+ // We got a file:/// URL and have no permission to read it. Technically an error of the calling app since
+ // it makes an assumption about other apps being able to read the url but well ...
+ if (data != null && "file".equals(data.getScheme()))
+ doRequestSDCardPermission(PERMISSION_REQUEST_READ_URL);
+
+ }
+
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ private void log(final String logmessage) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TextView tv = new TextView(ConfigConverter.this);
+ mLogEntries.add(logmessage);
+ tv.setText(logmessage);
+
+ addViewToLog(tv);
+ }
+ });
+ }
+
+ private void addViewToLog(View view) {
+ mLogLayout.addView(view, mLogLayout.getChildCount() - 1);
+ }
+
+ private void doImport(InputStream is) {
+ ConfigParser cp = new ConfigParser();
+ try {
+ InputStreamReader isr = new InputStreamReader(is);
+
+ cp.parseConfig(isr);
+ mResult = cp.convertProfile();
+ embedFiles(cp);
+ return;
+
+ } catch (IOException | ConfigParseError e) {
+ log(R.string.error_reading_config_file);
+ log(e.getLocalizedMessage());
+ }
+ mResult = null;
+
+ }
+
+ private void displayWarnings() {
+ if (mResult.mUseCustomConfig) {
+ log(R.string.import_warning_custom_options);
+ String copt = mResult.mCustomConfigOptions;
+ if (copt.startsWith("#")) {
+ int until = copt.indexOf('\n');
+ copt = copt.substring(until + 1);
+ }
+
+ log(copt);
+ }
+
+ if (mResult.mAuthenticationType == VpnProfile.TYPE_KEYSTORE ||
+ mResult.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
+ findViewById(R.id.importpkcs12).setVisibility(View.VISIBLE);
+ }
+
+ }
+
+ private void log(int ressourceId, Object... formatArgs) {
+ log(getString(ressourceId, formatArgs));
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java b/main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java
new file mode 100644
index 00000000..e1cb8862
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java
@@ -0,0 +1,159 @@
+/*
+ * 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 de.blinkt.openvpn.activities;
+
+import android.app.ListActivity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import de.blinkt.openvpn.LaunchVPN;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+import java.util.Collection;
+import java.util.Vector;
+
+/**
+ * This Activity actually handles two stages of a launcher shortcut's life cycle.
+ *
+ * 1. Your application offers to provide shortcuts to the launcher. When
+ * the user installs a shortcut, an activity within your application
+ * generates the actual shortcut and returns it to the launcher, where it
+ * is shown to the user as an icon.
+ *
+ * 2. Any time the user clicks on an installed shortcut, an intent is sent.
+ * Typically this would then be handled as necessary by an activity within
+ * your application.
+ *
+ * We handle stage 1 (creating a shortcut) by simply sending back the information (in the form
+ * of an {@link android.content.Intent} that the launcher will use to create the shortcut.
+ *
+ * You can also implement this in an interactive way, by having your activity actually present
+ * UI for the user to select the specific nature of the shortcut, such as a contact, picture, URL,
+ * media item, or action.
+ *
+ * We handle stage 2 (responding to a shortcut) in this sample by simply displaying the contents
+ * of the incoming {@link android.content.Intent}.
+ *
+ * In a real application, you would probably use the shortcut intent to display specific content
+ * or start a particular operation.
+ */
+public class CreateShortcuts extends ListActivity implements OnItemClickListener {
+
+
+ private static final int START_VPN_PROFILE= 70;
+
+
+ private ProfileManager mPM;
+ private VpnProfile mSelectedProfile;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPM =ProfileManager.getInstance(this);
+
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // Resolve the intent
+
+ createListView();
+ }
+
+ private void createListView() {
+ ListView lv = getListView();
+ //lv.setTextFilterEnabled(true);
+
+ Collection<VpnProfile> vpnList = mPM.getProfiles();
+
+ Vector<String> vpnNames=new Vector<String>();
+ for (VpnProfile vpnProfile : vpnList) {
+ vpnNames.add(vpnProfile.mName);
+ }
+
+
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,vpnNames);
+ lv.setAdapter(adapter);
+
+ lv.setOnItemClickListener(this);
+ }
+
+ /**
+ * This function creates a shortcut and returns it to the caller. There are actually two
+ * intents that you will send back.
+ *
+ * The first intent serves as a container for the shortcut and is returned to the launcher by
+ * setResult(). This intent must contain three fields:
+ *
+ * <ul>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with
+ * the shortcut.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a
+ * bitmap, <i>or</i> {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as
+ * a drawable resource.</li>
+ * </ul>
+ *
+ * If you use a simple drawable resource, note that you must wrapper it using
+ * {@link android.content.Intent.ShortcutIconResource}, as shown below. This is required so
+ * that the launcher can access resources that are stored in your application's .apk file. If
+ * you return a bitmap, such as a thumbnail, you can simply put the bitmap into the extras
+ * bundle using {@link android.content.Intent#EXTRA_SHORTCUT_ICON}.
+ *
+ * The shortcut intent can be any intent that you wish the launcher to send, when the user
+ * clicks on the shortcut. Typically this will be {@link android.content.Intent#ACTION_VIEW}
+ * with an appropriate Uri for your content, but any Intent will work here as long as it
+ * triggers the desired action within your Activity.
+ * @param profile
+ */
+ private void setupShortcut(VpnProfile profile) {
+ // First, set up the shortcut intent. For this example, we simply create an intent that
+ // will bring us directly back to this activity. A more typical implementation would use a
+ // data Uri in order to display a more specific result, or a custom action in order to
+ // launch a specific operation.
+
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ shortcutIntent.setClass(this, LaunchVPN.class);
+ shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY,profile.getUUID().toString());
+
+ // Then, set up the container intent (the response to the caller)
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, profile.getName());
+ Parcelable iconResource = Intent.ShortcutIconResource.fromContext(
+ this, R.mipmap.ic_launcher);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+
+ // Now, return the result to the launcher
+
+ setResult(RESULT_OK, intent);
+ }
+
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ String profileName = ((TextView) view).getText().toString();
+
+ VpnProfile profile = mPM.getProfileByName(profileName);
+
+ setupShortcut(profile);
+ finish();
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java b/main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java
new file mode 100644
index 00000000..80a134a9
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java
@@ -0,0 +1,257 @@
+/*
+ * 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 de.blinkt.openvpn.activities;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Base64;
+import android.widget.Toast;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.fragments.FileSelectionFragment;
+import de.blinkt.openvpn.fragments.InlineFileTab;
+
+public class FileSelect extends BaseActivity {
+ public static final String RESULT_DATA = "RESULT_PATH";
+ public static final String START_DATA = "START_DATA";
+ public static final String WINDOW_TITLE = "WINDOW_TILE";
+ public static final String NO_INLINE_SELECTION = "de.blinkt.openvpn.NO_INLINE_SELECTION";
+ public static final String SHOW_CLEAR_BUTTON = "de.blinkt.openvpn.SHOW_CLEAR_BUTTON";
+ public static final String DO_BASE64_ENCODE = "de.blinkt.openvpn.BASE64ENCODE";
+ private static final int PERMISSION_REQUEST = 23621;
+
+ private FileSelectionFragment mFSFragment;
+ private InlineFileTab mInlineFragment;
+ private String mData;
+ private Tab inlineFileTab;
+ private Tab fileExplorerTab;
+ private boolean mNoInline;
+ private boolean mShowClear;
+ private boolean mBase64Encode;
+
+
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.file_dialog);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ checkPermission();
+
+ mData = getIntent().getStringExtra(START_DATA);
+ if(mData==null)
+ mData=Environment.getExternalStorageDirectory().getPath();
+
+ String title = getIntent().getStringExtra(WINDOW_TITLE);
+ int titleId = getIntent().getIntExtra(WINDOW_TITLE, 0);
+ if(titleId!=0)
+ title =getString(titleId);
+ if(title!=null)
+ setTitle(title);
+
+ mNoInline = getIntent().getBooleanExtra(NO_INLINE_SELECTION, false);
+ mShowClear = getIntent().getBooleanExtra(SHOW_CLEAR_BUTTON, false);
+ mBase64Encode = getIntent().getBooleanExtra(DO_BASE64_ENCODE, false);
+
+ ActionBar bar = getActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ fileExplorerTab = bar.newTab().setText(R.string.file_explorer_tab);
+ inlineFileTab = bar.newTab().setText(R.string.inline_file_tab);
+
+ mFSFragment = new FileSelectionFragment();
+ fileExplorerTab.setTabListener(new MyTabsListener<FileSelectionFragment>(this, mFSFragment));
+ bar.addTab(fileExplorerTab);
+
+ if(!mNoInline) {
+ mInlineFragment = new InlineFileTab();
+ inlineFileTab.setTabListener(new MyTabsListener<InlineFileTab>(this, mInlineFragment));
+ bar.addTab(inlineFileTab);
+ } else {
+ mFSFragment.setNoInLine();
+ }
+
+
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private void checkPermission() {
+ if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
+ if (mNoInline) {
+ setResult(RESULT_CANCELED);
+ finish();
+ } else {
+ if (fileExplorerTab!=null)
+ getActionBar().removeTab(fileExplorerTab);
+ }
+ } else {
+ mFSFragment.refresh();
+ }
+ }
+
+ public boolean showClear() {
+ if(mData == null || mData.equals(""))
+ return false;
+ else
+ return mShowClear;
+ }
+
+ protected class MyTabsListener<T extends Fragment> implements ActionBar.TabListener
+ {
+ private Fragment mFragment;
+ private boolean mAdded=false;
+
+ public MyTabsListener( Activity activity, Fragment fragment){
+ this.mFragment = fragment;
+ }
+
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ // Check if the fragment is already initialized
+ if (!mAdded) {
+ // If not, instantiate and add it to the activity
+ ft.add(android.R.id.content, mFragment);
+ mAdded =true;
+ } else {
+ // If it exists, simply attach it in order to show it
+ ft.attach(mFragment);
+ }
+ }
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ ft.detach(mFragment);
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+
+ }
+ }
+
+ public void importFile(String path) {
+ File ifile = new File(path);
+ Exception fe = null;
+ try {
+
+ String data = "";
+
+ byte[] fileData = readBytesFromFile(ifile) ;
+ if(mBase64Encode)
+ data += Base64.encodeToString(fileData, Base64.DEFAULT);
+ else
+ data += new String(fileData);
+
+ mData =data;
+
+ /*
+ mInlineFragment.setData(data);
+ getActionBar().selectTab(inlineFileTab); */
+ saveInlineData(ifile.getName(), data);
+ } catch (IOException e) {
+ fe =e;
+ }
+ if(fe!=null) {
+ Builder ab = new AlertDialog.Builder(this);
+ ab.setTitle(R.string.error_importing_file);
+ ab.setMessage(getString(R.string.import_error_message) + "\n" + fe.getLocalizedMessage());
+ ab.setPositiveButton(android.R.string.ok, null);
+ ab.show();
+ }
+ }
+
+ static private byte[] readBytesFromFile(File file) throws IOException {
+ InputStream input = new FileInputStream(file);
+
+ long len= file.length();
+ if (len > VpnProfile.MAX_EMBED_FILE_SIZE)
+ throw new IOException("selected file size too big to embed into profile");
+
+ // Create the byte array to hold the data
+ byte[] bytes = new byte[(int) len];
+
+ // Read in the bytes
+ int offset = 0;
+ int bytesRead = 0;
+ while (offset < bytes.length
+ && (bytesRead=input.read(bytes, offset, bytes.length-offset)) >= 0) {
+ offset += bytesRead;
+ }
+
+ input.close();
+ return bytes;
+ }
+
+
+ public void setFile(String path) {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_DATA, path);
+ setResult(Activity.RESULT_OK,intent);
+ finish();
+ }
+
+ public String getSelectPath() {
+ if(VpnProfile.isEmbedded(mData))
+ return mData;
+ else
+ return Environment.getExternalStorageDirectory().getPath();
+ }
+
+ public CharSequence getInlineData() {
+ if(VpnProfile.isEmbedded(mData))
+ return VpnProfile.getEmbeddedContent(mData);
+ else
+ return "";
+ }
+
+ public void clearData() {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_DATA, (String)null);
+ setResult(Activity.RESULT_OK,intent);
+ finish();
+
+ }
+
+ public void saveInlineData(String fileName, String string) {
+ Intent intent = new Intent();
+
+ if(fileName==null)
+ intent.putExtra(RESULT_DATA, VpnProfile.INLINE_TAG + string);
+ else
+ intent.putExtra(RESULT_DATA,VpnProfile.DISPLAYNAME_TAG + fileName + VpnProfile.INLINE_TAG + string);
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java b/main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java
new file mode 100644
index 00000000..db70eca9
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java
@@ -0,0 +1,37 @@
+/*
+ * 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 de.blinkt.openvpn.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.fragments.LogFragment;
+
+/**
+ * Created by arne on 13.10.13.
+ */
+public class LogWindow extends BaseActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.log_window);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ if (savedInstanceState == null) {
+ getFragmentManager().beginTransaction()
+ .add(R.id.container, new LogFragment())
+ .commit();
+ }
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java b/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java
new file mode 100644
index 00000000..f7c46d01
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java
@@ -0,0 +1,131 @@
+/*
+ * 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 de.blinkt.openvpn.activities;
+
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.support.v4n.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.fragments.AboutFragment;
+import de.blinkt.openvpn.fragments.FaqFragment;
+import de.blinkt.openvpn.fragments.GeneralSettings;
+import de.blinkt.openvpn.fragments.GraphFragment;
+import de.blinkt.openvpn.fragments.LogFragment;
+import de.blinkt.openvpn.fragments.SendDumpFragment;
+import de.blinkt.openvpn.fragments.VPNProfileList;
+import de.blinkt.openvpn.views.ScreenSlidePagerAdapter;
+import de.blinkt.openvpn.views.SlidingTabLayout;
+import de.blinkt.openvpn.views.TabBarView;
+
+
+public class MainActivity extends BaseActivity {
+
+ private ViewPager mPager;
+ private ScreenSlidePagerAdapter mPagerAdapter;
+ private SlidingTabLayout mSlidingTabLayout;
+ private TabBarView mTabs;
+
+ protected void onCreate(android.os.Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.main_activity);
+
+
+ // Instantiate a ViewPager and a PagerAdapter.
+ mPager = (ViewPager) findViewById(R.id.pager);
+ mPagerAdapter = new ScreenSlidePagerAdapter(getFragmentManager(), this);
+
+ /* Toolbar and slider should have the same elevation */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ disableToolbarElevation();
+ }
+
+
+ mPagerAdapter.addTab(R.string.vpn_list_title, VPNProfileList.class);
+ mPagerAdapter.addTab(R.string.graph, GraphFragment.class);
+
+ mPagerAdapter.addTab(R.string.generalsettings, GeneralSettings.class);
+ mPagerAdapter.addTab(R.string.faq, FaqFragment.class);
+
+ if (SendDumpFragment.getLastestDump(this) != null) {
+ mPagerAdapter.addTab(R.string.crashdump, SendDumpFragment.class);
+ }
+
+
+ if (isDirectToTV())
+ mPagerAdapter.addTab(R.string.openvpn_log, LogFragment.class);
+
+ mPagerAdapter.addTab(R.string.about, AboutFragment.class);
+ mPager.setAdapter(mPagerAdapter);
+
+ mTabs = (TabBarView) findViewById(R.id.sliding_tabs);
+ mTabs.setViewPager(mPager);
+ }
+
+ private static final String FEATURE_TELEVISION = "android.hardware.type.television";
+ private static final String FEATURE_LEANBACK = "android.software.leanback";
+
+ private boolean isDirectToTV() {
+ return(getPackageManager().hasSystemFeature(FEATURE_TELEVISION)
+ || getPackageManager().hasSystemFeature(FEATURE_LEANBACK));
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void disableToolbarElevation() {
+ ActionBar toolbar = getActionBar();
+ toolbar.setElevation(0);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (getIntent()!=null) {
+ String page = getIntent().getStringExtra("PAGE");
+ if ("graph".equals(page)) {
+ mPager.setCurrentItem(1);
+ }
+ setIntent(null);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main_menu,menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId()==R.id.show_log){
+ Intent showLog = new Intent(this, LogWindow.class);
+ startActivity(showLog);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ System.out.println(data);
+
+
+ }
+
+
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java b/main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java
new file mode 100644
index 00000000..4720dd60
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2012-2017 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.activities;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.app.Activity;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.Locale;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.NativeUtils;
+import de.blinkt.openvpn.core.OpenVPNService;
+
+public class OpenSSLSpeed extends Activity {
+
+ private static SpeeedTest runTestAlgorithms;
+ private EditText mCipher;
+ private SpeedArrayAdapter mAdapter;
+ private ListView mListView;
+
+
+ static class SpeedArrayAdapter extends ArrayAdapter<SpeedResult> {
+
+ private final Context mContext;
+ private final LayoutInflater mInflater;
+
+ public SpeedArrayAdapter(@NonNull Context context) {
+ super(context, 0);
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+
+ }
+
+ class ViewHolder {
+ TextView ciphername;
+ TextView blocksize;
+ TextView blocksInTime;
+ TextView speed;
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
+ SpeedResult res = getItem(position);
+ if (view == null) {
+ view = mInflater.inflate(R.layout.speedviewitem, parent, false);
+ ViewHolder holder = new ViewHolder();
+ holder.ciphername = view.findViewById(R.id.ciphername);
+ holder.speed = view.findViewById(R.id.speed);
+ holder.blocksize = view.findViewById(R.id.blocksize);
+ holder.blocksInTime = view.findViewById(R.id.blocksintime);
+ view.setTag(holder);
+ }
+
+ ViewHolder holder = (ViewHolder) view.getTag();
+
+ double total = res.count * res.length;
+ String size = OpenVPNService.humanReadableByteCount((long) res.length, false, mContext.getResources());
+
+ holder.blocksize.setText(size);
+ holder.ciphername.setText(res.algorithm);
+
+ if (res.failed) {
+ holder.blocksInTime.setText(R.string.openssl_error);
+ holder.speed.setText("-");
+ } else if (res.running) {
+ holder.blocksInTime.setText(R.string.running_test);
+ holder.speed.setText("-");
+ } else {
+ String totalBytes = OpenVPNService.humanReadableByteCount((long) total, false, mContext.getResources());
+ // TODO: Fix localisation here
+ String blockPerSec = OpenVPNService.humanReadableByteCount((long) (total / res.time), false, mContext.getResources()) + "/s";
+ holder.speed.setText(blockPerSec);
+ holder.blocksInTime.setText(String.format(Locale.ENGLISH, "%d blocks (%s) in %2.1fs", (long) res.count, totalBytes, res.time));
+ }
+
+ return view;
+
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.openssl_speed);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ findViewById(R.id.testSpecific).setOnClickListener((view) -> {
+ runAlgorithms(mCipher.getText().toString());
+ });
+ mCipher = (EditText) findViewById(R.id.ciphername);
+
+ mListView = findViewById(R.id.results);
+
+ mAdapter = new SpeedArrayAdapter(this);
+ mListView.setAdapter(mAdapter);
+
+ }
+
+ private void runAlgorithms(String algorithms) {
+ if (runTestAlgorithms != null)
+ runTestAlgorithms.cancel(true);
+ runTestAlgorithms = new SpeeedTest();
+ runTestAlgorithms.execute(algorithms.split(" "));
+ }
+
+
+ static class SpeedResult {
+ String algorithm;
+ boolean failed = false;
+
+ double count;
+ double time;
+ int length;
+ public boolean running=true;
+
+ SpeedResult(String algorithm) {
+ this.algorithm = algorithm;
+ }
+ }
+
+
+ private class SpeeedTest extends AsyncTask<String, SpeedResult, SpeedResult[]> {
+
+
+ private boolean mCancel = false;
+
+ @Override
+ protected SpeedResult[] doInBackground(String... strings) {
+ Vector<SpeedResult> mResult = new Vector<>();
+
+ for (String algorithm : strings) {
+
+ // Skip 16b and 16k as they are not relevevant for VPN
+ for (int i = 1; i < NativeUtils.openSSLlengths.length -1 && !mCancel; i++) {
+ SpeedResult result = new SpeedResult(algorithm);
+ result.length = NativeUtils.openSSLlengths[i];
+ mResult.add(result);
+ publishProgress(result);
+ double[] resi = NativeUtils.getOpenSSLSpeed(algorithm, i);
+ if (resi == null) {
+ result.failed = true;
+ } else {
+ result.count = resi[1];
+ result.time = resi[2];
+ }
+ result.running = false;
+ publishProgress(result);
+ }
+ }
+
+ return mResult.toArray(new SpeedResult[mResult.size()]);
+
+ }
+
+ @Override
+ protected void onProgressUpdate(SpeedResult... values) {
+ for (SpeedResult r : values) {
+ if (r.running)
+ mAdapter.add(r);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(SpeedResult[] speedResult) {
+
+ }
+
+ @Override
+ protected void onCancelled(SpeedResult[] speedResults) {
+ mCancel = true;
+ }
+ }
+
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java b/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java
new file mode 100644
index 00000000..06f1f7b7
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java
@@ -0,0 +1,244 @@
+/*
+ * 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 de.blinkt.openvpn.activities;
+
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.v4n.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import android.widget.Toast;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.fragments.Settings_Allowed_Apps;
+import de.blinkt.openvpn.fragments.Settings_Authentication;
+import de.blinkt.openvpn.fragments.Settings_Basic;
+import de.blinkt.openvpn.fragments.Settings_Connections;
+import de.blinkt.openvpn.fragments.Settings_IP;
+import de.blinkt.openvpn.fragments.Settings_Obscure;
+import de.blinkt.openvpn.fragments.Settings_Routing;
+import de.blinkt.openvpn.fragments.Settings_UserEditable;
+import de.blinkt.openvpn.fragments.ShowConfigFragment;
+import de.blinkt.openvpn.fragments.VPNProfileList;
+import de.blinkt.openvpn.views.ScreenSlidePagerAdapter;
+import de.blinkt.openvpn.views.TabBarView;
+
+
+public class VPNPreferences extends BaseActivity {
+
+ static final Class validFragments[] = new Class[] {
+ Settings_Authentication.class, Settings_Basic.class, Settings_IP.class,
+ Settings_Obscure.class, Settings_Routing.class, ShowConfigFragment.class,
+ Settings_Connections.class, Settings_Allowed_Apps.class
+ };
+
+ private String mProfileUUID;
+ private VpnProfile mProfile;
+ private ViewPager mPager;
+ private ScreenSlidePagerAdapter mPagerAdapter;
+
+ public VPNPreferences() {
+ super();
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ protected boolean isValidFragment(String fragmentName) {
+ for (Class c: validFragments)
+ if (c.getName().equals(fragmentName))
+ return true;
+ return false;
+
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putString(getIntent().getStringExtra(getPackageName() + ".profileUUID"),mProfileUUID);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getProfile();
+ // When a profile is deleted from a category fragment in hadset mod we need to finish
+ // this activity as well when returning
+ if (mProfile==null || mProfile.profileDeleted) {
+ setResult(VPNProfileList.RESULT_VPN_DELETED);
+ finish();
+ }
+ if (mProfile.mTemporaryProfile)
+ {
+ Toast.makeText(this, "Temporary profiles cannot be edited", Toast.LENGTH_LONG);
+ finish();
+ }
+ }
+
+ private void getProfile() {
+ Intent intent = getIntent();
+
+ if(intent!=null) {
+ String profileUUID = intent.getStringExtra(getPackageName() + ".profileUUID");
+ if(profileUUID==null) {
+ Bundle initialArguments = getIntent().getBundleExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ profileUUID = initialArguments.getString(getPackageName() + ".profileUUID");
+ }
+ if(profileUUID!=null){
+
+ mProfileUUID = profileUUID;
+ mProfile = ProfileManager.get(this, mProfileUUID);
+
+ }
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ mProfileUUID = getIntent().getStringExtra(getPackageName() + ".profileUUID");
+ if(savedInstanceState!=null){
+ String savedUUID = savedInstanceState.getString(getPackageName() + ".profileUUID");
+ if(savedUUID!=null)
+ mProfileUUID=savedUUID;
+ }
+
+ mProfile = ProfileManager.get(this,mProfileUUID);
+ if(mProfile!=null) {
+ setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+ }
+ super.onCreate(savedInstanceState);
+
+
+ setContentView(R.layout.main_activity);
+
+ /* Toolbar and slider should have the same elevation */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ disableToolbarElevation();
+ }
+
+ // Instantiate a ViewPager and a PagerAdapter.
+ mPager = (ViewPager) findViewById(R.id.pager);
+ mPagerAdapter = new ScreenSlidePagerAdapter(getFragmentManager(), this);
+
+
+ Bundle fragmentArguments = new Bundle();
+ fragmentArguments.putString(getPackageName() + ".profileUUID",mProfileUUID);
+ mPagerAdapter.setFragmentArgs(fragmentArguments);
+
+ if (mProfile.mUserEditable) {
+ mPagerAdapter.addTab(R.string.basic, Settings_Basic.class);
+ mPagerAdapter.addTab(R.string.server_list, Settings_Connections.class);
+ mPagerAdapter.addTab(R.string.ipdns, Settings_IP.class);
+ mPagerAdapter.addTab(R.string.routing, Settings_Routing.class);
+ mPagerAdapter.addTab(R.string.settings_auth, Settings_Authentication.class);
+
+ mPagerAdapter.addTab(R.string.advanced, Settings_Obscure.class);
+ } else {
+ mPagerAdapter.addTab(R.string.basic, Settings_UserEditable.class);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ mPagerAdapter.addTab(R.string.vpn_allowed_apps, Settings_Allowed_Apps.class);
+
+ mPagerAdapter.addTab(R.string.generated_config, ShowConfigFragment.class);
+
+
+ mPager.setAdapter(mPagerAdapter);
+
+ TabBarView tabs = (TabBarView) findViewById(R.id.sliding_tabs);
+ tabs.setViewPager(mPager);
+
+ }
+
+
+/*
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ loadHeadersFromResource(R.xml.vpn_headers, target);
+ Header headerToRemove=null;
+ for (Header header : target) {
+ if(header.fragmentArguments==null)
+ header.fragmentArguments = new Bundle();
+ header.fragmentArguments.putString(getPackageName() + ".profileUUID",mProfileUUID);
+ if (header.id == R.id.allowed_apps_header && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ headerToRemove = header;
+ }
+ if (headerToRemove != null)
+ target.remove(headerToRemove);
+ }*/
+
+ @Override
+ public void onBackPressed() {
+ setResult(RESULT_OK, getIntent());
+ super.onBackPressed();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.remove_vpn)
+ askProfileRemoval();
+ if (item.getItemId() == R.id.duplicate_vpn) {
+ Intent data = new Intent();
+ data.putExtra(VpnProfile.EXTRA_PROFILEUUID, mProfileUUID);
+ setResult(VPNProfileList.RESULT_VPN_DUPLICATE, data);
+ finish();
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ getMenuInflater().inflate(R.menu.vpnpreferences_menu, menu);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ private void askProfileRemoval() {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle("Confirm deletion");
+ dialog.setMessage(getString(R.string.remove_vpn_query, mProfile.mName));
+
+ dialog.setPositiveButton(android.R.string.yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ removeProfile(mProfile);
+ }
+
+ });
+ dialog.setNegativeButton(android.R.string.no,null);
+ dialog.create().show();
+ }
+
+ protected void removeProfile(VpnProfile profile) {
+ ProfileManager.getInstance(this).removeProfile(this,profile);
+ setResult(VPNProfileList.RESULT_VPN_DELETED);
+ finish();
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void disableToolbarElevation() {
+ ActionBar toolbar = getActionBar();
+ toolbar.setElevation(0);
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java
new file mode 100644
index 00000000..540f4a9a
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java
@@ -0,0 +1,333 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import de.blinkt.openvpn.core.NativeUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Locale;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.VpnStatus;
+
+public class AboutFragment extends Fragment implements View.OnClickListener {
+
+ public static final String INAPPITEM_TYPE_INAPP = "inapp";
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ private static final int DONATION_CODE = 12;
+ private static final int BILLING_RESPONSE_RESULT_OK = 0;
+ private static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ private static final String[] donationSkus = { "donation1eur", "donation2eur", "donation5eur", "donation10eur",
+ "donation1337eur","donation23eur","donation25eur",};
+ IInAppBillingService mService;
+ Hashtable<View, String> viewToProduct = new Hashtable<>();
+ ServiceConnection mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name,
+ IBinder service) {
+ mService = IInAppBillingService.Stub.asInterface(service);
+ initGooglePlayDonation();
+
+ }
+ };
+
+ private void initGooglePlayDonation() {
+ new Thread("queryGMSInApp") {
+ @Override
+ public void run() {
+ initGMSDonateOptions();
+ }
+ }.start();
+ }
+
+ private TextView gmsTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ /*
+ getActivity().bindService(new
+ Intent("com.android.vending.billing.InAppBillingService.BIND"),
+ mServiceConn, Context.BIND_AUTO_CREATE);
+ */
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mService != null) {
+ getActivity().unbindService(mServiceConn);
+ }
+
+ }
+
+ private void initGMSDonateOptions() {
+ try {
+ int billingSupported = mService.isBillingSupported(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP);
+ if (billingSupported != BILLING_RESPONSE_RESULT_OK) {
+ Log.i("OpenVPN", "Play store billing not supported");
+ return;
+ }
+
+ ArrayList skuList = new ArrayList();
+ Collections.addAll(skuList, donationSkus);
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList("ITEM_ID_LIST", skuList);
+
+ Bundle ownedItems = mService.getPurchases(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, null);
+
+
+ if (ownedItems.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK)
+ return;
+
+ final ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
+
+ Bundle skuDetails = mService.getSkuDetails(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, querySkus);
+
+
+ if (skuDetails.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK)
+ return;
+
+ final ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
+
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ createPlayBuyOptions(ownedSkus, responseList);
+
+ }
+ });
+ }
+
+ } catch (RemoteException e) {
+ VpnStatus.logException(e);
+ }
+ }
+
+ private static class SkuResponse {
+ String title;
+ String price;
+
+ SkuResponse(String p, String t)
+ {
+ title=t;
+ price=p;
+ }
+ }
+
+
+
+ private void createPlayBuyOptions(ArrayList<String> ownedSkus, ArrayList<String> responseList) {
+ try {
+ Vector<Pair<String,String>> gdonation = new Vector<Pair<String, String>>();
+
+ gdonation.add(new Pair<String, String>(getString(R.string.donatePlayStore),null));
+ HashMap<String, SkuResponse> responseMap = new HashMap<String, SkuResponse>();
+ for (String thisResponse : responseList) {
+ JSONObject object = new JSONObject(thisResponse);
+ responseMap.put(
+ object.getString("productId"),
+ new SkuResponse(
+ object.getString("price"),
+ object.getString("title")));
+
+ }
+ for (String sku: donationSkus)
+ if (responseMap.containsKey(sku))
+ gdonation.add(getSkuTitle(sku,
+ responseMap.get(sku).title, responseMap.get(sku).price, ownedSkus));
+
+ String gmsTextString="";
+ for(int i=0;i<gdonation.size();i++) {
+ if(i==1)
+ gmsTextString+= " ";
+ else if(i>1)
+ gmsTextString+= ", ";
+ gmsTextString+=gdonation.elementAt(i).first;
+ }
+ SpannableString gmsText = new SpannableString(gmsTextString);
+
+
+ int lStart = 0;
+ int lEnd=0;
+ for(Pair<String, String> item:gdonation){
+ lEnd = lStart + item.first.length();
+ if (item.second!=null) {
+ final String mSku = item.second;
+ ClickableSpan cspan = new ClickableSpan()
+ {
+ @Override
+ public void onClick(View widget) {
+ triggerBuy(mSku);
+ }
+ };
+ gmsText.setSpan(cspan,lStart,lEnd,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ lStart = lEnd+2; // Account for ", " between items
+ }
+
+ if(gmsTextView !=null) {
+ gmsTextView.setText(gmsText);
+ gmsTextView.setMovementMethod(LinkMovementMethod.getInstance());
+ gmsTextView.setVisibility(View.VISIBLE);
+ }
+
+ } catch (JSONException e) {
+ VpnStatus.logException("Parsing Play Store IAP",e);
+ }
+
+ }
+
+ private Pair<String,String> getSkuTitle(final String sku, String title, String price, ArrayList<String> ownedSkus) {
+ String text;
+ if (ownedSkus.contains(sku))
+ return new Pair<String,String>(getString(R.string.thanks_for_donation, price),null);
+
+ if (price.contains("€")|| price.contains("\u20ac")) {
+ text= title;
+ } else {
+ text = String.format(Locale.getDefault(), "%s (%s)", title, price);
+ }
+ //return text;
+ return new Pair<String,String>(price, sku);
+
+ }
+
+ private void triggerBuy(String sku) {
+ try {
+ Bundle buyBundle
+ = mService.getBuyIntent(3, getActivity().getPackageName(),
+ sku, INAPPITEM_TYPE_INAPP, "Thanks for the donation! :)");
+
+
+ if (buyBundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
+ PendingIntent buyIntent = buyBundle.getParcelable(RESPONSE_BUY_INTENT);
+ getActivity().startIntentSenderForResult(buyIntent.getIntentSender(), DONATION_CODE, new Intent(),
+ 0, 0, 0);
+ }
+
+ } catch (RemoteException e) {
+ VpnStatus.logException(e);
+ } catch (IntentSender.SendIntentException e) {
+ VpnStatus.logException(e);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.about, container, false);
+ TextView ver = (TextView) v.findViewById(R.id.version);
+
+ String version;
+ String name = "Openvpn";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+ ver.setText(getString(R.string.version_info, name, version));
+
+ TextView verO2 = v.findViewById(R.id.version_ovpn2);
+ TextView verO3 = v.findViewById(R.id.version_ovpn3);
+
+ verO2.setText(String.format(Locale.US, "OpenVPN version: %s", NativeUtils.getOpenVPN2GitVersion()));
+ verO3.setText(String.format(Locale.US, "OpenVPN3 core version: %s", NativeUtils.getOpenVPN3GitVersion()));
+
+ gmsTextView = (TextView) v.findViewById(R.id.donategms);
+ /* recreating view without onCreate/onDestroy cycle */
+
+ // Disable GMS for now
+ if (mService!=null)
+ initGooglePlayDonation();
+
+ TextView translation = (TextView) v.findViewById(R.id.translation);
+
+ // Don't print a text for myself
+ if (getString(R.string.translationby).contains("Arne Schwabe"))
+ translation.setText("");
+ else
+ translation.setText(R.string.translationby);
+
+ TextView wv = (TextView) v.findViewById(R.id.full_licenses);
+ wv.setText(Html.fromHtml(readHtmlFromAssets()));
+ return v;
+ }
+
+ String readHtmlFromAssets()
+ {
+ InputStream mvpn;
+
+ try {
+ mvpn = getActivity().getAssets().open("full_licenses.html");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(mvpn));
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ reader.close();
+ return sb.toString();
+ } catch (IOException errabi) {
+ return "full_licenses.html not found";
+ }
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (mService!=null)
+ initGooglePlayDonation();
+ }
+
+
+ @Override
+ public void onClick(View v) {
+
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
new file mode 100644
index 00000000..9c4c80de
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
@@ -0,0 +1,413 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+
+import java.util.Arrays;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.Connection;
+
+public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.ConnectionsHolder> {
+ private static final int TYPE_NORMAL = 0;
+ private static final int TYPE_FOOTER = TYPE_NORMAL + 1;
+ private final Context mContext;
+ private final VpnProfile mProfile;
+ private final Settings_Connections mConnectionFragment;
+ private Connection[] mConnections;
+
+ ConnectionsAdapter(Context c, Settings_Connections connections_fragments, VpnProfile vpnProfile) {
+ mContext = c;
+ mConnections = vpnProfile.mConnections;
+ mProfile = vpnProfile;
+ mConnectionFragment = connections_fragments;
+ }
+
+ @Override
+ public ConnectionsAdapter.ConnectionsHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ LayoutInflater li = LayoutInflater.from(mContext);
+
+ View card;
+ if (viewType == TYPE_NORMAL) {
+ card = li.inflate(R.layout.server_card, viewGroup, false);
+
+ } else { // TYPE_FOOTER
+ card = li.inflate(R.layout.server_footer, viewGroup, false);
+ }
+ return new ConnectionsHolder(card, this, viewType);
+
+ }
+
+ @Override
+ public void onBindViewHolder(final ConnectionsAdapter.ConnectionsHolder cH, int position) {
+ if (position == mConnections.length) {
+ // Footer
+ return;
+ }
+ final Connection connection = mConnections[position];
+
+ cH.mConnection = null;
+
+ cH.mPortNumberView.setText(connection.mServerPort);
+ cH.mServerNameView.setText(connection.mServerName);
+ cH.mPortNumberView.setText(connection.mServerPort);
+ cH.mRemoteSwitch.setChecked(connection.mEnabled);
+
+
+ cH.mProxyNameView.setText(connection.mProxyName);
+ cH.mProxyPortNumberView.setText(connection.mProxyPort);
+
+ cH.mConnectText.setText(String.valueOf(connection.getTimeout()));
+
+ cH.mConnectSlider.setProgress(connection.getTimeout());
+
+
+ cH.mProtoGroup.check(connection.mUseUdp ? R.id.udp_proto : R.id.tcp_proto);
+
+ switch (connection.mProxyType) {
+ case NONE:
+ cH.mProxyGroup.check(R.id.proxy_none);
+ break;
+ case HTTP:
+ cH.mProxyGroup.check(R.id.proxy_http);
+ break;
+ case SOCKS5:
+ cH.mProxyGroup.check(R.id.proxy_http);
+ break;
+ case ORBOT:
+ cH.mProxyGroup.check(R.id.proxy_orbot);
+ break;
+ }
+
+ cH.mProxyAuthCb.setChecked(connection.mUseProxyAuth);
+ cH.mProxyAuthUser.setText(connection.mProxyAuthUser);
+ cH.mProxyAuthPassword.setText(connection.mProxyAuthPassword);
+
+ cH.mCustomOptionsLayout.setVisibility(connection.mUseCustomConfig ? View.VISIBLE : View.GONE);
+ cH.mCustomOptionText.setText(connection.mCustomConfiguration);
+
+ cH.mCustomOptionCB.setChecked(connection.mUseCustomConfig);
+ cH.mConnection = connection;
+
+ setVisibilityProxyServer(cH, connection);
+
+ }
+
+ private void setVisibilityProxyServer(ConnectionsHolder cH, Connection connection) {
+ int visible = (connection.mProxyType == Connection.ProxyType.HTTP || connection.mProxyType == Connection.ProxyType.SOCKS5) ? View.VISIBLE : View.GONE;
+ int authVisible = (connection.mProxyType == Connection.ProxyType.HTTP) ? View.VISIBLE : View.GONE;
+
+ cH.mProxyNameView.setVisibility(visible);
+ cH.mProxyPortNumberView.setVisibility(visible);
+ cH.mProxyNameLabel.setVisibility(visible);
+
+ cH.mProxyAuthLayout.setVisibility(authVisible);
+
+ }
+
+ private void removeRemote(int idx) {
+ Connection[] mConnections2 = Arrays.copyOf(mConnections, mConnections.length - 1);
+ for (int i = idx + 1; i < mConnections.length; i++) {
+ mConnections2[i - 1] = mConnections[i];
+ }
+ mConnections = mConnections2;
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return mConnections.length + 1; //for footer
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == mConnections.length)
+ return TYPE_FOOTER;
+ else
+ return TYPE_NORMAL;
+ }
+
+ void addRemote() {
+ mConnections = Arrays.copyOf(mConnections, mConnections.length + 1);
+ mConnections[mConnections.length - 1] = new Connection();
+ notifyItemInserted(mConnections.length - 1);
+ displayWarningIfNoneEnabled();
+ }
+
+ void displayWarningIfNoneEnabled() {
+ int showWarning = View.VISIBLE;
+ for (Connection conn : mConnections) {
+ if (conn.mEnabled)
+ showWarning = View.GONE;
+ }
+ mConnectionFragment.setWarningVisible(showWarning);
+ }
+
+ void saveProfile() {
+ mProfile.mConnections = mConnections;
+ }
+
+ static abstract class OnTextChangedWatcher implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+ }
+
+ class ConnectionsHolder extends RecyclerView.ViewHolder {
+ private final EditText mServerNameView;
+ private final EditText mPortNumberView;
+ private final Switch mRemoteSwitch;
+ private final RadioGroup mProtoGroup;
+ private final EditText mCustomOptionText;
+ private final CheckBox mCustomOptionCB;
+ private final View mCustomOptionsLayout;
+ private final ImageButton mDeleteButton;
+ private final EditText mConnectText;
+ private final SeekBar mConnectSlider;
+ private final ConnectionsAdapter mConnectionsAdapter;
+ private final RadioGroup mProxyGroup;
+ private final EditText mProxyNameView;
+ private final EditText mProxyPortNumberView;
+ private final View mProxyNameLabel;
+ private final View mProxyAuthLayout;
+ private final EditText mProxyAuthUser;
+ private final EditText mProxyAuthPassword;
+ private final CheckBox mProxyAuthCb;
+
+ private Connection mConnection; // Set to null on update
+
+
+ ConnectionsHolder(View card, ConnectionsAdapter connectionsAdapter, int viewType) {
+ super(card);
+ mServerNameView = card.findViewById(R.id.servername);
+ mPortNumberView = card.findViewById(R.id.portnumber);
+ mRemoteSwitch = card.findViewById(R.id.remoteSwitch);
+ mCustomOptionCB = card.findViewById(R.id.use_customoptions);
+ mCustomOptionText = card.findViewById(R.id.customoptions);
+ mProtoGroup = card.findViewById(R.id.udptcpradiogroup);
+ mCustomOptionsLayout = card.findViewById(R.id.custom_options_layout);
+ mDeleteButton = card.findViewById(R.id.remove_connection);
+ mConnectSlider = card.findViewById(R.id.connect_silder);
+ mConnectText = card.findViewById(R.id.connect_timeout);
+
+ mProxyGroup = card.findViewById(R.id.proxyradiogroup);
+ mProxyNameView = card.findViewById(R.id.proxyname);
+ mProxyPortNumberView = card.findViewById(R.id.proxyport);
+ mProxyNameLabel = card.findViewById(R.id.proxyserver_label);
+
+ mProxyAuthLayout = card.findViewById(R.id.proxyauthlayout);
+ mProxyAuthCb = card.findViewById(R.id.enable_proxy_auth);
+ mProxyAuthUser = card.findViewById(R.id.proxyuser);
+ mProxyAuthPassword = card.findViewById(R.id.proxypassword);
+
+ mConnectionsAdapter = connectionsAdapter;
+
+ if (viewType == TYPE_NORMAL)
+ addListeners();
+ }
+
+
+ void addListeners() {
+ mRemoteSwitch.setOnCheckedChangeListener((CompoundButton buttonView, boolean isChecked) -> {
+ if (mConnection != null) {
+ mConnection.mEnabled = isChecked;
+ mConnectionsAdapter.displayWarningIfNoneEnabled();
+ }
+ });
+
+ mProtoGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (mConnection != null) {
+ if (checkedId == R.id.udp_proto)
+ mConnection.mUseUdp = true;
+ else if (checkedId == R.id.tcp_proto)
+ mConnection.mUseUdp = false;
+ }
+ });
+
+ mProxyGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (mConnection != null) {
+ switch (checkedId) {
+ case R.id.proxy_none:
+ mConnection.mProxyType = Connection.ProxyType.NONE;
+ break;
+ case R.id.proxy_http:
+ mConnection.mProxyType = Connection.ProxyType.HTTP;
+ break;
+ case R.id.proxy_socks:
+ mConnection.mProxyType = Connection.ProxyType.SOCKS5;
+ break;
+ case R.id.proxy_orbot:
+ mConnection.mProxyType = Connection.ProxyType.ORBOT;
+ break;
+ }
+ setVisibilityProxyServer(this, mConnection);
+ }
+ });
+
+ mProxyAuthCb.setOnCheckedChangeListener((group, isChecked) ->
+ {
+ if (mConnection != null) {
+ mConnection.mUseProxyAuth = isChecked;
+ setVisibilityProxyServer(this, mConnection);
+ }
+ });
+
+ mCustomOptionText.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null)
+ mConnection.mCustomConfiguration = s.toString();
+ }
+ });
+
+ mCustomOptionCB.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mConnection != null) {
+ mConnection.mUseCustomConfig = isChecked;
+ mCustomOptionsLayout.setVisibility(mConnection.mUseCustomConfig ? View.VISIBLE : View.GONE);
+ }
+ });
+
+
+ mServerNameView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mServerName = s.toString();
+ }
+ }
+
+ });
+
+ mPortNumberView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mServerPort = s.toString();
+ }
+ }
+ });
+
+ mProxyNameView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyName = s.toString();
+ }
+ }
+
+ });
+
+ mProxyPortNumberView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyPort = s.toString();
+ }
+ }
+ });
+
+ mCustomOptionCB.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mConnection != null) {
+ mConnection.mUseProxyAuth = isChecked;
+ }
+ });
+
+ mProxyAuthPassword.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyAuthPassword = s.toString();
+ }
+ }
+ });
+
+
+ mProxyAuthUser.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyAuthUser = s.toString();
+ }
+ }
+ });
+
+
+
+ mCustomOptionText.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mCustomConfiguration = s.toString();
+ }
+ }
+ });
+
+ mConnectSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser && mConnection != null) {
+ mConnectText.setText(String.valueOf(progress));
+ mConnection.mConnectTimeout = progress;
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ mConnectText.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ try {
+ int t = Integer.valueOf(String.valueOf(s));
+ mConnectSlider.setProgress(t);
+ mConnection.mConnectTimeout = t;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+ });
+
+ mDeleteButton.setOnClickListener(
+ v -> {
+ AlertDialog.Builder ab = new AlertDialog.Builder(mContext);
+ ab.setTitle(R.string.query_delete_remote);
+ ab.setPositiveButton(R.string.keep, null);
+ ab.setNegativeButton(R.string.delete, (dialog, which) -> {
+ removeRemote(getAdapterPosition());
+ notifyItemRemoved(getAdapterPosition());
+ });
+ ab.create().show();
+ }
+ );
+
+
+ }
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
new file mode 100644
index 00000000..af4c35fe
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
@@ -0,0 +1,216 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.StaggeredGridLayoutManager;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+
+public class FaqFragment extends Fragment {
+ static class FAQEntry {
+
+ public FAQEntry(int startVersion, int endVersion, int title, int description) {
+ this.startVersion = startVersion;
+ this.endVersion = endVersion;
+ this.description = description;
+ this.title = title;
+ }
+
+ final int startVersion;
+ final int endVersion;
+ final int description;
+ final int title;
+
+ public boolean runningVersion() {
+ if (Build.VERSION.SDK_INT >= startVersion) {
+ if (Build.VERSION.SDK_INT <= endVersion)
+ return true;
+
+ if (endVersion == -1)
+ return true;
+
+ String release = Build.VERSION.RELEASE;
+ boolean isOlderThan443 = !release.startsWith("4.4.3") && !release.startsWith("4.4.4") &&
+ !release.startsWith("4.4.5") && !release.startsWith("4.4.6");
+
+ boolean isOlderThan442 = isOlderThan443 && !release.startsWith("4.4.2");
+
+
+ if(Build.VERSION.SDK_INT== Build.VERSION_CODES.KITKAT) {
+ if (endVersion == -441 && isOlderThan442)
+ return true;
+
+ if (endVersion == -442 && isOlderThan443)
+ return true;
+ } else if (endVersion == -441 || endVersion == -442) {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT;
+ }
+
+
+ }
+ return false;
+ }
+
+ public String getVersionsString(Context c) {
+ if (startVersion == Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if (endVersion == -1)
+ return null;
+ else
+ return c.getString(R.string.version_upto, getAndroidVersionString(c, endVersion));
+ }
+
+ if (endVersion == -1)
+ return c.getString(R.string.version_and_later, getAndroidVersionString(c, startVersion));
+
+
+ String startver = getAndroidVersionString(c, startVersion);
+
+ if (endVersion == startVersion)
+ return startver;
+
+ return String.format("%s - %s", startver, getAndroidVersionString(c, endVersion));
+ }
+
+
+ private String getAndroidVersionString(Context c, int versionCode) {
+ switch (versionCode) {
+ case Build.VERSION_CODES.ICE_CREAM_SANDWICH:
+ return "4.0 (Ice Cream Sandwich)";
+ case -441:
+ return "4.4.1 (KitKat)";
+ case -442:
+ return "4.4.2 (KitKat)";
+ case Build.VERSION_CODES.JELLY_BEAN_MR2:
+ return "4.3 (Jelly Bean MR2)";
+ case Build.VERSION_CODES.KITKAT:
+ return "4.4 (KitKat)";
+ case Build.VERSION_CODES.LOLLIPOP:
+ return "5.0 (Lollipop)";
+ default:
+ return "API " + versionCode;
+ }
+ }
+
+
+ }
+
+ private static FAQEntry[] faqitemsVersionSpecific = {
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_howto_title, R.string.faq_howto),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_killswitch_title, R.string.faq_killswitch),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_remote_api_title, R.string.faq_remote_api),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.weakmd_title, R.string.weakmd),
+ new FAQEntry(Build.VERSION_CODES.LOLLIPOP, -1, R.string.samsung_broken_title, R.string.samsung_broken),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_duplicate_notification_title, R.string.faq_duplicate_notification),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_androids_clients_title, R.string.faq_android_clients),
+
+
+ new FAQEntry(Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1, R.string.ab_lollipop_reinstall_title, R.string.ab_lollipop_reinstall),
+
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.vpn_tethering_title, R.string.faq_tethering),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.broken_images, R.string.broken_images_faq),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.battery_consumption_title, R.string.baterry_consumption),
+
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.KITKAT, R.string.faq_system_dialogs_title, R.string.faq_system_dialogs),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.faq_tap_mode),
+
+ new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.ab_secondary_users_title, R.string.ab_secondary_users),
+ new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, -1, R.string.faq_vpndialog43_title, R.string.faq_vpndialog43),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tls_cipher_alert_title, R.string.tls_cipher_alert),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_security_title, R.string.faq_security),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_shortcut, R.string.faq_howto_shortcut),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq2),
+
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.vpn_tethering_title, R.string.ab_tethering_44),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -441, R.string.ab_kitkat_mss_title, R.string.ab_kitkat_mss),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.copying_log_entries, R.string.faq_copying),
+
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -442, R.string.ab_persist_tun_title, R.string.ab_persist_tun),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.faq_routing_title, R.string.faq_routing),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_kitkat_reconnect_title, R.string.ab_kitkat_reconnect),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_vpn_reachability_44_title, R.string.ab_vpn_reachability_44),
+
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_only_cidr_title, R.string.ab_only_cidr),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_proxy_title, R.string.ab_proxy),
+
+ new FAQEntry(Build.VERSION_CODES.LOLLIPOP, -1, R.string.ab_not_route_to_vpn_title, R.string.ab_not_route_to_vpn),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq3),
+
+ // DNS weirdness in Samsung 5.0: https://plus.google.com/117315704597472009168/posts/g78bZLWmqgD
+ };
+
+
+ private RecyclerView mRecyclerView;
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.faq, container, false);
+
+ DisplayMetrics displaymetrics = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
+ int dpWidth = (int) (displaymetrics.widthPixels / getResources().getDisplayMetrics().density);
+
+ //better way but does not work on 5.0
+ //int dpWidth = (int) (container.getWidth()/getResources().getDisplayMetrics().density);
+ int columns = dpWidth / 360;
+ columns = Math.max(1, columns);
+
+
+ mRecyclerView = (RecyclerView) v.findViewById(R.id.faq_recycler_view);
+
+ // use this setting to improve performance if you know that changes
+ // in content do not change the layout size of the RecyclerView
+ mRecyclerView.setHasFixedSize(true);
+
+
+ mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL));
+
+ mRecyclerView.setAdapter(new FaqViewAdapter(getActivity(), getFAQEntries()));
+
+ return v;
+ }
+
+ private FAQEntry[] getFAQEntries() {
+ Vector<FAQEntry> faqItems = new Vector<>();
+
+ for (FAQEntry fe : faqitemsVersionSpecific) {
+ if (fe.runningVersion())
+ faqItems.add(fe);
+ }
+ for (FAQEntry fe : faqitemsVersionSpecific) {
+ if (!fe.runningVersion())
+ faqItems.add(fe);
+ }
+
+ return faqItems.toArray(new FAQEntry[faqItems.size()]);
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
new file mode 100644
index 00000000..0be9f4a2
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
@@ -0,0 +1,132 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+
+public class FaqViewAdapter extends RecyclerView.Adapter<FaqViewAdapter.FaqViewHolder> {
+ private final FaqFragment.FAQEntry[] mFaqItems;
+ private final Spanned[] mHtmlEntries;
+ private final Spanned[] mHtmlEntriesTitle;
+ private final Context mContext;
+ private boolean loaded =false;
+
+ public FaqViewAdapter(Context context, FaqFragment.FAQEntry[] faqItems) {
+ mFaqItems = faqItems;
+ mContext = context;
+
+ mHtmlEntries = new Spanned[faqItems.length];
+ mHtmlEntriesTitle = new Spanned[faqItems.length];
+
+ new FetchStrings().execute(faqItems);
+
+ }
+
+ private class FetchStrings extends AsyncTask<FaqFragment.FAQEntry,Void,Void> {
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ loaded=true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ protected Void doInBackground(FaqFragment.FAQEntry... params) {
+ fetchStrings(params);
+ return null;
+ }
+ }
+
+ private void fetchStrings(FaqFragment.FAQEntry[] faqItems) {
+ for (int i =0; i < faqItems.length; i++) {
+ String versionText = mFaqItems[i].getVersionsString(mContext);
+ String title;
+ String textColor="";
+
+ if (mFaqItems[i].title==-1)
+ title ="";
+ else
+ title = mContext.getString(faqItems[i].title);
+
+
+ if (!mFaqItems[i].runningVersion())
+ textColor= "<font color=\"gray\">";
+
+ if (versionText != null) {
+
+ mHtmlEntriesTitle[i] = (Spanned) TextUtils.concat(Html.fromHtml(textColor + title),
+ Html.fromHtml(textColor + "<br><small>" + versionText + "</small>"));
+ } else {
+ mHtmlEntriesTitle[i] = Html.fromHtml(title);
+ }
+
+ String content = mContext.getString(faqItems[i].description);
+ mHtmlEntries[i] = Html.fromHtml(textColor + content);
+
+ // Add hack R.string.faq_system_dialogs_title -> R.string.faq_system_dialog_xposed
+ if (faqItems[i].title == R.string.faq_system_dialogs_title)
+ {
+ Spanned xPosedtext = Html.fromHtml(textColor + mContext.getString(R.string.faq_system_dialog_xposed));
+ mHtmlEntries[i] = (Spanned) TextUtils.concat(mHtmlEntries[i], xPosedtext);
+ }
+ }
+ }
+
+ public static class FaqViewHolder extends RecyclerView.ViewHolder {
+
+ private final CardView mView;
+ private final TextView mBody;
+ private final TextView mHead;
+
+ public FaqViewHolder(View itemView) {
+ super(itemView);
+
+ mView = (CardView) itemView;
+ mBody = (TextView)mView.findViewById(R.id.faq_body);
+ mHead = (TextView)mView.findViewById(R.id.faq_head);
+ mBody.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+
+ @Override
+ public FaqViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
+ View view = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.faqcard, viewGroup, false);
+ return new FaqViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(FaqViewHolder faqViewHolder, int i) {
+
+ faqViewHolder.mHead.setText(mHtmlEntriesTitle[i]);
+ faqViewHolder.mBody.setText(mHtmlEntries[i]);
+
+
+ }
+
+ @Override
+ public int getItemCount() {
+ if(loaded)
+ return mFaqItems.length;
+ else
+ return 0;
+ }
+
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java
new file mode 100644
index 00000000..9d12b83d
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java
@@ -0,0 +1,325 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.ListFragment;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.activities.FileSelect;
+
+public class FileSelectionFragment extends ListFragment {
+
+ private static final String ITEM_KEY = "key";
+ private static final String ITEM_IMAGE = "image";
+ private static final String ROOT = "/";
+
+
+ private List<String> path = null;
+ private TextView myPath;
+ private ArrayList<HashMap<String, Object>> mList;
+
+ private Button selectButton;
+
+
+ private String parentPath;
+ private String currentPath = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+
+ private String[] formatFilter = null;
+
+ private File selectedFile;
+ private HashMap<String, Integer> lastPositions = new HashMap<>();
+ private String mStartPath;
+ private CheckBox mInlineImport;
+ private Button mClearButton;
+ private boolean mHideImport = false;
+
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ onListItemClick(getListView(), view, position, id);
+ onFileSelectionClick();
+ return true;
+ }
+ }
+
+ );
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.file_dialog_main, container, false);
+
+ myPath = (TextView) v.findViewById(R.id.path);
+
+ mInlineImport = (CheckBox) v.findViewById(R.id.doinline);
+
+ if (mHideImport) {
+ mInlineImport.setVisibility(View.GONE);
+ mInlineImport.setChecked(false);
+ }
+
+
+ selectButton = (Button) v.findViewById(R.id.fdButtonSelect);
+ selectButton.setEnabled(false);
+ selectButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ onFileSelectionClick();
+ }
+ });
+
+ mClearButton = (Button) v.findViewById(R.id.fdClear);
+ mClearButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((FileSelect) getActivity()).clearData();
+ }
+ });
+ if (!((FileSelect) getActivity()).showClear()) {
+ mClearButton.setVisibility(View.GONE);
+ mClearButton.setEnabled(false);
+ }
+
+ return v;
+ }
+
+ private void onFileSelectionClick() {
+ if (selectedFile != null) {
+ if (mInlineImport.isChecked())
+
+ ((FileSelect) getActivity()).importFile(selectedFile.getPath());
+ else
+ ((FileSelect) getActivity()).setFile(selectedFile.getPath());
+ }
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mStartPath = ((FileSelect) getActivity()).getSelectPath();
+ getDir(mStartPath);
+ }
+
+ public void refresh() {
+ getDir(Environment.getExternalStorageDirectory().getAbsolutePath());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+
+ private void getDir(String dirPath) {
+
+ boolean useAutoSelection = dirPath.length() < currentPath.length();
+
+ Integer position = lastPositions.get(parentPath);
+
+ getDirImpl(dirPath);
+
+ if (position != null && useAutoSelection) {
+ getListView().setSelection(position);
+ }
+
+ }
+
+ private void getDirImpl(final String dirPath) {
+
+ currentPath = dirPath;
+
+ final List<String> item = new ArrayList<String>();
+ path = new ArrayList<String>();
+ mList = new ArrayList<HashMap<String, Object>>();
+
+ File f = new File(currentPath);
+ File[] files = f.listFiles();
+ if (files == null) {
+ currentPath = ROOT;
+ f = new File(currentPath);
+ files = f.listFiles();
+
+ if (files == null)
+ files = new File[]{};
+ }
+
+ myPath.setText(getText(R.string.location) + ": " + currentPath);
+
+ if (!currentPath.equals(ROOT)) {
+
+ item.add(ROOT);
+ addItem(ROOT, R.drawable.ic_root_folder_am);
+ path.add(ROOT);
+
+ item.add("../");
+ addItem("../", R.drawable.ic_root_folder_am);
+ path.add(f.getParent());
+ parentPath = f.getParent();
+
+ }
+
+
+ TreeMap<String, String> dirsMap = new TreeMap<>();
+ TreeMap<String, String> dirsPathMap = new TreeMap<>();
+ TreeMap<String, String> filesMap = new TreeMap<>();
+ TreeMap<String, String> filesPathMap = new TreeMap<>();
+
+ // add default locations
+ for (String dir: getExternalStorages()) {
+ // You got to love the P8 Lite to have null in this list ....
+ if (dir!=null) {
+ dirsMap.put(dir, dir);
+ dirsPathMap.put(dir, dir);
+ }
+ }
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String dirName = file.getName();
+ dirsMap.put(dirName, dirName);
+ dirsPathMap.put(dirName, file.getPath());
+ } else {
+ final String fileName = file.getName();
+ final String fileNameLwr = fileName.toLowerCase(Locale.getDefault());
+
+ if (formatFilter != null) {
+ boolean contains = false;
+ for (String aFormatFilter : formatFilter) {
+ final String formatLwr = aFormatFilter.toLowerCase(Locale.getDefault());
+ if (fileNameLwr.endsWith(formatLwr)) {
+ contains = true;
+ break;
+ }
+ }
+ if (contains) {
+ filesMap.put(fileName, fileName);
+ filesPathMap.put(fileName, file.getPath());
+ }
+ } else {
+ filesMap.put(fileName, fileName);
+ filesPathMap.put(fileName, file.getPath());
+ }
+ }
+ }
+ item.addAll(dirsMap.tailMap("").values());
+ item.addAll(filesMap.tailMap("").values());
+ path.addAll(dirsPathMap.tailMap("").values());
+ path.addAll(filesPathMap.tailMap("").values());
+
+ SimpleAdapter fileList = new SimpleAdapter(getActivity(), mList, R.layout.file_dialog_row, new String[]{
+ ITEM_KEY, ITEM_IMAGE}, new int[]{R.id.fdrowtext, R.id.fdrowimage});
+
+ for (String dir : dirsMap.tailMap("").values()) {
+ addItem(dir, R.drawable.ic_root_folder_am);
+ }
+
+ for (String file : filesMap.tailMap("").values()) {
+ addItem(file, R.drawable.ic_doc_generic_am);
+ }
+
+ fileList.notifyDataSetChanged();
+
+ setListAdapter(fileList);
+
+ }
+
+ private void addItem(String fileName, int imageId) {
+ HashMap<String, Object> item = new HashMap<String, Object>();
+ item.put(ITEM_KEY, fileName);
+ item.put(ITEM_IMAGE, imageId);
+ mList.add(item);
+ }
+
+ private Collection<String> getExternalStorages() {
+ Vector<String> dirs = new Vector<>();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ for (File d : getActivity().getExternalFilesDirs(null))
+ dirs.add(getRootOfInnerSdCardFolder(d));
+ } else {
+ dirs.add(Environment.getExternalStorageDirectory().getAbsolutePath());
+ }
+ return dirs;
+ }
+
+ private static String getRootOfInnerSdCardFolder(File file) {
+ if (file == null)
+ return null;
+ final long totalSpace = file.getTotalSpace();
+ while (true) {
+ final File parentFile = file.getParentFile();
+ if (parentFile == null || parentFile.getTotalSpace() != totalSpace
+ || file.equals(Environment.getExternalStorageDirectory()))
+ return file.getAbsolutePath();
+ file = parentFile;
+ }
+ }
+
+
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+
+ File file = new File(path.get(position));
+
+ if (file.isDirectory()) {
+ selectButton.setEnabled(false);
+
+ if (file.canRead()) {
+ lastPositions.put(currentPath, position);
+ getDir(path.get(position));
+ } else {
+ Toast.makeText(getActivity(),
+ "[" + file.getName() + "] " + getActivity().getText(R.string.cant_read_folder),
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ selectedFile = file;
+ v.setSelected(true);
+ selectButton.setEnabled(true);
+ }
+ }
+
+ public void setNoInLine() {
+ mHideImport = true;
+
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java b/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java
new file mode 100644
index 00000000..34d37823
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java
@@ -0,0 +1,190 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+import java.io.File;
+import java.util.Collection;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+
+import de.blinkt.openvpn.BuildConfig;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.OpenSSLSpeed;
+import de.blinkt.openvpn.api.ExternalAppDatabase;
+import de.blinkt.openvpn.core.ProfileManager;
+
+
+public class GeneralSettings extends PreferenceFragment implements OnPreferenceClickListener, OnClickListener, Preference.OnPreferenceChangeListener {
+
+ private ExternalAppDatabase mExtapp;
+ private ListPreference mAlwaysOnVPN;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.general_settings);
+
+
+ PreferenceCategory devHacks = (PreferenceCategory) findPreference("device_hacks");
+ mAlwaysOnVPN = (ListPreference) findPreference("alwaysOnVpn");
+ mAlwaysOnVPN.setOnPreferenceChangeListener(this);
+
+
+ Preference loadtun = findPreference("loadTunModule");
+ if(!isTunModuleAvailable()) {
+ loadtun.setEnabled(false);
+ devHacks.removePreference(loadtun);
+ }
+
+ CheckBoxPreference cm9hack = (CheckBoxPreference) findPreference("useCM9Fix");
+ if (!cm9hack.isChecked() && (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1)) {
+ devHacks.removePreference(cm9hack);
+ }
+
+ CheckBoxPreference useInternalFS = (CheckBoxPreference) findPreference("useInternalFileSelector");
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT)
+ {
+ devHacks.removePreference(useInternalFS);
+ }
+
+ /* Android P does not allow access to the file storage anymore */
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
+ {
+ Preference useInternalFileSelector = findPreference("useInternalFileSelector");
+ devHacks.removePreference(useInternalFileSelector);
+ }
+
+ mExtapp = new ExternalAppDatabase(getActivity());
+ Preference clearapi = findPreference("clearapi");
+ clearapi.setOnPreferenceClickListener(this);
+
+ findPreference("osslspeed").setOnPreferenceClickListener(this);
+
+ if(devHacks.getPreferenceCount()==0)
+ getPreferenceScreen().removePreference(devHacks);
+
+ if (!BuildConfig.openvpn3) {
+ PreferenceCategory appBehaviour = (PreferenceCategory) findPreference("app_behaviour");
+ CheckBoxPreference ovpn3 = (CheckBoxPreference) findPreference("ovpn3");
+ ovpn3.setEnabled(false);
+ ovpn3.setChecked(false);
+ }
+
+
+ setClearApiSummary();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+
+
+
+ VpnProfile vpn = ProfileManager.getAlwaysOnVPN(getActivity());
+ StringBuffer sb = new StringBuffer(getString(R.string.defaultvpnsummary));
+ sb.append('\n');
+ if (vpn== null)
+ sb.append(getString(R.string.novpn_selected));
+ else
+ sb.append(getString(R.string.vpnselected, vpn.getName()));
+ mAlwaysOnVPN.setSummary(sb.toString());
+
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference== mAlwaysOnVPN) {
+ VpnProfile vpn = ProfileManager.get(getActivity(), (String) newValue);
+ mAlwaysOnVPN.setSummary(vpn.getName());
+ }
+ return true;
+ }
+
+ private void setClearApiSummary() {
+ Preference clearapi = findPreference("clearapi");
+
+ if(mExtapp.getExtAppList().isEmpty()) {
+ clearapi.setEnabled(false);
+ clearapi.setSummary(R.string.no_external_app_allowed);
+ } else {
+ clearapi.setEnabled(true);
+ clearapi.setSummary(getString(R.string.allowed_apps, getExtAppList(", ")));
+ }
+ }
+
+ private String getExtAppList(String delim) {
+ ApplicationInfo app;
+ PackageManager pm = getActivity().getPackageManager();
+
+ StringBuilder applist = new StringBuilder();
+ for (String packagename : mExtapp.getExtAppList()) {
+ try {
+ app = pm.getApplicationInfo(packagename, 0);
+ if (applist.length() != 0)
+ applist.append(delim);
+ applist.append(app.loadLabel(pm));
+
+ } catch (NameNotFoundException e) {
+ // App not found. Remove it from the list
+ mExtapp.removeApp(packagename);
+ }
+ }
+
+ return applist.toString();
+ }
+
+ private boolean isTunModuleAvailable() {
+ // Check if the tun module exists on the file system
+ return new File("/system/lib/modules/tun.ko").length() > 10;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if(preference.getKey().equals("clearapi")){
+ Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setPositiveButton(R.string.clear, this);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setMessage(getString(R.string.clearappsdialog,getExtAppList("\n")));
+ builder.show();
+ } else if (preference.getKey().equals("osslspeed")) {
+ startActivity(new Intent(getActivity(), OpenSSLSpeed.class));
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if( which == Dialog.BUTTON_POSITIVE){
+ mExtapp.clearAllApiApps();
+ setClearApiSummary();
+ }
+ }
+
+
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java
new file mode 100644
index 00000000..10c09461
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2012-2017 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.github.mikephil.charting.charts.LineChart;
+import com.github.mikephil.charting.components.AxisBase;
+import com.github.mikephil.charting.components.XAxis;
+import com.github.mikephil.charting.components.YAxis;
+import com.github.mikephil.charting.data.Entry;
+import com.github.mikephil.charting.data.LineData;
+import com.github.mikephil.charting.data.LineDataSet;
+import com.github.mikephil.charting.formatter.IAxisValueFormatter;
+import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.OpenVPNManagement;
+import de.blinkt.openvpn.core.TrafficHistory;
+import de.blinkt.openvpn.core.VpnStatus;
+
+import static android.content.Context.MODE_PRIVATE;
+import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount;
+import static java.lang.Math.max;
+
+/**
+ * Created by arne on 19.05.17.
+ */
+
+public class GraphFragment extends Fragment implements VpnStatus.ByteCountListener {
+ private static final String PREF_USE_LOG = "useLogGraph";
+ private ListView mListView;
+
+ private ChartDataAdapter mChartAdapter;
+ private int mColourIn;
+ private int mColourOut;
+ private int mColourPoint;
+
+ private long firstTs;
+ private TextView mSpeedStatus;
+ private boolean mLogScale;
+
+ private static final int TIME_PERIOD_SECDONS = 0;
+ private static final int TIME_PERIOD_MINUTES = 1;
+ private static final int TIME_PERIOD_HOURS = 2;
+ private Handler mHandler;
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.graph, container, false);
+ mListView = (ListView) v.findViewById(R.id.graph_listview);
+ mSpeedStatus = (TextView) v.findViewById(R.id.speedStatus);
+ CheckBox logScaleView = (CheckBox) v.findViewById(R.id.useLogScale);
+ mLogScale = getActivity().getPreferences(MODE_PRIVATE).getBoolean(PREF_USE_LOG, false);
+ logScaleView.setChecked(mLogScale);
+
+ List<Integer> charts = new LinkedList<>();
+ charts.add(TIME_PERIOD_SECDONS);
+ charts.add(TIME_PERIOD_MINUTES);
+ charts.add(TIME_PERIOD_HOURS);
+
+ mChartAdapter = new ChartDataAdapter(getActivity(), charts);
+ mListView.setAdapter(mChartAdapter);
+
+ mColourIn = getActivity().getResources().getColor(R.color.dataIn);
+ mColourOut = getActivity().getResources().getColor(R.color.dataOut);
+ mColourPoint = getActivity().getResources().getColor(android.R.color.black);
+
+ logScaleView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mLogScale = isChecked;
+ mChartAdapter.notifyDataSetChanged();
+ getActivity().getPreferences(MODE_PRIVATE).edit().putBoolean(PREF_USE_LOG, isChecked).apply();
+ }
+ });
+
+ mHandler = new Handler();
+
+ return v;
+
+
+ }
+
+ private Runnable triggerRefresh = new Runnable() {
+ @Override
+ public void run() {
+ mChartAdapter.notifyDataSetChanged();
+ mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500);
+ }
+ };
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ VpnStatus.addByteCountListener(this);
+ mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ mHandler.removeCallbacks(triggerRefresh);
+ VpnStatus.removeByteCountListener(this);
+ }
+
+ @Override
+ public void updateByteCount(long in, long out, long diffIn, long diffOut) {
+ if (firstTs == 0)
+ firstTs = System.currentTimeMillis() / 100;
+
+ long now = (System.currentTimeMillis() / 100) - firstTs;
+ int interval = OpenVPNManagement.mBytecountInterval * 10;
+ Resources res = getActivity().getResources();
+
+ final String netstat = String.format(getString(R.string.statusline_bytecount),
+ humanReadableByteCount(in, false, res),
+ humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res),
+ humanReadableByteCount(out, false, res),
+ humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res));
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mHandler.removeCallbacks(triggerRefresh);
+ mSpeedStatus.setText(netstat);
+ mChartAdapter.notifyDataSetChanged();
+ mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500);
+ }
+ });
+
+ }
+
+ private class ChartDataAdapter extends ArrayAdapter<Integer> {
+
+ private Context mContext;
+
+ public ChartDataAdapter(Context context, List<Integer> trafficData) {
+ super(context, 0, trafficData);
+ mContext = context;
+ }
+
+ @NonNull
+ @Override
+ public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ViewHolder holder = null;
+
+ if (convertView == null) {
+
+ holder = new ViewHolder();
+
+ convertView = LayoutInflater.from(mContext).inflate(
+ R.layout.graph_item, parent, false);
+ holder.chart = (LineChart) convertView.findViewById(R.id.chart);
+ holder.title = (TextView) convertView.findViewById(R.id.tvName);
+ convertView.setTag(holder);
+
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ // apply styling
+ // holder.chart.setValueTypeface(mTf);
+ holder.chart.getDescription().setEnabled(false);
+ holder.chart.setDrawGridBackground(false);
+
+ XAxis xAxis = holder.chart.getXAxis();
+ xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
+ xAxis.setDrawGridLines(false);
+ xAxis.setDrawAxisLine(true);
+
+ switch (position) {
+ case TIME_PERIOD_HOURS:
+ holder.title.setText(R.string.avghour);
+ break;
+ case TIME_PERIOD_MINUTES:
+ holder.title.setText(R.string.avgmin);
+ break;
+ default:
+ holder.title.setText(R.string.last5minutes);
+ break;
+ }
+
+ xAxis.setValueFormatter(new IAxisValueFormatter() {
+
+
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ switch (position) {
+ case TIME_PERIOD_HOURS:
+ return String.format(Locale.getDefault(), "%.0f\u2009h ago", (axis.getAxisMaximum() - value) / 10 / 3600);
+ case TIME_PERIOD_MINUTES:
+ return String.format(Locale.getDefault(), "%.0f\u2009m ago", (axis.getAxisMaximum() - value) / 10 / 60);
+ default:
+ return String.format(Locale.getDefault(), "%.0f\u2009s ago", (axis.getAxisMaximum() - value) / 10);
+ }
+
+ }
+ });
+ xAxis.setLabelCount(5);
+
+ YAxis yAxis = holder.chart.getAxisLeft();
+ yAxis.setLabelCount(5, false);
+
+ final Resources res = getActivity().getResources();
+ yAxis.setValueFormatter(new IAxisValueFormatter() {
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ if (mLogScale && value < 2.1f)
+ return "< 100\u2009bit/s";
+ if (mLogScale)
+ value = (float) Math.pow(10, value) / 8;
+
+ return humanReadableByteCount((long) value, true, res);
+ }
+ });
+
+ holder.chart.getAxisRight().setEnabled(false);
+
+ LineData data = getDataSet(position);
+ float ymax = data.getYMax();
+
+ if (mLogScale) {
+ yAxis.setAxisMinimum(2f);
+ yAxis.setAxisMaximum((float) Math.ceil(ymax));
+ yAxis.setLabelCount((int) (Math.ceil(ymax - 2f)));
+ } else {
+ yAxis.setAxisMinimum(0f);
+ yAxis.resetAxisMaximum();
+ yAxis.setLabelCount(6);
+ }
+
+ if (data.getDataSetByIndex(0).getEntryCount() < 3)
+ holder.chart.setData(null);
+ else
+ holder.chart.setData(data);
+
+ holder.chart.setNoDataText(getString(R.string.notenoughdata));
+
+ holder.chart.invalidate();
+ //holder.chart.animateX(750);
+
+ return convertView;
+ }
+
+ private LineData getDataSet(int timeperiod) {
+
+ LinkedList<Entry> dataIn = new LinkedList<>();
+ LinkedList<Entry> dataOut = new LinkedList<>();
+
+ long interval;
+ long totalInterval;
+
+ LinkedList<TrafficHistory.TrafficDatapoint> list;
+ switch (timeperiod) {
+ case TIME_PERIOD_HOURS:
+ list = VpnStatus.trafficHistory.getHours();
+ interval = TrafficHistory.TIME_PERIOD_HOURS;
+ totalInterval = 0;
+ break;
+ case TIME_PERIOD_MINUTES:
+ list = VpnStatus.trafficHistory.getMinutes();
+ interval = TrafficHistory.TIME_PERIOD_MINTUES;
+ totalInterval = TrafficHistory.TIME_PERIOD_HOURS * TrafficHistory.PERIODS_TO_KEEP;
+ ;
+
+ break;
+ default:
+ list = VpnStatus.trafficHistory.getSeconds();
+ interval = OpenVPNManagement.mBytecountInterval * 1000;
+ totalInterval = TrafficHistory.TIME_PERIOD_MINTUES * TrafficHistory.PERIODS_TO_KEEP;
+ break;
+ }
+ if (list.size() == 0) {
+ list = TrafficHistory.getDummyList();
+ }
+
+
+ long lastts = 0;
+ float zeroValue;
+ if (mLogScale)
+ zeroValue = 2;
+ else
+ zeroValue = 0;
+
+ long now = System.currentTimeMillis();
+
+
+ long firstTimestamp = 0;
+ long lastBytecountOut = 0;
+ long lastBytecountIn = 0;
+
+ for (TrafficHistory.TrafficDatapoint tdp : list) {
+ if (totalInterval != 0 && (now - tdp.timestamp) > totalInterval)
+ continue;
+
+ if (firstTimestamp == 0) {
+ firstTimestamp = list.peek().timestamp;
+ lastBytecountIn = list.peek().in;
+ lastBytecountOut = list.peek().out;
+ }
+
+ float t = (tdp.timestamp - firstTimestamp) / 100f;
+
+ float in = (tdp.in - lastBytecountIn) / (float) (interval / 1000);
+ float out = (tdp.out - lastBytecountOut) / (float) (interval / 1000);
+
+ lastBytecountIn = tdp.in;
+ lastBytecountOut = tdp.out;
+
+ if (mLogScale) {
+ in = max(2f, (float) Math.log10(in * 8));
+ out = max(2f, (float) Math.log10(out * 8));
+ }
+
+ if (lastts > 0 && (tdp.timestamp - lastts > 2 * interval)) {
+ dataIn.add(new Entry((lastts - firstTimestamp + interval) / 100f, zeroValue));
+ dataOut.add(new Entry((lastts - firstTimestamp + interval) / 100f, zeroValue));
+
+ dataIn.add(new Entry(t - interval / 100f, zeroValue));
+ dataOut.add(new Entry(t - interval / 100f, zeroValue));
+ }
+
+ lastts = tdp.timestamp;
+
+ dataIn.add(new Entry(t, in));
+ dataOut.add(new Entry(t, out));
+
+ }
+ if (lastts < now - interval) {
+
+ if (now - lastts > 2 * interval * 1000) {
+ dataIn.add(new Entry((lastts - firstTimestamp + interval * 1000) / 100f, zeroValue));
+ dataOut.add(new Entry((lastts - firstTimestamp + interval * 1000) / 100f, zeroValue));
+ }
+
+ dataIn.add(new Entry((now - firstTimestamp) / 100, zeroValue));
+ dataOut.add(new Entry((now - firstTimestamp) / 100, zeroValue));
+ }
+
+ ArrayList<ILineDataSet> dataSets = new ArrayList<>();
+
+
+ LineDataSet indata = new LineDataSet(dataIn, getString(R.string.data_in));
+ LineDataSet outdata = new LineDataSet(dataOut, getString(R.string.data_out));
+
+ setLineDataAttributes(indata, mColourIn);
+ setLineDataAttributes(outdata, mColourOut);
+
+ dataSets.add(indata);
+ dataSets.add(outdata);
+
+ return new LineData(dataSets);
+ }
+
+ private void setLineDataAttributes(LineDataSet dataSet, int colour) {
+ dataSet.setLineWidth(2);
+ dataSet.setCircleRadius(1);
+ dataSet.setDrawCircles(true);
+ dataSet.setCircleColor(mColourPoint);
+ dataSet.setDrawFilled(true);
+ dataSet.setFillAlpha(42);
+ dataSet.setFillColor(colour);
+ dataSet.setColor(colour);
+ dataSet.setMode(LineDataSet.Mode.LINEAR);
+
+ dataSet.setDrawValues(false);
+ }
+ }
+
+ private static class ViewHolder {
+ LineChart chart;
+ TextView title;
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java b/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java
new file mode 100644
index 00000000..41206a54
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java
@@ -0,0 +1,70 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.os.Bundle;
+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.EditText;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.R;
+
+public class InlineFileTab extends Fragment
+{
+
+ private static final int MENU_SAVE = 0;
+ private EditText mInlineData;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mInlineData.setText(((FileSelect)getActivity()).getInlineData());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState)
+ {
+
+ View v = inflater.inflate(R.layout.file_dialog_inline, container, false);
+ mInlineData =(EditText) v.findViewById(R.id.inlineFileData);
+ return v;
+ }
+
+ public void setData(String data) {
+ if(mInlineData!=null)
+ mInlineData.setText(data);
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.add(0, MENU_SAVE, 0, R.string.menu_use_inline_data)
+ .setIcon(android.R.drawable.ic_menu_save)
+ .setAlphabeticShortcut('u')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==MENU_SAVE){
+ ((FileSelect)getActivity()).saveInlineData(null, mInlineData.getText().toString());
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
new file mode 100644
index 00000000..323b3a4d
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2012-2018 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Message
+import android.security.KeyChain
+import android.security.KeyChainException
+import android.security.keystore.KeyInfo
+import android.text.TextUtils
+import android.view.View
+import android.widget.AdapterView
+import android.widget.Spinner
+import android.widget.TextView
+import android.widget.Toast
+import de.blinkt.openvpn.R
+import de.blinkt.openvpn.VpnProfile
+import de.blinkt.openvpn.api.ExternalCertificateProvider
+import de.blinkt.openvpn.core.ExtAuthHelper
+import de.blinkt.openvpn.core.X509Utils
+import java.security.KeyFactory
+import java.security.PrivateKey
+
+import java.security.cert.X509Certificate
+
+internal abstract class KeyChainSettingsFragment : Settings_Fragment(), View.OnClickListener, Handler.Callback {
+
+
+ private lateinit var mAliasCertificate: TextView
+ private lateinit var mAliasName: TextView
+ private var mHandler: Handler? = null
+ private lateinit var mExtAliasName: TextView
+ private lateinit var mExtAuthSpinner: Spinner
+
+ private val isInHardwareKeystore: Boolean
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ @Throws(KeyChainException::class, InterruptedException::class)
+ get() {
+ val key : PrivateKey = KeyChain.getPrivateKey(activity.applicationContext, mProfile.mAlias) ?: return false
+
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
+ {
+ val keyFactory = KeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore")
+ val keyInfo = keyFactory.getKeySpec(key, KeyInfo::class.java)
+ return keyInfo.isInsideSecureHardware()
+
+ } else {
+ val algorithm = key.algorithm
+ return KeyChain.isBoundKeyAlgorithm(algorithm)
+ }
+ }
+
+
+ private fun setKeyStoreAlias() {
+ if (mProfile.mAlias == null) {
+ mAliasName.setText(R.string.client_no_certificate)
+ mAliasCertificate.text = ""
+ } else {
+ mAliasCertificate.text = "Loading certificate from Keystore..."
+ mAliasName.text = mProfile.mAlias
+ setCertificate(false)
+ }
+ }
+
+ private fun setExtAlias() {
+ if (mProfile.mAlias == null) {
+ mExtAliasName.setText(R.string.extauth_not_configured)
+ mAliasCertificate.text = ""
+ } else {
+ mAliasCertificate.text = "Querying certificate from external provider..."
+ mExtAliasName.text = ""
+ setCertificate(true)
+ }
+ }
+
+ private fun fetchExtCertificateMetaData() {
+ object : Thread() {
+ override fun run() {
+ try {
+ val b = ExtAuthHelper.getCertificateMetaData(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)
+ mProfile.mAlias = b.getString(ExtAuthHelper.EXTRA_ALIAS)
+ activity.runOnUiThread { setAlias() }
+ } catch (e: KeyChainException) {
+ e.printStackTrace()
+ }
+
+ }
+ }.start()
+ }
+
+
+ protected fun setCertificate(external: Boolean) {
+ object : Thread() {
+ override fun run() {
+ var certstr = ""
+ var metadata: Bundle? = null
+ try {
+ val cert: X509Certificate?
+
+ if (external) {
+ if (!TextUtils.isEmpty(mProfile.mExternalAuthenticator) && !TextUtils.isEmpty(mProfile.mAlias)) {
+ cert = ExtAuthHelper.getCertificateChain(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)!![0]
+ metadata = ExtAuthHelper.getCertificateMetaData(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)
+ } else {
+ cert = null
+ certstr = getString(R.string.extauth_not_configured)
+ }
+ } else {
+ cert = KeyChain.getCertificateChain(activity.applicationContext, mProfile.mAlias)!![0]
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ run {
+ if (isInHardwareKeystore)
+ certstr += getString(R.string.hwkeychain)
+ }
+ }
+ }
+ if (cert != null) {
+ certstr += X509Utils.getCertificateValidityString(cert, resources)
+ certstr += X509Utils.getCertificateFriendlyName(cert)
+ }
+
+
+ } catch (e: Exception) {
+ certstr = "Could not get certificate from Keystore: " + e.localizedMessage!!
+ }
+
+ val certStringCopy = certstr
+ val finalMetadata = metadata
+ activity.runOnUiThread {
+ mAliasCertificate.text = certStringCopy
+ if (finalMetadata != null)
+ mExtAliasName.text = finalMetadata.getString(ExtAuthHelper.EXTRA_DESCRIPTION)
+
+ }
+
+ }
+ }.start()
+ }
+
+ protected fun initKeychainViews(v: View) {
+ v.findViewById<View>(R.id.select_keystore_button).setOnClickListener(this)
+ v.findViewById<View>(R.id.configure_extauth_button)?.setOnClickListener(this)
+ v.findViewById<View>(R.id.install_keystore_button).setOnClickListener(this)
+ mAliasCertificate = v.findViewById(R.id.alias_certificate)
+ mExtAuthSpinner = v.findViewById(R.id.extauth_spinner)
+ mExtAuthSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
+ val selectedProvider = parent.getItemAtPosition(position) as ExtAuthHelper.ExternalAuthProvider
+ if (selectedProvider.packageName != mProfile.mExternalAuthenticator) {
+ mProfile.mAlias = ""
+ }
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>) {
+
+ }
+ }
+ mExtAliasName = v.findViewById(R.id.extauth_detail)
+ mAliasName = v.findViewById(R.id.aliasname)
+ if (mHandler == null) {
+ mHandler = Handler(this)
+ }
+ ExtAuthHelper.setExternalAuthProviderSpinnerList(mExtAuthSpinner, mProfile.mExternalAuthenticator)
+
+ v.findViewById<View>(R.id.install_keystore_button).setOnClickListener {
+ startActivity(KeyChain.createInstallIntent()) };
+ }
+
+ override fun onClick(v: View) {
+ if (v === v.findViewById<View>(R.id.select_keystore_button)) {
+ showCertDialog()
+ } else if (v === v.findViewById<View>(R.id.configure_extauth_button)) {
+ startExternalAuthConfig()
+ }
+ }
+
+ private fun startExternalAuthConfig() {
+ val eAuth = mExtAuthSpinner.selectedItem as ExtAuthHelper.ExternalAuthProvider
+ mProfile.mExternalAuthenticator = eAuth.packageName
+ if (!eAuth.configurable) {
+ fetchExtCertificateMetaData()
+ return
+ }
+ val extauth = Intent(ExtAuthHelper.ACTION_CERT_CONFIGURATION)
+ extauth.setPackage(eAuth.packageName)
+ extauth.putExtra(ExtAuthHelper.EXTRA_ALIAS, mProfile.mAlias)
+ startActivityForResult(extauth, UPDATEE_EXT_ALIAS)
+ }
+
+ override fun savePreferences() {
+
+ }
+
+ override fun onStart() {
+ super.onStart()
+ loadPreferences()
+ }
+
+ fun showCertDialog() {
+ try {
+ KeyChain.choosePrivateKeyAlias(activity,
+ { alias ->
+ // Credential alias selected. Remember the alias selection for future use.
+ mProfile.mAlias = alias
+ mHandler!!.sendEmptyMessage(UPDATE_ALIAS)
+ },
+ arrayOf("RSA", "EC"), null, // issuer, null for any
+ mProfile.mServerName, // host name of server requesting the cert, null if unavailable
+ -1, // port of server requesting the cert, -1 if unavailable
+ mProfile.mAlias)// List of acceptable key types. null for any
+ // alias to preselect, null if unavailable
+ } catch (anf: ActivityNotFoundException) {
+ val ab = AlertDialog.Builder(activity)
+ ab.setTitle(R.string.broken_image_cert_title)
+ ab.setMessage(R.string.broken_image_cert)
+ ab.setPositiveButton(android.R.string.ok, null)
+ ab.show()
+ }
+
+ }
+
+ protected open fun loadPreferences() {
+ setAlias()
+
+ }
+
+ private fun setAlias() {
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_EXTERNAL_APP)
+ setExtAlias()
+ else
+ setKeyStoreAlias()
+ }
+
+ override fun handleMessage(msg: Message): Boolean {
+ setAlias()
+ return true
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ if (requestCode == UPDATEE_EXT_ALIAS && resultCode == Activity.RESULT_OK) {
+ mProfile.mAlias = data.getStringExtra(ExtAuthHelper.EXTRA_ALIAS)
+ mExtAliasName.text = data.getStringExtra(ExtAuthHelper.EXTRA_DESCRIPTION)
+ }
+ }
+
+ companion object {
+ private val UPDATE_ALIAS = 20
+ private val UPDATEE_EXT_ALIAS = 210
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
new file mode 100644
index 00000000..e64ce2cd
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
@@ -0,0 +1,694 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.DataSetObserver;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.text.SpannableString;
+import android.text.format.DateFormat;
+import android.text.style.ImageSpan;
+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.CompoundButton;
+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.LaunchVPN;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.DisconnectVPN;
+import de.blinkt.openvpn.activities.MainActivity;
+import de.blinkt.openvpn.activities.VPNPreferences;
+import de.blinkt.openvpn.core.ConnectionStatus;
+import de.blinkt.openvpn.core.OpenVPNManagement;
+import de.blinkt.openvpn.core.OpenVPNService;
+import de.blinkt.openvpn.core.Preferences;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.LogItem;
+import de.blinkt.openvpn.core.VpnStatus.LogListener;
+import de.blinkt.openvpn.core.VpnStatus.StateListener;
+
+import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount;
+
+public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener {
+ private static final String LOGTIMEFORMAT = "logtimeformat";
+ private static final int START_VPN_CONFIG = 0;
+ 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);
+
+ //t.setSpan(getSpanImage(le,(int)v.getTextSize()),spanStart,spanStart+1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ 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 "";
+ }
+
+ }
+
+ private ImageSpan getSpanImage(LogItem li, int imageSize) {
+ int imageRes = android.R.drawable.ic_menu_call;
+
+ switch (li.getLogLevel()) {
+ case ERROR:
+ imageRes = android.R.drawable.ic_notification_clear_all;
+ break;
+ case INFO:
+ imageRes = android.R.drawable.ic_menu_compass;
+ break;
+ case VERBOSE:
+ imageRes = android.R.drawable.ic_menu_info_details;
+ break;
+ case WARNING:
+ imageRes = android.R.drawable.ic_menu_camera;
+ break;
+ }
+
+ Drawable d = getResources().getDrawable(imageRes);
+
+
+ //d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ d.setBounds(0, 0, imageSize, imageSize);
+ ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM);
+
+ return span;
+ }
+
+ @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.cancel) {
+ Intent intent = new Intent(getActivity(), DisconnectVPN.class);
+ startActivity(intent);
+ return true;
+ } else if (item.getItemId() == R.id.send) {
+ ladapter.shareLog();
+ } else if (item.getItemId() == R.id.edit_vpn) {
+ VpnProfile lastConnectedprofile = ProfileManager.get(getActivity(), VpnStatus.getLastConnectedVPNProfile());
+
+ if (lastConnectedprofile != null) {
+ Intent vprefintent = new Intent(getActivity(), VPNPreferences.class)
+ .putExtra(VpnProfile.EXTRA_PROFILEUUID, lastConnectedprofile.getUUIDString());
+ startActivityForResult(vprefintent, START_VPN_CONFIG);
+ } else {
+ Toast.makeText(getActivity(), R.string.log_no_last_vpn, Toast.LENGTH_LONG).show();
+ }
+ } else if (item.getItemId() == R.id.toggle_time) {
+ showHideOptionsPanel();
+ } else if (item.getItemId() == android.R.id.home) {
+ // This is called when the Home (Up) button is pressed
+ // in the Action Bar.
+ Intent parentActivityIntent = new Intent(getActivity(), MainActivity.class);
+ parentActivityIntent.addFlags(
+ Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(parentActivityIntent);
+ getActivity().finish();
+ return true;
+
+ }
+ 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.logmenu, 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 onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == START_VPN_CONFIG && resultCode == Activity.RESULT_OK) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ final VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN);
+ ProfileManager.getInstance(getActivity()).saveProfile(getActivity(), profile);
+ // Name could be modified, reset List adapter
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity());
+ dialog.setTitle(R.string.configuration_changed);
+ dialog.setMessage(R.string.restart_vpn_after_change);
+
+
+ dialog.setPositiveButton(R.string.restart,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(getActivity(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+
+
+ });
+ dialog.setNegativeButton(R.string.ignore, null);
+ dialog.create().show();
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+
+ @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.log_fragment, 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 = (RadioGroup) 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 = (CheckBox) v.findViewById(R.id.clearlogconnect);
+ mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(LaunchVPN.CLEARLOG, true));
+ mClearLogCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(LaunchVPN.CLEARLOG, isChecked).apply();
+ }
+ });
+
+ mSpeedView = (TextView) v.findViewById(R.id.speed);
+
+ mOptionsLayout = (LinearLayout) v.findViewById(R.id.logOptionsLayout);
+ mLogLevelSlider = (SeekBar) 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 = (TextView) v.findViewById(R.id.speedUp);
+ mDownStatus = (TextView) v.findViewById(R.id.speedDown);
+ mConnectStatus = (TextView) 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 activity) {
+ super.onAttach(activity);
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) {
+ mShowOptionsLayout = true;
+ if (mOptionsLayout != null)
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ //getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ }
+
+
+ @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(new Runnable() {
+
+ @Override
+ public void run() {
+ 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/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java
new file mode 100644
index 00000000..9ac8bebb
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java
@@ -0,0 +1,53 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+public abstract class OpenVpnPreferencesFragment extends PreferenceFragment {
+
+ protected VpnProfile mProfile;
+
+ protected abstract void loadSettings();
+ protected abstract void saveSettings();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile = ProfileManager.get(getActivity(),profileUUID);
+ getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ saveSettings();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if(savedInstanceState!=null) {
+ String profileUUID=savedInstanceState.getString(VpnProfile.EXTRA_PROFILEUUID);
+ mProfile = ProfileManager.get(getActivity(),profileUUID);
+ loadSettings();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveSettings();
+ outState.putString(VpnProfile.EXTRA_PROFILEUUID, mProfile.getUUIDString());
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java
new file mode 100644
index 00000000..0fe40905
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Date;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.VpnStatus;
+
+public class SendDumpFragment extends Fragment {
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ final View v = inflater.inflate(R.layout.fragment_senddump, container, false);
+ v.findViewById(R.id.senddump).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ emailMiniDumps();
+ }
+ });
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ final Pair<File, Long> ldump = getLastestDump(getActivity());
+ if (ldump==null)
+ return;
+ // Do in background since it does I/O
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TextView dumpDateText = (TextView) v.findViewById(R.id.dumpdate);
+ String datestr = (new Date(ldump.second)).toString();
+ long timediff = System.currentTimeMillis() - ldump.second;
+ long minutes = timediff / 1000 / 60 % 60;
+ long hours = timediff / 1000 / 60 / 60;
+ dumpDateText.setText(getString(R.string.lastdumpdate, hours, minutes, datestr));
+
+ }
+ });
+ }
+ }).start();
+ return v;
+ }
+
+ public void emailMiniDumps()
+ {
+ //need to "send multiple" to get more than one attachment
+ final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);
+ emailIntent.setType("*/*");
+ emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
+ new String[]{"Arne Schwabe <arne@rfc2549.org>"});
+
+ String version;
+ String name="ics-openvpn";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = packageinfo.applicationInfo.name;
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format("%s(%s) %s Minidump",name, getActivity().getPackageName(), version));
+
+ emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe the issue you have experienced");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+
+ Pair<File, Long> ldump = getLastestDump(getActivity());
+ if(ldump==null) {
+ VpnStatus.logError("No Minidump found!");
+ }
+
+ uris.add(Uri.parse("content://de.blinkt.openvpn.FileProvider/" + ldump.first.getName()));
+ uris.add(Uri.parse("content://de.blinkt.openvpn.FileProvider/" + ldump.first.getName() + ".log"));
+
+ emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ startActivity(emailIntent);
+ }
+
+ static public Pair<File,Long> getLastestDump(Context c) {
+ long newestDumpTime=0;
+ File newestDumpFile=null;
+
+ if (c.getCacheDir() ==null)
+ return null;
+
+ for(File f:c.getCacheDir().listFiles()) {
+ if(!f.getName().endsWith(".dmp"))
+ continue;
+
+ if (newestDumpTime < f.lastModified()) {
+ newestDumpTime = f.lastModified();
+ newestDumpFile=f;
+ }
+ }
+ // Ignore old dumps
+ if(System.currentTimeMillis() - 48 * 60 * 1000 > newestDumpTime )
+ return null;
+
+ return Pair.create(newestDumpFile, newestDumpTime);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt
new file mode 100644
index 00000000..dd2aa3b7
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt
@@ -0,0 +1,323 @@
+/*
+ * 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 de.blinkt.openvpn.fragments
+
+import android.Manifest
+import android.app.Activity
+import android.app.Fragment
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Bundle
+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.Switch
+import android.widget.TextView
+
+import java.util.Collections
+import java.util.Locale
+import java.util.Vector
+
+import de.blinkt.openvpn.R
+import de.blinkt.openvpn.VpnProfile
+import de.blinkt.openvpn.core.ProfileManager
+
+/**
+ * Created by arne on 16.11.14.
+ */
+class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener {
+ private lateinit var mListView: ListView
+ private lateinit var mProfile: VpnProfile
+ private lateinit var mDefaultAllowTextView: TextView
+ private lateinit var mListAdapter: PackageAdapter
+ private lateinit var mSettingsView: View
+
+
+ override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
+ val avh = view.tag as AppViewHolder
+ avh.checkBox.toggle()
+ }
+
+ override fun onClick(v: View) {
+
+ }
+
+ override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
+ val packageName = buttonView.tag as String
+ if (isChecked) {
+ mProfile.mAllowedAppsVpn.add(packageName)
+ } else {
+ mProfile.mAllowedAppsVpn.remove(packageName)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ changeDisallowText(mProfile.mAllowedAppsVpnAreDisallowed)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val profileUuid = arguments.getString(activity.packageName + ".profileUUID")
+ mProfile = ProfileManager.get(activity, profileUuid)
+ activity.title = getString(R.string.edit_profile_title, mProfile.name)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.allowed_apps, menu)
+
+ val searchView = menu.findItem(R.id.app_search_widget).actionView as SearchView
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ mListView.setFilterText(query)
+ mListView.isTextFilterEnabled = true
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ mListView.setFilterText(newText)
+ mListView.isTextFilterEnabled = !TextUtils.isEmpty(newText)
+
+ return true
+ }
+ })
+ searchView.setOnCloseListener {
+ mListView.clearTextFilter()
+ mListAdapter.filter.filter("")
+ false
+ }
+
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val v = inflater.inflate(R.layout.allowed_vpn_apps, container, false)
+
+ mSettingsView = inflater.inflate(R.layout.allowed_application_settings, container, false)
+ mDefaultAllowTextView = mSettingsView.findViewById<View>(R.id.default_allow_text) as TextView
+
+ val vpnOnDefaultSwitch = mSettingsView.findViewById<View>(R.id.default_allow) as Switch
+
+ vpnOnDefaultSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
+ changeDisallowText(isChecked)
+ mProfile.mAllowedAppsVpnAreDisallowed = isChecked
+ }
+
+ vpnOnDefaultSwitch.isChecked = mProfile.mAllowedAppsVpnAreDisallowed
+
+ val vpnAllowBypassSwitch = mSettingsView.findViewById<View>(R.id.allow_bypass) as Switch
+
+ vpnAllowBypassSwitch.setOnCheckedChangeListener { buttonView, isChecked -> mProfile.mAllowAppVpnBypass = isChecked }
+
+ vpnAllowBypassSwitch.isChecked = mProfile.mAllowAppVpnBypass
+
+ mListView = v.findViewById<View>(android.R.id.list) as ListView
+
+ mListAdapter = PackageAdapter(activity, mProfile)
+ mListView.adapter = mListAdapter
+ mListView.onItemClickListener = this
+
+ mListView.emptyView = v.findViewById(R.id.loading_container)
+
+ Thread(Runnable { mListAdapter.populateList(activity) }).start()
+
+ return v
+ }
+
+ private fun changeDisallowText(selectedAreDisallowed: Boolean) {
+ if (selectedAreDisallowed)
+ mDefaultAllowTextView.setText(R.string.vpn_disallow_radio)
+ else
+ mDefaultAllowTextView.setText(R.string.vpn_allow_radio)
+ }
+
+ internal class AppViewHolder {
+ var mInfo: ApplicationInfo? = null
+ var rootView: View? = null
+ lateinit var appName: TextView
+ lateinit var appIcon: ImageView
+ //public TextView appSize;
+ //public TextView disabled;
+ lateinit var checkBox: CompoundButton
+
+ companion object {
+
+ fun createOrRecycle(inflater: LayoutInflater, oldview: View?, parent: ViewGroup): AppViewHolder {
+ var convertView = oldview
+ 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.
+ val holder = AppViewHolder()
+ holder.rootView = convertView
+ holder.appName = convertView.findViewById<View>(R.id.app_name) as TextView
+ holder.appIcon = convertView.findViewById<View>(R.id.app_icon) as ImageView
+ //holder.appSize = (TextView) convertView.findViewById(R.id.app_size);
+ //holder.disabled = (TextView) convertView.findViewById(R.id.app_disabled);
+ holder.checkBox = convertView.findViewById<View>(R.id.app_selected) as CompoundButton
+ convertView.tag = holder
+
+
+ return holder
+ } else {
+ // Get the ViewHolder back to get fast access to the TextView
+ // and the ImageView.
+ return convertView.tag as AppViewHolder
+ }
+ }
+ }
+
+ }
+
+ internal inner class PackageAdapter(c: Context, vp: VpnProfile) : BaseAdapter(), Filterable {
+ private val mInflater: LayoutInflater = LayoutInflater.from(c)
+ private val mPm: PackageManager = c.packageManager
+ private var mPackages: Vector<ApplicationInfo> = Vector()
+ private val mFilter = ItemFilter()
+ private var mFilteredData: Vector<ApplicationInfo> = mPackages
+ private val mProfile = vp
+
+
+ fun populateList(c: Activity) {
+ val installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA)
+
+ // Remove apps not using Internet
+
+ var androidSystemUid = 0
+ val apps = Vector<ApplicationInfo>()
+
+ try {
+ val system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA)
+ androidSystemUid = system.uid
+ apps.add(system)
+ } catch (e: PackageManager.NameNotFoundException) {
+ }
+
+
+ for (app in installedPackages) {
+
+ if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && app.uid != androidSystemUid) {
+
+ apps.add(app)
+ }
+ }
+
+ Collections.sort(apps, ApplicationInfo.DisplayNameComparator(mPm))
+ mPackages = apps
+ mFilteredData = apps
+ c.runOnUiThread { notifyDataSetChanged() }
+ }
+
+ override fun getCount(): Int {
+ return mFilteredData.size + 1
+ }
+
+ override fun getItem(position: Int): Any {
+ return mFilteredData[position - 1]
+ }
+
+ override fun getItemId(position: Int): Long {
+ if (position == 0)
+ return "settings".hashCode().toLong()
+ return mFilteredData[position - 1].packageName.hashCode().toLong()
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
+ if (position == 0) {
+ return mSettingsView
+ } else
+ return getViewApp(position - 1, convertView, parent)
+
+ }
+
+ fun getViewApp(position: Int, convertView: View?, parent: ViewGroup): View? {
+ val viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent)
+ viewHolder.mInfo = mFilteredData[position]
+ val mInfo = mFilteredData[position]
+
+
+ var appName = mInfo.loadLabel(mPm)
+
+ if (TextUtils.isEmpty(appName))
+ appName = mInfo.packageName
+ viewHolder.appName.text = appName
+ viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm))
+ viewHolder.checkBox.tag = mInfo.packageName
+ viewHolder.checkBox.setOnCheckedChangeListener(this@Settings_Allowed_Apps)
+
+
+ viewHolder.checkBox.isChecked = mProfile.mAllowedAppsVpn.contains(mInfo.packageName)
+ return viewHolder.rootView
+ }
+
+ override fun getFilter(): Filter {
+ return mFilter
+ }
+
+ override fun getViewTypeCount(): Int {
+ return 2;
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == 0) 0 else 1
+ }
+
+ private inner class ItemFilter : Filter() {
+ override fun performFiltering(constraint: CharSequence): Filter.FilterResults {
+
+ val filterString = constraint.toString().toLowerCase(Locale.getDefault())
+
+ val results = Filter.FilterResults()
+
+
+ val count = mPackages.size
+ val nlist = Vector<ApplicationInfo>(count)
+
+ for (i in 0 until count) {
+ val pInfo = mPackages[i]
+ var appName = pInfo.loadLabel(mPm)
+
+ if (TextUtils.isEmpty(appName))
+ appName = pInfo.packageName
+
+ if (appName is String) {
+ if (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 fun publishResults(constraint: CharSequence, results: Filter.FilterResults) {
+ mFilteredData = results.values as Vector<ApplicationInfo>
+ notifyDataSetChanged()
+ }
+
+ }
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java
new file mode 100644
index 00000000..8fd6aa98
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java
@@ -0,0 +1,238 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
+import android.util.Pair;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.views.RemoteCNPreference;
+import de.blinkt.openvpn.VpnProfile;
+
+import java.io.IOException;
+
+
+public class Settings_Authentication extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener, OnPreferenceClickListener {
+ private static final int SELECT_TLS_FILE_LEGACY_DIALOG = 23223232;
+ private static final int SELECT_TLS_FILE_KITKAT = SELECT_TLS_FILE_LEGACY_DIALOG +1;
+ private CheckBoxPreference mExpectTLSCert;
+ private CheckBoxPreference mCheckRemoteCN;
+ private RemoteCNPreference mRemoteCN;
+ private ListPreference mTLSAuthDirection;
+ private Preference mTLSAuthFile;
+ private SwitchPreference mUseTLSAuth;
+ private EditTextPreference mCipher;
+ private String mTlsAuthFileData;
+ private EditTextPreference mAuth;
+ private EditTextPreference mRemoteX509Name;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_authentification);
+
+ mExpectTLSCert = (CheckBoxPreference) findPreference("remoteServerTLS");
+ mCheckRemoteCN = (CheckBoxPreference) findPreference("checkRemoteCN");
+ mRemoteCN = (RemoteCNPreference) findPreference("remotecn");
+ mRemoteCN.setOnPreferenceChangeListener(this);
+
+ mRemoteX509Name = (EditTextPreference) findPreference("remotex509name");
+ mRemoteX509Name.setOnPreferenceChangeListener(this);
+
+ mUseTLSAuth = (SwitchPreference) findPreference("useTLSAuth" );
+ mTLSAuthFile = findPreference("tlsAuthFile");
+ mTLSAuthDirection = (ListPreference) findPreference("tls_direction");
+
+
+ mTLSAuthFile.setOnPreferenceClickListener(this);
+
+ mCipher =(EditTextPreference) findPreference("cipher");
+ mCipher.setOnPreferenceChangeListener(this);
+
+ mAuth =(EditTextPreference) findPreference("auth");
+ mAuth.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mExpectTLSCert.setChecked(mProfile.mExpectTLSCert);
+ mCheckRemoteCN.setChecked(mProfile.mCheckRemoteCN);
+ mRemoteCN.setDN(mProfile.mRemoteCN);
+ mRemoteCN.setAuthType(mProfile.mX509AuthType);
+ onPreferenceChange(mRemoteCN,
+ new Pair<Integer, String>(mProfile.mX509AuthType, mProfile.mRemoteCN));
+
+ mRemoteX509Name.setText(mProfile.mx509UsernameField);
+ onPreferenceChange(mRemoteX509Name, mProfile.mx509UsernameField);
+
+ mUseTLSAuth.setChecked(mProfile.mUseTLSAuth);
+ mTlsAuthFileData= mProfile.mTLSAuthFilename;
+ setTlsAuthSummary(mTlsAuthFileData);
+ mTLSAuthDirection.setValue(mProfile.mTLSAuthDirection);
+ mCipher.setText(mProfile.mCipher);
+ onPreferenceChange(mCipher, mProfile.mCipher);
+ mAuth.setText(mProfile.mAuth);
+ onPreferenceChange(mAuth, mProfile.mAuth);
+
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_STATICKEYS) {
+ mExpectTLSCert.setEnabled(false);
+ mCheckRemoteCN.setEnabled(false);
+ mUseTLSAuth.setChecked(true);
+ } else {
+ mExpectTLSCert.setEnabled(true);
+ mCheckRemoteCN.setEnabled(true);
+
+ }
+ }
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mExpectTLSCert=mExpectTLSCert.isChecked();
+ mProfile.mCheckRemoteCN=mCheckRemoteCN.isChecked();
+ mProfile.mRemoteCN=mRemoteCN.getCNText();
+ mProfile.mX509AuthType=mRemoteCN.getAuthtype();
+
+ mProfile.mUseTLSAuth = mUseTLSAuth.isChecked();
+ mProfile.mTLSAuthFilename = mTlsAuthFileData;
+ mProfile.mx509UsernameField = mRemoteX509Name.getText();
+
+ if(mTLSAuthDirection.getValue()==null)
+ mProfile.mTLSAuthDirection=null;
+ else
+ mProfile.mTLSAuthDirection = mTLSAuthDirection.getValue();
+
+ if(mCipher.getText()==null)
+ mProfile.mCipher=null;
+ else
+ mProfile.mCipher = mCipher.getText();
+
+ if(mAuth.getText()==null)
+ mProfile.mAuth = null;
+ else
+ mProfile.mAuth = mAuth.getText();
+
+ }
+
+
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(preference==mRemoteCN) {
+ @SuppressWarnings("unchecked")
+ int authtype = ((Pair<Integer, String>) newValue).first;
+ @SuppressWarnings("unchecked")
+ String dn = ((Pair<Integer, String>) newValue).second;
+
+ if ("".equals(dn)) {
+ if (mProfile.mConnections.length > 0) {
+ preference.setSummary(getX509String(VpnProfile.X509_VERIFY_TLSREMOTE_RDN, mProfile.mConnections[0].mServerName));
+ } else {
+ preference.setSummary(R.string.no_remote_defined);
+ }
+ } else {
+ preference.setSummary(getX509String(authtype, dn));
+ }
+
+ } else if (preference == mCipher || preference == mAuth) {
+ preference.setSummary((CharSequence) newValue);
+ } else if (preference == mRemoteX509Name) {
+ preference.setSummary(TextUtils.isEmpty((CharSequence) newValue) ? "CN (default)" : (CharSequence)newValue);
+ }
+ return true;
+ }
+ private CharSequence getX509String(int authtype, String dn) {
+ String ret ="";
+ switch (authtype) {
+ case VpnProfile.X509_VERIFY_TLSREMOTE:
+ case VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING:
+ ret+="tls-remote ";
+ break;
+
+ case VpnProfile.X509_VERIFY_TLSREMOTE_DN:
+ ret="dn: ";
+ break;
+
+ case VpnProfile.X509_VERIFY_TLSREMOTE_RDN:
+ ret="rdn: ";
+ break;
+
+ case VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX:
+ ret="rdn prefix: ";
+ break;
+ }
+ return ret + dn;
+ }
+
+ void startFileDialog() {
+ Intent startFC = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && ! Utils.alwaysUseOldFileChooser(getActivity())) {
+ startFC = Utils.getFilePickerIntent(getActivity(), Utils.FileType.TLS_AUTH_FILE);
+ startActivityForResult(startFC, SELECT_TLS_FILE_KITKAT);
+ }
+
+ if (startFC == null) {
+ startFC = new Intent(getActivity(), FileSelect.class);
+ startFC.putExtra(FileSelect.START_DATA, mTlsAuthFileData);
+ startFC.putExtra(FileSelect.WINDOW_TITLE, R.string.tls_auth_file);
+ startActivityForResult(startFC, SELECT_TLS_FILE_LEGACY_DIALOG);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startFileDialog();
+ return true;
+
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if(requestCode == SELECT_TLS_FILE_LEGACY_DIALOG && resultCode == Activity.RESULT_OK){
+ String result = data.getStringExtra(FileSelect.RESULT_DATA);
+ mTlsAuthFileData=result;
+ setTlsAuthSummary(result);
+ } else if (requestCode == SELECT_TLS_FILE_KITKAT && resultCode == Activity.RESULT_OK) {
+ try {
+ mTlsAuthFileData= Utils.getFilePickerResult(Utils.FileType.TLS_AUTH_FILE,data,getActivity());
+ setTlsAuthSummary(mTlsAuthFileData);
+ } catch (IOException e) {
+ VpnStatus.logException(e);
+ } catch (SecurityException se) {
+ VpnStatus.logException(se);
+ }
+ }
+ }
+
+ private void setTlsAuthSummary(String result) {
+ if(result==null)
+ result = getString(R.string.no_certificate);
+ if(result.startsWith(VpnProfile.INLINE_TAG))
+ mTLSAuthFile.setSummary(R.string.inline_file_data);
+ else if (result.startsWith(VpnProfile.DISPLAYNAME_TAG))
+ mTLSAuthFile.setSummary(getString(R.string.imported_from_file, VpnProfile.getDisplayName(result)));
+ else
+ mTLSAuthFile.setSummary(result);
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java
new file mode 100644
index 00000000..81da76fe
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java
@@ -0,0 +1,227 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+import android.widget.AdapterView.OnItemSelectedListener;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.R.id;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.views.FileSelectLayout;
+
+public class Settings_Basic extends KeyChainSettingsFragment implements OnItemSelectedListener, FileSelectLayout.FileSelectCallback {
+ private static final int CHOOSE_FILE_OFFSET = 1000;
+
+ private FileSelectLayout mClientCert;
+ private FileSelectLayout mCaCert;
+ private FileSelectLayout mClientKey;
+ private CheckBox mUseLzo;
+ private Spinner mType;
+ private FileSelectLayout mpkcs12;
+ private FileSelectLayout mCrlFile;
+ private TextView mPKCS12Password;
+ private EditText mUserName;
+ private EditText mPassword;
+ private View mView;
+ private EditText mProfileName;
+ private EditText mKeyPassword;
+
+ private SparseArray<FileSelectLayout> fileselects = new SparseArray<>();
+ private Spinner mAuthRetry;
+
+
+ private void addFileSelectLayout(FileSelectLayout fsl, Utils.FileType type) {
+ int i = fileselects.size() + CHOOSE_FILE_OFFSET;
+ fileselects.put(i, fsl);
+ fsl.setCaller(this, i, type);
+ }
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ }
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+
+ mView = inflater.inflate(R.layout.basic_settings, container, false);
+
+ mProfileName = mView.findViewById(id.profilename);
+ mClientCert = mView.findViewById(id.certselect);
+ mClientKey = mView.findViewById(id.keyselect);
+ mCaCert = mView.findViewById(id.caselect);
+ mpkcs12 = mView.findViewById(id.pkcs12select);
+ mCrlFile = mView.findViewById(id.crlfile);
+ mUseLzo = mView.findViewById(id.lzo);
+ mType = mView.findViewById(id.type);
+ mPKCS12Password = mView.findViewById(id.pkcs12password);
+
+ mUserName = mView.findViewById(id.auth_username);
+ mPassword = mView.findViewById(id.auth_password);
+ mKeyPassword = mView.findViewById(id.key_password);
+ mAuthRetry = mView.findViewById(id.auth_retry);
+
+ addFileSelectLayout(mCaCert, Utils.FileType.CA_CERTIFICATE);
+ addFileSelectLayout(mClientCert, Utils.FileType.CLIENT_CERTIFICATE);
+ addFileSelectLayout(mClientKey, Utils.FileType.KEYFILE);
+ addFileSelectLayout(mpkcs12, Utils.FileType.PKCS12);
+ addFileSelectLayout(mCrlFile, Utils.FileType.CRL_FILE);
+ mCaCert.setShowClear();
+ mCrlFile.setShowClear();
+
+ mType.setOnItemSelectedListener(this);
+ mAuthRetry.setOnItemSelectedListener(this);
+
+ initKeychainViews(mView);
+
+ return mView;
+ }
+
+
+ @Override
+ public void onActivityResult(int request, int result, Intent data) {
+ super.onActivityResult(request, result, data);
+ if (result == Activity.RESULT_OK && request >= CHOOSE_FILE_OFFSET) {
+ FileSelectLayout fsl = fileselects.get(request);
+ fsl.parseResponse(data, getActivity());
+
+ savePreferences();
+
+ // Private key files may result in showing/hiding the private key password dialog
+ if (fsl == mClientKey) {
+ changeType(mType.getSelectedItemPosition());
+ }
+ }
+
+ }
+
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (parent == mType) {
+ changeType(position);
+ }
+ }
+
+
+ private void changeType(int type) {
+ // hide everything
+ mView.findViewById(R.id.pkcs12).setVisibility(View.GONE);
+ mView.findViewById(R.id.certs).setVisibility(View.GONE);
+ mView.findViewById(R.id.statickeys).setVisibility(View.GONE);
+ mView.findViewById(R.id.keystore).setVisibility(View.GONE);
+ mView.findViewById(R.id.cacert).setVisibility(View.GONE);
+ ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(false);
+ mView.findViewById(R.id.userpassword).setVisibility(View.GONE);
+ mView.findViewById(R.id.key_password_layout).setVisibility(View.GONE);
+ mView.findViewById(R.id.external_auth).setVisibility(View.GONE);
+
+ // Fall through are by design
+ switch (type) {
+ case VpnProfile.TYPE_USERPASS_CERTIFICATES:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_CERTIFICATES:
+ mView.findViewById(R.id.certs).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ if (mProfile.requireTLSKeyPassword())
+ mView.findViewById(R.id.key_password_layout).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_PKCS12:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_PKCS12:
+ mView.findViewById(R.id.pkcs12).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(true);
+ break;
+
+ case VpnProfile.TYPE_STATICKEYS:
+ mView.findViewById(R.id.statickeys).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_KEYSTORE:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_KEYSTORE:
+ mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(true);
+ break;
+
+ case VpnProfile.TYPE_USERPASS:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ break;
+ case VpnProfile.TYPE_EXTERNAL_APP:
+ mView.findViewById(R.id.external_auth).setVisibility(View.VISIBLE);
+ break;
+ }
+
+
+ }
+
+ protected void loadPreferences() {
+ super.loadPreferences();
+ mProfileName.setText(mProfile.mName);
+ mClientCert.setData(mProfile.mClientCertFilename, getActivity());
+ mClientKey.setData(mProfile.mClientKeyFilename, getActivity());
+ mCaCert.setData(mProfile.mCaFilename, getActivity());
+ mCrlFile.setData(mProfile.mCrlFilename, getActivity());
+
+ mUseLzo.setChecked(mProfile.mUseLzo);
+ mType.setSelection(mProfile.mAuthenticationType);
+ mpkcs12.setData(mProfile.mPKCS12Filename, getActivity());
+ mPKCS12Password.setText(mProfile.mPKCS12Password);
+ mUserName.setText(mProfile.mUsername);
+ mPassword.setText(mProfile.mPassword);
+ mKeyPassword.setText(mProfile.mKeyPassword);
+ mAuthRetry.setSelection(mProfile.mAuthRetry);
+ }
+
+ protected void savePreferences() {
+ super.savePreferences();
+ mProfile.mName = mProfileName.getText().toString();
+ mProfile.mCaFilename = mCaCert.getData();
+ mProfile.mClientCertFilename = mClientCert.getData();
+ mProfile.mClientKeyFilename = mClientKey.getData();
+ mProfile.mCrlFilename = mCrlFile.getData();
+
+ mProfile.mUseLzo = mUseLzo.isChecked();
+ mProfile.mAuthenticationType = mType.getSelectedItemPosition();
+ mProfile.mPKCS12Filename = mpkcs12.getData();
+ mProfile.mPKCS12Password = mPKCS12Password.getText().toString();
+
+ mProfile.mPassword = mPassword.getText().toString();
+ mProfile.mUsername = mUserName.getText().toString();
+ mProfile.mKeyPassword = mKeyPassword.getText().toString();
+ mProfile.mAuthRetry = mAuthRetry.getSelectedItemPosition();
+
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ savePreferences();
+ if (mProfile != null) {
+ outState.putString(getActivity().getPackageName() + "profileUUID", mProfile.getUUID().toString());
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java
new file mode 100644
index 00000000..e41e6cb9
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java
@@ -0,0 +1,101 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+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.Checkable;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+
+public class Settings_Connections extends Settings_Fragment implements View.OnClickListener {
+ private ConnectionsAdapter mConnectionsAdapter;
+ private TextView mWarning;
+ private Checkable mUseRandomRemote;
+ private RecyclerView mRecyclerView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ inflater.inflate(R.menu.connections, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.connections, container, false);
+
+ mWarning = (TextView) v.findViewById(R.id.noserver_active_warning);
+ mRecyclerView = (RecyclerView) v.findViewById(R.id.connection_recycler_view);
+
+ int dpwidth = (int) (container.getWidth()/getResources().getDisplayMetrics().density);
+ int columns = dpwidth/290;
+ columns = Math.max(1, columns);
+
+ mConnectionsAdapter = new ConnectionsAdapter(getActivity(), this, mProfile);
+
+ //mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL));
+ mRecyclerView.setHasFixedSize(true);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
+ mRecyclerView.setAdapter(mConnectionsAdapter);
+
+ ImageButton fab_button = (ImageButton) v.findViewById(R.id.add_new_remote);
+ if(fab_button!=null)
+ fab_button.setOnClickListener(this);
+
+ mUseRandomRemote = (Checkable) v.findViewById(R.id.remote_random);
+ mUseRandomRemote.setChecked(mProfile.mRemoteRandom);
+
+
+ mConnectionsAdapter.displayWarningIfNoneEnabled();
+
+ return v;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.add_new_remote) {
+ mConnectionsAdapter.addRemote();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId()==R.id.add_new_remote)
+ mConnectionsAdapter.addRemote();
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void savePreferences() {
+ mConnectionsAdapter.saveProfile();
+ mProfile.mRemoteRandom = mUseRandomRemote.isChecked();
+ }
+
+ public void setWarningVisible(int showWarning) {
+ mWarning.setVisibility(showWarning);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java
new file mode 100644
index 00000000..738bd0e9
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012-2015 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.os.Bundle;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+public abstract class Settings_Fragment extends Fragment {
+
+ protected VpnProfile mProfile;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String profileUuid = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile= ProfileManager.get(getActivity(), profileUuid);
+ getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+ }
+
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ savePreferences();
+ }
+
+ protected abstract void savePreferences();
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java
new file mode 100644
index 00000000..daf407b8
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java
@@ -0,0 +1,137 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceManager;
+import android.preference.SwitchPreference;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+
+public class Settings_IP extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private EditTextPreference mIPv4;
+ private EditTextPreference mIPv6;
+ private SwitchPreference mUsePull;
+ private CheckBoxPreference mOverrideDNS;
+ private EditTextPreference mSearchdomain;
+ private EditTextPreference mDNS1;
+ private EditTextPreference mDNS2;
+ private CheckBoxPreference mNobind;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ // Make sure default values are applied. In a real app, you would
+ // want this in a shared function that is used to retrieve the
+ // SharedPreferences wherever they are needed.
+ PreferenceManager.setDefaultValues(getActivity(),
+ R.xml.vpn_ipsettings, false);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_ipsettings);
+ mIPv4 = (EditTextPreference) findPreference("ipv4_address");
+ mIPv6 = (EditTextPreference) findPreference("ipv6_address");
+ mUsePull = (SwitchPreference) findPreference("usePull");
+ mOverrideDNS = (CheckBoxPreference) findPreference("overrideDNS");
+ mSearchdomain =(EditTextPreference) findPreference("searchdomain");
+ mDNS1 = (EditTextPreference) findPreference("dns1");
+ mDNS2 = (EditTextPreference) findPreference("dns2");
+ mNobind = (CheckBoxPreference) findPreference("nobind");
+
+ mIPv4.setOnPreferenceChangeListener(this);
+ mIPv6.setOnPreferenceChangeListener(this);
+ mDNS1.setOnPreferenceChangeListener(this);
+ mDNS2.setOnPreferenceChangeListener(this);
+ mUsePull.setOnPreferenceChangeListener(this);
+ mOverrideDNS.setOnPreferenceChangeListener(this);
+ mSearchdomain.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mUsePull.setChecked(mProfile.mUsePull);
+ mIPv4.setText(mProfile.mIPv4Address);
+ mIPv6.setText(mProfile.mIPv6Address);
+ mDNS1.setText(mProfile.mDNS1);
+ mDNS2.setText(mProfile.mDNS2);
+ mOverrideDNS.setChecked(mProfile.mOverrideDNS);
+ mSearchdomain.setText(mProfile.mSearchDomain);
+ mNobind.setChecked(mProfile.mNobind);
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_STATICKEYS)
+ mUsePull.setChecked(false);
+
+ mUsePull.setEnabled(mProfile.mAuthenticationType != VpnProfile.TYPE_STATICKEYS);
+
+ // Sets Summary
+ onPreferenceChange(mIPv4, mIPv4.getText());
+ onPreferenceChange(mIPv6, mIPv6.getText());
+ onPreferenceChange(mDNS1, mDNS1.getText());
+ onPreferenceChange(mDNS2, mDNS2.getText());
+ onPreferenceChange(mSearchdomain, mSearchdomain.getText());
+
+ setDNSState();
+ }
+
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mUsePull = mUsePull.isChecked();
+ mProfile.mIPv4Address = mIPv4.getText();
+ mProfile.mIPv6Address = mIPv6.getText();
+ mProfile.mDNS1 = mDNS1.getText();
+ mProfile.mDNS2 = mDNS2.getText();
+ mProfile.mOverrideDNS = mOverrideDNS.isChecked();
+ mProfile.mSearchDomain = mSearchdomain.getText();
+ mProfile.mNobind = mNobind.isChecked();
+
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference,
+ Object newValue) {
+ if(preference==mIPv4 || preference == mIPv6
+ || preference==mDNS1 || preference == mDNS2
+ || preference == mSearchdomain
+ )
+
+ preference.setSummary((String)newValue);
+
+ if(preference== mUsePull || preference == mOverrideDNS)
+ if(preference==mOverrideDNS) {
+ // Set so the function gets the right value
+ mOverrideDNS.setChecked((Boolean) newValue);
+ }
+ setDNSState();
+
+ saveSettings();
+ return true;
+ }
+
+ private void setDNSState() {
+ boolean enabled;
+ mOverrideDNS.setEnabled(mUsePull.isChecked());
+ if(!mUsePull.isChecked())
+ enabled =true;
+ else
+ enabled = mOverrideDNS.isChecked();
+
+ mDNS1.setEnabled(enabled);
+ mDNS2.setEnabled(enabled);
+ mSearchdomain.setEnabled(enabled);
+
+
+ }
+
+
+ } \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java
new file mode 100644
index 00000000..6674599d
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java
@@ -0,0 +1,212 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.widget.Toast;
+
+import java.util.Locale;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+
+public class Settings_Obscure extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private CheckBoxPreference mUseRandomHostName;
+ private CheckBoxPreference mUseFloat;
+ private CheckBoxPreference mUseCustomConfig;
+ private EditTextPreference mCustomConfig;
+ private EditTextPreference mMssFixValue;
+ private CheckBoxPreference mMssFixCheckBox;
+ private CheckBoxPreference mPeerInfo;
+
+ private CheckBoxPreference mPersistent;
+ private ListPreference mConnectRetrymax;
+ private EditTextPreference mConnectRetry;
+ private EditTextPreference mConnectRetryMaxTime;
+ private EditTextPreference mTunMtu;
+
+ public void onCreateBehaviour(Bundle savedInstanceState) {
+
+ mPersistent = (CheckBoxPreference) findPreference("usePersistTun");
+ mConnectRetrymax = (ListPreference) findPreference("connectretrymax");
+ mConnectRetry = (EditTextPreference) findPreference("connectretry");
+ mConnectRetryMaxTime = (EditTextPreference) findPreference("connectretrymaxtime");
+
+ mPeerInfo = (CheckBoxPreference) findPreference("peerInfo");
+
+ mConnectRetrymax.setOnPreferenceChangeListener(this);
+ mConnectRetrymax.setSummary("%s");
+
+ mConnectRetry.setOnPreferenceChangeListener(this);
+ mConnectRetryMaxTime.setOnPreferenceChangeListener(this);
+
+
+
+ }
+
+ protected void loadSettingsBehaviour() {
+ mPersistent.setChecked(mProfile.mPersistTun);
+ mPeerInfo.setChecked(mProfile.mPushPeerInfo);
+
+ mConnectRetrymax.setValue(mProfile.mConnectRetryMax);
+ onPreferenceChange(mConnectRetrymax, mProfile.mConnectRetryMax);
+
+ mConnectRetry.setText(mProfile.mConnectRetry);
+ onPreferenceChange(mConnectRetry, mProfile.mConnectRetry);
+
+ mConnectRetryMaxTime.setText(mProfile.mConnectRetryMaxTime);
+ onPreferenceChange(mConnectRetryMaxTime, mProfile.mConnectRetryMaxTime);
+
+ }
+
+
+ protected void saveSettingsBehaviour() {
+ mProfile.mConnectRetryMax = mConnectRetrymax.getValue();
+ mProfile.mPersistTun = mPersistent.isChecked();
+ mProfile.mConnectRetry = mConnectRetry.getText();
+ mProfile.mPushPeerInfo = mPeerInfo.isChecked();
+ mProfile.mConnectRetryMaxTime = mConnectRetryMaxTime.getText();
+ }
+
+
+ public boolean onPreferenceChangeBehaviour(Preference preference, Object newValue) {
+ if (preference == mConnectRetrymax) {
+ if(newValue==null) {
+ newValue="5";
+ }
+ mConnectRetrymax.setDefaultValue(newValue);
+
+ for(int i=0;i< mConnectRetrymax.getEntryValues().length;i++){
+ if(mConnectRetrymax.getEntryValues().equals(newValue))
+ mConnectRetrymax.setSummary(mConnectRetrymax.getEntries()[i]);
+ }
+
+ } else if (preference == mConnectRetry) {
+ if(newValue==null || newValue=="")
+ newValue="2";
+ mConnectRetry.setSummary(String.format("%s s", newValue));
+ } else if (preference == mConnectRetryMaxTime) {
+ if(newValue==null || newValue=="")
+ newValue="300";
+ mConnectRetryMaxTime.setSummary(String.format("%s s", newValue));
+ }
+
+
+ return true;
+ }
+
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_obscure);
+
+ mUseRandomHostName = (CheckBoxPreference) findPreference("useRandomHostname");
+ mUseFloat = (CheckBoxPreference) findPreference("useFloat");
+ mUseCustomConfig = (CheckBoxPreference) findPreference("enableCustomOptions");
+ mCustomConfig = (EditTextPreference) findPreference("customOptions");
+ mMssFixCheckBox = (CheckBoxPreference) findPreference("mssFix");
+ mMssFixValue = (EditTextPreference) findPreference("mssFixValue");
+ mMssFixValue.setOnPreferenceChangeListener(this);
+ mTunMtu = (EditTextPreference) findPreference("tunmtu");
+ mTunMtu.setOnPreferenceChangeListener(this);;
+
+ onCreateBehaviour(savedInstanceState);
+ loadSettings();
+
+ }
+
+ protected void loadSettings() {
+ mUseRandomHostName.setChecked(mProfile.mUseRandomHostname);
+ mUseFloat.setChecked(mProfile.mUseFloat);
+ mUseCustomConfig.setChecked(mProfile.mUseCustomConfig);
+ mCustomConfig.setText(mProfile.mCustomConfigOptions);
+
+ if (mProfile.mMssFix == 0) {
+ mMssFixValue.setText(String.valueOf(VpnProfile.DEFAULT_MSSFIX_SIZE));
+ mMssFixCheckBox.setChecked(false);
+ setMssSummary(VpnProfile.DEFAULT_MSSFIX_SIZE);
+ } else {
+ mMssFixValue.setText(String.valueOf(mProfile.mMssFix));
+ mMssFixCheckBox.setChecked(true);
+ setMssSummary(mProfile.mMssFix);
+ }
+
+
+ int tunmtu = mProfile.mTunMtu;
+ if (mProfile.mTunMtu < 48)
+ tunmtu = 1500;
+
+ mTunMtu.setText(String.valueOf(tunmtu));
+ setMtuSummary(tunmtu);
+
+
+ loadSettingsBehaviour();
+
+ }
+
+ private void setMssSummary(int value) {
+ mMssFixValue.setSummary(String.format(Locale.getDefault(),"Configured MSS value: %d", value));
+ }
+
+ private void setMtuSummary(int value) {
+ if (value == 1500)
+ mTunMtu.setSummary(String.format(Locale.getDefault(),"Using default (1500) MTU", value));
+ else
+ mTunMtu.setSummary(String.format(Locale.getDefault(),"Configured MTU value: %d", value));
+ }
+
+ protected void saveSettings() {
+ mProfile.mUseRandomHostname = mUseRandomHostName.isChecked();
+ mProfile.mUseFloat = mUseFloat.isChecked();
+ mProfile.mUseCustomConfig = mUseCustomConfig.isChecked();
+ mProfile.mCustomConfigOptions = mCustomConfig.getText();
+ if (mMssFixCheckBox.isChecked())
+ mProfile.mMssFix=Integer.parseInt(mMssFixValue.getText());
+ else
+ mProfile.mMssFix=0;
+
+ mProfile.mTunMtu = Integer.parseInt(mTunMtu.getText());
+ saveSettingsBehaviour();
+ }
+
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference.getKey().equals("mssFixValue"))
+ try {
+ int v = Integer.parseInt((String) newValue);
+ if (v < 0 || v > 9000)
+ throw new NumberFormatException("mssfix value");
+ setMssSummary(v);
+
+ } catch(NumberFormatException e) {
+ Toast.makeText(getActivity(), R.string.mssfix_invalid_value, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ else if (preference.getKey().equals("tunmtu"))
+ try {
+ int v = Integer.parseInt((String) newValue);
+ if (v < 48 || v > 9000)
+ throw new NumberFormatException("mtu value");
+ setMtuSummary(v);
+
+ } catch(NumberFormatException e) {
+ Toast.makeText(getActivity(), R.string.mtu_invalid_value, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ return onPreferenceChangeBehaviour(preference, newValue);
+
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
new file mode 100644
index 00000000..53f88bbf
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
@@ -0,0 +1,109 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import de.blinkt.openvpn.R;
+
+
+public class Settings_Routing extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private EditTextPreference mCustomRoutes;
+ private CheckBoxPreference mUseDefaultRoute;
+ private EditTextPreference mCustomRoutesv6;
+ private CheckBoxPreference mUseDefaultRoutev6;
+ private CheckBoxPreference mRouteNoPull;
+ private CheckBoxPreference mLocalVPNAccess;
+ private EditTextPreference mExcludedRoutes;
+ private EditTextPreference mExcludedRoutesv6;
+ private CheckBoxPreference mBlockUnusedAF;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_routing);
+ mCustomRoutes = (EditTextPreference) findPreference("customRoutes");
+ mUseDefaultRoute = (CheckBoxPreference) findPreference("useDefaultRoute");
+ mCustomRoutesv6 = (EditTextPreference) findPreference("customRoutesv6");
+ mUseDefaultRoutev6 = (CheckBoxPreference) findPreference("useDefaultRoutev6");
+ mExcludedRoutes = (EditTextPreference) findPreference("excludedRoutes");
+ mExcludedRoutesv6 = (EditTextPreference) findPreference("excludedRoutesv6");
+
+ mRouteNoPull = (CheckBoxPreference) findPreference("routenopull");
+ mLocalVPNAccess = (CheckBoxPreference) findPreference("unblockLocal");
+
+ mBlockUnusedAF = (CheckBoxPreference) findPreference("blockUnusedAF");
+
+ mCustomRoutes.setOnPreferenceChangeListener(this);
+ mCustomRoutesv6.setOnPreferenceChangeListener(this);
+ mExcludedRoutes.setOnPreferenceChangeListener(this);
+ mExcludedRoutesv6.setOnPreferenceChangeListener(this);
+ mBlockUnusedAF.setOnPreferenceChangeListener(this);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ getPreferenceScreen().removePreference(mBlockUnusedAF);
+
+ loadSettings();
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mUseDefaultRoute.setChecked(mProfile.mUseDefaultRoute);
+ mUseDefaultRoutev6.setChecked(mProfile.mUseDefaultRoutev6);
+
+ mCustomRoutes.setText(mProfile.mCustomRoutes);
+ mCustomRoutesv6.setText(mProfile.mCustomRoutesv6);
+
+ mExcludedRoutes.setText(mProfile.mExcludedRoutes);
+ mExcludedRoutesv6.setText(mProfile.mExcludedRoutesv6);
+
+ mRouteNoPull.setChecked(mProfile.mRoutenopull);
+ mLocalVPNAccess.setChecked(mProfile.mAllowLocalLAN);
+
+ mBlockUnusedAF.setChecked(mProfile.mBlockUnusedAddressFamilies);
+
+ // Sets Summary
+ onPreferenceChange(mCustomRoutes, mCustomRoutes.getText());
+ onPreferenceChange(mCustomRoutesv6, mCustomRoutesv6.getText());
+ onPreferenceChange(mExcludedRoutes, mExcludedRoutes.getText());
+ onPreferenceChange(mExcludedRoutesv6, mExcludedRoutesv6.getText());
+
+ mRouteNoPull.setEnabled(mProfile.mUsePull);
+ }
+
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mUseDefaultRoute = mUseDefaultRoute.isChecked();
+ mProfile.mUseDefaultRoutev6 = mUseDefaultRoutev6.isChecked();
+ mProfile.mCustomRoutes = mCustomRoutes.getText();
+ mProfile.mCustomRoutesv6 = mCustomRoutesv6.getText();
+ mProfile.mRoutenopull = mRouteNoPull.isChecked();
+ mProfile.mAllowLocalLAN =mLocalVPNAccess.isChecked();
+ mProfile.mExcludedRoutes = mExcludedRoutes.getText();
+ mProfile.mExcludedRoutesv6 = mExcludedRoutesv6.getText();
+ mProfile.mBlockUnusedAddressFamilies = mBlockUnusedAF.isChecked();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference,
+ Object newValue) {
+ if( preference == mCustomRoutes || preference == mCustomRoutesv6
+ || preference == mExcludedRoutes || preference == mExcludedRoutesv6)
+ preference.setSummary((String)newValue);
+
+ saveSettings();
+ return true;
+ }
+
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java
new file mode 100644
index 00000000..98ebb55b
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2012-2015 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.api.AppRestrictions;
+
+public class Settings_UserEditable extends KeyChainSettingsFragment implements View.OnClickListener {
+
+ private View mView;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mView = inflater.inflate(R.layout.settings_usereditable, container, false);
+ TextView messageView = (TextView) mView.findViewById(R.id.messageUserEdit);
+ messageView.setText(getString(R.string.message_no_user_edit, getPackageString(mProfile.mProfileCreator)));
+ initKeychainViews(this.mView);
+ return mView;
+ }
+
+
+ private String getPackageString(String packageName) {
+
+ if (AppRestrictions.PROFILE_CREATOR.equals(packageName))
+ return "Android Enterprise Management";
+
+ final PackageManager pm = getActivity().getPackageManager();
+ ApplicationInfo ai;
+ try {
+ ai = pm.getApplicationInfo(packageName, 0);
+ } catch (final PackageManager.NameNotFoundException e) {
+ ai = null;
+ }
+ final String applicationName = (String) (ai != null ? pm.getApplicationLabel(ai) : "(unknown)");
+ return String.format("%s (%s)", applicationName, packageName);
+ }
+
+ @Override
+ protected void savePreferences() {
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mView.findViewById(R.id.keystore).setVisibility(View.GONE);
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE ||
+ mProfile.mAuthenticationType == VpnProfile.TYPE_KEYSTORE)
+ mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java
new file mode 100644
index 00000000..f5c1750a
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java
@@ -0,0 +1,134 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+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.ImageButton;
+import android.widget.TextView;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+
+public class ShowConfigFragment extends Fragment {
+ private String configtext;
+ private TextView mConfigView;
+ private ImageButton mfabButton;
+
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ {
+ View v=inflater.inflate(R.layout.viewconfig, container,false);
+ mConfigView = (TextView) v.findViewById(R.id.configview);
+
+
+ mfabButton = (ImageButton) v.findViewById(R.id.share_config);
+ if (mfabButton!=null) {
+ mfabButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ shareConfig();
+ }
+ });
+ mfabButton.setVisibility(View.INVISIBLE);
+ }
+ return v;
+ }
+
+ private void startGenConfig(final VpnProfile vp, final TextView cv) {
+
+ new Thread() {
+ public void run() {
+ /* Add a few newlines to make the textview scrollable past the FAB */
+ try {
+
+ configtext = vp.getConfigFile(getActivity(), VpnProfile.doUseOpenVPN3(getActivity())) + "\n\n\n";
+ } catch (Exception e) {
+ e.printStackTrace();
+ configtext = "Error generating config file: " + e.getLocalizedMessage();
+ }
+ getActivity().runOnUiThread(() -> {
+ cv.setText(configtext);
+ if (mfabButton!=null)
+ mfabButton.setVisibility(View.VISIBLE);
+ });
+
+
+ }
+ }.start();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ inflater.inflate(R.menu.configmenu, menu);
+ }
+
+ private void shareConfig() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, configtext);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.export_config_title));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.export_config_chooser_title)));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.sendConfig) {
+ shareConfig();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ populateConfigText();
+ }
+
+ private void populateConfigText() {
+ String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ final VpnProfile vp = ProfileManager.get(getActivity(),profileUUID);
+ int check=vp.checkProfile(getActivity());
+
+ if(check != R.string.no_error_found) {
+ mConfigView.setText(check);
+ configtext = getString(check);
+ }
+ else {
+ // Run in own Thread since Keystore does not like to be queried from the main thread
+
+ mConfigView.setText("Generating config...");
+ startGenConfig(vp, mConfigView);
+ }
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean visible)
+ {
+ super.setUserVisibleHint(visible);
+ if (visible && isResumed())
+ populateConfigText();
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java
new file mode 100644
index 00000000..abdc45f5
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java
@@ -0,0 +1,288 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.OpenableColumns;
+import android.util.Base64;
+import android.webkit.MimeTypeMap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.Vector;
+
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.Preferences;
+
+public class Utils {
+
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public static Intent getFilePickerIntent(Context c, FileType fileType) {
+ Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ i.addCategory(Intent.CATEGORY_OPENABLE);
+ TreeSet<String> supportedMimeTypes = new TreeSet<String>();
+ Vector<String> extensions = new Vector<String>();
+
+ switch (fileType) {
+ case PKCS12:
+ i.setType("application/x-pkcs12");
+ supportedMimeTypes.add("application/x-pkcs12");
+ extensions.add("p12");
+ extensions.add("pfx");
+ break;
+ case CLIENT_CERTIFICATE:
+ case CA_CERTIFICATE:
+ i.setType("application/x-pem-file");
+ supportedMimeTypes.add("application/x-x509-ca-cert");
+ supportedMimeTypes.add("application/x-x509-user-cert");
+ supportedMimeTypes.add("application/x-pem-file");
+ supportedMimeTypes.add("application/pkix-cert");
+ supportedMimeTypes.add("text/plain");
+
+ extensions.add("pem");
+ extensions.add("crt");
+ extensions.add("cer");
+ break;
+ case KEYFILE:
+ i.setType("application/x-pem-file");
+ supportedMimeTypes.add("application/x-pem-file");
+ supportedMimeTypes.add("application/pkcs8");
+
+ // Google drive ....
+ supportedMimeTypes.add("application/x-iwork-keynote-sffkey");
+ extensions.add("key");
+ break;
+
+ case TLS_AUTH_FILE:
+ i.setType("text/plain");
+
+ // Backup ....
+ supportedMimeTypes.add("application/pkcs8");
+ // Google Drive is kind of crazy .....
+ supportedMimeTypes.add("application/x-iwork-keynote-sffkey");
+
+ extensions.add("txt");
+ extensions.add("key");
+ break;
+
+ case OVPN_CONFIG:
+ i.setType("application/x-openvpn-profile");
+ supportedMimeTypes.add("application/x-openvpn-profile");
+ supportedMimeTypes.add("application/openvpn-profile");
+ supportedMimeTypes.add("application/ovpn");
+ supportedMimeTypes.add("text/plain");
+ extensions.add("ovpn");
+ extensions.add("conf");
+ break;
+
+ case CRL_FILE:
+ supportedMimeTypes.add("application/x-pkcs7-crl");
+ supportedMimeTypes.add("application/pkix-crl");
+ extensions.add("crl");
+ break;
+
+ case USERPW_FILE:
+ i.setType("text/plain");
+ supportedMimeTypes.add("text/plain");
+ break;
+ }
+
+ MimeTypeMap mtm = MimeTypeMap.getSingleton();
+
+ for (String ext : extensions) {
+ String mimeType = mtm.getMimeTypeFromExtension(ext);
+ if (mimeType != null)
+ supportedMimeTypes.add(mimeType);
+ }
+
+ // Always add this as fallback
+ supportedMimeTypes.add("application/octet-stream");
+
+ i.putExtra(Intent.EXTRA_MIME_TYPES, supportedMimeTypes.toArray(new String[supportedMimeTypes.size()]));
+
+ // People don't know that this is actually a system setting. Override it ...
+ // DocumentsContract.EXTRA_SHOW_ADVANCED is hidden
+ i.putExtra("android.content.extra.SHOW_ADVANCED", true);
+
+ /* Samsung has decided to do something strange, on stock Android GET_CONTENT opens the document UI */
+ /* fist try with documentsui */
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N)
+ i.setPackage("com.android.documentsui");
+
+
+
+
+ //noinspection ConstantConditions
+ if (!isIntentAvailable(c,i)) {
+ i.setAction(Intent.ACTION_OPEN_DOCUMENT);
+ i.setPackage(null);
+
+ // Check for really broken devices ... :(
+ if (!isIntentAvailable(c,i)) {
+ return null;
+ }
+ }
+
+
+ /*
+ final PackageManager packageManager = c.getPackageManager();
+ ResolveInfo list = packageManager.resolveActivity(i, 0);
+
+ Toast.makeText(c, "Starting package: "+ list.activityInfo.packageName
+ + "with ACTION " + i.getAction(), Toast.LENGTH_LONG).show();
+
+ */
+ return i;
+ }
+
+ public static boolean alwaysUseOldFileChooser(Context c)
+ {
+ /* Android P does not allow access to the file storage anymore */
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
+ return false;
+
+ SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c);
+ return prefs.getBoolean("useInternalFileSelector", false);
+ }
+
+ public static boolean isIntentAvailable(Context context, Intent i) {
+ final PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> list =
+ packageManager.queryIntentActivities(i,
+ PackageManager.MATCH_DEFAULT_ONLY);
+
+ // Ignore the Android TV framework app in the list
+ int size = list.size();
+ for (ResolveInfo ri: list)
+ {
+ // Ignore stub apps
+ if ("com.google.android.tv.frameworkpackagestubs".equals(ri.activityInfo.packageName))
+ {
+ size--;
+ }
+ }
+
+ return size > 0;
+ }
+
+
+ public enum FileType {
+ PKCS12(0),
+ CLIENT_CERTIFICATE(1),
+ CA_CERTIFICATE(2),
+ OVPN_CONFIG(3),
+ KEYFILE(4),
+ TLS_AUTH_FILE(5),
+ USERPW_FILE(6),
+ CRL_FILE(7);
+
+ private int value;
+
+ FileType(int i) {
+ value = i;
+ }
+
+ public static FileType getFileTypeByValue(int value) {
+ switch (value) {
+ case 0:
+ return PKCS12;
+ case 1:
+ return CLIENT_CERTIFICATE;
+ case 2:
+ return CA_CERTIFICATE;
+ case 3:
+ return OVPN_CONFIG;
+ case 4:
+ return KEYFILE;
+ case 5:
+ return TLS_AUTH_FILE;
+ case 6:
+ return USERPW_FILE;
+ case 7:
+ return CRL_FILE;
+ default:
+ return null;
+ }
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ static private byte[] readBytesFromStream(InputStream input) throws IOException {
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ int nRead;
+ byte[] data = new byte[16384];
+
+ ;
+
+ long totalread = 0;
+ while ((nRead = input.read(data, 0, data.length)) != -1 && totalread <VpnProfile.MAX_EMBED_FILE_SIZE ) {
+ buffer.write(data, 0, nRead);
+ totalread+=nRead;
+ }
+
+ buffer.flush();
+ input.close();
+ return buffer.toByteArray();
+ }
+
+ public static String getFilePickerResult(FileType ft, Intent result, Context c) throws IOException, SecurityException {
+
+ Uri uri = result.getData();
+ if (uri == null)
+ return null;
+
+ byte[] fileData = readBytesFromStream(c.getContentResolver().openInputStream(uri));
+ String newData = null;
+
+ Cursor cursor = c.getContentResolver().query(uri, null, null, null, null);
+
+ String prefix = "";
+ try {
+ if (cursor!=null && cursor.moveToFirst()) {
+ int cidx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (cidx != -1) {
+ String displayName = cursor.getString(cidx);
+
+ if (!displayName.contains(VpnProfile.INLINE_TAG) && !displayName.contains(VpnProfile.DISPLAYNAME_TAG))
+ prefix = VpnProfile.DISPLAYNAME_TAG + displayName;
+ }
+ }
+ } finally {
+ if(cursor!=null)
+ cursor.close();
+ }
+
+ switch (ft) {
+ case PKCS12:
+ newData = Base64.encodeToString(fileData, Base64.DEFAULT);
+ break;
+ default:
+ newData = new String(fileData, "UTF-8");
+ break;
+ }
+
+ return prefix + VpnProfile.INLINE_TAG + newData;
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java
new file mode 100644
index 00000000..7ad13aaf
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java
@@ -0,0 +1,636 @@
+/*
+ * 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 de.blinkt.openvpn.fragments;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.annotation.RequiresApi;
+import android.text.Html;
+import android.text.Html.ImageGetter;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TreeSet;
+
+import de.blinkt.openvpn.LaunchVPN;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.ConfigConverter;
+import de.blinkt.openvpn.activities.DisconnectVPN;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.activities.VPNPreferences;
+import de.blinkt.openvpn.core.ConnectionStatus;
+import de.blinkt.openvpn.core.Preferences;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.core.VpnStatus;
+
+import static de.blinkt.openvpn.core.OpenVPNService.DISCONNECT_VPN;
+
+
+public class VPNProfileList extends ListFragment implements OnClickListener, VpnStatus.StateListener {
+
+ public final static int RESULT_VPN_DELETED = Activity.RESULT_FIRST_USER;
+ public final static int RESULT_VPN_DUPLICATE = Activity.RESULT_FIRST_USER + 1;
+
+ private static final int MENU_ADD_PROFILE = Menu.FIRST;
+
+ private static final int START_VPN_CONFIG = 92;
+ private static final int SELECT_PROFILE = 43;
+ private static final int IMPORT_PROFILE = 231;
+ private static final int FILE_PICKER_RESULT_KITKAT = 392;
+
+ private static final int MENU_IMPORT_PROFILE = Menu.FIRST + 1;
+ private static final int MENU_CHANGE_SORTING = Menu.FIRST + 2;
+ private static final String PREF_SORT_BY_LRU = "sortProfilesByLRU";
+ private String mLastStatusMessage;
+
+ @Override
+ public void updateState(String state, String logmessage, final int localizedResId, ConnectionStatus level) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity());
+ mArrayadapter.notifyDataSetChanged();
+ }
+ });
+ }
+
+ @Override
+ public void setConnectedVPN(String uuid) {
+ }
+
+ private class VPNArrayAdapter extends ArrayAdapter<VpnProfile> {
+
+ public VPNArrayAdapter(Context context, int resource,
+ int textViewResourceId) {
+ super(context, resource, textViewResourceId);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v = super.getView(position, convertView, parent);
+
+ final VpnProfile profile = (VpnProfile) getListAdapter().getItem(position);
+
+ View titleview = v.findViewById(R.id.vpn_list_item_left);
+ titleview.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startOrStopVPN(profile);
+ }
+ });
+
+ View settingsview = v.findViewById(R.id.quickedit_settings);
+ settingsview.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ editVPN(profile);
+
+ }
+ });
+
+ TextView subtitle = (TextView) v.findViewById(R.id.vpn_item_subtitle);
+ if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) {
+ subtitle.setText(mLastStatusMessage);
+ subtitle.setVisibility(View.VISIBLE);
+ } else {
+ subtitle.setText("");
+ subtitle.setVisibility(View.GONE);
+ }
+
+
+ return v;
+ }
+ }
+
+ private void startOrStopVPN(VpnProfile profile) {
+ if (VpnStatus.isVPNActive() && profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) {
+ Intent disconnectVPN = new Intent(getActivity(), DisconnectVPN.class);
+ startActivity(disconnectVPN);
+ } else {
+ startVPN(profile);
+ }
+ }
+
+
+ private ArrayAdapter<VpnProfile> mArrayadapter;
+
+ protected VpnProfile mEditProfile = null;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+
+ // Shortcut version is increased to refresh all shortcuts
+ final static int SHORTCUT_VERSION = 1;
+
+ @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+ void updateDynamicShortcuts() {
+ PersistableBundle versionExtras = new PersistableBundle();
+ versionExtras.putInt("version", SHORTCUT_VERSION);
+
+ ShortcutManager shortcutManager = getContext().getSystemService(ShortcutManager.class);
+ if (shortcutManager.isRateLimitingActive())
+ return;
+
+ List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
+ int maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1;
+
+
+ ShortcutInfo disconnectShortcut = new ShortcutInfo.Builder(getContext(), "disconnectVPN")
+ .setShortLabel("Disconnect")
+ .setLongLabel("Disconnect VPN")
+ .setIntent(new Intent(getContext(), DisconnectVPN.class).setAction(DISCONNECT_VPN))
+ .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel))
+ .setExtras(versionExtras)
+ .build();
+
+ LinkedList<ShortcutInfo> newShortcuts = new LinkedList<>();
+ LinkedList<ShortcutInfo> updateShortcuts = new LinkedList<>();
+
+ LinkedList<String> removeShortcuts = new LinkedList<>();
+ LinkedList<String> disableShortcuts = new LinkedList<>();
+
+ boolean addDisconnect = true;
+
+
+ TreeSet<VpnProfile> sortedProfilesLRU = new TreeSet<VpnProfile>(new VpnProfileLRUComparator());
+ ProfileManager profileManager = ProfileManager.getInstance(getContext());
+ sortedProfilesLRU.addAll(profileManager.getProfiles());
+
+ LinkedList<VpnProfile> LRUProfiles = new LinkedList<>();
+ maxvpn = Math.min(maxvpn, sortedProfilesLRU.size());
+
+ for (int i = 0; i < maxvpn; i++) {
+ LRUProfiles.add(sortedProfilesLRU.pollFirst());
+ }
+
+ for (ShortcutInfo shortcut : shortcuts) {
+ if (shortcut.getId().equals("disconnectVPN")) {
+ addDisconnect = false;
+ if (shortcut.getExtras() == null
+ || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION)
+ updateShortcuts.add(disconnectShortcut);
+
+ } else {
+ VpnProfile p = ProfileManager.get(getContext(), shortcut.getId());
+ if (p == null || p.profileDeleted) {
+ if (shortcut.isEnabled()) {
+ disableShortcuts.add(shortcut.getId());
+ removeShortcuts.add(shortcut.getId());
+ }
+ if (!shortcut.isPinned())
+ removeShortcuts.add(shortcut.getId());
+ } else {
+
+ if (LRUProfiles.contains(p))
+ LRUProfiles.remove(p);
+ else
+ removeShortcuts.add(p.getUUIDString());
+
+ if (!p.getName().equals(shortcut.getShortLabel())
+ || shortcut.getExtras() == null
+ || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION)
+ updateShortcuts.add(createShortcut(p));
+
+
+ }
+
+ }
+
+ }
+ if (addDisconnect)
+ newShortcuts.add(disconnectShortcut);
+ for (VpnProfile p : LRUProfiles)
+ newShortcuts.add(createShortcut(p));
+
+ if (updateShortcuts.size() > 0)
+ shortcutManager.updateShortcuts(updateShortcuts);
+ if (removeShortcuts.size() > 0)
+ shortcutManager.removeDynamicShortcuts(removeShortcuts);
+ if (newShortcuts.size() > 0)
+ shortcutManager.addDynamicShortcuts(newShortcuts);
+ if (disableShortcuts.size() > 0)
+ shortcutManager.disableShortcuts(disableShortcuts, "VpnProfile does not exist anymore.");
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
+ ShortcutInfo createShortcut(VpnProfile profile) {
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ shortcutIntent.setClass(getActivity(), LaunchVPN.class);
+ shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
+ shortcutIntent.setAction(Intent.ACTION_MAIN);
+ shortcutIntent.putExtra("EXTRA_HIDELOG", true);
+
+ PersistableBundle versionExtras = new PersistableBundle();
+ versionExtras.putInt("version", SHORTCUT_VERSION);
+
+ return new ShortcutInfo.Builder(getContext(), profile.getUUIDString())
+ .setShortLabel(profile.getName())
+ .setLongLabel(getString(R.string.qs_connect, profile.getName()))
+ .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key))
+ .setIntent(shortcutIntent)
+ .setExtras(versionExtras)
+ .build();
+ }
+
+ class MiniImageGetter implements ImageGetter {
+
+
+ @Override
+ public Drawable getDrawable(String source) {
+ Drawable d = null;
+ if ("ic_menu_add".equals(source))
+ d = getActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey);
+ else if ("ic_menu_archive".equals(source))
+ d = getActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey);
+
+
+ if (d != null) {
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ return d;
+ } else {
+ return null;
+ }
+ }
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setListAdapter();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ updateDynamicShortcuts();
+ }
+ VpnStatus.addStateListener(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ VpnStatus.removeStateListener(this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.vpn_profile_list, container, false);
+
+ TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint);
+ TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint);
+
+ newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint), new MiniImageGetter(), null));
+ importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint), new MiniImageGetter(), null));
+
+ ImageButton fab_add = (ImageButton) v.findViewById(R.id.fab_add);
+ ImageButton fab_import = (ImageButton) v.findViewById(R.id.fab_import);
+ if (fab_add != null)
+ fab_add.setOnClickListener(this);
+
+ if (fab_import != null)
+ fab_import.setOnClickListener(this);
+
+ return v;
+
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ setListAdapter();
+ }
+
+ static class VpnProfileNameComparator implements Comparator<VpnProfile> {
+
+ @Override
+ public int compare(VpnProfile lhs, VpnProfile rhs) {
+ if (lhs == rhs)
+ // Catches also both null
+ return 0;
+
+ if (lhs == null)
+ return -1;
+ if (rhs == null)
+ return 1;
+
+ if (lhs.mName == null)
+ return -1;
+ if (rhs.mName == null)
+ return 1;
+
+ return lhs.mName.compareTo(rhs.mName);
+ }
+
+ }
+
+ static class VpnProfileLRUComparator implements Comparator<VpnProfile> {
+
+ VpnProfileNameComparator nameComparator = new VpnProfileNameComparator();
+
+ @Override
+ public int compare(VpnProfile lhs, VpnProfile rhs) {
+ if (lhs == rhs)
+ // Catches also both null
+ return 0;
+
+ if (lhs == null)
+ return -1;
+ if (rhs == null)
+ return 1;
+
+ // Copied from Long.compare
+ if (lhs.mLastUsed > rhs.mLastUsed)
+ return -1;
+ if (lhs.mLastUsed < rhs.mLastUsed)
+ return 1;
+ else
+ return nameComparator.compare(lhs, rhs);
+ }
+ }
+
+
+ private void setListAdapter() {
+ if (mArrayadapter == null) {
+ mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title);
+
+ }
+ populateVpnList();
+ }
+
+ private void populateVpnList() {
+ boolean sortByLRU = Preferences.getDefaultSharedPreferences(getActivity()).getBoolean(PREF_SORT_BY_LRU, false);
+ Collection<VpnProfile> allvpn = getPM().getProfiles();
+ TreeSet<VpnProfile> sortedset;
+ if (sortByLRU)
+ sortedset = new TreeSet<>(new VpnProfileLRUComparator());
+ else
+ sortedset = new TreeSet<>(new VpnProfileNameComparator());
+
+ sortedset.addAll(allvpn);
+ mArrayadapter.clear();
+ mArrayadapter.addAll(sortedset);
+
+ setListAdapter(mArrayadapter);
+ mArrayadapter.notifyDataSetChanged();
+ }
+
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile)
+ .setIcon(R.drawable.ic_menu_add)
+ .setAlphabeticShortcut('a')
+ .setTitleCondensed(getActivity().getString(R.string.add))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+ menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import)
+ .setIcon(R.drawable.ic_menu_import)
+ .setAlphabeticShortcut('i')
+ .setTitleCondensed(getActivity().getString(R.string.menu_import_short))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+ menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting)
+ .setIcon(R.drawable.ic_sort)
+ .setAlphabeticShortcut('s')
+ .setTitleCondensed(getString(R.string.sort))
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == MENU_ADD_PROFILE) {
+ onAddOrDuplicateProfile(null);
+ return true;
+ } else if (itemId == MENU_IMPORT_PROFILE) {
+ return startImportConfigFilePicker();
+ } else if (itemId == MENU_CHANGE_SORTING) {
+ return changeSorting();
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private boolean changeSorting() {
+ SharedPreferences prefs = Preferences.getDefaultSharedPreferences(getActivity());
+ boolean oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false);
+ SharedPreferences.Editor prefsedit = prefs.edit();
+ if (oldValue) {
+ Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show();
+ prefsedit.putBoolean(PREF_SORT_BY_LRU, false);
+ } else {
+ prefsedit.putBoolean(PREF_SORT_BY_LRU, true);
+ Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show();
+ }
+ prefsedit.apply();
+ populateVpnList();
+ return true;
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.fab_import:
+ startImportConfigFilePicker();
+ break;
+ case R.id.fab_add:
+ onAddOrDuplicateProfile(null);
+ break;
+ }
+ }
+
+ private boolean startImportConfigFilePicker() {
+ boolean startOldFileDialog = true;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity() ))
+ startOldFileDialog = !startFilePicker();
+
+ if (startOldFileDialog)
+ startImportConfig();
+
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private boolean startFilePicker() {
+
+ Intent i = Utils.getFilePickerIntent(getActivity(), Utils.FileType.OVPN_CONFIG);
+ if (i != null) {
+ startActivityForResult(i, FILE_PICKER_RESULT_KITKAT);
+ return true;
+ } else
+ return false;
+ }
+
+ private void startImportConfig() {
+ Intent intent = new Intent(getActivity(), FileSelect.class);
+ intent.putExtra(FileSelect.NO_INLINE_SELECTION, true);
+ intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file);
+ startActivityForResult(intent, SELECT_PROFILE);
+ }
+
+
+ private void onAddOrDuplicateProfile(final VpnProfile mCopyProfile) {
+ Context context = getActivity();
+ if (context != null) {
+ final EditText entry = new EditText(context);
+ entry.setSingleLine();
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(context);
+ if (mCopyProfile == null)
+ dialog.setTitle(R.string.menu_add_profile);
+ else {
+ dialog.setTitle(context.getString(R.string.duplicate_profile_title, mCopyProfile.mName));
+ entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName));
+ }
+
+ dialog.setMessage(R.string.add_profile_name_prompt);
+ dialog.setView(entry);
+
+ dialog.setNeutralButton(R.string.menu_import_short,
+ (dialog1, which) -> startImportConfigFilePicker());
+ dialog.setPositiveButton(android.R.string.ok,
+ (dialog12, which) -> {
+ String name = entry.getText().toString();
+ if (getPM().getProfileByName(name) == null) {
+ VpnProfile profile;
+ if (mCopyProfile != null) {
+ profile = mCopyProfile.copy(name);
+ // Remove restrictions on copy profile
+ profile.mProfileCreator = null;
+ profile.mUserEditable = true;
+ } else
+ profile = new VpnProfile(name);
+
+ addProfile(profile);
+ editVPN(profile);
+ } else {
+ Toast.makeText(getActivity(), R.string.duplicate_profile_name, Toast.LENGTH_LONG).show();
+ }
+ });
+ dialog.setNegativeButton(android.R.string.cancel, null);
+ dialog.create().show();
+ }
+
+ }
+
+ private void addProfile(VpnProfile profile) {
+ getPM().addProfile(profile);
+ getPM().saveProfileList(getActivity());
+ getPM().saveProfile(getActivity(), profile);
+ mArrayadapter.add(profile);
+ }
+
+ private ProfileManager getPM() {
+ return ProfileManager.getInstance(getActivity());
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (resultCode == RESULT_VPN_DELETED) {
+ if (mArrayadapter != null && mEditProfile != null)
+ mArrayadapter.remove(mEditProfile);
+ } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) {
+ String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+ VpnProfile profile = ProfileManager.get(getActivity(), profileUUID);
+ if (profile != null)
+ onAddOrDuplicateProfile(profile);
+ }
+
+
+ if (resultCode != Activity.RESULT_OK)
+ return;
+
+
+ if (requestCode == START_VPN_CONFIG) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN);
+ getPM().saveProfile(getActivity(), profile);
+ // Name could be modified, reset List adapter
+ setListAdapter();
+
+ } else if (requestCode == SELECT_PROFILE) {
+ String fileData = data.getStringExtra(FileSelect.RESULT_DATA);
+ Uri uri = new Uri.Builder().path(fileData).scheme("file").build();
+
+ startConfigImport(uri);
+ } else if (requestCode == IMPORT_PROFILE) {
+ String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+ mArrayadapter.add(ProfileManager.get(getActivity(), profileUUID));
+ } else if (requestCode == FILE_PICKER_RESULT_KITKAT) {
+ if (data != null) {
+ Uri uri = data.getData();
+ startConfigImport(uri);
+ }
+ }
+
+ }
+
+ private void startConfigImport(Uri uri) {
+ Intent startImport = new Intent(getActivity(), ConfigConverter.class);
+ startImport.setAction(ConfigConverter.IMPORT_PROFILE);
+ startImport.setData(uri);
+ startActivityForResult(startImport, IMPORT_PROFILE);
+ }
+
+
+ private void editVPN(VpnProfile profile) {
+ mEditProfile = profile;
+ Intent vprefintent = new Intent(getActivity(), VPNPreferences.class)
+ .putExtra(getActivity().getPackageName() + ".profileUUID", profile.getUUID().toString());
+
+ startActivityForResult(vprefintent, START_VPN_CONFIG);
+ }
+
+ private void startVPN(VpnProfile profile) {
+
+ getPM().saveProfile(getActivity(), profile);
+
+ Intent intent = new Intent(getActivity(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java b/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java
new file mode 100644
index 00000000..e8328f5c
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2012-2018 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+import java.util.Collection;
+
+public class DefaultVPNListPreference extends ListPreference {
+ public DefaultVPNListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setVPNs(context);
+ }
+
+ private void setVPNs(Context c) {
+ ProfileManager pm = ProfileManager.getInstance(c);
+ Collection<VpnProfile> profiles = pm.getProfiles();
+ CharSequence[] entries = new CharSequence[profiles.size()];
+ CharSequence[] entryValues = new CharSequence[profiles.size()];;
+
+ int i=0;
+ for (VpnProfile p: profiles)
+ {
+ entries[i]=p.getName();
+ entryValues[i]=p.getUUIDString();
+ i++;
+ }
+
+ setEntries(entries);
+ setEntryValues(entryValues);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java b/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java
new file mode 100644
index 00000000..bc3bd5cd
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java
@@ -0,0 +1,189 @@
+/*
+ * 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 de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.io.IOException;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.X509Utils;
+import de.blinkt.openvpn.fragments.Utils;
+
+import static android.os.Build.VERSION;
+import static android.os.Build.VERSION_CODES;
+
+
+public class FileSelectLayout extends LinearLayout implements OnClickListener {
+
+
+ public void parseResponse(Intent data, Context c) {
+
+ try {
+ String newData = Utils.getFilePickerResult(fileType, data, c);
+ if (newData!=null)
+ setData(newData, c);
+
+ if (newData == null) {
+ String fileData = data.getStringExtra(FileSelect.RESULT_DATA);
+ setData(fileData, c);
+ }
+
+ } catch (IOException | SecurityException e) {
+ VpnStatus.logException(e);
+ }
+ }
+
+ public interface FileSelectCallback {
+
+ String getString(int res);
+
+ void startActivityForResult(Intent startFC, int mTaskId);
+ }
+
+ private boolean mIsCertificate;
+ private TextView mDataView;
+ private String mData;
+ private FileSelectCallback mFragment;
+ private int mTaskId;
+ private Button mSelectButton;
+ private Utils.FileType fileType;
+ private String mTitle;
+ private boolean mShowClear;
+ private TextView mDataDetails;
+ private Button mShowClearButton;
+
+
+ public FileSelectLayout(Context context, AttributeSet attrset) {
+ super(context, attrset);
+
+ TypedArray ta = context.obtainStyledAttributes(attrset, R.styleable.FileSelectLayout);
+
+ setupViews(ta.getString(R.styleable.FileSelectLayout_fileTitle), ta.getBoolean(R.styleable.FileSelectLayout_certificate, true)
+ );
+
+ ta.recycle();
+ }
+
+ public FileSelectLayout (Context context, String title, boolean isCertificate, boolean showClear)
+ {
+ super(context);
+
+ setupViews(title, isCertificate);
+ mShowClear = showClear;
+
+ }
+
+ private void setupViews(String title, boolean isCertificate) {
+ inflate(getContext(), R.layout.file_select, this);
+
+ mTitle = title;
+ mIsCertificate = isCertificate;
+
+ TextView tView = (TextView) findViewById(R.id.file_title);
+ tView.setText(mTitle);
+
+ mDataView = (TextView) findViewById(R.id.file_selected_item);
+ mDataDetails = (TextView) findViewById(R.id.file_selected_description);
+ mSelectButton = (Button) findViewById(R.id.file_select_button);
+ mSelectButton.setOnClickListener(this);
+
+ mShowClearButton = (Button) findViewById(R.id.file_clear_button);
+ mShowClearButton.setOnClickListener(this);
+ }
+
+ public void setClearable(boolean clearable)
+ {
+ mShowClear = clearable;
+ if (mShowClearButton != null && mData !=null)
+ mShowClearButton.setVisibility(mShowClear? VISIBLE : GONE);
+
+ }
+
+
+ public void setCaller(FileSelectCallback fragment, int i, Utils.FileType ft) {
+ mTaskId = i;
+ mFragment = fragment;
+ fileType = ft;
+ }
+
+ public void getCertificateFileDialog() {
+ Intent startFC = new Intent(getContext(), FileSelect.class);
+ startFC.putExtra(FileSelect.START_DATA, mData);
+ startFC.putExtra(FileSelect.WINDOW_TITLE, mTitle);
+ if (fileType == Utils.FileType.PKCS12)
+ startFC.putExtra(FileSelect.DO_BASE64_ENCODE, true);
+ if (mShowClear)
+ startFC.putExtra(FileSelect.SHOW_CLEAR_BUTTON, true);
+
+ mFragment.startActivityForResult(startFC, mTaskId);
+ }
+
+
+ public String getData() {
+ return mData;
+ }
+
+ public void setData(String data, Context c) {
+ mData = data;
+ if (data == null) {
+ mDataView.setText(c.getString(R.string.no_data));
+ mDataDetails.setText("");
+ mShowClearButton.setVisibility(GONE);
+ } else {
+ if (mData.startsWith(VpnProfile.DISPLAYNAME_TAG)) {
+ mDataView.setText(c.getString(R.string.imported_from_file, VpnProfile.getDisplayName(mData)));
+ } else if (mData.startsWith(VpnProfile.INLINE_TAG))
+ mDataView.setText(R.string.inline_file_data);
+ else
+ mDataView.setText(data);
+ if (mIsCertificate) {
+ mDataDetails.setText(X509Utils.getCertificateFriendlyName(c, data));
+ }
+
+ // Show clear button if it should be shown
+ mShowClearButton.setVisibility(mShowClear? VISIBLE : GONE);
+ }
+
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mSelectButton) {
+ Intent startFilePicker=null;
+ if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
+ startFilePicker = Utils.getFilePickerIntent(getContext(), fileType);
+ }
+
+ if (startFilePicker == null || Utils.alwaysUseOldFileChooser(v.getContext())) {
+ getCertificateFileDialog();
+ } else {
+ mFragment.startActivityForResult(startFilePicker, mTaskId);
+ }
+ } else if (v == mShowClearButton) {
+ setData(null, getContext());
+ }
+ }
+
+
+
+
+ public void setShowClear() {
+ mShowClear = true;
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java b/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java
new file mode 100644
index 00000000..8296a644
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2012-2018 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RadioGroup;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MultiLineRadioGroup extends RadioGroup {
+ private Map<View, Rect> viewRectMap;
+
+ public MultiLineRadioGroup(Context context) {
+ this(context, null);
+ }
+
+ public MultiLineRadioGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ viewRectMap = new HashMap<View, Rect>();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
+ int widthMeasurement = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMeasurement = MeasureSpec.getSize(heightMeasureSpec);
+ switch (getOrientation()){
+ case HORIZONTAL:
+ heightMeasurement = findHorizontalHeight(widthMeasureSpec, heightMeasureSpec);
+ break;
+ case VERTICAL:
+ widthMeasurement = findVerticalWidth(widthMeasureSpec, heightMeasureSpec);
+ break;
+ }
+ setMeasuredDimension(widthMeasurement, heightMeasurement);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int count = getChildCount();
+ for(int x=0; x < count; ++x) {
+ View button = getChildAt(x);
+ Rect dims = viewRectMap.get(button);
+ button.layout(dims.left, dims.top, dims.right, dims.bottom);
+ }
+ }
+
+ private int findHorizontalHeight(int widthMeasureSpec, int heightMeasureSpec){
+ int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+ int maxRight = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight();
+
+ // create MeasureSpecs to accommodate max space that RadioButtons can occupy
+ int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxRight - getPaddingLeft(),
+ MeasureSpec.getMode(widthMeasureSpec));
+ int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ parentHeight - (getPaddingTop() + getPaddingBottom()),
+ MeasureSpec.getMode(heightMeasureSpec));
+
+ int nextLeft = getPaddingLeft();
+ int nextTop = getPaddingTop();
+ int maxRowHeight = 0;
+ viewRectMap.clear();
+ // measure and find placement for each RadioButton (results to be used in onLayout() stage)
+ int count = getChildCount();
+ for(int x=0; x < count; ++x){
+ View button = getChildAt(x);
+ measureChild(button, newWidthMeasureSpec, newHeightMeasureSpec);
+
+ maxRowHeight = Math.max(maxRowHeight, button.getMeasuredHeight());
+
+ // determine RadioButton placement
+ int nextRight = nextLeft + button.getMeasuredWidth();
+ if(nextRight > maxRight){ // if current button will exceed border on this row ...
+ // ... move to next row
+ nextLeft = getPaddingLeft();
+ nextTop += maxRowHeight;
+
+ // adjust for next row values
+ nextRight = nextLeft + button.getMeasuredWidth();
+ maxRowHeight = button.getMeasuredHeight();
+ }
+
+ int nextBottom = nextTop + button.getMeasuredHeight();
+ viewRectMap.put(button, new Rect(nextLeft, nextTop, nextRight, nextBottom));
+
+ // update nextLeft
+ nextLeft = nextRight;
+ }
+
+ // height of RadioGroup is a natural by-product of placing all the children
+ int idealHeight = nextTop + maxRowHeight + getPaddingBottom();
+ switch(MeasureSpec.getMode(heightMeasureSpec)){
+ case MeasureSpec.UNSPECIFIED:
+ return idealHeight;
+ case MeasureSpec.AT_MOST:
+ return Math.min(idealHeight, parentHeight);
+ case MeasureSpec.EXACTLY:
+ default:
+ return parentHeight;
+ }
+ }
+
+ private int findVerticalWidth(int widthMeasureSpec, int heightMeasureSpec){
+ int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int maxBottom = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom();
+
+ // create MeasureSpecs to accommodate max space that RadioButtons can occupy
+ int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ parentWidth - (getPaddingLeft() + getPaddingRight()),
+ MeasureSpec.getMode(widthMeasureSpec));
+ int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxBottom - getPaddingTop(),
+ MeasureSpec.getMode(heightMeasureSpec));
+
+ int nextTop = getPaddingTop();
+ int nextLeft = getPaddingLeft();
+ int maxColWidth = 0;
+ viewRectMap.clear();
+ // measure and find placement for each RadioButton (results to be used in onLayout() stage)
+ int count = getChildCount();
+ for(int x=0; x < count; ++x){
+ View button = getChildAt(x);
+ measureChild(button, newWidthMeasureSpec, newHeightMeasureSpec);
+
+ maxColWidth = Math.max(maxColWidth, button.getMeasuredWidth());
+
+ // determine RadioButton placement
+ int nextBottom = nextTop + button.getMeasuredHeight();
+ if(nextBottom > maxBottom){ // if current button will exceed border for this column ...
+ // ... move to next column
+ nextTop = getPaddingTop();
+ nextLeft += maxColWidth;
+
+ // adjust for next row values
+ nextBottom = nextTop + button.getMeasuredHeight();
+ maxColWidth = button.getMeasuredWidth();
+ }
+
+ int nextRight = nextLeft + button.getMeasuredWidth();
+ viewRectMap.put(button, new Rect(nextLeft, nextTop, nextRight, nextBottom));
+
+ // update nextTop
+ nextTop = nextBottom;
+ }
+
+ // width of RadioGroup is a natural by-product of placing all the children
+ int idealWidth = nextLeft + maxColWidth + getPaddingRight();
+ switch(MeasureSpec.getMode(widthMeasureSpec)){
+ case MeasureSpec.UNSPECIFIED:
+ return idealWidth;
+ case MeasureSpec.AT_MOST:
+ return Math.min(idealWidth, parentWidth);
+ case MeasureSpec.EXACTLY:
+ default:
+ return parentWidth;
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java b/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java
new file mode 100644
index 00000000..ab8598c6
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2013 Andreas Stuetz <andreas.stuetz@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.blinkt.openvpn.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.support.v4n.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Pair;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+
+import java.util.Locale;
+
+import de.blinkt.openvpn.R;
+
+public class PagerSlidingTabStrip extends HorizontalScrollView implements TabBarView {
+
+ private static final float OPAQUE = 1.0f;
+ private static final float HALF_TRANSP = 0.5f;
+
+ public interface CustomTabProvider {
+ public View getCustomTabView(ViewGroup parent, int position);
+ }
+
+ // @formatter:off
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.textSize,
+ android.R.attr.textColor,
+ android.R.attr.paddingLeft,
+ android.R.attr.paddingRight,
+ };
+ // @formatter:on
+
+ private final PagerAdapterObserver adapterObserver = new PagerAdapterObserver();
+
+ //These indexes must be related with the ATTR array above
+ private static final int TEXT_SIZE_INDEX = 0;
+ private static final int TEXT_COLOR_INDEX = 1;
+ private static final int PADDING_LEFT_INDEX = 2;
+ private static final int PADDING_RIGHT_INDEX = 3;
+
+ private LinearLayout.LayoutParams defaultTabLayoutParams;
+ private LinearLayout.LayoutParams expandedTabLayoutParams;
+
+ private final PageListener pageListener = new PageListener();
+ public ViewPager.OnPageChangeListener delegatePageListener;
+
+ private LinearLayout tabsContainer;
+ private ViewPager pager;
+
+ private int tabCount;
+
+ private int currentPosition = 0;
+ private float currentPositionOffset = 0f;
+
+ private Paint rectPaint;
+ private Paint dividerPaint;
+
+ private int indicatorColor;
+ private int indicatorHeight = 2;
+
+ private int underlineHeight = 0;
+ private int underlineColor;
+
+ private int dividerWidth = 0;
+ private int dividerPadding = 0;
+ private int dividerColor;
+
+ private int tabPadding = 12;
+ private int tabTextSize = 14;
+ private ColorStateList tabTextColor = null;
+ private float tabTextAlpha = HALF_TRANSP;
+ private float tabTextSelectedAlpha = OPAQUE;
+
+ private int paddingLeft = 0;
+ private int paddingRight = 0;
+
+ private boolean shouldExpand = false;
+ private boolean textAllCaps = true;
+ private boolean isPaddingMiddle = false;
+
+ private Typeface tabTypeface = null;
+ private int tabTypefaceStyle = Typeface.BOLD;
+ private int tabTypefaceSelectedStyle = Typeface.BOLD;
+
+ private int scrollOffset;
+ private int lastScrollX = 0;
+
+ private int tabBackgroundResId = R.drawable.slidingtab_background;
+
+ private Locale locale;
+
+ public PagerSlidingTabStrip(Context context) {
+ this(context, null);
+ }
+
+ public PagerSlidingTabStrip(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PagerSlidingTabStrip(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setFillViewport(true);
+ setWillNotDraw(false);
+ tabsContainer = new LinearLayout(context);
+ tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
+ tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ addView(tabsContainer);
+
+ //Default color will be 'textColorPrimary'
+ int colorPrimary = context.getResources().getColor(android.R.color.primary_text_dark);
+ setTextColor(colorPrimary);
+ underlineColor = colorPrimary;
+ dividerColor = colorPrimary;
+ indicatorColor = colorPrimary;
+
+
+ DisplayMetrics dm = getResources().getDisplayMetrics();
+ scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm);
+ indicatorHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, indicatorHeight, dm);
+ underlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, underlineHeight, dm);
+ dividerPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerPadding, dm);
+ tabPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tabPadding, dm);
+ dividerWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerWidth, dm);
+ tabTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, tabTextSize, dm);
+
+ // get system attrs (android:textSize and android:textColor)
+ TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+ tabTextSize = a.getDimensionPixelSize(TEXT_SIZE_INDEX, tabTextSize);
+ ColorStateList colorStateList = a.getColorStateList(TEXT_COLOR_INDEX);
+ if (colorStateList != null) {
+ tabTextColor = colorStateList;
+ }
+ paddingLeft = a.getDimensionPixelSize(PADDING_LEFT_INDEX, paddingLeft);
+ paddingRight = a.getDimensionPixelSize(PADDING_RIGHT_INDEX, paddingRight);
+ a.recycle();
+
+ //In case we have the padding they must be equal so we take the biggest
+ if (paddingRight < paddingLeft) {
+ paddingRight = paddingLeft;
+ }
+
+ if (paddingLeft < paddingRight) {
+ paddingLeft = paddingRight;
+ }
+
+ // get custom attrs
+ a = context.obtainStyledAttributes(attrs, R.styleable.PagerSlidingTabStrip);
+ indicatorColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsIndicatorColor, indicatorColor);
+ underlineColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsUnderlineColor, underlineColor);
+ dividerColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsDividerColor, dividerColor);
+ dividerWidth = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerWidth, dividerWidth);
+ indicatorHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsIndicatorHeight, indicatorHeight);
+ underlineHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsUnderlineHeight, underlineHeight);
+ dividerPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerPadding, dividerPadding);
+ tabPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsTabPaddingLeftRight, tabPadding);
+ tabBackgroundResId = a.getResourceId(R.styleable.PagerSlidingTabStrip_pstsTabBackground, tabBackgroundResId);
+ shouldExpand = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsShouldExpand, shouldExpand);
+ scrollOffset = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsScrollOffset, scrollOffset);
+ textAllCaps = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsTextAllCaps, textAllCaps);
+ isPaddingMiddle = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsPaddingMiddle, isPaddingMiddle);
+ tabTypefaceStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextStyle, Typeface.BOLD);
+ tabTypefaceSelectedStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextSelectedStyle, Typeface.BOLD);
+ tabTextAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextAlpha, HALF_TRANSP);
+ tabTextSelectedAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextSelectedAlpha, OPAQUE);
+ a.recycle();
+
+ rectPaint = new Paint();
+ rectPaint.setAntiAlias(true);
+ rectPaint.setStyle(Style.FILL);
+
+
+ dividerPaint = new Paint();
+ dividerPaint.setAntiAlias(true);
+ dividerPaint.setStrokeWidth(dividerWidth);
+
+ defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ expandedTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
+
+ if (locale == null) {
+ locale = getResources().getConfiguration().locale;
+ }
+ }
+
+ public void setViewPager(ViewPager pager) {
+ this.pager = pager;
+ if (pager.getAdapter() == null) {
+ throw new IllegalStateException("ViewPager does not have adapter instance.");
+ }
+
+ pager.setOnPageChangeListener(pageListener);
+ pager.getAdapter().registerDataSetObserver(adapterObserver);
+ adapterObserver.setAttached(true);
+ notifyDataSetChanged();
+ }
+
+ public void notifyDataSetChanged() {
+ tabsContainer.removeAllViews();
+ tabCount = pager.getAdapter().getCount();
+ View tabView;
+ for (int i = 0; i < tabCount; i++) {
+
+ if (pager.getAdapter() instanceof CustomTabProvider) {
+ tabView = ((CustomTabProvider) pager.getAdapter()).getCustomTabView(this, i);
+ } else {
+ tabView = LayoutInflater.from(getContext()).inflate(R.layout.padersliding_tab, this, false);
+ }
+
+ CharSequence title = pager.getAdapter().getPageTitle(i);
+
+ addTab(i, title, tabView);
+ }
+
+ updateTabStyles();
+ getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ @Override
+ public void onGlobalLayout() {
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ } else {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+
+ currentPosition = pager.getCurrentItem();
+ currentPositionOffset = 0f;
+ scrollToChild(currentPosition, 0);
+ updateSelection(currentPosition);
+ }
+ });
+ }
+
+ private void addTab(final int position, CharSequence title, View tabView) {
+ TextView textView = (TextView) tabView.findViewById(R.id.tab_title);
+ if (textView != null) {
+ if (title != null) textView.setText(title);
+ float alpha = pager.getCurrentItem() == position ? tabTextSelectedAlpha : tabTextAlpha;
+ ViewCompat.setAlpha(textView, alpha);
+ }
+
+ tabView.setFocusable(true);
+ tabView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (pager.getCurrentItem() != position) {
+ View tab = tabsContainer.getChildAt(pager.getCurrentItem());
+ notSelected(tab);
+ pager.setCurrentItem(position);
+ }
+ }
+ });
+
+ tabView.setPadding(tabPadding, tabView.getPaddingTop(), tabPadding, tabView.getPaddingBottom());
+ tabsContainer.addView(tabView, position, shouldExpand ? expandedTabLayoutParams : defaultTabLayoutParams);
+ }
+
+ private void updateTabStyles() {
+ for (int i = 0; i < tabCount; i++) {
+ View v = tabsContainer.getChildAt(i);
+ v.setBackgroundResource(tabBackgroundResId);
+ TextView tab_title = (TextView) v.findViewById(R.id.tab_title);
+
+ if (tab_title != null) {
+ tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize);
+ tab_title.setTypeface(tabTypeface, pager.getCurrentItem() == i ? tabTypefaceSelectedStyle : tabTypefaceStyle);
+ if (tabTextColor != null) {
+ tab_title.setTextColor(tabTextColor);
+ }
+ // setAllCaps() is only available from API 14, so the upper case is made manually if we are on a
+ // pre-ICS-build
+ if (textAllCaps) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ tab_title.setAllCaps(true);
+ } else {
+ tab_title.setText(tab_title.getText().toString().toUpperCase(locale));
+ }
+ }
+ }
+ }
+
+ }
+
+ private void scrollToChild(int position, int offset) {
+ if (tabCount == 0) {
+ return;
+ }
+
+ int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;
+ if (position > 0 || offset > 0) {
+
+ //Half screen offset.
+ //- Either tabs start at the middle of the view scrolling straight away
+ //- Or tabs start at the begging (no padding) scrolling when indicator gets
+ // to the middle of the view width
+ newScrollX -= scrollOffset;
+ Pair<Float, Float> lines = getIndicatorCoordinates();
+ newScrollX += ((lines.second - lines.first) / 2);
+ }
+
+ if (newScrollX != lastScrollX) {
+ lastScrollX = newScrollX;
+ scrollTo(newScrollX, 0);
+ }
+ }
+
+ private Pair<Float, Float> getIndicatorCoordinates() {
+ // default: line below current tab
+ View currentTab = tabsContainer.getChildAt(currentPosition);
+ float lineLeft = currentTab.getLeft();
+ float lineRight = currentTab.getRight();
+
+ // if there is an offset, start interpolating left and right coordinates between current and next tab
+ if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
+
+ View nextTab = tabsContainer.getChildAt(currentPosition + 1);
+ final float nextTabLeft = nextTab.getLeft();
+ final float nextTabRight = nextTab.getRight();
+
+ lineLeft = (currentPositionOffset * nextTabLeft + (1f - currentPositionOffset) * lineLeft);
+ lineRight = (currentPositionOffset * nextTabRight + (1f - currentPositionOffset) * lineRight);
+ }
+ return new Pair<Float, Float>(lineLeft, lineRight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (isInEditMode() || tabCount == 0) {
+ return;
+ }
+
+ final int height = getHeight();
+ // draw indicator line
+ rectPaint.setColor(indicatorColor);
+ Pair<Float, Float> lines = getIndicatorCoordinates();
+ canvas.drawRect(lines.first + paddingLeft, height - indicatorHeight, lines.second + paddingRight, height, rectPaint);
+ // draw underline
+ rectPaint.setColor(underlineColor);
+ canvas.drawRect(paddingLeft, height - underlineHeight, tabsContainer.getWidth() + paddingRight, height, rectPaint);
+ // draw divider
+ if (dividerWidth != 0) {
+ dividerPaint.setStrokeWidth(dividerWidth);
+ dividerPaint.setColor(dividerColor);
+ for (int i = 0; i < tabCount - 1; i++) {
+ View tab = tabsContainer.getChildAt(i);
+ canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (isPaddingMiddle) {
+ //Make sure tabContainer is bigger than the HorizontalScrollView to be able to scroll
+ tabsContainer.setMinimumWidth(getWidth());
+ int halfFirstTab = 0;
+ if (tabsContainer.getChildCount() > 0) {
+ halfFirstTab = (tabsContainer.getChildAt(0).getWidth() / 2);
+ }
+ //The user choose the tabs to start in the middle of the view width (padding)
+ paddingLeft = paddingRight = getWidth() / 2 - halfFirstTab;
+ //Clipping padding to false to see the tabs while we pass them swiping
+ setClipToPadding(false);
+ }
+
+ if (scrollOffset == 0) scrollOffset = getWidth() / 2 - paddingLeft;
+ setPadding(paddingLeft, getPaddingTop(), paddingRight, getPaddingBottom());
+ super.onLayout(changed, l, t, r, b);
+ }
+
+ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
+ this.delegatePageListener = listener;
+ }
+
+ private class PageListener implements ViewPager.OnPageChangeListener {
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ currentPosition = position;
+ currentPositionOffset = positionOffset;
+ int offset = tabCount > 0 ? (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()) : 0;
+ scrollToChild(position, offset);
+ invalidate();
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_IDLE) {
+ scrollToChild(pager.getCurrentItem(), 0);
+ }
+ //Full alpha for current item
+ View currentTab = tabsContainer.getChildAt(pager.getCurrentItem());
+ selected(currentTab);
+ //Half transparent for prev item
+ if (pager.getCurrentItem() - 1 >= 0) {
+ View prevTab = tabsContainer.getChildAt(pager.getCurrentItem() - 1);
+ notSelected(prevTab);
+ }
+ //Half transparent for next item
+ if (pager.getCurrentItem() + 1 <= pager.getAdapter().getCount() - 1) {
+ View nextTab = tabsContainer.getChildAt(pager.getCurrentItem() + 1);
+ notSelected(nextTab);
+ }
+
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageScrollStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ updateSelection(position);
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageSelected(position);
+ }
+ }
+
+ }
+
+ private void updateSelection(int position) {
+ for (int i = 0; i < tabCount; ++i) {
+ View tv = tabsContainer.getChildAt(i);
+ tv.setSelected(i == position);
+ }
+ }
+
+ private void notSelected(View tab) {
+ TextView title = (TextView) tab.findViewById(R.id.tab_title);
+ if (title != null) {
+ title.setTypeface(tabTypeface, tabTypefaceStyle);
+ ViewCompat.setAlpha(title, tabTextAlpha);
+ }
+ }
+
+ private void selected(View tab) {
+ TextView title = (TextView) tab.findViewById(R.id.tab_title);
+ if (title != null) {
+ title.setTypeface(tabTypeface, tabTypefaceSelectedStyle);
+ ViewCompat.setAlpha(title, tabTextSelectedAlpha);
+ }
+ }
+
+ private class PagerAdapterObserver extends DataSetObserver {
+
+ private boolean attached = false;
+
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ public void setAttached(boolean attached) {
+ this.attached = attached;
+ }
+
+ public boolean isAttached() {
+ return attached;
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (pager != null) {
+ if (!adapterObserver.isAttached()) {
+ pager.getAdapter().registerDataSetObserver(adapterObserver);
+ adapterObserver.setAttached(true);
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (pager != null) {
+ if (adapterObserver.isAttached()) {
+ pager.getAdapter().unregisterDataSetObserver(adapterObserver);
+ adapterObserver.setAttached(false);
+ }
+ }
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ currentPosition = savedState.currentPosition;
+ if (currentPosition != 0 && tabsContainer.getChildCount() > 0) {
+ notSelected(tabsContainer.getChildAt(0));
+ selected(tabsContainer.getChildAt(currentPosition));
+ }
+ requestLayout();
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState savedState = new SavedState(superState);
+ savedState.currentPosition = currentPosition;
+ return savedState;
+ }
+
+ static class SavedState extends BaseSavedState {
+ int currentPosition;
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ currentPosition = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(currentPosition);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ public int getIndicatorColor() {
+ return this.indicatorColor;
+ }
+
+ public int getIndicatorHeight() {
+ return indicatorHeight;
+ }
+
+ public int getUnderlineColor() {
+ return underlineColor;
+ }
+
+ public int getDividerColor() {
+ return dividerColor;
+ }
+
+ public int getDividerWidth() {
+ return dividerWidth;
+ }
+
+ public int getUnderlineHeight() {
+ return underlineHeight;
+ }
+
+ public int getDividerPadding() {
+ return dividerPadding;
+ }
+
+ public int getScrollOffset() {
+ return scrollOffset;
+ }
+
+ public boolean getShouldExpand() {
+ return shouldExpand;
+ }
+
+ public int getTextSize() {
+ return tabTextSize;
+ }
+
+ public boolean isTextAllCaps() {
+ return textAllCaps;
+ }
+
+ public ColorStateList getTextColor() {
+ return tabTextColor;
+ }
+
+ public int getTabBackground() {
+ return tabBackgroundResId;
+ }
+
+ public int getTabPaddingLeftRight() {
+ return tabPadding;
+ }
+
+ public void setIndicatorColor(int indicatorColor) {
+ this.indicatorColor = indicatorColor;
+ invalidate();
+ }
+
+ public void setIndicatorColorResource(int resId) {
+ this.indicatorColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public void setIndicatorHeight(int indicatorLineHeightPx) {
+ this.indicatorHeight = indicatorLineHeightPx;
+ invalidate();
+ }
+
+ public void setUnderlineColor(int underlineColor) {
+ this.underlineColor = underlineColor;
+ invalidate();
+ }
+
+ public void setUnderlineColorResource(int resId) {
+ this.underlineColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public void setDividerColor(int dividerColor) {
+ this.dividerColor = dividerColor;
+ invalidate();
+ }
+
+ public void setDividerColorResource(int resId) {
+ this.dividerColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public void setDividerWidth(int dividerWidthPx) {
+ this.dividerWidth = dividerWidthPx;
+ invalidate();
+ }
+
+ public void setUnderlineHeight(int underlineHeightPx) {
+ this.underlineHeight = underlineHeightPx;
+ invalidate();
+ }
+
+ public void setDividerPadding(int dividerPaddingPx) {
+ this.dividerPadding = dividerPaddingPx;
+ invalidate();
+ }
+
+ public void setScrollOffset(int scrollOffsetPx) {
+ this.scrollOffset = scrollOffsetPx;
+ invalidate();
+ }
+
+ public void setShouldExpand(boolean shouldExpand) {
+ this.shouldExpand = shouldExpand;
+ if (pager != null) {
+ requestLayout();
+ }
+ }
+
+ public void setAllCaps(boolean textAllCaps) {
+ this.textAllCaps = textAllCaps;
+ }
+
+ public void setTextSize(int textSizePx) {
+ this.tabTextSize = textSizePx;
+ updateTabStyles();
+ }
+
+ public void setTextColor(int textColor) {
+ setTextColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{textColor}));
+ }
+
+ public void setTextColor(ColorStateList colorStateList) {
+ this.tabTextColor = colorStateList;
+ updateTabStyles();
+ }
+
+ public void setTextColorResource(int resId) {
+ setTextColor(getResources().getColor(resId));
+ }
+
+ public void setTextColorStateListResource(int resId) {
+ setTextColor(getResources().getColorStateList(resId));
+ }
+
+ public void setTypeface(Typeface typeface, int style) {
+ this.tabTypeface = typeface;
+ this.tabTypefaceSelectedStyle = style;
+ updateTabStyles();
+ }
+
+ public void setTabBackground(int resId) {
+ this.tabBackgroundResId = resId;
+ }
+
+ public void setTabPaddingLeftRight(int paddingPx) {
+ this.tabPadding = paddingPx;
+ updateTabStyles();
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java b/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java
new file mode 100644
index 00000000..4b477f9c
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java
@@ -0,0 +1,146 @@
+/*
+ * 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 de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+
+public class RemoteCNPreference extends DialogPreference {
+
+
+ private Spinner mSpinner;
+ private EditText mEditText;
+ private int mDNType;
+ private String mDn;
+ private TextView mRemoteTLSNote;
+ //private ScrollView mScrollView;
+
+ public RemoteCNPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setDialogLayoutResource(R.layout.tlsremote);
+
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+
+ super.onBindDialogView(view);
+
+ mEditText = (EditText) view.findViewById(R.id.tlsremotecn);
+ mSpinner = (Spinner) view.findViewById(R.id.x509verifytype);
+ mRemoteTLSNote = (TextView) view.findViewById(R.id.tlsremotenote);
+ //mScrollView = (ScrollView) view.findViewById(R.id.tlsremotescroll);
+ if(mDn!=null)
+ mEditText.setText(mDn);
+
+ populateSpinner();
+
+ }
+
+
+
+ public String getCNText() {
+ return mDn;
+ }
+
+ public int getAuthtype() {
+ return mDNType;
+ }
+
+ public void setDN(String dn) {
+ mDn = dn;
+ if(mEditText!=null)
+ mEditText.setText(dn);
+ }
+
+ public void setAuthType(int x509authtype) {
+ mDNType = x509authtype;
+ if (mSpinner!=null)
+ populateSpinner();
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult) {
+ String dn = mEditText.getText().toString();
+ int authtype = getAuthTypeFromSpinner();
+ if (callChangeListener(new Pair<Integer, String>(authtype, dn))) {
+ mDn = dn;
+ mDNType = authtype;
+ }
+ }
+ }
+
+ private void populateSpinner() {
+ ArrayAdapter<String> authtypes = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item);
+ authtypes.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ authtypes.add(getContext().getString(R.string.complete_dn));
+ authtypes.add(getContext().getString(R.string.rdn));
+ authtypes.add(getContext().getString(R.string.rdn_prefix));
+ if ((mDNType == VpnProfile.X509_VERIFY_TLSREMOTE || mDNType == VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING)
+ && !(mDn==null || "".equals(mDn))) {
+ authtypes.add(getContext().getString(R.string.tls_remote_deprecated));
+ mRemoteTLSNote.setVisibility(View.VISIBLE);
+ } else {
+ mRemoteTLSNote.setVisibility(View.GONE);
+ }
+ mSpinner.setAdapter(authtypes);
+ mSpinner.setSelection(getSpinnerPositionFromAuthTYPE());
+ }
+
+ private int getSpinnerPositionFromAuthTYPE() {
+ switch (mDNType) {
+ case VpnProfile.X509_VERIFY_TLSREMOTE_DN:
+ return 0;
+ case VpnProfile.X509_VERIFY_TLSREMOTE_RDN:
+ return 1;
+ case VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX:
+ return 2;
+ case VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING:
+ case VpnProfile.X509_VERIFY_TLSREMOTE:
+ if (mDn==null || "".equals(mDn))
+ return 1;
+ else
+ return 3;
+
+
+ default:
+ return 0;
+ }
+ }
+
+ private int getAuthTypeFromSpinner() {
+ int pos = mSpinner.getSelectedItemPosition();
+ switch (pos) {
+ case 0:
+ return VpnProfile.X509_VERIFY_TLSREMOTE_DN;
+ case 1:
+ return VpnProfile.X509_VERIFY_TLSREMOTE_RDN;
+ case 2:
+ return VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX;
+ case 3:
+ // This is the tls-remote entry, only visible if mDntype is a
+ // tls-remote type
+ return mDNType;
+ default:
+ return VpnProfile.X509_VERIFY_TLSREMOTE;
+ }
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java b/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java
new file mode 100644
index 00000000..38bb54b5
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java
@@ -0,0 +1,79 @@
+/*
+ * 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 de.blinkt.openvpn.views;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.annotation.StringRes;
+import android.support.v4n.app.FragmentStatePagerAdapter;
+
+import java.util.Vector;
+
+import de.blinkt.openvpn.activities.MainActivity;
+
+/**
+* Created by arne on 18.11.14.
+*/
+public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
+
+ private final Resources res;
+ private Bundle mFragmentArguments;
+
+ public void setFragmentArgs(Bundle fragmentArguments) {
+ mFragmentArguments = fragmentArguments;
+ }
+
+ static class Tab {
+ public Class<? extends Fragment> fragmentClass;
+ String mName;
+
+ public Tab(Class<? extends Fragment> fClass, String name){
+ mName = name;
+ fragmentClass = fClass;
+ }
+
+ }
+
+
+ private Vector<Tab> mTabs = new Vector<Tab>();
+
+ public ScreenSlidePagerAdapter(FragmentManager fm, Context c) {
+ super(fm);
+ res = c.getResources();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ try {
+ Fragment fragment = mTabs.get(position).fragmentClass.newInstance();
+ if (mFragmentArguments!=null)
+ fragment.setArguments(mFragmentArguments);
+ return fragment;
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mTabs.get(position).mName;
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ public void addTab(@StringRes int name, Class<? extends Fragment> fragmentClass) {
+ mTabs.add(new Tab(fragmentClass, res.getString(name)));
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java b/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java
new file mode 100644
index 00000000..347ce708
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java
@@ -0,0 +1,73 @@
+/*
+ * 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 de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.ViewConfiguration;
+import android.widget.SeekBar;
+
+public class SeekBarTicks extends SeekBar {
+ private Paint mTickPaint;
+ private float mTickHeight;
+
+ private float tickHeightRatio = 0.6f;
+
+ public SeekBarTicks(Context context, AttributeSet attrs) {
+ super (context, attrs);
+
+ initTicks (context, attrs, android.R.attr.seekBarStyle);
+ }
+
+
+ public SeekBarTicks(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ initTicks (context, attrs, defStyle);
+
+ /*mTickHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ tickHeightDP,
+ ctx.getResources().getDisplayMetrics()); */
+ }
+
+ private void initTicks(Context context, AttributeSet attrs, int defStyle) {
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ new int[] { android.R.attr.secondaryProgress }, defStyle, 0);
+
+ mTickPaint = new Paint();
+ //noinspection deprecation
+ mTickPaint.setColor( context.getResources().getColor(android.R.color.black));
+ a.recycle();
+ }
+
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ drawTicks(canvas);
+ super.onDraw(canvas);
+ }
+
+ private void drawTicks(Canvas canvas) {
+
+ final int available = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int availableHeight = getHeight() - getPaddingBottom() - getPaddingTop();
+
+ int extrapadding = (int) ((availableHeight- (availableHeight * tickHeightRatio))/2);
+
+ int tickSpacing = available / (getMax() );
+
+ for (int i = 1; i < getMax(); i++) {
+ final float x = getPaddingLeft() + i * tickSpacing;
+
+ canvas.drawLine(x, getPaddingTop()+extrapadding, x, getHeight()-getPaddingBottom()-extrapadding, mTickPaint);
+ }
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java
new file mode 100644
index 00000000..ea3b1c26
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.support.v4n.view.PagerAdapter;
+import android.support.v4n.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+import android.widget.TextView;
+
+/**
+ * To be used with ViewPager to provide a tab indicator component which give constant feedback as to
+ * the user's scroll progress.
+ * <p>
+ * To use the component, simply add it to your view hierarchy. Then in your
+ * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call
+ * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for.
+ * <p>
+ * The colors can be customized in two ways. The first and simplest is to provide an array of colors
+ * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The
+ * alternative is via the {@link TabColorizer} interface which provides you complete control over
+ * which color is used for any individual position.
+ * <p>
+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)},
+ * providing the layout ID of your custom layout.
+ */
+public class SlidingTabLayout extends HorizontalScrollView implements TabBarView {
+
+ /**
+ * Allows complete control over the colors drawn in the tab layout. Set with
+ * {@link #setCustomTabColorizer(TabColorizer)}.
+ */
+ public interface TabColorizer {
+
+ /**
+ * @return return the color of the indicator used when {@code position} is selected.
+ */
+ int getIndicatorColor(int position);
+
+ /**
+ * @return return the color of the divider drawn to the right of {@code position}.
+ */
+ int getDividerColor(int position);
+
+ }
+
+ private static final int TITLE_OFFSET_DIPS = 24;
+ private static final int TAB_VIEW_PADDING_DIPS = 16;
+ private static final int TAB_VIEW_TEXT_SIZE_SP = 12;
+
+ private int mTitleOffset;
+
+ private int mTabViewLayoutId;
+ private int mTabViewTextViewId;
+
+ private ViewPager mViewPager;
+ private ViewPager.OnPageChangeListener mViewPagerPageChangeListener;
+
+ private final SlidingTabStrip mTabStrip;
+
+ public SlidingTabLayout(Context context) {
+ this(context, null);
+ }
+
+ public SlidingTabLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ // Disable the Scroll Bar
+ setHorizontalScrollBarEnabled(false);
+ // Make sure that the Tab Strips fills this View
+ setFillViewport(true);
+
+ mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);
+
+ mTabStrip = new SlidingTabStrip(context);
+ addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * Set the custom {@link TabColorizer} to be used.
+ *
+ * If you only require simple custmisation then you can use
+ * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve
+ * similar effects.
+ */
+ public void setCustomTabColorizer(TabColorizer tabColorizer) {
+ mTabStrip.setCustomTabColorizer(tabColorizer);
+ }
+
+ /**
+ * Sets the colors to be used for indicating the selected tab. These colors are treated as a
+ * circular array. Providing one color will mean that all tabs are indicated with the same color.
+ */
+ public void setSelectedIndicatorColors(int... colors) {
+ mTabStrip.setSelectedIndicatorColors(colors);
+ }
+
+ /**
+ * Sets the colors to be used for tab dividers. These colors are treated as a circular array.
+ * Providing one color will mean that all tabs are indicated with the same color.
+ */
+ public void setDividerColors(int... colors) {
+ mTabStrip.setDividerColors(colors);
+ }
+
+ /**
+ * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are
+ * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so
+ * that the layout can update it's scroll position correctly.
+ *
+ * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener)
+ */
+ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
+ mViewPagerPageChangeListener = listener;
+ }
+
+ /**
+ * Set the custom layout to be inflated for the tab views.
+ *
+ * @param layoutResId Layout id to be inflated
+ * @param textViewId id of the {@link TextView} in the inflated view
+ */
+ public void setCustomTabView(int layoutResId, int textViewId) {
+ mTabViewLayoutId = layoutResId;
+ mTabViewTextViewId = textViewId;
+ }
+
+ /**
+ * Sets the associated view pager. Note that the assumption here is that the pager content
+ * (number of tabs and tab titles) does not change after this call has been made.
+ */
+ public void setViewPager(ViewPager viewPager) {
+ mTabStrip.removeAllViews();
+
+ mViewPager = viewPager;
+ if (viewPager != null) {
+ viewPager.setOnPageChangeListener(new InternalViewPagerListener());
+ populateTabStrip();
+ }
+ }
+
+ /**
+ * Create a default view to be used for tabs. This is called if a custom tab view is not set via
+ * {@link #setCustomTabView(int, int)}.
+ */
+ protected TextView createDefaultTabView(Context context) {
+ TextView textView = new TextView(context);
+ textView.setGravity(Gravity.CENTER);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP);
+ textView.setTypeface(Typeface.DEFAULT_BOLD);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ // If we're running on Honeycomb or newer, then we can use the Theme's
+ // selectableItemBackground to ensure that the View has a pressed state
+ TypedValue outValue = new TypedValue();
+ getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
+ outValue, true);
+ textView.setBackgroundResource(outValue.resourceId);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style
+ textView.setAllCaps(true);
+ }
+
+ int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density);
+ textView.setPadding(padding, padding, padding, padding);
+
+ return textView;
+ }
+
+ private void populateTabStrip() {
+ final PagerAdapter adapter = mViewPager.getAdapter();
+ final View.OnClickListener tabClickListener = new TabClickListener();
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ View tabView = null;
+ TextView tabTitleView = null;
+
+ if (mTabViewLayoutId != 0) {
+ // If there is a custom tab view layout id set, try and inflate it
+ tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip,
+ false);
+ tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId);
+ }
+
+ if (tabView == null) {
+ tabView = createDefaultTabView(getContext());
+ }
+
+ if (tabTitleView == null && TextView.class.isInstance(tabView)) {
+ tabTitleView = (TextView) tabView;
+ }
+
+ tabTitleView.setText(adapter.getPageTitle(i));
+ tabView.setOnClickListener(tabClickListener);
+
+ mTabStrip.addView(tabView);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mViewPager != null) {
+ scrollToTab(mViewPager.getCurrentItem(), 0);
+ }
+ }
+
+ private void scrollToTab(int tabIndex, int positionOffset) {
+ final int tabStripChildCount = mTabStrip.getChildCount();
+ if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
+ return;
+ }
+
+ View selectedChild = mTabStrip.getChildAt(tabIndex);
+ if (selectedChild != null) {
+ int targetScrollX = selectedChild.getLeft() + positionOffset;
+
+ if (tabIndex > 0 || positionOffset > 0) {
+ // If we're not at the first child and are mid-scroll, make sure we obey the offset
+ targetScrollX -= mTitleOffset;
+ }
+
+ scrollTo(targetScrollX, 0);
+ }
+ }
+
+ private class InternalViewPagerListener implements ViewPager.OnPageChangeListener {
+ private int mScrollState;
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ int tabStripChildCount = mTabStrip.getChildCount();
+ if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+ return;
+ }
+
+ mTabStrip.onViewPagerPageChanged(position, positionOffset);
+
+ View selectedTitle = mTabStrip.getChildAt(position);
+ int extraOffset = (selectedTitle != null)
+ ? (int) (positionOffset * selectedTitle.getWidth())
+ : 0;
+ scrollToTab(position, extraOffset);
+
+ if (mViewPagerPageChangeListener != null) {
+ mViewPagerPageChangeListener.onPageScrolled(position, positionOffset,
+ positionOffsetPixels);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ mScrollState = state;
+
+ if (mViewPagerPageChangeListener != null) {
+ mViewPagerPageChangeListener.onPageScrollStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
+ mTabStrip.onViewPagerPageChanged(position, 0f);
+ scrollToTab(position, 0);
+ }
+
+ if (mViewPagerPageChangeListener != null) {
+ mViewPagerPageChangeListener.onPageSelected(position);
+ }
+ }
+
+ }
+
+ private class TabClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ for (int i = 0; i < mTabStrip.getChildCount(); i++) {
+ if (v == mTabStrip.getChildAt(i)) {
+ mViewPager.setCurrentItem(i);
+ return;
+ }
+ }
+ }
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java
new file mode 100644
index 00000000..88bfb9a3
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.LinearLayout;
+
+class SlidingTabStrip extends LinearLayout {
+
+ private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2;
+ private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26;
+ private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8;
+ private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5;
+
+ private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1;
+ private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20;
+ private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f;
+
+ private final int mBottomBorderThickness;
+ private final Paint mBottomBorderPaint;
+
+ private final int mSelectedIndicatorThickness;
+ private final Paint mSelectedIndicatorPaint;
+
+ private final int mDefaultBottomBorderColor;
+
+ private final Paint mDividerPaint;
+ private final float mDividerHeight;
+
+ private int mSelectedPosition;
+ private float mSelectionOffset;
+
+ private SlidingTabLayout.TabColorizer mCustomTabColorizer;
+ private final SimpleTabColorizer mDefaultTabColorizer;
+
+ SlidingTabStrip(Context context) {
+ this(context, null);
+ }
+
+ SlidingTabStrip(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setWillNotDraw(false);
+
+ final float density = getResources().getDisplayMetrics().density;
+
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true);
+ final int themeForegroundColor = outValue.data;
+
+ mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor,
+ DEFAULT_BOTTOM_BORDER_COLOR_ALPHA);
+
+ mDefaultTabColorizer = new SimpleTabColorizer();
+ mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR);
+ mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor,
+ DEFAULT_DIVIDER_COLOR_ALPHA));
+
+ mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density);
+ mBottomBorderPaint = new Paint();
+ mBottomBorderPaint.setColor(mDefaultBottomBorderColor);
+
+ mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density);
+ mSelectedIndicatorPaint = new Paint();
+
+ mDividerHeight = DEFAULT_DIVIDER_HEIGHT;
+ mDividerPaint = new Paint();
+ mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density));
+ }
+
+ void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) {
+ mCustomTabColorizer = customTabColorizer;
+ invalidate();
+ }
+
+ void setSelectedIndicatorColors(int... colors) {
+ // Make sure that the custom colorizer is removed
+ mCustomTabColorizer = null;
+ mDefaultTabColorizer.setIndicatorColors(colors);
+ invalidate();
+ }
+
+ void setDividerColors(int... colors) {
+ // Make sure that the custom colorizer is removed
+ mCustomTabColorizer = null;
+ mDefaultTabColorizer.setDividerColors(colors);
+ invalidate();
+ }
+
+ void onViewPagerPageChanged(int position, float positionOffset) {
+ mSelectedPosition = position;
+ mSelectionOffset = positionOffset;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int height = getHeight();
+ final int childCount = getChildCount();
+ final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height);
+ final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null
+ ? mCustomTabColorizer
+ : mDefaultTabColorizer;
+
+ // Thick colored underline below the current selection
+ if (childCount > 0) {
+ View selectedTitle = getChildAt(mSelectedPosition);
+ int left = selectedTitle.getLeft();
+ int right = selectedTitle.getRight();
+ int color = tabColorizer.getIndicatorColor(mSelectedPosition);
+
+ if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) {
+ int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1);
+ if (color != nextColor) {
+ color = blendColors(nextColor, color, mSelectionOffset);
+ }
+
+ // Draw the selection partway between the tabs
+ View nextTitle = getChildAt(mSelectedPosition + 1);
+ left = (int) (mSelectionOffset * nextTitle.getLeft() +
+ (1.0f - mSelectionOffset) * left);
+ right = (int) (mSelectionOffset * nextTitle.getRight() +
+ (1.0f - mSelectionOffset) * right);
+ }
+
+ mSelectedIndicatorPaint.setColor(color);
+
+ canvas.drawRect(left, height - mSelectedIndicatorThickness, right,
+ height, mSelectedIndicatorPaint);
+ }
+
+ // Thin underline along the entire bottom edge
+ canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint);
+
+ // Vertical separators between the titles
+ int separatorTop = (height - dividerHeightPx) / 2;
+ for (int i = 0; i < childCount - 1; i++) {
+ View child = getChildAt(i);
+ mDividerPaint.setColor(tabColorizer.getDividerColor(i));
+ canvas.drawLine(child.getRight(), separatorTop, child.getRight(),
+ separatorTop + dividerHeightPx, mDividerPaint);
+ }
+ }
+
+ /**
+ * Set the alpha value of the {@code color} to be the given {@code alpha} value.
+ */
+ private static int setColorAlpha(int color, byte alpha) {
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ /**
+ * Blend {@code color1} and {@code color2} using the given ratio.
+ *
+ * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend,
+ * 0.0 will return {@code color2}.
+ */
+ private static int blendColors(int color1, int color2, float ratio) {
+ final float inverseRation = 1f - ratio;
+ float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
+ float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
+ float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
+ return Color.rgb((int) r, (int) g, (int) b);
+ }
+
+ private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer {
+ private int[] mIndicatorColors;
+ private int[] mDividerColors;
+
+ @Override
+ public final int getIndicatorColor(int position) {
+ return mIndicatorColors[position % mIndicatorColors.length];
+ }
+
+ @Override
+ public final int getDividerColor(int position) {
+ return mDividerColors[position % mDividerColors.length];
+ }
+
+ void setIndicatorColors(int... colors) {
+ mIndicatorColors = colors;
+ }
+
+ void setDividerColors(int... colors) {
+ mDividerColors = colors;
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java b/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java
new file mode 100644
index 00000000..71f03c03
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java
@@ -0,0 +1,16 @@
+/*
+ * 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 de.blinkt.openvpn.views;
+
+import android.support.v4n.view.ViewPager;
+
+/**
+ * Created by arne on 18.11.14.
+ */
+public interface TabBarView {
+
+ void setViewPager(ViewPager mPager);
+}