From 32b080261845c7508581f9c452d48ffd2401c450 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Fri, 2 Aug 2019 12:50:57 +0200 Subject: Add skeleton build variant --- .../java/de/blinkt/openvpn/OpenVPNTileService.java | 151 ++++ .../de/blinkt/openvpn/activities/BaseActivity.java | 43 ++ .../blinkt/openvpn/activities/ConfigConverter.java | 847 +++++++++++++++++++++ .../blinkt/openvpn/activities/CreateShortcuts.java | 159 ++++ .../de/blinkt/openvpn/activities/FileSelect.java | 257 +++++++ .../de/blinkt/openvpn/activities/LogWindow.java | 37 + .../de/blinkt/openvpn/activities/MainActivity.java | 131 ++++ .../de/blinkt/openvpn/activities/OpenSSLSpeed.java | 193 +++++ .../blinkt/openvpn/activities/VPNPreferences.java | 244 ++++++ .../de/blinkt/openvpn/fragments/AboutFragment.java | 333 ++++++++ .../openvpn/fragments/ConnectionsAdapter.java | 413 ++++++++++ .../de/blinkt/openvpn/fragments/FaqFragment.java | 216 ++++++ .../blinkt/openvpn/fragments/FaqViewAdapter.java | 132 ++++ .../openvpn/fragments/FileSelectionFragment.java | 325 ++++++++ .../blinkt/openvpn/fragments/GeneralSettings.java | 190 +++++ .../de/blinkt/openvpn/fragments/GraphFragment.java | 400 ++++++++++ .../de/blinkt/openvpn/fragments/InlineFileTab.java | 70 ++ .../openvpn/fragments/KeyChainSettingsFragment.kt | 262 +++++++ .../de/blinkt/openvpn/fragments/LogFragment.java | 694 +++++++++++++++++ .../fragments/OpenVpnPreferencesFragment.java | 53 ++ .../blinkt/openvpn/fragments/SendDumpFragment.java | 128 ++++ .../openvpn/fragments/Settings_Allowed_Apps.kt | 323 ++++++++ .../openvpn/fragments/Settings_Authentication.java | 238 ++++++ .../blinkt/openvpn/fragments/Settings_Basic.java | 227 ++++++ .../openvpn/fragments/Settings_Connections.java | 101 +++ .../openvpn/fragments/Settings_Fragment.java | 35 + .../de/blinkt/openvpn/fragments/Settings_IP.java | 137 ++++ .../blinkt/openvpn/fragments/Settings_Obscure.java | 212 ++++++ .../blinkt/openvpn/fragments/Settings_Routing.java | 109 +++ .../openvpn/fragments/Settings_UserEditable.java | 63 ++ .../openvpn/fragments/ShowConfigFragment.java | 134 ++++ .../ui/java/de/blinkt/openvpn/fragments/Utils.java | 288 +++++++ .../blinkt/openvpn/fragments/VPNProfileList.java | 636 ++++++++++++++++ .../openvpn/views/DefaultVPNListPreference.java | 39 + .../de/blinkt/openvpn/views/FileSelectLayout.java | 189 +++++ .../blinkt/openvpn/views/MultiLineRadioGroup.java | 164 ++++ .../blinkt/openvpn/views/PagerSlidingTabStrip.java | 732 ++++++++++++++++++ .../blinkt/openvpn/views/RemoteCNPreference.java | 146 ++++ .../openvpn/views/ScreenSlidePagerAdapter.java | 79 ++ .../java/de/blinkt/openvpn/views/SeekBarTicks.java | 73 ++ .../de/blinkt/openvpn/views/SlidingTabLayout.java | 314 ++++++++ .../de/blinkt/openvpn/views/SlidingTabStrip.java | 207 +++++ .../java/de/blinkt/openvpn/views/TabBarView.java | 16 + 43 files changed, 9740 insertions(+) create mode 100644 main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java (limited to 'main/src/ui/java/de') 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 mPathsegments; + + private String mAliasName = null; + + + private Map fileSelectMap = new HashMap<>(); + private String mEmbeddedPwFile; + private Vector mLogEntries = new Vector<>(); + private Uri mSourceUri; + private EditText mProfilename; + private AsyncTask 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 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 item: fileSelectMap.entrySet()) { + if (item.getValue()==null) + addFileSelectDialog(item.getKey()); + } + } + + private void addFileSelectDialog(Utils.FileType type) { + + Pair 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 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 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() { + 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 vpnList = mPM.getProfiles(); + + Vector vpnNames=new Vector(); + for (VpnProfile vpnProfile : vpnList) { + vpnNames.add(vpnProfile.mName); + } + + + + ArrayAdapter adapter = new ArrayAdapter(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: + * + *
    + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.
  • + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with + * the shortcut.
  • + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a + * bitmap, or {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as + * a drawable resource.
  • + *
+ * + * 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(this, mFSFragment)); + bar.addTab(fileExplorerTab); + + if(!mNoInline) { + mInlineFragment = new InlineFileTab(); + inlineFileTab.setTabListener(new MyTabsListener(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 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 { + + 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 { + + + private boolean mCancel = false; + + @Override + protected SpeedResult[] doInBackground(String... strings) { + Vector 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
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 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 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 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 ownedSkus, ArrayList responseList) { + try { + Vector> gdonation = new Vector>(); + + gdonation.add(new Pair(getString(R.string.donatePlayStore),null)); + HashMap responseMap = new HashMap(); + 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;i1) + gmsTextString+= ", "; + gmsTextString+=gdonation.elementAt(i).first; + } + SpannableString gmsText = new SpannableString(gmsTextString); + + + int lStart = 0; + int lEnd=0; + for(Pair 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 getSkuTitle(final String sku, String title, String price, ArrayList ownedSkus) { + String text; + if (ownedSkus.contains(sku)) + return new Pair(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(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 { + 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 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 { + 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 { + + @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= ""; + + if (versionText != null) { + + mHtmlEntriesTitle[i] = (Spanned) TextUtils.concat(Html.fromHtml(textColor + title), + Html.fromHtml(textColor + "
" + versionText + "")); + } 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 path = null; + private TextView myPath; + private ArrayList> mList; + + private Button selectButton; + + + private String parentPath; + private String currentPath = Environment.getExternalStorageDirectory().getAbsolutePath(); + + + private String[] formatFilter = null; + + private File selectedFile; + private HashMap 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 item = new ArrayList(); + path = new ArrayList(); + mList = new ArrayList>(); + + 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 dirsMap = new TreeMap<>(); + TreeMap dirsPathMap = new TreeMap<>(); + TreeMap filesMap = new TreeMap<>(); + TreeMap 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 item = new HashMap(); + item.put(ITEM_KEY, fileName); + item.put(ITEM_IMAGE, imageId); + mList.add(item); + } + + private Collection getExternalStorages() { + Vector 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 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 { + + private Context mContext; + + public ChartDataAdapter(Context context, List 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 dataIn = new LinkedList<>(); + LinkedList dataOut = new LinkedList<>(); + + long interval; + long totalInterval; + + LinkedList 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 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(R.id.select_keystore_button).setOnClickListener(this) + v.findViewById(R.id.configure_extauth_button)?.setOnClickListener(this) + v.findViewById(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(R.id.install_keystore_button).setOnClickListener { + startActivity(KeyChain.createInstallIntent()) }; + } + + override fun onClick(v: View) { + if (v === v.findViewById(R.id.select_keystore_button)) { + showCertDialog() + } else if (v === v.findViewById(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 allEntries = new Vector<>(); + + private Vector currentLevelEntries = new Vector(); + + private Handler mHandler; + + private Vector observers = new Vector(); + + 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 oldAllEntries = allEntries; + allEntries = new Vector(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 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 "}); + + 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 uris = new ArrayList<>(); + + Pair 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 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(R.id.default_allow_text) as TextView + + val vpnOnDefaultSwitch = mSettingsView.findViewById(R.id.default_allow) as Switch + + vpnOnDefaultSwitch.setOnCheckedChangeListener { buttonView, isChecked -> + changeDisallowText(isChecked) + mProfile.mAllowedAppsVpnAreDisallowed = isChecked + } + + vpnOnDefaultSwitch.isChecked = mProfile.mAllowedAppsVpnAreDisallowed + + val vpnAllowBypassSwitch = mSettingsView.findViewById(R.id.allow_bypass) as Switch + + vpnAllowBypassSwitch.setOnCheckedChangeListener { buttonView, isChecked -> mProfile.mAllowAppVpnBypass = isChecked } + + vpnAllowBypassSwitch.isChecked = mProfile.mAllowAppVpnBypass + + mListView = v.findViewById(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(R.id.app_name) as TextView + holder.appIcon = convertView.findViewById(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(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 = Vector() + private val mFilter = ItemFilter() + private var mFilteredData: Vector = 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() + + 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(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 + 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(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) newValue).first; + @SuppressWarnings("unchecked") + String dn = ((Pair) 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 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 supportedMimeTypes = new TreeSet(); + Vector extensions = new Vector(); + + 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 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 { + + 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 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 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 newShortcuts = new LinkedList<>(); + LinkedList updateShortcuts = new LinkedList<>(); + + LinkedList removeShortcuts = new LinkedList<>(); + LinkedList disableShortcuts = new LinkedList<>(); + + boolean addDisconnect = true; + + + TreeSet sortedProfilesLRU = new TreeSet(new VpnProfileLRUComparator()); + ProfileManager profileManager = ProfileManager.getInstance(getContext()); + sortedProfilesLRU.addAll(profileManager.getProfiles()); + + LinkedList 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 { + + @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 { + + 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 allvpn = getPM().getProfiles(); + TreeSet 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 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 viewRectMap; + + public MultiLineRadioGroup(Context context) { + this(context, null); + } + + public MultiLineRadioGroup(Context context, AttributeSet attrs) { + super(context, attrs); + + viewRectMap = new HashMap(); + } + + @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 + * + * 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 lines = getIndicatorCoordinates(); + newScrollX += ((lines.second - lines.first) / 2); + } + + if (newScrollX != lastScrollX) { + lastScrollX = newScrollX; + scrollTo(newScrollX, 0); + } + } + + private Pair 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(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 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 CREATOR = new Parcelable.Creator() { + @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(authtype, dn))) { + mDn = dn; + mDNType = authtype; + } + } + } + + private void populateSpinner() { + ArrayAdapter authtypes = new ArrayAdapter(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 fragmentClass; + String mName; + + public Tab(Class fClass, String name){ + mName = name; + fragmentClass = fClass; + } + + } + + + private Vector mTabs = new Vector(); + + 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 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. + *

+ * 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. + *

+ * 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. + *

+ * 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); +} -- cgit v1.2.3