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