aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/frontends/android/app/build.gradle4
-rw-r--r--src/frontends/android/app/src/main/AndroidManifest.xml31
-rw-r--r--src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java25
-rw-r--r--src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java44
-rw-r--r--src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java33
-rw-r--r--src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java811
-rw-r--r--src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java86
-rw-r--r--src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java40
-rw-r--r--src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.pngbin0 -> 221 bytes
-rw-r--r--src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.pngbin0 -> 175 bytes
-rw-r--r--src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.pngbin0 -> 257 bytes
-rw-r--r--src/frontends/android/app/src/main/res/layout/profile_import_view.xml196
-rw-r--r--src/frontends/android/app/src/main/res/menu/profile_import.xml24
-rw-r--r--src/frontends/android/app/src/main/res/values-de/strings.xml11
-rw-r--r--src/frontends/android/app/src/main/res/values-pl/strings.xml11
-rw-r--r--src/frontends/android/app/src/main/res/values-ru/strings.xml11
-rw-r--r--src/frontends/android/app/src/main/res/values-ua/strings.xml11
-rw-r--r--src/frontends/android/app/src/main/res/values/strings.xml11
18 files changed, 1298 insertions, 51 deletions
diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle
index 450a12fbd..af1737299 100644
--- a/src/frontends/android/app/build.gradle
+++ b/src/frontends/android/app/build.gradle
@@ -8,8 +8,8 @@ android {
applicationId "org.strongswan.android"
minSdkVersion 15
targetSdkVersion 22
- versionCode 36
- versionName "1.7.2"
+ versionCode 40
+ versionName "1.8.0"
}
sourceSets.main {
diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml
index da465ba74..bc2de9af7 100644
--- a/src/frontends/android/app/src/main/AndroidManifest.xml
+++ b/src/frontends/android/app/src/main/AndroidManifest.xml
@@ -20,6 +20,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".logic.StrongSwanApplication"
@@ -64,6 +65,36 @@
</intent-filter>
</activity>
<activity
+ android:name=".ui.VpnProfileImportActivity"
+ android:label="@string/profile_import"
+ android:taskAffinity=""
+ android:excludeFromRecents="true" >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:scheme="file" />
+ <data android:scheme="content" />
+ <data android:mimeType="application/vnd.strongswan.profile" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:scheme="file" />
+ <data android:scheme="content" />
+ <data android:host="*" />
+ <data android:pathPattern=".*\\..*\\..*\\..*\\.sswan" />
+ <data android:pathPattern=".*\\..*\\..*\\.sswan" />
+ <data android:pathPattern=".*\\..*\\.sswan" />
+ <data android:pathPattern=".*\\.sswan" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".ui.TrustedCertificateImportActivity"
android:label="@string/import_certificate"
android:theme="@style/AlertDialogTheme" >
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 8a9d319b5..54bdfcbdd 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
@@ -18,6 +18,8 @@
package org.strongswan.android.data;
+import java.util.UUID;
+
public class VpnProfile implements Cloneable
{
/* While storing this as EnumSet would be nicer this simplifies storing it in a database */
@@ -28,8 +30,14 @@ public class VpnProfile implements Cloneable
private String mRemoteId, mLocalId;
private Integer mMTU, mPort, mSplitTunneling;
private VpnType mVpnType;
+ private UUID mUUID;
private long mId = -1;
+ public VpnProfile()
+ {
+ this.mUUID = UUID.randomUUID();
+ }
+
public long getId()
{
return mId;
@@ -40,6 +48,16 @@ public class VpnProfile implements Cloneable
this.mId = id;
}
+ public void setUUID(UUID uuid)
+ {
+ this.mUUID = uuid;
+ }
+
+ public UUID getUUID()
+ {
+ return mUUID;
+ }
+
public String getName()
{
return mName;
@@ -171,7 +189,12 @@ public class VpnProfile implements Cloneable
{
if (o != null && o instanceof VpnProfile)
{
- return this.mId == ((VpnProfile)o).getId();
+ VpnProfile other = (VpnProfile)o;
+ if (this.mUUID != null && other.getUUID() != null)
+ {
+ return this.mUUID.equals(other.getUUID());
+ }
+ return this.mId == other.getId();
}
return false;
}
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 17026536b..1c509a302 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
@@ -19,6 +19,7 @@ package org.strongswan.android.data;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
import android.content.ContentValues;
import android.content.Context;
@@ -33,6 +34,7 @@ public class VpnProfileDataSource
{
private static final String TAG = VpnProfileDataSource.class.getSimpleName();
public static final String KEY_ID = "_id";
+ public static final String KEY_UUID = "_uuid";
public static final String KEY_NAME = "name";
public static final String KEY_GATEWAY = "gateway";
public static final String KEY_VPN_TYPE = "vpn_type";
@@ -53,11 +55,12 @@ public class VpnProfileDataSource
private static final String DATABASE_NAME = "strongswan.db";
private static final String TABLE_VPNPROFILE = "vpnprofile";
- private static final int DATABASE_VERSION = 8;
+ private static final int DATABASE_VERSION = 9;
public static final String DATABASE_CREATE =
"CREATE TABLE " + TABLE_VPNPROFILE + " (" +
KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ KEY_UUID + " TEXT UNIQUE," +
KEY_NAME + " TEXT NOT NULL," +
KEY_GATEWAY + " TEXT NOT NULL," +
KEY_VPN_TYPE + " TEXT NOT NULL," +
@@ -73,6 +76,7 @@ public class VpnProfileDataSource
");";
private static final String[] ALL_COLUMNS = new String[] {
KEY_ID,
+ KEY_UUID,
KEY_NAME,
KEY_GATEWAY,
KEY_VPN_TYPE,
@@ -141,6 +145,12 @@ public class VpnProfileDataSource
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_REMOTE_ID +
" TEXT;");
}
+ if (oldVersion < 9)
+ {
+ db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_UUID +
+ " TEXT;");
+ updateColumns(db);
+ }
}
private void updateColumns(SQLiteDatabase db)
@@ -262,6 +272,24 @@ public class VpnProfileDataSource
}
/**
+ * Get a single VPN profile from the database by its UUID.
+ * @param uuid the UUID of the VPN profile
+ * @return the profile or null, if not found
+ */
+ public VpnProfile getVpnProfile(UUID uuid)
+ {
+ VpnProfile profile = null;
+ Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS,
+ KEY_UUID + "='" + uuid.toString() + "'", null, null, null, null);
+ if (cursor.moveToFirst())
+ {
+ profile = VpnProfileFromCursor(cursor);
+ }
+ cursor.close();
+ return profile;
+ }
+
+ /**
* Get a list of all VPN profiles stored in the database.
* @return list of VPN profiles
*/
@@ -285,6 +313,7 @@ public class VpnProfileDataSource
{
VpnProfile profile = new VpnProfile();
profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID)));
+ profile.setUUID(getUUID(cursor, cursor.getColumnIndex(KEY_UUID)));
profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME)));
profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY)));
profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndex(KEY_VPN_TYPE))));
@@ -303,6 +332,7 @@ public class VpnProfileDataSource
private ContentValues ContentValuesFromVpnProfile(VpnProfile profile)
{
ContentValues values = new ContentValues();
+ values.put(KEY_UUID, profile.getUUID() != null ? profile.getUUID().toString() : null);
values.put(KEY_NAME, profile.getName());
values.put(KEY_GATEWAY, profile.getGateway());
values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier());
@@ -322,4 +352,16 @@ public class VpnProfileDataSource
{
return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex);
}
+
+ private UUID getUUID(Cursor cursor, int columnIndex)
+ {
+ try
+ {
+ return cursor.isNull(columnIndex) ? null : UUID.fromString(cursor.getString(columnIndex));
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+ }
}
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 30fb101be..bf64370cf 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
@@ -26,6 +26,7 @@ import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
+import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDialogFragment;
@@ -63,14 +64,14 @@ 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 org.strongswan.android.utils.Constants;
import java.security.cert.X509Certificate;
+import java.util.UUID;
public class VpnProfileDetailActivity extends AppCompatActivity
{
private static final int SELECT_TRUSTED_CERTIFICATE = 0;
- private static final int MTU_MIN = 1280;
- private static final int MTU_MAX = 1500;
private VpnProfileDataSource mDataSource;
private Long mId;
@@ -453,6 +454,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity
if (mProfile != null)
{
updateProfileData();
+ if (mProfile.getUUID() == null)
+ {
+ mProfile.setUUID(UUID.randomUUID());
+ }
mDataSource.updateVpnProfile(mProfile);
}
else
@@ -461,6 +466,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity
updateProfileData();
mDataSource.insertProfile(mProfile);
}
+ Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
+ intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+
setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
finish();
}
@@ -496,9 +505,9 @@ public class VpnProfileDetailActivity extends AppCompatActivity
showCertificateAlert();
valid = false;
}
- if (!validateInteger(mMTU, MTU_MIN, MTU_MAX))
+ if (!validateInteger(mMTU, Constants.MTU_MIN, Constants.MTU_MAX))
{
- mMTUWrap.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), Constants.MTU_MIN, Constants.MTU_MAX));
valid = false;
}
if (!validateInteger(mPort, 1, 65535))
@@ -567,8 +576,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity
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);
+ mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0);
+ mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0);
useralias = mProfile.getUserCertificateAlias();
local_id = mProfile.getLocalId();
alias = mProfile.getCertificateAlias();
@@ -686,11 +695,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
}
});
}
- catch (KeyChainException e)
- {
- e.printStackTrace();
- }
- catch (InterruptedException e)
+ catch (KeyChainException | InterruptedException e)
{
e.printStackTrace();
}
@@ -722,11 +727,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
{
chain = KeyChain.getCertificateChain(mContext, mAlias);
}
- catch (KeyChainException e)
- {
- e.printStackTrace();
- }
- catch (InterruptedException e)
+ catch (KeyChainException | InterruptedException e)
{
e.printStackTrace();
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java
new file mode 100644
index 000000000..1b6b6e896
--- /dev/null
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java
@@ -0,0 +1,811 @@
+/*
+ * 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 <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.app.ProgressDialog;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.security.KeyChainException;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Base64;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+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.widget.TextInputLayoutHelper;
+import org.strongswan.android.utils.Constants;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.UUID;
+
+import javax.net.ssl.SSLHandshakeException;
+
+public class VpnProfileImportActivity extends AppCompatActivity
+{
+ private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED";
+ private static final int INSTALL_PKCS12 = 0;
+ private static final int PROFILE_LOADER = 0;
+ private static final int USER_CERT_LOADER = 1;
+
+ private VpnProfileDataSource mDataSource;
+ private ParsedVpnProfile mProfile;
+ private VpnProfile mExisting;
+ private TrustedCertificateEntry mCertEntry;
+ private TrustedCertificateEntry mUserCertEntry;
+ private String mUserCertLoading;
+ private boolean mHideImport;
+ private ProgressDialog mProgress;
+ private TextView mExistsWarning;
+ private ViewGroup mBasicDataGroup;
+ private TextView mName;
+ private TextView mGateway;
+ private TextView mSelectVpnType;
+ private ViewGroup mUsernamePassword;
+ private EditText mUsername;
+ private TextInputLayoutHelper mUsernameWrap;
+ private EditText mPassword;
+ private ViewGroup mUserCertificate;
+ private RelativeLayout mSelectUserCert;
+ private Button mImportUserCert;
+ private ViewGroup mRemoteCertificate;
+ private RelativeLayout mRemoteCert;
+
+ private LoaderManager.LoaderCallbacks<ProfileLoadResult> mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks<ProfileLoadResult>()
+ {
+ @Override
+ public Loader<ProfileLoadResult> onCreateLoader(int id, Bundle args)
+ {
+ return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ProfileLoadResult> loader, ProfileLoadResult data)
+ {
+ handleProfile(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ProfileLoadResult> loader)
+ {
+
+ }
+ };
+
+ private LoaderManager.LoaderCallbacks<TrustedCertificateEntry> mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks<TrustedCertificateEntry>()
+ {
+ @Override
+ public Loader<TrustedCertificateEntry> onCreateLoader(int id, Bundle args)
+ {
+ return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<TrustedCertificateEntry> loader, TrustedCertificateEntry data)
+ {
+ handleUserCertificate(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<TrustedCertificateEntry> loader)
+ {
+
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mDataSource = new VpnProfileDataSource(this);
+ mDataSource.open();
+
+ setContentView(R.layout.profile_import_view);
+
+ mExistsWarning = (TextView)findViewById(R.id.exists_warning);
+ mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group);
+ mName = (TextView)findViewById(R.id.name);
+ mGateway = (TextView)findViewById(R.id.gateway);
+ mSelectVpnType = (TextView)findViewById(R.id.vpn_type);
+
+ 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);
+ mImportUserCert = (Button)findViewById(R.id.import_user_certificate);
+
+ mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group);
+ mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate);
+
+ mExistsWarning.setVisibility(View.GONE);
+ mBasicDataGroup.setVisibility(View.GONE);
+ mUsernamePassword.setVisibility(View.GONE);
+ mUserCertificate.setVisibility(View.GONE);
+ mRemoteCertificate.setVisibility(View.GONE);
+
+ mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
+ mImportUserCert.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v)
+ {
+ Intent intent = KeyChain.createInstallIntent();
+ intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName()));
+ intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12);
+ startActivityForResult(intent, INSTALL_PKCS12);
+ }
+ });
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (Intent.ACTION_VIEW.equals(action))
+ {
+ mProgress = ProgressDialog.show(this, null, getString(R.string.loading),
+ true, true, new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog)
+ {
+ finish();
+ }
+ });
+
+ getLoaderManager().initLoader(PROFILE_LOADER, null, mProfileLoaderCallbacks);
+ }
+
+ if (savedInstanceState != null)
+ {
+ mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
+ if (mUserCertLoading != null)
+ {
+ getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
+ }
+ mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED));
+ }
+ }
+
+ @Override
+ protected void onDestroy()
+ {
+ super.onDestroy();
+ mDataSource.close();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState)
+ {
+ super.onSaveInstanceState(outState);
+ if (mUserCertEntry != null)
+ {
+ outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
+ }
+ outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.profile_import, menu);
+ if (mHideImport)
+ {
+ MenuItem item = menu.findItem(R.id.menu_accept);
+ item.setVisible(false);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case android.R.id.home:
+ finish();
+ return true;
+ case R.id.menu_accept:
+ saveProfile();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data)
+ {
+ super.onActivityResult(requestCode, resultCode, data);
+ switch (requestCode)
+ {
+ case INSTALL_PKCS12:
+ if (resultCode == Activity.RESULT_OK)
+ { /* no need to import twice */
+ mImportUserCert.setEnabled(false);
+ mSelectUserCert.performClick();
+ }
+ }
+ }
+
+ public void handleProfile(ProfileLoadResult data)
+ {
+ mProgress.dismiss();
+
+ mProfile = null;
+ if (data != null && data.ThrownException == null)
+ {
+ try
+ {
+ JSONObject obj = new JSONObject(data.Profile);
+ mProfile = parseProfile(obj);
+ }
+ catch (JSONException e)
+ {
+ mExistsWarning.setVisibility(View.VISIBLE);
+ mExistsWarning.setText(e.getLocalizedMessage());
+ mHideImport = true;
+ invalidateOptionsMenu();
+ return;
+ }
+ }
+ if (mProfile == null)
+ {
+ String error = null;
+ if (data.ThrownException != null)
+ {
+ try
+ {
+ throw data.ThrownException;
+ }
+ catch (FileNotFoundException e)
+ {
+ error = getString(R.string.profile_import_failed_not_found);
+ }
+ catch (UnknownHostException e)
+ {
+ error = getString(R.string.profile_import_failed_host);
+ }
+ catch (SSLHandshakeException e)
+ {
+ error = getString(R.string.profile_import_failed_tls);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ if (error != null)
+ {
+ Toast.makeText(this, getString(R.string.profile_import_failed_detail, error), Toast.LENGTH_LONG).show();
+ }
+ else
+ {
+ Toast.makeText(this, R.string.profile_import_failed, Toast.LENGTH_LONG).show();
+ }
+ finish();
+ return;
+ }
+ mExisting = mDataSource.getVpnProfile(mProfile.getUUID());
+ mExistsWarning.setVisibility(mExisting != null ? View.VISIBLE : View.GONE);
+
+ mBasicDataGroup.setVisibility(View.VISIBLE);
+ mName.setText(mProfile.getName());
+ mGateway.setText(mProfile.getGateway());
+ mSelectVpnType.setText(getResources().getStringArray(R.array.vpn_types)[mProfile.getVpnType().ordinal()]);
+
+ mUsernamePassword.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
+ if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
+ {
+ mUsername.setText(mProfile.getUsername());
+ if (mProfile.getUsername() != null && !mProfile.getUsername().isEmpty())
+ {
+ mUsername.setEnabled(false);
+ }
+ }
+
+ mUserCertificate.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
+ mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE);
+ mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE);
+
+ updateUserCertView();
+
+ if (mProfile.Certificate != null)
+ {
+ try
+ {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(mProfile.Certificate));
+ KeyStore store = KeyStore.getInstance("LocalCertificateStore");
+ store.load(null, null);
+ String alias = store.getCertificateAlias(certificate);
+ mCertEntry = new TrustedCertificateEntry(alias, certificate);
+ ((TextView)mRemoteCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
+ ((TextView)mRemoteCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
+ }
+ catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e)
+ {
+ e.printStackTrace();
+ mRemoteCertificate.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void handleUserCertificate(TrustedCertificateEntry data)
+ {
+ mUserCertEntry = data;
+ mUserCertLoading = null;
+ updateUserCertView();
+ }
+
+ private void updateUserCertView()
+ {
+ if (mUserCertLoading != null)
+ {
+ ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
+ ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
+ }
+ else if (mUserCertEntry != null)
+ { /* clear any errors and set the new data */
+ ((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());
+ }
+ 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);
+ }
+ }
+
+ private ParsedVpnProfile parseProfile(JSONObject obj) throws JSONException
+ {
+ UUID uuid;
+ try
+ {
+ uuid = UUID.fromString(obj.getString("uuid"));
+ }
+ catch (IllegalArgumentException e)
+ {
+ e.printStackTrace();
+ return null;
+ }
+ ParsedVpnProfile profile = new ParsedVpnProfile();
+
+ profile.setUUID(uuid);
+ profile.setName(obj.getString("name"));
+ VpnType type = VpnType.fromIdentifier(obj.getString("type"));
+ profile.setVpnType(type);
+
+ JSONObject remote = obj.getJSONObject("remote");
+ profile.setGateway(remote.getString("addr"));
+ profile.setPort(getInteger(remote, "port", 1, 65535));
+ profile.setRemoteId(remote.optString("id", null));
+ profile.Certificate = decodeBase64(remote.optString("cert", null));
+
+ JSONObject local = obj.optJSONObject("local");
+ if (local != null)
+ {
+ if (type.has(VpnTypeFeature.USER_PASS))
+ {
+ profile.setUsername(local.optString("eap_id", null));
+ }
+
+ if (type.has(VpnTypeFeature.CERTIFICATE))
+ {
+ profile.setLocalId(local.optString("id", null));
+ profile.PKCS12 = decodeBase64(local.optString("p12", null));
+ }
+ }
+
+ profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX));
+ JSONObject split = obj.optJSONObject("split-tunneling");
+ if (split != null)
+ {
+ int st = 0;
+ st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
+ st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
+ profile.setSplitTunneling(st == 0 ? null : st);
+ }
+ return profile;
+ }
+
+ private Integer getInteger(JSONObject obj, String key, int min, int max)
+ {
+ Integer res = obj.optInt(key);
+ return res < min || res > max ? null : res;
+ }
+
+ /**
+ * Save or update the profile depending on whether we actually have a
+ * profile object or not (this was created in updateProfileData)
+ */
+ private void saveProfile()
+ {
+ if (verifyInput())
+ {
+ updateProfileData();
+ if (mExisting != null)
+ {
+ mProfile.setId(mExisting.getId());
+ mDataSource.updateVpnProfile(mProfile);
+ }
+ else
+ {
+ mDataSource.insertProfile(mProfile);
+ }
+ if (mCertEntry != null)
+ {
+ try
+ { /* store the CA/server certificate */
+ KeyStore store = KeyStore.getInstance("LocalCertificateStore");
+ store.load(null, null);
+ store.setCertificateEntry(null, mCertEntry.getCertificate());
+ TrustedCertificateManager.getInstance().reset();
+ }
+ catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
+ intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+
+ intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+
+ setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
+ finish();
+ }
+ }
+
+ /**
+ * Verify the user input and display error messages.
+ * @return true if the input is valid
+ */
+ private boolean verifyInput()
+ {
+ boolean valid = true;
+ if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
+ {
+ if (mUsername.getText().toString().trim().isEmpty())
+ {
+ mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
+ valid = false;
+ }
+ }
+ if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
+ { /* let's show an error icon */
+ ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
+ valid = false;
+ }
+ return valid;
+ }
+
+ /**
+ * Update the profile object with the data entered by the user
+ */
+ private void updateProfileData()
+ {
+ if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
+ {
+ mProfile.setUsername(mUsername.getText().toString().trim());
+ String password = mPassword.getText().toString().trim();
+ password = password.isEmpty() ? null : password;
+ mProfile.setPassword(password);
+ }
+ if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
+ {
+ mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
+ }
+ if (mCertEntry != null)
+ {
+ mProfile.setCertificateAlias(mCertEntry.getAlias());
+ }
+ }
+
+ /**
+ * Load the JSON-encoded VPN profile from the given URI
+ */
+ private static class ProfileLoader extends AsyncTaskLoader<ProfileLoadResult>
+ {
+ private final Uri mUri;
+ private ProfileLoadResult mData;
+
+ public ProfileLoader(Context context, Uri uri)
+ {
+ super(context);
+ mUri = uri;
+ }
+
+ @Override
+ public ProfileLoadResult loadInBackground()
+ {
+ ProfileLoadResult result = new ProfileLoadResult();
+ InputStream in = null;
+
+ if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) ||
+ ContentResolver.SCHEME_FILE.equals(mUri.getScheme()))
+ {
+ try
+ {
+ in = getContext().getContentResolver().openInputStream(mUri);
+ }
+ catch (FileNotFoundException e)
+ {
+ result.ThrownException = e;
+ }
+ }
+ else
+ {
+ try
+ {
+ URL url = new URL(mUri.toString());
+ in = url.openStream();
+ }
+ catch (IOException e)
+ {
+ result.ThrownException = e;
+ }
+ }
+ if (in != null)
+ {
+ result.Profile = streamToString(in);
+ }
+ return result;
+ }
+
+ @Override
+ protected void onStartLoading()
+ {
+ if (mData != null)
+ { /* if we have data ready, deliver it directly */
+ deliverResult(mData);
+ }
+ if (takeContentChanged() || mData == null)
+ {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void deliverResult(ProfileLoadResult data)
+ {
+ if (isReset())
+ {
+ return;
+ }
+ mData = data;
+ if (isStarted())
+ { /* if it is started we deliver the data directly,
+ * otherwise this is handled in onStartLoading */
+ super.deliverResult(data);
+ }
+ }
+
+ @Override
+ protected void onReset()
+ {
+ mData = null;
+ super.onReset();
+ }
+
+ private String streamToString(InputStream in)
+ {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buf = new byte[1024];
+ int len;
+
+ try
+ {
+ while ((len = in.read(buf)) != -1)
+ {
+ out.write(buf, 0, len);
+ }
+ return out.toString("UTF-8");
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ return null;
+ }
+ }
+
+ private static class ProfileLoadResult
+ {
+ public String Profile;
+ public Exception ThrownException;
+ }
+
+ /**
+ * Ask the user to select an available certificate.
+ */
+ private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback
+ {
+ @Override
+ public void onClick(View v)
+ {
+ String alias = null;
+ if (mUserCertEntry != null)
+ {
+ alias = mUserCertEntry.getAlias();
+ mUserCertEntry = null;
+ }
+ else if (mProfile != null)
+ {
+ alias = getString(R.string.profile_cert_alias, mProfile.getName());
+ }
+ KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias);
+ }
+
+ @Override
+ public void alias(final String alias)
+ {
+ /* alias() is not called from our main thread */
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run()
+ {
+ mUserCertLoading = alias;
+ updateUserCertView();
+ if (alias != null)
+ { /* otherwise the dialog was canceled, the request denied */
+ getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Load the selected user certificate asynchronously. This cannot be done
+ * from the main thread as getCertificateChain() calls back to our main
+ * thread to bind to the KeyChain service resulting in a deadlock.
+ */
+ private static class UserCertificateLoader extends AsyncTaskLoader<TrustedCertificateEntry>
+ {
+ private final String mAlias;
+ private TrustedCertificateEntry mData;
+
+ public UserCertificateLoader(Context context, String alias)
+ {
+ super(context);
+ mAlias = alias;
+ }
+
+ @Override
+ public TrustedCertificateEntry loadInBackground()
+ {
+ X509Certificate[] chain = null;
+ try
+ {
+ chain = KeyChain.getCertificateChain(getContext(), mAlias);
+ }
+ catch (KeyChainException | InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ if (chain != null && chain.length > 0)
+ {
+ return new TrustedCertificateEntry(mAlias, chain[0]);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onStartLoading()
+ {
+ if (mData != null)
+ { /* if we have data ready, deliver it directly */
+ deliverResult(mData);
+ }
+ if (takeContentChanged() || mData == null)
+ {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void deliverResult(TrustedCertificateEntry data)
+ {
+ if (isReset())
+ {
+ return;
+ }
+ mData = data;
+ if (isStarted())
+ { /* if it is started we deliver the data directly,
+ * otherwise this is handled in onStartLoading */
+ super.deliverResult(data);
+ }
+ }
+
+ @Override
+ protected void onReset()
+ {
+ mData = null;
+ super.onReset();
+ }
+ }
+
+ private byte[] decodeBase64(String encoded)
+ {
+ if (encoded == null || encoded.isEmpty())
+ {
+ return null;
+ }
+ byte[] data = null;
+ try
+ {
+ data = Base64.decode(encoded, Base64.DEFAULT);
+ }
+ catch (IllegalArgumentException e)
+ {
+ e.printStackTrace();
+ }
+ return data;
+ }
+
+ private class ParsedVpnProfile extends VpnProfile
+ {
+ public byte[] Certificate;
+ public byte[] PKCS12;
+ }
+}
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 d8d99ff00..8210d597c 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,12 +17,14 @@
package org.strongswan.android.ui;
-import android.app.Activity;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.support.v4.app.Fragment;
+import android.support.v4.content.LocalBroadcastManager;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.LayoutInflater;
@@ -41,9 +43,11 @@ 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 org.strongswan.android.utils.Constants;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
public class VpnProfileListFragment extends Fragment
@@ -58,11 +62,47 @@ public class VpnProfileListFragment extends Fragment
private OnVpnProfileSelectedListener mListener;
private boolean mReadOnly;
+ private BroadcastReceiver mProfilesChanged = new BroadcastReceiver()
+ {
+ @Override
+ public void onReceive(Context context, Intent intent)
+ {
+ long id, ids[];
+ if ((id = intent.getLongExtra(Constants.VPN_PROFILES_SINGLE, 0)) > 0)
+ {
+ VpnProfile profile = mDataSource.getVpnProfile(id);
+ if (profile != null)
+ { /* in case this was an edit, we remove it first */
+ mVpnProfiles.remove(profile);
+ mVpnProfiles.add(profile);
+ mListAdapter.notifyDataSetChanged();
+ }
+ }
+ else if ((ids = intent.getLongArrayExtra(Constants.VPN_PROFILES_MULTIPLE)) != null)
+ {
+ for (long i : ids)
+ {
+ Iterator<VpnProfile> profiles = mVpnProfiles.iterator();
+ while (profiles.hasNext())
+ {
+ VpnProfile profile = profiles.next();
+ if (profile.getId() == i)
+ {
+ profiles.remove();
+ break;
+ }
+ }
+ }
+ mListAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
/**
* The activity containing this fragment should implement this interface
*/
public interface OnVpnProfileSelectedListener {
- public void onVpnProfileSelected(VpnProfile profile);
+ void onVpnProfileSelected(VpnProfile profile);
}
@Override
@@ -116,6 +156,9 @@ public class VpnProfileListFragment extends Fragment
mVpnProfiles = mDataSource.getAllVpnProfiles();
mListAdapter = new VpnProfileAdapter(getActivity(), R.layout.profile_list_item, mVpnProfiles);
+
+ IntentFilter profileChangesFilter = new IntentFilter(Constants.VPN_PROFILES_CHANGED);
+ LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mProfilesChanged, profileChangesFilter);
}
@Override
@@ -123,6 +166,7 @@ public class VpnProfileListFragment extends Fragment
{
super.onDestroy();
mDataSource.close();
+ LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mProfilesChanged);
}
@Override
@@ -157,30 +201,6 @@ public class VpnProfileListFragment extends Fragment
}
}
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data)
- {
- switch (requestCode)
- {
- case ADD_REQUEST:
- case EDIT_REQUEST:
- if (resultCode != Activity.RESULT_OK)
- {
- return;
- }
- long id = data.getLongExtra(VpnProfileDataSource.KEY_ID, 0);
- VpnProfile profile = mDataSource.getVpnProfile(id);
- if (profile != null)
- { /* in case this was an edit, we remove it first */
- mVpnProfiles.remove(profile);
- mVpnProfiles.add(profile);
- mListAdapter.notifyDataSetChanged();
- }
- return;
- }
- super.onActivityResult(requestCode, resultCode, data);
- }
-
private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> a, View v, int position, long id)
@@ -213,7 +233,7 @@ public class VpnProfileListFragment extends Fragment
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.profile_list_context, menu);
mEditProfile = menu.findItem(R.id.edit_profile);
- mSelected = new HashSet<Integer>();
+ mSelected = new HashSet<>();
mode.setTitle(R.string.select_profiles);
return true;
}
@@ -234,17 +254,21 @@ public class VpnProfileListFragment extends Fragment
}
case R.id.delete_profile:
{
- ArrayList<VpnProfile> profiles = new ArrayList<VpnProfile>();
+ ArrayList<VpnProfile> profiles = new ArrayList<>();
for (int position : mSelected)
{
profiles.add((VpnProfile)mListView.getItemAtPosition(position));
}
- for (VpnProfile profile : profiles)
+ long ids[] = new long[profiles.size()];
+ for (int i = 0; i < profiles.size(); i++)
{
+ VpnProfile profile = profiles.get(i);
+ ids[i] = profile.getId();
mDataSource.deleteVpnProfile(profile);
- mVpnProfiles.remove(profile);
}
- mListAdapter.notifyDataSetChanged();
+ Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
+ intent.putExtra(Constants.VPN_PROFILES_MULTIPLE, ids);
+ LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent);
Toast.makeText(VpnProfileListFragment.this.getActivity(),
R.string.profiles_deleted, Toast.LENGTH_SHORT).show();
break;
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java
new file mode 100644
index 000000000..413ecae97
--- /dev/null
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 Tobias Brunner
+ * 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 <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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.utils;
+
+public final class Constants
+{
+ /**
+ * Intent action used to notify about changes to the VPN profiles
+ */
+ public static final String VPN_PROFILES_CHANGED = "org.strongswan.android.VPN_PROFILES_CHANGED";
+
+ /**
+ * Used in the intent above to notify about edits or inserts of a VPN profile (long)
+ */
+ public static final String VPN_PROFILES_SINGLE = "org.strongswan.android.VPN_PROFILES_SINGLE";
+
+ /**
+ * Used in the intent above to notify about the deletion of multiple VPN profiles (array of longs)
+ */
+ public static final String VPN_PROFILES_MULTIPLE = "org.strongswan.android.VPN_PROFILES_MULTIPLE";
+
+ /**
+ * Limits for MTU
+ */
+ public static final int MTU_MAX = 1500;
+ public static final int MTU_MIN = 1280;
+}
diff --git a/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..ceb1a1eeb
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..af7f8288d
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..b7c7ffd0e
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/src/frontends/android/app/src/main/res/layout/profile_import_view.xml b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml
new file mode 100644
index 000000000..fc06aa5d4
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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 <http://www.fsf.org/copyleft/gpl.txt>.
+
+ 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.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="10dp"
+ android:animateLayoutChanges="true" >
+
+ <TextView
+ android:id="@+id/exists_warning"
+ android:background="@drawable/state_background"
+ android:padding="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:drawableLeft="@android:drawable/ic_dialog_alert"
+ android:drawableStart="@android:drawable/ic_dialog_alert"
+ android:drawablePadding="8dp"
+ android:textStyle="bold"
+ android:text="@string/profile_import_exists"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <LinearLayout
+ android:id="@+id/basic_data_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:textSize="12sp"
+ android:text="@string/profile_name_label_simple" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:textSize="12sp"
+ android:text="@string/profile_gateway_label" />
+
+ <TextView
+ android:id="@+id/gateway"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:textSize="12sp"
+ android:text="@string/profile_vpn_type_label" />
+
+ <TextView
+ android:id="@+id/vpn_type"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/username_password_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="6dp">
+
+ <org.strongswan.android.ui.widget.TextInputLayoutHelper
+ android:id="@+id/username_wrap"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <android.support.design.widget.TextInputEditText
+ android:id="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions"
+ android:hint="@string/profile_username_label" />
+
+ </org.strongswan.android.ui.widget.TextInputLayoutHelper>
+
+ <org.strongswan.android.ui.widget.TextInputLayoutHelper
+ android:id="@+id/password_wrap"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ app:helper_text="@string/profile_password_hint" >
+
+ <android.support.design.widget.TextInputEditText
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:inputType="textPassword|textNoSuggestions"
+ android:hint="@string/profile_password_label" />
+
+ </org.strongswan.android.ui.widget.TextInputLayoutHelper>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/user_certificate_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginLeft="4dp"
+ android:textSize="12sp"
+ android:text="@string/profile_user_certificate_label" />
+
+ <include
+ android:id="@+id/select_user_certificate"
+ layout="@layout/two_line_button" />
+
+ <Button
+ android:id="@+id/import_user_certificate"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:text="@string/profile_cert_import" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/remote_certificate_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="12sp"
+ android:text="@string/profile_ca_label" />
+
+ <include
+ android:id="@+id/remote_certificate"
+ layout="@layout/two_line_button" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/src/frontends/android/app/src/main/res/menu/profile_import.xml b/src/frontends/android/app/src/main/res/menu/profile_import.xml
new file mode 100644
index 000000000..99893f7b0
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/menu/profile_import.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 Tobias Brunner
+ 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 <http://www.fsf.org/copyleft/gpl.txt>.
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_accept"
+ android:title="@string/profile_edit_import"
+ app:showAsAction="always|withText" />
+
+</menu>
diff --git a/src/frontends/android/app/src/main/res/values-de/strings.xml b/src/frontends/android/app/src/main/res/values-de/strings.xml
index 857a18aaa..0bb73cb1e 100644
--- a/src/frontends/android/app/src/main/res/values-de/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-de/strings.xml
@@ -48,8 +48,10 @@
<!-- VPN profile details -->
<string name="profile_edit_save">Speichern</string>
+ <string name="profile_edit_import">Importieren</string>
<string name="profile_edit_cancel">Abbrechen</string>
<string name="profile_name_label">Profilname (optional)</string>
+ <string name="profile_name_label_simple">Profilname</string>
<string name="profile_name_hint">Standardwert ist der konfigurierte Server</string>
<string name="profile_name_hint_gateway">Standardwert ist \"%1$s\"</string>
<string name="profile_gateway_label">Server</string>
@@ -80,6 +82,15 @@
<string name="profile_split_tunneling_label">Split-Tunneling</string>
<string name="profile_split_tunnelingv4_title">Blockiere IPv4 Verkehr der nicht für das VPN bestimmt ist</string>
<string name="profile_split_tunnelingv6_title">Blockiere IPv6 Verkehr der nicht für das VPN bestimmt ist</string>
+ <string name="profile_import">VPN Profile importieren</string>
+ <string name="profile_import_failed">VPN Profil-Import fehlgeschlagen</string>
+ <string name="profile_import_failed_detail">VPN Profil-Import fehlgeschlagen: %1$s</string>
+ <string name="profile_import_failed_not_found">Datei nicht gefunden</string>
+ <string name="profile_import_failed_host">Host unbekannt</string>
+ <string name="profile_import_failed_tls">TLS-Handshake fehlgeschlagen</string>
+ <string name="profile_import_exists">Dieses VPN Profil existiert bereits, die bestehenden Einstellungen werden ersetzt.</string>
+ <string name="profile_cert_import">Zertifikat aus VPN Profil importieren</string>
+ <string name="profile_cert_alias">Zertifikat für \"%1$s\"</string>
<!-- Warnings/Notifications in the details view -->
<string name="alert_text_no_input_gateway">Ein Wert wird benötigt, um die Verbindung aufbauen zu können</string>
<string name="alert_text_no_input_username">Bitte geben Sie Ihren Benutzernamen ein</string>
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 df4412210..bf08b8ee1 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
@@ -48,8 +48,10 @@
<!-- VPN profile details -->
<string name="profile_edit_save">Zapisz</string>
+ <string name="profile_edit_import">Import</string>
<string name="profile_edit_cancel">Anuluj</string>
<string name="profile_name_label">Nazwa profilu (opcjonalny)</string>
+ <string name="profile_name_label_simple">Nazwa profilu</string>
<string name="profile_name_hint">Defaults to the configured server</string>
<string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
<string name="profile_gateway_label">Serwer</string>
@@ -80,6 +82,15 @@
<string name="profile_split_tunneling_label">Split tunneling</string>
<string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
<string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+ <string name="profile_import">Import VPN profile</string>
+ <string name="profile_import_failed">Failed to import VPN profile</string>
+ <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
+ <string name="profile_import_failed_not_found">File not found</string>
+ <string name="profile_import_failed_host">Host unknown</string>
+ <string name="profile_import_failed_tls">TLS handshake failed</string>
+ <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+ <string name="profile_cert_import">Import certificate from VPN profile</string>
+ <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
<!-- Warnings/Notifications in the details view -->
<string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
<string name="alert_text_no_input_username">Wprowadź swoją nazwę użytkownika</string>
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 933b2fb2d..03ee02670 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
@@ -45,8 +45,10 @@
<!-- VPN profile details -->
<string name="profile_edit_save">Сохранить</string>
+ <string name="profile_edit_import">Import</string>
<string name="profile_edit_cancel">Отмена</string>
<string name="profile_name_label">Название профиля (необязательный)</string>
+ <string name="profile_name_label_simple">Название профиля</string>
<string name="profile_name_hint">Defaults to the configured server</string>
<string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
<string name="profile_gateway_label">Сервер</string>
@@ -77,6 +79,15 @@
<string name="profile_split_tunneling_label">Split tunneling</string>
<string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
<string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+ <string name="profile_import">Import VPN profile</string>
+ <string name="profile_import_failed">Failed to import VPN profile</string>
+ <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
+ <string name="profile_import_failed_not_found">File not found</string>
+ <string name="profile_import_failed_host">Host unknown</string>
+ <string name="profile_import_failed_tls">TLS handshake failed</string>
+ <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+ <string name="profile_cert_import">Import certificate from VPN profile</string>
+ <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
<!-- Warnings/Notifications in the details view -->
<string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
<string name="alert_text_no_input_username">Пожалуйста введите имя пользователя</string>
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 e48a9215e..07c726a96 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
@@ -46,8 +46,10 @@
<!-- VPN profile details -->
<string name="profile_edit_save">Зберегти</string>
+ <string name="profile_edit_import">Import</string>
<string name="profile_edit_cancel">Відміна</string>
<string name="profile_name_label">Назва профілю (необов\'язковий)</string>
+ <string name="profile_name_label_simple">Назва профілю</string>
<string name="profile_name_hint">Defaults to the configured server</string>
<string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
<string name="profile_gateway_label">Сервер</string>
@@ -78,6 +80,15 @@
<string name="profile_split_tunneling_label">Split tunneling</string>
<string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
<string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+ <string name="profile_import">Import VPN profile</string>
+ <string name="profile_import_failed">Failed to import VPN profile</string>
+ <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
+ <string name="profile_import_failed_not_found">File not found</string>
+ <string name="profile_import_failed_host">Host unknown</string>
+ <string name="profile_import_failed_tls">TLS handshake failed</string>
+ <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+ <string name="profile_cert_import">Import certificate from VPN profile</string>
+ <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
<!-- Warnings/Notifications in the details view -->
<string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
<string name="alert_text_no_input_username">Введіть ім\'я користувача </string>
diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml
index 0ceace143..8d74e4544 100644
--- a/src/frontends/android/app/src/main/res/values/strings.xml
+++ b/src/frontends/android/app/src/main/res/values/strings.xml
@@ -48,8 +48,10 @@
<!-- VPN profile details -->
<string name="profile_edit_save">Save</string>
+ <string name="profile_edit_import">Import</string>
<string name="profile_edit_cancel">Cancel</string>
<string name="profile_name_label">Profile name (optional)</string>
+ <string name="profile_name_label_simple">Profile name</string>
<string name="profile_name_hint">Defaults to the configured server</string>
<string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
<string name="profile_gateway_label">Server</string>
@@ -80,6 +82,15 @@
<string name="profile_split_tunneling_label">Split tunneling</string>
<string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
<string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+ <string name="profile_import">Import VPN profile</string>
+ <string name="profile_import_failed">Failed to import VPN profile</string>
+ <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
+ <string name="profile_import_failed_not_found">File not found</string>
+ <string name="profile_import_failed_host">Host unknown</string>
+ <string name="profile_import_failed_tls">TLS handshake failed</string>
+ <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+ <string name="profile_cert_import">Import certificate from VPN profile</string>
+ <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
<!-- Warnings/Notifications in the details view -->
<string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
<string name="alert_text_no_input_username">Please enter your username </string>