diff --git a/src/frontends/android/README.ndk b/src/frontends/android/README.ndk index 7c8cd309e..22150dd55 100644 --- a/src/frontends/android/README.ndk +++ b/src/frontends/android/README.ndk @@ -1,13 +1,14 @@ -To build this within the NDK several things have to be added in the -app/src/main/jni/ folder: +To build this within the NDK the following things have to be done: - - strongswan: The strongSwan sources. This can either be an extracted tarball, - or a symlink to the Git repository. To build from the repository the sources - have to be prepared first (see HACKING for a list of required tools): + - By default the strongSwan sources of the current Git tree are used. They have + to be prepared first (see HACKING for a list of required tools): ./autogen.sh && ./configure && make && make distclean - - openssl: The OpenSSL sources. Since the sources need to be changed to be - built on Android (and especially in the NDK), we provide a modified mirror - of the official Android OpenSSL version on git.strongswan.org. + It is also possible to use the sources from a different directory (e.g. an + extracted tarball) by setting strongswan_DIR in app/src/main/jni/Android.mk. + - The OpenSSL or BoringSSL sources are expected in app/src/main/jni/openssl. + Since the sources need to be changed to be built on Android (and especially + in the NDK) we provide a modified mirror of the official Android repositories + on git.strongswan.org. diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java index 5c64ad0e5..8a9d319b5 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java @@ -1,8 +1,8 @@ /* - * Copyright (C) 2012-2015 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -25,6 +25,7 @@ public class VpnProfile implements Cloneable public static final int SPLIT_TUNNELING_BLOCK_IPV6 = 2; private String mName, mGateway, mUsername, mPassword, mCertificate, mUserCertificate; + private String mRemoteId, mLocalId; private Integer mMTU, mPort, mSplitTunneling; private VpnType mVpnType; private long mId = -1; @@ -109,6 +110,26 @@ public class VpnProfile implements Cloneable this.mUserCertificate = alias; } + public String getLocalId() + { + return mLocalId; + } + + public void setLocalId(String localId) + { + this.mLocalId = localId; + } + + public String getRemoteId() + { + return mRemoteId; + } + + public void setRemoteId(String remoteId) + { + this.mRemoteId = remoteId; + } + public Integer getMTU() { return mMTU; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java index 45e9b8650..17026536b 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java @@ -1,8 +1,8 @@ /* - * Copyright (C) 2012-2015 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -43,6 +43,8 @@ public class VpnProfileDataSource public static final String KEY_MTU = "mtu"; public static final String KEY_PORT = "port"; public static final String KEY_SPLIT_TUNNELING = "split_tunneling"; + public static final String KEY_LOCAL_ID = "local_id"; + public static final String KEY_REMOTE_ID = "remote_id"; private DatabaseHelper mDbHelper; private SQLiteDatabase mDatabase; @@ -51,7 +53,7 @@ public class VpnProfileDataSource private static final String DATABASE_NAME = "strongswan.db"; private static final String TABLE_VPNPROFILE = "vpnprofile"; - private static final int DATABASE_VERSION = 7; + private static final int DATABASE_VERSION = 8; public static final String DATABASE_CREATE = "CREATE TABLE " + TABLE_VPNPROFILE + " (" + @@ -65,7 +67,9 @@ public class VpnProfileDataSource KEY_USER_CERTIFICATE + " TEXT," + KEY_MTU + " INTEGER," + KEY_PORT + " INTEGER," + - KEY_SPLIT_TUNNELING + " INTEGER" + + KEY_SPLIT_TUNNELING + " INTEGER," + + KEY_LOCAL_ID + " TEXT," + + KEY_REMOTE_ID + " TEXT" + ");"; private static final String[] ALL_COLUMNS = new String[] { KEY_ID, @@ -79,6 +83,8 @@ public class VpnProfileDataSource KEY_MTU, KEY_PORT, KEY_SPLIT_TUNNELING, + KEY_LOCAL_ID, + KEY_REMOTE_ID, }; private static class DatabaseHelper extends SQLiteOpenHelper @@ -128,6 +134,13 @@ public class VpnProfileDataSource db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SPLIT_TUNNELING + " INTEGER;"); } + if (oldVersion < 8) + { + db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_LOCAL_ID + + " TEXT;"); + db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_REMOTE_ID + + " TEXT;"); + } } private void updateColumns(SQLiteDatabase db) @@ -282,6 +295,8 @@ public class VpnProfileDataSource profile.setMTU(getInt(cursor, cursor.getColumnIndex(KEY_MTU))); profile.setPort(getInt(cursor, cursor.getColumnIndex(KEY_PORT))); profile.setSplitTunneling(getInt(cursor, cursor.getColumnIndex(KEY_SPLIT_TUNNELING))); + profile.setLocalId(cursor.getString(cursor.getColumnIndex(KEY_LOCAL_ID))); + profile.setRemoteId(cursor.getString(cursor.getColumnIndex(KEY_REMOTE_ID))); return profile; } @@ -298,6 +313,8 @@ public class VpnProfileDataSource values.put(KEY_MTU, profile.getMTU()); values.put(KEY_PORT, profile.getPort()); values.put(KEY_SPLIT_TUNNELING, profile.getSplitTunneling()); + values.put(KEY_LOCAL_ID, profile.getLocalId()); + values.put(KEY_REMOTE_ID, profile.getRemoteId()); return values; } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java index 84c44c123..a6b9fc52d 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -1,8 +1,8 @@ /* - * Copyright (C) 2012-2015 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -232,6 +232,8 @@ public class CharonVpnService extends VpnService implements Runnable writer.setValue("connection.port", mCurrentProfile.getPort()); writer.setValue("connection.username", mCurrentProfile.getUsername()); writer.setValue("connection.password", mCurrentProfile.getPassword()); + writer.setValue("connection.local_id", mCurrentProfile.getLocalId()); + writer.setValue("connection.remote_id", mCurrentProfile.getRemoteId()); initiate(writer.serialize()); } else diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java index 143741faf..5e9873d1b 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java @@ -1,6 +1,6 @@ /* - * Copyright (C) 2012 Tobias Brunner - * Hochschule fuer Technik Rapperswil + * Copyright (C) 2012-2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -15,10 +15,15 @@ package org.strongswan.android.security; -import java.security.cert.X509Certificate; - import android.net.http.SslCertificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + public class TrustedCertificateEntry implements Comparable { private final X509Certificate mCert; @@ -86,6 +91,40 @@ public class TrustedCertificateEntry implements Comparable getSubjectAltNames() + { + List list = new ArrayList<>(); + try + { + Collection> sans = mCert.getSubjectAlternativeNames(); + if (sans != null) + { + for (List san : sans) + { + switch ((Integer)san.get(0)) + { + case 1: /* rfc822Name */ + case 2: /* dnSName */ + case 7: /* iPAddress */ + list.add((String)san.get(1)); + break; + } + } + } + Collections.sort(list); + } + catch(CertificateParsingException ex) + { + ex.printStackTrace(); + } + return list; + } + /** * The alias associated with this certificate. * diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java index 7328693f1..ab8dfc2de 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java @@ -15,9 +15,6 @@ package org.strongswan.android.ui; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; import android.app.Service; import android.content.ComponentName; import android.content.Context; @@ -25,6 +22,9 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v4.content.ContextCompat; import android.view.GestureDetector; import android.view.LayoutInflater; @@ -66,7 +66,6 @@ public class ImcStateFragment extends Fragment implements VpnStateListener { mService = ((VpnStateService.LocalBinder)service).getService(); mService.registerListener(ImcStateFragment.this); - updateView(); } }; @@ -147,9 +146,9 @@ public class ImcStateFragment extends Fragment implements VpnStateListener } @Override - public void onStart() + public void onResume() { - super.onStart(); + super.onResume(); if (mService != null) { mService.registerListener(this); @@ -158,9 +157,9 @@ public class ImcStateFragment extends Fragment implements VpnStateListener } @Override - public void onStop() + public void onPause() { - super.onStop(); + super.onPause(); if (mService != null) { mService.unregisterListener(this); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java index 8740e0c46..2f6240726 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java @@ -15,24 +15,24 @@ package org.strongswan.android.ui; +import android.os.Bundle; +import android.os.FileObserver; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.strongswan.android.R; +import org.strongswan.android.logic.CharonVpnService; + import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.StringReader; -import org.strongswan.android.R; -import org.strongswan.android.logic.CharonVpnService; - -import android.app.Fragment; -import android.os.Bundle; -import android.os.FileObserver; -import android.os.Handler; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - public class LogFragment extends Fragment implements Runnable { private String mLogFilePath; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java index 3d5324056..890434255 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java @@ -18,9 +18,6 @@ package org.strongswan.android.ui; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; import android.app.Service; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -31,6 +28,9 @@ import android.net.VpnService; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -230,7 +230,7 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec removeFragmentByTag(DIALOG_TAG); - if (mService != null && mService.getState() == State.CONNECTED) + if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING)) { profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId()); @@ -317,7 +317,7 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec */ public void removeFragmentByTag(String tag) { - FragmentManager fm = getFragmentManager(); + FragmentManager fm = getSupportFragmentManager(); Fragment login = fm.findFragmentByTag(tag); if (login != null) { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionFragment.java index 04c288bcf..c17c552a2 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Tobias Brunner + * Copyright (C) 2013-2016 Tobias Brunner * Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it @@ -15,17 +15,18 @@ package org.strongswan.android.ui; -import org.strongswan.android.R; -import org.strongswan.android.logic.imc.RemediationInstruction; - -import android.app.ListFragment; import android.os.Bundle; +import android.support.v4.app.ListFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.FrameLayout; import android.widget.TextView; +import org.strongswan.android.R; +import org.strongswan.android.logic.imc.RemediationInstruction; + public class RemediationInstructionFragment extends ListFragment { public static final String ARG_REMEDIATION_INSTRUCTION = "instruction"; @@ -37,7 +38,13 @@ public class RemediationInstructionFragment extends ListFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.remediation_instruction, container, false); + /* while the documentation recommends to include "@android:layout/list_content" to retain + * the default functionality, this does not actually work with the ListFragment provided by + * the support library as it builds the view manually and uses different IDs */ + View layout = inflater.inflate(R.layout.remediation_instruction, container, false); + FrameLayout list = (FrameLayout)layout.findViewById(R.id.list_container); + list.addView(super.onCreateView(inflater, list, savedInstanceState)); + return layout; } @Override diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java index 7e7e870be..7765a2e02 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java @@ -38,7 +38,7 @@ public class RemediationInstructionsActivity extends AppCompatActivity implement { /* only update if we're not restoring */ return; } - RemediationInstructionsFragment frag = (RemediationInstructionsFragment)getFragmentManager().findFragmentById(R.id.remediation_instructions_fragment); + RemediationInstructionsFragment frag = (RemediationInstructionsFragment)getSupportFragmentManager().findFragmentById(R.id.remediation_instructions_fragment); if (frag != null) { /* two-pane layout, update fragment */ Bundle extras = getIntent().getExtras(); @@ -49,7 +49,7 @@ public class RemediationInstructionsActivity extends AppCompatActivity implement { /* one-pane layout, create fragment */ frag = new RemediationInstructionsFragment(); frag.setArguments(getIntent().getExtras()); - getFragmentManager().beginTransaction().add(R.id.fragment_container, frag).commit(); + getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, frag).commit(); } } @@ -60,7 +60,7 @@ public class RemediationInstructionsActivity extends AppCompatActivity implement { case android.R.id.home: /* one-pane layout, pop possible fragment from stack, finish otherwise */ - if (!getFragmentManager().popBackStackImmediate()) + if (!getSupportFragmentManager().popBackStackImmediate()) { finish(); } @@ -74,7 +74,7 @@ public class RemediationInstructionsActivity extends AppCompatActivity implement @Override public void onRemediationInstructionSelected(RemediationInstruction instruction) { - RemediationInstructionFragment frag = (RemediationInstructionFragment)getFragmentManager().findFragmentById(R.id.remediation_instruction_fragment); + RemediationInstructionFragment frag = (RemediationInstructionFragment)getSupportFragmentManager().findFragmentById(R.id.remediation_instruction_fragment); if (frag != null) { /* two-pane layout, update directly */ @@ -87,7 +87,7 @@ public class RemediationInstructionsActivity extends AppCompatActivity implement args.putParcelable(RemediationInstructionFragment.ARG_REMEDIATION_INSTRUCTION, instruction); frag.setArguments(args); - getFragmentManager().beginTransaction().replace(R.id.fragment_container, frag).addToBackStack(null).commit(); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, frag).addToBackStack(null).commit(); getSupportActionBar().setTitle(instruction.getTitle()); } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsFragment.java index 86467dc35..a2beffd94 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsFragment.java @@ -15,17 +15,17 @@ package org.strongswan.android.ui; -import java.util.ArrayList; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.View; +import android.widget.ListView; import org.strongswan.android.R; import org.strongswan.android.logic.imc.RemediationInstruction; import org.strongswan.android.ui.adapter.RemediationInstructionAdapter; -import android.app.Activity; -import android.app.ListFragment; -import android.os.Bundle; -import android.view.View; -import android.widget.ListView; +import java.util.ArrayList; public class RemediationInstructionsFragment extends ListFragment { @@ -65,13 +65,13 @@ public class RemediationInstructionsFragment extends ListFragment } @Override - public void onAttach(Activity activity) + public void onAttach(Context context) { - super.onAttach(activity); + super.onAttach(context); - if (activity instanceof OnRemediationInstructionSelectedListener) + if (context instanceof OnRemediationInstructionSelectedListener) { - mListener = (OnRemediationInstructionSelectedListener)activity; + mListener = (OnRemediationInstructionSelectedListener)context; } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java index bcc226b1f..29124ced7 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -1,8 +1,8 @@ /* - * Copyright (C) 2012-2014 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -29,7 +29,12 @@ import android.security.KeyChainException; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDialogFragment; +import android.text.Editable; import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.TextWatcher; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -39,10 +44,12 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; +import android.widget.MultiAutoCompleteTextView; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; @@ -54,6 +61,8 @@ import org.strongswan.android.data.VpnType; import org.strongswan.android.data.VpnType.VpnTypeFeature; import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.security.TrustedCertificateEntry; +import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter; +import org.strongswan.android.ui.widget.TextInputLayoutHelper; import java.security.cert.X509Certificate; @@ -67,24 +76,34 @@ public class VpnProfileDetailActivity extends AppCompatActivity private Long mId; private TrustedCertificateEntry mCertEntry; private String mUserCertLoading; + private CertificateIdentitiesAdapter mSelectUserIdAdapter; + private String mSelectedUserId; private TrustedCertificateEntry mUserCertEntry; private VpnType mVpnType = VpnType.IKEV2_EAP; private VpnProfile mProfile; - private EditText mName; + private MultiAutoCompleteTextView mName; + private TextInputLayoutHelper mNameWrap; private EditText mGateway; + private TextInputLayoutHelper mGatewayWrap; private Spinner mSelectVpnType; private ViewGroup mUsernamePassword; private EditText mUsername; + private TextInputLayoutHelper mUsernameWrap; private EditText mPassword; private ViewGroup mUserCertificate; private RelativeLayout mSelectUserCert; + private Spinner mSelectUserId; private CheckBox mCheckAuto; private RelativeLayout mSelectCert; private RelativeLayout mTncNotice; private CheckBox mShowAdvanced; private ViewGroup mAdvancedSettings; + private MultiAutoCompleteTextView mRemoteId; + private TextInputLayoutHelper mRemoteIdWrap; private EditText mMTU; + private TextInputLayoutHelper mMTUWrap; private EditText mPort; + private TextInputLayoutHelper mPortWrap; private CheckBox mBlockIPv4; private CheckBox mBlockIPv6; @@ -101,17 +120,21 @@ public class VpnProfileDetailActivity extends AppCompatActivity setContentView(R.layout.profile_detail_view); - mName = (EditText)findViewById(R.id.name); + mName = (MultiAutoCompleteTextView)findViewById(R.id.name); + mNameWrap = (TextInputLayoutHelper)findViewById(R.id.name_wrap); mGateway = (EditText)findViewById(R.id.gateway); + mGatewayWrap = (TextInputLayoutHelper) findViewById(R.id.gateway_wrap); mSelectVpnType = (Spinner)findViewById(R.id.vpn_type); mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice); mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group); mUsername = (EditText)findViewById(R.id.username); + mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap); mPassword = (EditText)findViewById(R.id.password); mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group); mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate); + mSelectUserId = (Spinner)findViewById(R.id.select_user_id); mCheckAuto = (CheckBox)findViewById(R.id.ca_auto); mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate); @@ -119,11 +142,47 @@ public class VpnProfileDetailActivity extends AppCompatActivity mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced); mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings); + mRemoteId = (MultiAutoCompleteTextView)findViewById(R.id.remote_id); + mRemoteIdWrap = (TextInputLayoutHelper) findViewById(R.id.remote_id_wrap); mMTU = (EditText)findViewById(R.id.mtu); + mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap); mPort = (EditText)findViewById(R.id.port); + mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap); mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4); mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6); + final SpaceTokenizer spaceTokenizer = new SpaceTokenizer(); + mName.setTokenizer(spaceTokenizer); + mRemoteId.setTokenizer(spaceTokenizer); + final ArrayAdapter completeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line); + mName.setAdapter(completeAdapter); + mRemoteId.setAdapter(completeAdapter); + + mGateway.addTextChangedListener(new 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) {} + + @Override + public void afterTextChanged(Editable s) + { + completeAdapter.clear(); + completeAdapter.add(mGateway.getText().toString()); + if (TextUtils.isEmpty(mGateway.getText())) + { + mNameWrap.setHelperText(getString(R.string.profile_name_hint)); + mRemoteIdWrap.setHelperText(getString(R.string.profile_remote_id_hint)); + } + else + { + mNameWrap.setHelperText(String.format(getString(R.string.profile_name_hint_gateway), mGateway.getText())); + mRemoteIdWrap.setHelperText(String.format(getString(R.string.profile_remote_id_hint_gateway), mGateway.getText())); + } + } + }); + mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) @@ -151,6 +210,24 @@ public class VpnProfileDetailActivity extends AppCompatActivity }); mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener()); + mSelectUserIdAdapter = new CertificateIdentitiesAdapter(this); + mSelectUserId.setAdapter(mSelectUserIdAdapter); + mSelectUserId.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) + { + if (mUserCertEntry != null) + { /* we don't store the subject DN as it is in the reverse order and the default anyway */ + mSelectedUserId = position == 0 ? null : mSelectUserIdAdapter.getItem(position); + } + } + + @Override + public void onNothingSelected(AdapterView parent) + { + mSelectedUserId = null; + } + }); mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override @@ -211,6 +288,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity { outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias()); } + if (mSelectedUserId != null) + { + outState.putString(VpnProfileDataSource.KEY_LOCAL_ID, mSelectedUserId); + } if (mCertEntry != null) { outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias()); @@ -272,6 +353,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity if (mVpnType.has(VpnTypeFeature.CERTIFICATE)) { + mSelectUserId.setEnabled(false); if (mUserCertLoading != null) { ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading); @@ -282,11 +364,15 @@ public class VpnProfileDetailActivity extends AppCompatActivity ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias()); ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString()); + mSelectUserIdAdapter.setCertificate(mUserCertEntry); + mSelectUserId.setSelection(mSelectUserIdAdapter.getPosition(mSelectedUserId)); + mSelectUserId.setEnabled(true); } else { ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label); ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate); + mSelectUserIdAdapter.setCertificate(null); } } } @@ -349,7 +435,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity if (!show && mProfile != null) { Integer st = mProfile.getSplitTunneling(); - show = mProfile.getMTU() != null || mProfile.getPort() != null || (st != null && st != 0); + show = mProfile.getRemoteId() != null || mProfile.getMTU() != null || + mProfile.getPort() != null || (st != null && st != 0); } mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE); mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE); @@ -388,14 +475,14 @@ public class VpnProfileDetailActivity extends AppCompatActivity boolean valid = true; if (mGateway.getText().toString().trim().isEmpty()) { - mGateway.setError(getString(R.string.alert_text_no_input_gateway)); + mGatewayWrap.setError(getString(R.string.alert_text_no_input_gateway)); valid = false; } if (mVpnType.has(VpnTypeFeature.USER_PASS)) { if (mUsername.getText().toString().trim().isEmpty()) { - mUsername.setError(getString(R.string.alert_text_no_input_username)); + mUsernameWrap.setError(getString(R.string.alert_text_no_input_username)); valid = false; } } @@ -412,13 +499,13 @@ public class VpnProfileDetailActivity extends AppCompatActivity Integer mtu = getInteger(mMTU); if (mtu != null && (mtu < MTU_MIN || mtu > MTU_MAX)) { - mMTU.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX)); + mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX)); valid = false; } Integer port = getInteger(mPort); if (port != null && (port < 1 || port > 65535)) { - mPort.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535)); + mPortWrap.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535)); valid = false; } return valid; @@ -445,9 +532,12 @@ public class VpnProfileDetailActivity extends AppCompatActivity if (mVpnType.has(VpnTypeFeature.CERTIFICATE)) { mProfile.setUserCertificateAlias(mUserCertEntry.getAlias()); + mProfile.setLocalId(mSelectedUserId); } String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias(); mProfile.setCertificateAlias(certAlias); + String remote_id = mRemoteId.getText().toString().trim(); + mProfile.setRemoteId(remote_id.isEmpty() ? null : remote_id); mProfile.setMTU(getInteger(mMTU)); mProfile.setPort(getInteger(mPort)); int st = 0; @@ -463,7 +553,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity */ private void loadProfileData(Bundle savedInstanceState) { - String useralias = null, alias = null; + String useralias = null, local_id = null, alias = null; getSupportActionBar().setTitle(R.string.add_profile); if (mId != null && mId != 0) @@ -476,11 +566,13 @@ public class VpnProfileDetailActivity extends AppCompatActivity mVpnType = mProfile.getVpnType(); mUsername.setText(mProfile.getUsername()); mPassword.setText(mProfile.getPassword()); + mRemoteId.setText(mProfile.getRemoteId()); mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null); mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null); mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0 : false); mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0 : false); useralias = mProfile.getUserCertificateAlias(); + local_id = mProfile.getLocalId(); alias = mProfile.getCertificateAlias(); getSupportActionBar().setTitle(mProfile.getName()); } @@ -495,11 +587,13 @@ public class VpnProfileDetailActivity extends AppCompatActivity mSelectVpnType.setSelection(mVpnType.ordinal()); /* check if the user selected a user certificate previously */ - useralias = savedInstanceState == null ? useralias: savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE); + useralias = savedInstanceState == null ? useralias : savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE); + local_id = savedInstanceState == null ? local_id : savedInstanceState.getString(VpnProfileDataSource.KEY_LOCAL_ID); if (useralias != null) { UserCertificateLoader loader = new UserCertificateLoader(this, useralias); mUserCertLoading = useralias; + mSelectedUserId = local_id; loader.execute(); } @@ -650,4 +744,66 @@ public class VpnProfileDetailActivity extends AppCompatActivity }).create(); } } + + /** + * Tokenizer implementation that separates by white-space + */ + public static class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer + { + @Override + public int findTokenStart(CharSequence text, int cursor) + { + int i = cursor; + + while (i > 0 && !Character.isWhitespace(text.charAt(i - 1))) + { + i--; + } + return i; + } + + @Override + public int findTokenEnd(CharSequence text, int cursor) + { + int i = cursor; + int len = text.length(); + + while (i < len) + { + if (Character.isWhitespace(text.charAt(i))) + { + return i; + } + else + { + i++; + } + } + return len; + } + + @Override + public CharSequence terminateToken(CharSequence text) + { + int i = text.length(); + + if (i > 0 && Character.isWhitespace(text.charAt(i - 1))) + { + return text; + } + else + { + if (text instanceof Spanned) + { + SpannableString sp = new SpannableString(text + " "); + TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0); + return sp; + } + else + { + return text + " "; + } + } + } + } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java index a23df054b..d8d99ff00 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java @@ -17,21 +17,12 @@ package org.strongswan.android.ui; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -import org.strongswan.android.R; -import org.strongswan.android.data.VpnProfile; -import org.strongswan.android.data.VpnProfileDataSource; -import org.strongswan.android.ui.adapter.VpnProfileAdapter; - import android.app.Activity; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.util.AttributeSet; import android.view.ActionMode; import android.view.LayoutInflater; @@ -46,6 +37,15 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import android.widget.Toast; +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.ui.adapter.VpnProfileAdapter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + public class VpnProfileListFragment extends Fragment { private static final int ADD_REQUEST = 1; @@ -66,10 +66,10 @@ public class VpnProfileListFragment extends Fragment } @Override - public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) + public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) { - super.onInflate(activity, attrs, savedInstanceState); - TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.Fragment); + super.onInflate(context, attrs, savedInstanceState); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Fragment); mReadOnly = a.getBoolean(R.styleable.Fragment_read_only, false); a.recycle(); } @@ -126,13 +126,13 @@ public class VpnProfileListFragment extends Fragment } @Override - public void onAttach(Activity activity) + public void onAttach(Context context) { - super.onAttach(activity); + super.onAttach(context); - if (activity instanceof OnVpnProfileSelectedListener) + if (context instanceof OnVpnProfileSelectedListener) { - mListener = (OnVpnProfileSelectedListener)activity; + mListener = (OnVpnProfileSelectedListener)context; } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java index c49e5cc7e..0b093d78f 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java @@ -1,8 +1,8 @@ /* - * Copyright (C) 2012-2013 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -17,8 +17,6 @@ package org.strongswan.android.ui; -import android.app.Fragment; -import android.app.ProgressDialog; import android.app.Service; import android.content.ComponentName; import android.content.Context; @@ -27,6 +25,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; +import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; @@ -34,6 +33,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; +import android.widget.ProgressBar; import android.widget.TextView; import org.strongswan.android.R; @@ -60,9 +60,7 @@ public class VpnStateFragment extends Fragment implements VpnStateListener private int mColorStateError; private int mColorStateSuccess; private Button mActionButton; - private ProgressDialog mConnectDialog; - private ProgressDialog mDisconnectDialog; - private ProgressDialog mProgressDialog; + private ProgressBar mProgress; private AlertDialog mErrorDialog; private long mErrorConnectionID; private long mDismissedConnectionID; @@ -133,8 +131,9 @@ public class VpnStateFragment extends Fragment implements VpnStateListener } } }); - enableActionButton(false); + enableActionButton(null); + mProgress = (ProgressBar)view.findViewById(R.id.progress); mStateView = (TextView)view.findViewById(R.id.vpn_state); mColorStateBase = mStateView.getCurrentTextColor(); mProfileView = (TextView)view.findViewById(R.id.vpn_profile_label); @@ -163,7 +162,6 @@ public class VpnStateFragment extends Fragment implements VpnStateListener mService.unregisterListener(this); } hideErrorDialog(); - hideProgressDialog(); } @Override @@ -202,33 +200,35 @@ public class VpnStateFragment extends Fragment implements VpnStateListener return; } - enableActionButton(false); mProfileNameView.setText(name); switch (state) { case DISABLED: showProfile(false); - hideProgressDialog(); + mProgress.setVisibility(View.GONE); + enableActionButton(null); mStateView.setText(R.string.state_disabled); mStateView.setTextColor(mColorStateBase); break; case CONNECTING: showProfile(true); - showConnectDialog(name, gateway); + mProgress.setVisibility(View.VISIBLE); + enableActionButton(getString(android.R.string.cancel)); mStateView.setText(R.string.state_connecting); mStateView.setTextColor(mColorStateBase); break; case CONNECTED: showProfile(true); - hideProgressDialog(); - enableActionButton(true); + mProgress.setVisibility(View.GONE); + enableActionButton(getString(R.string.disconnect)); mStateView.setText(R.string.state_connected); mStateView.setTextColor(mColorStateSuccess); break; case DISCONNECTING: showProfile(true); - showDisconnectDialog(name); + mProgress.setVisibility(View.VISIBLE); + enableActionButton(null); mStateView.setText(R.string.state_disconnecting); mStateView.setTextColor(mColorStateBase); break; @@ -254,10 +254,10 @@ public class VpnStateFragment extends Fragment implements VpnStateListener { /* we already show the dialog */ return true; } - hideProgressDialog(); mProfileNameView.setText(name); showProfile(true); - enableActionButton(false); + mProgress.setVisibility(View.GONE); + enableActionButton(null); mStateView.setText(R.string.state_error); mStateView.setTextColor(mColorStateError); switch (error) @@ -294,20 +294,11 @@ public class VpnStateFragment extends Fragment implements VpnStateListener mProfileNameView.setVisibility(show ? View.VISIBLE : View.GONE); } - private void enableActionButton(boolean enable) + private void enableActionButton(String text) { - mActionButton.setEnabled(enable); - mActionButton.setVisibility(enable ? View.VISIBLE : View.GONE); - } - - private void hideProgressDialog() - { - if (mProgressDialog != null) - { - mProgressDialog.dismiss(); - mProgressDialog = null; - mDisconnectDialog = mConnectDialog = null; - } + mActionButton.setText(text); + mActionButton.setEnabled(text != null); + mActionButton.setVisibility(text != null ? View.VISIBLE : View.GONE); } private void hideErrorDialog() @@ -329,50 +320,6 @@ public class VpnStateFragment extends Fragment implements VpnStateListener updateView(); } - private void showConnectDialog(String profile, String gateway) - { - if (mConnectDialog != null) - { /* already showing the dialog */ - return; - } - hideProgressDialog(); - mConnectDialog = new ProgressDialog(getActivity()); - mConnectDialog.setTitle(String.format(getString(R.string.connecting_title), profile)); - mConnectDialog.setMessage(String.format(getString(R.string.connecting_message), gateway)); - mConnectDialog.setIndeterminate(true); - mConnectDialog.setCancelable(false); - mConnectDialog.setButton(DialogInterface.BUTTON_NEGATIVE, - getString(android.R.string.cancel), - new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - if (mService != null) - { - mService.disconnect(); - } - } - }); - mConnectDialog.show(); - mProgressDialog = mConnectDialog; - } - - private void showDisconnectDialog(String profile) - { - if (mDisconnectDialog != null) - { /* already showing the dialog */ - return; - } - hideProgressDialog(); - mDisconnectDialog = new ProgressDialog(getActivity()); - mDisconnectDialog.setMessage(getString(R.string.state_disconnecting)); - mDisconnectDialog.setIndeterminate(true); - mDisconnectDialog.setCancelable(false); - mDisconnectDialog.show(); - mProgressDialog = mDisconnectDialog; - } - private void showErrorDialog(int textid) { final List instructions = mService.getRemediationInstructions(); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/CertificateIdentitiesAdapter.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/CertificateIdentitiesAdapter.java new file mode 100644 index 000000000..c8e3df38b --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/CertificateIdentitiesAdapter.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.ui.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import org.strongswan.android.R; +import org.strongswan.android.security.TrustedCertificateEntry; + +import java.util.List; + +public class CertificateIdentitiesAdapter extends ArrayAdapter +{ + TrustedCertificateEntry mCertificate; + + public CertificateIdentitiesAdapter(Context context) + { + super(context, android.R.layout.simple_dropdown_item_1line); + extractIdentities(); + } + + /** + * Set a new certificate for this adapter. + * + * @param certificate the certificate to extract identities from (null to clear) + */ + public void setCertificate(TrustedCertificateEntry certificate) + { + mCertificate = certificate; + clear(); + extractIdentities(); + } + + private void extractIdentities() + { + if (mCertificate == null) + { + add(getContext().getString(R.string.profile_user_select_id_init)); + } + else + { + add(String.format(getContext().getString(R.string.profile_user_select_id_default), + mCertificate.getCertificate().getSubjectDN().getName())); + addAll(mCertificate.getSubjectAltNames()); + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java index f3bb271bc..58296a018 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java @@ -63,12 +63,18 @@ public class VpnProfileAdapter extends ArrayAdapter TextView tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_name); tv.setText(profile.getName()); tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_gateway); - tv.setText(getContext().getString(R.string.profile_gateway_label) + " " + profile.getGateway()); + tv.setText(getContext().getString(R.string.profile_gateway_label) + ": " + profile.getGateway()); tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_username); if (profile.getVpnType().has(VpnTypeFeature.USER_PASS)) { /* if the view is reused we make sure it is visible */ tv.setVisibility(View.VISIBLE); - tv.setText(getContext().getString(R.string.profile_username_label) + " " + profile.getUsername()); + tv.setText(getContext().getString(R.string.profile_username_label) + ": " + profile.getUsername()); + } + else if (profile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && + profile.getLocalId() != null) + { + tv.setVisibility(View.VISIBLE); + tv.setText(getContext().getString(R.string.profile_user_select_id_label) + ": " + profile.getLocalId()); } else { @@ -77,7 +83,7 @@ public class VpnProfileAdapter extends ArrayAdapter tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_certificate); if (profile.getVpnType().has(VpnTypeFeature.CERTIFICATE)) { - tv.setText(getContext().getString(R.string.profile_user_certificate_label) + " " + profile.getUserCertificateAlias()); + tv.setText(getContext().getString(R.string.profile_user_certificate_label) + ": " + profile.getUserCertificateAlias()); tv.setVisibility(View.VISIBLE); } else diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/TextInputLayoutHelper.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/TextInputLayoutHelper.java new file mode 100644 index 000000000..45b0ae592 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/TextInputLayoutHelper.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.ui.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.strongswan.android.R; + +/** + * Layout that extends {@link android.support.design.widget.TextInputLayout} with a helper text + * displayed below the text field when it receives the focus. Also, any error message shown with + * {@link #setError(CharSequence)} is hidden when the text field is changed (this mirrors the + * behavior of {@link android.widget.EditText}). + */ +public class TextInputLayoutHelper extends TextInputLayout +{ + private LinearLayout mHelperContainer; + private TextView mHelperText; + + public TextInputLayoutHelper(Context context) + { + this(context, null); + } + + public TextInputLayoutHelper(Context context, AttributeSet attrs) + { + this(context, attrs, 0); + } + + public TextInputLayoutHelper(Context context, AttributeSet attrs, int defStyleAttr) + { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextInputLayoutHelper); + String helper = a.getString(R.styleable.TextInputLayoutHelper_helper_text); + a.recycle(); + if (helper != null) + { + mHelperContainer = new LinearLayout(context); + mHelperContainer.setOrientation(LinearLayout.HORIZONTAL); + mHelperContainer.setVisibility(View.INVISIBLE); + addView(mHelperContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + + mHelperText = new TextView(context); + mHelperText.setText(helper); + mHelperText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textColorSecondary}); + mHelperText.setTextColor(a.getColor(0, mHelperText.getCurrentTextColor())); + a.recycle(); + + mHelperContainer.addView(mHelperText, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) + { + super.addView(child, index, params); + if (child instanceof EditText) + { + EditText text = (EditText)child; + text.addTextChangedListener(new 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) {} + + @Override + public void afterTextChanged(Editable s) + { + if (getError() != null) + { + setError(null); + } + } + }); + if (mHelperContainer != null) + { + text.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) + { + showHelper(hasFocus); + } + }); + ViewCompat.setPaddingRelative(mHelperContainer, ViewCompat.getPaddingStart(text), + 0, ViewCompat.getPaddingEnd(text), text.getPaddingBottom()); + } + } + } + + @Override + public void setError(@Nullable CharSequence error) + { + super.setError(error); + if (mHelperContainer != null) + { + if (error == null) + { /* this frees up space used by the now invisible error message */ + setErrorEnabled(false); + } + else + { /* re-add the helper as the error message should be displayed directly under the textbox */ + removeView(mHelperContainer); + addView(mHelperContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + } + } + + /** + * Set the helper text to be displayed below the text field. + * + * @attr ref R.styleable#TextInputLayoutHelper_helper_text + */ + public void setHelperText(CharSequence text) + { + mHelperText.setText(text); + } + + private void showHelper(boolean show) + { + if (show == (mHelperContainer.getVisibility() == View.VISIBLE)) + { + return; + } + if (show) + { + ViewCompat.animate(mHelperContainer) + .alpha(1f) + .setDuration(200) + .setListener(new ViewPropertyAnimatorListenerAdapter() { + @Override + public void onAnimationStart(View view) + { + view.setVisibility(View.VISIBLE); + } + }).start(); + } + else + { + ViewCompat.animate(mHelperContainer) + .alpha(0f) + .setDuration(200) + .setListener(new ViewPropertyAnimatorListenerAdapter() { + @Override + public void onAnimationEnd(View view) + { + view.setVisibility(View.INVISIBLE); + } + }).start(); + } + } +} diff --git a/src/frontends/android/app/src/main/jni/Android.mk b/src/frontends/android/app/src/main/jni/Android.mk index 9e81a14c5..5bbfeaa1b 100644 --- a/src/frontends/android/app/src/main/jni/Android.mk +++ b/src/frontends/android/app/src/main/jni/Android.mk @@ -16,12 +16,14 @@ endif strongswan_PLUGINS := $(strongswan_CHARON_PLUGINS) \ $(strongswan_BYOD_PLUGINS) -include $(LOCAL_PATH)/strongswan/Android.common.mk +strongswan_DIR := ../../../../../../../ # includes -strongswan_PATH := $(LOCAL_PATH)/strongswan +strongswan_PATH := $(LOCAL_PATH)/$(strongswan_DIR) openssl_PATH := $(LOCAL_PATH)/openssl/include +include $(strongswan_PATH)/Android.common.mk + # CFLAGS (partially from a configure run using droid-gcc) strongswan_CFLAGS := \ -Wall \ @@ -67,15 +69,15 @@ endif strongswan_BUILD := \ openssl \ libandroidbridge \ - strongswan/src/libipsec \ - strongswan/src/libcharon \ - strongswan/src/libstrongswan + $(strongswan_DIR)/src/libipsec \ + $(strongswan_DIR)/src/libcharon \ + $(strongswan_DIR)/src/libstrongswan ifneq ($(strongswan_USE_BYOD),) strongswan_BUILD += \ - strongswan/src/libtnccs \ - strongswan/src/libtncif \ - strongswan/src/libimcv + $(strongswan_DIR)/src/libtnccs \ + $(strongswan_DIR)/src/libtncif \ + $(strongswan_DIR)/src/libimcv endif include $(addprefix $(LOCAL_PATH)/,$(addsuffix /Android.mk, \ diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_service.c b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_service.c index b3e91d808..2532402bb 100644 --- a/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_service.c +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_service.c @@ -1,8 +1,8 @@ /* - * Copyright (C) 2010-2015 Tobias Brunner + * Copyright (C) 2010-2016 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -430,6 +430,84 @@ CALLBACK(reestablish, job_requeue_t, return JOB_REQUEUE_NONE; } +METHOD(listener_t, ike_updown, bool, + private_android_service_t *this, ike_sa_t *ike_sa, bool up) +{ + /* this callback is only registered during initiation, so if the IKE_SA + * goes down we assume some kind of authentication error, more specific + * errors are catched in the alert() handler */ + if (this->ike_sa == ike_sa && !up) + { + charonservice->update_status(charonservice, + CHARONSERVICE_AUTH_ERROR); + return FALSE; + } + return TRUE; +} + +METHOD(listener_t, ike_rekey, bool, + private_android_service_t *this, ike_sa_t *old, ike_sa_t *new) +{ + if (this->ike_sa == old) + { + this->ike_sa = new; + } + return TRUE; +} + +METHOD(listener_t, ike_reestablish_post_redirect, bool, + private_android_service_t *this, ike_sa_t *old, ike_sa_t *new, + bool initiated) +{ + if (this->ike_sa == old && initiated) + { /* if we get redirected during IKE_AUTH we just migrate to the new SA, + * we don't have a TUN device yet, so reinstalling it without DNS would + * fail (and using the DNS proxy is not required anyway) */ + this->ike_sa = new; + } + return TRUE; +} + +METHOD(listener_t, ike_reestablish_pre, bool, + private_android_service_t *this, ike_sa_t *old, ike_sa_t *new) +{ + if (this->ike_sa == old) + { + /* enable DNS proxy so hosts are properly resolved while the TUN device + * is still active */ + this->lock->write_lock(this->lock); + this->use_dns_proxy = TRUE; + this->lock->unlock(this->lock); + /* if DNS servers are installed that are only reachable through the VPN + * the DNS proxy doesn't help, so uninstall DNS servers */ + if (!setup_tun_device_without_dns(this)) + { + DBG1(DBG_DMN, "failed to setup TUN device without DNS"); + charonservice->update_status(charonservice, + CHARONSERVICE_GENERIC_ERROR); + } + } + return TRUE; +} + +METHOD(listener_t, ike_reestablish_post, bool, + private_android_service_t *this, ike_sa_t *old, ike_sa_t *new, + bool initiated) +{ + if (this->ike_sa == old && initiated) + { + this->ike_sa = new; + /* re-register hook to detect initiation failures */ + this->public.listener.ike_updown = _ike_updown; + /* if the IKE_SA got deleted by the responder we get the child_down() + * event on the old IKE_SA after this hook has been called, so they + * get ignored and thus we trigger the event here */ + charonservice->update_status(charonservice, + CHARONSERVICE_CHILD_STATE_DOWN); + } + return TRUE; +} + METHOD(listener_t, child_updown, bool, private_android_service_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa, bool up) @@ -440,6 +518,9 @@ METHOD(listener_t, child_updown, bool, { /* disable the hooks registered to catch initiation failures */ this->public.listener.ike_updown = NULL; + /* enable hooks to handle reauthentications */ + this->public.listener.ike_reestablish_pre = _ike_reestablish_pre; + this->public.listener.ike_reestablish_post = _ike_reestablish_post; /* CHILD_SA is up so we can disable the DNS proxy we enabled to * reestablish the SA */ this->lock->write_lock(this->lock); @@ -465,21 +546,6 @@ METHOD(listener_t, child_updown, bool, return TRUE; } -METHOD(listener_t, ike_updown, bool, - private_android_service_t *this, ike_sa_t *ike_sa, bool up) -{ - /* this callback is only registered during initiation, so if the IKE_SA - * goes down we assume some kind of authentication error, more specific - * errors are catched in the alert() handler */ - if (this->ike_sa == ike_sa && !up) - { - charonservice->update_status(charonservice, - CHARONSERVICE_AUTH_ERROR); - return FALSE; - } - return TRUE; -} - METHOD(listener_t, alert, bool, private_android_service_t *this, ike_sa_t *ike_sa, alert_t alert, va_list args) @@ -554,62 +620,12 @@ METHOD(listener_t, alert, bool, return TRUE; } -METHOD(listener_t, ike_rekey, bool, - private_android_service_t *this, ike_sa_t *old, ike_sa_t *new) -{ - if (this->ike_sa == old) - { - this->ike_sa = new; - } - return TRUE; -} - -METHOD(listener_t, ike_reestablish_pre, bool, - private_android_service_t *this, ike_sa_t *old, ike_sa_t *new) -{ - if (this->ike_sa == old) - { - /* enable DNS proxy so hosts are properly resolved while the TUN device - * is still active */ - this->lock->write_lock(this->lock); - this->use_dns_proxy = TRUE; - this->lock->unlock(this->lock); - /* if DNS servers are installed that are only reachable through the VPN - * the DNS proxy doesn't help, so uninstall DNS servers */ - if (!setup_tun_device_without_dns(this)) - { - DBG1(DBG_DMN, "failed to setup TUN device without DNS"); - charonservice->update_status(charonservice, - CHARONSERVICE_GENERIC_ERROR); - } - } - return TRUE; -} - -METHOD(listener_t, ike_reestablish_post, bool, - private_android_service_t *this, ike_sa_t *old, ike_sa_t *new, - bool initiated) -{ - if (this->ike_sa == old && initiated) - { - this->ike_sa = new; - /* re-register hook to detect initiation failures */ - this->public.listener.ike_updown = _ike_updown; - /* if the IKE_SA got deleted by the responder we get the child_down() - * event on the old IKE_SA after this hook has been called, so they - * get ignored and thus we trigger the event here */ - charonservice->update_status(charonservice, - CHARONSERVICE_CHILD_STATE_DOWN); - } - return TRUE; -} - static void add_auth_cfg_pw(private_android_service_t *this, peer_cfg_t *peer_cfg, bool byod) { - identification_t *user; + identification_t *user, *id = NULL; auth_cfg_t *auth; - char *username, *password; + char *username, *password, *local_id; auth = auth_cfg_create(); auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP); @@ -622,8 +638,19 @@ static void add_auth_cfg_pw(private_android_service_t *this, NULL); password = this->settings->get_str(this->settings, "connection.password", NULL); + local_id = this->settings->get_str(this->settings, "connection.local_id", + NULL); user = identification_create_from_string(username); - auth->add(auth, AUTH_RULE_IDENTITY, user); + auth->add(auth, AUTH_RULE_EAP_IDENTITY, user); + if (local_id) + { + id = identification_create_from_string(local_id); + } + if (!id) + { + id = user->clone(user); + } + auth->add(auth, AUTH_RULE_IDENTITY, id); this->creds->add_username_password(this->creds, username, password); peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE); @@ -633,9 +660,9 @@ static bool add_auth_cfg_cert(private_android_service_t *this, peer_cfg_t *peer_cfg) { certificate_t *cert; - identification_t *id; + identification_t *id = NULL; auth_cfg_t *auth; - char *type; + char *type, *local_id; cert = this->creds->load_user_certificate(this->creds); if (!cert) @@ -649,8 +676,8 @@ static bool add_auth_cfg_cert(private_android_service_t *this, { auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP); auth->add(auth, AUTH_RULE_EAP_TYPE, EAP_TLS); - id = identification_create_from_string("%any"); - auth->add(auth, AUTH_RULE_AAA_IDENTITY, id); + auth->add(auth, AUTH_RULE_AAA_IDENTITY, + identification_create_from_string("%any")); } else { @@ -658,15 +685,25 @@ static bool add_auth_cfg_cert(private_android_service_t *this, } auth->add(auth, AUTH_RULE_SUBJECT_CERT, cert); - id = cert->get_subject(cert); - auth->add(auth, AUTH_RULE_IDENTITY, id->clone(id)); + local_id = this->settings->get_str(this->settings, "connection.local_id", + NULL); + if (local_id) + { + id = identification_create_from_string(local_id); + } + if (!id) + { + id = cert->get_subject(cert); + id = id->clone(id); + } + auth->add(auth, AUTH_RULE_IDENTITY, id); peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE); return TRUE; } static job_requeue_t initiate(private_android_service_t *this) { - identification_t *gateway; + identification_t *gateway = NULL; ike_cfg_t *ike_cfg; peer_cfg_t *peer_cfg; child_cfg_t *child_cfg; @@ -692,7 +729,7 @@ static job_requeue_t initiate(private_android_service_t *this) .dpd_action = ACTION_RESTART, .close_action = ACTION_RESTART, }; - char *type, *server; + char *type, *server, *remote_id; int port; server = this->settings->get_str(this->settings, "connection.server", NULL); @@ -731,9 +768,20 @@ static job_requeue_t initiate(private_android_service_t *this) /* remote auth config */ auth = auth_cfg_create(); - gateway = identification_create_from_string(server); + remote_id = this->settings->get_str(this->settings, "connection.remote_id", + NULL); + if (remote_id) + { + gateway = identification_create_from_string(remote_id); + } + if (!gateway || gateway->get_type(gateway) == ID_ANY) + { + DESTROY_IF(gateway); + gateway = identification_create_from_string(server); + /* only use this if remote ID was not configured explicitly */ + auth->add(auth, AUTH_RULE_IDENTITY_LOOSE, TRUE); + } auth->add(auth, AUTH_RULE_IDENTITY, gateway); - auth->add(auth, AUTH_RULE_IDENTITY_LOOSE, TRUE); auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY); peer_cfg->add_auth_cfg(peer_cfg, auth, FALSE); @@ -824,8 +872,7 @@ android_service_t *android_service_create(android_creds_t *creds, .public = { .listener = { .ike_rekey = _ike_rekey, - .ike_reestablish_pre = _ike_reestablish_pre, - .ike_reestablish_post = _ike_reestablish_post, + .ike_reestablish_post = _ike_reestablish_post_redirect, .ike_updown = _ike_updown, .child_updown = _child_updown, .alert = _alert, diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c b/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c index 3bed6eb4d..fa9708963 100644 --- a/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c @@ -43,6 +43,7 @@ #define ANDROID_RETRASNMIT_TRIES 3 #define ANDROID_RETRANSMIT_TIMEOUT 2.0 #define ANDROID_RETRANSMIT_BASE 1.4 +#define ANDROID_KEEPALIVE_INTERVAL 45 typedef struct private_charonservice_t private_charonservice_t; @@ -466,6 +467,9 @@ static void set_options(char *logfile) "charon.retransmit_timeout", ANDROID_RETRANSMIT_TIMEOUT); lib->settings->set_double(lib->settings, "charon.retransmit_base", ANDROID_RETRANSMIT_BASE); + /* increase NAT-T keepalive interval a bit to save battery power */ + lib->settings->set_time(lib->settings, + "charon.keep_alive", ANDROID_KEEPALIVE_INTERVAL); lib->settings->set_bool(lib->settings, "charon.initiator_only", TRUE); lib->settings->set_bool(lib->settings, diff --git a/src/frontends/android/app/src/main/res/layout/login_dialog.xml b/src/frontends/android/app/src/main/res/layout/login_dialog.xml index 0262af0a3..adfaac5e2 100644 --- a/src/frontends/android/app/src/main/res/layout/login_dialog.xml +++ b/src/frontends/android/app/src/main/res/layout/login_dialog.xml @@ -1,9 +1,9 @@ - + android:padding="10dp" > - - - + android:layout_height="wrap_content" > - + - + + + android:layout_height="wrap_content" + android:layout_marginTop="4dp" > + + + + diff --git a/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml b/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml index 57d5606ff..08881b38c 100644 --- a/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml +++ b/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml @@ -1,9 +1,9 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" > + android:padding="10dp" + android:animateLayoutChanges="true" > + + + + + + - - - - - - - - - + android:layout_height="wrap_content" > - + - + - + android:layout_marginTop="4dp" + app:helper_text="@string/profile_password_hint" > + + + + @@ -109,36 +107,73 @@ android:id="@+id/user_certificate_group" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="4dp" android:orientation="vertical" > + + + + + + + + + + + android:layout_marginLeft="4dp" + android:textSize="20sp" + android:text="@string/profile_advanced_label" /> - + android:layout_marginTop="10dp" + app:helper_text="@string/profile_remote_id_hint" > + + - + + + app:helper_text="@string/profile_mtu_hint" > - + + + + + app:helper_text="@string/profile_port_hint" > + + + + + - \ No newline at end of file + diff --git a/src/frontends/android/app/src/main/res/layout/remediation_instruction.xml b/src/frontends/android/app/src/main/res/layout/remediation_instruction.xml index 09c0d43a3..04fffaa4e 100644 --- a/src/frontends/android/app/src/main/res/layout/remediation_instruction.xml +++ b/src/frontends/android/app/src/main/res/layout/remediation_instruction.xml @@ -1,6 +1,6 @@ @@ -25,6 +25,6 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - strongswan:read_only="true" /> + app:read_only="true" /> \ No newline at end of file diff --git a/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml b/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml index e347c4c4b..86a2f1321 100644 --- a/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml +++ b/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml @@ -1,6 +1,6 @@ Speichern Abbrechen - Profilname: - (Server-Adresse verwenden) - Server: - Typ: - Benutzername: - Passwort: - (anfordern wenn benötigt) - Benutzer-Zertifikat: + Profilname (optional) + Standardwert ist der konfigurierte Server + Standardwert ist \"%1$s\" + Server + IP-Adresse oder Hostname des VPN Servers + VPN-Typ + Benutzername + Passwort (optional) + Leer lassen, um bei Bedarf danach gefragt zu werden + Benutzer-Zertifikat Benutzer-Zertifikat auswählen Wählen Sie ein bestimmtes Benutzer-Zertifikat - CA-Zertifikat: + Benutzer-Identität + Wählen Sie zuerst ein Benutzer-Zertifikat + Standardwert (%1$s) + CA-Zertifikat Automatisch wählen CA-Zertifikat auswählen Wählen Sie ein bestimmtes CA-Zertifikat + Erweiterte Einstellungen Erweiterte Einstellungen anzeigen - MTU: - Server Port: - (Standardwert verwenden) - Split-Tunneling: + Server-Identität + Standardwert ist der konfigurierte Server. Eigene Werte werden explizit and den Server gesendet und während der Authentifizierung erzwungen + Standardwert ist \"%1$s\". Eigene Werte werden explizit and den Server gesendet und während der Authentifizierung erzwungen + MTU des VPN Tunnel-Device + Falls der Standardwert in einem bestimmten Netzwerk nicht geeignet ist + Server Port + UDP-Port zu dem verbunden wird, falls dieser vom Standard-Port abweicht + Split-Tunneling Blockiere IPv4 Verkehr der nicht für das VPN bestimmt ist Blockiere IPv6 Verkehr der nicht für das VPN bestimmt ist - Bitte geben Sie hier die Server-Adresse ein - Bitte geben Sie hier Ihren Benutzernamen ein + Ein Wert wird benötigt, um die Verbindung aufbauen zu können + Bitte geben Sie Ihren Benutzernamen ein Kein CA-Zertifikat ausgewählt Bitte wählen Sie eines aus oder aktivieren Sie Automatisch wählen Bitte geben Sie eine Nummer von %1$d - %2$d ein @@ -122,8 +132,6 @@ Benutzerauthentifizierung ist fehlgeschlagen. Sicherheitsassessment ist fehlgeschlagen. Unbekannter Fehler während des Verbindens. - Verbinden: %1$s - Verbinde mit \""%1$s\". VPN verbunden Dieses VPN Profil ist momentan verbunden! Neu verbinden diff --git a/src/frontends/android/app/src/main/res/values-pl/strings.xml b/src/frontends/android/app/src/main/res/values-pl/strings.xml index 26387190b..c7aadb994 100644 --- a/src/frontends/android/app/src/main/res/values-pl/strings.xml +++ b/src/frontends/android/app/src/main/res/values-pl/strings.xml @@ -49,36 +49,46 @@ Zapisz Anuluj - Nazwa profilu: - (użyj adresu serwer) - Serwer: - Typ: - Użytkownik: - Hasło: - (w razie potrzeby zapromptuj) - Certyfikat użytkownika: + Nazwa profilu (opcjonalny) + Defaults to the configured server + Defaults to \"%1$s\" + Serwer + IP address or hostname of the VPN server + Typ VPN + Użytkownik + Hasło (opcjonalny) + Leave blank to get prompted on demand + Certyfikat użytkownika Wybierz certyfikat użytkownika >Wybierz określony certyfikat użytkownika - Certyfikat CA: + User identity + Select a certificate first + Default (%1$s) + Certyfikat CA Wybierz automatycznie Wybierz certyfikat CA Wybierz określony certyfikat CA + Advanced settings Show advanced settings - MTU: - Server port: - (use default) - Split tunneling: + Server identity + Defaults to the configured server. Custom values are explicitly sent to the server and enforced during authentication + Defaults to \"%1$s\". Custom values are explicitly sent to the server and enforced during authentication + MTU of the VPN tunnel device + In case the default value is unsuitable for a particular network + Server port + UDP port to connect to, if different from the default + Split tunneling Block IPv4 traffic not destined for the VPN Block IPv6 traffic not destined for the VPN - Wprowadź adres serwer + A value is required to initiate the connection Wprowadź swoją nazwę użytkownika Nie wybrano żadnego certyfikatu CA Wybierz lub uaktywnij jeden Wybierz automatycznie Please enter a number in the range from %1$d - %2$d EAP-TNC may affect your privacy Device data is sent to the server operator - <p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b> + Trusted Network Connect (TNC) allows server operators to assess the health of a client device.

For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.

Any data will be sent only after verifying the server\'s identity.]]>
Certyfikaty CA @@ -122,8 +132,6 @@ Błąd przy autoryzacji użytkownika Security assessment failed. Nieznany błąd w czasie połączenia - Łączenie: %1$s - Tworzenie tunelu VPN z \""%1$s\". Połączenie z VPN Ten profil VPN jest obecnie połaczony! Połączyć ponownie diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml index ec9d66334..c0a6484f8 100644 --- a/src/frontends/android/app/src/main/res/values-ru/strings.xml +++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml @@ -46,29 +46,39 @@ Сохранить Отмена - Название профиля: - (адрес cервер) - Сервер: - Тип: - Логин: - Пароль: - (спросить если нужно) - Сертификат пользователя: + Название профиля (необязательный) + Defaults to the configured server + Defaults to \"%1$s\" + Сервер + IP address or hostname of the VPN server + VPN Тип + Логин + Пароль (необязательный) + Leave blank to get prompted on demand + Сертификат пользователя Выбрать сертификат пользователя Выбрать сертификат пользователя - Сертификат CA: + User identity + Select a certificate first + Default (%1$s) + Сертификат CA Выбрать автоматически Выбрать сертификат CA Выбрать CA сертификат + Advanced settings Show advanced settings - MTU: - Server port: - (use default) - Split tunneling: + Server identity + Defaults to the configured server. Custom values are explicitly sent to the server and enforced during authentication + Defaults to \"%1$s\". Custom values are explicitly sent to the server and enforced during authentication + MTU of the VPN tunnel device + In case the default value is unsuitable for a particular network + Server port + UDP port to connect to, if different from the default + Split tunneling Block IPv4 traffic not destined for the VPN Block IPv6 traffic not destined for the VPN - Пожалуйста введите адрес cервер + A value is required to initiate the connection Пожалуйста введите имя пользователя Не выбран сертификат CA Пожалуйста выберите один Выбрать автоматически @@ -119,8 +129,6 @@ Ошибка авторизации пользователя. Security assessment failed. Неизвестная ошибка. - Подключение: %1$s - Подключение к VPN с \""%1$s\". Соединение с VPN установлено Подключение к этому профилю VPN уже существует! Переподключить @@ -129,4 +137,3 @@ Соединить - diff --git a/src/frontends/android/app/src/main/res/values-ua/strings.xml b/src/frontends/android/app/src/main/res/values-ua/strings.xml index a5e668b84..a852dfdba 100644 --- a/src/frontends/android/app/src/main/res/values-ua/strings.xml +++ b/src/frontends/android/app/src/main/res/values-ua/strings.xml @@ -47,30 +47,40 @@ Зберегти Відміна - Назва профілю: - (використовувати адресу cервер) - Сервер: - Тип: - Логін: - Пароль: - (запитати якщо потрібно) - Сертифікат користувача: + Назва профілю (необов\'язковий) + Defaults to the configured server + Defaults to \"%1$s\" + Сервер + IP address or hostname of the VPN server + VPN Тип + Логін + Пароль (необов\'язковий) + Leave blank to get prompted on demand + Сертифікат користувача Виберіть сертифікат користувача Вибрати спеціальний сертифікат користувача - Сертифікат CA: + User identity + Select a certificate first + Default (%1$s) + Сертифікат CA Вибрати автоматично Вибрати сертифікат CA Вибрати спеціальний сертифікат CA + Advanced settings Show advanced settings - MTU: - Server port: - (use default) - Split tunneling: + Server identity + Defaults to the configured server. Custom values are explicitly sent to the server and enforced during authentication + Defaults to \"%1$s\". Custom values are explicitly sent to the server and enforced during authentication + MTU of the VPN tunnel device + In case the default value is unsuitable for a particular network + Server port + UDP port to connect to, if different from the default + Split tunneling Block IPv4 traffic not destined for the VPN Block IPv6 traffic not destined for the VPN - Введіть адресу cервер тут - Введіть ім\'я користувача тут + A value is required to initiate the connection + Введіть ім\'я користувача Не вибрано сертифікат CA Будь ласка виберіть один Вибрати автоматично Please enter a number in the range from %1$d - %2$d @@ -120,8 +130,6 @@ Помилка аутентифікації користувача. Security assessment failed. Невідома помилка під час підключення. - Підключення: %1$s - Підключення VPN з \""%1$s\". VPN підключено Цей VPN профіль зараз підключений! Перепідключитися @@ -130,4 +138,3 @@ Підключити - diff --git a/src/frontends/android/app/src/main/res/values/attrs.xml b/src/frontends/android/app/src/main/res/values/attrs.xml index 6c3480b0e..592555c8c 100644 --- a/src/frontends/android/app/src/main/res/values/attrs.xml +++ b/src/frontends/android/app/src/main/res/values/attrs.xml @@ -1,7 +1,7 @@ Save Cancel - Profile Name: - (use server address) - Server: - Type: - Username: - Password: - (prompt when needed) - User certificate: + Profile name (optional) + Defaults to the configured server + Defaults to \"%1$s\" + Server + IP address or hostname of the VPN server + VPN Type + Username + Password (optional) + Leave blank to get prompted on demand + User certificate Select user certificate Select a specific user certificate - CA certificate: + User identity + Select a certificate first + Default (%1$s) + CA certificate Select automatically Select CA certificate Select a specific CA certificate + Advanced settings Show advanced settings - MTU: - Server port: - (use default) - Split tunneling: + Server identity + Defaults to the configured server. Custom values are explicitly sent to the server and enforced during authentication + Defaults to \"%1$s\". Custom values are explicitly sent to the server and enforced during authentication + MTU of the VPN tunnel device + In case the default value is unsuitable for a particular network + Server port + UDP port to connect to, if different from the default + Split tunneling Block IPv4 traffic not destined for the VPN Block IPv6 traffic not destined for the VPN - Please enter the server address here - Please enter your username here + A value is required to initiate the connection + Please enter your username No CA certificate selected Please select one or activate Select automatically Please enter a number in the range from %1$d - %2$d @@ -122,8 +132,6 @@ User authentication failed. Security assessment failed. Unspecified failure while connecting. - Connecting: %1$s - Establishing VPN with \""%1$s\". VPN connected This VPN profile is currently connected! Reconnect