android: Add activity to import VPN profiles from JSON-encoded files

The file format is documented on the wiki.

URLs to .sswan files may be intercepted and downloaded files with a media
type of application/vnd.strongswan.profile may also be opened (the file
extension doesn't matter in that case).  Whether downloaded files for which
the media type is not correct but the extension is .sswan can be opened
depends on the app that issues the Intent.  For instance, from the default
Downloads app it won't work due to the content:// URLs that do not contain
the file name but when opening the downloaded file from within Chrome's
Downloads view it works as these Intents use file:// URLs, which contain
the complete file name (the latter requires a new permission).
This commit is contained in:
Tobias Brunner 2016-12-29 17:35:57 +01:00
parent cf6110f152
commit 3107634e30
12 changed files with 1053 additions and 0 deletions

View File

@ -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"
@ -63,6 +64,33 @@
<category android:name="android.intent.category.DEFAULT" />
</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" />
</intent-filter>
</activity>
<activity
android:name=".ui.TrustedCertificateImportActivity"
android:label="@string/import_certificate"

View File

@ -0,0 +1,770 @@
/*
* 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.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;
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<String> mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks<String>()
{
@Override
public Loader<String> onCreateLoader(int id, Bundle args)
{
return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData());
}
@Override
public void onLoadFinished(Loader<String> loader, String data)
{
handleProfile(data);
}
@Override
public void onLoaderReset(Loader<String> 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(String data)
{
mProgress.dismiss();
mProfile = null;
if (data != null)
{
try
{
JSONObject obj = new JSONObject(data);
mProfile = parseProfile(obj);
}
catch (JSONException e)
{
mExistsWarning.setVisibility(View.VISIBLE);
mExistsWarning.setText(e.getLocalizedMessage());
mHideImport = true;
invalidateOptionsMenu();
return;
}
}
if (mProfile == null)
{
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<String>
{
private final Uri mUri;
private String mData;
public ProfileLoader(Context context, Uri uri)
{
super(context);
mUri = uri;
}
@Override
public String loadInBackground()
{
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)
{
e.printStackTrace();
}
}
else
{
try
{
URL url = new URL(mUri.toString());
in = url.openStream();
}
catch (IOException e)
{
e.printStackTrace();
}
}
if (in != null)
{
return streamToString(in);
}
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(String 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;
}
}
/**
* 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

View File

@ -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>

View File

@ -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>

View File

@ -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,11 @@
<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_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>

View File

@ -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,11 @@
<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_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>

View File

@ -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,11 @@
<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_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>

View File

@ -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,11 @@
<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_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>

View File

@ -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,11 @@
<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_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>