aboutsummaryrefslogtreecommitdiffstats
path: root/src/frontends
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontends')
-rw-r--r--src/frontends/android/AndroidManifest.xml56
-rw-r--r--src/frontends/android/jni/libandroidbridge/Android.mk9
-rw-r--r--src/frontends/android/jni/libandroidbridge/android_jni.c107
-rw-r--r--src/frontends/android/jni/libandroidbridge/android_jni.h103
-rw-r--r--src/frontends/android/jni/libandroidbridge/backend/android_attr.c120
-rw-r--r--src/frontends/android/jni/libandroidbridge/backend/android_attr.h52
-rw-r--r--src/frontends/android/jni/libandroidbridge/backend/android_creds.c176
-rw-r--r--src/frontends/android/jni/libandroidbridge/backend/android_creds.h67
-rw-r--r--src/frontends/android/jni/libandroidbridge/backend/android_service.c533
-rw-r--r--src/frontends/android/jni/libandroidbridge/backend/android_service.h61
-rw-r--r--src/frontends/android/jni/libandroidbridge/charonservice.c397
-rw-r--r--src/frontends/android/jni/libandroidbridge/charonservice.h104
-rw-r--r--src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c193
-rw-r--r--src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h48
-rw-r--r--src/frontends/android/jni/libandroidbridge/kernel/android_net.c64
-rw-r--r--src/frontends/android/jni/libandroidbridge/kernel/android_net.h49
-rw-r--r--src/frontends/android/jni/libandroidbridge/vpnservice_builder.c274
-rw-r--r--src/frontends/android/jni/libandroidbridge/vpnservice_builder.h95
-rw-r--r--src/frontends/android/res/drawable-hdpi/ic_launcher.pngbin4147 -> 4953 bytes
-rw-r--r--src/frontends/android/res/drawable-ldpi/ic_launcher.pngbin1723 -> 0 bytes
-rw-r--r--src/frontends/android/res/drawable-mdpi/ic_launcher.pngbin2574 -> 3149 bytes
-rw-r--r--src/frontends/android/res/drawable-xhdpi/ic_launcher.pngbin0 -> 6786 bytes
-rw-r--r--src/frontends/android/res/drawable/vpn_state_background.xml21
-rw-r--r--src/frontends/android/res/layout/log_activity.xml26
-rw-r--r--src/frontends/android/res/layout/log_fragment.xml41
-rw-r--r--src/frontends/android/res/layout/login_dialog.xml51
-rw-r--r--src/frontends/android/res/layout/main.xml36
-rw-r--r--src/frontends/android/res/layout/profile_detail_view.xml109
-rw-r--r--src/frontends/android/res/layout/profile_list_fragment.xml38
-rw-r--r--src/frontends/android/res/layout/profile_list_item.xml49
-rw-r--r--src/frontends/android/res/layout/trusted_certificates_item.xml29
-rw-r--r--src/frontends/android/res/layout/vpn_state_fragment.xml91
-rw-r--r--src/frontends/android/res/menu/log.xml23
-rw-r--r--src/frontends/android/res/menu/main.xml28
-rw-r--r--src/frontends/android/res/menu/profile_edit.xml28
-rw-r--r--src/frontends/android/res/menu/profile_list.xml22
-rw-r--r--src/frontends/android/res/menu/profile_list_context.xml24
-rw-r--r--src/frontends/android/res/values-de/strings.xml83
-rw-r--r--src/frontends/android/res/values/colors.xml24
-rw-r--r--src/frontends/android/res/values/strings.xml82
-rw-r--r--src/frontends/android/res/values/styles.xml21
-rw-r--r--src/frontends/android/src/org/strongswan/android/CharonVpnService.java57
-rw-r--r--src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java149
-rw-r--r--src/frontends/android/src/org/strongswan/android/data/VpnProfile.java113
-rw-r--r--src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java231
-rw-r--r--src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java595
-rw-r--r--src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java211
-rw-r--r--src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java264
-rw-r--r--src/frontends/android/src/org/strongswan/android/strongSwanActivity.java40
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/LogActivity.java86
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/LogFragment.java227
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java78
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/MainActivity.java201
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java375
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java264
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java371
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java149
-rw-r--r--src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java88
58 files changed, 6703 insertions, 130 deletions
diff --git a/src/frontends/android/AndroidManifest.xml b/src/frontends/android/AndroidManifest.xml
index 5b1d03d7c..747fe1df3 100644
--- a/src/frontends/android/AndroidManifest.xml
+++ b/src/frontends/android/AndroidManifest.xml
@@ -1,29 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.strongswan.android"
android:versionCode="1"
- android:versionName="1.0" >
+ android:versionName="1.0.0" >
<uses-sdk android:minSdkVersion="14" />
+
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@drawable/ic_launcher"
- android:label="@string/app_name" >
+ android:label="@string/app_name"
+ android:theme="@style/ApplicationTheme" >
<activity
- android:name=".strongSwanActivity"
- android:label="@string/app_name" >
+ android:name=".ui.MainActivity"
+ android:label="@string/main_activity_name"
+ android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <service android:name=".CharonVpnService" android:permission="android.permission.BIND_VPN_SERVICE">
+ <activity
+ android:name=".ui.VpnProfileDetailActivity" >
+ </activity>
+ <activity
+ android:name=".ui.LogActivity"
+ android:label="@string/log_title" >
+ </activity>
+
+ <service
+ android:name=".logic.VpnStateService"
+ android:exported="false" >
+ </service>
+ <service
+ android:name=".logic.CharonVpnService"
+ android:exported="false"
+ android:permission="android.permission.BIND_VPN_SERVICE" >
<intent-filter>
- <action android:name="android.net.VpnService"/>
+ <action android:name="org.strongswan.android.logic.CharonVpnService" />
</intent-filter>
</service>
+
+ <provider
+ android:name=".data.LogContentProvider"
+ android:authorities="org.strongswan.android.content.log" >
+ <!-- android:grantUriPermissions="true" combined with a custom permission does
+ not work (probably too many indirections with ACTION_SEND) so we secure
+ this provider with a custom ticketing system -->
+ </provider>
</application>
-</manifest> \ No newline at end of file
+</manifest>
diff --git a/src/frontends/android/jni/libandroidbridge/Android.mk b/src/frontends/android/jni/libandroidbridge/Android.mk
index 3b8b98b86..e1806f702 100644
--- a/src/frontends/android/jni/libandroidbridge/Android.mk
+++ b/src/frontends/android/jni/libandroidbridge/Android.mk
@@ -3,7 +3,14 @@ include $(CLEAR_VARS)
# copy-n-paste from Makefile.am
LOCAL_SRC_FILES := \
-charonservice.c
+android_jni.c android_jni.h \
+backend/android_attr.c backend/android_attr.h \
+backend/android_creds.c backend/android_creds.h \
+backend/android_service.c backend/android_service.h \
+charonservice.c charonservice.h \
+kernel/android_ipsec.c kernel/android_ipsec.h \
+kernel/android_net.c kernel/android_net.h \
+vpnservice_builder.c vpnservice_builder.h
# build libandroidbridge -------------------------------------------------------
diff --git a/src/frontends/android/jni/libandroidbridge/android_jni.c b/src/frontends/android/jni/libandroidbridge/android_jni.c
new file mode 100644
index 000000000..e7cb14fb7
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/android_jni.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+#include "android_jni.h"
+
+#include <library.h>
+#include <threading/thread_value.h>
+
+/**
+ * JVM
+ */
+static JavaVM *android_jvm;
+
+jclass *android_charonvpnservice_class;
+jclass *android_charonvpnservice_builder_class;
+
+/**
+ * Thread-local variable. Only used because of the destructor
+ */
+static thread_value_t *androidjni_threadlocal;
+
+/**
+ * Thread-local destructor to ensure that a native thread is detached
+ * from the JVM even if androidjni_detach_thread() is not called.
+ */
+static void attached_thread_cleanup(void *arg)
+{
+ (*android_jvm)->DetachCurrentThread(android_jvm);
+}
+
+/*
+ * Described in header
+ */
+void androidjni_attach_thread(JNIEnv **env)
+{
+ if ((*android_jvm)->GetEnv(android_jvm, (void**)env,
+ JNI_VERSION_1_6) == JNI_OK)
+ { /* already attached or even a Java thread */
+ return;
+ }
+ (*android_jvm)->AttachCurrentThread(android_jvm, env, NULL);
+ /* use a thread-local value with a destructor that automatically detaches
+ * the thread from the JVM before it terminates, if not done manually */
+ androidjni_threadlocal->set(androidjni_threadlocal, (void*)*env);
+}
+
+/*
+ * Described in header
+ */
+void androidjni_detach_thread()
+{
+ if (androidjni_threadlocal->get(androidjni_threadlocal))
+ { /* only do this if we actually attached this thread */
+ androidjni_threadlocal->set(androidjni_threadlocal, NULL);
+ (*android_jvm)->DetachCurrentThread(android_jvm);
+ }
+}
+
+/**
+ * Called when this library is loaded by the JVM
+ */
+jint JNI_OnLoad(JavaVM *vm, void *reserved)
+{
+ JNIEnv *env;
+
+ android_jvm = vm;
+
+ if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK)
+ {
+ return -1;
+ }
+
+ androidjni_threadlocal = thread_value_create(attached_thread_cleanup);
+
+ android_charonvpnservice_class =
+ (*env)->NewGlobalRef(env, (*env)->FindClass(env,
+ JNI_PACKAGE_STRING "/CharonVpnService"));
+ android_charonvpnservice_builder_class =
+ (*env)->NewGlobalRef(env, (*env)->FindClass(env,
+ JNI_PACKAGE_STRING "/CharonVpnService$BuilderAdapter"));
+
+ return JNI_VERSION_1_6;
+}
+
+/**
+ * Called when this library is unloaded by the JVM (which never happens on
+ * Android)
+ */
+void JNI_OnUnload(JavaVM *vm, void *reserved)
+{
+ androidjni_threadlocal->destroy(androidjni_threadlocal);
+}
+
diff --git a/src/frontends/android/jni/libandroidbridge/android_jni.h b/src/frontends/android/jni/libandroidbridge/android_jni.h
new file mode 100644
index 000000000..774d37d7e
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/android_jni.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+/**
+ * @defgroup android_jni android_jni
+ * @{ @ingroup libandroidbridge
+ */
+
+#ifndef ANDROID_JNI_H_
+#define ANDROID_JNI_H_
+
+#include <jni.h>
+#include <library.h>
+
+#define JNI_PACKAGE org_strongswan_android_logic
+#define JNI_PACKAGE_STRING "org/strongswan/android/logic"
+
+#define JNI_METHOD_PP(pack, klass, name, ret, ...) \
+ ret Java_##pack##_##klass##_##name(JNIEnv *env, jobject this, ##__VA_ARGS__)
+
+#define JNI_METHOD_P(pack, klass, name, ret, ...) \
+ JNI_METHOD_PP(pack, klass, name, ret, ##__VA_ARGS__)
+
+#define JNI_METHOD(klass, name, ret, ...) \
+ JNI_METHOD_P(JNI_PACKAGE, klass, name, ret, ##__VA_ARGS__)
+
+/**
+ * Java classes
+ * Initialized in JNI_OnLoad()
+ */
+extern jclass *android_charonvpnservice_class;
+extern jclass *android_charonvpnservice_builder_class;
+
+/**
+ * Attach the current thread to the JVM
+ *
+ * As local JNI references are not freed until the thread detaches
+ * androidjni_detach_thread() should be called as soon as possible.
+ * If it is not called a thread-local destructor ensures that the
+ * thread is at least detached as soon as it terminates.
+ *
+ * @param env JNIEnv
+ */
+void androidjni_attach_thread(JNIEnv **env);
+
+/**
+ * Detach the current thread from the JVM
+ *
+ * Call this as soon as possible to ensure that local JNI references are freed.
+ */
+void androidjni_detach_thread();
+
+/**
+ * Handle exceptions thrown by a JNI call
+ *
+ * @param env JNIEnv
+ * @return TRUE if an exception was thrown
+ */
+static inline bool androidjni_exception_occurred(JNIEnv *env)
+{
+ if ((*env)->ExceptionOccurred(env))
+ { /* clear any exception, otherwise the VM is terminated */
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * Convert a Java string to a C string. Memory is allocated.
+ *
+ * @param env JNIEnv
+ * @param jstr Java string
+ * @return native C string (allocated)
+ */
+static inline char *androidjni_convert_jstring(JNIEnv *env, jstring jstr)
+{
+ char *str;
+ jsize len;
+
+ len = (*env)->GetStringUTFLength(env, jstr);
+ str = malloc(len + 1);
+ (*env)->GetStringUTFRegion(env, jstr, 0, len, str);
+ str[len] = '\0';
+ return str;
+}
+
+#endif /** ANDROID_JNI_H_ @}*/
diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_attr.c b/src/frontends/android/jni/libandroidbridge/backend/android_attr.c
new file mode 100644
index 000000000..e8c506950
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/backend/android_attr.c
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+#include "android_attr.h"
+#include "../charonservice.h"
+
+#include <hydra.h>
+#include <debug.h>
+#include <library.h>
+
+typedef struct private_android_attr_t private_android_attr_t;
+
+/**
+ * Private data of an android_attr_t object.
+ */
+struct private_android_attr_t {
+
+ /**
+ * Public interface.
+ */
+ android_attr_t public;
+};
+
+METHOD(attribute_handler_t, handle, bool,
+ private_android_attr_t *this, identification_t *server,
+ configuration_attribute_type_t type, chunk_t data)
+{
+ vpnservice_builder_t *builder;
+ host_t *dns;
+
+ switch (type)
+ {
+ case INTERNAL_IP4_DNS:
+ dns = host_create_from_chunk(AF_INET, data, 0);
+ break;
+ default:
+ return FALSE;
+ }
+
+ if (!dns || dns->is_anyaddr(dns))
+ {
+ DESTROY_IF(dns);
+ return FALSE;
+ }
+
+ builder = charonservice->get_vpnservice_builder(charonservice);
+ builder->add_dns(builder, dns);
+ dns->destroy(dns);
+ return TRUE;
+}
+
+METHOD(attribute_handler_t, release, void,
+ private_android_attr_t *this, identification_t *server,
+ configuration_attribute_type_t type, chunk_t data)
+{
+ /* DNS servers cannot be removed from an existing TUN device */
+}
+
+METHOD(enumerator_t, enumerate_dns, bool,
+ enumerator_t *this, configuration_attribute_type_t *type, chunk_t *data)
+{
+ *type = INTERNAL_IP4_DNS;
+ *data = chunk_empty;
+ this->enumerate = (void*)return_false;
+ return TRUE;
+}
+
+METHOD(attribute_handler_t, create_attribute_enumerator, enumerator_t*,
+ private_android_attr_t *this, identification_t *server, host_t *vip)
+{
+ enumerator_t *enumerator;
+
+ INIT(enumerator,
+ .enumerate = (void*)_enumerate_dns,
+ .destroy = (void*)free,
+ );
+ return enumerator;
+}
+
+METHOD(android_attr_t, destroy, void,
+ private_android_attr_t *this)
+{
+ free(this);
+}
+
+/**
+ * Described in header
+ */
+android_attr_t *android_attr_create()
+{
+ private_android_attr_t *this;
+
+ INIT(this,
+ .public = {
+ .handler = {
+ .handle = _handle,
+ .release = _release,
+ .create_attribute_enumerator = _create_attribute_enumerator,
+ },
+ .destroy = _destroy,
+ },
+ );
+
+ return &this->public;
+}
+
diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_attr.h b/src/frontends/android/jni/libandroidbridge/backend/android_attr.h
new file mode 100644
index 000000000..56b02e1ce
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/backend/android_attr.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+/**
+ * @defgroup android_attr android_attr
+ * @{ @ingroup android_backend
+ */
+
+#ifndef ANDROID_ATTR_H_
+#define ANDROID_ATTR_H_
+
+#include <library.h>
+#include <attributes/attribute_handler.h>
+
+typedef struct android_attr_t android_attr_t;
+
+/**
+ * Handler for DNS configuration
+ */
+struct android_attr_t {
+
+ /**
+ * implements the attribute_handler_t interface
+ */
+ attribute_handler_t handler;
+
+ /**
+ * Destroy a android_attr_t
+ */
+ void (*destroy)(android_attr_t *this);
+};
+
+/**
+ * Create a android_attr_t instance.
+ */
+android_attr_t *android_attr_create(void);
+
+#endif /** ANDROID_ATTR_H_ @}*/
+
diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_creds.c b/src/frontends/android/jni/libandroidbridge/backend/android_creds.c
new file mode 100644
index 000000000..27023d721
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/backend/android_creds.c
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "android_creds.h"
+#include "../charonservice.h"
+
+#include <daemon.h>
+#include <library.h>
+#include <credentials/sets/mem_cred.h>
+#include <threading/rwlock.h>
+
+typedef struct private_android_creds_t private_android_creds_t;
+
+/**
+ * Private data of an android_creds_t object
+ */
+struct private_android_creds_t {
+
+ /**
+ * Public interface
+ */
+ android_creds_t public;
+
+ /**
+ * Credential set storing trusted certificates and user credentials
+ */
+ mem_cred_t *creds;
+
+ /**
+ * read/write lock to make sure certificates are only loaded once
+ */
+ rwlock_t *lock;
+
+ /**
+ * TRUE if certificates have been loaded via JNI
+ */
+ bool loaded;
+};
+
+/**
+ * Load trusted certificates via charonservice (JNI).
+ */
+static void load_trusted_certificates(private_android_creds_t *this)
+{
+ linked_list_t *certs;
+ certificate_t *cert;
+ chunk_t *current;
+
+ certs = charonservice->get_trusted_certificates(charonservice);
+ if (certs)
+ {
+ while (certs->remove_first(certs, (void**)&current) == SUCCESS)
+ {
+ cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
+ BUILD_BLOB_ASN1_DER, *current, BUILD_END);
+ if (cert)
+ {
+ DBG2(DBG_CFG, "loaded CA certificate '%Y'",
+ cert->get_subject(cert));
+ this->creds->add_cert(this->creds, TRUE, cert);
+ }
+ chunk_free(current);
+ free(current);
+ }
+ certs->destroy(certs);
+ }
+}
+
+METHOD(credential_set_t, create_cert_enumerator, enumerator_t*,
+ private_android_creds_t *this, certificate_type_t cert, key_type_t key,
+ identification_t *id, bool trusted)
+{
+ enumerator_t *enumerator;
+
+ if (!trusted || (cert != CERT_ANY && cert != CERT_X509))
+ {
+ return NULL;
+ }
+ this->lock->read_lock(this->lock);
+ if (!this->loaded)
+ {
+ this->lock->unlock(this->lock);
+ this->lock->write_lock(this->lock);
+ /* check again after acquiring the write lock */
+ if (!this->loaded)
+ {
+ load_trusted_certificates(this);
+ this->loaded = TRUE;
+ }
+ this->lock->unlock(this->lock);
+ this->lock->read_lock(this->lock);
+ }
+ enumerator = this->creds->set.create_cert_enumerator(&this->creds->set,
+ cert, key, id, trusted);
+ return enumerator_create_cleaner(enumerator, (void*)this->lock->unlock,
+ this->lock);
+}
+
+METHOD(android_creds_t, add_username_password, void,
+ private_android_creds_t *this, char *username, char *password)
+{
+ shared_key_t *shared_key;
+ identification_t *id;
+ chunk_t secret;
+
+ secret = chunk_create(password, strlen(password));
+ shared_key = shared_key_create(SHARED_EAP, chunk_clone(secret));
+ id = identification_create_from_string(username);
+
+ this->creds->add_shared(this->creds, shared_key, id, NULL);
+}
+
+METHOD(credential_set_t, create_shared_enumerator, enumerator_t*,
+ private_android_creds_t *this, shared_key_type_t type,
+ identification_t *me, identification_t *other)
+{
+ return this->creds->set.create_shared_enumerator(&this->creds->set,
+ type, me, other);
+}
+
+METHOD(android_creds_t, clear, void,
+ private_android_creds_t *this)
+{
+ this->lock->write_lock(this->lock);
+ this->creds->clear(this->creds);
+ this->loaded = FALSE;
+ this->lock->unlock(this->lock);
+}
+
+METHOD(android_creds_t, destroy, void,
+ private_android_creds_t *this)
+{
+ clear(this);
+ this->creds->destroy(this->creds);
+ this->lock->destroy(this->lock);
+ free(this);
+}
+
+/**
+ * Described in header.
+ */
+android_creds_t *android_creds_create()
+{
+ private_android_creds_t *this;
+
+ INIT(this,
+ .public = {
+ .set = {
+ .create_cert_enumerator = _create_cert_enumerator,
+ .create_shared_enumerator = _create_shared_enumerator,
+ .create_private_enumerator = (void*)return_null,
+ .create_cdp_enumerator = (void*)return_null,
+ .cache_cert = (void*)nop,
+ },
+ .add_username_password = _add_username_password,
+ .clear = _clear,
+ .destroy = _destroy,
+ },
+ .creds = mem_cred_create(),
+ .lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
+ );
+
+ return &this->public;
+}
diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_creds.h b/src/frontends/android/jni/libandroidbridge/backend/android_creds.h
new file mode 100644
index 000000000..33de838c1
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/backend/android_creds.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+/**
+ * @defgroup android_creds android_creds
+ * @{ @ingroup android_backend
+ */
+
+#ifndef ANDROID_CREDS_H_
+#define ANDROID_CREDS_H_
+
+#include <library.h>
+#include <credentials/credential_set.h>
+
+typedef struct android_creds_t android_creds_t;
+
+/**
+ * Android credential set that provides CA certificates via JNI and supplied
+ * user credentials.
+ */
+struct android_creds_t {
+
+ /**
+ * Implements credential_set_t
+ */
+ credential_set_t set;
+
+ /**
+ * Add user name and password for EAP authentication
+ *
+ * @param username user name
+ * @param password password
+ */
+ void (*add_username_password)(android_creds_t *this, char *username,
+ char *password);
+
+ /**
+ * Clear the cached certificates and stored credentials.
+ */
+ void (*clear)(android_creds_t *this);
+
+ /**
+ * Destroy a android_creds instance.
+ */
+ void (*destroy)(android_creds_t *this);
+
+};
+
+/**
+ * Create an android_creds instance.
+ */
+android_creds_t *android_creds_create();
+
+#endif /** ANDROID_CREDS_H_ @}*/
+
diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_service.c b/src/frontends/android/jni/libandroidbridge/backend/android_service.c
new file mode 100644
index 000000000..dfc0d2342
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/backend/android_service.c
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2010-2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+
+#include "android_service.h"
+#include "../charonservice.h"
+#include "../vpnservice_builder.h"
+
+#include <daemon.h>
+#include <library.h>
+#include <ipsec.h>
+#include <processing/jobs/callback_job.h>
+#include <threading/rwlock.h>
+#include <threading/thread.h>
+
+typedef struct private_android_service_t private_android_service_t;
+
+#define TUN_DEFAULT_MTU 1400
+
+/**
+ * private data of Android service
+ */
+struct private_android_service_t {
+
+ /**
+ * public interface
+ */
+ android_service_t public;
+
+ /**
+ * current IKE_SA
+ */
+ ike_sa_t *ike_sa;
+
+ /**
+ * local ipv4 address
+ */
+ char *local_address;
+
+ /**
+ * gateway
+ */
+ char *gateway;
+
+ /**
+ * username
+ */
+ char *username;
+
+ /**
+ * lock to safely access the TUN device fd
+ */
+ rwlock_t *lock;
+
+ /**
+ * TUN device file descriptor
+ */
+ int tunfd;
+
+};
+
+/**
+ * Outbound callback
+ */
+static void send_esp(void *data, esp_packet_t *packet)
+{
+ charon->sender->send_no_marker(charon->sender, (packet_t*)packet);
+}
+
+/**
+ * Inbound callback
+ */
+static void deliver_plain(private_android_service_t *this,
+ ip_packet_t *packet)
+{
+ chunk_t encoding;
+ ssize_t len;
+
+ encoding = packet->get_encoding(packet);
+
+ this->lock->read_lock(this->lock);
+ if (this->tunfd < 0)
+ { /* the TUN device is already closed */
+ this->lock->unlock(this->lock);
+ packet->destroy(packet);
+ return;
+ }
+ len = write(this->tunfd, encoding.ptr, encoding.len);
+ this->lock->unlock(this->lock);
+
+ if (len < 0 || len != encoding.len)
+ {
+ DBG1(DBG_DMN, "failed to write packet to TUN device: %s",
+ strerror(errno));
+ }
+ packet->destroy(packet);
+}
+
+/**
+ * Receiver callback
+ */
+static void receiver_esp_cb(void *data, packet_t *packet)
+{
+ esp_packet_t *esp_packet;
+
+ esp_packet = esp_packet_create_from_packet(packet);
+ ipsec->processor->queue_inbound(ipsec->processor, esp_packet);
+}
+
+/**
+ * Job handling outbound plaintext packets
+ */
+static job_requeue_t handle_plain(private_android_service_t *this)
+{
+ ip_packet_t *packet;
+ chunk_t raw;
+ fd_set set;
+ ssize_t len;
+ int tunfd;
+ bool old;
+
+ FD_ZERO(&set);
+
+ this->lock->read_lock(this->lock);
+ if (this->tunfd < 0)
+ { /* the TUN device is already closed */
+ this->lock->unlock(this->lock);
+ return JOB_REQUEUE_NONE;
+ }
+ tunfd = this->tunfd;
+ FD_SET(tunfd, &set);
+ this->lock->unlock(this->lock);
+
+ old = thread_cancelability(TRUE);
+ len = select(tunfd + 1, &set, NULL, NULL, NULL);
+ thread_cancelability(old);
+
+ if (len < 0)
+ {
+ DBG1(DBG_DMN, "select on TUN device failed: %s", strerror(errno));
+ return JOB_REQUEUE_NONE;
+ }
+
+ raw = chunk_alloc(TUN_DEFAULT_MTU);
+ len = read(tunfd, raw.ptr, raw.len);
+ if (len < 0)
+ {
+ DBG1(DBG_DMN, "reading from TUN device failed: %s", strerror(errno));
+ chunk_free(&raw);
+ return JOB_REQUEUE_FAIR;
+ }
+ raw.len = len;
+
+ packet = ip_packet_create(raw);
+ if (packet)
+ {
+ ipsec->processor->queue_outbound(ipsec->processor, packet);
+ }
+ else
+ {
+ DBG1(DBG_DMN, "invalid IP packet read from TUN device");
+ }
+ return JOB_REQUEUE_DIRECT;
+}
+
+/**
+ * Add a route to the TUN device builder
+ */
+static bool add_route(vpnservice_builder_t *builder, host_t *net,
+ u_int8_t prefix)
+{
+ /* if route is 0.0.0.0/0, split it into two routes 0.0.0.0/1 and
+ * 128.0.0.0/1 because otherwise it would conflict with the current default
+ * route */
+ if (net->is_anyaddr(net) && prefix == 0)
+ {
+ bool success;
+
+ success = add_route(builder, net, 1);
+ net = host_create_from_string("128.0.0.0", 0);
+ success = success && add_route(builder, net, 1);
+ net->destroy(net);
+ return success;
+ }
+ return builder->add_route(builder, net, prefix);
+}
+
+/**
+ * Generate and set routes from installed IPsec policies
+ */
+static bool add_routes(vpnservice_builder_t *builder, child_sa_t *child_sa)
+{
+ traffic_selector_t *src_ts, *dst_ts;
+ enumerator_t *enumerator;
+ bool success = TRUE;
+
+ enumerator = child_sa->create_policy_enumerator(child_sa);
+ while (success && enumerator->enumerate(enumerator, &src_ts, &dst_ts))
+ {
+ host_t *net;
+ u_int8_t prefix;
+
+ dst_ts->to_subnet(dst_ts, &net, &prefix);
+ success = add_route(builder, net, prefix);
+ net->destroy(net);
+ }
+ enumerator->destroy(enumerator);
+ return success;
+}
+
+/**
+ * Setup a new TUN device for the supplied SAs, also queues a job that
+ * reads packets from this device.
+ * Additional information such as DNS servers are gathered in appropriate
+ * listeners asynchronously. To be sure every required bit of information is
+ * available this should be called after the CHILD_SA has been established.
+ */
+static bool setup_tun_device(private_android_service_t *this,
+ ike_sa_t *ike_sa, child_sa_t *child_sa)
+{
+ vpnservice_builder_t *builder;
+ host_t *vip;
+ int tunfd;
+
+ DBG1(DBG_DMN, "setting up TUN device for CHILD_SA %s{%u}",
+ child_sa->get_name(child_sa), child_sa->get_reqid(child_sa));
+ vip = ike_sa->get_virtual_ip(ike_sa, TRUE);
+ if (!vip || vip->is_anyaddr(vip))
+ {
+ DBG1(DBG_DMN, "setting up TUN device failed, no virtual IP found");
+ return FALSE;
+ }
+
+ builder = charonservice->get_vpnservice_builder(charonservice);
+ if (!builder->add_address(builder, vip) ||
+ !add_routes(builder, child_sa) ||
+ !builder->set_mtu(builder, TUN_DEFAULT_MTU))
+ {
+ return FALSE;
+ }
+
+ tunfd = builder->establish(builder);
+ if (tunfd == -1)
+ {
+ return FALSE;
+ }
+
+ this->lock->write_lock(this->lock);
+ this->tunfd = tunfd;
+ this->lock->unlock(this->lock);
+
+ DBG1(DBG_DMN, "successfully created TUN device");
+
+ charon->receiver->add_esp_cb(charon->receiver,
+ (receiver_esp_cb_t)receiver_esp_cb, NULL);
+ ipsec->processor->register_inbound(ipsec->processor,
+ (ipsec_inbound_cb_t)deliver_plain, this);
+ ipsec->processor->register_outbound(ipsec->processor,
+ (ipsec_outbound_cb_t)send_esp, NULL);
+
+ lib->processor->queue_job(lib->processor,
+ (job_t*)callback_job_create((callback_job_cb_t)handle_plain, this,
+ NULL, (callback_job_cancel_t)return_false));
+ return TRUE;
+}
+
+/**
+ * Close the current tun device
+ */
+static void close_tun_device(private_android_service_t *this)
+{
+ int tunfd;
+
+ this->lock->write_lock(this->lock);
+ if (this->tunfd < 0)
+ { /* already closed (or never created) */
+ this->lock->unlock(this->lock);
+ return;
+ }
+ tunfd = this->tunfd;
+ this->tunfd = -1;
+ this->lock->unlock(this->lock);
+
+ ipsec->processor->unregister_outbound(ipsec->processor,
+ (ipsec_outbound_cb_t)send_esp);
+ ipsec->processor->unregister_inbound(ipsec->processor,
+ (ipsec_inbound_cb_t)deliver_plain);
+ charon->receiver->del_esp_cb(charon->receiver,
+ (receiver_esp_cb_t)receiver_esp_cb);
+ close(tunfd);
+}
+
+METHOD(listener_t, child_updown, bool,
+ private_android_service_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa,
+ bool up)
+{
+ if (this->ike_sa == ike_sa)
+ {
+ if (up)
+ {
+ /* disable the hooks registered to catch initiation failures */
+ this->public.listener.ike_updown = NULL;
+ this->public.listener.ike_state_change = NULL;
+ if (!setup_tun_device(this, ike_sa, child_sa))
+ {
+ DBG1(DBG_DMN, "failed to setup TUN device");
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_GENERIC_ERROR);
+ return FALSE;
+
+ }
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_CHILD_STATE_UP);
+ }
+ else
+ {
+ close_tun_device(this);
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_CHILD_STATE_DOWN);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+METHOD(listener_t, ike_updown, bool,
+ private_android_service_t *this, ike_sa_t *ike_sa, bool up)
+{
+ /* this callback is only registered during initiation, so if the IKE_SA
+ * goes down we assume an authentication error */
+ if (this->ike_sa == ike_sa && !up)
+ {
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_AUTH_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+METHOD(listener_t, ike_state_change, bool,
+ private_android_service_t *this, ike_sa_t *ike_sa, ike_sa_state_t state)
+{
+ /* this call back is only registered during initiation */
+ if (this->ike_sa == ike_sa && state == IKE_DESTROYING)
+ {
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_UNREACHABLE_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+METHOD(listener_t, alert, bool,
+ private_android_service_t *this, ike_sa_t *ike_sa, alert_t alert,
+ va_list args)
+{
+ if (this->ike_sa == ike_sa)
+ {
+ switch (alert)
+ {
+ case ALERT_PEER_ADDR_FAILED:
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_LOOKUP_ERROR);
+ break;
+ case ALERT_PEER_AUTH_FAILED:
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_PEER_AUTH_ERROR);
+ break;
+ default:
+ break;
+ }
+ }
+ return TRUE;
+}
+
+METHOD(listener_t, ike_rekey, bool,
+ private_android_service_t *this, ike_sa_t *old, ike_sa_t *new)
+{
+ if (this->ike_sa == old)
+ {
+ this->ike_sa = new;
+ }
+ return TRUE;
+}
+
+static job_requeue_t initiate(private_android_service_t *this)
+{
+ identification_t *gateway, *user;
+ ike_cfg_t *ike_cfg;
+ peer_cfg_t *peer_cfg;
+ child_cfg_t *child_cfg;
+ traffic_selector_t *ts;
+ ike_sa_t *ike_sa;
+ auth_cfg_t *auth;
+ lifetime_cfg_t lifetime = {
+ .time = {
+ .life = 10800, /* 3h */
+ .rekey = 10200, /* 2h50min */
+ .jitter = 300 /* 5min */
+ }
+ };
+
+ ike_cfg = ike_cfg_create(TRUE, TRUE, this->local_address, FALSE,
+ charon->socket->get_port(charon->socket, FALSE),
+ this->gateway, FALSE, IKEV2_UDP_PORT);
+ ike_cfg->add_proposal(ike_cfg, proposal_create_default(PROTO_IKE));
+
+ peer_cfg = peer_cfg_create("android", IKEV2, ike_cfg, CERT_SEND_IF_ASKED,
+ UNIQUE_REPLACE, 1, /* keyingtries */
+ 36000, 0, /* rekey 10h, reauth none */
+ 600, 600, /* jitter, over 10min */
+ TRUE, FALSE, /* mobike, aggressive */
+ 0, 0, /* DPD delay, timeout */
+ host_create_from_string("0.0.0.0", 0) /* virt */,
+ NULL, FALSE, NULL, NULL); /* pool, mediation */
+
+
+ auth = auth_cfg_create();
+ auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP);
+ user = identification_create_from_string(this->username);
+ auth->add(auth, AUTH_RULE_IDENTITY, user);
+ peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE);
+ auth = auth_cfg_create();
+ auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY);
+ gateway = identification_create_from_string(this->gateway);
+ auth->add(auth, AUTH_RULE_IDENTITY, gateway);
+ peer_cfg->add_auth_cfg(peer_cfg, auth, FALSE);
+
+ child_cfg = child_cfg_create("android", &lifetime, NULL, TRUE, MODE_TUNNEL,
+ ACTION_NONE, ACTION_NONE, ACTION_NONE, FALSE,
+ 0, 0, NULL, NULL, 0);
+ child_cfg->add_proposal(child_cfg, proposal_create_default(PROTO_ESP));
+ ts = traffic_selector_create_dynamic(0, 0, 65535);
+ child_cfg->add_traffic_selector(child_cfg, TRUE, ts);
+ ts = traffic_selector_create_from_string(0, TS_IPV4_ADDR_RANGE, "0.0.0.0",
+ 0, "255.255.255.255", 65535);
+ child_cfg->add_traffic_selector(child_cfg, FALSE, ts);
+ peer_cfg->add_child_cfg(peer_cfg, child_cfg);
+
+ /* get us an IKE_SA */
+ ike_sa = charon->ike_sa_manager->checkout_by_config(charon->ike_sa_manager,
+ peer_cfg);
+ if (!ike_sa)
+ {
+ peer_cfg->destroy(peer_cfg);
+ charonservice->update_status(charonservice,
+ CHARONSERVICE_GENERIC_ERROR);
+ return JOB_REQUEUE_NONE;
+ }
+ if (!ike_sa->get_peer_cfg(ike_sa))
+ {
+ ike_sa->set_peer_cfg(ike_sa, peer_cfg);
+ }
+ peer_cfg->destroy(peer_cfg);
+
+ /* store the IKE_SA so we can track its progress */
+ this->ike_sa = ike_sa;
+
+ /* get an additional reference because initiate consumes one */
+ child_cfg->get_ref(child_cfg);
+ if (ike_sa->initiate(ike_sa, child_cfg, 0, NULL, NULL) != SUCCESS)
+ {
+ DBG1(DBG_CFG, "failed to initiate tunnel");
+ charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
+ ike_sa);
+ return JOB_REQUEUE_NONE;
+ }
+ charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
+ return JOB_REQUEUE_NONE;
+}
+
+METHOD(android_service_t, destroy, void,
+ private_android_service_t *this)
+{
+ charon->bus->remove_listener(charon->bus, &this->public.listener);
+ /* make sure the tun device is actually closed */
+ close_tun_device(this);
+ this->lock->destroy(this->lock);
+ free(this->local_address);
+ free(this->username);
+ free(this->gateway);
+ free(this);
+}
+
+/**
+ * See header
+ */
+android_service_t *android_service_create(char *local_address, char *gateway,
+ char *username)
+{
+ private_android_service_t *this;
+
+ INIT(this,
+ .public = {
+ .listener = {
+ .ike_rekey = _ike_rekey,
+ .ike_updown = _ike_updown,
+ .ike_state_change = _ike_state_change,
+ .child_updown = _child_updown,
+ .alert = _alert,
+ },
+ .destroy = _destroy,
+ },
+ .lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
+ .local_address = local_address,
+ .username = username,
+ .gateway = gateway,
+ .tunfd = -1,
+ );
+
+ charon->bus->add_listener(charon->bus, &this->public.listener);
+
+ lib->processor->queue_job(lib->processor,
+ (job_t*)callback_job_create((callback_job_cb_t)initiate, this,
+ NULL, NULL));
+ return &this->public;
+}
diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_service.h b/src/frontends/android/jni/libandroidbridge/backend/android_service.h
new file mode 100644
index 000000000..a7bd8b059
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/backend/android_service.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010-2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+/**
+ * @defgroup android_service android_service
+ * @{ @ingroup android_backend
+ */
+
+#ifndef ANDROID_SERVICE_H_
+#define ANDROID_SERVICE_H_
+
+#include "android_creds.h"
+
+#include <library.h>
+#include <bus/listeners/listener.h>
+
+typedef struct android_service_t android_service_t;
+
+/**
+ * Service that sets up an IKE_SA/CHILD_SA and handles events
+ */
+struct android_service_t {
+
+ /**
+ * Implements listener_t.
+ */
+ listener_t listener;
+
+ /**
+ * Destroy a android_service_t.
+ */
+ void (*destroy)(android_service_t *this);
+
+};
+
+/**
+ * Create an Android service instance. Queues a job that starts initiation of a
+ * new IKE SA.
+ *
+ * @param local_address local ip address
+ * @param gateway gateway address
+ * @param username user name (local identity)
+ */
+android_service_t *android_service_create(char *local_address, char *gateway,
+ char *username);
+
+#endif /** ANDROID_SERVICE_H_ @}*/
diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.c b/src/frontends/android/jni/libandroidbridge/charonservice.c
index 424d50d24..fab99ac10 100644
--- a/src/frontends/android/jni/libandroidbridge/charonservice.c
+++ b/src/frontends/android/jni/libandroidbridge/charonservice.c
@@ -1,4 +1,6 @@
/*
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
* Copyright (C) 2012 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
@@ -13,30 +15,78 @@
* for more details.
*/
+#include <signal.h>
#include <string.h>
+#include <sys/utsname.h>
#include <android/log.h>
-#include <jni.h>
+#include <errno.h>
+#include "charonservice.h"
+#include "android_jni.h"
+#include "backend/android_attr.h"
+#include "backend/android_creds.h"
+#include "backend/android_service.h"
+#include "kernel/android_ipsec.h"
+#include "kernel/android_net.h"
+
+#include <daemon.h>
#include <hydra.h>
#include <ipsec.h>
-#include <daemon.h>
#include <library.h>
+#include <threading/thread.h>
+
+#define ANDROID_DEBUG_LEVEL 1
+#define ANDROID_RETRASNMIT_TRIES 3
+#define ANDROID_RETRANSMIT_TIMEOUT 3.0
+#define ANDROID_RETRANSMIT_BASE 1.4
+
+typedef struct private_charonservice_t private_charonservice_t;
+
+/**
+ * private data of charonservice
+ */
+struct private_charonservice_t {
-#define JNI_PACKAGE org_strongswan_android
+ /**
+ * public interface
+ */
+ charonservice_t public;
-#define JNI_METHOD_PP(pack, klass, name, ret, ...) \
- ret Java_##pack##_##klass##_##name(JNIEnv *env, jobject this, ##__VA_ARGS__)
+ /**
+ * android_attr instance
+ */
+ android_attr_t *attr;
-#define JNI_METHOD_P(pack, klass, name, ret, ...) \
- JNI_METHOD_PP(pack, klass, name, ret, ##__VA_ARGS__)
+ /**
+ * android_creds instance
+ */
+ android_creds_t *creds;
-#define JNI_METHOD(klass, name, ret, ...) \
- JNI_METHOD_P(JNI_PACKAGE, klass, name, ret, ##__VA_ARGS__)
+ /**
+ * android_service instance
+ */
+ android_service_t *service;
+
+ /**
+ * VpnService builder (accessed via JNI)
+ */
+ vpnservice_builder_t *builder;
+
+ /**
+ * CharonVpnService reference
+ */
+ jobject vpn_service;
+};
+
+/**
+ * Single instance of charonservice_t.
+ */
+charonservice_t *charonservice;
/**
* hook in library for debugging messages
*/
-extern void (*dbg) (debug_t group, level_t level, char *fmt, ...);
+extern void (*dbg)(debug_t group, level_t level, char *fmt, ...);
/**
* Logging hook for library logs, using android specific logging
@@ -45,10 +95,11 @@ static void dbg_android(debug_t group, level_t level, char *fmt, ...)
{
va_list args;
- if (level <= 4)
+ if (level <= ANDROID_DEBUG_LEVEL)
{
char sgroup[16], buffer[8192];
char *current = buffer, *next;
+
snprintf(sgroup, sizeof(sgroup), "%N", debug_names, group);
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
@@ -68,10 +119,276 @@ static void dbg_android(debug_t group, level_t level, char *fmt, ...)
}
/**
+ * Initialize file logger
+ */
+static void initialize_logger(char *logfile)
+{
+ file_logger_t *file_logger;
+ debug_t group;
+ FILE *file;
+
+ /* truncate an existing file */
+ file = fopen(logfile, "w");
+ if (!file)
+ {
+ DBG1(DBG_DMN, "opening file %s for logging failed: %s",
+ logfile, strerror(errno));
+ return;
+ }
+ /* flush each line */
+ setlinebuf(file);
+
+ file_logger = file_logger_create(file, "%b %e %T", FALSE);
+ for (group = 0; group < DBG_MAX; group++)
+ {
+ file_logger->set_level(file_logger, group, ANDROID_DEBUG_LEVEL);
+ }
+ charon->file_loggers->insert_last(charon->file_loggers, file_logger);
+ charon->bus->add_logger(charon->bus, &file_logger->logger);
+}
+
+METHOD(charonservice_t, update_status, bool,
+ private_charonservice_t *this, android_vpn_state_t code)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+ bool success = FALSE;
+
+ androidjni_attach_thread(&env);
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_class,
+ "updateStatus", "(I)V");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ (*env)->CallVoidMethod(env, this->vpn_service, method_id, (jint)code);
+ success = !androidjni_exception_occurred(env);
+
+failed:
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return success;
+}
+
+METHOD(charonservice_t, bypass_socket, bool,
+ private_charonservice_t *this, int fd, int family)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+
+ androidjni_attach_thread(&env);
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_class,
+ "protect", "(I)Z");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ if (!(*env)->CallBooleanMethod(env, this->vpn_service, method_id, fd))
+ {
+ DBG1(DBG_CFG, "VpnService.protect() failed");
+ goto failed;
+ }
+ androidjni_detach_thread();
+ return TRUE;
+
+failed:
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return FALSE;
+}
+
+METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
+ private_charonservice_t *this)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+ jobjectArray jcerts;
+ linked_list_t *list;
+ jsize i;
+
+ androidjni_attach_thread(&env);
+
+ method_id = (*env)->GetMethodID(env,
+ android_charonvpnservice_class,
+ "getTrustedCertificates", "(Ljava/lang/String;)[[B");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ jcerts = (*env)->CallObjectMethod(env, this->vpn_service, method_id, NULL);
+ if (!jcerts)
+ {
+ goto failed;
+ }
+ list = linked_list_create();
+ for (i = 0; i < (*env)->GetArrayLength(env, jcerts); ++i)
+ {
+ chunk_t *ca_cert;
+ jbyteArray jcert;
+
+ ca_cert = malloc_thing(chunk_t);
+ list->insert_last(list, ca_cert);
+
+ jcert = (*env)->GetObjectArrayElement(env, jcerts, i);
+ *ca_cert = chunk_alloc((*env)->GetArrayLength(env, jcert));
+ (*env)->GetByteArrayRegion(env, jcert, 0, ca_cert->len, ca_cert->ptr);
+ (*env)->DeleteLocalRef(env, jcert);
+ }
+ (*env)->DeleteLocalRef(env, jcerts);
+ androidjni_detach_thread();
+ return list;
+
+failed:
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return NULL;
+}
+
+METHOD(charonservice_t, get_vpnservice_builder, vpnservice_builder_t*,
+ private_charonservice_t *this)
+{
+ return this->builder;
+}
+
+/**
+ * Initiate a new connection
+ *
+ * @param local local ip address (gets owned)
+ * @param gateway gateway address (gets owned)
+ * @param username username (gets owned)
+ * @param password password (gets owned)
+ */
+static void initiate(char *local, char *gateway, char *username, char *password)
+{
+ private_charonservice_t *this = (private_charonservice_t*)charonservice;
+
+ this->creds->clear(this->creds);
+ this->creds->add_username_password(this->creds, username, password);
+ memwipe(password, strlen(password));
+ free(password);
+
+ DESTROY_IF(this->service);
+ this->service = android_service_create(local, gateway, username);
+}
+
+/**
+ * Initialize/deinitialize Android backend
+ */
+static bool charonservice_register(void *plugin, plugin_feature_t *feature,
+ bool reg, void *data)
+{
+ private_charonservice_t *this = (private_charonservice_t*)charonservice;
+ if (reg)
+ {
+ lib->credmgr->add_set(lib->credmgr, &this->creds->set);
+ hydra->attributes->add_handler(hydra->attributes,
+ &this->attr->handler);
+ }
+ else
+ {
+ lib->credmgr->remove_set(lib->credmgr, &this->creds->set);
+ hydra->attributes->remove_handler(hydra->attributes,
+ &this->attr->handler);
+ if (this->service)
+ {
+ this->service->destroy(this->service);
+ this->service = NULL;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Initialize the charonservice object
+ */
+static void charonservice_init(JNIEnv *env, jobject service, jobject builder)
+{
+ private_charonservice_t *this;
+ static plugin_feature_t features[] = {
+ PLUGIN_CALLBACK(kernel_net_register, kernel_android_net_create),
+ PLUGIN_PROVIDE(CUSTOM, "kernel-net"),
+ PLUGIN_CALLBACK(kernel_ipsec_register, kernel_android_ipsec_create),
+ PLUGIN_PROVIDE(CUSTOM, "kernel-ipsec"),
+ PLUGIN_CALLBACK((plugin_feature_callback_t)charonservice_register, NULL),
+ PLUGIN_PROVIDE(CUSTOM, "Android backend"),
+ PLUGIN_DEPENDS(CUSTOM, "libcharon"),
+ };
+
+ INIT(this,
+ .public = {
+ .update_status = _update_status,
+ .bypass_socket = _bypass_socket,
+ .get_trusted_certificates = _get_trusted_certificates,
+ .get_vpnservice_builder = _get_vpnservice_builder,
+ },
+ .attr = android_attr_create(),
+ .creds = android_creds_create(),
+ .builder = vpnservice_builder_create(builder),
+ .vpn_service = (*env)->NewGlobalRef(env, service),
+ );
+ charonservice = &this->public;
+
+ lib->plugins->add_static_features(lib->plugins, "androidbridge", features,
+ countof(features), TRUE);
+
+ lib->settings->set_int(lib->settings,
+ "charon.plugins.android_log.loglevel", ANDROID_DEBUG_LEVEL);
+ lib->settings->set_int(lib->settings,
+ "charon.retransmit_tries", ANDROID_RETRASNMIT_TRIES);
+ lib->settings->set_double(lib->settings,
+ "charon.retransmit_timeout", ANDROID_RETRANSMIT_TIMEOUT);
+ lib->settings->set_double(lib->settings,
+ "charon.retransmit_base", ANDROID_RETRANSMIT_BASE);
+ lib->settings->set_bool(lib->settings,
+ "charon.close_ike_on_child_failure", TRUE);
+ /* setting the source address breaks the VpnService.protect() function which
+ * uses SO_BINDTODEVICE internally. the addresses provided to the kernel as
+ * auxiliary data have precedence over this option causing a routing loop if
+ * the gateway is contained in the VPN routes. alternatively, providing an
+ * explicit device (in addition or instead of the source address) in the
+ * auxiliary data would also work, but we currently don't have that
+ * information */
+ lib->settings->set_bool(lib->settings,
+ "charon.plugins.socket-default.set_source", FALSE);
+}
+
+/**
+ * Deinitialize the charonservice object
+ */
+static void charonservice_deinit(JNIEnv *env)
+{
+ private_charonservice_t *this = (private_charonservice_t*)charonservice;
+
+ this->builder->destroy(this->builder);
+ this->creds->destroy(this->creds);
+ this->attr->destroy(this->attr);
+ (*env)->DeleteGlobalRef(env, this->vpn_service);
+ free(this);
+ charonservice = NULL;
+}
+
+/**
+ * Handle SIGSEGV/SIGILL signals raised by threads
+ */
+static void segv_handler(int signal)
+{
+ dbg_android(DBG_DMN, 1, "thread %u received %d", thread_current_id(),
+ signal);
+ exit(1);
+}
+
+/**
* Initialize charon and the libraries via JNI
*/
-JNI_METHOD(CharonVpnService, initializeCharon, void)
+JNI_METHOD(CharonVpnService, initializeCharon, void,
+ jobject builder, jstring jlogfile)
{
+ struct sigaction action;
+ struct utsname utsname;
+ char *logfile;
+
/* logging for library during initialization, as we have no bus yet */
dbg = dbg_android;
@@ -97,28 +414,78 @@ JNI_METHOD(CharonVpnService, initializeCharon, void)
return;
}
- if (!libcharon_init("charon") ||
- !charon->initialize(charon, PLUGINS))
+ if (!libcharon_init("charon"))
+ {
+ libcharon_deinit();
+ libipsec_deinit();
+ libhydra_deinit();
+ library_deinit();
+ return;
+ }
+
+ logfile = androidjni_convert_jstring(env, jlogfile);
+ initialize_logger(logfile);
+ free(logfile);
+
+ charonservice_init(env, this, builder);
+
+ if (uname(&utsname) != 0)
+ {
+ memset(&utsname, 0, sizeof(utsname));
+ }
+ DBG1(DBG_DMN, "Starting IKE charon daemon (strongSwan "VERSION", %s %s, %s)",
+ utsname.sysname, utsname.release, utsname.machine);
+
+ if (!charon->initialize(charon, PLUGINS))
{
libcharon_deinit();
+ charonservice_deinit(env);
libipsec_deinit();
libhydra_deinit();
library_deinit();
return;
}
+ /* add handler for SEGV and ILL etc. */
+ action.sa_handler = segv_handler;
+ action.sa_flags = 0;
+ sigemptyset(&action.sa_mask);
+ sigaction(SIGSEGV, &action, NULL);
+ sigaction(SIGILL, &action, NULL);
+ sigaction(SIGBUS, &action, NULL);
+ action.sa_handler = SIG_IGN;
+ sigaction(SIGPIPE, &action, NULL);
+
/* start daemon (i.e. the threads in the thread-pool) */
charon->start(charon);
}
/**
- * Initialize charon and the libraries via JNI
+ * Deinitialize charon and all libraries
*/
JNI_METHOD(CharonVpnService, deinitializeCharon, void)
{
+ /* deinitialize charon before we destroy our own objects */
libcharon_deinit();
+ charonservice_deinit(env);
libipsec_deinit();
libhydra_deinit();
library_deinit();
}
+/**
+ * Initiate SA
+ */
+JNI_METHOD(CharonVpnService, initiate, void,
+ jstring jlocal_address, jstring jgateway, jstring jusername,
+ jstring jpassword)
+{
+ char *local_address, *gateway, *username, *password;
+
+ local_address = androidjni_convert_jstring(env, jlocal_address);
+ gateway = androidjni_convert_jstring(env, jgateway);
+ username = androidjni_convert_jstring(env, jusername);
+ password = androidjni_convert_jstring(env, jpassword);
+
+ initiate(local_address, gateway, username, password);
+}
diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.h b/src/frontends/android/jni/libandroidbridge/charonservice.h
new file mode 100644
index 000000000..706eaa220
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/charonservice.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+/**
+ * @defgroup libandroidbridge libandroidbridge
+ *
+ * @defgroup android_backend backend
+ * @ingroup libandroidbridge
+ *
+ * @defgroup android_kernel kernel
+ * @ingroup libandroidbridge
+ *
+ * @defgroup charonservice charonservice
+ * @{ @ingroup libandroidbridge
+ */
+
+#ifndef CHARONSERVICE_H_
+#define CHARONSERVICE_H_
+
+#include "vpnservice_builder.h"
+
+#include <library.h>
+#include <utils/linked_list.h>
+
+typedef enum android_vpn_state_t android_vpn_state_t;
+typedef struct charonservice_t charonservice_t;
+
+/**
+ * VPN status codes. As defined in CharonVpnService.java
+ */
+enum android_vpn_state_t {
+ CHARONSERVICE_CHILD_STATE_UP = 1,
+ CHARONSERVICE_CHILD_STATE_DOWN,
+ CHARONSERVICE_AUTH_ERROR,
+ CHARONSERVICE_PEER_AUTH_ERROR,
+ CHARONSERVICE_LOOKUP_ERROR,
+ CHARONSERVICE_UNREACHABLE_ERROR,
+ CHARONSERVICE_GENERIC_ERROR,
+};
+
+/**
+ * Public interface of charonservice.
+ *
+ * Used to communicate with CharonVpnService via JNI
+ */
+struct charonservice_t {
+
+ /**
+ * Update the status in the Java domain (UI)
+ *
+ * @param code status code
+ * @return TRUE on success
+ */
+ bool (*update_status)(charonservice_t *this, android_vpn_state_t code);
+
+ /**
+ * Install a bypass policy for the given socket using the protect() Method
+ * of the Android VpnService interface
+ *
+ * @param fd socket file descriptor
+ * @param family socket protocol family
+ * @return TRUE if operation successful
+ */
+ bool (*bypass_socket)(charonservice_t *this, int fd, int family);
+
+ /**
+ * Get a list of trusted certificates via JNI
+ *
+ * @return list of DER encoded certificates (as chunk_t*),
+ * NULL on failure
+ */
+ linked_list_t *(*get_trusted_certificates)(charonservice_t *this);
+
+ /**
+ * Get the current vpnservice_builder_t object
+ *
+ * @return VpnService.Builder instance
+ */
+ vpnservice_builder_t *(*get_vpnservice_builder)(charonservice_t *this);
+
+};
+
+/**
+ * The single instance of charonservice_t.
+ *
+ * Set between JNI calls to initializeCharon() and deinitializeCharon().
+ */
+extern charonservice_t *charonservice;
+
+#endif /** CHARONSERVICE_H_ @}*/
diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c
new file mode 100644
index 000000000..08cc61610
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+#include "android_ipsec.h"
+#include "../charonservice.h"
+
+#include <debug.h>
+#include <library.h>
+#include <hydra.h>
+#include <ipsec.h>
+
+typedef struct private_kernel_android_ipsec_t private_kernel_android_ipsec_t;
+
+struct private_kernel_android_ipsec_t {
+
+ /**
+ * Public kernel interface
+ */
+ kernel_android_ipsec_t public;
+
+ /**
+ * Listener for lifetime expire events
+ */
+ ipsec_event_listener_t ipsec_listener;
+};
+
+/**
+ * Callback registrered with libipsec.
+ */
+void expire(u_int32_t reqid, u_int8_t protocol, u_int32_t spi, bool hard)
+{
+ hydra->kernel_interface->expire(hydra->kernel_interface, reqid, protocol,
+ spi, hard);
+}
+
+METHOD(kernel_ipsec_t, get_spi, status_t,
+ private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
+ u_int8_t protocol, u_int32_t reqid, u_int32_t *spi)
+{
+ return ipsec->sas->get_spi(ipsec->sas, src, dst, protocol, reqid, spi);
+}
+
+METHOD(kernel_ipsec_t, get_cpi, status_t,
+ private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
+ u_int32_t reqid, u_int16_t *cpi)
+{
+ return NOT_SUPPORTED;
+}
+
+METHOD(kernel_ipsec_t, add_sa, status_t,
+ private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
+ u_int32_t spi, u_int8_t protocol, u_int32_t reqid, mark_t mark,
+ u_int32_t tfc, lifetime_cfg_t *lifetime, u_int16_t enc_alg, chunk_t enc_key,
+ u_int16_t int_alg, chunk_t int_key, ipsec_mode_t mode, u_int16_t ipcomp,
+ u_int16_t cpi, bool encap, bool esn, bool inbound,
+ traffic_selector_t *src_ts, traffic_selector_t *dst_ts)
+{
+ return ipsec->sas->add_sa(ipsec->sas, src, dst, spi, protocol, reqid, mark,
+ tfc, lifetime, enc_alg, enc_key, int_alg, int_key,
+ mode, ipcomp, cpi, encap, esn, inbound, src_ts,
+ dst_ts);
+}
+
+METHOD(kernel_ipsec_t, update_sa, status_t,
+ private_kernel_android_ipsec_t *this, u_int32_t spi, u_int8_t protocol,
+ u_int16_t cpi, host_t *src, host_t *dst, host_t *new_src, host_t *new_dst,
+ bool encap, bool new_encap, mark_t mark)
+{
+ return NOT_SUPPORTED;
+}
+
+METHOD(kernel_ipsec_t, query_sa, status_t,
+ private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
+ u_int32_t spi, u_int8_t protocol, mark_t mark, u_int64_t *bytes)
+{
+ return NOT_SUPPORTED;
+}
+
+METHOD(kernel_ipsec_t, del_sa, status_t,
+ private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
+ u_int32_t spi, u_int8_t protocol, u_int16_t cpi, mark_t mark)
+{
+ return ipsec->sas->del_sa(ipsec->sas, src, dst, spi, protocol, cpi, mark);
+}
+
+METHOD(kernel_ipsec_t, flush_sas, status_t,
+ private_kernel_android_ipsec_t *this)
+{
+ return ipsec->sas->flush_sas(ipsec->sas);
+}
+
+METHOD(kernel_ipsec_t, add_policy, status_t,
+ private_kernel_android_ipsec_t *this, host_t *src, host_t *dst,
+ traffic_selector_t *src_ts, traffic_selector_t *dst_ts,
+ policy_dir_t direction, policy_type_t type, ipsec_sa_cfg_t *sa, mark_t mark,
+ policy_priority_t priority)
+{
+ return ipsec->policies->add_policy(ipsec->policies, src, dst, src_ts,
+ dst_ts, direction, type, sa, mark,
+ priority);
+}
+
+METHOD(kernel_ipsec_t, query_policy, status_t,
+ private_kernel_android_ipsec_t *this, traffic_selector_t *src_ts,
+ traffic_selector_t *dst_ts, policy_dir_t direction, mark_t mark,
+ u_int32_t *use_time)
+{
+ return NOT_SUPPORTED;
+}
+
+METHOD(kernel_ipsec_t, del_policy, status_t,
+ private_kernel_android_ipsec_t *this, traffic_selector_t *src_ts,
+ traffic_selector_t *dst_ts, policy_dir_t direction, u_int32_t reqid,
+ mark_t mark, policy_priority_t priority)
+{
+ return ipsec->policies->del_policy(ipsec->policies, src_ts, dst_ts,
+ direction, reqid, mark, priority);
+}
+
+METHOD(kernel_ipsec_t, flush_policies, status_t,
+ private_kernel_android_ipsec_t *this)
+{
+ ipsec->policies->flush_policies(ipsec->policies);
+ return SUCCESS;
+}
+
+METHOD(kernel_ipsec_t, bypass_socket, bool,
+ private_kernel_android_ipsec_t *this, int fd, int family)
+{
+ return charonservice->bypass_socket(charonservice, fd, family);
+}
+
+METHOD(kernel_ipsec_t, enable_udp_decap, bool,
+ private_kernel_android_ipsec_t *this, int fd, int family, u_int16_t port)
+{
+ return NOT_SUPPORTED;
+}
+
+METHOD(kernel_ipsec_t, destroy, void,
+ private_kernel_android_ipsec_t *this)
+{
+ ipsec->events->unregister_listener(ipsec->events, &this->ipsec_listener);
+ free(this);
+}
+
+/*
+ * Described in header.
+ */
+kernel_android_ipsec_t *kernel_android_ipsec_create()
+{
+ private_kernel_android_ipsec_t *this;
+
+ INIT(this,
+ .public = {
+ .interface = {
+ .get_spi = _get_spi,
+ .get_cpi = _get_cpi,
+ .add_sa = _add_sa,
+ .update_sa = _update_sa,
+ .query_sa = _query_sa,
+ .del_sa = _del_sa,
+ .flush_sas = _flush_sas,
+ .add_policy = _add_policy,
+ .query_policy = _query_policy,
+ .del_policy = _del_policy,
+ .flush_policies = _flush_policies,
+ .bypass_socket = _bypass_socket,
+ .enable_udp_decap = _enable_udp_decap,
+ .destroy = _destroy,
+ },
+ },
+ .ipsec_listener = {
+ .expire = expire,
+ },
+ );
+
+ ipsec->events->register_listener(ipsec->events, &this->ipsec_listener);
+
+ return &this->public;
+}
diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h
new file mode 100644
index 000000000..3a2e8343f
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+/**
+ * @defgroup kernel_android_ipsec kernel_android_ipsec
+ * @{ @ingroup kernel_android
+ */
+
+#ifndef KERNEL_ANDROID_IPSEC_H_
+#define KERNEL_ANDROID_IPSEC_H_
+
+#include <library.h>
+#include <kernel/kernel_ipsec.h>
+
+typedef struct kernel_android_ipsec_t kernel_android_ipsec_t;
+
+/**
+ * Implementation of the ipsec interface using libipsec on Android
+ */
+struct kernel_android_ipsec_t {
+
+ /**
+ * Implements kernel_ipsec_t interface
+ */
+ kernel_ipsec_t interface;
+};
+
+/**
+ * Create a android ipsec interface instance.
+ *
+ * @return kernel_android_ipsec_t instance
+ */
+kernel_android_ipsec_t *kernel_android_ipsec_create();
+
+#endif /** KERNEL_ANDROID_IPSEC_H_ @}*/
diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_net.c b/src/frontends/android/jni/libandroidbridge/kernel/android_net.c
new file mode 100644
index 000000000..e29f95510
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/kernel/android_net.c
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "android_net.h"
+
+typedef struct private_kernel_android_net_t private_kernel_android_net_t;
+
+struct private_kernel_android_net_t {
+
+ /**
+ * Public kernel interface
+ */
+ kernel_android_net_t public;
+};
+
+METHOD(kernel_net_t, add_ip, status_t,
+ private_kernel_android_net_t *this, host_t *virtual_ip, host_t *iface_ip)
+{
+ /* we get the IP from the IKE_SA once the CHILD_SA is established */
+ return SUCCESS;
+}
+
+METHOD(kernel_net_t, destroy, void,
+ private_kernel_android_net_t *this)
+{
+ free(this);
+}
+
+/*
+ * Described in header.
+ */
+kernel_android_net_t *kernel_android_net_create()
+{
+ private_kernel_android_net_t *this;
+
+ INIT(this,
+ .public = {
+ .interface = {
+ .get_source_addr = (void*)return_null,
+ .get_nexthop = (void*)return_null,
+ .get_interface = (void*)return_null,
+ .create_address_enumerator = (void*)enumerator_create_empty,
+ .add_ip = _add_ip,
+ .del_ip = (void*)return_failed,
+ .add_route = (void*)return_failed,
+ .del_route = (void*)return_failed,
+ .destroy = _destroy,
+ },
+ },
+ );
+
+ return &this->public;
+};
diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_net.h b/src/frontends/android/jni/libandroidbridge/kernel/android_net.h
new file mode 100644
index 000000000..470029fad
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/kernel/android_net.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+/**
+ * @defgroup kernel_android_net kernel_android_net
+ * @{ @ingroup kernel_android
+ */
+
+#ifndef KERNEL_ANDROID_NET_H_
+#define KERNEL_ANDROID_NET_H_
+
+#include <library.h>
+#include <kernel/kernel_net.h>
+
+typedef struct kernel_android_net_t kernel_android_net_t;
+
+/**
+ * Implementation of the kernel-net interface. This currently consists of only
+ * noops because a kernel_net_t implementation is required and we can't use
+ * kernel_netlink_net_t at the moment.
+ */
+struct kernel_android_net_t {
+
+ /**
+ * Implements kernel_net_t interface
+ */
+ kernel_net_t interface;
+};
+
+/**
+ * Create a android net interface instance.
+ *
+ * @return kernel_android_net_t instance
+ */
+kernel_android_net_t *kernel_android_net_create();
+
+#endif /** KERNEL_ANDROID_NET_H_ @}*/
diff --git a/src/frontends/android/jni/libandroidbridge/vpnservice_builder.c b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.c
new file mode 100644
index 000000000..6ff732520
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.c
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+#include "vpnservice_builder.h"
+#include "android_jni.h"
+
+#include <debug.h>
+#include <library.h>
+
+typedef struct private_vpnservice_builder_t private_vpnservice_builder_t;
+
+/**
+ * private data of vpnservice_builder
+ */
+struct private_vpnservice_builder_t {
+
+ /**
+ * public interface
+ */
+ vpnservice_builder_t public;
+
+ /**
+ * Java object
+ */
+ jobject builder;
+};
+
+METHOD(vpnservice_builder_t, add_address, bool,
+ private_vpnservice_builder_t *this, host_t *addr)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+ jstring str;
+ char buf[INET_ADDRSTRLEN];
+
+ androidjni_attach_thread(&env);
+
+ DBG2(DBG_LIB, "builder: adding interface address %H", addr);
+
+ if (addr->get_family(addr) != AF_INET)
+ {
+ goto failed;
+ }
+ if (snprintf(buf, sizeof(buf), "%H", addr) >= sizeof(buf))
+ {
+ goto failed;
+ }
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
+ "addAddress", "(Ljava/lang/String;I)Z");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ str = (*env)->NewStringUTF(env, buf);
+ if (!str)
+ {
+ goto failed;
+ }
+ if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str, 32))
+ {
+ goto failed;
+ }
+ androidjni_detach_thread();
+ return TRUE;
+
+failed:
+ DBG1(DBG_LIB, "builder: failed to add address");
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return FALSE;
+}
+
+METHOD(vpnservice_builder_t, set_mtu, bool,
+ private_vpnservice_builder_t *this, int mtu)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+
+ androidjni_attach_thread(&env);
+
+ DBG2(DBG_LIB, "builder: setting MTU to %d", mtu);
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
+ "setMtu", "(I)Z");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ if (!(*env)->CallBooleanMethod(env, this->builder, method_id, mtu))
+ {
+ goto failed;
+ }
+ androidjni_detach_thread();
+ return TRUE;
+
+failed:
+ DBG1(DBG_LIB, "builder: failed to set MTU");
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return FALSE;
+}
+
+METHOD(vpnservice_builder_t, add_route, bool,
+ private_vpnservice_builder_t *this, host_t *net, int prefix)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+ jstring str;
+ char buf[INET_ADDRSTRLEN];
+
+ androidjni_attach_thread(&env);
+
+ DBG2(DBG_LIB, "builder: adding route %+H/%d", net, prefix);
+
+ if (net->get_family(net) != AF_INET)
+ {
+ goto failed;
+ }
+ if (snprintf(buf, sizeof(buf), "%+H", net) >= sizeof(buf))
+ {
+ goto failed;
+ }
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
+ "addRoute", "(Ljava/lang/String;I)Z");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ str = (*env)->NewStringUTF(env, buf);
+ if (!str)
+ {
+ goto failed;
+ }
+ if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str, prefix))
+ {
+ goto failed;
+ }
+ androidjni_detach_thread();
+ return TRUE;
+
+failed:
+ DBG1(DBG_LIB, "builder: failed to add route");
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return FALSE;
+}
+
+METHOD(vpnservice_builder_t, add_dns, bool,
+ private_vpnservice_builder_t *this, host_t *dns)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+ jstring str;
+ char buf[INET_ADDRSTRLEN];
+
+ androidjni_attach_thread(&env);
+
+ DBG2(DBG_LIB, "builder: adding DNS server %H", dns);
+
+ if (dns->get_family(dns) != AF_INET)
+ {
+ goto failed;
+ }
+ if (snprintf(buf, sizeof(buf), "%H", dns) >= sizeof(buf))
+ {
+ goto failed;
+ }
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
+ "addDnsServer", "(Ljava/lang/String;)Z");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ str = (*env)->NewStringUTF(env, buf);
+ if (!str)
+ {
+ goto failed;
+ }
+ if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str))
+ {
+ goto failed;
+ }
+ androidjni_detach_thread();
+ return TRUE;
+
+failed:
+ DBG1(DBG_LIB, "builder: failed to add DNS server");
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return FALSE;
+}
+
+METHOD(vpnservice_builder_t, establish, int,
+ private_vpnservice_builder_t *this)
+{
+ JNIEnv *env;
+ jmethodID method_id;
+ int fd;
+
+ androidjni_attach_thread(&env);
+
+ DBG2(DBG_LIB, "builder: building TUN device");
+
+ method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class,
+ "establish", "()I");
+ if (!method_id)
+ {
+ goto failed;
+ }
+ fd = (*env)->CallIntMethod(env, this->builder, method_id);
+ if (fd == -1)
+ {
+ goto failed;
+ }
+ androidjni_detach_thread();
+ return fd;
+
+failed:
+ DBG1(DBG_LIB, "builder: failed to build TUN device");
+ androidjni_exception_occurred(env);
+ androidjni_detach_thread();
+ return -1;
+}
+
+METHOD(vpnservice_builder_t, destroy, void,
+ private_vpnservice_builder_t *this)
+{
+ JNIEnv *env;
+
+ androidjni_attach_thread(&env);
+ (*env)->DeleteGlobalRef(env, this->builder);
+ androidjni_detach_thread();
+ free(this);
+}
+
+vpnservice_builder_t *vpnservice_builder_create(jobject builder)
+{
+ JNIEnv *env;
+ private_vpnservice_builder_t *this;
+
+ INIT(this,
+ .public = {
+ .add_address = _add_address,
+ .add_route = _add_route,
+ .add_dns = _add_dns,
+ .set_mtu = _set_mtu,
+ .establish = _establish,
+ .destroy = _destroy,
+ },
+ );
+
+ androidjni_attach_thread(&env);
+ this->builder = (*env)->NewGlobalRef(env, builder);
+ androidjni_detach_thread();
+
+ return &this->public;
+}
diff --git a/src/frontends/android/jni/libandroidbridge/vpnservice_builder.h b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.h
new file mode 100644
index 000000000..82efd05f7
--- /dev/null
+++ b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.
+ */
+
+/**
+ * @defgroup vpnservice_builder vpnservice_builder
+ * @{ @ingroup libandroidbridge
+ */
+
+#ifndef VPNSERVICE_BUILDER_H_
+#define VPNSERVICE_BUILDER_H_
+
+#include <jni.h>
+
+#include <library.h>
+#include <utils/host.h>
+
+typedef struct vpnservice_builder_t vpnservice_builder_t;
+
+/**
+ * VpnService.Builder, used to build a TUN device.
+ *
+ * Communicates with CharonVpnService.BuilderAdapter via JNI
+ */
+struct vpnservice_builder_t {
+
+ /**
+ * Add an interface address
+ *
+ * @param addr the desired interface address
+ * @return TRUE on success
+ */
+ bool (*add_address)(vpnservice_builder_t *this, host_t *addr);
+
+ /**
+ * Add a route
+ *
+ * @param net the network address
+ * @param prefix_length the prefix length
+ * @return TRUE on success
+ */
+ bool (*add_route)(vpnservice_builder_t *this, host_t *net, int prefix);
+
+ /**
+ * Add a DNS server
+ *
+ * @param dns the address of the DNS server
+ * @return TRUE on success
+ */
+ bool (*add_dns)(vpnservice_builder_t *this, host_t *dns);
+
+ /**
+ * Set the MTU for the TUN device
+ *
+ * @param mtu the MTU to set
+ * @return TRUE on success
+ */
+ bool (*set_mtu)(vpnservice_builder_t *this, int mtu);
+
+ /**
+ * Build the TUN device
+ *
+ * @return the TUN file descriptor, -1 if failed
+ */
+ int (*establish)(vpnservice_builder_t *this);
+
+ /**
+ * Destroy a vpnservice_builder
+ */
+ void (*destroy)(vpnservice_builder_t *this);
+
+};
+
+/**
+ * Create a vpnservice_builder instance
+ *
+ * @param builder CharonVpnService.BuilderAdapter object
+ * @return vpnservice_builder_t instance
+ */
+vpnservice_builder_t *vpnservice_builder_create(jobject builder);
+
+#endif /** VPNSERVICE_BUILDER_H_ @}*/
diff --git a/src/frontends/android/res/drawable-hdpi/ic_launcher.png b/src/frontends/android/res/drawable-hdpi/ic_launcher.png
index 8074c4c57..7cd1df4ee 100644
--- a/src/frontends/android/res/drawable-hdpi/ic_launcher.png
+++ b/src/frontends/android/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/src/frontends/android/res/drawable-ldpi/ic_launcher.png b/src/frontends/android/res/drawable-ldpi/ic_launcher.png
deleted file mode 100644
index 1095584ec..000000000
--- a/src/frontends/android/res/drawable-ldpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/src/frontends/android/res/drawable-mdpi/ic_launcher.png b/src/frontends/android/res/drawable-mdpi/ic_launcher.png
index a07c69fa5..200ee9677 100644
--- a/src/frontends/android/res/drawable-mdpi/ic_launcher.png
+++ b/src/frontends/android/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/src/frontends/android/res/drawable-xhdpi/ic_launcher.png b/src/frontends/android/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..2eb6db1b6
--- /dev/null
+++ b/src/frontends/android/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/src/frontends/android/res/drawable/vpn_state_background.xml b/src/frontends/android/res/drawable/vpn_state_background.xml
new file mode 100644
index 000000000..24f469add
--- /dev/null
+++ b/src/frontends/android/res/drawable/vpn_state_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <solid
+ android:color="#333" />
+
+</shape> \ No newline at end of file
diff --git a/src/frontends/android/res/layout/log_activity.xml b/src/frontends/android/res/layout/log_activity.xml
new file mode 100644
index 000000000..80fee09fb
--- /dev/null
+++ b/src/frontends/android/res/layout/log_activity.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <fragment
+ class="org.strongswan.android.ui.LogFragment"
+ android:id="@+id/log_frag"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
diff --git a/src/frontends/android/res/layout/log_fragment.xml b/src/frontends/android/res/layout/log_fragment.xml
new file mode 100644
index 000000000..c2e187a66
--- /dev/null
+++ b/src/frontends/android/res/layout/log_fragment.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <org.strongswan.android.ui.LogScrollView
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="10dp"
+ android:scrollbarFadeDuration="0"
+ android:scrollbarAlwaysDrawVerticalTrack="true" >
+
+ <TextView
+ android:id="@+id/log_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="9sp"
+ android:typeface="monospace" >
+ </TextView>
+
+ </org.strongswan.android.ui.LogScrollView>
+
+</LinearLayout>
diff --git a/src/frontends/android/res/layout/login_dialog.xml b/src/frontends/android/res/layout/login_dialog.xml
new file mode 100644
index 000000000..0262af0a3
--- /dev/null
+++ b/src/frontends/android/res/layout/login_dialog.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
+<LinearLayout
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:padding="5dp"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/profile_username_label"
+ android:textStyle="bold" />
+
+ <EditText
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:id="@+id/username"
+ android:enabled="false"
+ android:inputType="none" />
+
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/profile_password_label"
+ android:textStyle="bold" />
+
+ <EditText
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:id="@+id/password"
+ android:inputType="textPassword|textNoSuggestions"
+ android:singleLine="true" />
+
+</LinearLayout>
diff --git a/src/frontends/android/res/layout/main.xml b/src/frontends/android/res/layout/main.xml
index bc12cd823..1c7973e20 100644
--- a/src/frontends/android/res/layout/main.xml
+++ b/src/frontends/android/res/layout/main.xml
@@ -1,12 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:orientation="vertical" >
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="@string/hello" />
+ <fragment
+ class="org.strongswan.android.ui.VpnStateFragment"
+ android:id="@+id/vpn_state_frag"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <fragment
+ class="org.strongswan.android.ui.VpnProfileListFragment"
+ android:id="@+id/profile_list_frag"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
-</LinearLayout> \ No newline at end of file
+</LinearLayout>
diff --git a/src/frontends/android/res/layout/profile_detail_view.xml b/src/frontends/android/res/layout/profile_detail_view.xml
new file mode 100644
index 000000000..4952ebaa5
--- /dev/null
+++ b/src/frontends/android/res/layout/profile_detail_view.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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"
+ 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" >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/profile_name_label" />
+
+ <EditText
+ android:id="@+id/name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions"
+ android:hint="@string/profile_name_hint" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/profile_gateway_label" />
+
+ <EditText
+ android:id="@+id/gateway"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/profile_username_label" />
+
+ <EditText
+ android:id="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/profile_password_label" />
+
+ <EditText
+ 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_hint" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/profile_ca_label" />
+
+ <CheckBox
+ android:id="@+id/ca_auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/profile_ca_auto_label" />
+
+ <CheckBox
+ android:id="@+id/ca_show_all"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:text="@string/profile_ca_show_all" />
+
+ <Spinner
+ android:id="@+id/ca_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="5dp" />
+
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/src/frontends/android/res/layout/profile_list_fragment.xml b/src/frontends/android/res/layout/profile_list_fragment.xml
new file mode 100644
index 000000000..50d628bfa
--- /dev/null
+++ b/src/frontends/android/res/layout/profile_list_fragment.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="10dp"
+ android:paddingTop="10dp"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp" >
+
+ <ListView
+ android:id="@+id/profile_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:dividerHeight="1dp"
+ android:divider="?android:attr/listDivider"
+ android:scrollbarAlwaysDrawVerticalTrack="true" />
+
+ <TextView android:id="@+id/profile_list_empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="15dp"
+ android:text="@string/no_profiles"/>
+
+</FrameLayout>
diff --git a/src/frontends/android/res/layout/profile_list_item.xml b/src/frontends/android/res/layout/profile_list_item.xml
new file mode 100644
index 000000000..f55c8357a
--- /dev/null
+++ b/src/frontends/android/res/layout/profile_list_item.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="6dip"
+ android:paddingTop="4dip"
+ android:background="?android:attr/activatedBackgroundIndicator" >
+
+ <TextView
+ android:id="@+id/profile_item_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="15dp"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/profile_item_gateway"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_marginLeft="15dp" />
+
+ <TextView
+ android:id="@+id/profile_item_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_marginLeft="15dp" />
+
+</LinearLayout>
diff --git a/src/frontends/android/res/layout/trusted_certificates_item.xml b/src/frontends/android/res/layout/trusted_certificates_item.xml
new file mode 100644
index 000000000..48d77757d
--- /dev/null
+++ b/src/frontends/android/res/layout/trusted_certificates_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
+<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/certificate_name"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:textSize="18sp" />
+
+</TableLayout> \ No newline at end of file
diff --git a/src/frontends/android/res/layout/vpn_state_fragment.xml b/src/frontends/android/res/layout/vpn_state_fragment.xml
new file mode 100644
index 000000000..6353f3289
--- /dev/null
+++ b/src/frontends/android/res/layout/vpn_state_fragment.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="5dp"
+ android:background="@drawable/vpn_state_background"
+ android:orientation="vertical" >
+
+ <GridLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:layout_marginLeft="20dp"
+ android:layout_marginRight="20dp"
+ android:layout_marginTop="10dp"
+ android:columnCount="2"
+ android:rowCount="2" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:gravity="top"
+ android:text="@string/state_label"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="20sp" />
+
+ <TextView
+ android:id="@+id/vpn_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="top"
+ android:text="@string/state_disabled"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="20sp" />
+
+ <TextView
+ android:id="@+id/vpn_profile_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:gravity="top"
+ android:text="@string/profile_label"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="20sp"
+ android:visibility="gone" >
+ </TextView>
+
+ <TextView
+ android:id="@+id/vpn_profile_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="top"
+ android:textSize="20sp"
+ android:visibility="gone" >
+ </TextView>
+ </GridLayout>
+
+ <Button
+ android:id="@+id/action"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:layout_marginLeft="20dp"
+ android:layout_marginRight="20dp"
+ android:text="@string/disconnect"
+ style="?android:attr/borderlessButtonStyle" >
+ </Button>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="?android:attr/listDivider" />
+
+</LinearLayout>
diff --git a/src/frontends/android/res/menu/log.xml b/src/frontends/android/res/menu/log.xml
new file mode 100644
index 000000000..1af5bd397
--- /dev/null
+++ b/src/frontends/android/res/menu/log.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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">
+
+ <item
+ android:id="@+id/menu_send_log"
+ android:title="@string/send_log"
+ android:showAsAction="ifRoom|withText" />
+
+</menu>
diff --git a/src/frontends/android/res/menu/main.xml b/src/frontends/android/res/menu/main.xml
new file mode 100644
index 000000000..4063110da
--- /dev/null
+++ b/src/frontends/android/res/menu/main.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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" >
+
+ <item
+ android:id="@+id/menu_reload_certs"
+ android:title="@string/reload_trusted_certs"
+ android:showAsAction="withText" />
+
+ <item
+ android:id="@+id/menu_show_log"
+ android:title="@string/show_log"
+ android:showAsAction="withText" />
+
+</menu> \ No newline at end of file
diff --git a/src/frontends/android/res/menu/profile_edit.xml b/src/frontends/android/res/menu/profile_edit.xml
new file mode 100644
index 000000000..e69020ed0
--- /dev/null
+++ b/src/frontends/android/res/menu/profile_edit.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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" >
+
+ <item
+ android:id="@+id/menu_accept"
+ android:title="@string/profile_edit_save"
+ android:showAsAction="always|withText" />
+
+ <item
+ android:id="@+id/menu_cancel"
+ android:title="@string/profile_edit_cancel"
+ android:showAsAction="ifRoom" />
+
+</menu>
diff --git a/src/frontends/android/res/menu/profile_list.xml b/src/frontends/android/res/menu/profile_list.xml
new file mode 100644
index 000000000..57c9a86a4
--- /dev/null
+++ b/src/frontends/android/res/menu/profile_list.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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" >
+
+ <item android:id="@+id/add_profile"
+ android:title="@string/add_profile"
+ android:showAsAction="always|withText" />
+
+</menu>
diff --git a/src/frontends/android/res/menu/profile_list_context.xml b/src/frontends/android/res/menu/profile_list_context.xml
new file mode 100644
index 000000000..e674ae856
--- /dev/null
+++ b/src/frontends/android/res/menu/profile_list_context.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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" >
+
+ <item android:id="@+id/edit_profile"
+ android:title="@string/edit_profile" ></item>
+
+ <item android:id="@+id/delete_profile"
+ android:title="@string/delete_profile" ></item>
+
+</menu> \ No newline at end of file
diff --git a/src/frontends/android/res/values-de/strings.xml b/src/frontends/android/res/values-de/strings.xml
new file mode 100644
index 000000000..0e0ddd583
--- /dev/null
+++ b/src/frontends/android/res/values-de/strings.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
+<resources>
+
+ <!-- Application -->
+ <string name="app_name">strongSwan VPN Client</string>
+ <string name="main_activity_name">strongSwan</string>
+ <string name="reload_trusted_certs">CA-Zertifikate neu laden</string>
+ <string name="show_log">Log anzeigen</string>
+
+ <!-- Log view -->
+ <string name="log_title">Log</string>
+ <string name="send_log">Logdatei senden</string>
+ <string name="empty_log">Logdatei ist leer</string>
+ <string name="log_mail_subject">strongSwan %1$s Logdatei</string>
+
+ <!-- VPN profile list -->
+ <string name="no_profiles">Keine VPN Profile vorhanden.</string>
+ <string name="add_profile">Profil hinzufügen</string>
+ <string name="edit_profile">Bearbeiten</string>
+ <string name="delete_profile">Löschen</string>
+ <string name="select_profiles">Profile auswählen</string>
+ <string name="profiles_deleted">Ausgewählte Profile gelöscht</string>
+ <string name="no_profile_selected">Kein Profil ausgewählt</string>
+ <string name="one_profile_selected">Ein Profil ausgewählt</string>
+ <string name="x_profiles_selected">%1$d Profile ausgewählt</string>
+
+ <!-- VPN profile details -->
+ <string name="profile_edit_save">Speichern</string>
+ <string name="profile_edit_cancel">Abbrechen</string>
+ <string name="profile_name_label">Profilname:</string>
+ <string name="profile_name_hint">(Gateway-Adresse verwenden)</string>
+ <string name="profile_gateway_label">Gateway:</string>
+ <string name="profile_username_label">Benutzername:</string>
+ <string name="profile_password_label">Passwort:</string>
+ <string name="profile_password_hint">(anfordern wenn benötigt)</string>
+ <string name="profile_ca_label">CA-Zertifikat:</string>
+ <string name="profile_ca_auto_label">Automatisch wählen</string>
+ <string name="profile_ca_show_all">Alle Zertifikate anzeigen</string>
+ <!-- Warnings/Notifications in the details view -->
+ <string name="alert_text_no_input_gateway">Bitte geben Sie hier die Gateway-Adresse ein</string>
+ <string name="alert_text_no_input_username">Bitte geben Sie hier Ihren Benutzernamen ein</string>
+ <string name="alert_text_nocertfound_title">Kein CA-Zertifikat ausgewählt</string>
+ <string name="alert_text_nocertfound">Bitte wählen Sie eines aus oder aktivieren Sie <i>Automatisch wählen</i></string>
+
+ <!-- VPN state fragment -->
+ <string name="state_label">Status:</string>
+ <string name="profile_label">Profil:</string>
+ <string name="disconnect">Trennen</string>
+ <string name="state_connecting">Verbinden&#8230;</string>
+ <string name="state_connected">Verbunden</string>
+ <string name="state_disconnecting">Trennen&#8230;</string>
+ <string name="state_disabled">Kein aktives Profil</string>
+ <string name="state_error">Fehler</string>
+
+ <!-- Dialogs -->
+ <string name="login_title">Passwort eingeben um zu verbinden</string>
+ <string name="login_confirm">Verbinden</string>
+ <string name="error_introduction">Fehler beim Aufsetzen des VPN:</string>
+ <string name="error_lookup_failed">Gateway-Adresse konnte nicht aufgelöst werden.</string>
+ <string name="error_unreachable">Gateway ist nicht erreichbar.</string>
+ <string name="error_peer_auth_failed">Authentifizierung des Gateway ist fehlgeschlagen.</string>
+ <string name="error_auth_failed">Benutzerauthentifizierung ist fehlgeschlagen.</string>
+ <string name="error_generic">Unbekannter Fehler während des Verbindens.</string>
+ <string name="connecting_title">Verbinden: %1$s</string>
+ <string name="connecting_message">Verbinde mit \""%1$s\".</string>
+
+</resources>
diff --git a/src/frontends/android/res/values/colors.xml b/src/frontends/android/res/values/colors.xml
new file mode 100644
index 000000000..be64d5d5a
--- /dev/null
+++ b/src/frontends/android/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+<resources>
+
+ <color
+ name="error_text">#D9192C</color>
+
+ <color
+ name="success_text">#99CC00</color>
+
+</resources>
diff --git a/src/frontends/android/res/values/strings.xml b/src/frontends/android/res/values/strings.xml
index f4df7613e..a83e219a7 100644
--- a/src/frontends/android/res/values/strings.xml
+++ b/src/frontends/android/res/values/strings.xml
@@ -1,7 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Tobias Brunner
+ Copyright (C) 2012 Giuliano Grassi
+ Copyright (C) 2012 Ralf Sager
+ 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.
+-->
<resources>
- <string name="hello">Hello World, strongSwanActivity!</string>
- <string name="app_name">strongSwan</string>
+ <!-- Application -->
+ <string name="app_name">strongSwan VPN Client</string>
+ <string name="main_activity_name">strongSwan</string>
+ <string name="reload_trusted_certs">Reload CA certificates</string>
+ <string name="show_log">View log</string>
+
+ <!-- Log view -->
+ <string name="log_title">Log</string>
+ <string name="send_log">Send log file</string>
+ <string name="empty_log">Log file is empty</string>
+ <string name="log_mail_subject">strongSwan %1$s Log File</string>
+
+ <!-- VPN profile list -->
+ <string name="no_profiles">No VPN profiles.</string>
+ <string name="add_profile">Add VPN profile</string>
+ <string name="edit_profile">Edit</string>
+ <string name="delete_profile">Delete</string>
+ <string name="select_profiles">Select profiles</string>
+ <string name="profiles_deleted">Selected profiles deleted</string>
+ <string name="no_profile_selected">No profile selected</string>
+ <string name="one_profile_selected">One profile selected</string>
+ <string name="x_profiles_selected">%1$d profiles selected"</string>
+
+ <!-- VPN profile details -->
+ <string name="profile_edit_save">Save</string>
+ <string name="profile_edit_cancel">Cancel</string>
+ <string name="profile_name_label">Profile Name:</string>
+ <string name="profile_name_hint">(use gateway address)</string>
+ <string name="profile_gateway_label">Gateway:</string>
+ <string name="profile_username_label">Username:</string>
+ <string name="profile_password_label">Password:</string>
+ <string name="profile_password_hint">(prompt when needed)</string>
+ <string name="profile_ca_label">CA certificate:</string>
+ <string name="profile_ca_auto_label">Select automatically</string>
+ <string name="profile_ca_show_all">Show all certificates</string>
+ <!-- Warnings/Notifications in the details view -->
+ <string name="alert_text_no_input_gateway">Please enter the gateway address here</string>
+ <string name="alert_text_no_input_username">Please enter your username here</string>
+ <string name="alert_text_nocertfound_title">No CA certificate selected</string>
+ <string name="alert_text_nocertfound">Please select one or activate <i>Select automatically</i></string>
+
+ <!-- VPN state fragment -->
+ <string name="state_label">Status:</string>
+ <string name="profile_label">Profile:</string>
+ <string name="disconnect">Disconnect</string>
+ <string name="state_connecting">Connecting&#8230;</string>
+ <string name="state_connected">Connected</string>
+ <string name="state_disconnecting">Disconnecting&#8230;</string>
+ <string name="state_disabled">No active VPN</string>
+ <string name="state_error">Error</string>
+
+ <!-- Dialogs -->
+ <string name="login_title">Enter password to connect</string>
+ <string name="login_confirm">Connect</string>
+ <string name="error_introduction">Failed to establish VPN:</string>
+ <string name="error_lookup_failed">Gateway address lookup failed.</string>
+ <string name="error_unreachable">Gateway is unreachable.</string>
+ <string name="error_peer_auth_failed">Verifying gateway authentication failed.</string>
+ <string name="error_auth_failed">User authentication failed.</string>
+ <string name="error_generic">Unspecified failure while connecting.</string>
+ <string name="connecting_title">Connecting: %1$s</string>
+ <string name="connecting_message">Establishing VPN with \""%1$s\".</string>
-</resources> \ No newline at end of file
+</resources>
diff --git a/src/frontends/android/res/values/styles.xml b/src/frontends/android/res/values/styles.xml
new file mode 100644
index 000000000..739ba7000
--- /dev/null
+++ b/src/frontends/android/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="ApplicationTheme" parent="@android:style/Theme.Holo">
+ </style>
+
+</resources>
diff --git a/src/frontends/android/src/org/strongswan/android/CharonVpnService.java b/src/frontends/android/src/org/strongswan/android/CharonVpnService.java
deleted file mode 100644
index d917d3eae..000000000
--- a/src/frontends/android/src/org/strongswan/android/CharonVpnService.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package org.strongswan.android;
-
-import android.content.Intent;
-import android.net.VpnService;
-
-public class CharonVpnService extends VpnService
-{
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId)
- {
- // called whenever the service is started with startService
- // create our own thread because we are running in the calling processes
- // main thread
- return super.onStartCommand(intent, flags, startId);
- }
-
- @Override
- public void onCreate()
- {
- // onCreate is only called once
- initializeCharon();
- super.onCreate();
- }
-
- @Override
- public void onDestroy()
- {
- // called once the service is to be destroyed
- deinitializeCharon();
- super.onDestroy();
- }
-
- /**
- * Initialization of charon, provided by libandroidbridge.so
- */
- public native void initializeCharon();
-
- /**
- * Deinitialize charon, provided by libandroidbridge.so
- */
- public native void deinitializeCharon();
-
- /*
- * The libraries are extracted to /data/data/org.strongswan.android/...
- * during installation.
- */
- static
- {
- System.loadLibrary("crypto");
- System.loadLibrary("strongswan");
- System.loadLibrary("hydra");
- System.loadLibrary("charon");
- System.loadLibrary("ipsec");
- System.loadLibrary("androidbridge");
- }
-}
diff --git a/src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java b/src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java
new file mode 100644
index 000000000..370a8d5e4
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2012 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.data;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.strongswan.android.logic.CharonVpnService;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.OpenableColumns;
+
+public class LogContentProvider extends ContentProvider
+{
+ private static final String AUTHORITY = "org.strongswan.android.content.log";
+ /* an Uri is valid for 30 minutes */
+ private static final long URI_VALIDITY = 30 * 60 * 1000;
+ private static ConcurrentHashMap<Uri, Long> mUris = new ConcurrentHashMap<Uri, Long>();
+ private File mLogFile;
+
+ public LogContentProvider()
+ {
+ }
+
+ @Override
+ public boolean onCreate()
+ {
+ mLogFile = new File(getContext().getFilesDir(), CharonVpnService.LOG_FILE);
+ return true;
+ }
+
+ /**
+ * The log file can only be accessed by Uris created with this method
+ * @return null if failed to create the Uri
+ */
+ public static Uri createContentUri()
+ {
+ SecureRandom random;
+ try
+ {
+ random = SecureRandom.getInstance("SHA1PRNG");
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ return null;
+ }
+ Uri uri = Uri.parse("content://" + AUTHORITY + "/" + random.nextLong());
+ mUris.put(uri, SystemClock.uptimeMillis());
+ return uri;
+ }
+
+ @Override
+ public String getType(Uri uri)
+ {
+ /* MIME type for our log file */
+ return "text/plain";
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder)
+ {
+ /* this is called by apps to find out the name and size of the file.
+ * since we only provide a single file this is simple to implement */
+ if (projection == null || projection.length < 1)
+ {
+ return null;
+ }
+ Long timestamp = mUris.get(uri);
+ if (timestamp == null)
+ { /* don't check the validity as this information is not really private */
+ return null;
+ }
+ MatrixCursor cursor = new MatrixCursor(projection, 1);
+ if (OpenableColumns.DISPLAY_NAME.equals(cursor.getColumnName(0)))
+ {
+ cursor.newRow().add(CharonVpnService.LOG_FILE);
+ }
+ else if (OpenableColumns.SIZE.equals(cursor.getColumnName(0)))
+ {
+ cursor.newRow().add(mLogFile.length());
+ }
+ else
+ {
+ return null;
+ }
+ return cursor;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException
+ {
+ Long timestamp = mUris.get(uri);
+ if (timestamp != null)
+ {
+ long elapsed = SystemClock.uptimeMillis() - timestamp;
+ if (elapsed > 0 && elapsed < URI_VALIDITY)
+ { /* we fail if clock wrapped, should happen rarely though */
+ return ParcelFileDescriptor.open(mLogFile, ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+ mUris.remove(uri);
+ }
+ return super.openFile(uri, mode);
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values)
+ {
+ /* not supported */
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs)
+ {
+ /* not supported */
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs)
+ {
+ /* not supported */
+ return 0;
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java
new file mode 100644
index 000000000..053f91555
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.data;
+
+public class VpnProfile implements Cloneable
+{
+ private String mName, mGateway, mUsername, mPassword, mCertificate;
+ private long mId = -1;
+
+ public long getId()
+ {
+ return mId;
+ }
+
+ public void setId(long id)
+ {
+ this.mId = id;
+ }
+
+ public String getName()
+ {
+ return mName;
+ }
+
+ public void setName(String name)
+ {
+ this.mName = name;
+ }
+
+ public String getGateway()
+ {
+ return mGateway;
+ }
+
+ public void setGateway(String gateway)
+ {
+ this.mGateway = gateway;
+ }
+
+ public String getUsername()
+ {
+ return mUsername;
+ }
+
+ public void setUsername(String username)
+ {
+ this.mUsername = username;
+ }
+
+ public String getPassword()
+ {
+ return mPassword;
+ }
+
+ public void setPassword(String password)
+ {
+ this.mPassword = password;
+ }
+
+ public String getCertificateAlias()
+ {
+ return mCertificate;
+ }
+
+ public void setCertificateAlias(String certificate)
+ {
+ this.mCertificate = certificate;
+ }
+
+ @Override
+ public String toString()
+ {
+ return mName;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o != null && o instanceof VpnProfile)
+ {
+ return this.mId == ((VpnProfile)o).getId();
+ }
+ return false;
+ }
+
+ @Override
+ public VpnProfile clone()
+ {
+ try
+ {
+ return (VpnProfile)super.clone();
+ }
+ catch (CloneNotSupportedException e)
+ {
+ throw new AssertionError();
+ }
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java
new file mode 100644
index 000000000..18632ad6f
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+public class VpnProfileDataSource
+{
+ private static final String TAG = VpnProfileDataSource.class.getSimpleName();
+ public static final String KEY_ID = "_id";
+ public static final String KEY_NAME = "name";
+ public static final String KEY_GATEWAY = "gateway";
+ public static final String KEY_USERNAME = "username";
+ public static final String KEY_PASSWORD = "password";
+ public static final String KEY_CERTIFICATE = "certificate";
+
+ private DatabaseHelper mDbHelper;
+ private SQLiteDatabase mDatabase;
+ private final Context mContext;
+
+ private static final String DATABASE_NAME = "strongswan.db";
+ private static final String TABLE_VPNPROFILE = "vpnprofile";
+
+ private static final int DATABASE_VERSION = 1;
+
+ public static final String DATABASE_CREATE =
+ "CREATE TABLE " + TABLE_VPNPROFILE + " (" +
+ KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ KEY_NAME + " TEXT NOT NULL," +
+ KEY_GATEWAY + " TEXT NOT NULL," +
+ KEY_USERNAME + " TEXT NOT NULL," +
+ KEY_PASSWORD + " TEXT," +
+ KEY_CERTIFICATE + " TEXT" +
+ ");";
+ private final String[] ALL_COLUMNS = new String[] {
+ KEY_ID,
+ KEY_NAME,
+ KEY_GATEWAY,
+ KEY_USERNAME,
+ KEY_PASSWORD,
+ KEY_CERTIFICATE
+ };
+
+ private static class DatabaseHelper extends SQLiteOpenHelper
+ {
+ public DatabaseHelper(Context context)
+ {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase database)
+ {
+ database.execSQL(DATABASE_CREATE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
+ {
+ Log.w(TAG, "Upgrading database from version " + oldVersion +
+ " to " + newVersion + ", which will destroy all old data");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_VPNPROFILE);
+ onCreate(db);
+ }
+ }
+
+ /**
+ * Construct a new VPN profile data source. The context is used to
+ * open/create the database.
+ * @param context context used to access the database
+ */
+ public VpnProfileDataSource(Context context)
+ {
+ this.mContext = context;
+ }
+
+ /**
+ * Open the VPN profile data source. The database is automatically created
+ * if it does not yet exist. If that fails an exception is thrown.
+ * @return itself (allows to chain initialization calls)
+ * @throws SQLException if the database could not be opened or created
+ */
+ public VpnProfileDataSource open() throws SQLException
+ {
+ if (mDbHelper == null)
+ {
+ mDbHelper = new DatabaseHelper(mContext);
+ mDatabase = mDbHelper.getWritableDatabase();
+ }
+ return this;
+ }
+
+ /**
+ * Close the data source.
+ */
+ public void close()
+ {
+ if (mDbHelper != null)
+ {
+ mDbHelper.close();
+ mDbHelper = null;
+ }
+ }
+
+ /**
+ * Insert the given VPN profile into the database. On success the Id of
+ * the object is updated and the object returned.
+ *
+ * @param profile the profile to add
+ * @return the added VPN profile or null, if failed
+ */
+ public VpnProfile insertProfile(VpnProfile profile)
+ {
+ ContentValues values = ContentValuesFromVpnProfile(profile);
+ long insertId = mDatabase.insert(TABLE_VPNPROFILE, null, values);
+ if (insertId == -1)
+ {
+ return null;
+ }
+ profile.setId(insertId);
+ return profile;
+ }
+
+ /**
+ * Updates the given VPN profile in the database.
+ * @param profile the profile to update
+ * @return true if update succeeded, false otherwise
+ */
+ public boolean updateVpnProfile(VpnProfile profile)
+ {
+ long id = profile.getId();
+ ContentValues values = ContentValuesFromVpnProfile(profile);
+ return mDatabase.update(TABLE_VPNPROFILE, values, KEY_ID + " = " + id, null) > 0;
+ }
+
+ /**
+ * Delete the given VPN profile from the database.
+ * @param profile the profile to delete
+ * @return true if deleted, false otherwise
+ */
+ public boolean deleteVpnProfile(VpnProfile profile)
+ {
+ long id = profile.getId();
+ return mDatabase.delete(TABLE_VPNPROFILE, KEY_ID + " = " + id, null) > 0;
+ }
+
+ /**
+ * Get a single VPN profile from the database.
+ * @param id the ID of the VPN profile
+ * @return the profile or null, if not found
+ */
+ public VpnProfile getVpnProfile(long id)
+ {
+ VpnProfile profile = null;
+ Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS,
+ KEY_ID + "=" + id, 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
+ */
+ public List<VpnProfile> getAllVpnProfiles()
+ {
+ List<VpnProfile> vpnProfiles = new ArrayList<VpnProfile>();
+
+ Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, null, null, null, null, null);
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast())
+ {
+ VpnProfile vpnProfile = VpnProfileFromCursor(cursor);
+ vpnProfiles.add(vpnProfile);
+ cursor.moveToNext();
+ }
+ cursor.close();
+ return vpnProfiles;
+ }
+
+ private VpnProfile VpnProfileFromCursor(Cursor cursor)
+ {
+ VpnProfile profile = new VpnProfile();
+ profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID)));
+ profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME)));
+ profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY)));
+ profile.setUsername(cursor.getString(cursor.getColumnIndex(KEY_USERNAME)));
+ profile.setPassword(cursor.getString(cursor.getColumnIndex(KEY_PASSWORD)));
+ profile.setCertificateAlias(cursor.getString(cursor.getColumnIndex(KEY_CERTIFICATE)));
+ return profile;
+ }
+
+ private ContentValues ContentValuesFromVpnProfile(VpnProfile profile)
+ {
+ ContentValues values = new ContentValues();
+ values.put(KEY_NAME, profile.getName());
+ values.put(KEY_GATEWAY, profile.getGateway());
+ values.put(KEY_USERNAME, profile.getUsername());
+ values.put(KEY_PASSWORD, profile.getPassword());
+ values.put(KEY_CERTIFICATE, profile.getCertificateAlias());
+ return values;
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java
new file mode 100644
index 000000000..c9c1ad02a
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.logic;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.logic.VpnStateService.ErrorState;
+import org.strongswan.android.logic.VpnStateService.State;
+import org.strongswan.android.ui.MainActivity;
+
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+public class CharonVpnService extends VpnService implements Runnable
+{
+ private static final String TAG = CharonVpnService.class.getSimpleName();
+ public static final String LOG_FILE = "charon.log";
+
+ private String mLogFile;
+ private VpnProfileDataSource mDataSource;
+ private Thread mConnectionHandler;
+ private VpnProfile mCurrentProfile;
+ private volatile String mCurrentCertificateAlias;
+ private VpnProfile mNextProfile;
+ private volatile boolean mProfileUpdated;
+ private volatile boolean mTerminate;
+ private volatile boolean mIsDisconnecting;
+ private VpnStateService mService;
+ private final Object mServiceLock = new Object();
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name)
+ { /* since the service is local this is theoretically only called when the process is terminated */
+ synchronized (mServiceLock)
+ {
+ mService = null;
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service)
+ {
+ synchronized (mServiceLock)
+ {
+ mService = ((VpnStateService.LocalBinder)service).getService();
+ }
+ /* we are now ready to start the handler thread */
+ mConnectionHandler.start();
+ }
+ };
+
+ /**
+ * as defined in charonservice.h
+ */
+ static final int STATE_CHILD_SA_UP = 1;
+ static final int STATE_CHILD_SA_DOWN = 2;
+ static final int STATE_AUTH_ERROR = 3;
+ static final int STATE_PEER_AUTH_ERROR = 4;
+ static final int STATE_LOOKUP_ERROR = 5;
+ static final int STATE_UNREACHABLE_ERROR = 6;
+ static final int STATE_GENERIC_ERROR = 7;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId)
+ {
+ if (intent != null)
+ {
+ Bundle bundle = intent.getExtras();
+ VpnProfile profile = null;
+ if (bundle != null)
+ {
+ profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
+ if (profile != null)
+ {
+ String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
+ profile.setPassword(password);
+ }
+ }
+ setNextProfile(profile);
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onCreate()
+ {
+ mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
+
+ mDataSource = new VpnProfileDataSource(this);
+ mDataSource.open();
+ /* use a separate thread as main thread for charon */
+ mConnectionHandler = new Thread(this);
+ /* the thread is started when the service is bound */
+ bindService(new Intent(this, VpnStateService.class),
+ mServiceConnection, Service.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onRevoke()
+ { /* the system revoked the rights grated with the initial prepare() call.
+ * called when the user clicks disconnect in the system's VPN dialog */
+ setNextProfile(null);
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ mTerminate = true;
+ setNextProfile(null);
+ try
+ {
+ mConnectionHandler.join();
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ if (mService != null)
+ {
+ unbindService(mServiceConnection);
+ }
+ mDataSource.close();
+ }
+
+ /**
+ * Set the profile that is to be initiated next. Notify the handler thread.
+ *
+ * @param profile the profile to initiate
+ */
+ private void setNextProfile(VpnProfile profile)
+ {
+ synchronized (this)
+ {
+ this.mNextProfile = profile;
+ mProfileUpdated = true;
+ notifyAll();
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ while (true)
+ {
+ synchronized (this)
+ {
+ try
+ {
+ while (!mProfileUpdated)
+ {
+ wait();
+ }
+
+ mProfileUpdated = false;
+ stopCurrentConnection();
+ if (mNextProfile == null)
+ {
+ setProfile(null);
+ setState(State.DISABLED);
+ if (mTerminate)
+ {
+ break;
+ }
+ }
+ else
+ {
+ mCurrentProfile = mNextProfile;
+ mNextProfile = null;
+
+ /* store this in a separate (volatile) variable to avoid
+ * a possible deadlock during deinitialization */
+ mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
+
+ setProfile(mCurrentProfile);
+ setError(ErrorState.NO_ERROR);
+ setState(State.CONNECTING);
+ mIsDisconnecting = false;
+
+ BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName());
+ initializeCharon(builder, mLogFile);
+ Log.i(TAG, "charon started");
+
+ String local_address = getLocalIPv4Address();
+ initiate(local_address != null ? local_address : "0.0.0.0",
+ mCurrentProfile.getGateway(), mCurrentProfile.getUsername(),
+ mCurrentProfile.getPassword());
+ }
+ }
+ catch (InterruptedException ex)
+ {
+ stopCurrentConnection();
+ setState(State.DISABLED);
+ }
+ }
+ }
+ }
+
+ /**
+ * Stop any existing connection by deinitializing charon.
+ */
+ private void stopCurrentConnection()
+ {
+ synchronized (this)
+ {
+ if (mCurrentProfile != null)
+ {
+ setState(State.DISCONNECTING);
+ mIsDisconnecting = true;
+ deinitializeCharon();
+ Log.i(TAG, "charon stopped");
+ mCurrentProfile = null;
+ }
+ }
+ }
+
+ /**
+ * Update the VPN profile on the state service. Called by the handler thread.
+ *
+ * @param profile currently active VPN profile
+ */
+ private void setProfile(VpnProfile profile)
+ {
+ synchronized (mServiceLock)
+ {
+ if (mService != null)
+ {
+ mService.setProfile(profile);
+ }
+ }
+ }
+
+ /**
+ * Update the current VPN state on the state service. Called by the handler
+ * thread and any of charon's threads.
+ *
+ * @param state current state
+ */
+ private void setState(State state)
+ {
+ synchronized (mServiceLock)
+ {
+ if (mService != null)
+ {
+ mService.setState(state);
+ }
+ }
+ }
+
+ /**
+ * Set an error on the state service. Called by the handler thread and any
+ * of charon's threads.
+ *
+ * @param error error state
+ */
+ private void setError(ErrorState error)
+ {
+ synchronized (mServiceLock)
+ {
+ if (mService != null)
+ {
+ mService.setError(error);
+ }
+ }
+ }
+
+ /**
+ * Set an error on the state service and disconnect the current connection.
+ * This is not done by calling stopCurrentConnection() above, but instead
+ * is done asynchronously via state service.
+ *
+ * @param error error state
+ */
+ private void setErrorDisconnect(ErrorState error)
+ {
+ synchronized (mServiceLock)
+ {
+ if (mService != null)
+ {
+ mService.setError(error);
+ if (!mIsDisconnecting)
+ {
+ mService.disconnect();
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the state of the current connection.
+ * Called via JNI by different threads (but not concurrently).
+ *
+ * @param status new state
+ */
+ public void updateStatus(int status)
+ {
+ switch (status)
+ {
+ case STATE_CHILD_SA_DOWN:
+ synchronized (mServiceLock)
+ {
+ /* if we are not actively disconnecting we assume the remote terminated
+ * the connection and call disconnect() to deinitialize charon properly */
+ if (mService != null && !mIsDisconnecting)
+ {
+ mService.disconnect();
+ }
+ }
+ break;
+ case STATE_CHILD_SA_UP:
+ setState(State.CONNECTED);
+ break;
+ case STATE_AUTH_ERROR:
+ setErrorDisconnect(ErrorState.AUTH_FAILED);
+ break;
+ case STATE_PEER_AUTH_ERROR:
+ setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
+ break;
+ case STATE_LOOKUP_ERROR:
+ setErrorDisconnect(ErrorState.LOOKUP_FAILED);
+ break;
+ case STATE_UNREACHABLE_ERROR:
+ setErrorDisconnect(ErrorState.UNREACHABLE);
+ break;
+ case STATE_GENERIC_ERROR:
+ setErrorDisconnect(ErrorState.GENERIC_ERROR);
+ break;
+ default:
+ Log.e(TAG, "Unknown status code received");
+ break;
+ }
+ }
+
+ /**
+ * Function called via JNI to generate a list of DER encoded CA certificates
+ * as byte array.
+ *
+ * @param hash optional alias (only hash part), if given matching certificates are returned
+ * @return a list of DER encoded CA certificates
+ */
+ private byte[][] getTrustedCertificates(String hash)
+ {
+ ArrayList<byte[]> certs = new ArrayList<byte[]>();
+ TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
+ try
+ {
+ if (hash != null)
+ {
+ String alias = "user:" + hash + ".0";
+ X509Certificate cert = certman.getCACertificateFromAlias(alias);
+ if (cert == null)
+ {
+ alias = "system:" + hash + ".0";
+ cert = certman.getCACertificateFromAlias(alias);
+ }
+ if (cert == null)
+ {
+ return null;
+ }
+ certs.add(cert.getEncoded());
+ }
+ else
+ {
+ String alias = this.mCurrentCertificateAlias;
+ if (alias != null)
+ {
+ X509Certificate cert = certman.getCACertificateFromAlias(alias);
+ if (cert == null)
+ {
+ return null;
+ }
+ certs.add(cert.getEncoded());
+ }
+ else
+ {
+ for (X509Certificate cert : certman.getAllCACertificates().values())
+ {
+ certs.add(cert.getEncoded());
+ }
+ }
+ }
+ }
+ catch (CertificateEncodingException e)
+ {
+ e.printStackTrace();
+ return null;
+ }
+ return certs.toArray(new byte[certs.size()][]);
+ }
+
+ /**
+ * Initialization of charon, provided by libandroidbridge.so
+ *
+ * @param builder BuilderAdapter for this connection
+ * @param logfile absolute path to the logfile
+ */
+ public native void initializeCharon(BuilderAdapter builder, String logfile);
+
+ /**
+ * Deinitialize charon, provided by libandroidbridge.so
+ */
+ public native void deinitializeCharon();
+
+ /**
+ * Initiate VPN, provided by libandroidbridge.so
+ */
+ public native void initiate(String local_address, String gateway,
+ String username, String password);
+
+ /**
+ * Helper function that retrieves a local IPv4 address.
+ *
+ * @return string representation of an IPv4 address, or null if none found
+ */
+ private static String getLocalIPv4Address()
+ {
+ try
+ {
+ Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
+ while (en.hasMoreElements())
+ {
+ NetworkInterface intf = en.nextElement();
+
+ Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
+ while (enumIpAddr.hasMoreElements())
+ {
+ InetAddress inetAddress = enumIpAddr.nextElement();
+ if (!inetAddress.isLoopbackAddress() && inetAddress.getAddress().length == 4)
+ {
+ return inetAddress.getHostAddress().toString();
+ }
+ }
+ }
+ }
+ catch (SocketException ex)
+ {
+ ex.printStackTrace();
+ return null;
+ }
+ return null;
+ }
+
+ /**
+ * Adapter for VpnService.Builder which is used to access it safely via JNI.
+ * There is a corresponding C object to access it from native code.
+ */
+ public class BuilderAdapter
+ {
+ VpnService.Builder builder;
+
+ public BuilderAdapter(String name)
+ {
+ builder = new CharonVpnService.Builder();
+ builder.setSession(name);
+
+ /* even though the option displayed in the system dialog says "Configure"
+ * we just use our main Activity */
+ Context context = getApplicationContext();
+ Intent intent = new Intent(context, MainActivity.class);
+ PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ builder.setConfigureIntent(pending);
+ }
+
+ public synchronized boolean addAddress(String address, int prefixLength)
+ {
+ try
+ {
+ builder.addAddress(address, prefixLength);
+ }
+ catch (IllegalArgumentException ex)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public synchronized boolean addDnsServer(String address)
+ {
+ try
+ {
+ builder.addDnsServer(address);
+ }
+ catch (IllegalArgumentException ex)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public synchronized boolean addRoute(String address, int prefixLength)
+ {
+ try
+ {
+ builder.addRoute(address, prefixLength);
+ }
+ catch (IllegalArgumentException ex)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public synchronized boolean addSearchDomain(String domain)
+ {
+ try
+ {
+ builder.addSearchDomain(domain);
+ }
+ catch (IllegalArgumentException ex)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public synchronized boolean setMtu(int mtu)
+ {
+ try
+ {
+ builder.setMtu(mtu);
+ }
+ catch (IllegalArgumentException ex)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public synchronized int establish()
+ {
+ ParcelFileDescriptor fd;
+ try
+ {
+ fd = builder.establish();
+ }
+ catch (Exception ex)
+ {
+ ex.printStackTrace();
+ return -1;
+ }
+ if (fd == null)
+ {
+ return -1;
+ }
+ return fd.detachFd();
+ }
+ }
+
+ /*
+ * The libraries are extracted to /data/data/org.strongswan.android/...
+ * during installation.
+ */
+ static
+ {
+ System.loadLibrary("crypto");
+ System.loadLibrary("strongswan");
+ System.loadLibrary("hydra");
+ System.loadLibrary("charon");
+ System.loadLibrary("ipsec");
+ System.loadLibrary("androidbridge");
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java b/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java
new file mode 100644
index 000000000..74868dc44
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.logic;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import android.util.Log;
+
+public class TrustedCertificateManager
+{
+ private static final String TAG = TrustedCertificateManager.class.getSimpleName();
+ private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
+ private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>();
+ private boolean mLoaded;
+
+ /**
+ * Private constructor to prevent instantiation from other classes.
+ */
+ private TrustedCertificateManager()
+ {
+ }
+
+ /**
+ * This is not instantiated until the first call to getInstance()
+ */
+ private static class Singleton {
+ public static final TrustedCertificateManager mInstance = new TrustedCertificateManager();
+ }
+
+ /**
+ * Get the single instance of the CA certificate manager.
+ * @return CA certificate manager
+ */
+ public static TrustedCertificateManager getInstance()
+ {
+ return Singleton.mInstance;
+ }
+
+ /**
+ * Forces a load/reload of the cached CA certificates.
+ * As this takes a while it should be called asynchronously.
+ * @return reference to itself
+ */
+ public TrustedCertificateManager reload()
+ {
+ Log.d(TAG, "Force reload of cached CA certificates");
+ this.mLock.writeLock().lock();
+ loadCertificates();
+ this.mLock.writeLock().unlock();
+ return this;
+ }
+
+ /**
+ * Ensures that the certificates are loaded but does not force a reload.
+ * As this takes a while if the certificates are not loaded yet it should
+ * be called asynchronously.
+ * @return reference to itself
+ */
+ public TrustedCertificateManager load()
+ {
+ Log.d(TAG, "Ensure cached CA certificates are loaded");
+ this.mLock.writeLock().lock();
+ if (!this.mLoaded)
+ {
+ loadCertificates();
+ }
+ this.mLock.writeLock().unlock();
+ return this;
+ }
+
+ /**
+ * Opens the CA certificate KeyStore and loads the cached certificates.
+ * The lock must be locked when calling this method.
+ */
+ private void loadCertificates()
+ {
+ Log.d(TAG, "Load cached CA certificates");
+ try
+ {
+ KeyStore store = KeyStore.getInstance("AndroidCAStore");
+ store.load(null, null);
+ this.mCACerts = fetchCertificates(store);
+ this.mLoaded = true;
+ Log.d(TAG, "Cached CA certificates loaded");
+ }
+ catch (Exception ex)
+ {
+ ex.printStackTrace();
+ this.mCACerts = new Hashtable<String, X509Certificate>();
+ }
+ }
+
+ /**
+ * Load all X.509 certificates from the given KeyStore.
+ * @param store KeyStore to load certificates from
+ * @return Hashtable mapping aliases to certificates
+ */
+ private Hashtable<String, X509Certificate> fetchCertificates(KeyStore store)
+ {
+ Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
+ try
+ {
+ Enumeration<String> aliases = store.aliases();
+ while (aliases.hasMoreElements())
+ {
+ String alias = aliases.nextElement();
+ Certificate cert;
+ cert = store.getCertificate(alias);
+ if (cert != null && cert instanceof X509Certificate)
+ {
+ certs.put(alias, (X509Certificate)cert);
+ }
+ }
+ }
+ catch (KeyStoreException ex)
+ {
+ ex.printStackTrace();
+ }
+ return certs;
+ }
+
+ /**
+ * Retrieve the CA certificate with the given alias.
+ * @param alias alias of the certificate to get
+ * @return the certificate, null if not found
+ */
+ public X509Certificate getCACertificateFromAlias(String alias)
+ {
+ X509Certificate certificate = null;
+
+ if (this.mLock.readLock().tryLock())
+ {
+ certificate = this.mCACerts.get(alias);
+ this.mLock.readLock().unlock();
+ }
+ else
+ { /* if we cannot get the lock load it directly from the KeyStore,
+ * should be fast for a single certificate */
+ try
+ {
+ KeyStore store = KeyStore.getInstance("AndroidCAStore");
+ store.load(null, null);
+ Certificate cert = store.getCertificate(alias);
+ if (cert != null && cert instanceof X509Certificate)
+ {
+ certificate = (X509Certificate)cert;
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ }
+ return certificate;
+ }
+
+ /**
+ * Get all CA certificates (from the system and user keystore).
+ * @return Hashtable mapping aliases to certificates
+ */
+ @SuppressWarnings("unchecked")
+ public Hashtable<String, X509Certificate> getAllCACertificates()
+ {
+ Hashtable<String, X509Certificate> certs;
+ this.mLock.readLock().lock();
+ certs = (Hashtable<String, X509Certificate>)this.mCACerts.clone();
+ this.mLock.readLock().unlock();
+ return certs;
+ }
+
+ /**
+ * Get only the CA certificates installed by the user.
+ * @return Hashtable mapping aliases to certificates
+ */
+ public Hashtable<String, X509Certificate> getUserCACertificates()
+ {
+ Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
+ this.mLock.readLock().lock();
+ for (String alias : this.mCACerts.keySet())
+ {
+ if (alias.startsWith("user:"))
+ {
+ certs.put(alias, this.mCACerts.get(alias));
+ }
+ }
+ this.mLock.readLock().unlock();
+ return certs;
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java b/src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java
new file mode 100644
index 000000000..1c14cb601
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2012 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.logic;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.strongswan.android.data.VpnProfile;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+
+public class VpnStateService extends Service
+{
+ private final List<VpnStateListener> mListeners = new ArrayList<VpnStateListener>();
+ private final IBinder mBinder = new LocalBinder();
+ private Handler mHandler;
+ private VpnProfile mProfile;
+ private State mState = State.DISABLED;
+ private ErrorState mError = ErrorState.NO_ERROR;
+
+ public enum State
+ {
+ DISABLED,
+ CONNECTING,
+ CONNECTED,
+ DISCONNECTING,
+ }
+
+ public enum ErrorState
+ {
+ NO_ERROR,
+ AUTH_FAILED,
+ PEER_AUTH_FAILED,
+ LOOKUP_FAILED,
+ UNREACHABLE,
+ GENERIC_ERROR,
+ }
+
+ /**
+ * Listener interface for bound clients that are interested in changes to
+ * this Service.
+ */
+ public interface VpnStateListener
+ {
+ public void stateChanged();
+ }
+
+ /**
+ * Simple Binder that allows to directly access this Service class itself
+ * after binding to it.
+ */
+ public class LocalBinder extends Binder
+ {
+ public VpnStateService getService()
+ {
+ return VpnStateService.this;
+ }
+ }
+
+ @Override
+ public void onCreate()
+ {
+ /* this handler allows us to notify listeners from the UI thread and
+ * not from the threads that actually report any state changes */
+ mHandler = new Handler();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent)
+ {
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ }
+
+ /**
+ * Register a listener with this Service. We assume this is called from
+ * the main thread so no synchronization is happening.
+ *
+ * @param listener listener to register
+ */
+ public void registerListener(VpnStateListener listener)
+ {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Unregister a listener from this Service.
+ *
+ * @param listener listener to unregister
+ */
+ public void unregisterListener(VpnStateListener listener)
+ {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Get the current VPN profile.
+ *
+ * @return profile
+ */
+ public VpnProfile getProfile()
+ { /* only updated from the main thread so no synchronization needed */
+ return mProfile;
+ }
+
+ /**
+ * Get the current state.
+ *
+ * @return state
+ */
+ public State getState()
+ { /* only updated from the main thread so no synchronization needed */
+ return mState;
+ }
+
+ /**
+ * Get the current error, if any.
+ *
+ * @return error
+ */
+ public ErrorState getErrorState()
+ { /* only updated from the main thread so no synchronization needed */
+ return mError;
+ }
+
+ /**
+ * Disconnect any existing connection and shutdown the daemon, the
+ * VpnService is not stopped but it is reset so new connections can be
+ * started.
+ */
+ public void disconnect()
+ {
+ /* as soon as the TUN device is created by calling establish() on the
+ * VpnService.Builder object the system binds to the service and keeps
+ * bound until the file descriptor of the TUN device is closed. thus
+ * calling stopService() here would not stop (destroy) the service yet,
+ * instead we call startService() with an empty Intent which shuts down
+ * the daemon (and closes the TUN device, if any) */
+ Context context = getApplicationContext();
+ Intent intent = new Intent(context, CharonVpnService.class);
+ context.startService(intent);
+ }
+
+ /**
+ * Update state and notify all listeners about the change. By using a Handler
+ * this is done from the main UI thread and not the initial reporter thread.
+ * Also, in doing the actual state change from the main thread, listeners
+ * see all changes and none are skipped.
+ *
+ * @param change the state update to perform before notifying listeners, returns true if state changed
+ */
+ private void notifyListeners(final Callable<Boolean> change)
+ {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run()
+ {
+ try
+ {
+ if (change.call())
+ { /* otherwise there is no need to notify the listeners */
+ for (VpnStateListener listener : mListeners)
+ {
+ listener.stateChanged();
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ /**
+ * Set the VPN profile currently active. Listeners are not notified.
+ *
+ * May be called from threads other than the main thread.
+ *
+ * @param profile current profile
+ */
+ public void setProfile(final VpnProfile profile)
+ {
+ /* even though we don't notify the listeners the update is done from the
+ * same handler so updates are predictable for listeners */
+ mHandler.post(new Runnable() {
+ @Override
+ public void run()
+ {
+ VpnStateService.this.mProfile = profile;
+ }
+ });
+ }
+
+ /**
+ * Update the state and notify all listeners, if changed.
+ *
+ * May be called from threads other than the main thread.
+ *
+ * @param state new state
+ */
+ public void setState(final State state)
+ {
+ notifyListeners(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception
+ {
+ if (VpnStateService.this.mState != state)
+ {
+ VpnStateService.this.mState = state;
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Set the current error state and notify all listeners, if changed.
+ *
+ * May be called from threads other than the main thread.
+ *
+ * @param error error state
+ */
+ public void setError(final ErrorState error)
+ {
+ notifyListeners(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception
+ {
+ if (VpnStateService.this.mError != error)
+ {
+ VpnStateService.this.mError = error;
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/strongSwanActivity.java b/src/frontends/android/src/org/strongswan/android/strongSwanActivity.java
deleted file mode 100644
index fabf71897..000000000
--- a/src/frontends/android/src/org/strongswan/android/strongSwanActivity.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.strongswan.android;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.VpnService;
-import android.os.Bundle;
-
-public class strongSwanActivity extends Activity
-{
- @Override
- public void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- startVpnService();
- }
-
- private void startVpnService()
- {
- Intent intent = VpnService.prepare(this);
- if (intent != null)
- {
- startActivityForResult(intent, 0);
- }
- else
- {
- onActivityResult(0, RESULT_OK, null);
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data)
- {
- if (resultCode == RESULT_OK)
- {
- Intent intent = new Intent(this, CharonVpnService.class);
- startService(intent);
- }
- }
-}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/LogActivity.java b/src/frontends/android/src/org/strongswan/android/ui/LogActivity.java
new file mode 100644
index 000000000..a5efecc09
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/LogActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import java.io.File;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.LogContentProvider;
+import org.strongswan.android.logic.CharonVpnService;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+public class LogActivity extends Activity
+{
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.log_activity);
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ getMenuInflater().inflate(R.menu.log, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case android.R.id.home:
+ finish();
+ return true;
+ case R.id.menu_send_log:
+ File logfile = new File(getFilesDir(), CharonVpnService.LOG_FILE);
+ if (!logfile.exists() || logfile.length() == 0)
+ {
+ Toast.makeText(this, getString(R.string.empty_log), Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ String version = "";
+ try
+ {
+ version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+ }
+ catch (NameNotFoundException e)
+ {
+ e.printStackTrace();
+ }
+
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.putExtra(Intent.EXTRA_EMAIL, new String[] { MainActivity.CONTACT_EMAIL });
+ intent.putExtra(Intent.EXTRA_SUBJECT, String.format(getString(R.string.log_mail_subject), version));
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_STREAM, LogContentProvider.createContentUri());
+ startActivity(Intent.createChooser(intent, getString(R.string.send_log)));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/LogFragment.java b/src/frontends/android/src/org/strongswan/android/ui/LogFragment.java
new file mode 100644
index 000000000..8740e0c46
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/LogFragment.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.StringReader;
+
+import org.strongswan.android.R;
+import org.strongswan.android.logic.CharonVpnService;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.os.FileObserver;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class LogFragment extends Fragment implements Runnable
+{
+ private String mLogFilePath;
+ private Handler mLogHandler;
+ private TextView mLogView;
+ private LogScrollView mScrollView;
+ private BufferedReader mReader;
+ private Thread mThread;
+ private volatile boolean mRunning;
+ private FileObserver mDirectoryObserver;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ mLogFilePath = getActivity().getFilesDir() + File.separator + CharonVpnService.LOG_FILE;
+ /* use a handler to update the log view */
+ mLogHandler = new Handler();
+
+ mDirectoryObserver = new LogDirectoryObserver(getActivity().getFilesDir().getAbsolutePath());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.log_fragment, null);
+ mLogView = (TextView)view.findViewById(R.id.log_view);
+ mScrollView = (LogScrollView)view.findViewById(R.id.scroll_view);
+ return view;
+ }
+
+ @Override
+ public void onStart()
+ {
+ super.onStart();
+ startLogReader();
+ mDirectoryObserver.startWatching();
+ }
+
+ @Override
+ public void onStop()
+ {
+ super.onStop();
+ mDirectoryObserver.stopWatching();
+ stopLogReader();
+ }
+
+ /**
+ * Start reading from the log file
+ */
+ private void startLogReader()
+ {
+ try
+ {
+ mReader = new BufferedReader(new FileReader(mLogFilePath));
+ }
+ catch (FileNotFoundException e)
+ {
+ mReader = new BufferedReader(new StringReader(""));
+ }
+
+ mLogView.setText("");
+ mRunning = true;
+ mThread = new Thread(this);
+ mThread.start();
+ }
+
+ /**
+ * Stop reading from the log file
+ */
+ private void stopLogReader()
+ {
+ try
+ {
+ mRunning = false;
+ mThread.interrupt();
+ mThread.join();
+ }
+ catch (InterruptedException e)
+ {
+ }
+ }
+
+ /**
+ * Write the given log line to the TextView. We strip the prefix off to save
+ * some space (it is not that helpful for regular users anyway).
+ *
+ * @param line log line to log
+ */
+ public void logLine(final String line)
+ {
+ mLogHandler.post(new Runnable() {
+ @Override
+ public void run()
+ {
+ /* strip off prefix (month=3, day=2, time=8, thread=2, spaces=3) */
+ mLogView.append((line.length() > 18 ? line.substring(18) : line) + '\n');
+ /* calling autoScroll() directly does not work, probably because content
+ * is not yet updated, so we post this to be done later */
+ mScrollView.post(new Runnable() {
+ @Override
+ public void run()
+ {
+ mScrollView.autoScroll();
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void run()
+ {
+ while (mRunning)
+ {
+ try
+ { /* this works as long as the file is not truncated */
+ String line = mReader.readLine();
+ if (line == null)
+ { /* wait until there is more to log */
+ Thread.sleep(1000);
+ }
+ else
+ {
+ logLine(line);
+ }
+ }
+ catch (Exception e)
+ {
+ break;
+ }
+ }
+ }
+
+ /**
+ * FileObserver that checks for changes regarding the log file. Since charon
+ * truncates it (for which there is no explicit event) we check for any modification
+ * to the file, keep track of the file size and reopen it if it got smaller.
+ */
+ private class LogDirectoryObserver extends FileObserver
+ {
+ private final File mFile;
+ private long mSize;
+
+ public LogDirectoryObserver(String path)
+ {
+ super(path, FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE);
+ mFile = new File(mLogFilePath);
+ mSize = mFile.length();
+ }
+
+ @Override
+ public void onEvent(int event, String path)
+ {
+ if (path == null || !path.equals(CharonVpnService.LOG_FILE))
+ {
+ return;
+ }
+ switch (event)
+ { /* even though we only subscribed for these we check them,
+ * as strange events are sometimes received */
+ case FileObserver.CREATE:
+ case FileObserver.DELETE:
+ restartLogReader();
+ break;
+ case FileObserver.MODIFY:
+ /* if the size got smaller reopen the log file, as it was probably truncated */
+ long size = mFile.length();
+ if (size < mSize)
+ {
+ restartLogReader();
+ }
+ mSize = size;
+ break;
+ }
+ }
+
+ private void restartLogReader()
+ {
+ /* we are called from a separate thread, so we use the handler */
+ mLogHandler.post(new Runnable() {
+ @Override
+ public void run()
+ {
+ stopLogReader();
+ startLogReader();
+ }
+ });
+ }
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java b/src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java
new file mode 100644
index 000000000..7eee820ce
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ScrollView;
+
+public class LogScrollView extends ScrollView
+{
+ private boolean mAutoScroll = true;
+
+ public LogScrollView(Context context)
+ {
+ super(context);
+ }
+
+ public LogScrollView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ public LogScrollView(Context context, AttributeSet attrs, int defStyle)
+ {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev)
+ {
+ /* disable auto-scrolling when the user starts scrolling around */
+ if (ev.getActionMasked() == MotionEvent.ACTION_DOWN)
+ {
+ mAutoScroll = false;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ /**
+ * Call this to move newly added content into view by scrolling to the bottom.
+ * Nothing happens if auto-scrolling is disabled.
+ */
+ public void autoScroll()
+ {
+ if (mAutoScroll)
+ {
+ fullScroll(View.FOCUS_DOWN);
+ }
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt)
+ {
+ super.onScrollChanged(l, t, oldl, oldt);
+ /* if the user scrolls to the bottom we enable auto-scrolling again */
+ if (t == getChildAt(getChildCount() - 1).getHeight() - getHeight())
+ {
+ mAutoScroll = true;
+ }
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java
new file mode 100644
index 000000000..80f1a27b3
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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 org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.logic.CharonVpnService;
+import org.strongswan.android.logic.TrustedCertificateManager;
+import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.EditText;
+
+public class MainActivity extends Activity implements OnVpnProfileSelectedListener
+{
+ public static final String CONTACT_EMAIL = "android@strongswan.org";
+ private static final int PREPARE_VPN_SERVICE = 0;
+ private VpnProfile activeProfile;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.main);
+
+ ActionBar bar = getActionBar();
+ bar.setDisplayShowTitleEnabled(false);
+
+ /* load CA certificates in a background task */
+ new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, false);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.menu_reload_certs:
+ new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, true);
+ return true;
+ case R.id.menu_show_log:
+ Intent logIntent = new Intent(this, LogActivity.class);
+ startActivity(logIntent);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * Prepare the VpnService. If this succeeds the current VPN profile is
+ * started.
+ */
+ protected void prepareVpnService()
+ {
+ Intent intent = VpnService.prepare(this);
+ if (intent != null)
+ {
+ startActivityForResult(intent, PREPARE_VPN_SERVICE);
+ }
+ else
+ {
+ onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data)
+ {
+ switch (requestCode)
+ {
+ case PREPARE_VPN_SERVICE:
+ if (resultCode == RESULT_OK && activeProfile != null)
+ {
+ Intent intent = new Intent(this, CharonVpnService.class);
+ intent.putExtra(VpnProfileDataSource.KEY_ID, activeProfile.getId());
+ /* submit the password as the profile might not store one */
+ intent.putExtra(VpnProfileDataSource.KEY_PASSWORD, activeProfile.getPassword());
+ this.startService(intent);
+ }
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ public void onVpnProfileSelected(VpnProfile profile)
+ {
+ activeProfile = profile;
+ if (activeProfile.getPassword() == null)
+ {
+ new LoginDialog().show(getFragmentManager(), "LoginDialog");
+ }
+ else
+ {
+ prepareVpnService();
+ }
+ }
+
+ /**
+ * Class that loads or reloads the cached CA certificates.
+ */
+ private class CertificateLoadTask extends AsyncTask<Boolean, Void, TrustedCertificateManager>
+ {
+ @Override
+ protected void onPreExecute()
+ {
+ setProgressBarIndeterminateVisibility(true);
+ }
+ @Override
+ protected TrustedCertificateManager doInBackground(Boolean... params)
+ {
+ if (params.length > 0 && params[0])
+ { /* force a reload of the certificates */
+ return TrustedCertificateManager.getInstance().reload();
+ }
+ return TrustedCertificateManager.getInstance().load();
+ }
+ @Override
+ protected void onPostExecute(TrustedCertificateManager result)
+ {
+ setProgressBarIndeterminateVisibility(false);
+ }
+ }
+
+ private class LoginDialog extends DialogFragment
+ {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState)
+ {
+ LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.login_dialog, null);
+ EditText username = (EditText)view.findViewById(R.id.username);
+ username.setText(activeProfile.getUsername());
+ final EditText password = (EditText)view.findViewById(R.id.password);
+
+ Builder adb = new AlertDialog.Builder(MainActivity.this);
+ adb.setView(view);
+ adb.setTitle(getString(R.string.login_title));
+ adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton)
+ {
+ /* let's work on a clone of the profile when updating the password */
+ activeProfile = activeProfile.clone();
+ activeProfile.setPassword(password.getText().toString().trim());
+ prepareVpnService();
+ }
+ });
+ adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ dismiss();
+ }
+ });
+ return adb.create();
+ }
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java
new file mode 100644
index 000000000..05ba5e8b3
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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 java.security.cert.X509Certificate;
+import java.util.Hashtable;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.logic.TrustedCertificateManager;
+import org.strongswan.android.ui.adapter.TrustedCertificateAdapter;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+public class VpnProfileDetailActivity extends Activity
+{
+ private VpnProfileDataSource mDataSource;
+ private Long mId;
+ private VpnProfile mProfile;
+ private boolean mCertsLoaded;
+ private String mCertAlias;
+ private Spinner mCertSpinner;
+ private TrustedCertificateAdapter.CertEntry mSelectedCert;
+ private EditText mName;
+ private EditText mGateway;
+ private EditText mUsername;
+ private EditText mPassword;
+ private CheckBox mCheckAll;
+ private CheckBox mCheckAuto;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ /* the title is set when we load the profile, if any */
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mDataSource = new VpnProfileDataSource(this);
+ mDataSource.open();
+
+ setContentView(R.layout.profile_detail_view);
+
+ mName = (EditText)findViewById(R.id.name);
+ mPassword = (EditText)findViewById(R.id.password);
+ mGateway = (EditText)findViewById(R.id.gateway);
+ mUsername = (EditText)findViewById(R.id.username);
+
+ mCheckAll = (CheckBox)findViewById(R.id.ca_show_all);
+ mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
+ mCertSpinner = (Spinner)findViewById(R.id.ca_spinner);
+
+ mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
+ {
+ updateCertSpinner();
+ }
+ });
+
+ mCheckAll.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
+ {
+ Hashtable<String, X509Certificate> certs;
+ certs = isChecked ? TrustedCertificateManager.getInstance().getAllCACertificates()
+ : TrustedCertificateManager.getInstance().getUserCACertificates();
+ mCertSpinner.setAdapter(new TrustedCertificateAdapter(VpnProfileDetailActivity.this, certs));
+ mSelectedCert = (TrustedCertificateAdapter.CertEntry)mCertSpinner.getSelectedItem();
+ }
+ });
+
+ mCertSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view,
+ int pos, long id)
+ {
+ mSelectedCert = (TrustedCertificateAdapter.CertEntry)parent.getSelectedItem();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> arg0)
+ {
+ mSelectedCert = null;
+ }
+ });
+
+ mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
+ if (mId == null)
+ {
+ Bundle extras = getIntent().getExtras();
+ mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
+ }
+
+ loadProfileData();
+
+ new CertificateLoadTask().execute();
+ }
+
+ @Override
+ protected void onDestroy()
+ {
+ super.onDestroy();
+ mDataSource.close();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState)
+ {
+ super.onSaveInstanceState(outState);
+ outState.putLong(VpnProfileDataSource.KEY_ID, mId);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.profile_edit, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case android.R.id.home:
+ case R.id.menu_cancel:
+ finish();
+ return true;
+ case R.id.menu_accept:
+ saveProfile();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * Show an alert in case the previously selected certificate is not found anymore
+ * or the user did not select a certificate in the spinner.
+ */
+ private void showCertificateAlert()
+ {
+ AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
+ adb.setTitle(R.string.alert_text_nocertfound_title);
+ adb.setMessage(R.string.alert_text_nocertfound);
+ adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id)
+ {
+ dialog.cancel();
+ }
+ });
+ adb.show();
+ }
+
+ /**
+ * Asynchronously executed task which confirms that the certificates are loaded.
+ * They are loaded from the main Activity already but might not be ready yet, or
+ * unloaded again.
+ *
+ * Once loaded the CA certificate spinner and checkboxes are updated
+ * accordingly.
+ */
+ private class CertificateLoadTask extends AsyncTask<Void, Void, TrustedCertificateManager>
+ {
+ @Override
+ protected void onPreExecute()
+ {
+ setProgressBarIndeterminateVisibility(true);
+ }
+
+ @Override
+ protected TrustedCertificateManager doInBackground(Void... params)
+ {
+ return TrustedCertificateManager.getInstance().load();
+ }
+
+ @Override
+ protected void onPostExecute(TrustedCertificateManager result)
+ {
+ TrustedCertificateAdapter adapter;
+ if (mCertAlias != null && mCertAlias.startsWith("system:"))
+ {
+ mCheckAll.setChecked(true);
+ adapter = new TrustedCertificateAdapter(VpnProfileDetailActivity.this,
+ result.getAllCACertificates());
+ }
+ else
+ {
+ mCheckAll.setChecked(false);
+ adapter = new TrustedCertificateAdapter(VpnProfileDetailActivity.this,
+ result.getUserCACertificates());
+ }
+ mCertSpinner.setAdapter(adapter);
+
+ if (mCertAlias != null)
+ {
+ int position = adapter.getItemPosition(mCertAlias);
+ if (position == -1)
+ { /* previously selected certificate is not here anymore */
+ showCertificateAlert();
+ }
+ else
+ {
+ mCertSpinner.setSelection(position);
+ }
+ }
+
+ mSelectedCert = (TrustedCertificateAdapter.CertEntry)mCertSpinner.getSelectedItem();
+
+ setProgressBarIndeterminateVisibility(false);
+ mCertsLoaded = true;
+ updateCertSpinner();
+ }
+ }
+
+ /**
+ * Update the CA certificate selection UI depending on whether the
+ * certificate should be automatically selected or not.
+ */
+ private void updateCertSpinner()
+ {
+ if (!mCheckAuto.isChecked())
+ {
+ if (mCertsLoaded)
+ {
+ mCertSpinner.setEnabled(true);
+ mCertSpinner.setVisibility(View.VISIBLE);
+ mCheckAll.setEnabled(true);
+ mCheckAll.setVisibility(View.VISIBLE);
+ }
+ }
+ else
+ {
+ mCertSpinner.setEnabled(false);
+ mCertSpinner.setVisibility(View.GONE);
+ mCheckAll.setEnabled(false);
+ mCheckAll.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * 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())
+ {
+ if (mProfile != null)
+ {
+ updateProfileData();
+ mDataSource.updateVpnProfile(mProfile);
+ }
+ else
+ {
+ mProfile = new VpnProfile();
+ updateProfileData();
+ mDataSource.insertProfile(mProfile);
+ }
+ 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 (mGateway.getText().toString().trim().isEmpty())
+ {
+ mGateway.setError(getString(R.string.alert_text_no_input_gateway));
+ valid = false;
+ }
+ if (mUsername.getText().toString().trim().isEmpty())
+ {
+ mUsername.setError(getString(R.string.alert_text_no_input_username));
+ valid = false;
+ }
+ if (!mCheckAuto.isChecked() && mSelectedCert == null)
+ {
+ showCertificateAlert();
+ valid = false;
+ }
+ return valid;
+ }
+
+ /**
+ * Update the profile object with the data entered by the user
+ */
+ private void updateProfileData()
+ {
+ /* the name is optional, we default to the gateway if none is given */
+ String name = mName.getText().toString().trim();
+ String gateway = mGateway.getText().toString().trim();
+ mProfile.setName(name.isEmpty() ? gateway : name);
+ mProfile.setGateway(gateway);
+ mProfile.setUsername(mUsername.getText().toString().trim());
+ String password = mPassword.getText().toString().trim();
+ password = password.isEmpty() ? null : password;
+ mProfile.setPassword(password);
+ String certAlias = mCheckAuto.isChecked() ? null : mSelectedCert.mAlias;
+ mProfile.setCertificateAlias(certAlias);
+ }
+
+ /**
+ * Load an existing profile if we got an ID
+ */
+ private void loadProfileData()
+ {
+ getActionBar().setTitle(R.string.add_profile);
+ if (mId != null)
+ {
+ mProfile = mDataSource.getVpnProfile(mId);
+ if (mProfile != null)
+ {
+ mName.setText(mProfile.getName());
+ mGateway.setText(mProfile.getGateway());
+ mUsername.setText(mProfile.getUsername());
+ mPassword.setText(mProfile.getPassword());
+ mCertAlias = mProfile.getCertificateAlias();
+ getActionBar().setTitle(mProfile.getName());
+ }
+ else
+ {
+ Log.e(VpnProfileDetailActivity.class.getSimpleName(),
+ "VPN profile with id " + mId + " not found");
+ finish();
+ }
+ }
+ mCheckAll.setChecked(false);
+ mCheckAuto.setChecked(mCertAlias == null);
+ updateCertSpinner();
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java
new file mode 100644
index 000000000..1052558f2
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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 java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.ui.adapter.VpnProfileAdapter;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class VpnProfileListFragment extends Fragment
+{
+ private static final int ADD_REQUEST = 1;
+ private static final int EDIT_REQUEST = 2;
+
+ private List<VpnProfile> mVpnProfiles;
+ private VpnProfileDataSource mDataSource;
+ private VpnProfileAdapter mListAdapter;
+ private ListView mListView;
+ private OnVpnProfileSelectedListener mListener;
+
+ /**
+ * The activity containing this fragment should implement this interface
+ */
+ public interface OnVpnProfileSelectedListener {
+ public void onVpnProfileSelected(VpnProfile profile);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.profile_list_fragment, null);
+
+ mListView = (ListView)view.findViewById(R.id.profile_list);
+ mListView.setEmptyView(view.findViewById(R.id.profile_list_empty));
+ mListView.setOnItemClickListener(mVpnProfileClicked);
+ mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
+ mListView.setMultiChoiceModeListener(mVpnProfileSelected);
+ mListView.setAdapter(mListAdapter);
+
+ return view;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
+ Context context = getActivity().getApplicationContext();
+
+ mDataSource = new VpnProfileDataSource(this.getActivity());
+ mDataSource.open();
+
+ /* cached list of profiles used as backend for the ListView */
+ mVpnProfiles = mDataSource.getAllVpnProfiles();
+
+ mListAdapter = new VpnProfileAdapter(context, R.layout.profile_list_item, mVpnProfiles);
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ super.onDestroy();
+ mDataSource.close();
+ }
+
+ @Override
+ public void onAttach(Activity activity)
+ {
+ super.onAttach(activity);
+
+ if (activity instanceof OnVpnProfileSelectedListener)
+ {
+ mListener = (OnVpnProfileSelectedListener)activity;
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+ {
+ inflater.inflate(R.menu.profile_list, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.add_profile:
+ Intent connectionIntent = new Intent(getActivity(),
+ VpnProfileDetailActivity.class);
+ startActivityForResult(connectionIntent, ADD_REQUEST);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @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)
+ {
+ if (mListener != null)
+ {
+ mListener.onVpnProfileSelected((VpnProfile)a.getItemAtPosition(position));
+ }
+ }
+ };
+
+ private final MultiChoiceModeListener mVpnProfileSelected = new MultiChoiceModeListener() {
+ private HashSet<Integer> mSelected;
+ private MenuItem mEditProfile;
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu)
+ {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode)
+ {
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu)
+ {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.profile_list_context, menu);
+ mEditProfile = menu.findItem(R.id.edit_profile);
+ mSelected = new HashSet<Integer>();
+ mode.setTitle(R.string.select_profiles);
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.edit_profile:
+ {
+ int position = mSelected.iterator().next();
+ VpnProfile profile = (VpnProfile)mListView.getItemAtPosition(position);
+ Intent connectionIntent = new Intent(getActivity(), VpnProfileDetailActivity.class);
+ connectionIntent.putExtra(VpnProfileDataSource.KEY_ID, profile.getId());
+ startActivityForResult(connectionIntent, EDIT_REQUEST);
+ break;
+ }
+ case R.id.delete_profile:
+ {
+ ArrayList<VpnProfile> profiles = new ArrayList<VpnProfile>();
+ for (int position : mSelected)
+ {
+ profiles.add((VpnProfile)mListView.getItemAtPosition(position));
+ }
+ for (VpnProfile profile : profiles)
+ {
+ mDataSource.deleteVpnProfile(profile);
+ mVpnProfiles.remove(profile);
+ }
+ mListAdapter.notifyDataSetChanged();
+ Toast.makeText(VpnProfileListFragment.this.getActivity(),
+ R.string.profiles_deleted, Toast.LENGTH_SHORT).show();
+ break;
+ }
+ default:
+ return false;
+ }
+ mode.finish();
+ return true;
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position,
+ long id, boolean checked)
+ {
+ if (checked)
+ {
+ mSelected.add(position);
+ }
+ else
+ {
+ mSelected.remove(position);
+ }
+ final int checkedCount = mSelected.size();
+ mEditProfile.setEnabled(checkedCount == 1);
+ switch (checkedCount)
+ {
+ case 0:
+ mode.setSubtitle(R.string.no_profile_selected);
+ break;
+ case 1:
+ mode.setSubtitle(R.string.one_profile_selected);
+ break;
+ default:
+ mode.setSubtitle(String.format(getString(R.string.x_profiles_selected), checkedCount));
+ break;
+ }
+ }
+ };
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java b/src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java
new file mode 100644
index 000000000..738ed111f
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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 org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.logic.VpnStateService;
+import org.strongswan.android.logic.VpnStateService.ErrorState;
+import org.strongswan.android.logic.VpnStateService.State;
+import org.strongswan.android.logic.VpnStateService.VpnStateListener;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.ProgressDialog;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class VpnStateFragment extends Fragment implements VpnStateListener
+{
+ private static final String KEY_ERROR = "error";
+ private static final String KEY_NAME = "name";
+
+ private TextView mProfileNameView;
+ private TextView mProfileView;
+ private TextView mStateView;
+ private int stateBaseColor;
+ private Button mActionButton;
+ private ProgressDialog mProgressDialog;
+ private State mState;
+ private AlertDialog mErrorDialog;
+ private ErrorState mError;
+ private String mErrorProfileName;
+ private VpnStateService mService;
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name)
+ {
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service)
+ {
+ mService = ((VpnStateService.LocalBinder)service).getService();
+ mService.registerListener(VpnStateFragment.this);
+ updateView();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ /* bind to the service only seems to work from the ApplicationContext */
+ Context context = getActivity().getApplicationContext();
+ context.bindService(new Intent(context, VpnStateService.class),
+ mServiceConnection, Service.BIND_AUTO_CREATE);
+
+ mError = ErrorState.NO_ERROR;
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ERROR))
+ {
+ mError = (ErrorState)savedInstanceState.getSerializable(KEY_ERROR);
+ mErrorProfileName = savedInstanceState.getString(KEY_NAME);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState)
+ {
+ super.onSaveInstanceState(outState);
+
+ outState.putSerializable(KEY_ERROR, mError);
+ outState.putString(KEY_NAME, mErrorProfileName);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.vpn_state_fragment, null);
+
+ mActionButton = (Button)view.findViewById(R.id.action);
+ mActionButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v)
+ {
+ if (mService != null)
+ {
+ mService.disconnect();
+ }
+ }
+ });
+ enableActionButton(false);
+
+ mStateView = (TextView)view.findViewById(R.id.vpn_state);
+ stateBaseColor = mStateView.getCurrentTextColor();
+ mProfileView = (TextView)view.findViewById(R.id.vpn_profile_label);
+ mProfileNameView = (TextView)view.findViewById(R.id.vpn_profile_name);
+
+ return view;
+ }
+
+ @Override
+ public void onStart()
+ {
+ super.onStart();
+ if (mService != null)
+ {
+ updateView();
+ }
+ }
+
+ @Override
+ public void onStop()
+ {
+ super.onStop();
+ hideErrorDialog();
+ hideProgressDialog();
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ super.onDestroy();
+ if (mService != null)
+ {
+ mService.unregisterListener(this);
+ getActivity().getApplicationContext().unbindService(mServiceConnection);
+ }
+ }
+
+ @Override
+ public void stateChanged()
+ {
+ updateView();
+ }
+
+ public void updateView()
+ {
+ State state = mService.getState();
+ ErrorState error = ErrorState.NO_ERROR;
+ String name = "", gateway = "";
+
+ if (state != State.DISABLED)
+ {
+ VpnProfile profile = mService.getProfile();
+ if (profile != null)
+ {
+ name = profile.getName();
+ gateway = profile.getGateway();
+ }
+ error = mService.getErrorState();
+ }
+
+ if (reportError(name, state, error))
+ {
+ return;
+ }
+
+ if (state == mState)
+ { /* avoid unnecessary updates */
+ return;
+ }
+
+ hideProgressDialog();
+ enableActionButton(false);
+ mProfileNameView.setText(name);
+ mState = state;
+
+ switch (state)
+ {
+ case DISABLED:
+ showProfile(false);
+ mStateView.setText(R.string.state_disabled);
+ mStateView.setTextColor(stateBaseColor);
+ break;
+ case CONNECTING:
+ showProfile(true);
+ showConnectDialog(name, gateway);
+ mStateView.setText(R.string.state_connecting);
+ mStateView.setTextColor(stateBaseColor);
+ break;
+ case CONNECTED:
+ showProfile(true);
+ enableActionButton(true);
+ mStateView.setText(R.string.state_connected);
+ mStateView.setTextColor(getResources().getColor(R.color.success_text));
+ break;
+ case DISCONNECTING:
+ showProfile(true);
+ showDisconnectDialog(name);
+ mStateView.setText(R.string.state_disconnecting);
+ mStateView.setTextColor(stateBaseColor);
+ break;
+ }
+ }
+
+ private boolean reportError(String name, State state, ErrorState error)
+ {
+ if (mError != ErrorState.NO_ERROR)
+ { /* we are currently reporting an error which was not yet dismissed */
+ error = mError;
+ name = mErrorProfileName;
+ }
+ else if (error != ErrorState.NO_ERROR && (state == State.CONNECTING || state == State.CONNECTED))
+ { /* while initiating we report errors */
+ mError = error;
+ mErrorProfileName = name;
+ }
+ else
+ { /* ignore all other errors */
+ error = ErrorState.NO_ERROR;
+ }
+ if (error == ErrorState.NO_ERROR)
+ {
+ hideErrorDialog();
+ return false;
+ }
+ else if (mErrorDialog != null)
+ { /* we already show the dialog */
+ return true;
+ }
+ hideProgressDialog();
+ mProfileNameView.setText(name);
+ showProfile(true);
+ enableActionButton(false);
+ mStateView.setText(R.string.state_error);
+ mStateView.setTextColor(getResources().getColor(R.color.error_text));
+ switch (error)
+ {
+ case AUTH_FAILED:
+ showErrorDialog(R.string.error_auth_failed);
+ break;
+ case PEER_AUTH_FAILED:
+ showErrorDialog(R.string.error_peer_auth_failed);
+ break;
+ case LOOKUP_FAILED:
+ showErrorDialog(R.string.error_lookup_failed);
+ break;
+ case UNREACHABLE:
+ showErrorDialog(R.string.error_unreachable);
+ break;
+ default:
+ showErrorDialog(R.string.error_generic);
+ break;
+ }
+ return true;
+ }
+
+ private void showProfile(boolean show)
+ {
+ mProfileView.setVisibility(show ? View.VISIBLE : View.GONE);
+ mProfileNameView.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ private void enableActionButton(boolean enable)
+ {
+ mActionButton.setEnabled(enable);
+ mActionButton.setVisibility(enable ? View.VISIBLE : View.GONE);
+ }
+
+ private void hideProgressDialog()
+ {
+ if (mProgressDialog != null)
+ {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+
+ private void hideErrorDialog()
+ {
+ if (mErrorDialog != null)
+ {
+ mErrorDialog.dismiss();
+ mErrorDialog = null;
+ }
+ }
+
+ private void showConnectDialog(String profile, String gateway)
+ {
+ mProgressDialog = new ProgressDialog(getActivity());
+ mProgressDialog.setTitle(String.format(getString(R.string.connecting_title), profile));
+ mProgressDialog.setMessage(String.format(getString(R.string.connecting_message), gateway));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setButton(getString(android.R.string.cancel),
+ new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ if (mService != null)
+ {
+ mService.disconnect();
+ }
+ }
+ });
+ mProgressDialog.show();
+ }
+
+ private void showDisconnectDialog(String profile)
+ {
+ mProgressDialog = new ProgressDialog(getActivity());
+ mProgressDialog.setMessage(getString(R.string.state_disconnecting));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
+ private void showErrorDialog(int textid)
+ {
+ mErrorDialog = new AlertDialog.Builder(getActivity())
+ .setMessage(getString(R.string.error_introduction) + " " + getString(textid))
+ .setCancelable(false)
+ .setNeutralButton(R.string.show_log, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ dialog.dismiss();
+ Intent logIntent = new Intent(getActivity(), LogActivity.class);
+ startActivity(logIntent);
+ }
+ })
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id)
+ {
+ dialog.dismiss();
+ }
+ }).create();
+ mErrorDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog)
+ { /* clear the error */
+ mError = ErrorState.NO_ERROR;
+ mErrorDialog = null;
+ updateView();
+ }
+ });
+ mErrorDialog.show();
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java b/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java
new file mode 100644
index 000000000..ae94adc52
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.adapter;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.Map.Entry;
+
+import org.strongswan.android.R;
+
+import android.content.Context;
+import android.net.http.SslCertificate;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+public class TrustedCertificateAdapter extends BaseAdapter
+{
+ private final ArrayList<CertEntry> mContent;
+ private final Context mContext;
+
+ public class CertEntry implements Comparable<CertEntry>
+ {
+ public X509Certificate mCert;
+ public String mAlias;
+ public String mDisplayName;
+
+ public CertEntry(String alias, X509Certificate cert)
+ {
+ mCert = cert;
+ mAlias = alias;
+ }
+
+ public String getDisplayText()
+ {
+ if (mDisplayName == null)
+ {
+ SslCertificate cert = new SslCertificate(mCert);
+ String o = cert.getIssuedTo().getOName();
+ String ou = cert.getIssuedTo().getUName();
+ String cn = cert.getIssuedTo().getCName();
+ if (!o.isEmpty())
+ {
+ mDisplayName = o;
+ if (!cn.isEmpty())
+ {
+ mDisplayName = mDisplayName + ", " + cn;
+ }
+ else if (!ou.isEmpty())
+ {
+ mDisplayName = mDisplayName + ", " + ou;
+ }
+ }
+ else if (!cn.isEmpty())
+ {
+ mDisplayName = cn;
+ }
+ else
+ {
+ mDisplayName = cert.getIssuedTo().getDName();
+ }
+ }
+ return mDisplayName;
+ }
+
+ @Override
+ public int compareTo(CertEntry another)
+ {
+ return getDisplayText().compareToIgnoreCase(another.getDisplayText());
+ }
+ }
+
+ public TrustedCertificateAdapter(Context context,
+ Hashtable<String, X509Certificate> content)
+ {
+ mContext = context;
+ mContent = new ArrayList<TrustedCertificateAdapter.CertEntry>();
+ for (Entry<String, X509Certificate> entry : content.entrySet())
+ {
+ mContent.add(new CertEntry(entry.getKey(), entry.getValue()));
+ }
+ Collections.sort(mContent);
+ }
+
+ @Override
+ public int getCount()
+ {
+ return mContent.size();
+ }
+
+ @Override
+ public Object getItem(int position)
+ {
+ return mContent.get(position);
+ }
+
+ /**
+ * Returns the position (index) of the entry with the given alias.
+ *
+ * @param alias alias of the item to find
+ * @return the position (index) in the list
+ */
+ public int getItemPosition(String alias)
+ {
+ for (int i = 0; i < mContent.size(); i++)
+ {
+ if (mContent.get(i).mAlias.equals(alias))
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public long getItemId(int position)
+ {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent)
+ {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ final View certView = inflater.inflate(R.layout.trusted_certificates_item, null);
+ final TextView certText = (TextView)certView.findViewById(R.id.certificate_name);
+ certText.setText(mContent.get(position).getDisplayText());
+ return certView;
+ }
+}
diff --git a/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java b/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java
new file mode 100644
index 000000000..39e3e586a
--- /dev/null
+++ b/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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.adapter;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+public class VpnProfileAdapter extends ArrayAdapter<VpnProfile>
+{
+ private final int resource;
+ private final List<VpnProfile> items;
+
+ public VpnProfileAdapter(Context context, int resource,
+ List<VpnProfile> items)
+ {
+ super(context, resource, items);
+ this.resource = resource;
+ this.items = items;
+ sortItems();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent)
+ {
+ View vpnProfileView;
+ if (convertView != null)
+ {
+ vpnProfileView = convertView;
+ }
+ else
+ {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ vpnProfileView = inflater.inflate(resource, null);
+ }
+ VpnProfile profile = getItem(position);
+ TextView tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_name);
+ tv.setText(profile.getName());
+ tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_gateway);
+ tv.setText(getContext().getString(R.string.profile_gateway_label) + " " + profile.getGateway());
+ tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_username);
+ tv.setText(getContext().getString(R.string.profile_username_label) + " " + profile.getUsername());
+ return vpnProfileView;
+ }
+
+ @Override
+ public void notifyDataSetChanged()
+ {
+ sortItems();
+ super.notifyDataSetChanged();
+ }
+
+ private void sortItems()
+ {
+ Collections.sort(this.items, new Comparator<VpnProfile>() {
+ @Override
+ public int compare(VpnProfile lhs, VpnProfile rhs)
+ {
+ return lhs.getName().compareToIgnoreCase(rhs.getName());
+ }
+ });
+ }
+}