summaryrefslogtreecommitdiff
path: root/main/src/ui/java/de/blinkt/openvpn/fragments
diff options
context:
space:
mode:
authorArne Schwabe <arne@rfc2549.org>2019-08-02 12:50:57 +0200
committerArne Schwabe <arne@rfc2549.org>2019-08-05 16:01:34 +0200
commit32b080261845c7508581f9c452d48ffd2401c450 (patch)
tree76d194fedd0ec9e9250a96b4157aa32b3eead627 /main/src/ui/java/de/blinkt/openvpn/fragments
parentf72ab87b31044eb5df3a8b6ed802208444d226e3 (diff)
Add skeleton build variant
Diffstat (limited to 'main/src/ui/java/de/blinkt/openvpn/fragments')
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java333
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java413
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java216
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java132
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java325
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java190
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java400
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java70
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt262
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java694
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java53
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java128
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt323
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java238
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java227
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java101
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java35
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java137
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java212
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java109
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java63
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java134
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java288
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java636
24 files changed, 5719 insertions, 0 deletions
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java
new file mode 100644
index 00000000..540f4a9a
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import de.blinkt.openvpn.core.NativeUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Locale;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.VpnStatus;
+
+public class AboutFragment extends Fragment implements View.OnClickListener {
+
+ public static final String INAPPITEM_TYPE_INAPP = "inapp";
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ private static final int DONATION_CODE = 12;
+ private static final int BILLING_RESPONSE_RESULT_OK = 0;
+ private static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ private static final String[] donationSkus = { "donation1eur", "donation2eur", "donation5eur", "donation10eur",
+ "donation1337eur","donation23eur","donation25eur",};
+ IInAppBillingService mService;
+ Hashtable<View, String> viewToProduct = new Hashtable<>();
+ ServiceConnection mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name,
+ IBinder service) {
+ mService = IInAppBillingService.Stub.asInterface(service);
+ initGooglePlayDonation();
+
+ }
+ };
+
+ private void initGooglePlayDonation() {
+ new Thread("queryGMSInApp") {
+ @Override
+ public void run() {
+ initGMSDonateOptions();
+ }
+ }.start();
+ }
+
+ private TextView gmsTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ /*
+ getActivity().bindService(new
+ Intent("com.android.vending.billing.InAppBillingService.BIND"),
+ mServiceConn, Context.BIND_AUTO_CREATE);
+ */
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mService != null) {
+ getActivity().unbindService(mServiceConn);
+ }
+
+ }
+
+ private void initGMSDonateOptions() {
+ try {
+ int billingSupported = mService.isBillingSupported(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP);
+ if (billingSupported != BILLING_RESPONSE_RESULT_OK) {
+ Log.i("OpenVPN", "Play store billing not supported");
+ return;
+ }
+
+ ArrayList skuList = new ArrayList();
+ Collections.addAll(skuList, donationSkus);
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList("ITEM_ID_LIST", skuList);
+
+ Bundle ownedItems = mService.getPurchases(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, null);
+
+
+ if (ownedItems.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK)
+ return;
+
+ final ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
+
+ Bundle skuDetails = mService.getSkuDetails(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, querySkus);
+
+
+ if (skuDetails.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK)
+ return;
+
+ final ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
+
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ createPlayBuyOptions(ownedSkus, responseList);
+
+ }
+ });
+ }
+
+ } catch (RemoteException e) {
+ VpnStatus.logException(e);
+ }
+ }
+
+ private static class SkuResponse {
+ String title;
+ String price;
+
+ SkuResponse(String p, String t)
+ {
+ title=t;
+ price=p;
+ }
+ }
+
+
+
+ private void createPlayBuyOptions(ArrayList<String> ownedSkus, ArrayList<String> responseList) {
+ try {
+ Vector<Pair<String,String>> gdonation = new Vector<Pair<String, String>>();
+
+ gdonation.add(new Pair<String, String>(getString(R.string.donatePlayStore),null));
+ HashMap<String, SkuResponse> responseMap = new HashMap<String, SkuResponse>();
+ for (String thisResponse : responseList) {
+ JSONObject object = new JSONObject(thisResponse);
+ responseMap.put(
+ object.getString("productId"),
+ new SkuResponse(
+ object.getString("price"),
+ object.getString("title")));
+
+ }
+ for (String sku: donationSkus)
+ if (responseMap.containsKey(sku))
+ gdonation.add(getSkuTitle(sku,
+ responseMap.get(sku).title, responseMap.get(sku).price, ownedSkus));
+
+ String gmsTextString="";
+ for(int i=0;i<gdonation.size();i++) {
+ if(i==1)
+ gmsTextString+= " ";
+ else if(i>1)
+ gmsTextString+= ", ";
+ gmsTextString+=gdonation.elementAt(i).first;
+ }
+ SpannableString gmsText = new SpannableString(gmsTextString);
+
+
+ int lStart = 0;
+ int lEnd=0;
+ for(Pair<String, String> item:gdonation){
+ lEnd = lStart + item.first.length();
+ if (item.second!=null) {
+ final String mSku = item.second;
+ ClickableSpan cspan = new ClickableSpan()
+ {
+ @Override
+ public void onClick(View widget) {
+ triggerBuy(mSku);
+ }
+ };
+ gmsText.setSpan(cspan,lStart,lEnd,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ lStart = lEnd+2; // Account for ", " between items
+ }
+
+ if(gmsTextView !=null) {
+ gmsTextView.setText(gmsText);
+ gmsTextView.setMovementMethod(LinkMovementMethod.getInstance());
+ gmsTextView.setVisibility(View.VISIBLE);
+ }
+
+ } catch (JSONException e) {
+ VpnStatus.logException("Parsing Play Store IAP",e);
+ }
+
+ }
+
+ private Pair<String,String> getSkuTitle(final String sku, String title, String price, ArrayList<String> ownedSkus) {
+ String text;
+ if (ownedSkus.contains(sku))
+ return new Pair<String,String>(getString(R.string.thanks_for_donation, price),null);
+
+ if (price.contains("€")|| price.contains("\u20ac")) {
+ text= title;
+ } else {
+ text = String.format(Locale.getDefault(), "%s (%s)", title, price);
+ }
+ //return text;
+ return new Pair<String,String>(price, sku);
+
+ }
+
+ private void triggerBuy(String sku) {
+ try {
+ Bundle buyBundle
+ = mService.getBuyIntent(3, getActivity().getPackageName(),
+ sku, INAPPITEM_TYPE_INAPP, "Thanks for the donation! :)");
+
+
+ if (buyBundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
+ PendingIntent buyIntent = buyBundle.getParcelable(RESPONSE_BUY_INTENT);
+ getActivity().startIntentSenderForResult(buyIntent.getIntentSender(), DONATION_CODE, new Intent(),
+ 0, 0, 0);
+ }
+
+ } catch (RemoteException e) {
+ VpnStatus.logException(e);
+ } catch (IntentSender.SendIntentException e) {
+ VpnStatus.logException(e);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.about, container, false);
+ TextView ver = (TextView) v.findViewById(R.id.version);
+
+ String version;
+ String name = "Openvpn";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+ ver.setText(getString(R.string.version_info, name, version));
+
+ TextView verO2 = v.findViewById(R.id.version_ovpn2);
+ TextView verO3 = v.findViewById(R.id.version_ovpn3);
+
+ verO2.setText(String.format(Locale.US, "OpenVPN version: %s", NativeUtils.getOpenVPN2GitVersion()));
+ verO3.setText(String.format(Locale.US, "OpenVPN3 core version: %s", NativeUtils.getOpenVPN3GitVersion()));
+
+ gmsTextView = (TextView) v.findViewById(R.id.donategms);
+ /* recreating view without onCreate/onDestroy cycle */
+
+ // Disable GMS for now
+ if (mService!=null)
+ initGooglePlayDonation();
+
+ TextView translation = (TextView) v.findViewById(R.id.translation);
+
+ // Don't print a text for myself
+ if (getString(R.string.translationby).contains("Arne Schwabe"))
+ translation.setText("");
+ else
+ translation.setText(R.string.translationby);
+
+ TextView wv = (TextView) v.findViewById(R.id.full_licenses);
+ wv.setText(Html.fromHtml(readHtmlFromAssets()));
+ return v;
+ }
+
+ String readHtmlFromAssets()
+ {
+ InputStream mvpn;
+
+ try {
+ mvpn = getActivity().getAssets().open("full_licenses.html");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(mvpn));
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ reader.close();
+ return sb.toString();
+ } catch (IOException errabi) {
+ return "full_licenses.html not found";
+ }
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (mService!=null)
+ initGooglePlayDonation();
+ }
+
+
+ @Override
+ public void onClick(View v) {
+
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
new file mode 100644
index 00000000..9c4c80de
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+
+import java.util.Arrays;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.Connection;
+
+public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.ConnectionsHolder> {
+ private static final int TYPE_NORMAL = 0;
+ private static final int TYPE_FOOTER = TYPE_NORMAL + 1;
+ private final Context mContext;
+ private final VpnProfile mProfile;
+ private final Settings_Connections mConnectionFragment;
+ private Connection[] mConnections;
+
+ ConnectionsAdapter(Context c, Settings_Connections connections_fragments, VpnProfile vpnProfile) {
+ mContext = c;
+ mConnections = vpnProfile.mConnections;
+ mProfile = vpnProfile;
+ mConnectionFragment = connections_fragments;
+ }
+
+ @Override
+ public ConnectionsAdapter.ConnectionsHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ LayoutInflater li = LayoutInflater.from(mContext);
+
+ View card;
+ if (viewType == TYPE_NORMAL) {
+ card = li.inflate(R.layout.server_card, viewGroup, false);
+
+ } else { // TYPE_FOOTER
+ card = li.inflate(R.layout.server_footer, viewGroup, false);
+ }
+ return new ConnectionsHolder(card, this, viewType);
+
+ }
+
+ @Override
+ public void onBindViewHolder(final ConnectionsAdapter.ConnectionsHolder cH, int position) {
+ if (position == mConnections.length) {
+ // Footer
+ return;
+ }
+ final Connection connection = mConnections[position];
+
+ cH.mConnection = null;
+
+ cH.mPortNumberView.setText(connection.mServerPort);
+ cH.mServerNameView.setText(connection.mServerName);
+ cH.mPortNumberView.setText(connection.mServerPort);
+ cH.mRemoteSwitch.setChecked(connection.mEnabled);
+
+
+ cH.mProxyNameView.setText(connection.mProxyName);
+ cH.mProxyPortNumberView.setText(connection.mProxyPort);
+
+ cH.mConnectText.setText(String.valueOf(connection.getTimeout()));
+
+ cH.mConnectSlider.setProgress(connection.getTimeout());
+
+
+ cH.mProtoGroup.check(connection.mUseUdp ? R.id.udp_proto : R.id.tcp_proto);
+
+ switch (connection.mProxyType) {
+ case NONE:
+ cH.mProxyGroup.check(R.id.proxy_none);
+ break;
+ case HTTP:
+ cH.mProxyGroup.check(R.id.proxy_http);
+ break;
+ case SOCKS5:
+ cH.mProxyGroup.check(R.id.proxy_http);
+ break;
+ case ORBOT:
+ cH.mProxyGroup.check(R.id.proxy_orbot);
+ break;
+ }
+
+ cH.mProxyAuthCb.setChecked(connection.mUseProxyAuth);
+ cH.mProxyAuthUser.setText(connection.mProxyAuthUser);
+ cH.mProxyAuthPassword.setText(connection.mProxyAuthPassword);
+
+ cH.mCustomOptionsLayout.setVisibility(connection.mUseCustomConfig ? View.VISIBLE : View.GONE);
+ cH.mCustomOptionText.setText(connection.mCustomConfiguration);
+
+ cH.mCustomOptionCB.setChecked(connection.mUseCustomConfig);
+ cH.mConnection = connection;
+
+ setVisibilityProxyServer(cH, connection);
+
+ }
+
+ private void setVisibilityProxyServer(ConnectionsHolder cH, Connection connection) {
+ int visible = (connection.mProxyType == Connection.ProxyType.HTTP || connection.mProxyType == Connection.ProxyType.SOCKS5) ? View.VISIBLE : View.GONE;
+ int authVisible = (connection.mProxyType == Connection.ProxyType.HTTP) ? View.VISIBLE : View.GONE;
+
+ cH.mProxyNameView.setVisibility(visible);
+ cH.mProxyPortNumberView.setVisibility(visible);
+ cH.mProxyNameLabel.setVisibility(visible);
+
+ cH.mProxyAuthLayout.setVisibility(authVisible);
+
+ }
+
+ private void removeRemote(int idx) {
+ Connection[] mConnections2 = Arrays.copyOf(mConnections, mConnections.length - 1);
+ for (int i = idx + 1; i < mConnections.length; i++) {
+ mConnections2[i - 1] = mConnections[i];
+ }
+ mConnections = mConnections2;
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return mConnections.length + 1; //for footer
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == mConnections.length)
+ return TYPE_FOOTER;
+ else
+ return TYPE_NORMAL;
+ }
+
+ void addRemote() {
+ mConnections = Arrays.copyOf(mConnections, mConnections.length + 1);
+ mConnections[mConnections.length - 1] = new Connection();
+ notifyItemInserted(mConnections.length - 1);
+ displayWarningIfNoneEnabled();
+ }
+
+ void displayWarningIfNoneEnabled() {
+ int showWarning = View.VISIBLE;
+ for (Connection conn : mConnections) {
+ if (conn.mEnabled)
+ showWarning = View.GONE;
+ }
+ mConnectionFragment.setWarningVisible(showWarning);
+ }
+
+ void saveProfile() {
+ mProfile.mConnections = mConnections;
+ }
+
+ static abstract class OnTextChangedWatcher implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+ }
+
+ class ConnectionsHolder extends RecyclerView.ViewHolder {
+ private final EditText mServerNameView;
+ private final EditText mPortNumberView;
+ private final Switch mRemoteSwitch;
+ private final RadioGroup mProtoGroup;
+ private final EditText mCustomOptionText;
+ private final CheckBox mCustomOptionCB;
+ private final View mCustomOptionsLayout;
+ private final ImageButton mDeleteButton;
+ private final EditText mConnectText;
+ private final SeekBar mConnectSlider;
+ private final ConnectionsAdapter mConnectionsAdapter;
+ private final RadioGroup mProxyGroup;
+ private final EditText mProxyNameView;
+ private final EditText mProxyPortNumberView;
+ private final View mProxyNameLabel;
+ private final View mProxyAuthLayout;
+ private final EditText mProxyAuthUser;
+ private final EditText mProxyAuthPassword;
+ private final CheckBox mProxyAuthCb;
+
+ private Connection mConnection; // Set to null on update
+
+
+ ConnectionsHolder(View card, ConnectionsAdapter connectionsAdapter, int viewType) {
+ super(card);
+ mServerNameView = card.findViewById(R.id.servername);
+ mPortNumberView = card.findViewById(R.id.portnumber);
+ mRemoteSwitch = card.findViewById(R.id.remoteSwitch);
+ mCustomOptionCB = card.findViewById(R.id.use_customoptions);
+ mCustomOptionText = card.findViewById(R.id.customoptions);
+ mProtoGroup = card.findViewById(R.id.udptcpradiogroup);
+ mCustomOptionsLayout = card.findViewById(R.id.custom_options_layout);
+ mDeleteButton = card.findViewById(R.id.remove_connection);
+ mConnectSlider = card.findViewById(R.id.connect_silder);
+ mConnectText = card.findViewById(R.id.connect_timeout);
+
+ mProxyGroup = card.findViewById(R.id.proxyradiogroup);
+ mProxyNameView = card.findViewById(R.id.proxyname);
+ mProxyPortNumberView = card.findViewById(R.id.proxyport);
+ mProxyNameLabel = card.findViewById(R.id.proxyserver_label);
+
+ mProxyAuthLayout = card.findViewById(R.id.proxyauthlayout);
+ mProxyAuthCb = card.findViewById(R.id.enable_proxy_auth);
+ mProxyAuthUser = card.findViewById(R.id.proxyuser);
+ mProxyAuthPassword = card.findViewById(R.id.proxypassword);
+
+ mConnectionsAdapter = connectionsAdapter;
+
+ if (viewType == TYPE_NORMAL)
+ addListeners();
+ }
+
+
+ void addListeners() {
+ mRemoteSwitch.setOnCheckedChangeListener((CompoundButton buttonView, boolean isChecked) -> {
+ if (mConnection != null) {
+ mConnection.mEnabled = isChecked;
+ mConnectionsAdapter.displayWarningIfNoneEnabled();
+ }
+ });
+
+ mProtoGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (mConnection != null) {
+ if (checkedId == R.id.udp_proto)
+ mConnection.mUseUdp = true;
+ else if (checkedId == R.id.tcp_proto)
+ mConnection.mUseUdp = false;
+ }
+ });
+
+ mProxyGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (mConnection != null) {
+ switch (checkedId) {
+ case R.id.proxy_none:
+ mConnection.mProxyType = Connection.ProxyType.NONE;
+ break;
+ case R.id.proxy_http:
+ mConnection.mProxyType = Connection.ProxyType.HTTP;
+ break;
+ case R.id.proxy_socks:
+ mConnection.mProxyType = Connection.ProxyType.SOCKS5;
+ break;
+ case R.id.proxy_orbot:
+ mConnection.mProxyType = Connection.ProxyType.ORBOT;
+ break;
+ }
+ setVisibilityProxyServer(this, mConnection);
+ }
+ });
+
+ mProxyAuthCb.setOnCheckedChangeListener((group, isChecked) ->
+ {
+ if (mConnection != null) {
+ mConnection.mUseProxyAuth = isChecked;
+ setVisibilityProxyServer(this, mConnection);
+ }
+ });
+
+ mCustomOptionText.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null)
+ mConnection.mCustomConfiguration = s.toString();
+ }
+ });
+
+ mCustomOptionCB.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mConnection != null) {
+ mConnection.mUseCustomConfig = isChecked;
+ mCustomOptionsLayout.setVisibility(mConnection.mUseCustomConfig ? View.VISIBLE : View.GONE);
+ }
+ });
+
+
+ mServerNameView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mServerName = s.toString();
+ }
+ }
+
+ });
+
+ mPortNumberView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mServerPort = s.toString();
+ }
+ }
+ });
+
+ mProxyNameView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyName = s.toString();
+ }
+ }
+
+ });
+
+ mProxyPortNumberView.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyPort = s.toString();
+ }
+ }
+ });
+
+ mCustomOptionCB.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mConnection != null) {
+ mConnection.mUseProxyAuth = isChecked;
+ }
+ });
+
+ mProxyAuthPassword.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyAuthPassword = s.toString();
+ }
+ }
+ });
+
+
+ mProxyAuthUser.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mProxyAuthUser = s.toString();
+ }
+ }
+ });
+
+
+
+ mCustomOptionText.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ mConnection.mCustomConfiguration = s.toString();
+ }
+ }
+ });
+
+ mConnectSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser && mConnection != null) {
+ mConnectText.setText(String.valueOf(progress));
+ mConnection.mConnectTimeout = progress;
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ mConnectText.addTextChangedListener(new OnTextChangedWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mConnection != null) {
+ try {
+ int t = Integer.valueOf(String.valueOf(s));
+ mConnectSlider.setProgress(t);
+ mConnection.mConnectTimeout = t;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+ });
+
+ mDeleteButton.setOnClickListener(
+ v -> {
+ AlertDialog.Builder ab = new AlertDialog.Builder(mContext);
+ ab.setTitle(R.string.query_delete_remote);
+ ab.setPositiveButton(R.string.keep, null);
+ ab.setNegativeButton(R.string.delete, (dialog, which) -> {
+ removeRemote(getAdapterPosition());
+ notifyItemRemoved(getAdapterPosition());
+ });
+ ab.create().show();
+ }
+ );
+
+
+ }
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
new file mode 100644
index 00000000..af4c35fe
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.StaggeredGridLayoutManager;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+
+public class FaqFragment extends Fragment {
+ static class FAQEntry {
+
+ public FAQEntry(int startVersion, int endVersion, int title, int description) {
+ this.startVersion = startVersion;
+ this.endVersion = endVersion;
+ this.description = description;
+ this.title = title;
+ }
+
+ final int startVersion;
+ final int endVersion;
+ final int description;
+ final int title;
+
+ public boolean runningVersion() {
+ if (Build.VERSION.SDK_INT >= startVersion) {
+ if (Build.VERSION.SDK_INT <= endVersion)
+ return true;
+
+ if (endVersion == -1)
+ return true;
+
+ String release = Build.VERSION.RELEASE;
+ boolean isOlderThan443 = !release.startsWith("4.4.3") && !release.startsWith("4.4.4") &&
+ !release.startsWith("4.4.5") && !release.startsWith("4.4.6");
+
+ boolean isOlderThan442 = isOlderThan443 && !release.startsWith("4.4.2");
+
+
+ if(Build.VERSION.SDK_INT== Build.VERSION_CODES.KITKAT) {
+ if (endVersion == -441 && isOlderThan442)
+ return true;
+
+ if (endVersion == -442 && isOlderThan443)
+ return true;
+ } else if (endVersion == -441 || endVersion == -442) {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT;
+ }
+
+
+ }
+ return false;
+ }
+
+ public String getVersionsString(Context c) {
+ if (startVersion == Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if (endVersion == -1)
+ return null;
+ else
+ return c.getString(R.string.version_upto, getAndroidVersionString(c, endVersion));
+ }
+
+ if (endVersion == -1)
+ return c.getString(R.string.version_and_later, getAndroidVersionString(c, startVersion));
+
+
+ String startver = getAndroidVersionString(c, startVersion);
+
+ if (endVersion == startVersion)
+ return startver;
+
+ return String.format("%s - %s", startver, getAndroidVersionString(c, endVersion));
+ }
+
+
+ private String getAndroidVersionString(Context c, int versionCode) {
+ switch (versionCode) {
+ case Build.VERSION_CODES.ICE_CREAM_SANDWICH:
+ return "4.0 (Ice Cream Sandwich)";
+ case -441:
+ return "4.4.1 (KitKat)";
+ case -442:
+ return "4.4.2 (KitKat)";
+ case Build.VERSION_CODES.JELLY_BEAN_MR2:
+ return "4.3 (Jelly Bean MR2)";
+ case Build.VERSION_CODES.KITKAT:
+ return "4.4 (KitKat)";
+ case Build.VERSION_CODES.LOLLIPOP:
+ return "5.0 (Lollipop)";
+ default:
+ return "API " + versionCode;
+ }
+ }
+
+
+ }
+
+ private static FAQEntry[] faqitemsVersionSpecific = {
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_howto_title, R.string.faq_howto),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_killswitch_title, R.string.faq_killswitch),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_remote_api_title, R.string.faq_remote_api),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.weakmd_title, R.string.weakmd),
+ new FAQEntry(Build.VERSION_CODES.LOLLIPOP, -1, R.string.samsung_broken_title, R.string.samsung_broken),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_duplicate_notification_title, R.string.faq_duplicate_notification),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_androids_clients_title, R.string.faq_android_clients),
+
+
+ new FAQEntry(Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1, R.string.ab_lollipop_reinstall_title, R.string.ab_lollipop_reinstall),
+
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.vpn_tethering_title, R.string.faq_tethering),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.broken_images, R.string.broken_images_faq),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.battery_consumption_title, R.string.baterry_consumption),
+
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.KITKAT, R.string.faq_system_dialogs_title, R.string.faq_system_dialogs),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.faq_tap_mode),
+
+ new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.ab_secondary_users_title, R.string.ab_secondary_users),
+ new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, -1, R.string.faq_vpndialog43_title, R.string.faq_vpndialog43),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tls_cipher_alert_title, R.string.tls_cipher_alert),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_security_title, R.string.faq_security),
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_shortcut, R.string.faq_howto_shortcut),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq2),
+
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.vpn_tethering_title, R.string.ab_tethering_44),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -441, R.string.ab_kitkat_mss_title, R.string.ab_kitkat_mss),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.copying_log_entries, R.string.faq_copying),
+
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -442, R.string.ab_persist_tun_title, R.string.ab_persist_tun),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.faq_routing_title, R.string.faq_routing),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_kitkat_reconnect_title, R.string.ab_kitkat_reconnect),
+ new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_vpn_reachability_44_title, R.string.ab_vpn_reachability_44),
+
+
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_only_cidr_title, R.string.ab_only_cidr),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_proxy_title, R.string.ab_proxy),
+
+ new FAQEntry(Build.VERSION_CODES.LOLLIPOP, -1, R.string.ab_not_route_to_vpn_title, R.string.ab_not_route_to_vpn),
+ new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq3),
+
+ // DNS weirdness in Samsung 5.0: https://plus.google.com/117315704597472009168/posts/g78bZLWmqgD
+ };
+
+
+ private RecyclerView mRecyclerView;
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.faq, container, false);
+
+ DisplayMetrics displaymetrics = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
+ int dpWidth = (int) (displaymetrics.widthPixels / getResources().getDisplayMetrics().density);
+
+ //better way but does not work on 5.0
+ //int dpWidth = (int) (container.getWidth()/getResources().getDisplayMetrics().density);
+ int columns = dpWidth / 360;
+ columns = Math.max(1, columns);
+
+
+ mRecyclerView = (RecyclerView) v.findViewById(R.id.faq_recycler_view);
+
+ // use this setting to improve performance if you know that changes
+ // in content do not change the layout size of the RecyclerView
+ mRecyclerView.setHasFixedSize(true);
+
+
+ mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL));
+
+ mRecyclerView.setAdapter(new FaqViewAdapter(getActivity(), getFAQEntries()));
+
+ return v;
+ }
+
+ private FAQEntry[] getFAQEntries() {
+ Vector<FAQEntry> faqItems = new Vector<>();
+
+ for (FAQEntry fe : faqitemsVersionSpecific) {
+ if (fe.runningVersion())
+ faqItems.add(fe);
+ }
+ for (FAQEntry fe : faqitemsVersionSpecific) {
+ if (!fe.runningVersion())
+ faqItems.add(fe);
+ }
+
+ return faqItems.toArray(new FAQEntry[faqItems.size()]);
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
new file mode 100644
index 00000000..0be9f4a2
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+
+public class FaqViewAdapter extends RecyclerView.Adapter<FaqViewAdapter.FaqViewHolder> {
+ private final FaqFragment.FAQEntry[] mFaqItems;
+ private final Spanned[] mHtmlEntries;
+ private final Spanned[] mHtmlEntriesTitle;
+ private final Context mContext;
+ private boolean loaded =false;
+
+ public FaqViewAdapter(Context context, FaqFragment.FAQEntry[] faqItems) {
+ mFaqItems = faqItems;
+ mContext = context;
+
+ mHtmlEntries = new Spanned[faqItems.length];
+ mHtmlEntriesTitle = new Spanned[faqItems.length];
+
+ new FetchStrings().execute(faqItems);
+
+ }
+
+ private class FetchStrings extends AsyncTask<FaqFragment.FAQEntry,Void,Void> {
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ loaded=true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ protected Void doInBackground(FaqFragment.FAQEntry... params) {
+ fetchStrings(params);
+ return null;
+ }
+ }
+
+ private void fetchStrings(FaqFragment.FAQEntry[] faqItems) {
+ for (int i =0; i < faqItems.length; i++) {
+ String versionText = mFaqItems[i].getVersionsString(mContext);
+ String title;
+ String textColor="";
+
+ if (mFaqItems[i].title==-1)
+ title ="";
+ else
+ title = mContext.getString(faqItems[i].title);
+
+
+ if (!mFaqItems[i].runningVersion())
+ textColor= "<font color=\"gray\">";
+
+ if (versionText != null) {
+
+ mHtmlEntriesTitle[i] = (Spanned) TextUtils.concat(Html.fromHtml(textColor + title),
+ Html.fromHtml(textColor + "<br><small>" + versionText + "</small>"));
+ } else {
+ mHtmlEntriesTitle[i] = Html.fromHtml(title);
+ }
+
+ String content = mContext.getString(faqItems[i].description);
+ mHtmlEntries[i] = Html.fromHtml(textColor + content);
+
+ // Add hack R.string.faq_system_dialogs_title -> R.string.faq_system_dialog_xposed
+ if (faqItems[i].title == R.string.faq_system_dialogs_title)
+ {
+ Spanned xPosedtext = Html.fromHtml(textColor + mContext.getString(R.string.faq_system_dialog_xposed));
+ mHtmlEntries[i] = (Spanned) TextUtils.concat(mHtmlEntries[i], xPosedtext);
+ }
+ }
+ }
+
+ public static class FaqViewHolder extends RecyclerView.ViewHolder {
+
+ private final CardView mView;
+ private final TextView mBody;
+ private final TextView mHead;
+
+ public FaqViewHolder(View itemView) {
+ super(itemView);
+
+ mView = (CardView) itemView;
+ mBody = (TextView)mView.findViewById(R.id.faq_body);
+ mHead = (TextView)mView.findViewById(R.id.faq_head);
+ mBody.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+
+ @Override
+ public FaqViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
+ View view = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.faqcard, viewGroup, false);
+ return new FaqViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(FaqViewHolder faqViewHolder, int i) {
+
+ faqViewHolder.mHead.setText(mHtmlEntriesTitle[i]);
+ faqViewHolder.mBody.setText(mHtmlEntries[i]);
+
+
+ }
+
+ @Override
+ public int getItemCount() {
+ if(loaded)
+ return mFaqItems.length;
+ else
+ return 0;
+ }
+
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java
new file mode 100644
index 00000000..9d12b83d
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.ListFragment;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.activities.FileSelect;
+
+public class FileSelectionFragment extends ListFragment {
+
+ private static final String ITEM_KEY = "key";
+ private static final String ITEM_IMAGE = "image";
+ private static final String ROOT = "/";
+
+
+ private List<String> path = null;
+ private TextView myPath;
+ private ArrayList<HashMap<String, Object>> mList;
+
+ private Button selectButton;
+
+
+ private String parentPath;
+ private String currentPath = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+
+ private String[] formatFilter = null;
+
+ private File selectedFile;
+ private HashMap<String, Integer> lastPositions = new HashMap<>();
+ private String mStartPath;
+ private CheckBox mInlineImport;
+ private Button mClearButton;
+ private boolean mHideImport = false;
+
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ onListItemClick(getListView(), view, position, id);
+ onFileSelectionClick();
+ return true;
+ }
+ }
+
+ );
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.file_dialog_main, container, false);
+
+ myPath = (TextView) v.findViewById(R.id.path);
+
+ mInlineImport = (CheckBox) v.findViewById(R.id.doinline);
+
+ if (mHideImport) {
+ mInlineImport.setVisibility(View.GONE);
+ mInlineImport.setChecked(false);
+ }
+
+
+ selectButton = (Button) v.findViewById(R.id.fdButtonSelect);
+ selectButton.setEnabled(false);
+ selectButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ onFileSelectionClick();
+ }
+ });
+
+ mClearButton = (Button) v.findViewById(R.id.fdClear);
+ mClearButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((FileSelect) getActivity()).clearData();
+ }
+ });
+ if (!((FileSelect) getActivity()).showClear()) {
+ mClearButton.setVisibility(View.GONE);
+ mClearButton.setEnabled(false);
+ }
+
+ return v;
+ }
+
+ private void onFileSelectionClick() {
+ if (selectedFile != null) {
+ if (mInlineImport.isChecked())
+
+ ((FileSelect) getActivity()).importFile(selectedFile.getPath());
+ else
+ ((FileSelect) getActivity()).setFile(selectedFile.getPath());
+ }
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mStartPath = ((FileSelect) getActivity()).getSelectPath();
+ getDir(mStartPath);
+ }
+
+ public void refresh() {
+ getDir(Environment.getExternalStorageDirectory().getAbsolutePath());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+
+ private void getDir(String dirPath) {
+
+ boolean useAutoSelection = dirPath.length() < currentPath.length();
+
+ Integer position = lastPositions.get(parentPath);
+
+ getDirImpl(dirPath);
+
+ if (position != null && useAutoSelection) {
+ getListView().setSelection(position);
+ }
+
+ }
+
+ private void getDirImpl(final String dirPath) {
+
+ currentPath = dirPath;
+
+ final List<String> item = new ArrayList<String>();
+ path = new ArrayList<String>();
+ mList = new ArrayList<HashMap<String, Object>>();
+
+ File f = new File(currentPath);
+ File[] files = f.listFiles();
+ if (files == null) {
+ currentPath = ROOT;
+ f = new File(currentPath);
+ files = f.listFiles();
+
+ if (files == null)
+ files = new File[]{};
+ }
+
+ myPath.setText(getText(R.string.location) + ": " + currentPath);
+
+ if (!currentPath.equals(ROOT)) {
+
+ item.add(ROOT);
+ addItem(ROOT, R.drawable.ic_root_folder_am);
+ path.add(ROOT);
+
+ item.add("../");
+ addItem("../", R.drawable.ic_root_folder_am);
+ path.add(f.getParent());
+ parentPath = f.getParent();
+
+ }
+
+
+ TreeMap<String, String> dirsMap = new TreeMap<>();
+ TreeMap<String, String> dirsPathMap = new TreeMap<>();
+ TreeMap<String, String> filesMap = new TreeMap<>();
+ TreeMap<String, String> filesPathMap = new TreeMap<>();
+
+ // add default locations
+ for (String dir: getExternalStorages()) {
+ // You got to love the P8 Lite to have null in this list ....
+ if (dir!=null) {
+ dirsMap.put(dir, dir);
+ dirsPathMap.put(dir, dir);
+ }
+ }
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String dirName = file.getName();
+ dirsMap.put(dirName, dirName);
+ dirsPathMap.put(dirName, file.getPath());
+ } else {
+ final String fileName = file.getName();
+ final String fileNameLwr = fileName.toLowerCase(Locale.getDefault());
+
+ if (formatFilter != null) {
+ boolean contains = false;
+ for (String aFormatFilter : formatFilter) {
+ final String formatLwr = aFormatFilter.toLowerCase(Locale.getDefault());
+ if (fileNameLwr.endsWith(formatLwr)) {
+ contains = true;
+ break;
+ }
+ }
+ if (contains) {
+ filesMap.put(fileName, fileName);
+ filesPathMap.put(fileName, file.getPath());
+ }
+ } else {
+ filesMap.put(fileName, fileName);
+ filesPathMap.put(fileName, file.getPath());
+ }
+ }
+ }
+ item.addAll(dirsMap.tailMap("").values());
+ item.addAll(filesMap.tailMap("").values());
+ path.addAll(dirsPathMap.tailMap("").values());
+ path.addAll(filesPathMap.tailMap("").values());
+
+ SimpleAdapter fileList = new SimpleAdapter(getActivity(), mList, R.layout.file_dialog_row, new String[]{
+ ITEM_KEY, ITEM_IMAGE}, new int[]{R.id.fdrowtext, R.id.fdrowimage});
+
+ for (String dir : dirsMap.tailMap("").values()) {
+ addItem(dir, R.drawable.ic_root_folder_am);
+ }
+
+ for (String file : filesMap.tailMap("").values()) {
+ addItem(file, R.drawable.ic_doc_generic_am);
+ }
+
+ fileList.notifyDataSetChanged();
+
+ setListAdapter(fileList);
+
+ }
+
+ private void addItem(String fileName, int imageId) {
+ HashMap<String, Object> item = new HashMap<String, Object>();
+ item.put(ITEM_KEY, fileName);
+ item.put(ITEM_IMAGE, imageId);
+ mList.add(item);
+ }
+
+ private Collection<String> getExternalStorages() {
+ Vector<String> dirs = new Vector<>();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ for (File d : getActivity().getExternalFilesDirs(null))
+ dirs.add(getRootOfInnerSdCardFolder(d));
+ } else {
+ dirs.add(Environment.getExternalStorageDirectory().getAbsolutePath());
+ }
+ return dirs;
+ }
+
+ private static String getRootOfInnerSdCardFolder(File file) {
+ if (file == null)
+ return null;
+ final long totalSpace = file.getTotalSpace();
+ while (true) {
+ final File parentFile = file.getParentFile();
+ if (parentFile == null || parentFile.getTotalSpace() != totalSpace
+ || file.equals(Environment.getExternalStorageDirectory()))
+ return file.getAbsolutePath();
+ file = parentFile;
+ }
+ }
+
+
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+
+ File file = new File(path.get(position));
+
+ if (file.isDirectory()) {
+ selectButton.setEnabled(false);
+
+ if (file.canRead()) {
+ lastPositions.put(currentPath, position);
+ getDir(path.get(position));
+ } else {
+ Toast.makeText(getActivity(),
+ "[" + file.getName() + "] " + getActivity().getText(R.string.cant_read_folder),
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ selectedFile = file;
+ v.setSelected(true);
+ selectButton.setEnabled(true);
+ }
+ }
+
+ public void setNoInLine() {
+ mHideImport = true;
+
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java b/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java
new file mode 100644
index 00000000..34d37823
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+import java.io.File;
+import java.util.Collection;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+
+import de.blinkt.openvpn.BuildConfig;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.OpenSSLSpeed;
+import de.blinkt.openvpn.api.ExternalAppDatabase;
+import de.blinkt.openvpn.core.ProfileManager;
+
+
+public class GeneralSettings extends PreferenceFragment implements OnPreferenceClickListener, OnClickListener, Preference.OnPreferenceChangeListener {
+
+ private ExternalAppDatabase mExtapp;
+ private ListPreference mAlwaysOnVPN;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.general_settings);
+
+
+ PreferenceCategory devHacks = (PreferenceCategory) findPreference("device_hacks");
+ mAlwaysOnVPN = (ListPreference) findPreference("alwaysOnVpn");
+ mAlwaysOnVPN.setOnPreferenceChangeListener(this);
+
+
+ Preference loadtun = findPreference("loadTunModule");
+ if(!isTunModuleAvailable()) {
+ loadtun.setEnabled(false);
+ devHacks.removePreference(loadtun);
+ }
+
+ CheckBoxPreference cm9hack = (CheckBoxPreference) findPreference("useCM9Fix");
+ if (!cm9hack.isChecked() && (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1)) {
+ devHacks.removePreference(cm9hack);
+ }
+
+ CheckBoxPreference useInternalFS = (CheckBoxPreference) findPreference("useInternalFileSelector");
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT)
+ {
+ devHacks.removePreference(useInternalFS);
+ }
+
+ /* Android P does not allow access to the file storage anymore */
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
+ {
+ Preference useInternalFileSelector = findPreference("useInternalFileSelector");
+ devHacks.removePreference(useInternalFileSelector);
+ }
+
+ mExtapp = new ExternalAppDatabase(getActivity());
+ Preference clearapi = findPreference("clearapi");
+ clearapi.setOnPreferenceClickListener(this);
+
+ findPreference("osslspeed").setOnPreferenceClickListener(this);
+
+ if(devHacks.getPreferenceCount()==0)
+ getPreferenceScreen().removePreference(devHacks);
+
+ if (!BuildConfig.openvpn3) {
+ PreferenceCategory appBehaviour = (PreferenceCategory) findPreference("app_behaviour");
+ CheckBoxPreference ovpn3 = (CheckBoxPreference) findPreference("ovpn3");
+ ovpn3.setEnabled(false);
+ ovpn3.setChecked(false);
+ }
+
+
+ setClearApiSummary();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+
+
+
+ VpnProfile vpn = ProfileManager.getAlwaysOnVPN(getActivity());
+ StringBuffer sb = new StringBuffer(getString(R.string.defaultvpnsummary));
+ sb.append('\n');
+ if (vpn== null)
+ sb.append(getString(R.string.novpn_selected));
+ else
+ sb.append(getString(R.string.vpnselected, vpn.getName()));
+ mAlwaysOnVPN.setSummary(sb.toString());
+
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference== mAlwaysOnVPN) {
+ VpnProfile vpn = ProfileManager.get(getActivity(), (String) newValue);
+ mAlwaysOnVPN.setSummary(vpn.getName());
+ }
+ return true;
+ }
+
+ private void setClearApiSummary() {
+ Preference clearapi = findPreference("clearapi");
+
+ if(mExtapp.getExtAppList().isEmpty()) {
+ clearapi.setEnabled(false);
+ clearapi.setSummary(R.string.no_external_app_allowed);
+ } else {
+ clearapi.setEnabled(true);
+ clearapi.setSummary(getString(R.string.allowed_apps, getExtAppList(", ")));
+ }
+ }
+
+ private String getExtAppList(String delim) {
+ ApplicationInfo app;
+ PackageManager pm = getActivity().getPackageManager();
+
+ StringBuilder applist = new StringBuilder();
+ for (String packagename : mExtapp.getExtAppList()) {
+ try {
+ app = pm.getApplicationInfo(packagename, 0);
+ if (applist.length() != 0)
+ applist.append(delim);
+ applist.append(app.loadLabel(pm));
+
+ } catch (NameNotFoundException e) {
+ // App not found. Remove it from the list
+ mExtapp.removeApp(packagename);
+ }
+ }
+
+ return applist.toString();
+ }
+
+ private boolean isTunModuleAvailable() {
+ // Check if the tun module exists on the file system
+ return new File("/system/lib/modules/tun.ko").length() > 10;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if(preference.getKey().equals("clearapi")){
+ Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setPositiveButton(R.string.clear, this);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setMessage(getString(R.string.clearappsdialog,getExtAppList("\n")));
+ builder.show();
+ } else if (preference.getKey().equals("osslspeed")) {
+ startActivity(new Intent(getActivity(), OpenSSLSpeed.class));
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if( which == Dialog.BUTTON_POSITIVE){
+ mExtapp.clearAllApiApps();
+ setClearApiSummary();
+ }
+ }
+
+
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java
new file mode 100644
index 00000000..10c09461
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2012-2017 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.github.mikephil.charting.charts.LineChart;
+import com.github.mikephil.charting.components.AxisBase;
+import com.github.mikephil.charting.components.XAxis;
+import com.github.mikephil.charting.components.YAxis;
+import com.github.mikephil.charting.data.Entry;
+import com.github.mikephil.charting.data.LineData;
+import com.github.mikephil.charting.data.LineDataSet;
+import com.github.mikephil.charting.formatter.IAxisValueFormatter;
+import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.OpenVPNManagement;
+import de.blinkt.openvpn.core.TrafficHistory;
+import de.blinkt.openvpn.core.VpnStatus;
+
+import static android.content.Context.MODE_PRIVATE;
+import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount;
+import static java.lang.Math.max;
+
+/**
+ * Created by arne on 19.05.17.
+ */
+
+public class GraphFragment extends Fragment implements VpnStatus.ByteCountListener {
+ private static final String PREF_USE_LOG = "useLogGraph";
+ private ListView mListView;
+
+ private ChartDataAdapter mChartAdapter;
+ private int mColourIn;
+ private int mColourOut;
+ private int mColourPoint;
+
+ private long firstTs;
+ private TextView mSpeedStatus;
+ private boolean mLogScale;
+
+ private static final int TIME_PERIOD_SECDONS = 0;
+ private static final int TIME_PERIOD_MINUTES = 1;
+ private static final int TIME_PERIOD_HOURS = 2;
+ private Handler mHandler;
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.graph, container, false);
+ mListView = (ListView) v.findViewById(R.id.graph_listview);
+ mSpeedStatus = (TextView) v.findViewById(R.id.speedStatus);
+ CheckBox logScaleView = (CheckBox) v.findViewById(R.id.useLogScale);
+ mLogScale = getActivity().getPreferences(MODE_PRIVATE).getBoolean(PREF_USE_LOG, false);
+ logScaleView.setChecked(mLogScale);
+
+ List<Integer> charts = new LinkedList<>();
+ charts.add(TIME_PERIOD_SECDONS);
+ charts.add(TIME_PERIOD_MINUTES);
+ charts.add(TIME_PERIOD_HOURS);
+
+ mChartAdapter = new ChartDataAdapter(getActivity(), charts);
+ mListView.setAdapter(mChartAdapter);
+
+ mColourIn = getActivity().getResources().getColor(R.color.dataIn);
+ mColourOut = getActivity().getResources().getColor(R.color.dataOut);
+ mColourPoint = getActivity().getResources().getColor(android.R.color.black);
+
+ logScaleView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mLogScale = isChecked;
+ mChartAdapter.notifyDataSetChanged();
+ getActivity().getPreferences(MODE_PRIVATE).edit().putBoolean(PREF_USE_LOG, isChecked).apply();
+ }
+ });
+
+ mHandler = new Handler();
+
+ return v;
+
+
+ }
+
+ private Runnable triggerRefresh = new Runnable() {
+ @Override
+ public void run() {
+ mChartAdapter.notifyDataSetChanged();
+ mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500);
+ }
+ };
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ VpnStatus.addByteCountListener(this);
+ mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ mHandler.removeCallbacks(triggerRefresh);
+ VpnStatus.removeByteCountListener(this);
+ }
+
+ @Override
+ public void updateByteCount(long in, long out, long diffIn, long diffOut) {
+ if (firstTs == 0)
+ firstTs = System.currentTimeMillis() / 100;
+
+ long now = (System.currentTimeMillis() / 100) - firstTs;
+ int interval = OpenVPNManagement.mBytecountInterval * 10;
+ Resources res = getActivity().getResources();
+
+ final String netstat = String.format(getString(R.string.statusline_bytecount),
+ humanReadableByteCount(in, false, res),
+ humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res),
+ humanReadableByteCount(out, false, res),
+ humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res));
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mHandler.removeCallbacks(triggerRefresh);
+ mSpeedStatus.setText(netstat);
+ mChartAdapter.notifyDataSetChanged();
+ mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500);
+ }
+ });
+
+ }
+
+ private class ChartDataAdapter extends ArrayAdapter<Integer> {
+
+ private Context mContext;
+
+ public ChartDataAdapter(Context context, List<Integer> trafficData) {
+ super(context, 0, trafficData);
+ mContext = context;
+ }
+
+ @NonNull
+ @Override
+ public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ViewHolder holder = null;
+
+ if (convertView == null) {
+
+ holder = new ViewHolder();
+
+ convertView = LayoutInflater.from(mContext).inflate(
+ R.layout.graph_item, parent, false);
+ holder.chart = (LineChart) convertView.findViewById(R.id.chart);
+ holder.title = (TextView) convertView.findViewById(R.id.tvName);
+ convertView.setTag(holder);
+
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ // apply styling
+ // holder.chart.setValueTypeface(mTf);
+ holder.chart.getDescription().setEnabled(false);
+ holder.chart.setDrawGridBackground(false);
+
+ XAxis xAxis = holder.chart.getXAxis();
+ xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
+ xAxis.setDrawGridLines(false);
+ xAxis.setDrawAxisLine(true);
+
+ switch (position) {
+ case TIME_PERIOD_HOURS:
+ holder.title.setText(R.string.avghour);
+ break;
+ case TIME_PERIOD_MINUTES:
+ holder.title.setText(R.string.avgmin);
+ break;
+ default:
+ holder.title.setText(R.string.last5minutes);
+ break;
+ }
+
+ xAxis.setValueFormatter(new IAxisValueFormatter() {
+
+
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ switch (position) {
+ case TIME_PERIOD_HOURS:
+ return String.format(Locale.getDefault(), "%.0f\u2009h ago", (axis.getAxisMaximum() - value) / 10 / 3600);
+ case TIME_PERIOD_MINUTES:
+ return String.format(Locale.getDefault(), "%.0f\u2009m ago", (axis.getAxisMaximum() - value) / 10 / 60);
+ default:
+ return String.format(Locale.getDefault(), "%.0f\u2009s ago", (axis.getAxisMaximum() - value) / 10);
+ }
+
+ }
+ });
+ xAxis.setLabelCount(5);
+
+ YAxis yAxis = holder.chart.getAxisLeft();
+ yAxis.setLabelCount(5, false);
+
+ final Resources res = getActivity().getResources();
+ yAxis.setValueFormatter(new IAxisValueFormatter() {
+ @Override
+ public String getFormattedValue(float value, AxisBase axis) {
+ if (mLogScale && value < 2.1f)
+ return "< 100\u2009bit/s";
+ if (mLogScale)
+ value = (float) Math.pow(10, value) / 8;
+
+ return humanReadableByteCount((long) value, true, res);
+ }
+ });
+
+ holder.chart.getAxisRight().setEnabled(false);
+
+ LineData data = getDataSet(position);
+ float ymax = data.getYMax();
+
+ if (mLogScale) {
+ yAxis.setAxisMinimum(2f);
+ yAxis.setAxisMaximum((float) Math.ceil(ymax));
+ yAxis.setLabelCount((int) (Math.ceil(ymax - 2f)));
+ } else {
+ yAxis.setAxisMinimum(0f);
+ yAxis.resetAxisMaximum();
+ yAxis.setLabelCount(6);
+ }
+
+ if (data.getDataSetByIndex(0).getEntryCount() < 3)
+ holder.chart.setData(null);
+ else
+ holder.chart.setData(data);
+
+ holder.chart.setNoDataText(getString(R.string.notenoughdata));
+
+ holder.chart.invalidate();
+ //holder.chart.animateX(750);
+
+ return convertView;
+ }
+
+ private LineData getDataSet(int timeperiod) {
+
+ LinkedList<Entry> dataIn = new LinkedList<>();
+ LinkedList<Entry> dataOut = new LinkedList<>();
+
+ long interval;
+ long totalInterval;
+
+ LinkedList<TrafficHistory.TrafficDatapoint> list;
+ switch (timeperiod) {
+ case TIME_PERIOD_HOURS:
+ list = VpnStatus.trafficHistory.getHours();
+ interval = TrafficHistory.TIME_PERIOD_HOURS;
+ totalInterval = 0;
+ break;
+ case TIME_PERIOD_MINUTES:
+ list = VpnStatus.trafficHistory.getMinutes();
+ interval = TrafficHistory.TIME_PERIOD_MINTUES;
+ totalInterval = TrafficHistory.TIME_PERIOD_HOURS * TrafficHistory.PERIODS_TO_KEEP;
+ ;
+
+ break;
+ default:
+ list = VpnStatus.trafficHistory.getSeconds();
+ interval = OpenVPNManagement.mBytecountInterval * 1000;
+ totalInterval = TrafficHistory.TIME_PERIOD_MINTUES * TrafficHistory.PERIODS_TO_KEEP;
+ break;
+ }
+ if (list.size() == 0) {
+ list = TrafficHistory.getDummyList();
+ }
+
+
+ long lastts = 0;
+ float zeroValue;
+ if (mLogScale)
+ zeroValue = 2;
+ else
+ zeroValue = 0;
+
+ long now = System.currentTimeMillis();
+
+
+ long firstTimestamp = 0;
+ long lastBytecountOut = 0;
+ long lastBytecountIn = 0;
+
+ for (TrafficHistory.TrafficDatapoint tdp : list) {
+ if (totalInterval != 0 && (now - tdp.timestamp) > totalInterval)
+ continue;
+
+ if (firstTimestamp == 0) {
+ firstTimestamp = list.peek().timestamp;
+ lastBytecountIn = list.peek().in;
+ lastBytecountOut = list.peek().out;
+ }
+
+ float t = (tdp.timestamp - firstTimestamp) / 100f;
+
+ float in = (tdp.in - lastBytecountIn) / (float) (interval / 1000);
+ float out = (tdp.out - lastBytecountOut) / (float) (interval / 1000);
+
+ lastBytecountIn = tdp.in;
+ lastBytecountOut = tdp.out;
+
+ if (mLogScale) {
+ in = max(2f, (float) Math.log10(in * 8));
+ out = max(2f, (float) Math.log10(out * 8));
+ }
+
+ if (lastts > 0 && (tdp.timestamp - lastts > 2 * interval)) {
+ dataIn.add(new Entry((lastts - firstTimestamp + interval) / 100f, zeroValue));
+ dataOut.add(new Entry((lastts - firstTimestamp + interval) / 100f, zeroValue));
+
+ dataIn.add(new Entry(t - interval / 100f, zeroValue));
+ dataOut.add(new Entry(t - interval / 100f, zeroValue));
+ }
+
+ lastts = tdp.timestamp;
+
+ dataIn.add(new Entry(t, in));
+ dataOut.add(new Entry(t, out));
+
+ }
+ if (lastts < now - interval) {
+
+ if (now - lastts > 2 * interval * 1000) {
+ dataIn.add(new Entry((lastts - firstTimestamp + interval * 1000) / 100f, zeroValue));
+ dataOut.add(new Entry((lastts - firstTimestamp + interval * 1000) / 100f, zeroValue));
+ }
+
+ dataIn.add(new Entry((now - firstTimestamp) / 100, zeroValue));
+ dataOut.add(new Entry((now - firstTimestamp) / 100, zeroValue));
+ }
+
+ ArrayList<ILineDataSet> dataSets = new ArrayList<>();
+
+
+ LineDataSet indata = new LineDataSet(dataIn, getString(R.string.data_in));
+ LineDataSet outdata = new LineDataSet(dataOut, getString(R.string.data_out));
+
+ setLineDataAttributes(indata, mColourIn);
+ setLineDataAttributes(outdata, mColourOut);
+
+ dataSets.add(indata);
+ dataSets.add(outdata);
+
+ return new LineData(dataSets);
+ }
+
+ private void setLineDataAttributes(LineDataSet dataSet, int colour) {
+ dataSet.setLineWidth(2);
+ dataSet.setCircleRadius(1);
+ dataSet.setDrawCircles(true);
+ dataSet.setCircleColor(mColourPoint);
+ dataSet.setDrawFilled(true);
+ dataSet.setFillAlpha(42);
+ dataSet.setFillColor(colour);
+ dataSet.setColor(colour);
+ dataSet.setMode(LineDataSet.Mode.LINEAR);
+
+ dataSet.setDrawValues(false);
+ }
+ }
+
+ private static class ViewHolder {
+ LineChart chart;
+ TextView title;
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java b/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java
new file mode 100644
index 00000000..41206a54
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.R;
+
+public class InlineFileTab extends Fragment
+{
+
+ private static final int MENU_SAVE = 0;
+ private EditText mInlineData;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mInlineData.setText(((FileSelect)getActivity()).getInlineData());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState)
+ {
+
+ View v = inflater.inflate(R.layout.file_dialog_inline, container, false);
+ mInlineData =(EditText) v.findViewById(R.id.inlineFileData);
+ return v;
+ }
+
+ public void setData(String data) {
+ if(mInlineData!=null)
+ mInlineData.setText(data);
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.add(0, MENU_SAVE, 0, R.string.menu_use_inline_data)
+ .setIcon(android.R.drawable.ic_menu_save)
+ .setAlphabeticShortcut('u')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==MENU_SAVE){
+ ((FileSelect)getActivity()).saveInlineData(null, mInlineData.getText().toString());
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
new file mode 100644
index 00000000..323b3a4d
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2012-2018 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Message
+import android.security.KeyChain
+import android.security.KeyChainException
+import android.security.keystore.KeyInfo
+import android.text.TextUtils
+import android.view.View
+import android.widget.AdapterView
+import android.widget.Spinner
+import android.widget.TextView
+import android.widget.Toast
+import de.blinkt.openvpn.R
+import de.blinkt.openvpn.VpnProfile
+import de.blinkt.openvpn.api.ExternalCertificateProvider
+import de.blinkt.openvpn.core.ExtAuthHelper
+import de.blinkt.openvpn.core.X509Utils
+import java.security.KeyFactory
+import java.security.PrivateKey
+
+import java.security.cert.X509Certificate
+
+internal abstract class KeyChainSettingsFragment : Settings_Fragment(), View.OnClickListener, Handler.Callback {
+
+
+ private lateinit var mAliasCertificate: TextView
+ private lateinit var mAliasName: TextView
+ private var mHandler: Handler? = null
+ private lateinit var mExtAliasName: TextView
+ private lateinit var mExtAuthSpinner: Spinner
+
+ private val isInHardwareKeystore: Boolean
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ @Throws(KeyChainException::class, InterruptedException::class)
+ get() {
+ val key : PrivateKey = KeyChain.getPrivateKey(activity.applicationContext, mProfile.mAlias) ?: return false
+
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
+ {
+ val keyFactory = KeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore")
+ val keyInfo = keyFactory.getKeySpec(key, KeyInfo::class.java)
+ return keyInfo.isInsideSecureHardware()
+
+ } else {
+ val algorithm = key.algorithm
+ return KeyChain.isBoundKeyAlgorithm(algorithm)
+ }
+ }
+
+
+ private fun setKeyStoreAlias() {
+ if (mProfile.mAlias == null) {
+ mAliasName.setText(R.string.client_no_certificate)
+ mAliasCertificate.text = ""
+ } else {
+ mAliasCertificate.text = "Loading certificate from Keystore..."
+ mAliasName.text = mProfile.mAlias
+ setCertificate(false)
+ }
+ }
+
+ private fun setExtAlias() {
+ if (mProfile.mAlias == null) {
+ mExtAliasName.setText(R.string.extauth_not_configured)
+ mAliasCertificate.text = ""
+ } else {
+ mAliasCertificate.text = "Querying certificate from external provider..."
+ mExtAliasName.text = ""
+ setCertificate(true)
+ }
+ }
+
+ private fun fetchExtCertificateMetaData() {
+ object : Thread() {
+ override fun run() {
+ try {
+ val b = ExtAuthHelper.getCertificateMetaData(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)
+ mProfile.mAlias = b.getString(ExtAuthHelper.EXTRA_ALIAS)
+ activity.runOnUiThread { setAlias() }
+ } catch (e: KeyChainException) {
+ e.printStackTrace()
+ }
+
+ }
+ }.start()
+ }
+
+
+ protected fun setCertificate(external: Boolean) {
+ object : Thread() {
+ override fun run() {
+ var certstr = ""
+ var metadata: Bundle? = null
+ try {
+ val cert: X509Certificate?
+
+ if (external) {
+ if (!TextUtils.isEmpty(mProfile.mExternalAuthenticator) && !TextUtils.isEmpty(mProfile.mAlias)) {
+ cert = ExtAuthHelper.getCertificateChain(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)!![0]
+ metadata = ExtAuthHelper.getCertificateMetaData(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)
+ } else {
+ cert = null
+ certstr = getString(R.string.extauth_not_configured)
+ }
+ } else {
+ cert = KeyChain.getCertificateChain(activity.applicationContext, mProfile.mAlias)!![0]
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ run {
+ if (isInHardwareKeystore)
+ certstr += getString(R.string.hwkeychain)
+ }
+ }
+ }
+ if (cert != null) {
+ certstr += X509Utils.getCertificateValidityString(cert, resources)
+ certstr += X509Utils.getCertificateFriendlyName(cert)
+ }
+
+
+ } catch (e: Exception) {
+ certstr = "Could not get certificate from Keystore: " + e.localizedMessage!!
+ }
+
+ val certStringCopy = certstr
+ val finalMetadata = metadata
+ activity.runOnUiThread {
+ mAliasCertificate.text = certStringCopy
+ if (finalMetadata != null)
+ mExtAliasName.text = finalMetadata.getString(ExtAuthHelper.EXTRA_DESCRIPTION)
+
+ }
+
+ }
+ }.start()
+ }
+
+ protected fun initKeychainViews(v: View) {
+ v.findViewById<View>(R.id.select_keystore_button).setOnClickListener(this)
+ v.findViewById<View>(R.id.configure_extauth_button)?.setOnClickListener(this)
+ v.findViewById<View>(R.id.install_keystore_button).setOnClickListener(this)
+ mAliasCertificate = v.findViewById(R.id.alias_certificate)
+ mExtAuthSpinner = v.findViewById(R.id.extauth_spinner)
+ mExtAuthSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
+ val selectedProvider = parent.getItemAtPosition(position) as ExtAuthHelper.ExternalAuthProvider
+ if (selectedProvider.packageName != mProfile.mExternalAuthenticator) {
+ mProfile.mAlias = ""
+ }
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>) {
+
+ }
+ }
+ mExtAliasName = v.findViewById(R.id.extauth_detail)
+ mAliasName = v.findViewById(R.id.aliasname)
+ if (mHandler == null) {
+ mHandler = Handler(this)
+ }
+ ExtAuthHelper.setExternalAuthProviderSpinnerList(mExtAuthSpinner, mProfile.mExternalAuthenticator)
+
+ v.findViewById<View>(R.id.install_keystore_button).setOnClickListener {
+ startActivity(KeyChain.createInstallIntent()) };
+ }
+
+ override fun onClick(v: View) {
+ if (v === v.findViewById<View>(R.id.select_keystore_button)) {
+ showCertDialog()
+ } else if (v === v.findViewById<View>(R.id.configure_extauth_button)) {
+ startExternalAuthConfig()
+ }
+ }
+
+ private fun startExternalAuthConfig() {
+ val eAuth = mExtAuthSpinner.selectedItem as ExtAuthHelper.ExternalAuthProvider
+ mProfile.mExternalAuthenticator = eAuth.packageName
+ if (!eAuth.configurable) {
+ fetchExtCertificateMetaData()
+ return
+ }
+ val extauth = Intent(ExtAuthHelper.ACTION_CERT_CONFIGURATION)
+ extauth.setPackage(eAuth.packageName)
+ extauth.putExtra(ExtAuthHelper.EXTRA_ALIAS, mProfile.mAlias)
+ startActivityForResult(extauth, UPDATEE_EXT_ALIAS)
+ }
+
+ override fun savePreferences() {
+
+ }
+
+ override fun onStart() {
+ super.onStart()
+ loadPreferences()
+ }
+
+ fun showCertDialog() {
+ try {
+ KeyChain.choosePrivateKeyAlias(activity,
+ { alias ->
+ // Credential alias selected. Remember the alias selection for future use.
+ mProfile.mAlias = alias
+ mHandler!!.sendEmptyMessage(UPDATE_ALIAS)
+ },
+ arrayOf("RSA", "EC"), null, // issuer, null for any
+ mProfile.mServerName, // host name of server requesting the cert, null if unavailable
+ -1, // port of server requesting the cert, -1 if unavailable
+ mProfile.mAlias)// List of acceptable key types. null for any
+ // alias to preselect, null if unavailable
+ } catch (anf: ActivityNotFoundException) {
+ val ab = AlertDialog.Builder(activity)
+ ab.setTitle(R.string.broken_image_cert_title)
+ ab.setMessage(R.string.broken_image_cert)
+ ab.setPositiveButton(android.R.string.ok, null)
+ ab.show()
+ }
+
+ }
+
+ protected open fun loadPreferences() {
+ setAlias()
+
+ }
+
+ private fun setAlias() {
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_EXTERNAL_APP)
+ setExtAlias()
+ else
+ setKeyStoreAlias()
+ }
+
+ override fun handleMessage(msg: Message): Boolean {
+ setAlias()
+ return true
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ if (requestCode == UPDATEE_EXT_ALIAS && resultCode == Activity.RESULT_OK) {
+ mProfile.mAlias = data.getStringExtra(ExtAuthHelper.EXTRA_ALIAS)
+ mExtAliasName.text = data.getStringExtra(ExtAuthHelper.EXTRA_DESCRIPTION)
+ }
+ }
+
+ companion object {
+ private val UPDATE_ALIAS = 20
+ private val UPDATEE_EXT_ALIAS = 210
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
new file mode 100644
index 00000000..e64ce2cd
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java
@@ -0,0 +1,694 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.DataSetObserver;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.text.SpannableString;
+import android.text.format.DateFormat;
+import android.text.style.ImageSpan;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Vector;
+
+import de.blinkt.openvpn.LaunchVPN;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.DisconnectVPN;
+import de.blinkt.openvpn.activities.MainActivity;
+import de.blinkt.openvpn.activities.VPNPreferences;
+import de.blinkt.openvpn.core.ConnectionStatus;
+import de.blinkt.openvpn.core.OpenVPNManagement;
+import de.blinkt.openvpn.core.OpenVPNService;
+import de.blinkt.openvpn.core.Preferences;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.LogItem;
+import de.blinkt.openvpn.core.VpnStatus.LogListener;
+import de.blinkt.openvpn.core.VpnStatus.StateListener;
+
+import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount;
+
+public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener {
+ private static final String LOGTIMEFORMAT = "logtimeformat";
+ private static final int START_VPN_CONFIG = 0;
+ private static final String VERBOSITYLEVEL = "verbositylevel";
+
+
+
+ private SeekBar mLogLevelSlider;
+ private LinearLayout mOptionsLayout;
+ private RadioGroup mTimeRadioGroup;
+ private TextView mUpStatus;
+ private TextView mDownStatus;
+ private TextView mConnectStatus;
+ private boolean mShowOptionsLayout;
+ private CheckBox mClearLogCheckBox;
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ ladapter.setLogLevel(progress + 1);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ switch (checkedId) {
+ case R.id.radioISO:
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO);
+ break;
+ case R.id.radioNone:
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE);
+ break;
+ case R.id.radioShort:
+ ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT);
+ break;
+
+ }
+ }
+
+ @Override
+ public void updateByteCount(long in, long out, long diffIn, long diffOut) {
+ //%2$s/s %1$s - ↑%4$s/s %3$s
+ Resources res = getActivity().getResources();
+ final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res));
+ final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res));
+
+ if (mUpStatus != null && mDownStatus != null) {
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mUpStatus.setText(up);
+ mDownStatus.setText(down);
+ }
+ });
+ }
+ }
+
+ }
+
+
+ class LogWindowListAdapter implements ListAdapter, LogListener, Callback {
+
+ private static final int MESSAGE_NEWLOG = 0;
+
+ private static final int MESSAGE_CLEARLOG = 1;
+
+ private static final int MESSAGE_NEWTS = 2;
+ private static final int MESSAGE_NEWLOGLEVEL = 3;
+
+ public static final int TIME_FORMAT_NONE = 0;
+ public static final int TIME_FORMAT_SHORT = 1;
+ public static final int TIME_FORMAT_ISO = 2;
+ private static final int MAX_STORED_LOG_ENTRIES = 1000;
+
+ private Vector<LogItem> allEntries = new Vector<>();
+
+ private Vector<LogItem> currentLevelEntries = new Vector<LogItem>();
+
+ private Handler mHandler;
+
+ private Vector<DataSetObserver> observers = new Vector<DataSetObserver>();
+
+ private int mTimeFormat = 0;
+ private int mLogLevel = 3;
+
+
+ public LogWindowListAdapter() {
+ initLogBuffer();
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ VpnStatus.addLogListener(this);
+ }
+
+
+ private void initLogBuffer() {
+ allEntries.clear();
+ Collections.addAll(allEntries, VpnStatus.getlogbuffer());
+ initCurrentMessages();
+ }
+
+ String getLogStr() {
+ String str = "";
+ for (LogItem entry : allEntries) {
+ str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n';
+ }
+ return str;
+ }
+
+
+ private void shareLog() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr());
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, "Send Logfile"));
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ observers.add(observer);
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ observers.remove(observer);
+ }
+
+ @Override
+ public int getCount() {
+ return currentLevelEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return currentLevelEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return ((Object) currentLevelEntries.get(position)).hashCode();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView v;
+ if (convertView == null)
+ v = new TextView(getActivity());
+ else
+ v = (TextView) convertView;
+
+ LogItem le = currentLevelEntries.get(position);
+ String msg = le.getString(getActivity());
+ String time = getTime(le, mTimeFormat);
+ msg = time + msg;
+
+ int spanStart = time.length();
+
+ SpannableString t = new SpannableString(msg);
+
+ //t.setSpan(getSpanImage(le,(int)v.getTextSize()),spanStart,spanStart+1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ v.setText(t);
+ return v;
+ }
+
+ private String getTime(LogItem le, int time) {
+ if (time != TIME_FORMAT_NONE) {
+ Date d = new Date(le.getLogtime());
+ java.text.DateFormat timeformat;
+ if (time == TIME_FORMAT_ISO)
+ timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ else
+ timeformat = DateFormat.getTimeFormat(getActivity());
+
+ return timeformat.format(d) + " ";
+
+ } else {
+ return "";
+ }
+
+ }
+
+ private ImageSpan getSpanImage(LogItem li, int imageSize) {
+ int imageRes = android.R.drawable.ic_menu_call;
+
+ switch (li.getLogLevel()) {
+ case ERROR:
+ imageRes = android.R.drawable.ic_notification_clear_all;
+ break;
+ case INFO:
+ imageRes = android.R.drawable.ic_menu_compass;
+ break;
+ case VERBOSE:
+ imageRes = android.R.drawable.ic_menu_info_details;
+ break;
+ case WARNING:
+ imageRes = android.R.drawable.ic_menu_camera;
+ break;
+ }
+
+ Drawable d = getResources().getDrawable(imageRes);
+
+
+ //d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ d.setBounds(0, 0, imageSize, imageSize);
+ ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM);
+
+ return span;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return currentLevelEntries.isEmpty();
+
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public void newLog(LogItem logMessage) {
+ Message msg = Message.obtain();
+ assert (msg != null);
+ msg.what = MESSAGE_NEWLOG;
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("logmessage", logMessage);
+ msg.setData(bundle);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ // We have been called
+ if (msg.what == MESSAGE_NEWLOG) {
+
+ LogItem logMessage = msg.getData().getParcelable("logmessage");
+ if (addLogMessage(logMessage))
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+ } else if (msg.what == MESSAGE_CLEARLOG) {
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ initLogBuffer();
+ } else if (msg.what == MESSAGE_NEWTS) {
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ } else if (msg.what == MESSAGE_NEWLOGLEVEL) {
+ initCurrentMessages();
+
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+
+ }
+
+ return true;
+ }
+
+ private void initCurrentMessages() {
+ currentLevelEntries.clear();
+ for (LogItem li : allEntries) {
+ if (li.getVerbosityLevel() <= mLogLevel ||
+ mLogLevel == VpnProfile.MAXLOGLEVEL)
+ currentLevelEntries.add(li);
+ }
+ }
+
+ /**
+ * @param logmessage
+ * @return True if the current entries have changed
+ */
+ private boolean addLogMessage(LogItem logmessage) {
+ allEntries.add(logmessage);
+
+ if (allEntries.size() > MAX_STORED_LOG_ENTRIES) {
+ Vector<LogItem> oldAllEntries = allEntries;
+ allEntries = new Vector<LogItem>(allEntries.size());
+ for (int i = 50; i < oldAllEntries.size(); i++) {
+ allEntries.add(oldAllEntries.elementAt(i));
+ }
+ initCurrentMessages();
+ return true;
+ } else {
+ if (logmessage.getVerbosityLevel() <= mLogLevel) {
+ currentLevelEntries.add(logmessage);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ void clearLog() {
+ // Actually is probably called from GUI Thread as result of the user
+ // pressing a button. But better safe than sorry
+ VpnStatus.clearLog();
+ VpnStatus.logInfo(R.string.logCleared);
+ mHandler.sendEmptyMessage(MESSAGE_CLEARLOG);
+ }
+
+
+ public void setTimeFormat(int newTimeFormat) {
+ mTimeFormat = newTimeFormat;
+ mHandler.sendEmptyMessage(MESSAGE_NEWTS);
+ }
+
+ public void setLogLevel(int logLevel) {
+ mLogLevel = logLevel;
+ mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL);
+ }
+
+ }
+
+
+ private LogWindowListAdapter ladapter;
+ private TextView mSpeedView;
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.clearlog) {
+ ladapter.clearLog();
+ return true;
+ } else if (item.getItemId() == R.id.cancel) {
+ Intent intent = new Intent(getActivity(), DisconnectVPN.class);
+ startActivity(intent);
+ return true;
+ } else if (item.getItemId() == R.id.send) {
+ ladapter.shareLog();
+ } else if (item.getItemId() == R.id.edit_vpn) {
+ VpnProfile lastConnectedprofile = ProfileManager.get(getActivity(), VpnStatus.getLastConnectedVPNProfile());
+
+ if (lastConnectedprofile != null) {
+ Intent vprefintent = new Intent(getActivity(), VPNPreferences.class)
+ .putExtra(VpnProfile.EXTRA_PROFILEUUID, lastConnectedprofile.getUUIDString());
+ startActivityForResult(vprefintent, START_VPN_CONFIG);
+ } else {
+ Toast.makeText(getActivity(), R.string.log_no_last_vpn, Toast.LENGTH_LONG).show();
+ }
+ } else if (item.getItemId() == R.id.toggle_time) {
+ showHideOptionsPanel();
+ } else if (item.getItemId() == android.R.id.home) {
+ // This is called when the Home (Up) button is pressed
+ // in the Action Bar.
+ Intent parentActivityIntent = new Intent(getActivity(), MainActivity.class);
+ parentActivityIntent.addFlags(
+ Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(parentActivityIntent);
+ getActivity().finish();
+ return true;
+
+ }
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ private void showHideOptionsPanel() {
+ boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE);
+
+ ObjectAnimator anim;
+ if (optionsVisible) {
+ anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f);
+ anim.addListener(collapseListener);
+
+ } else {
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f);
+ //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f);
+
+ }
+
+ //anim.setInterpolator(new AccelerateInterpolator(1.0f));
+ //anim.setDuration(300);
+ //mOptionsLayout.startAnimation(anim);
+ anim.start();
+
+ }
+
+ AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mOptionsLayout.setVisibility(View.GONE);
+ }
+
+ };
+
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.logmenu, menu);
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible))
+ menu.removeItem(R.id.toggle_time);
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Intent intent = new Intent(getActivity(), OpenVPNService.class);
+ intent.setAction(OpenVPNService.START_SERVICE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ VpnStatus.addStateListener(this);
+ VpnStatus.addByteCountListener(this);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == START_VPN_CONFIG && resultCode == Activity.RESULT_OK) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ final VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN);
+ ProfileManager.getInstance(getActivity()).saveProfile(getActivity(), profile);
+ // Name could be modified, reset List adapter
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity());
+ dialog.setTitle(R.string.configuration_changed);
+ dialog.setMessage(R.string.restart_vpn_after_change);
+
+
+ dialog.setPositiveButton(R.string.restart,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(getActivity(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+
+
+ });
+ dialog.setNegativeButton(R.string.ignore, null);
+ dialog.create().show();
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ VpnStatus.removeStateListener(this);
+ VpnStatus.removeByteCountListener(this);
+
+ getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat)
+ .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply();
+
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ ListView lv = getListView();
+
+ lv.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ ClipboardManager clipboard = (ClipboardManager)
+ getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Log Entry", ((TextView) view).getText());
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ });
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.log_fragment, container, false);
+
+ setHasOptionsMenu(true);
+
+ ladapter = new LogWindowListAdapter();
+ ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1);
+ int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1);
+ ladapter.setLogLevel(logLevel);
+
+ setListAdapter(ladapter);
+
+ mTimeRadioGroup = (RadioGroup) v.findViewById(R.id.timeFormatRadioGroup);
+ mTimeRadioGroup.setOnCheckedChangeListener(this);
+
+ if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) {
+ mTimeRadioGroup.check(R.id.radioISO);
+ } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) {
+ mTimeRadioGroup.check(R.id.radioNone);
+ } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) {
+ mTimeRadioGroup.check(R.id.radioShort);
+ }
+
+ mClearLogCheckBox = (CheckBox) v.findViewById(R.id.clearlogconnect);
+ mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(LaunchVPN.CLEARLOG, true));
+ mClearLogCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(LaunchVPN.CLEARLOG, isChecked).apply();
+ }
+ });
+
+ mSpeedView = (TextView) v.findViewById(R.id.speed);
+
+ mOptionsLayout = (LinearLayout) v.findViewById(R.id.logOptionsLayout);
+ mLogLevelSlider = (SeekBar) v.findViewById(R.id.LogLevelSlider);
+ mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1);
+ mLogLevelSlider.setProgress(logLevel - 1);
+
+ mLogLevelSlider.setOnSeekBarChangeListener(this);
+
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible))
+ mOptionsLayout.setVisibility(View.VISIBLE);
+
+ mUpStatus = (TextView) v.findViewById(R.id.speedUp);
+ mDownStatus = (TextView) v.findViewById(R.id.speedDown);
+ mConnectStatus = (TextView) v.findViewById(R.id.speedStatus);
+ if (mShowOptionsLayout)
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // Scroll to the end of the list end
+ //getListView().setSelection(getListView().getAdapter().getCount()-1);
+ }
+
+ @Override
+ public void onAttach(Context activity) {
+ super.onAttach(activity);
+ if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) {
+ mShowOptionsLayout = true;
+ if (mOptionsLayout != null)
+ mOptionsLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ //getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ }
+
+
+ @Override
+ public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) {
+ if (isAdded()) {
+ final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity());
+
+ getActivity().runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (isAdded()) {
+ if (mSpeedView != null) {
+ mSpeedView.setText(cleanLogMessage);
+ }
+ if (mConnectStatus != null)
+ mConnectStatus.setText(cleanLogMessage);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setConnectedVPN(String uuid) {
+ }
+
+
+ @Override
+ public void onDestroy() {
+ VpnStatus.removeLogListener(ladapter);
+ super.onDestroy();
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java
new file mode 100644
index 00000000..9ac8bebb
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+public abstract class OpenVpnPreferencesFragment extends PreferenceFragment {
+
+ protected VpnProfile mProfile;
+
+ protected abstract void loadSettings();
+ protected abstract void saveSettings();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile = ProfileManager.get(getActivity(),profileUUID);
+ getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ saveSettings();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if(savedInstanceState!=null) {
+ String profileUUID=savedInstanceState.getString(VpnProfile.EXTRA_PROFILEUUID);
+ mProfile = ProfileManager.get(getActivity(),profileUUID);
+ loadSettings();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveSettings();
+ outState.putString(VpnProfile.EXTRA_PROFILEUUID, mProfile.getUUIDString());
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java
new file mode 100644
index 00000000..0fe40905
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Date;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.VpnStatus;
+
+public class SendDumpFragment extends Fragment {
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ final View v = inflater.inflate(R.layout.fragment_senddump, container, false);
+ v.findViewById(R.id.senddump).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ emailMiniDumps();
+ }
+ });
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ final Pair<File, Long> ldump = getLastestDump(getActivity());
+ if (ldump==null)
+ return;
+ // Do in background since it does I/O
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TextView dumpDateText = (TextView) v.findViewById(R.id.dumpdate);
+ String datestr = (new Date(ldump.second)).toString();
+ long timediff = System.currentTimeMillis() - ldump.second;
+ long minutes = timediff / 1000 / 60 % 60;
+ long hours = timediff / 1000 / 60 / 60;
+ dumpDateText.setText(getString(R.string.lastdumpdate, hours, minutes, datestr));
+
+ }
+ });
+ }
+ }).start();
+ return v;
+ }
+
+ public void emailMiniDumps()
+ {
+ //need to "send multiple" to get more than one attachment
+ final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);
+ emailIntent.setType("*/*");
+ emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
+ new String[]{"Arne Schwabe <arne@rfc2549.org>"});
+
+ String version;
+ String name="ics-openvpn";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = packageinfo.applicationInfo.name;
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format("%s(%s) %s Minidump",name, getActivity().getPackageName(), version));
+
+ emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe the issue you have experienced");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+
+ Pair<File, Long> ldump = getLastestDump(getActivity());
+ if(ldump==null) {
+ VpnStatus.logError("No Minidump found!");
+ }
+
+ uris.add(Uri.parse("content://de.blinkt.openvpn.FileProvider/" + ldump.first.getName()));
+ uris.add(Uri.parse("content://de.blinkt.openvpn.FileProvider/" + ldump.first.getName() + ".log"));
+
+ emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ startActivity(emailIntent);
+ }
+
+ static public Pair<File,Long> getLastestDump(Context c) {
+ long newestDumpTime=0;
+ File newestDumpFile=null;
+
+ if (c.getCacheDir() ==null)
+ return null;
+
+ for(File f:c.getCacheDir().listFiles()) {
+ if(!f.getName().endsWith(".dmp"))
+ continue;
+
+ if (newestDumpTime < f.lastModified()) {
+ newestDumpTime = f.lastModified();
+ newestDumpFile=f;
+ }
+ }
+ // Ignore old dumps
+ if(System.currentTimeMillis() - 48 * 60 * 1000 > newestDumpTime )
+ return null;
+
+ return Pair.create(newestDumpFile, newestDumpTime);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt
new file mode 100644
index 00000000..dd2aa3b7
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments
+
+import android.Manifest
+import android.app.Activity
+import android.app.Fragment
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.BaseAdapter
+import android.widget.CompoundButton
+import android.widget.Filter
+import android.widget.Filterable
+import android.widget.ImageView
+import android.widget.ListView
+import android.widget.SearchView
+import android.widget.Switch
+import android.widget.TextView
+
+import java.util.Collections
+import java.util.Locale
+import java.util.Vector
+
+import de.blinkt.openvpn.R
+import de.blinkt.openvpn.VpnProfile
+import de.blinkt.openvpn.core.ProfileManager
+
+/**
+ * Created by arne on 16.11.14.
+ */
+class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener {
+ private lateinit var mListView: ListView
+ private lateinit var mProfile: VpnProfile
+ private lateinit var mDefaultAllowTextView: TextView
+ private lateinit var mListAdapter: PackageAdapter
+ private lateinit var mSettingsView: View
+
+
+ override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
+ val avh = view.tag as AppViewHolder
+ avh.checkBox.toggle()
+ }
+
+ override fun onClick(v: View) {
+
+ }
+
+ override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
+ val packageName = buttonView.tag as String
+ if (isChecked) {
+ mProfile.mAllowedAppsVpn.add(packageName)
+ } else {
+ mProfile.mAllowedAppsVpn.remove(packageName)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ changeDisallowText(mProfile.mAllowedAppsVpnAreDisallowed)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val profileUuid = arguments.getString(activity.packageName + ".profileUUID")
+ mProfile = ProfileManager.get(activity, profileUuid)
+ activity.title = getString(R.string.edit_profile_title, mProfile.name)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.allowed_apps, menu)
+
+ val searchView = menu.findItem(R.id.app_search_widget).actionView as SearchView
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ mListView.setFilterText(query)
+ mListView.isTextFilterEnabled = true
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ mListView.setFilterText(newText)
+ mListView.isTextFilterEnabled = !TextUtils.isEmpty(newText)
+
+ return true
+ }
+ })
+ searchView.setOnCloseListener {
+ mListView.clearTextFilter()
+ mListAdapter.filter.filter("")
+ false
+ }
+
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val v = inflater.inflate(R.layout.allowed_vpn_apps, container, false)
+
+ mSettingsView = inflater.inflate(R.layout.allowed_application_settings, container, false)
+ mDefaultAllowTextView = mSettingsView.findViewById<View>(R.id.default_allow_text) as TextView
+
+ val vpnOnDefaultSwitch = mSettingsView.findViewById<View>(R.id.default_allow) as Switch
+
+ vpnOnDefaultSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
+ changeDisallowText(isChecked)
+ mProfile.mAllowedAppsVpnAreDisallowed = isChecked
+ }
+
+ vpnOnDefaultSwitch.isChecked = mProfile.mAllowedAppsVpnAreDisallowed
+
+ val vpnAllowBypassSwitch = mSettingsView.findViewById<View>(R.id.allow_bypass) as Switch
+
+ vpnAllowBypassSwitch.setOnCheckedChangeListener { buttonView, isChecked -> mProfile.mAllowAppVpnBypass = isChecked }
+
+ vpnAllowBypassSwitch.isChecked = mProfile.mAllowAppVpnBypass
+
+ mListView = v.findViewById<View>(android.R.id.list) as ListView
+
+ mListAdapter = PackageAdapter(activity, mProfile)
+ mListView.adapter = mListAdapter
+ mListView.onItemClickListener = this
+
+ mListView.emptyView = v.findViewById(R.id.loading_container)
+
+ Thread(Runnable { mListAdapter.populateList(activity) }).start()
+
+ return v
+ }
+
+ private fun changeDisallowText(selectedAreDisallowed: Boolean) {
+ if (selectedAreDisallowed)
+ mDefaultAllowTextView.setText(R.string.vpn_disallow_radio)
+ else
+ mDefaultAllowTextView.setText(R.string.vpn_allow_radio)
+ }
+
+ internal class AppViewHolder {
+ var mInfo: ApplicationInfo? = null
+ var rootView: View? = null
+ lateinit var appName: TextView
+ lateinit var appIcon: ImageView
+ //public TextView appSize;
+ //public TextView disabled;
+ lateinit var checkBox: CompoundButton
+
+ companion object {
+
+ fun createOrRecycle(inflater: LayoutInflater, oldview: View?, parent: ViewGroup): AppViewHolder {
+ var convertView = oldview
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.allowed_application_layout, parent, false)
+
+ // Creates a ViewHolder and store references to the two children views
+ // we want to bind data to.
+ val holder = AppViewHolder()
+ holder.rootView = convertView
+ holder.appName = convertView.findViewById<View>(R.id.app_name) as TextView
+ holder.appIcon = convertView.findViewById<View>(R.id.app_icon) as ImageView
+ //holder.appSize = (TextView) convertView.findViewById(R.id.app_size);
+ //holder.disabled = (TextView) convertView.findViewById(R.id.app_disabled);
+ holder.checkBox = convertView.findViewById<View>(R.id.app_selected) as CompoundButton
+ convertView.tag = holder
+
+
+ return holder
+ } else {
+ // Get the ViewHolder back to get fast access to the TextView
+ // and the ImageView.
+ return convertView.tag as AppViewHolder
+ }
+ }
+ }
+
+ }
+
+ internal inner class PackageAdapter(c: Context, vp: VpnProfile) : BaseAdapter(), Filterable {
+ private val mInflater: LayoutInflater = LayoutInflater.from(c)
+ private val mPm: PackageManager = c.packageManager
+ private var mPackages: Vector<ApplicationInfo> = Vector()
+ private val mFilter = ItemFilter()
+ private var mFilteredData: Vector<ApplicationInfo> = mPackages
+ private val mProfile = vp
+
+
+ fun populateList(c: Activity) {
+ val installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA)
+
+ // Remove apps not using Internet
+
+ var androidSystemUid = 0
+ val apps = Vector<ApplicationInfo>()
+
+ try {
+ val system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA)
+ androidSystemUid = system.uid
+ apps.add(system)
+ } catch (e: PackageManager.NameNotFoundException) {
+ }
+
+
+ for (app in installedPackages) {
+
+ if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && app.uid != androidSystemUid) {
+
+ apps.add(app)
+ }
+ }
+
+ Collections.sort(apps, ApplicationInfo.DisplayNameComparator(mPm))
+ mPackages = apps
+ mFilteredData = apps
+ c.runOnUiThread { notifyDataSetChanged() }
+ }
+
+ override fun getCount(): Int {
+ return mFilteredData.size + 1
+ }
+
+ override fun getItem(position: Int): Any {
+ return mFilteredData[position - 1]
+ }
+
+ override fun getItemId(position: Int): Long {
+ if (position == 0)
+ return "settings".hashCode().toLong()
+ return mFilteredData[position - 1].packageName.hashCode().toLong()
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
+ if (position == 0) {
+ return mSettingsView
+ } else
+ return getViewApp(position - 1, convertView, parent)
+
+ }
+
+ fun getViewApp(position: Int, convertView: View?, parent: ViewGroup): View? {
+ val viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent)
+ viewHolder.mInfo = mFilteredData[position]
+ val mInfo = mFilteredData[position]
+
+
+ var appName = mInfo.loadLabel(mPm)
+
+ if (TextUtils.isEmpty(appName))
+ appName = mInfo.packageName
+ viewHolder.appName.text = appName
+ viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm))
+ viewHolder.checkBox.tag = mInfo.packageName
+ viewHolder.checkBox.setOnCheckedChangeListener(this@Settings_Allowed_Apps)
+
+
+ viewHolder.checkBox.isChecked = mProfile.mAllowedAppsVpn.contains(mInfo.packageName)
+ return viewHolder.rootView
+ }
+
+ override fun getFilter(): Filter {
+ return mFilter
+ }
+
+ override fun getViewTypeCount(): Int {
+ return 2;
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == 0) 0 else 1
+ }
+
+ private inner class ItemFilter : Filter() {
+ override fun performFiltering(constraint: CharSequence): Filter.FilterResults {
+
+ val filterString = constraint.toString().toLowerCase(Locale.getDefault())
+
+ val results = Filter.FilterResults()
+
+
+ val count = mPackages.size
+ val nlist = Vector<ApplicationInfo>(count)
+
+ for (i in 0 until count) {
+ val pInfo = mPackages[i]
+ var appName = pInfo.loadLabel(mPm)
+
+ if (TextUtils.isEmpty(appName))
+ appName = pInfo.packageName
+
+ if (appName is String) {
+ if (appName.toLowerCase(Locale.getDefault()).contains(filterString))
+ nlist.add(pInfo)
+ } else {
+ if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString))
+ nlist.add(pInfo)
+ }
+ }
+ results.values = nlist
+ results.count = nlist.size
+
+ return results
+ }
+
+ override fun publishResults(constraint: CharSequence, results: Filter.FilterResults) {
+ mFilteredData = results.values as Vector<ApplicationInfo>
+ notifyDataSetChanged()
+ }
+
+ }
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java
new file mode 100644
index 00000000..8fd6aa98
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
+import android.util.Pair;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.views.RemoteCNPreference;
+import de.blinkt.openvpn.VpnProfile;
+
+import java.io.IOException;
+
+
+public class Settings_Authentication extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener, OnPreferenceClickListener {
+ private static final int SELECT_TLS_FILE_LEGACY_DIALOG = 23223232;
+ private static final int SELECT_TLS_FILE_KITKAT = SELECT_TLS_FILE_LEGACY_DIALOG +1;
+ private CheckBoxPreference mExpectTLSCert;
+ private CheckBoxPreference mCheckRemoteCN;
+ private RemoteCNPreference mRemoteCN;
+ private ListPreference mTLSAuthDirection;
+ private Preference mTLSAuthFile;
+ private SwitchPreference mUseTLSAuth;
+ private EditTextPreference mCipher;
+ private String mTlsAuthFileData;
+ private EditTextPreference mAuth;
+ private EditTextPreference mRemoteX509Name;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_authentification);
+
+ mExpectTLSCert = (CheckBoxPreference) findPreference("remoteServerTLS");
+ mCheckRemoteCN = (CheckBoxPreference) findPreference("checkRemoteCN");
+ mRemoteCN = (RemoteCNPreference) findPreference("remotecn");
+ mRemoteCN.setOnPreferenceChangeListener(this);
+
+ mRemoteX509Name = (EditTextPreference) findPreference("remotex509name");
+ mRemoteX509Name.setOnPreferenceChangeListener(this);
+
+ mUseTLSAuth = (SwitchPreference) findPreference("useTLSAuth" );
+ mTLSAuthFile = findPreference("tlsAuthFile");
+ mTLSAuthDirection = (ListPreference) findPreference("tls_direction");
+
+
+ mTLSAuthFile.setOnPreferenceClickListener(this);
+
+ mCipher =(EditTextPreference) findPreference("cipher");
+ mCipher.setOnPreferenceChangeListener(this);
+
+ mAuth =(EditTextPreference) findPreference("auth");
+ mAuth.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mExpectTLSCert.setChecked(mProfile.mExpectTLSCert);
+ mCheckRemoteCN.setChecked(mProfile.mCheckRemoteCN);
+ mRemoteCN.setDN(mProfile.mRemoteCN);
+ mRemoteCN.setAuthType(mProfile.mX509AuthType);
+ onPreferenceChange(mRemoteCN,
+ new Pair<Integer, String>(mProfile.mX509AuthType, mProfile.mRemoteCN));
+
+ mRemoteX509Name.setText(mProfile.mx509UsernameField);
+ onPreferenceChange(mRemoteX509Name, mProfile.mx509UsernameField);
+
+ mUseTLSAuth.setChecked(mProfile.mUseTLSAuth);
+ mTlsAuthFileData= mProfile.mTLSAuthFilename;
+ setTlsAuthSummary(mTlsAuthFileData);
+ mTLSAuthDirection.setValue(mProfile.mTLSAuthDirection);
+ mCipher.setText(mProfile.mCipher);
+ onPreferenceChange(mCipher, mProfile.mCipher);
+ mAuth.setText(mProfile.mAuth);
+ onPreferenceChange(mAuth, mProfile.mAuth);
+
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_STATICKEYS) {
+ mExpectTLSCert.setEnabled(false);
+ mCheckRemoteCN.setEnabled(false);
+ mUseTLSAuth.setChecked(true);
+ } else {
+ mExpectTLSCert.setEnabled(true);
+ mCheckRemoteCN.setEnabled(true);
+
+ }
+ }
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mExpectTLSCert=mExpectTLSCert.isChecked();
+ mProfile.mCheckRemoteCN=mCheckRemoteCN.isChecked();
+ mProfile.mRemoteCN=mRemoteCN.getCNText();
+ mProfile.mX509AuthType=mRemoteCN.getAuthtype();
+
+ mProfile.mUseTLSAuth = mUseTLSAuth.isChecked();
+ mProfile.mTLSAuthFilename = mTlsAuthFileData;
+ mProfile.mx509UsernameField = mRemoteX509Name.getText();
+
+ if(mTLSAuthDirection.getValue()==null)
+ mProfile.mTLSAuthDirection=null;
+ else
+ mProfile.mTLSAuthDirection = mTLSAuthDirection.getValue();
+
+ if(mCipher.getText()==null)
+ mProfile.mCipher=null;
+ else
+ mProfile.mCipher = mCipher.getText();
+
+ if(mAuth.getText()==null)
+ mProfile.mAuth = null;
+ else
+ mProfile.mAuth = mAuth.getText();
+
+ }
+
+
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(preference==mRemoteCN) {
+ @SuppressWarnings("unchecked")
+ int authtype = ((Pair<Integer, String>) newValue).first;
+ @SuppressWarnings("unchecked")
+ String dn = ((Pair<Integer, String>) newValue).second;
+
+ if ("".equals(dn)) {
+ if (mProfile.mConnections.length > 0) {
+ preference.setSummary(getX509String(VpnProfile.X509_VERIFY_TLSREMOTE_RDN, mProfile.mConnections[0].mServerName));
+ } else {
+ preference.setSummary(R.string.no_remote_defined);
+ }
+ } else {
+ preference.setSummary(getX509String(authtype, dn));
+ }
+
+ } else if (preference == mCipher || preference == mAuth) {
+ preference.setSummary((CharSequence) newValue);
+ } else if (preference == mRemoteX509Name) {
+ preference.setSummary(TextUtils.isEmpty((CharSequence) newValue) ? "CN (default)" : (CharSequence)newValue);
+ }
+ return true;
+ }
+ private CharSequence getX509String(int authtype, String dn) {
+ String ret ="";
+ switch (authtype) {
+ case VpnProfile.X509_VERIFY_TLSREMOTE:
+ case VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING:
+ ret+="tls-remote ";
+ break;
+
+ case VpnProfile.X509_VERIFY_TLSREMOTE_DN:
+ ret="dn: ";
+ break;
+
+ case VpnProfile.X509_VERIFY_TLSREMOTE_RDN:
+ ret="rdn: ";
+ break;
+
+ case VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX:
+ ret="rdn prefix: ";
+ break;
+ }
+ return ret + dn;
+ }
+
+ void startFileDialog() {
+ Intent startFC = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && ! Utils.alwaysUseOldFileChooser(getActivity())) {
+ startFC = Utils.getFilePickerIntent(getActivity(), Utils.FileType.TLS_AUTH_FILE);
+ startActivityForResult(startFC, SELECT_TLS_FILE_KITKAT);
+ }
+
+ if (startFC == null) {
+ startFC = new Intent(getActivity(), FileSelect.class);
+ startFC.putExtra(FileSelect.START_DATA, mTlsAuthFileData);
+ startFC.putExtra(FileSelect.WINDOW_TITLE, R.string.tls_auth_file);
+ startActivityForResult(startFC, SELECT_TLS_FILE_LEGACY_DIALOG);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startFileDialog();
+ return true;
+
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if(requestCode == SELECT_TLS_FILE_LEGACY_DIALOG && resultCode == Activity.RESULT_OK){
+ String result = data.getStringExtra(FileSelect.RESULT_DATA);
+ mTlsAuthFileData=result;
+ setTlsAuthSummary(result);
+ } else if (requestCode == SELECT_TLS_FILE_KITKAT && resultCode == Activity.RESULT_OK) {
+ try {
+ mTlsAuthFileData= Utils.getFilePickerResult(Utils.FileType.TLS_AUTH_FILE,data,getActivity());
+ setTlsAuthSummary(mTlsAuthFileData);
+ } catch (IOException e) {
+ VpnStatus.logException(e);
+ } catch (SecurityException se) {
+ VpnStatus.logException(se);
+ }
+ }
+ }
+
+ private void setTlsAuthSummary(String result) {
+ if(result==null)
+ result = getString(R.string.no_certificate);
+ if(result.startsWith(VpnProfile.INLINE_TAG))
+ mTLSAuthFile.setSummary(R.string.inline_file_data);
+ else if (result.startsWith(VpnProfile.DISPLAYNAME_TAG))
+ mTLSAuthFile.setSummary(getString(R.string.imported_from_file, VpnProfile.getDisplayName(result)));
+ else
+ mTLSAuthFile.setSummary(result);
+ }
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java
new file mode 100644
index 00000000..81da76fe
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+import android.widget.AdapterView.OnItemSelectedListener;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.R.id;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.views.FileSelectLayout;
+
+public class Settings_Basic extends KeyChainSettingsFragment implements OnItemSelectedListener, FileSelectLayout.FileSelectCallback {
+ private static final int CHOOSE_FILE_OFFSET = 1000;
+
+ private FileSelectLayout mClientCert;
+ private FileSelectLayout mCaCert;
+ private FileSelectLayout mClientKey;
+ private CheckBox mUseLzo;
+ private Spinner mType;
+ private FileSelectLayout mpkcs12;
+ private FileSelectLayout mCrlFile;
+ private TextView mPKCS12Password;
+ private EditText mUserName;
+ private EditText mPassword;
+ private View mView;
+ private EditText mProfileName;
+ private EditText mKeyPassword;
+
+ private SparseArray<FileSelectLayout> fileselects = new SparseArray<>();
+ private Spinner mAuthRetry;
+
+
+ private void addFileSelectLayout(FileSelectLayout fsl, Utils.FileType type) {
+ int i = fileselects.size() + CHOOSE_FILE_OFFSET;
+ fileselects.put(i, fsl);
+ fsl.setCaller(this, i, type);
+ }
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ }
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+
+ mView = inflater.inflate(R.layout.basic_settings, container, false);
+
+ mProfileName = mView.findViewById(id.profilename);
+ mClientCert = mView.findViewById(id.certselect);
+ mClientKey = mView.findViewById(id.keyselect);
+ mCaCert = mView.findViewById(id.caselect);
+ mpkcs12 = mView.findViewById(id.pkcs12select);
+ mCrlFile = mView.findViewById(id.crlfile);
+ mUseLzo = mView.findViewById(id.lzo);
+ mType = mView.findViewById(id.type);
+ mPKCS12Password = mView.findViewById(id.pkcs12password);
+
+ mUserName = mView.findViewById(id.auth_username);
+ mPassword = mView.findViewById(id.auth_password);
+ mKeyPassword = mView.findViewById(id.key_password);
+ mAuthRetry = mView.findViewById(id.auth_retry);
+
+ addFileSelectLayout(mCaCert, Utils.FileType.CA_CERTIFICATE);
+ addFileSelectLayout(mClientCert, Utils.FileType.CLIENT_CERTIFICATE);
+ addFileSelectLayout(mClientKey, Utils.FileType.KEYFILE);
+ addFileSelectLayout(mpkcs12, Utils.FileType.PKCS12);
+ addFileSelectLayout(mCrlFile, Utils.FileType.CRL_FILE);
+ mCaCert.setShowClear();
+ mCrlFile.setShowClear();
+
+ mType.setOnItemSelectedListener(this);
+ mAuthRetry.setOnItemSelectedListener(this);
+
+ initKeychainViews(mView);
+
+ return mView;
+ }
+
+
+ @Override
+ public void onActivityResult(int request, int result, Intent data) {
+ super.onActivityResult(request, result, data);
+ if (result == Activity.RESULT_OK && request >= CHOOSE_FILE_OFFSET) {
+ FileSelectLayout fsl = fileselects.get(request);
+ fsl.parseResponse(data, getActivity());
+
+ savePreferences();
+
+ // Private key files may result in showing/hiding the private key password dialog
+ if (fsl == mClientKey) {
+ changeType(mType.getSelectedItemPosition());
+ }
+ }
+
+ }
+
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (parent == mType) {
+ changeType(position);
+ }
+ }
+
+
+ private void changeType(int type) {
+ // hide everything
+ mView.findViewById(R.id.pkcs12).setVisibility(View.GONE);
+ mView.findViewById(R.id.certs).setVisibility(View.GONE);
+ mView.findViewById(R.id.statickeys).setVisibility(View.GONE);
+ mView.findViewById(R.id.keystore).setVisibility(View.GONE);
+ mView.findViewById(R.id.cacert).setVisibility(View.GONE);
+ ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(false);
+ mView.findViewById(R.id.userpassword).setVisibility(View.GONE);
+ mView.findViewById(R.id.key_password_layout).setVisibility(View.GONE);
+ mView.findViewById(R.id.external_auth).setVisibility(View.GONE);
+
+ // Fall through are by design
+ switch (type) {
+ case VpnProfile.TYPE_USERPASS_CERTIFICATES:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_CERTIFICATES:
+ mView.findViewById(R.id.certs).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ if (mProfile.requireTLSKeyPassword())
+ mView.findViewById(R.id.key_password_layout).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_PKCS12:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_PKCS12:
+ mView.findViewById(R.id.pkcs12).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(true);
+ break;
+
+ case VpnProfile.TYPE_STATICKEYS:
+ mView.findViewById(R.id.statickeys).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_KEYSTORE:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_KEYSTORE:
+ mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(true);
+ break;
+
+ case VpnProfile.TYPE_USERPASS:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ break;
+ case VpnProfile.TYPE_EXTERNAL_APP:
+ mView.findViewById(R.id.external_auth).setVisibility(View.VISIBLE);
+ break;
+ }
+
+
+ }
+
+ protected void loadPreferences() {
+ super.loadPreferences();
+ mProfileName.setText(mProfile.mName);
+ mClientCert.setData(mProfile.mClientCertFilename, getActivity());
+ mClientKey.setData(mProfile.mClientKeyFilename, getActivity());
+ mCaCert.setData(mProfile.mCaFilename, getActivity());
+ mCrlFile.setData(mProfile.mCrlFilename, getActivity());
+
+ mUseLzo.setChecked(mProfile.mUseLzo);
+ mType.setSelection(mProfile.mAuthenticationType);
+ mpkcs12.setData(mProfile.mPKCS12Filename, getActivity());
+ mPKCS12Password.setText(mProfile.mPKCS12Password);
+ mUserName.setText(mProfile.mUsername);
+ mPassword.setText(mProfile.mPassword);
+ mKeyPassword.setText(mProfile.mKeyPassword);
+ mAuthRetry.setSelection(mProfile.mAuthRetry);
+ }
+
+ protected void savePreferences() {
+ super.savePreferences();
+ mProfile.mName = mProfileName.getText().toString();
+ mProfile.mCaFilename = mCaCert.getData();
+ mProfile.mClientCertFilename = mClientCert.getData();
+ mProfile.mClientKeyFilename = mClientKey.getData();
+ mProfile.mCrlFilename = mCrlFile.getData();
+
+ mProfile.mUseLzo = mUseLzo.isChecked();
+ mProfile.mAuthenticationType = mType.getSelectedItemPosition();
+ mProfile.mPKCS12Filename = mpkcs12.getData();
+ mProfile.mPKCS12Password = mPKCS12Password.getText().toString();
+
+ mProfile.mPassword = mPassword.getText().toString();
+ mProfile.mUsername = mUserName.getText().toString();
+ mProfile.mKeyPassword = mKeyPassword.getText().toString();
+ mProfile.mAuthRetry = mAuthRetry.getSelectedItemPosition();
+
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ savePreferences();
+ if (mProfile != null) {
+ outState.putString(getActivity().getPackageName() + "profileUUID", mProfile.getUUID().toString());
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java
new file mode 100644
index 00000000..e41e6cb9
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Checkable;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+
+public class Settings_Connections extends Settings_Fragment implements View.OnClickListener {
+ private ConnectionsAdapter mConnectionsAdapter;
+ private TextView mWarning;
+ private Checkable mUseRandomRemote;
+ private RecyclerView mRecyclerView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ inflater.inflate(R.menu.connections, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.connections, container, false);
+
+ mWarning = (TextView) v.findViewById(R.id.noserver_active_warning);
+ mRecyclerView = (RecyclerView) v.findViewById(R.id.connection_recycler_view);
+
+ int dpwidth = (int) (container.getWidth()/getResources().getDisplayMetrics().density);
+ int columns = dpwidth/290;
+ columns = Math.max(1, columns);
+
+ mConnectionsAdapter = new ConnectionsAdapter(getActivity(), this, mProfile);
+
+ //mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL));
+ mRecyclerView.setHasFixedSize(true);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
+ mRecyclerView.setAdapter(mConnectionsAdapter);
+
+ ImageButton fab_button = (ImageButton) v.findViewById(R.id.add_new_remote);
+ if(fab_button!=null)
+ fab_button.setOnClickListener(this);
+
+ mUseRandomRemote = (Checkable) v.findViewById(R.id.remote_random);
+ mUseRandomRemote.setChecked(mProfile.mRemoteRandom);
+
+
+ mConnectionsAdapter.displayWarningIfNoneEnabled();
+
+ return v;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.add_new_remote) {
+ mConnectionsAdapter.addRemote();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId()==R.id.add_new_remote)
+ mConnectionsAdapter.addRemote();
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void savePreferences() {
+ mConnectionsAdapter.saveProfile();
+ mProfile.mRemoteRandom = mUseRandomRemote.isChecked();
+ }
+
+ public void setWarningVisible(int showWarning) {
+ mWarning.setVisibility(showWarning);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java
new file mode 100644
index 00000000..738bd0e9
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012-2015 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.os.Bundle;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+public abstract class Settings_Fragment extends Fragment {
+
+ protected VpnProfile mProfile;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String profileUuid = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile= ProfileManager.get(getActivity(), profileUuid);
+ getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+ }
+
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ savePreferences();
+ }
+
+ protected abstract void savePreferences();
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java
new file mode 100644
index 00000000..daf407b8
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceManager;
+import android.preference.SwitchPreference;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+
+public class Settings_IP extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private EditTextPreference mIPv4;
+ private EditTextPreference mIPv6;
+ private SwitchPreference mUsePull;
+ private CheckBoxPreference mOverrideDNS;
+ private EditTextPreference mSearchdomain;
+ private EditTextPreference mDNS1;
+ private EditTextPreference mDNS2;
+ private CheckBoxPreference mNobind;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ // Make sure default values are applied. In a real app, you would
+ // want this in a shared function that is used to retrieve the
+ // SharedPreferences wherever they are needed.
+ PreferenceManager.setDefaultValues(getActivity(),
+ R.xml.vpn_ipsettings, false);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_ipsettings);
+ mIPv4 = (EditTextPreference) findPreference("ipv4_address");
+ mIPv6 = (EditTextPreference) findPreference("ipv6_address");
+ mUsePull = (SwitchPreference) findPreference("usePull");
+ mOverrideDNS = (CheckBoxPreference) findPreference("overrideDNS");
+ mSearchdomain =(EditTextPreference) findPreference("searchdomain");
+ mDNS1 = (EditTextPreference) findPreference("dns1");
+ mDNS2 = (EditTextPreference) findPreference("dns2");
+ mNobind = (CheckBoxPreference) findPreference("nobind");
+
+ mIPv4.setOnPreferenceChangeListener(this);
+ mIPv6.setOnPreferenceChangeListener(this);
+ mDNS1.setOnPreferenceChangeListener(this);
+ mDNS2.setOnPreferenceChangeListener(this);
+ mUsePull.setOnPreferenceChangeListener(this);
+ mOverrideDNS.setOnPreferenceChangeListener(this);
+ mSearchdomain.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mUsePull.setChecked(mProfile.mUsePull);
+ mIPv4.setText(mProfile.mIPv4Address);
+ mIPv6.setText(mProfile.mIPv6Address);
+ mDNS1.setText(mProfile.mDNS1);
+ mDNS2.setText(mProfile.mDNS2);
+ mOverrideDNS.setChecked(mProfile.mOverrideDNS);
+ mSearchdomain.setText(mProfile.mSearchDomain);
+ mNobind.setChecked(mProfile.mNobind);
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_STATICKEYS)
+ mUsePull.setChecked(false);
+
+ mUsePull.setEnabled(mProfile.mAuthenticationType != VpnProfile.TYPE_STATICKEYS);
+
+ // Sets Summary
+ onPreferenceChange(mIPv4, mIPv4.getText());
+ onPreferenceChange(mIPv6, mIPv6.getText());
+ onPreferenceChange(mDNS1, mDNS1.getText());
+ onPreferenceChange(mDNS2, mDNS2.getText());
+ onPreferenceChange(mSearchdomain, mSearchdomain.getText());
+
+ setDNSState();
+ }
+
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mUsePull = mUsePull.isChecked();
+ mProfile.mIPv4Address = mIPv4.getText();
+ mProfile.mIPv6Address = mIPv6.getText();
+ mProfile.mDNS1 = mDNS1.getText();
+ mProfile.mDNS2 = mDNS2.getText();
+ mProfile.mOverrideDNS = mOverrideDNS.isChecked();
+ mProfile.mSearchDomain = mSearchdomain.getText();
+ mProfile.mNobind = mNobind.isChecked();
+
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference,
+ Object newValue) {
+ if(preference==mIPv4 || preference == mIPv6
+ || preference==mDNS1 || preference == mDNS2
+ || preference == mSearchdomain
+ )
+
+ preference.setSummary((String)newValue);
+
+ if(preference== mUsePull || preference == mOverrideDNS)
+ if(preference==mOverrideDNS) {
+ // Set so the function gets the right value
+ mOverrideDNS.setChecked((Boolean) newValue);
+ }
+ setDNSState();
+
+ saveSettings();
+ return true;
+ }
+
+ private void setDNSState() {
+ boolean enabled;
+ mOverrideDNS.setEnabled(mUsePull.isChecked());
+ if(!mUsePull.isChecked())
+ enabled =true;
+ else
+ enabled = mOverrideDNS.isChecked();
+
+ mDNS1.setEnabled(enabled);
+ mDNS2.setEnabled(enabled);
+ mSearchdomain.setEnabled(enabled);
+
+
+ }
+
+
+ } \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java
new file mode 100644
index 00000000..6674599d
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.widget.Toast;
+
+import java.util.Locale;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+
+public class Settings_Obscure extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private CheckBoxPreference mUseRandomHostName;
+ private CheckBoxPreference mUseFloat;
+ private CheckBoxPreference mUseCustomConfig;
+ private EditTextPreference mCustomConfig;
+ private EditTextPreference mMssFixValue;
+ private CheckBoxPreference mMssFixCheckBox;
+ private CheckBoxPreference mPeerInfo;
+
+ private CheckBoxPreference mPersistent;
+ private ListPreference mConnectRetrymax;
+ private EditTextPreference mConnectRetry;
+ private EditTextPreference mConnectRetryMaxTime;
+ private EditTextPreference mTunMtu;
+
+ public void onCreateBehaviour(Bundle savedInstanceState) {
+
+ mPersistent = (CheckBoxPreference) findPreference("usePersistTun");
+ mConnectRetrymax = (ListPreference) findPreference("connectretrymax");
+ mConnectRetry = (EditTextPreference) findPreference("connectretry");
+ mConnectRetryMaxTime = (EditTextPreference) findPreference("connectretrymaxtime");
+
+ mPeerInfo = (CheckBoxPreference) findPreference("peerInfo");
+
+ mConnectRetrymax.setOnPreferenceChangeListener(this);
+ mConnectRetrymax.setSummary("%s");
+
+ mConnectRetry.setOnPreferenceChangeListener(this);
+ mConnectRetryMaxTime.setOnPreferenceChangeListener(this);
+
+
+
+ }
+
+ protected void loadSettingsBehaviour() {
+ mPersistent.setChecked(mProfile.mPersistTun);
+ mPeerInfo.setChecked(mProfile.mPushPeerInfo);
+
+ mConnectRetrymax.setValue(mProfile.mConnectRetryMax);
+ onPreferenceChange(mConnectRetrymax, mProfile.mConnectRetryMax);
+
+ mConnectRetry.setText(mProfile.mConnectRetry);
+ onPreferenceChange(mConnectRetry, mProfile.mConnectRetry);
+
+ mConnectRetryMaxTime.setText(mProfile.mConnectRetryMaxTime);
+ onPreferenceChange(mConnectRetryMaxTime, mProfile.mConnectRetryMaxTime);
+
+ }
+
+
+ protected void saveSettingsBehaviour() {
+ mProfile.mConnectRetryMax = mConnectRetrymax.getValue();
+ mProfile.mPersistTun = mPersistent.isChecked();
+ mProfile.mConnectRetry = mConnectRetry.getText();
+ mProfile.mPushPeerInfo = mPeerInfo.isChecked();
+ mProfile.mConnectRetryMaxTime = mConnectRetryMaxTime.getText();
+ }
+
+
+ public boolean onPreferenceChangeBehaviour(Preference preference, Object newValue) {
+ if (preference == mConnectRetrymax) {
+ if(newValue==null) {
+ newValue="5";
+ }
+ mConnectRetrymax.setDefaultValue(newValue);
+
+ for(int i=0;i< mConnectRetrymax.getEntryValues().length;i++){
+ if(mConnectRetrymax.getEntryValues().equals(newValue))
+ mConnectRetrymax.setSummary(mConnectRetrymax.getEntries()[i]);
+ }
+
+ } else if (preference == mConnectRetry) {
+ if(newValue==null || newValue=="")
+ newValue="2";
+ mConnectRetry.setSummary(String.format("%s s", newValue));
+ } else if (preference == mConnectRetryMaxTime) {
+ if(newValue==null || newValue=="")
+ newValue="300";
+ mConnectRetryMaxTime.setSummary(String.format("%s s", newValue));
+ }
+
+
+ return true;
+ }
+
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_obscure);
+
+ mUseRandomHostName = (CheckBoxPreference) findPreference("useRandomHostname");
+ mUseFloat = (CheckBoxPreference) findPreference("useFloat");
+ mUseCustomConfig = (CheckBoxPreference) findPreference("enableCustomOptions");
+ mCustomConfig = (EditTextPreference) findPreference("customOptions");
+ mMssFixCheckBox = (CheckBoxPreference) findPreference("mssFix");
+ mMssFixValue = (EditTextPreference) findPreference("mssFixValue");
+ mMssFixValue.setOnPreferenceChangeListener(this);
+ mTunMtu = (EditTextPreference) findPreference("tunmtu");
+ mTunMtu.setOnPreferenceChangeListener(this);;
+
+ onCreateBehaviour(savedInstanceState);
+ loadSettings();
+
+ }
+
+ protected void loadSettings() {
+ mUseRandomHostName.setChecked(mProfile.mUseRandomHostname);
+ mUseFloat.setChecked(mProfile.mUseFloat);
+ mUseCustomConfig.setChecked(mProfile.mUseCustomConfig);
+ mCustomConfig.setText(mProfile.mCustomConfigOptions);
+
+ if (mProfile.mMssFix == 0) {
+ mMssFixValue.setText(String.valueOf(VpnProfile.DEFAULT_MSSFIX_SIZE));
+ mMssFixCheckBox.setChecked(false);
+ setMssSummary(VpnProfile.DEFAULT_MSSFIX_SIZE);
+ } else {
+ mMssFixValue.setText(String.valueOf(mProfile.mMssFix));
+ mMssFixCheckBox.setChecked(true);
+ setMssSummary(mProfile.mMssFix);
+ }
+
+
+ int tunmtu = mProfile.mTunMtu;
+ if (mProfile.mTunMtu < 48)
+ tunmtu = 1500;
+
+ mTunMtu.setText(String.valueOf(tunmtu));
+ setMtuSummary(tunmtu);
+
+
+ loadSettingsBehaviour();
+
+ }
+
+ private void setMssSummary(int value) {
+ mMssFixValue.setSummary(String.format(Locale.getDefault(),"Configured MSS value: %d", value));
+ }
+
+ private void setMtuSummary(int value) {
+ if (value == 1500)
+ mTunMtu.setSummary(String.format(Locale.getDefault(),"Using default (1500) MTU", value));
+ else
+ mTunMtu.setSummary(String.format(Locale.getDefault(),"Configured MTU value: %d", value));
+ }
+
+ protected void saveSettings() {
+ mProfile.mUseRandomHostname = mUseRandomHostName.isChecked();
+ mProfile.mUseFloat = mUseFloat.isChecked();
+ mProfile.mUseCustomConfig = mUseCustomConfig.isChecked();
+ mProfile.mCustomConfigOptions = mCustomConfig.getText();
+ if (mMssFixCheckBox.isChecked())
+ mProfile.mMssFix=Integer.parseInt(mMssFixValue.getText());
+ else
+ mProfile.mMssFix=0;
+
+ mProfile.mTunMtu = Integer.parseInt(mTunMtu.getText());
+ saveSettingsBehaviour();
+ }
+
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference.getKey().equals("mssFixValue"))
+ try {
+ int v = Integer.parseInt((String) newValue);
+ if (v < 0 || v > 9000)
+ throw new NumberFormatException("mssfix value");
+ setMssSummary(v);
+
+ } catch(NumberFormatException e) {
+ Toast.makeText(getActivity(), R.string.mssfix_invalid_value, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ else if (preference.getKey().equals("tunmtu"))
+ try {
+ int v = Integer.parseInt((String) newValue);
+ if (v < 48 || v > 9000)
+ throw new NumberFormatException("mtu value");
+ setMtuSummary(v);
+
+ } catch(NumberFormatException e) {
+ Toast.makeText(getActivity(), R.string.mtu_invalid_value, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ return onPreferenceChangeBehaviour(preference, newValue);
+
+ }
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
new file mode 100644
index 00000000..53f88bbf
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import de.blinkt.openvpn.R;
+
+
+public class Settings_Routing extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private EditTextPreference mCustomRoutes;
+ private CheckBoxPreference mUseDefaultRoute;
+ private EditTextPreference mCustomRoutesv6;
+ private CheckBoxPreference mUseDefaultRoutev6;
+ private CheckBoxPreference mRouteNoPull;
+ private CheckBoxPreference mLocalVPNAccess;
+ private EditTextPreference mExcludedRoutes;
+ private EditTextPreference mExcludedRoutesv6;
+ private CheckBoxPreference mBlockUnusedAF;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_routing);
+ mCustomRoutes = (EditTextPreference) findPreference("customRoutes");
+ mUseDefaultRoute = (CheckBoxPreference) findPreference("useDefaultRoute");
+ mCustomRoutesv6 = (EditTextPreference) findPreference("customRoutesv6");
+ mUseDefaultRoutev6 = (CheckBoxPreference) findPreference("useDefaultRoutev6");
+ mExcludedRoutes = (EditTextPreference) findPreference("excludedRoutes");
+ mExcludedRoutesv6 = (EditTextPreference) findPreference("excludedRoutesv6");
+
+ mRouteNoPull = (CheckBoxPreference) findPreference("routenopull");
+ mLocalVPNAccess = (CheckBoxPreference) findPreference("unblockLocal");
+
+ mBlockUnusedAF = (CheckBoxPreference) findPreference("blockUnusedAF");
+
+ mCustomRoutes.setOnPreferenceChangeListener(this);
+ mCustomRoutesv6.setOnPreferenceChangeListener(this);
+ mExcludedRoutes.setOnPreferenceChangeListener(this);
+ mExcludedRoutesv6.setOnPreferenceChangeListener(this);
+ mBlockUnusedAF.setOnPreferenceChangeListener(this);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ getPreferenceScreen().removePreference(mBlockUnusedAF);
+
+ loadSettings();
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mUseDefaultRoute.setChecked(mProfile.mUseDefaultRoute);
+ mUseDefaultRoutev6.setChecked(mProfile.mUseDefaultRoutev6);
+
+ mCustomRoutes.setText(mProfile.mCustomRoutes);
+ mCustomRoutesv6.setText(mProfile.mCustomRoutesv6);
+
+ mExcludedRoutes.setText(mProfile.mExcludedRoutes);
+ mExcludedRoutesv6.setText(mProfile.mExcludedRoutesv6);
+
+ mRouteNoPull.setChecked(mProfile.mRoutenopull);
+ mLocalVPNAccess.setChecked(mProfile.mAllowLocalLAN);
+
+ mBlockUnusedAF.setChecked(mProfile.mBlockUnusedAddressFamilies);
+
+ // Sets Summary
+ onPreferenceChange(mCustomRoutes, mCustomRoutes.getText());
+ onPreferenceChange(mCustomRoutesv6, mCustomRoutesv6.getText());
+ onPreferenceChange(mExcludedRoutes, mExcludedRoutes.getText());
+ onPreferenceChange(mExcludedRoutesv6, mExcludedRoutesv6.getText());
+
+ mRouteNoPull.setEnabled(mProfile.mUsePull);
+ }
+
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mUseDefaultRoute = mUseDefaultRoute.isChecked();
+ mProfile.mUseDefaultRoutev6 = mUseDefaultRoutev6.isChecked();
+ mProfile.mCustomRoutes = mCustomRoutes.getText();
+ mProfile.mCustomRoutesv6 = mCustomRoutesv6.getText();
+ mProfile.mRoutenopull = mRouteNoPull.isChecked();
+ mProfile.mAllowLocalLAN =mLocalVPNAccess.isChecked();
+ mProfile.mExcludedRoutes = mExcludedRoutes.getText();
+ mProfile.mExcludedRoutesv6 = mExcludedRoutesv6.getText();
+ mProfile.mBlockUnusedAddressFamilies = mBlockUnusedAF.isChecked();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference,
+ Object newValue) {
+ if( preference == mCustomRoutes || preference == mCustomRoutesv6
+ || preference == mExcludedRoutes || preference == mExcludedRoutesv6)
+ preference.setSummary((String)newValue);
+
+ saveSettings();
+ return true;
+ }
+
+
+} \ No newline at end of file
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java
new file mode 100644
index 00000000..98ebb55b
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2012-2015 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.api.AppRestrictions;
+
+public class Settings_UserEditable extends KeyChainSettingsFragment implements View.OnClickListener {
+
+ private View mView;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mView = inflater.inflate(R.layout.settings_usereditable, container, false);
+ TextView messageView = (TextView) mView.findViewById(R.id.messageUserEdit);
+ messageView.setText(getString(R.string.message_no_user_edit, getPackageString(mProfile.mProfileCreator)));
+ initKeychainViews(this.mView);
+ return mView;
+ }
+
+
+ private String getPackageString(String packageName) {
+
+ if (AppRestrictions.PROFILE_CREATOR.equals(packageName))
+ return "Android Enterprise Management";
+
+ final PackageManager pm = getActivity().getPackageManager();
+ ApplicationInfo ai;
+ try {
+ ai = pm.getApplicationInfo(packageName, 0);
+ } catch (final PackageManager.NameNotFoundException e) {
+ ai = null;
+ }
+ final String applicationName = (String) (ai != null ? pm.getApplicationLabel(ai) : "(unknown)");
+ return String.format("%s (%s)", applicationName, packageName);
+ }
+
+ @Override
+ protected void savePreferences() {
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mView.findViewById(R.id.keystore).setVisibility(View.GONE);
+ if (mProfile.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE ||
+ mProfile.mAuthenticationType == VpnProfile.TYPE_KEYSTORE)
+ mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE);
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java
new file mode 100644
index 00000000..f5c1750a
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ProfileManager;
+
+
+public class ShowConfigFragment extends Fragment {
+ private String configtext;
+ private TextView mConfigView;
+ private ImageButton mfabButton;
+
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ {
+ View v=inflater.inflate(R.layout.viewconfig, container,false);
+ mConfigView = (TextView) v.findViewById(R.id.configview);
+
+
+ mfabButton = (ImageButton) v.findViewById(R.id.share_config);
+ if (mfabButton!=null) {
+ mfabButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ shareConfig();
+ }
+ });
+ mfabButton.setVisibility(View.INVISIBLE);
+ }
+ return v;
+ }
+
+ private void startGenConfig(final VpnProfile vp, final TextView cv) {
+
+ new Thread() {
+ public void run() {
+ /* Add a few newlines to make the textview scrollable past the FAB */
+ try {
+
+ configtext = vp.getConfigFile(getActivity(), VpnProfile.doUseOpenVPN3(getActivity())) + "\n\n\n";
+ } catch (Exception e) {
+ e.printStackTrace();
+ configtext = "Error generating config file: " + e.getLocalizedMessage();
+ }
+ getActivity().runOnUiThread(() -> {
+ cv.setText(configtext);
+ if (mfabButton!=null)
+ mfabButton.setVisibility(View.VISIBLE);
+ });
+
+
+ }
+ }.start();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ inflater.inflate(R.menu.configmenu, menu);
+ }
+
+ private void shareConfig() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, configtext);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.export_config_title));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.export_config_chooser_title)));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.sendConfig) {
+ shareConfig();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ populateConfigText();
+ }
+
+ private void populateConfigText() {
+ String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ final VpnProfile vp = ProfileManager.get(getActivity(),profileUUID);
+ int check=vp.checkProfile(getActivity());
+
+ if(check != R.string.no_error_found) {
+ mConfigView.setText(check);
+ configtext = getString(check);
+ }
+ else {
+ // Run in own Thread since Keystore does not like to be queried from the main thread
+
+ mConfigView.setText("Generating config...");
+ startGenConfig(vp, mConfigView);
+ }
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean visible)
+ {
+ super.setUserVisibleHint(visible);
+ if (visible && isResumed())
+ populateConfigText();
+ }
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java
new file mode 100644
index 00000000..abdc45f5
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.OpenableColumns;
+import android.util.Base64;
+import android.webkit.MimeTypeMap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.Vector;
+
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.Preferences;
+
+public class Utils {
+
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public static Intent getFilePickerIntent(Context c, FileType fileType) {
+ Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ i.addCategory(Intent.CATEGORY_OPENABLE);
+ TreeSet<String> supportedMimeTypes = new TreeSet<String>();
+ Vector<String> extensions = new Vector<String>();
+
+ switch (fileType) {
+ case PKCS12:
+ i.setType("application/x-pkcs12");
+ supportedMimeTypes.add("application/x-pkcs12");
+ extensions.add("p12");
+ extensions.add("pfx");
+ break;
+ case CLIENT_CERTIFICATE:
+ case CA_CERTIFICATE:
+ i.setType("application/x-pem-file");
+ supportedMimeTypes.add("application/x-x509-ca-cert");
+ supportedMimeTypes.add("application/x-x509-user-cert");
+ supportedMimeTypes.add("application/x-pem-file");
+ supportedMimeTypes.add("application/pkix-cert");
+ supportedMimeTypes.add("text/plain");
+
+ extensions.add("pem");
+ extensions.add("crt");
+ extensions.add("cer");
+ break;
+ case KEYFILE:
+ i.setType("application/x-pem-file");
+ supportedMimeTypes.add("application/x-pem-file");
+ supportedMimeTypes.add("application/pkcs8");
+
+ // Google drive ....
+ supportedMimeTypes.add("application/x-iwork-keynote-sffkey");
+ extensions.add("key");
+ break;
+
+ case TLS_AUTH_FILE:
+ i.setType("text/plain");
+
+ // Backup ....
+ supportedMimeTypes.add("application/pkcs8");
+ // Google Drive is kind of crazy .....
+ supportedMimeTypes.add("application/x-iwork-keynote-sffkey");
+
+ extensions.add("txt");
+ extensions.add("key");
+ break;
+
+ case OVPN_CONFIG:
+ i.setType("application/x-openvpn-profile");
+ supportedMimeTypes.add("application/x-openvpn-profile");
+ supportedMimeTypes.add("application/openvpn-profile");
+ supportedMimeTypes.add("application/ovpn");
+ supportedMimeTypes.add("text/plain");
+ extensions.add("ovpn");
+ extensions.add("conf");
+ break;
+
+ case CRL_FILE:
+ supportedMimeTypes.add("application/x-pkcs7-crl");
+ supportedMimeTypes.add("application/pkix-crl");
+ extensions.add("crl");
+ break;
+
+ case USERPW_FILE:
+ i.setType("text/plain");
+ supportedMimeTypes.add("text/plain");
+ break;
+ }
+
+ MimeTypeMap mtm = MimeTypeMap.getSingleton();
+
+ for (String ext : extensions) {
+ String mimeType = mtm.getMimeTypeFromExtension(ext);
+ if (mimeType != null)
+ supportedMimeTypes.add(mimeType);
+ }
+
+ // Always add this as fallback
+ supportedMimeTypes.add("application/octet-stream");
+
+ i.putExtra(Intent.EXTRA_MIME_TYPES, supportedMimeTypes.toArray(new String[supportedMimeTypes.size()]));
+
+ // People don't know that this is actually a system setting. Override it ...
+ // DocumentsContract.EXTRA_SHOW_ADVANCED is hidden
+ i.putExtra("android.content.extra.SHOW_ADVANCED", true);
+
+ /* Samsung has decided to do something strange, on stock Android GET_CONTENT opens the document UI */
+ /* fist try with documentsui */
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N)
+ i.setPackage("com.android.documentsui");
+
+
+
+
+ //noinspection ConstantConditions
+ if (!isIntentAvailable(c,i)) {
+ i.setAction(Intent.ACTION_OPEN_DOCUMENT);
+ i.setPackage(null);
+
+ // Check for really broken devices ... :(
+ if (!isIntentAvailable(c,i)) {
+ return null;
+ }
+ }
+
+
+ /*
+ final PackageManager packageManager = c.getPackageManager();
+ ResolveInfo list = packageManager.resolveActivity(i, 0);
+
+ Toast.makeText(c, "Starting package: "+ list.activityInfo.packageName
+ + "with ACTION " + i.getAction(), Toast.LENGTH_LONG).show();
+
+ */
+ return i;
+ }
+
+ public static boolean alwaysUseOldFileChooser(Context c)
+ {
+ /* Android P does not allow access to the file storage anymore */
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
+ return false;
+
+ SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c);
+ return prefs.getBoolean("useInternalFileSelector", false);
+ }
+
+ public static boolean isIntentAvailable(Context context, Intent i) {
+ final PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> list =
+ packageManager.queryIntentActivities(i,
+ PackageManager.MATCH_DEFAULT_ONLY);
+
+ // Ignore the Android TV framework app in the list
+ int size = list.size();
+ for (ResolveInfo ri: list)
+ {
+ // Ignore stub apps
+ if ("com.google.android.tv.frameworkpackagestubs".equals(ri.activityInfo.packageName))
+ {
+ size--;
+ }
+ }
+
+ return size > 0;
+ }
+
+
+ public enum FileType {
+ PKCS12(0),
+ CLIENT_CERTIFICATE(1),
+ CA_CERTIFICATE(2),
+ OVPN_CONFIG(3),
+ KEYFILE(4),
+ TLS_AUTH_FILE(5),
+ USERPW_FILE(6),
+ CRL_FILE(7);
+
+ private int value;
+
+ FileType(int i) {
+ value = i;
+ }
+
+ public static FileType getFileTypeByValue(int value) {
+ switch (value) {
+ case 0:
+ return PKCS12;
+ case 1:
+ return CLIENT_CERTIFICATE;
+ case 2:
+ return CA_CERTIFICATE;
+ case 3:
+ return OVPN_CONFIG;
+ case 4:
+ return KEYFILE;
+ case 5:
+ return TLS_AUTH_FILE;
+ case 6:
+ return USERPW_FILE;
+ case 7:
+ return CRL_FILE;
+ default:
+ return null;
+ }
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ static private byte[] readBytesFromStream(InputStream input) throws IOException {
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ int nRead;
+ byte[] data = new byte[16384];
+
+ ;
+
+ long totalread = 0;
+ while ((nRead = input.read(data, 0, data.length)) != -1 && totalread <VpnProfile.MAX_EMBED_FILE_SIZE ) {
+ buffer.write(data, 0, nRead);
+ totalread+=nRead;
+ }
+
+ buffer.flush();
+ input.close();
+ return buffer.toByteArray();
+ }
+
+ public static String getFilePickerResult(FileType ft, Intent result, Context c) throws IOException, SecurityException {
+
+ Uri uri = result.getData();
+ if (uri == null)
+ return null;
+
+ byte[] fileData = readBytesFromStream(c.getContentResolver().openInputStream(uri));
+ String newData = null;
+
+ Cursor cursor = c.getContentResolver().query(uri, null, null, null, null);
+
+ String prefix = "";
+ try {
+ if (cursor!=null && cursor.moveToFirst()) {
+ int cidx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (cidx != -1) {
+ String displayName = cursor.getString(cidx);
+
+ if (!displayName.contains(VpnProfile.INLINE_TAG) && !displayName.contains(VpnProfile.DISPLAYNAME_TAG))
+ prefix = VpnProfile.DISPLAYNAME_TAG + displayName;
+ }
+ }
+ } finally {
+ if(cursor!=null)
+ cursor.close();
+ }
+
+ switch (ft) {
+ case PKCS12:
+ newData = Base64.encodeToString(fileData, Base64.DEFAULT);
+ break;
+ default:
+ newData = new String(fileData, "UTF-8");
+ break;
+ }
+
+ return prefix + VpnProfile.INLINE_TAG + newData;
+ }
+
+}
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java
new file mode 100644
index 00000000..7ad13aaf
--- /dev/null
+++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java
@@ -0,0 +1,636 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.fragments;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.annotation.RequiresApi;
+import android.text.Html;
+import android.text.Html.ImageGetter;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TreeSet;
+
+import de.blinkt.openvpn.LaunchVPN;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.ConfigConverter;
+import de.blinkt.openvpn.activities.DisconnectVPN;
+import de.blinkt.openvpn.activities.FileSelect;
+import de.blinkt.openvpn.activities.VPNPreferences;
+import de.blinkt.openvpn.core.ConnectionStatus;
+import de.blinkt.openvpn.core.Preferences;
+import de.blinkt.openvpn.core.ProfileManager;
+import de.blinkt.openvpn.core.VpnStatus;
+
+import static de.blinkt.openvpn.core.OpenVPNService.DISCONNECT_VPN;
+
+
+public class VPNProfileList extends ListFragment implements OnClickListener, VpnStatus.StateListener {
+
+ public final static int RESULT_VPN_DELETED = Activity.RESULT_FIRST_USER;
+ public final static int RESULT_VPN_DUPLICATE = Activity.RESULT_FIRST_USER + 1;
+
+ private static final int MENU_ADD_PROFILE = Menu.FIRST;
+
+ private static final int START_VPN_CONFIG = 92;
+ private static final int SELECT_PROFILE = 43;
+ private static final int IMPORT_PROFILE = 231;
+ private static final int FILE_PICKER_RESULT_KITKAT = 392;
+
+ private static final int MENU_IMPORT_PROFILE = Menu.FIRST + 1;
+ private static final int MENU_CHANGE_SORTING = Menu.FIRST + 2;
+ private static final String PREF_SORT_BY_LRU = "sortProfilesByLRU";
+ private String mLastStatusMessage;
+
+ @Override
+ public void updateState(String state, String logmessage, final int localizedResId, ConnectionStatus level) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity());
+ mArrayadapter.notifyDataSetChanged();
+ }
+ });
+ }
+
+ @Override
+ public void setConnectedVPN(String uuid) {
+ }
+
+ private class VPNArrayAdapter extends ArrayAdapter<VpnProfile> {
+
+ public VPNArrayAdapter(Context context, int resource,
+ int textViewResourceId) {
+ super(context, resource, textViewResourceId);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v = super.getView(position, convertView, parent);
+
+ final VpnProfile profile = (VpnProfile) getListAdapter().getItem(position);
+
+ View titleview = v.findViewById(R.id.vpn_list_item_left);
+ titleview.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startOrStopVPN(profile);
+ }
+ });
+
+ View settingsview = v.findViewById(R.id.quickedit_settings);
+ settingsview.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ editVPN(profile);
+
+ }
+ });
+
+ TextView subtitle = (TextView) v.findViewById(R.id.vpn_item_subtitle);
+ if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) {
+ subtitle.setText(mLastStatusMessage);
+ subtitle.setVisibility(View.VISIBLE);
+ } else {
+ subtitle.setText("");
+ subtitle.setVisibility(View.GONE);
+ }
+
+
+ return v;
+ }
+ }
+
+ private void startOrStopVPN(VpnProfile profile) {
+ if (VpnStatus.isVPNActive() && profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) {
+ Intent disconnectVPN = new Intent(getActivity(), DisconnectVPN.class);
+ startActivity(disconnectVPN);
+ } else {
+ startVPN(profile);
+ }
+ }
+
+
+ private ArrayAdapter<VpnProfile> mArrayadapter;
+
+ protected VpnProfile mEditProfile = null;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+
+ // Shortcut version is increased to refresh all shortcuts
+ final static int SHORTCUT_VERSION = 1;
+
+ @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+ void updateDynamicShortcuts() {
+ PersistableBundle versionExtras = new PersistableBundle();
+ versionExtras.putInt("version", SHORTCUT_VERSION);
+
+ ShortcutManager shortcutManager = getContext().getSystemService(ShortcutManager.class);
+ if (shortcutManager.isRateLimitingActive())
+ return;
+
+ List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
+ int maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1;
+
+
+ ShortcutInfo disconnectShortcut = new ShortcutInfo.Builder(getContext(), "disconnectVPN")
+ .setShortLabel("Disconnect")
+ .setLongLabel("Disconnect VPN")
+ .setIntent(new Intent(getContext(), DisconnectVPN.class).setAction(DISCONNECT_VPN))
+ .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel))
+ .setExtras(versionExtras)
+ .build();
+
+ LinkedList<ShortcutInfo> newShortcuts = new LinkedList<>();
+ LinkedList<ShortcutInfo> updateShortcuts = new LinkedList<>();
+
+ LinkedList<String> removeShortcuts = new LinkedList<>();
+ LinkedList<String> disableShortcuts = new LinkedList<>();
+
+ boolean addDisconnect = true;
+
+
+ TreeSet<VpnProfile> sortedProfilesLRU = new TreeSet<VpnProfile>(new VpnProfileLRUComparator());
+ ProfileManager profileManager = ProfileManager.getInstance(getContext());
+ sortedProfilesLRU.addAll(profileManager.getProfiles());
+
+ LinkedList<VpnProfile> LRUProfiles = new LinkedList<>();
+ maxvpn = Math.min(maxvpn, sortedProfilesLRU.size());
+
+ for (int i = 0; i < maxvpn; i++) {
+ LRUProfiles.add(sortedProfilesLRU.pollFirst());
+ }
+
+ for (ShortcutInfo shortcut : shortcuts) {
+ if (shortcut.getId().equals("disconnectVPN")) {
+ addDisconnect = false;
+ if (shortcut.getExtras() == null
+ || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION)
+ updateShortcuts.add(disconnectShortcut);
+
+ } else {
+ VpnProfile p = ProfileManager.get(getContext(), shortcut.getId());
+ if (p == null || p.profileDeleted) {
+ if (shortcut.isEnabled()) {
+ disableShortcuts.add(shortcut.getId());
+ removeShortcuts.add(shortcut.getId());
+ }
+ if (!shortcut.isPinned())
+ removeShortcuts.add(shortcut.getId());
+ } else {
+
+ if (LRUProfiles.contains(p))
+ LRUProfiles.remove(p);
+ else
+ removeShortcuts.add(p.getUUIDString());
+
+ if (!p.getName().equals(shortcut.getShortLabel())
+ || shortcut.getExtras() == null
+ || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION)
+ updateShortcuts.add(createShortcut(p));
+
+
+ }
+
+ }
+
+ }
+ if (addDisconnect)
+ newShortcuts.add(disconnectShortcut);
+ for (VpnProfile p : LRUProfiles)
+ newShortcuts.add(createShortcut(p));
+
+ if (updateShortcuts.size() > 0)
+ shortcutManager.updateShortcuts(updateShortcuts);
+ if (removeShortcuts.size() > 0)
+ shortcutManager.removeDynamicShortcuts(removeShortcuts);
+ if (newShortcuts.size() > 0)
+ shortcutManager.addDynamicShortcuts(newShortcuts);
+ if (disableShortcuts.size() > 0)
+ shortcutManager.disableShortcuts(disableShortcuts, "VpnProfile does not exist anymore.");
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
+ ShortcutInfo createShortcut(VpnProfile profile) {
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ shortcutIntent.setClass(getActivity(), LaunchVPN.class);
+ shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
+ shortcutIntent.setAction(Intent.ACTION_MAIN);
+ shortcutIntent.putExtra("EXTRA_HIDELOG", true);
+
+ PersistableBundle versionExtras = new PersistableBundle();
+ versionExtras.putInt("version", SHORTCUT_VERSION);
+
+ return new ShortcutInfo.Builder(getContext(), profile.getUUIDString())
+ .setShortLabel(profile.getName())
+ .setLongLabel(getString(R.string.qs_connect, profile.getName()))
+ .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key))
+ .setIntent(shortcutIntent)
+ .setExtras(versionExtras)
+ .build();
+ }
+
+ class MiniImageGetter implements ImageGetter {
+
+
+ @Override
+ public Drawable getDrawable(String source) {
+ Drawable d = null;
+ if ("ic_menu_add".equals(source))
+ d = getActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey);
+ else if ("ic_menu_archive".equals(source))
+ d = getActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey);
+
+
+ if (d != null) {
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ return d;
+ } else {
+ return null;
+ }
+ }
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setListAdapter();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ updateDynamicShortcuts();
+ }
+ VpnStatus.addStateListener(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ VpnStatus.removeStateListener(this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.vpn_profile_list, container, false);
+
+ TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint);
+ TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint);
+
+ newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint), new MiniImageGetter(), null));
+ importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint), new MiniImageGetter(), null));
+
+ ImageButton fab_add = (ImageButton) v.findViewById(R.id.fab_add);
+ ImageButton fab_import = (ImageButton) v.findViewById(R.id.fab_import);
+ if (fab_add != null)
+ fab_add.setOnClickListener(this);
+
+ if (fab_import != null)
+ fab_import.setOnClickListener(this);
+
+ return v;
+
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ setListAdapter();
+ }
+
+ static class VpnProfileNameComparator implements Comparator<VpnProfile> {
+
+ @Override
+ public int compare(VpnProfile lhs, VpnProfile rhs) {
+ if (lhs == rhs)
+ // Catches also both null
+ return 0;
+
+ if (lhs == null)
+ return -1;
+ if (rhs == null)
+ return 1;
+
+ if (lhs.mName == null)
+ return -1;
+ if (rhs.mName == null)
+ return 1;
+
+ return lhs.mName.compareTo(rhs.mName);
+ }
+
+ }
+
+ static class VpnProfileLRUComparator implements Comparator<VpnProfile> {
+
+ VpnProfileNameComparator nameComparator = new VpnProfileNameComparator();
+
+ @Override
+ public int compare(VpnProfile lhs, VpnProfile rhs) {
+ if (lhs == rhs)
+ // Catches also both null
+ return 0;
+
+ if (lhs == null)
+ return -1;
+ if (rhs == null)
+ return 1;
+
+ // Copied from Long.compare
+ if (lhs.mLastUsed > rhs.mLastUsed)
+ return -1;
+ if (lhs.mLastUsed < rhs.mLastUsed)
+ return 1;
+ else
+ return nameComparator.compare(lhs, rhs);
+ }
+ }
+
+
+ private void setListAdapter() {
+ if (mArrayadapter == null) {
+ mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title);
+
+ }
+ populateVpnList();
+ }
+
+ private void populateVpnList() {
+ boolean sortByLRU = Preferences.getDefaultSharedPreferences(getActivity()).getBoolean(PREF_SORT_BY_LRU, false);
+ Collection<VpnProfile> allvpn = getPM().getProfiles();
+ TreeSet<VpnProfile> sortedset;
+ if (sortByLRU)
+ sortedset = new TreeSet<>(new VpnProfileLRUComparator());
+ else
+ sortedset = new TreeSet<>(new VpnProfileNameComparator());
+
+ sortedset.addAll(allvpn);
+ mArrayadapter.clear();
+ mArrayadapter.addAll(sortedset);
+
+ setListAdapter(mArrayadapter);
+ mArrayadapter.notifyDataSetChanged();
+ }
+
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile)
+ .setIcon(R.drawable.ic_menu_add)
+ .setAlphabeticShortcut('a')
+ .setTitleCondensed(getActivity().getString(R.string.add))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+ menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import)
+ .setIcon(R.drawable.ic_menu_import)
+ .setAlphabeticShortcut('i')
+ .setTitleCondensed(getActivity().getString(R.string.menu_import_short))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+ menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting)
+ .setIcon(R.drawable.ic_sort)
+ .setAlphabeticShortcut('s')
+ .setTitleCondensed(getString(R.string.sort))
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == MENU_ADD_PROFILE) {
+ onAddOrDuplicateProfile(null);
+ return true;
+ } else if (itemId == MENU_IMPORT_PROFILE) {
+ return startImportConfigFilePicker();
+ } else if (itemId == MENU_CHANGE_SORTING) {
+ return changeSorting();
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private boolean changeSorting() {
+ SharedPreferences prefs = Preferences.getDefaultSharedPreferences(getActivity());
+ boolean oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false);
+ SharedPreferences.Editor prefsedit = prefs.edit();
+ if (oldValue) {
+ Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show();
+ prefsedit.putBoolean(PREF_SORT_BY_LRU, false);
+ } else {
+ prefsedit.putBoolean(PREF_SORT_BY_LRU, true);
+ Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show();
+ }
+ prefsedit.apply();
+ populateVpnList();
+ return true;
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.fab_import:
+ startImportConfigFilePicker();
+ break;
+ case R.id.fab_add:
+ onAddOrDuplicateProfile(null);
+ break;
+ }
+ }
+
+ private boolean startImportConfigFilePicker() {
+ boolean startOldFileDialog = true;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity() ))
+ startOldFileDialog = !startFilePicker();
+
+ if (startOldFileDialog)
+ startImportConfig();
+
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private boolean startFilePicker() {
+
+ Intent i = Utils.getFilePickerIntent(getActivity(), Utils.FileType.OVPN_CONFIG);
+ if (i != null) {
+ startActivityForResult(i, FILE_PICKER_RESULT_KITKAT);
+ return true;
+ } else
+ return false;
+ }
+
+ private void startImportConfig() {
+ Intent intent = new Intent(getActivity(), FileSelect.class);
+ intent.putExtra(FileSelect.NO_INLINE_SELECTION, true);
+ intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file);
+ startActivityForResult(intent, SELECT_PROFILE);
+ }
+
+
+ private void onAddOrDuplicateProfile(final VpnProfile mCopyProfile) {
+ Context context = getActivity();
+ if (context != null) {
+ final EditText entry = new EditText(context);
+ entry.setSingleLine();
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(context);
+ if (mCopyProfile == null)
+ dialog.setTitle(R.string.menu_add_profile);
+ else {
+ dialog.setTitle(context.getString(R.string.duplicate_profile_title, mCopyProfile.mName));
+ entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName));
+ }
+
+ dialog.setMessage(R.string.add_profile_name_prompt);
+ dialog.setView(entry);
+
+ dialog.setNeutralButton(R.string.menu_import_short,
+ (dialog1, which) -> startImportConfigFilePicker());
+ dialog.setPositiveButton(android.R.string.ok,
+ (dialog12, which) -> {
+ String name = entry.getText().toString();
+ if (getPM().getProfileByName(name) == null) {
+ VpnProfile profile;
+ if (mCopyProfile != null) {
+ profile = mCopyProfile.copy(name);
+ // Remove restrictions on copy profile
+ profile.mProfileCreator = null;
+ profile.mUserEditable = true;
+ } else
+ profile = new VpnProfile(name);
+
+ addProfile(profile);
+ editVPN(profile);
+ } else {
+ Toast.makeText(getActivity(), R.string.duplicate_profile_name, Toast.LENGTH_LONG).show();
+ }
+ });
+ dialog.setNegativeButton(android.R.string.cancel, null);
+ dialog.create().show();
+ }
+
+ }
+
+ private void addProfile(VpnProfile profile) {
+ getPM().addProfile(profile);
+ getPM().saveProfileList(getActivity());
+ getPM().saveProfile(getActivity(), profile);
+ mArrayadapter.add(profile);
+ }
+
+ private ProfileManager getPM() {
+ return ProfileManager.getInstance(getActivity());
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (resultCode == RESULT_VPN_DELETED) {
+ if (mArrayadapter != null && mEditProfile != null)
+ mArrayadapter.remove(mEditProfile);
+ } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) {
+ String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+ VpnProfile profile = ProfileManager.get(getActivity(), profileUUID);
+ if (profile != null)
+ onAddOrDuplicateProfile(profile);
+ }
+
+
+ if (resultCode != Activity.RESULT_OK)
+ return;
+
+
+ if (requestCode == START_VPN_CONFIG) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN);
+ getPM().saveProfile(getActivity(), profile);
+ // Name could be modified, reset List adapter
+ setListAdapter();
+
+ } else if (requestCode == SELECT_PROFILE) {
+ String fileData = data.getStringExtra(FileSelect.RESULT_DATA);
+ Uri uri = new Uri.Builder().path(fileData).scheme("file").build();
+
+ startConfigImport(uri);
+ } else if (requestCode == IMPORT_PROFILE) {
+ String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+ mArrayadapter.add(ProfileManager.get(getActivity(), profileUUID));
+ } else if (requestCode == FILE_PICKER_RESULT_KITKAT) {
+ if (data != null) {
+ Uri uri = data.getData();
+ startConfigImport(uri);
+ }
+ }
+
+ }
+
+ private void startConfigImport(Uri uri) {
+ Intent startImport = new Intent(getActivity(), ConfigConverter.class);
+ startImport.setAction(ConfigConverter.IMPORT_PROFILE);
+ startImport.setData(uri);
+ startActivityForResult(startImport, IMPORT_PROFILE);
+ }
+
+
+ private void editVPN(VpnProfile profile) {
+ mEditProfile = profile;
+ Intent vprefintent = new Intent(getActivity(), VPNPreferences.class)
+ .putExtra(getActivity().getPackageName() + ".profileUUID", profile.getUUID().toString());
+
+ startActivityForResult(vprefintent, START_VPN_CONFIG);
+ }
+
+ private void startVPN(VpnProfile profile) {
+
+ getPM().saveProfile(getActivity(), profile);
+
+ Intent intent = new Intent(getActivity(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+}