Merge branch 'android-updates'
authorTobias Brunner <tobias@strongswan.org>
Mon, 3 Jul 2017 08:32:35 +0000 (10:32 +0200)
committerTobias Brunner <tobias@strongswan.org>
Mon, 3 Jul 2017 08:32:35 +0000 (10:32 +0200)
This adds support for configuring split-tunneling and per-app VPN, adds
a simple HTTP/S fetcher and enables the revocation plugin, makes the log
view more efficient, imports profiles via SAF and changes multiple other
things.

56 files changed:
src/frontends/android/app/build.gradle
src/frontends/android/app/src/main/AndroidManifest.xml
src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java
src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java
src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
src/frontends/android/app/src/main/java/org/strongswan/android/logic/SimpleFetcher.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationEntry.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/CheckableLinearLayout.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRangeSet.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/utils/SettingsWriter.java
src/frontends/android/app/src/main/jni/Android.mk
src/frontends/android/app/src/main/jni/libandroidbridge/Android.mk
src/frontends/android/app/src/main/jni/libandroidbridge/android_jni.c
src/frontends/android/app/src/main/jni/libandroidbridge/android_jni.h
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_fetcher.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_fetcher.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c
src/frontends/android/app/src/main/res/color/checkable_text_color.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable/activated_background.xml
src/frontends/android/app/src/main/res/layout/profile_detail_view.xml
src/frontends/android/app/src/main/res/layout/selected_application_item.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/main.xml
src/frontends/android/app/src/main/res/values-de/arrays.xml
src/frontends/android/app/src/main/res/values-de/strings.xml
src/frontends/android/app/src/main/res/values-pl/arrays.xml
src/frontends/android/app/src/main/res/values-pl/strings.xml
src/frontends/android/app/src/main/res/values-ru/arrays.xml
src/frontends/android/app/src/main/res/values-ru/strings.xml
src/frontends/android/app/src/main/res/values-ua/arrays.xml
src/frontends/android/app/src/main/res/values-ua/strings.xml
src/frontends/android/app/src/main/res/values-zh-rCN/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-zh-rTW/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-zh/arrays.xml [deleted file]
src/frontends/android/app/src/main/res/values-zh/strings.xml [deleted file]
src/frontends/android/app/src/main/res/values/arrays.xml
src/frontends/android/app/src/main/res/values/colors.xml
src/frontends/android/app/src/main/res/values/strings.xml
src/frontends/android/app/src/test/java/org/strongswan/android/test/IPRangeSetTest.java [new file with mode: 0644]
src/frontends/android/app/src/test/java/org/strongswan/android/test/IPRangeTest.java [new file with mode: 0644]
src/frontends/android/app/src/test/java/org/strongswan/android/test/SettingsWriterTest.java
src/frontends/android/build.gradle
src/frontends/android/gradle/wrapper/gradle-wrapper.properties
src/libstrongswan/Android.mk
src/libtnccs/Android.mk

index 3c1c68a..d0ab256 100644 (file)
@@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.3"
+    buildToolsVersion '25.0.0'
 
     defaultConfig {
         applicationId "org.strongswan.android"
index bc2de9a..c7f745b 100644 (file)
             android:label="@string/trusted_certs_title" >
         </activity>
         <activity
+            android:name=".ui.SelectedApplicationsActivity"
+            android:label="@string/profile_select_apps" >
+        </activity>
+        <activity
             android:name=".ui.LogActivity"
             android:label="@string/log_title" >
         </activity>
index 54bdfcb..0b552f4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2016 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
  * HSR Hochschule fuer Technik Rapperswil
 package org.strongswan.android.data;
 
 
+import android.text.TextUtils;
+
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.UUID;
 
 public class VpnProfile implements Cloneable
@@ -27,12 +32,32 @@ public class VpnProfile implements Cloneable
        public static final int SPLIT_TUNNELING_BLOCK_IPV6 = 2;
 
        private String mName, mGateway, mUsername, mPassword, mCertificate, mUserCertificate;
-       private String mRemoteId, mLocalId;
+       private String mRemoteId, mLocalId, mExcludedSubnets, mIncludedSubnets, mSelectedApps;
        private Integer mMTU, mPort, mSplitTunneling;
+       private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
        private VpnType mVpnType;
        private UUID mUUID;
        private long mId = -1;
 
+       public enum SelectedAppsHandling
+       {
+               SELECTED_APPS_DISABLE(0),
+               SELECTED_APPS_EXCLUDE(1),
+               SELECTED_APPS_ONLY(2);
+
+               private Integer mValue;
+
+               SelectedAppsHandling(int value)
+               {
+                       mValue = value;
+               }
+
+               public Integer getValue()
+               {
+                       return mValue;
+               }
+       }
+
        public VpnProfile()
        {
                this.mUUID = UUID.randomUUID();
@@ -168,6 +193,74 @@ public class VpnProfile implements Cloneable
                this.mPort = port;
        }
 
+       public void setExcludedSubnets(String excludedSubnets)
+       {
+               this.mExcludedSubnets = excludedSubnets;
+       }
+
+       public String getExcludedSubnets()
+       {
+               return mExcludedSubnets;
+       }
+
+       public void setIncludedSubnets(String includedSubnets)
+       {
+               this.mIncludedSubnets = includedSubnets;
+       }
+
+       public String getIncludedSubnets()
+       {
+               return mIncludedSubnets;
+       }
+
+       public void setSelectedApps(String selectedApps)
+       {
+               this.mSelectedApps = selectedApps;
+       }
+
+       public void setSelectedApps(SortedSet<String> selectedApps)
+       {
+               this.mSelectedApps = selectedApps.size() > 0 ? TextUtils.join(" ", selectedApps) : null;
+       }
+
+       public String getSelectedApps()
+       {
+               return mSelectedApps;
+       }
+
+       public SortedSet<String> getSelectedAppsSet()
+       {
+               TreeSet<String> set = new TreeSet<>();
+               if (!TextUtils.isEmpty(mSelectedApps))
+               {
+                       set.addAll(Arrays.asList(mSelectedApps.split("\\s+")));
+               }
+               return set;
+       }
+
+       public void setSelectedAppsHandling(SelectedAppsHandling selectedAppsHandling)
+       {
+               this.mSelectedAppsHandling = selectedAppsHandling;
+       }
+
+       public void setSelectedAppsHandling(Integer value)
+       {
+               mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
+               for (SelectedAppsHandling handling : SelectedAppsHandling.values())
+               {
+                       if (handling.mValue.equals(value))
+                       {
+                               mSelectedAppsHandling = handling;
+                               break;
+                       }
+               }
+       }
+
+       public SelectedAppsHandling getSelectedAppsHandling()
+       {
+               return mSelectedAppsHandling;
+       }
+
        public Integer getSplitTunneling()
        {
                return mSplitTunneling;
index 1c509a3..380dbda 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2016 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
  * HSR Hochschule fuer Technik Rapperswil
 
 package org.strongswan.android.data;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
@@ -30,6 +26,10 @@ import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
 public class VpnProfileDataSource
 {
        private static final String TAG = VpnProfileDataSource.class.getSimpleName();
@@ -47,6 +47,10 @@ public class VpnProfileDataSource
        public static final String KEY_SPLIT_TUNNELING = "split_tunneling";
        public static final String KEY_LOCAL_ID = "local_id";
        public static final String KEY_REMOTE_ID = "remote_id";
+       public static final String KEY_EXCLUDED_SUBNETS = "excluded_subnets";
+       public static final String KEY_INCLUDED_SUBNETS = "included_subnets";
+       public static final String KEY_SELECTED_APPS = "selected_apps";
+       public static final String KEY_SELECTED_APPS_LIST = "selected_apps_list";
 
        private DatabaseHelper mDbHelper;
        private SQLiteDatabase mDatabase;
@@ -55,7 +59,7 @@ public class VpnProfileDataSource
        private static final String DATABASE_NAME = "strongswan.db";
        private static final String TABLE_VPNPROFILE = "vpnprofile";
 
-       private static final int DATABASE_VERSION = 9;
+       private static final int DATABASE_VERSION = 12;
 
        public static final String DATABASE_CREATE =
                                                        "CREATE TABLE " + TABLE_VPNPROFILE + " (" +
@@ -72,7 +76,11 @@ public class VpnProfileDataSource
                                                                KEY_PORT + " INTEGER," +
                                                                KEY_SPLIT_TUNNELING + " INTEGER," +
                                                                KEY_LOCAL_ID + " TEXT," +
-                                                               KEY_REMOTE_ID + " TEXT" +
+                                                               KEY_REMOTE_ID + " TEXT," +
+                                                               KEY_EXCLUDED_SUBNETS + " TEXT," +
+                                                               KEY_INCLUDED_SUBNETS + " TEXT," +
+                                                               KEY_SELECTED_APPS + " INTEGER," +
+                                                               KEY_SELECTED_APPS_LIST + " TEXT" +
                                                        ");";
        private static final String[] ALL_COLUMNS = new String[] {
                                                                KEY_ID,
@@ -89,6 +97,10 @@ public class VpnProfileDataSource
                                                                KEY_SPLIT_TUNNELING,
                                                                KEY_LOCAL_ID,
                                                                KEY_REMOTE_ID,
+                                                               KEY_EXCLUDED_SUBNETS,
+                                                               KEY_INCLUDED_SUBNETS,
+                                                               KEY_SELECTED_APPS,
+                                                               KEY_SELECTED_APPS_LIST,
                                                        };
 
        private static class DatabaseHelper extends SQLiteOpenHelper
@@ -151,6 +163,23 @@ public class VpnProfileDataSource
                                                   " TEXT;");
                                updateColumns(db);
                        }
+                       if (oldVersion < 10)
+                       {
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_EXCLUDED_SUBNETS +
+                                                  " TEXT;");
+                       }
+                       if (oldVersion < 11)
+                       {
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_INCLUDED_SUBNETS +
+                                                  " TEXT;");
+                       }
+                       if (oldVersion < 12)
+                       {
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SELECTED_APPS +
+                                                  " INTEGER;");
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SELECTED_APPS_LIST +
+                                                  " TEXT;");
+                       }
                }
 
                private void updateColumns(SQLiteDatabase db)
@@ -326,6 +355,10 @@ public class VpnProfileDataSource
                profile.setSplitTunneling(getInt(cursor, cursor.getColumnIndex(KEY_SPLIT_TUNNELING)));
                profile.setLocalId(cursor.getString(cursor.getColumnIndex(KEY_LOCAL_ID)));
                profile.setRemoteId(cursor.getString(cursor.getColumnIndex(KEY_REMOTE_ID)));
+               profile.setExcludedSubnets(cursor.getString(cursor.getColumnIndex(KEY_EXCLUDED_SUBNETS)));
+               profile.setIncludedSubnets(cursor.getString(cursor.getColumnIndex(KEY_INCLUDED_SUBNETS)));
+               profile.setSelectedAppsHandling(getInt(cursor, cursor.getColumnIndex(KEY_SELECTED_APPS)));
+               profile.setSelectedApps(cursor.getString(cursor.getColumnIndex(KEY_SELECTED_APPS_LIST)));
                return profile;
        }
 
@@ -345,6 +378,10 @@ public class VpnProfileDataSource
                values.put(KEY_SPLIT_TUNNELING, profile.getSplitTunneling());
                values.put(KEY_LOCAL_ID, profile.getLocalId());
                values.put(KEY_REMOTE_ID, profile.getRemoteId());
+               values.put(KEY_EXCLUDED_SUBNETS, profile.getExcludedSubnets());
+               values.put(KEY_INCLUDED_SUBNETS, profile.getIncludedSubnets());
+               values.put(KEY_SELECTED_APPS, profile.getSelectedAppsHandling().getValue());
+               values.put(KEY_SELECTED_APPS_LIST, profile.getSelectedApps());
                return values;
        }
 
index 9e9b673..965edfb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2016 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
  * HSR Hochschule fuer Technik Rapperswil
@@ -26,6 +26,7 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.net.VpnService;
 import android.os.Build;
 import android.os.Bundle;
@@ -40,6 +41,7 @@ import android.util.Log;
 
 import org.strongswan.android.R;
 import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
 import org.strongswan.android.data.VpnProfileDataSource;
 import org.strongswan.android.data.VpnType.VpnTypeFeature;
 import org.strongswan.android.logic.VpnStateService.ErrorState;
@@ -47,6 +49,8 @@ import org.strongswan.android.logic.VpnStateService.State;
 import org.strongswan.android.logic.imc.ImcState;
 import org.strongswan.android.logic.imc.RemediationInstruction;
 import org.strongswan.android.ui.MainActivity;
+import org.strongswan.android.utils.IPRange;
+import org.strongswan.android.utils.IPRangeSet;
 import org.strongswan.android.utils.SettingsWriter;
 
 import java.io.File;
@@ -60,10 +64,12 @@ import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.SortedSet;
 
 public class CharonVpnService extends VpnService implements Runnable, VpnStateService.VpnStateListener
 {
        private static final String TAG = CharonVpnService.class.getSimpleName();
+       public static final String DISCONNECT_ACTION = "org.strongswan.android.CharonVpnService.DISCONNECT";
        public static final String LOG_FILE = "charon.log";
        public static final int VPN_STATE_NOTIFICATION_ID = 1;
 
@@ -119,18 +125,25 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
        {
                if (intent != null)
                {
-                       Bundle bundle = intent.getExtras();
-                       VpnProfile profile = null;
-                       if (bundle != null)
+                       if (DISCONNECT_ACTION.equals(intent.getAction()))
                        {
-                               profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
-                               if (profile != null)
+                               setNextProfile(null);
+                       }
+                       else
+                       {
+                               Bundle bundle = intent.getExtras();
+                               VpnProfile profile = null;
+                               if (bundle != null)
                                {
-                                       String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
-                                       profile.setPassword(password);
+                                       profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
+                                       if (profile != null)
+                                       {
+                                               String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
+                                               profile.setPassword(password);
+                                       }
                                }
+                               setNextProfile(profile);
                        }
-                       setNextProfile(profile);
                }
                return START_NOT_STICKY;
        }
@@ -230,7 +243,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                                                mIsDisconnecting = false;
 
                                                addNotification();
-                                               BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName(), mCurrentProfile.getSplitTunneling());
+                                               BuilderAdapter builder = new BuilderAdapter(mCurrentProfile);
                                                if (initializeCharon(builder, mLogFile, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD)))
                                                {
                                                        Log.i(TAG, "charon started");
@@ -640,24 +653,22 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
         */
        public class BuilderAdapter
        {
-               private final String mName;
-               private final Integer mSplitTunneling;
+               private final VpnProfile mProfile;
                private VpnService.Builder mBuilder;
                private BuilderCache mCache;
                private BuilderCache mEstablishedCache;
 
-               public BuilderAdapter(String name, Integer splitTunneling)
+               public BuilderAdapter(VpnProfile profile)
                {
-                       mName = name;
-                       mSplitTunneling = splitTunneling;
-                       mBuilder = createBuilder(name);
-                       mCache = new BuilderCache(mSplitTunneling);
+                       mProfile = profile;
+                       mBuilder = createBuilder(mProfile.getName());
+                       mCache = new BuilderCache(mProfile);
                }
 
                private VpnService.Builder createBuilder(String name)
                {
                        VpnService.Builder builder = new CharonVpnService.Builder();
-                       builder.setSession(mName);
+                       builder.setSession(name);
 
                        /* even though the option displayed in the system dialog says "Configure"
                         * we just use our main Activity */
@@ -754,9 +765,9 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                        }
                        /* now that the TUN device is created we don't need the current
                         * builder anymore, but we might need another when reestablishing */
-                       mBuilder = createBuilder(mName);
+                       mBuilder = createBuilder(mProfile.getName());
                        mEstablishedCache = mCache;
-                       mCache = new BuilderCache(mSplitTunneling);
+                       mCache = new BuilderCache(mProfile);
                        return fd.detachFd();
                }
 
@@ -770,7 +781,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                        }
                        try
                        {
-                               Builder builder = createBuilder(mName);
+                               Builder builder = createBuilder(mProfile.getName());
                                mEstablishedCache.applyData(builder);
                                fd = builder.establish();
                        }
@@ -793,22 +804,50 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
         */
        public class BuilderCache
        {
-               private final List<PrefixedAddress> mAddresses = new ArrayList<PrefixedAddress>();
-               private final List<PrefixedAddress> mRoutesIPv4 = new ArrayList<PrefixedAddress>();
-               private final List<PrefixedAddress> mRoutesIPv6 = new ArrayList<PrefixedAddress>();
+               private final List<IPRange> mAddresses = new ArrayList<>();
+               private final List<IPRange> mRoutesIPv4 = new ArrayList<>();
+               private final List<IPRange> mRoutesIPv6 = new ArrayList<>();
+               private final IPRangeSet mIncludedSubnetsv4 = new IPRangeSet();
+               private final IPRangeSet mIncludedSubnetsv6 = new IPRangeSet();
+               private final IPRangeSet mExcludedSubnets;
                private final int mSplitTunneling;
+               private final SelectedAppsHandling mAppHandling;
+               private final SortedSet<String> mSelectedApps;
                private int mMtu;
                private boolean mIPv4Seen, mIPv6Seen;
 
-               public BuilderCache(Integer splitTunneling)
+               public BuilderCache(VpnProfile profile)
                {
+                       IPRangeSet included = IPRangeSet.fromString(profile.getIncludedSubnets());
+                       for (IPRange range : included)
+                       {
+                               if (range.getFrom() instanceof Inet4Address)
+                               {
+                                       mIncludedSubnetsv4.add(range);
+                               }
+                               else if (range.getFrom() instanceof Inet6Address)
+                               {
+                                       mIncludedSubnetsv6.add(range);
+                               }
+                       }
+                       mExcludedSubnets = IPRangeSet.fromString(profile.getExcludedSubnets());
+                       Integer splitTunneling = profile.getSplitTunneling();
                        mSplitTunneling = splitTunneling != null ? splitTunneling : 0;
+                       mAppHandling = profile.getSelectedAppsHandling();
+                       mSelectedApps = profile.getSelectedAppsSet();
                }
 
                public void addAddress(String address, int prefixLength)
                {
-                       mAddresses.add(new PrefixedAddress(address, prefixLength));
-                       recordAddressFamily(address);
+                       try
+                       {
+                               mAddresses.add(new IPRange(address, prefixLength));
+                               recordAddressFamily(address);
+                       }
+                       catch (UnknownHostException ex)
+                       {
+                               ex.printStackTrace();
+                       }
                }
 
                public void addRoute(String address, int prefixLength)
@@ -817,11 +856,11 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                        {
                                if (isIPv6(address))
                                {
-                                       mRoutesIPv6.add(new PrefixedAddress(address, prefixLength));
+                                       mRoutesIPv6.add(new IPRange(address, prefixLength));
                                }
                                else
                                {
-                                       mRoutesIPv4.add(new PrefixedAddress(address, prefixLength));
+                                       mRoutesIPv4.add(new IPRange(address, prefixLength));
                                }
                        }
                        catch (UnknownHostException ex)
@@ -857,19 +896,29 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                @TargetApi(Build.VERSION_CODES.LOLLIPOP)
                public void applyData(VpnService.Builder builder)
                {
-                       for (PrefixedAddress address : mAddresses)
+                       for (IPRange address : mAddresses)
                        {
-                               builder.addAddress(address.mAddress, address.mPrefix);
+                               builder.addAddress(address.getFrom(), address.getPrefix());
                        }
                        /* add routes depending on whether split tunneling is allowed or not,
                         * that is, whether we have to handle and block non-VPN traffic */
                        if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) == 0)
                        {
                                if (mIPv4Seen)
-                               {       /* split tunneling is used depending on the routes */
-                                       for (PrefixedAddress route : mRoutesIPv4)
+                               {       /* split tunneling is used depending on the routes and configuration */
+                                       IPRangeSet ranges = new IPRangeSet();
+                                       if (mIncludedSubnetsv4.size() > 0)
                                        {
-                                               builder.addRoute(route.mAddress, route.mPrefix);
+                                               ranges.add(mIncludedSubnetsv4);
+                                       }
+                                       else
+                                       {
+                                               ranges.addAll(mRoutesIPv4);
+                                       }
+                                       ranges.remove(mExcludedSubnets);
+                                       for (IPRange subnet : ranges.subnets())
+                                       {
+                                               builder.addRoute(subnet.getFrom(), subnet.getPrefix());
                                        }
                                }
                                else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
@@ -887,9 +936,19 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                        {
                                if (mIPv6Seen)
                                {
-                                       for (PrefixedAddress route : mRoutesIPv6)
+                                       IPRangeSet ranges = new IPRangeSet();
+                                       if (mIncludedSubnetsv6.size() > 0)
+                                       {
+                                               ranges.add(mIncludedSubnetsv6);
+                                       }
+                                       else
+                                       {
+                                               ranges.addAll(mRoutesIPv6);
+                                       }
+                                       ranges.remove(mExcludedSubnets);
+                                       for (IPRange subnet : ranges.subnets())
                                        {
-                                               builder.addRoute(route.mAddress, route.mPrefix);
+                                               builder.addRoute(subnet.getFrom(), subnet.getPrefix());
                                        }
                                }
                                else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
@@ -901,6 +960,41 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                        {
                                builder.addRoute("::", 0);
                        }
+                       /* apply selected applications */
+                       if (mSelectedApps.size() > 0)
+                       {
+                               switch (mAppHandling)
+                               {
+                                       case SELECTED_APPS_EXCLUDE:
+                                               for (String app : mSelectedApps)
+                                               {
+                                                       try
+                                                       {
+                                                               builder.addDisallowedApplication(app);
+                                                       }
+                                                       catch (PackageManager.NameNotFoundException e)
+                                                       {
+                                                               // possible if not configured via GUI or app was uninstalled
+                                                       }
+                                               }
+                                               break;
+                                       case SELECTED_APPS_ONLY:
+                                               for (String app : mSelectedApps)
+                                               {
+                                                       try
+                                                       {
+                                                               builder.addAllowedApplication(app);
+                                                       }
+                                                       catch (PackageManager.NameNotFoundException e)
+                                                       {
+                                                               // possible if not configured via GUI or app was uninstalled
+                                                       }
+                                               }
+                                               break;
+                                       default:
+                                               break;
+                               }
+                       }
                        builder.setMtu(mMtu);
                }
 
@@ -917,18 +1011,23 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                        }
                        return false;
                }
+       }
 
-               private class PrefixedAddress
-               {
-                       public String mAddress;
-                       public int mPrefix;
+       /**
+        * Function called via JNI to determine information about the Android version.
+        */
+       private static String getAndroidVersion()
+       {
+               return "Android " + Build.VERSION.RELEASE + " - " + Build.DISPLAY +
+                          "/" + Build.VERSION.SECURITY_PATCH;
+       }
 
-                       public PrefixedAddress(String address, int prefix)
-                       {
-                               this.mAddress = address;
-                               this.mPrefix = prefix;
-                       }
-               }
+       /**
+        * Function called via JNI to determine information about the device.
+        */
+       private static String getDeviceString()
+       {
+               return Build.MODEL + " - " + Build.BRAND + "/" + Build.PRODUCT + "/" + Build.MANUFACTURER;
        }
 
        /*
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/SimpleFetcher.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/SimpleFetcher.java
new file mode 100644 (file)
index 0000000..e7f029b
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic;
+
+import android.support.annotation.Keep;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+@Keep
+public class SimpleFetcher
+{
+       public static byte[] fetch(String uri) throws IOException
+       {
+               URL url = new URL(uri);
+               HttpURLConnection conn = (HttpURLConnection)url.openConnection();
+               conn.setConnectTimeout(10000);
+               conn.setReadTimeout(10000);
+               try
+               {
+                       return streamToArray(conn.getInputStream());
+               }
+               finally
+               {
+                       conn.disconnect();
+               }
+       }
+
+       private static byte[] streamToArray(InputStream in)
+       {
+               ByteArrayOutputStream out = new ByteArrayOutputStream();
+               byte[] buf = new byte[1024];
+               int len;
+
+               try
+               {
+                       while ((len = in.read(buf)) != -1)
+                       {
+                               out.write(buf, 0, len);
+                       }
+                       return out.toByteArray();
+               }
+               catch (IOException e)
+               {
+                       e.printStackTrace();
+               }
+               return null;
+       }
+}
index e35277d..cd30049 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Copyright (C) 2012-2013 Tobias Brunner
- * Hochschule fuer Technik Rapperswil
+ * Copyright (C) 2012-2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
 
 package org.strongswan.android.logic;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-import org.strongswan.android.data.VpnProfile;
-import org.strongswan.android.logic.imc.ImcState;
-import org.strongswan.android.logic.imc.RemediationInstruction;
-
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
@@ -32,9 +22,19 @@ import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.logic.imc.ImcState;
+import org.strongswan.android.logic.imc.RemediationInstruction;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
 public class VpnStateService extends Service
 {
-       private final List<VpnStateListener> mListeners = new ArrayList<VpnStateListener>();
+       private final HashSet<VpnStateListener> mListeners = new HashSet<VpnStateListener>();
        private final IBinder mBinder = new LocalBinder();
        private long mConnectionID = 0;
        private Handler mHandler;
@@ -197,10 +197,11 @@ public class VpnStateService extends Service
                 * 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
+                * instead we call startService() with a specific action which shuts down
                 * the daemon (and closes the TUN device, if any) */
                Context context = getApplicationContext();
                Intent intent = new Intent(context, CharonVpnService.class);
+               intent.setAction(CharonVpnService.DISCONNECT_ACTION);
                context.startService(intent);
        }
 
index 2f62407..f68d0c6 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Copyright (C) 2012 Tobias Brunner
- * Hochschule fuer Technik Rapperswil
+ * Copyright (C) 2012-2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
@@ -32,6 +32,7 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.StringReader;
+import java.util.ArrayList;
 
 public class LogFragment extends Fragment implements Runnable
 {
@@ -121,16 +122,20 @@ public class LogFragment extends Fragment implements Runnable
         * 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
+        * @param lines log lines to log
         */
-       public void logLine(final String line)
+       public void logLines(final ArrayList<String> lines)
        {
                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');
+                               mLogView.beginBatchEdit();
+                               for (String line : lines)
+                               {       /* strip off prefix (month=3, day=2, time=8, thread=2, spaces=3) */
+                                       mLogView.append((line.length() > 18 ? line.substring(18) : line) + '\n');
+                               }
+                               mLogView.endBatchEdit();
                                /* 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() {
@@ -147,18 +152,30 @@ public class LogFragment extends Fragment implements Runnable
        @Override
        public void run()
        {
+               ArrayList<String> lines = null;
+
                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 */
+                               {
+                                       if (lines != null)
+                                       {
+                                               logLines(lines);
+                                               lines = null;
+                                       }
+                                       /* wait until there is more to log */
                                        Thread.sleep(1000);
                                }
                                else
                                {
-                                       logLine(line);
+                                       if (lines == null)
+                                       {
+                                               lines = new ArrayList<>();
+                                       }
+                                       lines.add(line);
                                }
                        }
                        catch (Exception e)
@@ -166,6 +183,10 @@ public class LogFragment extends Fragment implements Runnable
                                break;
                        }
                }
+               if (lines != null)
+               {
+                       logLines(lines);
+               }
        }
 
        /**
index 8904342..5ba1061 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
  * Hochschule fuer Technik Rapperswil
@@ -26,6 +26,7 @@ import android.content.Intent;
 import android.content.ServiceConnection;
 import android.net.VpnService;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.support.v4.app.Fragment;
@@ -141,10 +142,24 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec
        }
 
        @Override
+       public boolean onPrepareOptionsMenu(Menu menu)
+       {
+               if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
+               {
+                       menu.removeItem(R.id.menu_import_profile);
+               }
+               return true;
+       }
+
+       @Override
        public boolean onOptionsItemSelected(MenuItem item)
        {
                switch (item.getItemId())
                {
+                       case R.id.menu_import_profile:
+                               Intent intent = new Intent(this, VpnProfileImportActivity.class);
+                               startActivity(intent);
+                               return true;
                        case R.id.menu_manage_certs:
                                Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
                                startActivity(certIntent);
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java
new file mode 100644 (file)
index 0000000..db2e566
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import org.strongswan.android.data.VpnProfileDataSource;
+
+public class SelectedApplicationsActivity extends AppCompatActivity
+{
+       private static final String LIST_TAG = "ApplicationList";
+       private SelectedApplicationsListFragment mApps;
+
+       @Override
+       protected void onCreate(@Nullable Bundle savedInstanceState)
+       {
+               super.onCreate(savedInstanceState);
+
+               ActionBar actionBar = getSupportActionBar();
+               actionBar.setDisplayHomeAsUpEnabled(true);
+
+               FragmentManager fm = getSupportFragmentManager();
+               mApps = (SelectedApplicationsListFragment)fm.findFragmentByTag(LIST_TAG);
+               if (mApps == null)
+               {
+                       mApps = new SelectedApplicationsListFragment();
+                       fm.beginTransaction().add(android.R.id.content, mApps, LIST_TAG).commit();
+               }
+       }
+
+       @Override
+       public boolean onOptionsItemSelected(MenuItem item)
+       {
+               switch (item.getItemId())
+               {
+                       case android.R.id.home:
+                               prepareResult();
+                               finish();
+                               return true;
+               }
+               return super.onOptionsItemSelected(item);
+       }
+
+       @Override
+       public void onBackPressed()
+       {
+               prepareResult();
+               super.onBackPressed();
+       }
+
+       private void prepareResult()
+       {
+               Intent data = new Intent();
+               data.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, mApps.getSelectedApplications());
+               setResult(RESULT_OK, data);
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java
new file mode 100644 (file)
index 0000000..38c7494
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Filter;
+import android.widget.ListView;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.ui.adapter.SelectedApplicationEntry;
+import org.strongswan.android.ui.adapter.SelectedApplicationsAdapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class SelectedApplicationsListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Pair<List<SelectedApplicationEntry>, List<String>>>, SearchView.OnQueryTextListener
+{
+       private SelectedApplicationsAdapter mAdapter;
+       private SortedSet<String> mSelection;
+
+       @Override
+       public void onActivityCreated(@Nullable Bundle savedInstanceState)
+       {
+               super.onActivityCreated(savedInstanceState);
+               setHasOptionsMenu(true);
+
+               getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+               mAdapter = new SelectedApplicationsAdapter(getActivity());
+               setListAdapter(mAdapter);
+               setListShown(false);
+
+               ArrayList<String> selection;
+               if (savedInstanceState == null)
+               {
+                       selection = getActivity().getIntent().getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
+               }
+               else
+               {
+                       selection = savedInstanceState.getStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
+               }
+               mSelection = new TreeSet<>(selection);
+
+               getLoaderManager().initLoader(0, null, this);
+       }
+
+       @Override
+       public void onSaveInstanceState(Bundle outState)
+       {
+               super.onSaveInstanceState(outState);
+               outState.putStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelection));
+       }
+
+       /**
+        * Returns the package names of all selected apps
+        */
+       public ArrayList<String> getSelectedApplications()
+       {
+               return new ArrayList<>(mSelection);
+       }
+
+       /**
+        * Track check state as ListView is unable to do that when using filters
+        */
+       @Override
+       public void onListItemClick(ListView l, View v, int position, long id)
+       {
+               super.onListItemClick(l, v, position, id);
+               SelectedApplicationEntry item = (SelectedApplicationEntry)getListView().getItemAtPosition(position);
+               item.setSelected(!item.isSelected());
+               if (item.isSelected())
+               {
+                       mSelection.add(item.getInfo().packageName);
+               }
+               else
+               {
+                       mSelection.remove(item.getInfo().packageName);
+               }
+       }
+
+       /**
+        * Manually set the check state as ListView is unable to track that when using filters
+        */
+       private void setCheckState()
+       {
+               for (int i = 0; i < getListView().getCount(); i++)
+               {
+                       SelectedApplicationEntry item = mAdapter.getItem(i);
+                       getListView().setItemChecked(i, item.isSelected());
+               }
+       }
+
+       @Override
+       public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+       {
+               MenuItem item = menu.add(R.string.search);
+               item.setIcon(android.R.drawable.ic_menu_search);
+               item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+               SearchView sv = new SearchView(getActivity());
+               sv.setOnQueryTextListener(this);
+               item.setActionView(sv);
+
+               super.onCreateOptionsMenu(menu, inflater);
+       }
+
+       @Override
+       public Loader<Pair<List<SelectedApplicationEntry>, List<String>>> onCreateLoader(int id, Bundle args)
+       {
+               return new InstalledPackagesLoader(getActivity(), mSelection);
+       }
+
+       @Override
+       public void onLoadFinished(Loader<Pair<List<SelectedApplicationEntry>, List<String>>> loader, Pair<List<SelectedApplicationEntry>, List<String>> data)
+       {
+               mAdapter.setData(data.first);
+               mSelection.removeAll(data.second);
+               setCheckState();
+
+               if (isResumed())
+               {
+                       setListShown(true);
+               }
+               else
+               {
+                       setListShownNoAnimation(true);
+               }
+       }
+
+       @Override
+       public void onLoaderReset(Loader<Pair<List<SelectedApplicationEntry>, List<String>>> loader)
+       {
+               mAdapter.setData(null);
+       }
+
+       @Override
+       public boolean onQueryTextSubmit(String query)
+       {
+               return true;
+       }
+
+       @Override
+       public boolean onQueryTextChange(String newText)
+       {
+               String search = TextUtils.isEmpty(newText) ? null : newText;
+               mAdapter.getFilter().filter(search, new Filter.FilterListener()
+               {
+                       @Override
+                       public void onFilterComplete(int count)
+                       {
+                               setCheckState();
+                       }
+               });
+               return true;
+       }
+
+       public static class InstalledPackagesLoader extends AsyncTaskLoader<Pair<List<SelectedApplicationEntry>, List<String>>>
+       {
+               private final PackageManager mPackageManager;
+               private final SortedSet<String> mSelection;
+               private Pair<List<SelectedApplicationEntry>, List<String>> mData;
+
+               public InstalledPackagesLoader(Context context, SortedSet<String> selection)
+               {
+                       super(context);
+                       mPackageManager = context.getPackageManager();
+                       mSelection = selection;
+               }
+
+               @Override
+               public Pair<List<SelectedApplicationEntry>, List<String>> loadInBackground()
+               {
+                       List<SelectedApplicationEntry> apps = new ArrayList<>();
+                       SortedSet<String> seen = new TreeSet<>();
+                       for (ApplicationInfo info : mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA))
+                       {
+                               /* skip apps that can't access the network anyway */
+                               if (mPackageManager.checkPermission(Manifest.permission.INTERNET, info.packageName) == PackageManager.PERMISSION_GRANTED)
+                               {
+                                       SelectedApplicationEntry entry = new SelectedApplicationEntry(mPackageManager, info);
+                                       entry.setSelected(mSelection.contains(info.packageName));
+                                       apps.add(entry);
+                                       seen.add(info.packageName);
+                               }
+                       }
+                       Collections.sort(apps);
+                       /* check for selected packages that don't exist anymore */
+                       List<String> missing = new ArrayList<>();
+                       for (String pkg : mSelection)
+                       {
+                               if (!seen.contains(pkg))
+                               {
+                                       missing.add(pkg);
+                               }
+                       }
+                       return new Pair<>(apps, missing);
+               }
+
+               @Override
+               protected void onStartLoading()
+               {
+                       if (mData != null)
+                       {       /* if we have data ready, deliver it directly */
+                               deliverResult(mData);
+                       }
+                       if (takeContentChanged() || mData == null)
+                       {
+                               forceLoad();
+                       }
+               }
+
+               @Override
+               public void deliverResult(Pair<List<SelectedApplicationEntry>, List<String>> data)
+               {
+                       if (isReset())
+                       {
+                               return;
+                       }
+                       mData = data;
+                       if (isStarted())
+                       {       /* if it is started we deliver the data directly,
+                                * otherwise this is handled in onStartLoading */
+                               super.deliverResult(data);
+                       }
+               }
+
+               @Override
+               protected void onReset()
+               {
+                       mData = null;
+                       super.onReset();
+               }
+       }
+}
index bf64370..0bae614 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2016 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
  * HSR Hochschule fuer Technik Rapperswil
@@ -57,6 +57,7 @@ import android.widget.TextView;
 
 import org.strongswan.android.R;
 import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
 import org.strongswan.android.data.VpnProfileDataSource;
 import org.strongswan.android.data.VpnType;
 import org.strongswan.android.data.VpnType.VpnTypeFeature;
@@ -65,13 +66,18 @@ import org.strongswan.android.security.TrustedCertificateEntry;
 import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter;
 import org.strongswan.android.ui.widget.TextInputLayoutHelper;
 import org.strongswan.android.utils.Constants;
+import org.strongswan.android.utils.IPRangeSet;
 
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.UUID;
 
 public class VpnProfileDetailActivity extends AppCompatActivity
 {
        private static final int SELECT_TRUSTED_CERTIFICATE = 0;
+       private static final int SELECT_APPLICATIONS = 1;
 
        private VpnProfileDataSource mDataSource;
        private Long mId;
@@ -81,6 +87,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity
        private String mSelectedUserId;
        private TrustedCertificateEntry mUserCertEntry;
        private VpnType mVpnType = VpnType.IKEV2_EAP;
+       private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
+       private SortedSet<String> mSelectedApps = new TreeSet<>();
        private VpnProfile mProfile;
        private MultiAutoCompleteTextView mName;
        private TextInputLayoutHelper mNameWrap;
@@ -105,8 +113,14 @@ public class VpnProfileDetailActivity extends AppCompatActivity
        private TextInputLayoutHelper mMTUWrap;
        private EditText mPort;
        private TextInputLayoutHelper mPortWrap;
+       private EditText mIncludedSubnets;
+       private TextInputLayoutHelper mIncludedSubnetsWrap;
+       private EditText mExcludedSubnets;
+       private TextInputLayoutHelper mExcludedSubnetsWrap;
        private CheckBox mBlockIPv4;
        private CheckBox mBlockIPv6;
+       private Spinner mSelectSelectedAppsHandling;
+       private RelativeLayout mSelectApps;
 
        @Override
        public void onCreate(Bundle savedInstanceState)
@@ -149,9 +163,16 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap);
                mPort = (EditText)findViewById(R.id.port);
                mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap);
+               mIncludedSubnets = (EditText)findViewById(R.id.included_subnets);
+               mIncludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.included_subnets_wrap);
+               mExcludedSubnets = (EditText)findViewById(R.id.excluded_subnets);
+               mExcludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.excluded_subnets_wrap);
                mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4);
                mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6);
 
+               mSelectSelectedAppsHandling = (Spinner)findViewById(R.id.apps_handling);
+               mSelectApps = (RelativeLayout)findViewById(R.id.select_applications);
+
                final SpaceTokenizer spaceTokenizer = new SpaceTokenizer();
                mName.setTokenizer(spaceTokenizer);
                mRemoteId.setTokenizer(spaceTokenizer);
@@ -256,6 +277,32 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                        }
                });
 
+               mSelectSelectedAppsHandling.setOnItemSelectedListener(new OnItemSelectedListener() {
+                       @Override
+                       public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
+                       {
+                               mSelectedAppsHandling = SelectedAppsHandling.values()[position];
+                               updateAppsSelector();
+                       }
+
+                       @Override
+                       public void onNothingSelected(AdapterView<?> parent)
+                       {       /* should not happen */
+                               mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
+                               updateAppsSelector();
+                       }
+               });
+
+               mSelectApps.setOnClickListener(new OnClickListener() {
+                       @Override
+                       public void onClick(View v)
+                       {
+                               Intent intent = new Intent(VpnProfileDetailActivity.this, SelectedApplicationsActivity.class);
+                               intent.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
+                               startActivityForResult(intent, SELECT_APPLICATIONS);
+                       }
+               });
+
                mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
                if (mId == null)
                {
@@ -268,6 +315,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                updateCredentialView();
                updateCertificateSelector();
                updateAdvancedSettings();
+               updateAppsSelector();
        }
 
        @Override
@@ -297,6 +345,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                {
                        outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
                }
+               outState.putStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
        }
 
        @Override
@@ -338,6 +387,14 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                                        updateCertificateSelector();
                                }
                                break;
+                       case SELECT_APPLICATIONS:
+                               if (resultCode == RESULT_OK)
+                               {
+                                       ArrayList<String> selection = data.getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
+                                       mSelectedApps = new TreeSet<>(selection);
+                                       updateAppsSelector();
+                               }
+                               break;
                        default:
                                super.onActivityResult(requestCode, resultCode, data);
                }
@@ -427,6 +484,40 @@ public class VpnProfileDetailActivity extends AppCompatActivity
        }
 
        /**
+        * Update the application selection UI
+        */
+       private void updateAppsSelector()
+       {
+               if (mSelectedAppsHandling == SelectedAppsHandling.SELECTED_APPS_DISABLE)
+               {
+                       mSelectApps.setEnabled(false);
+                       mSelectApps.setVisibility(View.GONE);
+
+               }
+               else
+               {
+                       mSelectApps.setEnabled(true);
+                       mSelectApps.setVisibility(View.VISIBLE);
+
+                       ((TextView)mSelectApps.findViewById(android.R.id.text1)).setText(R.string.profile_select_apps);
+                       String selected;
+                       switch (mSelectedApps.size())
+                       {
+                               case 0:
+                                       selected = getString(R.string.profile_select_no_apps);
+                                       break;
+                               case 1:
+                                       selected = getString(R.string.profile_select_one_app);
+                                       break;
+                               default:
+                                       selected = getString(R.string.profile_select_x_apps, mSelectedApps.size());
+                                       break;
+                       }
+                       ((TextView)mSelectApps.findViewById(android.R.id.text2)).setText(selected);
+               }
+       }
+
+       /**
         * Update the advanced settings UI depending on whether any advanced
         * settings have already been made.
         */
@@ -437,7 +528,9 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                {
                        Integer st = mProfile.getSplitTunneling();
                        show = mProfile.getRemoteId() != null || mProfile.getMTU() != null ||
-                                  mProfile.getPort() != null || (st != null && st != 0);
+                                  mProfile.getPort() != null || (st != null && st != 0) ||
+                                  mProfile.getIncludedSubnets() != null || mProfile.getExcludedSubnets() != null ||
+                                  mProfile.getSelectedAppsHandling() != SelectedAppsHandling.SELECTED_APPS_DISABLE;
                }
                mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
                mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
@@ -510,6 +603,16 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                        mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), Constants.MTU_MIN, Constants.MTU_MAX));
                        valid = false;
                }
+               if (!validateSubnets(mIncludedSubnets))
+               {
+                       mIncludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
+                       valid = false;
+               }
+               if (!validateSubnets(mExcludedSubnets))
+               {
+                       mExcludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
+                       valid = false;
+               }
                if (!validateInteger(mPort, 1, 65535))
                {
                        mPortWrap.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535));
@@ -547,10 +650,16 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                mProfile.setRemoteId(remote_id.isEmpty() ? null : remote_id);
                mProfile.setMTU(getInteger(mMTU));
                mProfile.setPort(getInteger(mPort));
+               String included = mIncludedSubnets.getText().toString().trim();
+               mProfile.setIncludedSubnets(included.isEmpty() ? null : included);
+               String excluded = mExcludedSubnets.getText().toString().trim();
+               mProfile.setExcludedSubnets(excluded.isEmpty() ? null : excluded);
                int st = 0;
                st |= mBlockIPv4.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
                st |= mBlockIPv6.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
                mProfile.setSplitTunneling(st == 0 ? null : st);
+               mProfile.setSelectedAppsHandling(mSelectedAppsHandling);
+               mProfile.setSelectedApps(mSelectedApps);
        }
 
        /**
@@ -576,8 +685,12 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                                mRemoteId.setText(mProfile.getRemoteId());
                                mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
                                mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null);
+                               mIncludedSubnets.setText(mProfile.getIncludedSubnets());
+                               mExcludedSubnets.setText(mProfile.getExcludedSubnets());
                                mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0);
                                mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0);
+                               mSelectedAppsHandling = mProfile.getSelectedAppsHandling();
+                               mSelectedApps = mProfile.getSelectedAppsSet();
                                useralias = mProfile.getUserCertificateAlias();
                                local_id = mProfile.getLocalId();
                                alias = mProfile.getCertificateAlias();
@@ -620,6 +733,13 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                                mCertEntry = null;
                        }
                }
+
+               mSelectSelectedAppsHandling.setSelection(mSelectedAppsHandling.ordinal());
+               if (savedInstanceState != null)
+               {
+                       ArrayList<String> selectedApps = savedInstanceState.getStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
+                       mSelectedApps = new TreeSet<>(selectedApps);
+               }
        }
 
        /**
@@ -665,6 +785,17 @@ public class VpnProfileDetailActivity extends AppCompatActivity
                }
        }
 
+       /**
+        * Check that the value in the given text box is a valid list of subnets/ranges
+        *
+        * @param view text box
+        */
+       private boolean validateSubnets(EditText view)
+       {
+               String value = view.getText().toString().trim();
+               return value.isEmpty() || IPRangeSet.fromString(value) != null;
+       }
+
        private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
        {
                @Override
index 1b6b6e8..5833258 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 Tobias Brunner
+ * Copyright (C) 2016-2017 Tobias Brunner
  * HSR Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
@@ -25,12 +25,14 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.Loader;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.security.KeyChain;
 import android.security.KeyChainAliasCallback;
 import android.security.KeyChainException;
 import android.support.v4.content.LocalBroadcastManager;
 import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
 import android.util.Base64;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -43,10 +45,12 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.strongswan.android.R;
 import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
 import org.strongswan.android.data.VpnProfileDataSource;
 import org.strongswan.android.data.VpnType;
 import org.strongswan.android.data.VpnType.VpnTypeFeature;
@@ -54,6 +58,7 @@ import org.strongswan.android.logic.TrustedCertificateManager;
 import org.strongswan.android.security.TrustedCertificateEntry;
 import org.strongswan.android.ui.widget.TextInputLayoutHelper;
 import org.strongswan.android.utils.Constants;
+import org.strongswan.android.utils.IPRangeSet;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -68,6 +73,7 @@ import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.UUID;
 
 import javax.net.ssl.SSLHandshakeException;
@@ -75,7 +81,9 @@ import javax.net.ssl.SSLHandshakeException;
 public class VpnProfileImportActivity extends AppCompatActivity
 {
        private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED";
+       private static final String PROFILE_URI = "PROFILE_URI";
        private static final int INSTALL_PKCS12 = 0;
+       private static final int OPEN_DOCUMENT = 1;
        private static final int PROFILE_LOADER = 0;
        private static final int USER_CERT_LOADER = 1;
 
@@ -107,7 +115,7 @@ public class VpnProfileImportActivity extends AppCompatActivity
                @Override
                public Loader<ProfileLoadResult> onCreateLoader(int id, Bundle args)
                {
-                       return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData());
+                       return new ProfileLoader(VpnProfileImportActivity.this, (Uri)args.getParcelable(PROFILE_URI));
                }
 
                @Override
@@ -197,16 +205,13 @@ public class VpnProfileImportActivity extends AppCompatActivity
                String action = intent.getAction();
                if (Intent.ACTION_VIEW.equals(action))
                {
-                       mProgress = ProgressDialog.show(this, null, getString(R.string.loading),
-                                                                                       true, true, new DialogInterface.OnCancelListener() {
-                               @Override
-                               public void onCancel(DialogInterface dialog)
-                               {
-                                       finish();
-                               }
-                       });
-
-                       getLoaderManager().initLoader(PROFILE_LOADER, null, mProfileLoaderCallbacks);
+                       loadProfile(getIntent().getData());
+               }
+               else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
+               {
+                       Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                       openIntent.setType("*/*");
+                       startActivityForResult(openIntent, OPEN_DOCUMENT);
                }
 
                if (savedInstanceState != null)
@@ -279,9 +284,34 @@ public class VpnProfileImportActivity extends AppCompatActivity
                                        mImportUserCert.setEnabled(false);
                                        mSelectUserCert.performClick();
                                }
+                               break;
+                       case OPEN_DOCUMENT:
+                               if (resultCode == Activity.RESULT_OK && data != null)
+                               {
+                                       loadProfile(data.getData());
+                                       return;
+                               }
+                               finish();
+                               break;
                }
        }
 
+       private void loadProfile(Uri uri)
+       {
+               mProgress = ProgressDialog.show(this, null, getString(R.string.loading),
+                               true, true, new DialogInterface.OnCancelListener() {
+                                       @Override
+                                       public void onCancel(DialogInterface dialog)
+                                       {
+                                               finish();
+                                       }
+                               });
+
+               Bundle args = new Bundle();
+               args.putParcelable(PROFILE_URI, uri);
+               getLoaderManager().initLoader(PROFILE_LOADER, args, mProfileLoaderCallbacks);
+       }
+
        public void handleProfile(ProfileLoadResult data)
        {
                mProgress.dismiss();
@@ -362,7 +392,15 @@ public class VpnProfileImportActivity extends AppCompatActivity
                mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE);
                mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE);
 
-               updateUserCertView();
+               if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
+               {       /* try to load an existing certificate with the default name */
+                       if (mUserCertLoading == null)
+                       {
+                               mUserCertLoading = getString(R.string.profile_cert_alias, mProfile.getName());
+                               getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
+                       }
+                       updateUserCertView();
+               }
 
                if (mProfile.Certificate != null)
                {
@@ -456,11 +494,28 @@ public class VpnProfileImportActivity extends AppCompatActivity
                JSONObject split = obj.optJSONObject("split-tunneling");
                if (split != null)
                {
+                       String included = getSubnets(split, "subnets");
+                       profile.setIncludedSubnets(included != null ? included : null);
+                       String excluded = getSubnets(split, "excluded");
+                       profile.setExcludedSubnets(excluded != null ? excluded : null);
                        int st = 0;
                        st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
                        st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
                        profile.setSplitTunneling(st == 0 ? null : st);
                }
+               /* only one of these can be set, prefer specific apps */
+               String selectedApps = getApps(obj.optJSONArray("apps"));
+               String excludedApps = getApps(obj.optJSONArray("excluded-apps"));
+               if (!TextUtils.isEmpty(selectedApps))
+               {
+                       profile.setSelectedApps(selectedApps);
+                       profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_ONLY);
+               }
+               else if (!TextUtils.isEmpty(excludedApps))
+               {
+                       profile.setSelectedApps(excludedApps);
+                       profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_EXCLUDE);
+               }
                return profile;
        }
 
@@ -470,6 +525,52 @@ public class VpnProfileImportActivity extends AppCompatActivity
                return res < min || res > max ? null : res;
        }
 
+       private String getSubnets(JSONObject split, String key) throws JSONException
+       {
+               ArrayList<String> subnets = new ArrayList<>();
+               JSONArray arr = split.optJSONArray(key);
+               if (arr != null)
+               {
+                       for (int i = 0; i < arr.length(); i++)
+                       {       /* replace all spaces, e.g. in "192.168.1.1 - 192.168.1.10" */
+                               subnets.add(arr.getString(i).replace(" ", ""));
+                       }
+               }
+               else
+               {
+                       String value = split.optString(key, null);
+                       if (!TextUtils.isEmpty(value))
+                       {
+                               subnets.add(value);
+                       }
+               }
+               if (subnets.size() > 0)
+               {
+                       String joined = TextUtils.join(" ", subnets);
+                       IPRangeSet ranges = IPRangeSet.fromString(joined);
+                       if (ranges == null)
+                       {
+                               throw new JSONException(getString(R.string.profile_import_failed_value,
+                                                                                                 "split-tunneling." + key));
+                       }
+                       return ranges.toString();
+               }
+               return null;
+       }
+
+       private String getApps(JSONArray arr) throws JSONException
+       {
+               ArrayList<String> apps = new ArrayList<>();
+               if (arr != null)
+               {
+                       for (int i = 0; i < arr.length(); i++)
+                       {
+                               apps.add(arr.getString(i));
+                       }
+               }
+               return TextUtils.join(" ", apps);
+       }
+
        /**
         * Save or update the profile depending on whether we actually have a
         * profile object or not (this was created in updateProfileData)
index 1ea0151..0a8e1f3 100644 (file)
@@ -189,6 +189,11 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
                ImcState imcState = mService.getImcState();
                String name = "";
 
+               if (getActivity() == null)
+               {
+                       return;
+               }
+
                if (profile != null)
                {
                        name = profile.getName();
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationEntry.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationEntry.java
new file mode 100644 (file)
index 0000000..0bfbbcf
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui.adapter;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+
+import java.text.Collator;
+
+public class SelectedApplicationEntry implements Comparable<SelectedApplicationEntry>
+{
+       private final ApplicationInfo mInfo;
+       private final Drawable mIcon;
+       private final String mName;
+       private boolean mSelected;
+
+       public SelectedApplicationEntry(PackageManager packageManager, ApplicationInfo info)
+       {
+               mInfo = info;
+               CharSequence name = info.loadLabel(packageManager);
+               mName = name == null ? info.packageName : name.toString();
+               mIcon = info.loadIcon(packageManager);
+       }
+
+       public void setSelected(boolean selected)
+       {
+               mSelected = selected;
+       }
+
+       public boolean isSelected()
+       {
+               return mSelected;
+       }
+
+       public ApplicationInfo getInfo()
+       {
+               return mInfo;
+       }
+
+       public Drawable getIcon()
+       {
+               return mIcon;
+       }
+
+       @Override
+       public String toString()
+       {
+               return mName;
+       }
+
+       @Override
+       public int compareTo(@NonNull SelectedApplicationEntry another)
+       {
+               return Collator.getInstance().compare(toString(), another.toString());
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java
new file mode 100644 (file)
index 0000000..2a2fe95
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui.adapter;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.strongswan.android.R;
+import org.strongswan.android.ui.widget.CheckableLinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectedApplicationsAdapter extends BaseAdapter implements Filterable
+{
+       private Context mContext;
+       private final Object mLock = new Object();
+       private List<SelectedApplicationEntry> mData;
+       private List<SelectedApplicationEntry> mDataFiltered;
+       private SelectedApplicationsFilter mFilter;
+
+       public SelectedApplicationsAdapter(Context context)
+       {
+               mContext = context;
+               mData = mDataFiltered = new ArrayList<>();
+       }
+
+       /**
+        * Set new data for this adapter.
+        *
+        * @param data the new data (null to clear)
+        */
+       public void setData(List<SelectedApplicationEntry> data)
+       {
+               synchronized (mLock)
+               {
+                       mData.clear();
+                       mDataFiltered = mData;
+                       if (data != null)
+                       {
+                               mData.addAll(data);
+                       }
+               }
+               notifyDataSetChanged();
+       }
+
+       @Override
+       public int getCount()
+       {
+               return mDataFiltered.size();
+       }
+
+       @Override
+       public SelectedApplicationEntry getItem(int position)
+       {
+               return mDataFiltered.get(position);
+       }
+
+       @Override
+       public long getItemId(int position)
+       {
+               return mDataFiltered.get(position).toString().hashCode();
+       }
+
+       @Override
+       public View getView(int position, View convertView, ViewGroup parent)
+       {
+               View view;
+               if (convertView != null)
+               {
+                       view = convertView;
+               }
+               else
+               {
+                       LayoutInflater inflater = LayoutInflater.from(mContext);
+                       view = inflater.inflate(R.layout.selected_application_item, parent, false);
+               }
+               SelectedApplicationEntry item = getItem(position);
+               CheckableLinearLayout checkable = (CheckableLinearLayout)view;
+               checkable.setChecked(item.isSelected());
+               ImageView icon = (ImageView)view.findViewById(R.id.app_icon);
+               icon.setImageDrawable(item.getIcon());
+               TextView text = (TextView)view.findViewById(R.id.app_name);
+               text.setText(item.toString());
+               return view;
+       }
+
+       @Override
+       public Filter getFilter()
+       {
+               if (mFilter == null)
+               {
+                       mFilter = new SelectedApplicationsFilter();
+               }
+               return mFilter;
+       }
+
+       private class SelectedApplicationsFilter extends Filter
+       {
+
+               @Override
+               protected FilterResults performFiltering(CharSequence constraint)
+               {
+                       FilterResults results = new FilterResults();
+                       ArrayList<SelectedApplicationEntry> data, filtered;
+
+                       synchronized (mLock)
+                       {
+                               data = new ArrayList<>(mData);
+                       }
+
+                       if (TextUtils.isEmpty(constraint))
+                       {
+                               filtered = data;
+                       }
+                       else
+                       {
+                               String filter = constraint.toString().toLowerCase();
+                               filtered = new ArrayList<>();
+                               for (SelectedApplicationEntry entry : data)
+                               {
+                                       if (entry.toString().toLowerCase().contains(filter))
+                                       {
+                                               filtered.add(entry);
+                                       }
+                               }
+                       }
+                       results.values = filtered;
+                       results.count = filtered.size();
+                       return results;
+               }
+
+               @Override
+               @SuppressWarnings("unchecked")
+               protected void publishResults(CharSequence constraint, FilterResults results)
+               {
+                       mDataFiltered = (List<SelectedApplicationEntry>)results.values;
+                       notifyDataSetChanged();
+               }
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/CheckableLinearLayout.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/widget/CheckableLinearLayout.java
new file mode 100644 (file)
index 0000000..fb5e85b
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui.widget;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.LinearLayout;
+
+public class CheckableLinearLayout extends LinearLayout implements Checkable
+{
+       private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
+       private boolean mChecked;
+
+       public CheckableLinearLayout(Context context, @Nullable AttributeSet attrs)
+       {
+               super(context, attrs);
+       }
+
+       @Override
+       public void setChecked(boolean checked)
+       {
+               if (mChecked != checked)
+               {
+                       mChecked = checked;
+                       refreshDrawableState();
+               }
+       }
+
+       @Override
+       public boolean isChecked()
+       {
+               return mChecked;
+       }
+
+       @Override
+       public void toggle()
+       {
+               setChecked(!mChecked);
+       }
+
+       @Override
+       protected int[] onCreateDrawableState(int extraSpace)
+       {
+               final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+               if (isChecked())
+               {
+                       mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+               }
+               return drawableState;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java
new file mode 100644 (file)
index 0000000..6c0aa65
--- /dev/null
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2012-2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.utils;
+
+import android.support.annotation.NonNull;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class that represents a range of IP addresses. This range could be a proper subnet, but that's
+ * not necessarily the case (see {@code getPrefix} and {@code toSubnets}).
+ */
+public class IPRange implements Comparable<IPRange>
+{
+       private final byte[] mBitmask = { (byte)0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
+       private byte[] mFrom;
+       private byte[] mTo;
+       private Integer mPrefix;
+
+       /**
+        * Determine if the range is a proper subnet and, if so, what the network prefix is.
+        */
+       private void determinePrefix()
+       {
+               boolean matching = true;
+
+               mPrefix = mFrom.length * 8;
+               for (int i = 0; i < mFrom.length; i++)
+               {
+                       for (int bit = 0; bit < 8; bit++)
+                       {
+                               if (matching)
+                               {
+                                       if ((mFrom[i] & mBitmask[bit]) != (mTo[i] & mBitmask[bit]))
+                                       {
+                                               mPrefix = (i * 8) + bit;
+                                               matching = false;
+                                       }
+                               }
+                               else
+                               {
+                                       if ((mFrom[i] & mBitmask[bit]) != 0 || (mTo[i] & mBitmask[bit]) == 0)
+                                       {
+                                               mPrefix = null;
+                                               return;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private IPRange(byte[] from, byte[] to)
+       {
+               mFrom = from;
+               mTo = to;
+               determinePrefix();
+       }
+
+       public IPRange(String from, String to) throws UnknownHostException
+       {
+               this(InetAddress.getByName(from), InetAddress.getByName(to));
+       }
+
+       public IPRange(InetAddress from, InetAddress to)
+       {
+               initializeFromRange(from, to);
+       }
+
+       private void initializeFromRange(InetAddress from, InetAddress to)
+       {
+               byte[] fa = from.getAddress(), ta = to.getAddress();
+               if (fa.length != ta.length)
+               {
+                       throw new IllegalArgumentException("Invalid range");
+               }
+               if (compareAddr(fa, ta) < 0)
+               {
+                       mFrom = fa;
+                       mTo = ta;
+               }
+               else
+               {
+                       mTo = fa;
+                       mFrom = ta;
+               }
+               determinePrefix();
+       }
+
+       public IPRange(String base, int prefix) throws UnknownHostException
+       {
+               this(InetAddress.getByName(base), prefix);
+       }
+
+       public IPRange(InetAddress base, int prefix)
+       {
+               this(base.getAddress(), prefix);
+       }
+
+       private IPRange(byte[] from, int prefix)
+       {
+               initializeFromCIDR(from, prefix);
+       }
+
+       private void initializeFromCIDR(byte[] from, int prefix)
+       {
+               if (from.length != 4 && from.length != 16)
+               {
+                       throw new IllegalArgumentException("Invalid address");
+               }
+               if (prefix < 0 || prefix > from.length * 8)
+               {
+                       throw new IllegalArgumentException("Invalid prefix");
+               }
+               byte[] to = from.clone();
+               byte mask = (byte)(0xff << (8 - prefix % 8));
+               int i = prefix / 8;
+
+               if (i < from.length)
+               {
+                       from[i] = (byte)(from[i] & mask);
+                       to[i] = (byte)(to[i] | ~mask);
+                       Arrays.fill(from, i+1, from.length, (byte)0);
+                       Arrays.fill(to, i+1, to.length, (byte)0xff);
+               }
+               mFrom = from;
+               mTo = to;
+               mPrefix = prefix;
+       }
+
+       public IPRange(String cidr) throws UnknownHostException
+       {
+               /* only verify the basic structure */
+               if (!cidr.matches("(?i)^(([0-9.]+)|([0-9a-f:]+))(-(([0-9.]+)|([0-9a-f:]+))|(/\\d+))?$"))
+               {
+                       throw new IllegalArgumentException("Invalid CIDR or range notation");
+               }
+               if (cidr.contains("-"))
+               {
+                       String[] parts = cidr.split("-");
+                       InetAddress from = InetAddress.getByName(parts[0]);
+                       InetAddress to = InetAddress.getByName(parts[1]);
+                       initializeFromRange(from, to);
+               }
+               else
+               {
+                       String[] parts = cidr.split("/");
+                       InetAddress addr = InetAddress.getByName(parts[0]);
+                       byte[] base = addr.getAddress();
+                       int prefix = base.length * 8;
+                       if (parts.length > 1)
+                       {
+                               prefix = Integer.parseInt(parts[1]);
+                       }
+                       initializeFromCIDR(base, prefix);
+               }
+       }
+
+       /**
+        * Returns the first address of the range. The network ID in case this is a proper subnet.
+        */
+       public InetAddress getFrom()
+       {
+               try
+               {
+                       return InetAddress.getByAddress(mFrom);
+               }
+               catch (UnknownHostException ignored)
+               {
+                       return null;
+               }
+       }
+
+       /**
+        * Returns the last address of the range.
+        */
+       public InetAddress getTo()
+       {
+               try
+               {
+                       return InetAddress.getByAddress(mTo);
+               }
+               catch (UnknownHostException ignored)
+               {
+                       return null;
+               }
+       }
+
+       /**
+        * If this range is a proper subnet returns its prefix, otherwise returns null.
+        */
+       public Integer getPrefix()
+       {
+               return mPrefix;
+       }
+
+       @Override
+       public int compareTo(@NonNull IPRange other)
+       {
+               int cmp = compareAddr(mFrom, other.mFrom);
+               if (cmp == 0)
+               {       /* smaller ranges first */
+                       cmp = compareAddr(mTo, other.mTo);
+               }
+               return cmp;
+       }
+
+       @Override
+       public boolean equals(Object o)
+       {
+               if (o == null || !(o instanceof IPRange))
+               {
+                       return false;
+               }
+               return this == o || compareTo((IPRange)o) == 0;
+       }
+
+       @Override
+       public String toString()
+       {
+               try
+               {
+                       if (mPrefix != null)
+                       {
+                               return InetAddress.getByAddress(mFrom).getHostAddress() + "/" + mPrefix;
+                       }
+                       return InetAddress.getByAddress(mFrom).getHostAddress() + "-" +
+                                  InetAddress.getByAddress(mTo).getHostAddress();
+               }
+               catch (UnknownHostException ignored)
+               {
+                       return super.toString();
+               }
+       }
+
+       private int compareAddr(byte a[], byte b[])
+       {
+               if (a.length != b.length)
+               {
+                       return (a.length < b.length) ? -1 : 1;
+               }
+               for (int i = 0; i < a.length; i++)
+               {
+                       if (a[i] != b[i])
+                       {
+                               if (((int)a[i] & 0xff) < ((int)b[i] & 0xff))
+                               {
+                                       return -1;
+                               }
+                               else
+                               {
+                                       return 1;
+                               }
+                       }
+               }
+               return 0;
+       }
+
+       /**
+        * Check if this range fully contains the given range.
+        */
+       public boolean contains(IPRange range)
+       {
+               return compareAddr(mFrom, range.mFrom) <= 0 && compareAddr(range.mTo, mTo) <= 0;
+       }
+
+       /**
+        * Check if this and the given range overlap.
+        */
+       public boolean overlaps(IPRange range)
+       {
+               return !(compareAddr(mTo, range.mFrom) < 0 || compareAddr(range.mTo, mFrom) < 0);
+       }
+
+       private byte[] dec(byte[] addr)
+       {
+               for (int i = addr.length - 1; i >= 0; i--)
+               {
+                       if (--addr[i] != (byte)0xff)
+                       {
+                               break;
+                       }
+               }
+               return addr;
+       }
+
+       private byte[] inc(byte[] addr)
+       {
+               for (int i = addr.length - 1; i >= 0; i--)
+               {
+                       if (++addr[i] != 0)
+                       {
+                               break;
+                       }
+               }
+               return addr;
+       }
+
+       /**
+        * Remove the given range from the current range.  Returns a list of resulting ranges (these are
+        * not proper subnets). At most two ranges are returned, in case the given range is contained in
+        * this but does not equal it, which would result in an empty list (which is also the case if
+        * this range is fully contained in the given range).
+        */
+       public List<IPRange> remove(IPRange range)
+       {
+               ArrayList<IPRange> list = new ArrayList<>();
+               if (!overlaps(range))
+               {       /*           | this | or | this |
+                    * | range |                     | range | */
+                       list.add(this);
+               }
+               else if (!range.contains(this))
+               {       /* we are not completely removed, so none of these cases applies:
+                        * | this  | or  | this  |   or   | this  |
+                    * | range |     | range   |    |   range | */
+                       if (compareAddr(mFrom, range.mFrom) < 0 && compareAddr(range.mTo, mTo) < 0)
+                       {       /* the removed range is completely within our boundaries:
+                                * |    this    |
+                                *   | range |   */
+                               list.add(new IPRange(mFrom, dec(range.mFrom.clone())));
+                               list.add(new IPRange(inc(range.mTo.clone()), mTo));
+                       }
+                       else
+                       {       /* one end is within our boundaries the other at or outside it:
+                            * | this     | or    | this     | or | this    |  or  | this    |
+                            * | range |       | range |            | range |           | range | */
+                               byte[] from = compareAddr(mFrom, range.mFrom) < 0 ? mFrom : inc(range.mTo.clone());
+                               byte[] to = compareAddr(mTo, range.mTo) > 0 ? mTo : dec(range.mFrom.clone());
+                               list.add(new IPRange(from, to));
+                       }
+               }
+               return list;
+       }
+
+       private boolean adjacent(IPRange range)
+       {
+               if (compareAddr(mTo, range.mFrom) < 0)
+               {
+                       byte[] to = inc(mTo.clone());
+                       return compareAddr(to, range.mFrom) == 0;
+               }
+               byte[] from = dec(mFrom.clone());
+               return compareAddr(from, range.mTo) == 0;
+       }
+
+       /**
+        * Merge two adjacent or overlapping ranges, returns null if it's not possible to merge them.
+        */
+       public IPRange merge(IPRange range)
+       {
+               if (overlaps(range))
+               {
+                       if (contains(range))
+                       {
+                               return this;
+                       }
+                       else if (range.contains(this))
+                       {
+                               return range;
+                       }
+               }
+               else if (!adjacent(range))
+               {
+                       return null;
+               }
+               byte[] from = compareAddr(mFrom, range.mFrom) < 0 ? mFrom : range.mFrom;
+               byte[] to = compareAddr(mTo, range.mTo) > 0 ? mTo : range.mTo;
+               return new IPRange(from, to);
+       }
+
+       /**
+        * Split the given range into a sorted list of proper subnets.
+        */
+       public List<IPRange> toSubnets()
+       {
+               ArrayList<IPRange> list = new ArrayList<>();
+               if (mPrefix != null)
+               {
+                       list.add(this);
+               }
+               else
+               {
+                       int i = 0, bit = 0, prefix, netmask, common_byte, common_bit;
+                       int from_cur, from_prev = 0, to_cur, to_prev = 1;
+                       boolean from_full = true, to_full = true;
+
+                       byte[] from = mFrom.clone();
+                       byte[] to = mTo.clone();
+
+                       /* find a common prefix */
+                       while (i < from.length && (from[i] & mBitmask[bit]) == (to[i] & mBitmask[bit]))
+                       {
+                               if (++bit == 8)
+                               {
+                                       bit = 0;
+                                       i++;
+                               }
+                       }
+                       prefix = i * 8 + bit;
+
+                       /* at this point we know that the addresses are either equal, or that the
+                        * current bits in the 'from' and 'to' addresses are 0 and 1, respectively.
+                        * we now look at the rest of the bits as two binary trees (0=left, 1=right)
+                        * where 'from' and 'to' are both leaf nodes.  all leaf nodes between these
+                        * nodes are addresses contained in the range.  to collect them as subnets
+                        * we follow the trees from both leaf nodes to their root node and record
+                        * all complete subtrees (right for from, left for to) we come across as
+                        * subnets.  in that process host bits are zeroed out.  if both addresses
+                        * are equal we won't enter the loop below.
+                        *      0_____|_____1       for the 'from' address we assume we start on a
+                        *   0__|__ 1    0__|__1    left subtree (0) and follow the left edges until
+                        *  _|_   _|_   _|_   _|_   we reach the root of this subtree, which is
+                        * |   | |   | |   | |   |  either the root of this whole 'from'-subtree
+                        * 0   1 0   1 0   1 0   1  (causing us to leave the loop) or the root node
+                        * of the right subtree (1) of another node (which actually could be the
+                        * leaf node we start from).  that whole subtree gets recorded as subnet.
+                        * next we follow the right edges to the root of that subtree which again is
+                        * either the 'from'-root or the root node in the left subtree (0) of
+                        * another node.  the complete right subtree of that node is the next subnet
+                        * we record.  from there we assume that we are in that right subtree and
+                        * recursively follow right edges to its root.  for the 'to' address the
+                        * procedure is exactly the same but with left and right reversed.
+                        */
+                       if (++bit == 8)
+                       {
+                               bit = 0;
+                               i++;
+                       }
+                       common_byte = i;
+                       common_bit = bit;
+                       netmask = from.length * 8;
+                       for (i = from.length - 1; i >= common_byte; i--)
+                       {
+                               int bit_min = (i == common_byte) ? common_bit : 0;
+                               for (bit = 7; bit >= bit_min; bit--)
+                               {
+                                       byte mask = mBitmask[bit];
+
+                                       from_cur = from[i] & mask;
+                                       if (from_prev == 0 && from_cur != 0)
+                                       {       /* 0 -> 1: subnet is the whole current (right) subtree */
+                                               list.add(new IPRange(from.clone(), netmask));
+                                               from_full = false;
+                                       }
+                                       else if (from_prev != 0 && from_cur == 0)
+                                       {       /* 1 -> 0: invert bit to switch to right subtree and add it */
+                                               from[i] ^= mask;
+                                               list.add(new IPRange(from.clone(), netmask));
+                                               from_cur = 1;
+                                       }
+                                       /* clear the current bit */
+                                       from[i] &= ~mask;
+                                       from_prev = from_cur;
+
+                                       to_cur = to[i] & mask;
+                                       if (to_prev != 0 && to_cur == 0)
+                                       {       /* 1 -> 0: subnet is the whole current (left) subtree */
+                                               list.add(new IPRange(to.clone(), netmask));
+                                               to_full = false;
+                                       }
+                                       else if (to_prev == 0 && to_cur != 0)
+                                       {       /* 0 -> 1: invert bit to switch to left subtree and add it */
+                                               to[i] ^= mask;
+                                               list.add(new IPRange(to.clone(), netmask));
+                                               to_cur = 0;
+                                       }
+                                       /* clear the current bit */
+                                       to[i] &= ~mask;
+                                       to_prev = to_cur;
+                                       netmask--;
+                               }
+                       }
+
+                       if (from_full && to_full)
+                       {       /* full subnet (from=to or from=0.. and to=1.. after common prefix) - not reachable
+                                * due to the shortcut at the top */
+                               list.add(new IPRange(from.clone(), prefix));
+                       }
+                       else if (from_full)
+                       {       /* full from subnet (from=0.. after prefix) */
+                               list.add(new IPRange(from.clone(), prefix + 1));
+                       }
+                       else if (to_full)
+                       {       /* full to subnet (to=1.. after prefix) */
+                               list.add(new IPRange(to.clone(), prefix + 1));
+                       }
+               }
+               Collections.sort(list);
+               return list;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRangeSet.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRangeSet.java
new file mode 100644 (file)
index 0000000..4716055
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2012-2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * Class that represents a set of IP address ranges (not necessarily proper subnets) and allows
+ * modifying the set and enumerating the resulting subnets.
+ */
+public class IPRangeSet implements Iterable<IPRange>
+{
+       private TreeSet<IPRange> mRanges = new TreeSet<>();
+
+       /**
+        * Parse the given string (space separated ranges in CIDR or range notation) and return the
+        * resulting set or {@code null} if the string was invalid. An empty set is returned if the given string
+        * is {@code null}.
+        */
+       public static IPRangeSet fromString(String ranges)
+       {
+               IPRangeSet set = new IPRangeSet();
+               if (ranges != null)
+               {
+                       for (String range : ranges.split("\\s+"))
+                       {
+                               try
+                               {
+                                       set.add(new IPRange(range));
+                               }
+                               catch (Exception unused)
+                               {       /* besides due to invalid strings exceptions might get thrown if the string
+                                        * contains a hostname (NetworkOnMainThreadException) */
+                                       return null;
+                               }
+                       }
+               }
+               return set;
+       }
+
+       /**
+        * Add a range to this set. Automatically gets merged with existing ranges.
+        */
+       public void add(IPRange range)
+       {
+               if (mRanges.contains(range))
+               {
+                       return;
+               }
+               reinsert:
+               while (true)
+               {
+                       Iterator<IPRange> iterator = mRanges.iterator();
+                       while (iterator.hasNext())
+                       {
+                               IPRange existing = iterator.next();
+                               IPRange replacement = existing.merge(range);
+                               if (replacement != null)
+                               {
+                                       iterator.remove();
+                                       range = replacement;
+                                       continue reinsert;
+                               }
+                       }
+                       mRanges.add(range);
+                       break;
+               }
+       }
+
+       /**
+        * Add all ranges from the given set.
+        */
+       public void add(IPRangeSet ranges)
+       {
+               if (ranges == this)
+               {
+                       return;
+               }
+               for (IPRange range : ranges.mRanges)
+               {
+                       add(range);
+               }
+       }
+
+       /**
+        * Add all ranges from the given collection to this set.
+        */
+       public void addAll(Collection<? extends IPRange> coll)
+       {
+               for (IPRange range : coll)
+               {
+                       add(range);
+               }
+       }
+
+       /**
+        * Remove the given range from this set. Existing ranges are automatically adjusted.
+        */
+       public void remove(IPRange range)
+       {
+               ArrayList <IPRange> additions = new ArrayList<>();
+               Iterator<IPRange> iterator = mRanges.iterator();
+               while (iterator.hasNext())
+               {
+                       IPRange existing = iterator.next();
+                       List<IPRange> result = existing.remove(range);
+                       if (result.size() == 0)
+                       {
+                               iterator.remove();
+                       }
+                       else if (!result.get(0).equals(existing))
+                       {
+                               iterator.remove();
+                               additions.addAll(result);
+                       }
+               }
+               mRanges.addAll(additions);
+       }
+
+       /**
+        * Remove the given ranges from ranges in this set.
+        */
+       public void remove(IPRangeSet ranges)
+       {
+               if (ranges == this)
+               {
+                       mRanges.clear();
+                       return;
+               }
+               for (IPRange range : ranges.mRanges)
+               {
+                       remove(range);
+               }
+       }
+
+       /**
+        * Get all the subnets derived from all the ranges in this set.
+        */
+       public Iterable<IPRange> subnets()
+       {
+               return new Iterable<IPRange>()
+               {
+                       @Override
+                       public Iterator<IPRange> iterator()
+                       {
+                               return new Iterator<IPRange>()
+                               {
+                                       private Iterator<IPRange> mIterator = mRanges.iterator();
+                                       private List<IPRange> mSubnets;
+
+                                       @Override
+                                       public boolean hasNext()
+                                       {
+                                               return (mSubnets != null && mSubnets.size() > 0) || mIterator.hasNext();
+                                       }
+
+                                       @Override
+                                       public IPRange next()
+                                       {
+                                               if (mSubnets == null || mSubnets.size() == 0)
+                                               {
+                                                       IPRange range = mIterator.next();
+                                                       mSubnets = range.toSubnets();
+                                               }
+                                               return mSubnets.remove(0);
+                                       }
+
+                                       @Override
+                                       public void remove()
+                                       {
+                                               throw new UnsupportedOperationException();
+                                       }
+                               };
+                       }
+               };
+       }
+
+       @Override
+       public Iterator<IPRange> iterator()
+       {
+               return mRanges.iterator();
+       }
+
+       /**
+        * Returns the number of ranges, not subnets.
+        */
+       public int size()
+       {
+               return mRanges.size();
+       }
+
+       @Override
+       public String toString()
+       {       /* we could use TextUtils, but that causes the unit tests to fail */
+               StringBuilder sb = new StringBuilder();
+               for (IPRange range : mRanges)
+               {
+                       if (sb.length() > 0)
+                       {
+                               sb.append(" ");
+                       }
+                       sb.append(range.toString());
+               }
+               return sb.toString();
+       }
+}
index 01c0ab8..dd77490 100644 (file)
@@ -118,7 +118,7 @@ public class SettingsWriter
         */
        private String escapeValue(String value)
        {
-               return value.replace("\"", "\\\"");
+               return value.replace("\\", "\\\\").replace("\"", "\\\"");
        }
 
        /**
index 51a61b8..1cffa59 100644 (file)
@@ -6,7 +6,7 @@ include $(CLEAR_VARS)
 strongswan_USE_BYOD := true
 
 strongswan_CHARON_PLUGINS := android-log openssl fips-prf random nonce pubkey \
-       chapoly curve25519 pkcs1 pkcs8 pem xcbc hmac socket-default \
+       chapoly curve25519 pkcs1 pkcs8 pem xcbc hmac socket-default revocation \
        eap-identity eap-mschapv2 eap-md5 eap-gtc eap-tls
 
 ifneq ($(strongswan_USE_BYOD),)
index c37210d..49e313a 100644 (file)
@@ -6,6 +6,7 @@ LOCAL_SRC_FILES := \
 android_jni.c \
 backend/android_attr.c \
 backend/android_creds.c \
+backend/android_fetcher.c \
 backend/android_dns_proxy.c \
 backend/android_private_key.c \
 backend/android_service.c \
index fb973a8..ac108c3 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2012-2015 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
- * Hochschule fuer Technik Rapperswil
+ * HSR Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
@@ -44,7 +44,10 @@ static struct {
 
 jclass *android_charonvpnservice_class;
 jclass *android_charonvpnservice_builder_class;
+jclass *android_simple_fetcher_class;
 android_sdk_version_t android_sdk_version;
+char *android_version_string;
+char *android_device_string;
 
 /**
  * Thread-local variable. Only used because of the destructor
@@ -96,6 +99,8 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved)
        JNIEnv *env;
        jclass jversion;
        jfieldID jsdk_int;
+       jmethodID method_id;
+       jstring jstr;
        int i;
 
        android_jvm = vm;
@@ -122,11 +127,30 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved)
        android_charonvpnservice_builder_class =
                                (*env)->NewGlobalRef(env, (*env)->FindClass(env,
                                                JNI_PACKAGE_STRING "/CharonVpnService$BuilderAdapter"));
+       android_simple_fetcher_class =
+                               (*env)->NewGlobalRef(env, (*env)->FindClass(env,
+                                               JNI_PACKAGE_STRING "/SimpleFetcher"));
 
        jversion = (*env)->FindClass(env, "android/os/Build$VERSION");
        jsdk_int = (*env)->GetStaticFieldID(env, jversion, "SDK_INT", "I");
        android_sdk_version = (*env)->GetStaticIntField(env, jversion, jsdk_int);
 
+       method_id = (*env)->GetStaticMethodID(env, android_charonvpnservice_class,
+                                                                       "getAndroidVersion", "()Ljava/lang/String;");
+       jstr = (*env)->CallStaticObjectMethod(env,
+                                                                       android_charonvpnservice_class, method_id);
+       if (jstr)
+       {
+               android_version_string = androidjni_convert_jstring(env, jstr);
+       }
+       method_id = (*env)->GetStaticMethodID(env, android_charonvpnservice_class,
+                                                                       "getDeviceString", "()Ljava/lang/String;");
+       jstr = (*env)->CallStaticObjectMethod(env,
+                                                                       android_charonvpnservice_class, method_id);
+       if (jstr)
+       {
+               android_device_string = androidjni_convert_jstring(env, jstr);
+       }
        return JNI_VERSION_1_6;
 }
 
@@ -147,5 +171,6 @@ void JNI_OnUnload(JavaVM *vm, void *reserved)
                        dlclose(libs[i].handle);
                }
        }
+       free(android_version_string);
+       free(android_device_string);
 }
-
index b08670f..aab12c4 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
- * Hochschule fuer Technik Rapperswil
+ * HSR Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
@@ -44,6 +44,7 @@
  */
 extern jclass *android_charonvpnservice_class;
 extern jclass *android_charonvpnservice_builder_class;
+extern jclass *android_simple_fetcher_class;
 
 /**
  * Currently known (supported) SDK versions
@@ -66,6 +67,20 @@ typedef enum {
 extern android_sdk_version_t android_sdk_version;
 
 /**
+ * A description of the current Android release
+ *
+ * see android.os.Build
+ */
+extern char *android_version_string;
+
+/**
+ * A description of the current device
+ *
+ * see android.os.Build
+ */
+extern char *android_device_string;
+
+/**
  * Attach the current thread to the JVM
  *
  * As local JNI references are not freed until the thread detaches
diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_fetcher.c b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_fetcher.c
new file mode 100644 (file)
index 0000000..ce71410
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.  *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+#include "android_fetcher.h"
+
+#include "../android_jni.h"
+#include "../charonservice.h"
+#include <utils/debug.h>
+
+typedef struct android_fetcher_t android_fetcher_t;
+
+struct android_fetcher_t {
+
+       /**
+        * Public interface
+        */
+       fetcher_t public;
+
+       /**
+        * Callback function
+        */
+       fetcher_callback_t cb;
+};
+
+METHOD(fetcher_t, fetch, status_t,
+       android_fetcher_t *this, char *uri, void *userdata)
+{
+       JNIEnv *env;
+       jmethodID method_id;
+       jobjectArray jdata;
+       jstring juri;
+       chunk_t data;
+       status_t status = FAILED;
+
+       if (this->cb == fetcher_default_callback)
+       {
+               *(chunk_t*)userdata = chunk_empty;
+       }
+
+       androidjni_attach_thread(&env);
+       /* can't use FindClass here as this is not called by the main thread */
+       method_id = (*env)->GetStaticMethodID(env, android_simple_fetcher_class,
+                                                                                 "fetch", "(Ljava/lang/String;)[B");
+       if (!method_id)
+       {
+               goto failed;
+       }
+       juri = (*env)->NewStringUTF(env, uri);
+       if (!juri)
+       {
+               goto failed;
+       }
+       jdata = (*env)->CallStaticObjectMethod(env, android_simple_fetcher_class,
+                                                                                  method_id, juri);
+       if (!jdata || androidjni_exception_occurred(env))
+       {
+               goto failed;
+       }
+       data = chunk_from_byte_array(env, jdata);
+       if (this->cb(userdata, data))
+       {
+               status = SUCCESS;
+       }
+       chunk_free(&data);
+       androidjni_detach_thread();
+       return status;
+
+failed:
+       DBG1(DBG_LIB, "failed to fetch from '%s'", uri);
+       androidjni_exception_occurred(env);
+       androidjni_detach_thread();
+       return FAILED;
+}
+
+METHOD(fetcher_t, set_option, bool,
+       android_fetcher_t *this, fetcher_option_t option, ...)
+{
+       bool supported = TRUE;
+       va_list args;
+
+       va_start(args, option);
+       switch (option)
+       {
+               case FETCH_CALLBACK:
+               {
+                       this->cb = va_arg(args, fetcher_callback_t);
+                       break;
+               }
+               default:
+                       supported = FALSE;
+                       break;
+       }
+       va_end(args);
+       return supported;
+}
+
+METHOD(fetcher_t, destroy, void,
+       android_fetcher_t *this)
+{
+       free(this);
+}
+
+/*
+ * Described in header.
+ */
+fetcher_t *android_fetcher_create()
+{
+       android_fetcher_t *this;
+
+       INIT(this,
+               .public = {
+                       .fetch = _fetch,
+                       .set_option = _set_option,
+                       .destroy = _destroy,
+               },
+               .cb = fetcher_default_callback,
+       );
+
+       return &this->public;
+}
diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_fetcher.h b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_fetcher.h
new file mode 100644 (file)
index 0000000..a20cdfd
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+/**
+ * @defgroup android_fetcher android_fetcher
+ * @{ @ingroup android_backend
+ */
+
+#ifndef ANDROID_FETCHER_H_
+#define ANDROID_FETCHER_H_
+
+#include <library.h>
+
+/**
+ * Create an Android-specific fetcher instance based on SimpleFetcher
+ *
+ * @return                                             fetcher_t instance
+ */
+fetcher_t *android_fetcher_create();
+
+#endif /** ANDROID_FETCHER_H_ @}*/
index c41ee94..1e72cf5 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2012-2015 Tobias Brunner
+ * Copyright (C) 2012-2017 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
- * Hochschule fuer Technik Rapperswil
+ * HSR Hochschule fuer Technik Rapperswil
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the
@@ -25,6 +25,7 @@
 #include "android_jni.h"
 #include "backend/android_attr.h"
 #include "backend/android_creds.h"
+#include "backend/android_fetcher.h"
 #include "backend/android_private_key.h"
 #include "backend/android_service.h"
 #include "kernel/android_ipsec.h"
@@ -523,6 +524,9 @@ static void charonservice_init(JNIEnv *env, jobject service, jobject builder,
                PLUGIN_CALLBACK(charonservice_register, NULL),
                        PLUGIN_PROVIDE(CUSTOM, "android-backend"),
                                PLUGIN_DEPENDS(CUSTOM, "libcharon"),
+               PLUGIN_REGISTER(FETCHER, android_fetcher_create),
+                       PLUGIN_PROVIDE(FETCHER, "http://"),
+                       PLUGIN_PROVIDE(FETCHER, "https://"),
        };
 
        INIT(this,
@@ -640,7 +644,8 @@ JNI_METHOD(CharonVpnService, initializeCharon, jboolean,
        {
                memset(&utsname, 0, sizeof(utsname));
        }
-       DBG1(DBG_DMN, "Starting IKE charon daemon (strongSwan "VERSION", %s %s, %s)",
+       DBG1(DBG_DMN, "Starting IKE charon daemon (strongSwan "VERSION", %s, %s, "
+                "%s %s, %s)", android_version_string, android_device_string,
                  utsname.sysname, utsname.release, utsname.machine);
 
 #ifdef PLUGINS_BYOD
diff --git a/src/frontends/android/app/src/main/res/color/checkable_text_color.xml b/src/frontends/android/app/src/main/res/color/checkable_text_color.xml
new file mode 100644 (file)
index 0000000..e29e667
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 Tobias Brunner
+    HSR Hochschule fuer Technik Rapperswil
+
+    This program is free software; you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by the
+    Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+
+    This program is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="false" android:color="@android:color/secondary_text_dark" />
+    <item android:color="@android:color/primary_text_dark" />
+</selector>
index 04cf6ef..457df4f 100644 (file)
         android:drawable="@color/accent" />
 
     <item
+        android:state_checked="true"
+        android:drawable="@color/checked" />
+
+    <item
         android:drawable="@android:color/transparent" />
 
 </selector>
index 08881b3..8b153b8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2012-2016 Tobias Brunner
+    Copyright (C) 2012-2017 Tobias Brunner
     Copyright (C) 2012 Giuliano Grassi
     Copyright (C) 2012 Ralf Sager
     HSR Hochschule fuer Technik Rapperswil
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="10dp"
+                android:layout_marginBottom="10dp"
                 android:layout_marginLeft="4dp"
-                android:textSize="12sp"
+                android:layout_marginStart="4dp"
+                android:textSize="20sp"
                 android:text="@string/profile_split_tunneling_label" />
 
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="10dp"
+                android:layout_marginLeft="4dp"
+                android:textSize="12sp"
+                android:text="@string/profile_split_tunneling_intro" />
+
+            <org.strongswan.android.ui.widget.TextInputLayoutHelper
+                android:id="@+id/included_subnets_wrap"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:helper_text="@string/profile_included_subnets_hint" >
+
+                <android.support.design.widget.TextInputEditText
+                    android:id="@+id/included_subnets"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:singleLine="true"
+                    android:inputType="textNoSuggestions"
+                    android:hint="@string/profile_included_subnets_label" />
+
+            </org.strongswan.android.ui.widget.TextInputLayoutHelper>
+
+            <org.strongswan.android.ui.widget.TextInputLayoutHelper
+                android:id="@+id/excluded_subnets_wrap"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:helper_text="@string/profile_excluded_subnets_hint" >
+
+                <android.support.design.widget.TextInputEditText
+                    android:id="@+id/excluded_subnets"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:singleLine="true"
+                    android:inputType="textNoSuggestions"
+                    android:hint="@string/profile_excluded_subnets_label" />
+
+            </org.strongswan.android.ui.widget.TextInputLayoutHelper>
+
             <CheckBox
                 android:id="@+id/split_tunneling_v4"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:text="@string/profile_split_tunnelingv6_title" />
 
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="20dp"
+                android:layout_marginBottom="10dp"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textSize="20sp"
+                android:text="@string/profile_select_apps_label" />
+
+            <Spinner
+                android:id="@+id/apps_handling"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:spinnerMode="dropdown"
+                android:entries="@array/apps_handling" />
+
+            <include
+                android:id="@+id/select_applications"
+                layout="@layout/two_line_button" />
+
         </LinearLayout>
 
     </LinearLayout>
diff --git a/src/frontends/android/app/src/main/res/layout/selected_application_item.xml b/src/frontends/android/app/src/main/res/layout/selected_application_item.xml
new file mode 100644 (file)
index 0000000..faece83
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 Tobias Brunner
+    HSR Hochschule fuer Technik Rapperswil
+
+    This program is free software; you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by the
+    Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+
+    This program is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+-->
+<org.strongswan.android.ui.widget.CheckableLinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="8dp"
+    android:paddingRight="8dp"
+    android:minHeight="?android:listPreferredItemHeight"
+    android:background="@drawable/activated_background"
+    android:gravity="center_vertical" >
+
+    <ImageView android:id="@+id/app_icon"
+        android:duplicateParentState="true"
+        android:layout_width="@android:dimen/app_icon_size"
+        android:layout_height="@android:dimen/app_icon_size"
+        android:layout_marginRight="8dip"
+        android:layout_marginEnd="8dip"
+        android:scaleType="centerInside" />
+
+    <TextView android:id="@+id/app_name"
+        android:duplicateParentState="true"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textAppearance="?android:textAppearanceMedium"
+        android:textColor="@color/checkable_text_color" />
+
+    <ImageView android:src="?android:listChoiceIndicatorMultiple"
+        android:duplicateParentState="true"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="16dp"
+        android:layout_marginStart="16dp" />
+
+</org.strongswan.android.ui.widget.CheckableLinearLayout>
index 599254a..3c8da08 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2012-2014 Tobias Brunner
+    Copyright (C) 2012-2017 Tobias Brunner
     Hochschule fuer Technik Rapperswil
 
     This program is free software; you can redistribute it and/or modify it
       xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <item
+        android:id="@+id/menu_import_profile"
+        android:title="@string/profile_import"
+        app:showAsAction="withText" />
+
+    <item
         android:id="@+id/menu_manage_certs"
         android:title="@string/trusted_certs_title"
         app:showAsAction="withText" />
index d051401..1eda4f1 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2012-2013 Tobias Brunner
-    Hochschule fuer Technik Rapperswil
+    Copyright (C) 2012-2017 Tobias Brunner
+    HSR Hochschule fuer Technik Rapperswil
 
     This program is free software; you can redistribute it and/or modify it
     under the terms of the GNU General Public License as published by the
         <item>IKEv2 EAP-TLS (Zertifikat)</item>
         <item>IKEv2 EAP-TNC (Benutzername/Passwort)</item>
     </string-array>
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>Alle Apps verwenden das VPN</item>
+        <item>Ausgewählte Apps vom VPN ausschliessen</item>
+        <item>Nur ausgewählte Apps verwenden das VPN</item>
+    </string-array>
 </resources>
\ No newline at end of file
index 0bb73cb..5c5a22a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2012-2016 Tobias Brunner
+    Copyright (C) 2012-2017 Tobias Brunner
     Copyright (C) 2012 Giuliano Grassi
     Copyright (C) 2012 Ralf Sager
     HSR Hochschule fuer Technik Rapperswil
     <string name="profile_port_label">Server Port</string>
     <string name="profile_port_hint">UDP-Port zu dem verbunden wird, falls dieser vom Standard-Port abweicht</string>
     <string name="profile_split_tunneling_label">Split-Tunneling</string>
+    <string name="profile_split_tunneling_intro">Standardmässig leitet der Client allen Netzwerkverkehr durch den VPN Tunnel, ausser der Server schränkt die Subnetze beim Verbindungsaufbau ein, in welchem Fall nur der Verkehr via VPN geleitet wird, den der Server erlaubt (der Rest wird standardmässig behandelt, als ob kein VPN vorhanden wäre).</string>
     <string name="profile_split_tunnelingv4_title">Blockiere IPv4 Verkehr der nicht für das VPN bestimmt ist</string>
     <string name="profile_split_tunnelingv6_title">Blockiere IPv6 Verkehr der nicht für das VPN bestimmt ist</string>
+    <string name="profile_included_subnets_label">Benutzerdefinierte Subnetze</string>
+    <string name="profile_included_subnets_hint">Nur Verkehr in die spezifizierten Subnetze wird via VPN geleitet, der Rest wird behandelt, als ob kein VPN vorhanden wäre (mit Leerzeichen getrennt, z.B. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Ausgeschlossene Subnetze</string>
+    <string name="profile_excluded_subnets_hint">Verkehr in diese Subnetze wird vom VPN ausgeschlossen und behandelt, als ob kein VPN vorhanden wäre (mit Leerzeichen getrennt, z.B. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Apps</string>
+    <string name="profile_select_apps">Apps auswählen</string>
+    <string name="profile_select_no_apps">Keine Apps ausgewählt</string>
+    <string name="profile_select_one_app">Eine App ausgewählt</string>
+    <string name="profile_select_x_apps">%1$d Apps ausgewählt</string>
     <string name="profile_import">VPN Profile importieren</string>
     <string name="profile_import_failed">VPN Profil-Import fehlgeschlagen</string>
     <string name="profile_import_failed_detail">VPN Profil-Import fehlgeschlagen: %1$s</string>
     <string name="profile_import_failed_not_found">Datei nicht gefunden</string>
     <string name="profile_import_failed_host">Host unbekannt</string>
     <string name="profile_import_failed_tls">TLS-Handshake fehlgeschlagen</string>
+    <string name="profile_import_failed_value">Ungültiger Wert in \"%1$s\"</string>
     <string name="profile_import_exists">Dieses VPN Profil existiert bereits, die bestehenden Einstellungen werden ersetzt.</string>
     <string name="profile_cert_import">Zertifikat aus VPN Profil importieren</string>
     <string name="profile_cert_alias">Zertifikat für \"%1$s\"</string>
     <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>
     <string name="alert_text_out_of_range">Bitte geben Sie eine Nummer von %1$d - %2$d ein</string>
+    <string name="alert_text_no_subnets">Bitte geben Sie mit Leerzeichen getrennte, gültige Subnetzte und/oder IP-Adressen ein</string>
     <string name="tnc_notice_title">EAP-TNC kann Ihre Privatsphäre beeinträchtigen</string>
     <string name="tnc_notice_subtitle">Gerätedaten werden an den Server-Betreiber gesendet</string>
     <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) erlaubt Server-Betreibern den Gesundheitszustand von Endgeräten zu prüfen.</p><p>Dazu kann der Betreiber Daten verlangen, wie etwa eine eindeutige Identifikationsnummer, eine Liste der installierten Pakete, Systemeinstellungen oder kryptografische Prüfsummen von Dateien.</p><b>Solche Daten werden nur übermittelt nachdem die Identität des Servers geprüft wurde.</b>]]></string>
index 30e43f1..5df0e72 100644 (file)
         <item>IKEv2 EAP-TLS (certyfikat)</item>
         <item>IKEv2 EAP-TNC (użytkownik/hasło)</item>
     </string-array>
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>All applications use the VPN</item>
+        <item>Exclude selected applications from the VPN</item>
+        <item>Only selected applications use the VPN</item>
+    </string-array>
 </resources>
\ No newline at end of file
index 36cdff6..d8965a1 100644 (file)
     <string name="profile_port_label">Server port</string>
     <string name="profile_port_hint">UDP port to connect to, if different from the default</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
+    <string name="profile_split_tunneling_intro">By default, the client will route all network traffic through the VPN, unless the server narrows the subnets when the connection is established, in which case only traffic the server allows will be routed via VPN (by default, all other traffic is routed as if there was no VPN).</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_included_subnets_label">Custom subnets</string>
+    <string name="profile_included_subnets_hint">Only route traffic to specific subnets via VPN, everything else is routed as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Excluded subnets</string>
+    <string name="profile_excluded_subnets_hint">Traffic to these subnets will not be routed via VPN, but as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Applications</string>
+    <string name="profile_select_apps">Select applications</string>
+    <string name="profile_select_no_apps">No applications selected</string>
+    <string name="profile_select_one_app">One application selected</string>
+    <string name="profile_select_x_apps">%1$d applications selected</string>
     <string name="profile_import">Import VPN profile</string>
     <string name="profile_import_failed">Failed to import VPN profile</string>
     <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
     <string name="profile_import_failed_not_found">File not found</string>
     <string name="profile_import_failed_host">Host unknown</string>
     <string name="profile_import_failed_tls">TLS handshake failed</string>
+    <string name="profile_import_failed_value">Invalid value in \"%1$s\"</string>
     <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
     <string name="profile_cert_import">Import certificate from VPN profile</string>
     <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <string name="alert_text_nocertfound_title">Nie wybrano żadnego certyfikatu CA</string>
     <string name="alert_text_nocertfound">Wybierz lub uaktywnij jeden <i>Wybierz automatycznie</i></string>
     <string name="alert_text_out_of_range">Please enter a number in the range from %1$d - %2$d</string>
+    <string name="alert_text_no_subnets">Please enter valid subnets and/or IP addresses, separated by spaces</string>
     <string name="tnc_notice_title">EAP-TNC may affect your privacy</string>
     <string name="tnc_notice_subtitle">Device data is sent to the server operator</string>
     <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
index 5fbd431..64c234e 100644 (file)
         <item>IKEv2 EAP-TLS (Сертификат)</item>
         <item>IKEv2 EAP-TNC (Логин/Пароль)</item>
     </string-array>
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>All applications use the VPN</item>
+        <item>Exclude selected applications from the VPN</item>
+        <item>Only selected applications use the VPN</item>
+    </string-array>
 </resources>
index 6f336fd..7296554 100644 (file)
     <string name="profile_port_label">Server port</string>
     <string name="profile_port_hint">UDP port to connect to, if different from the default</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
+    <string name="profile_split_tunneling_intro">By default, the client will route all network traffic through the VPN, unless the server narrows the subnets when the connection is established, in which case only traffic the server allows will be routed via VPN (by default, all other traffic is routed as if there was no VPN).</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_included_subnets_label">Custom subnets</string>
+    <string name="profile_included_subnets_hint">Only route traffic to specific subnets via VPN, everything else is routed as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Excluded subnets</string>
+    <string name="profile_excluded_subnets_hint">Traffic to these subnets will not be routed via VPN, but as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Applications</string>
+    <string name="profile_select_apps">Select applications</string>
+    <string name="profile_select_no_apps">No applications selected</string>
+    <string name="profile_select_one_app">One application selected</string>
+    <string name="profile_select_x_apps">%1$d applications selected</string>
     <string name="profile_import">Import VPN profile</string>
     <string name="profile_import_failed">Failed to import VPN profile</string>
     <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
     <string name="profile_import_failed_not_found">File not found</string>
     <string name="profile_import_failed_host">Host unknown</string>
     <string name="profile_import_failed_tls">TLS handshake failed</string>
+    <string name="profile_import_failed_value">Invalid value in \"%1$s\"</string>
     <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
     <string name="profile_cert_import">Import certificate from VPN profile</string>
     <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <string name="alert_text_nocertfound_title">Не выбран сертификат CA</string>
     <string name="alert_text_nocertfound">Пожалуйста выберите один <i>Выбрать автоматически</i></string>
     <string name="alert_text_out_of_range">Please enter a number in the range from %1$d - %2$d</string>
+    <string name="alert_text_no_subnets">Please enter valid subnets and/or IP addresses, separated by spaces</string>
     <string name="tnc_notice_title">EAP-TNC may affect your privacy</string>
     <string name="tnc_notice_subtitle">Device data is sent to the server operator</string>
     <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
index 1acc0d7..8401ef9 100644 (file)
         <item>IKEv2 EAP-TLS (Сертифікати)</item>
         <item>IKEv2 EAP-TNC (Логін/Пароль)</item>
     </string-array>
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>All applications use the VPN</item>
+        <item>Exclude selected applications from the VPN</item>
+        <item>Only selected applications use the VPN</item>
+    </string-array>
 </resources>
index e8cdc53..6f30fcf 100644 (file)
     <string name="profile_port_label">Server port</string>
     <string name="profile_port_hint">UDP port to connect to, if different from the default</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
+    <string name="profile_split_tunneling_intro">By default, the client will route all network traffic through the VPN, unless the server narrows the subnets when the connection is established, in which case only traffic the server allows will be routed via VPN (by default, all other traffic is routed as if there was no VPN).</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_included_subnets_label">Custom subnets</string>
+    <string name="profile_included_subnets_hint">Only route traffic to specific subnets via VPN, everything else is routed as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Excluded subnets</string>
+    <string name="profile_excluded_subnets_hint">Traffic to these subnets will not be routed via VPN, but as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Applications</string>
+    <string name="profile_select_apps">Select applications</string>
+    <string name="profile_select_no_apps">No applications selected</string>
+    <string name="profile_select_one_app">One application selected</string>
+    <string name="profile_select_x_apps">%1$d applications selected</string>
     <string name="profile_import">Import VPN profile</string>
     <string name="profile_import_failed">Failed to import VPN profile</string>
     <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
     <string name="profile_import_failed_not_found">File not found</string>
     <string name="profile_import_failed_host">Host unknown</string>
     <string name="profile_import_failed_tls">TLS handshake failed</string>
+    <string name="profile_import_failed_value">Invalid value in \"%1$s\"</string>
     <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
     <string name="profile_cert_import">Import certificate from VPN profile</string>
     <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <string name="alert_text_nocertfound_title">Не вибрано сертифікат CA</string>
     <string name="alert_text_nocertfound">Будь ласка виберіть один <i>Вибрати автоматично</i></string>
     <string name="alert_text_out_of_range">Please enter a number in the range from %1$d - %2$d</string>
+    <string name="alert_text_no_subnets">Please enter valid subnets and/or IP addresses, separated by spaces</string>
     <string name="tnc_notice_title">EAP-TNC may affect your privacy</string>
     <string name="tnc_notice_subtitle">Device data is sent to the server operator</string>
     <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
diff --git a/src/frontends/android/app/src/main/res/values-zh-rCN/arrays.xml b/src/frontends/android/app/src/main/res/values-zh-rCN/arrays.xml
new file mode 100644 (file)
index 0000000..64bd21b
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 Yick Xie
+
+    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>
+    <!-- the order here must match the enum entries in VpnType.java -->
+    <string-array name="vpn_types">
+        <item>IKEv2 EAP (用户名/密码)</item>
+        <item>IKEv2 证书</item>
+        <item>IKEv2 证书 + EAP (用户名/密码)</item>
+        <item>IKEv2 EAP-TLS (证书)</item>
+        <item>IKEv2 EAP-TNC (用户名/密码)</item>
+    </string-array>
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>All applications use the VPN</item>
+        <item>Exclude selected applications from the VPN</item>
+        <item>Only selected applications use the VPN</item>
+    </string-array>
+</resources>
\ No newline at end of file
diff --git a/src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml b/src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644 (file)
index 0000000..415212e
--- /dev/null
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016-2017 Yick Xie
+
+    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 客户端</string>
+    <string name="main_activity_name">strongSwan</string>
+    <string name="show_log">浏览日志</string>
+    <string name="search">搜索</string>
+    <string name="vpn_not_supported_title">无法支持VPN</string>
+    <string name="vpn_not_supported">您的设备无法支持VPN应用。\n请联系供应商。</string>
+    <string name="vpn_not_supported_during_lockdown">锁定模式下无法支持VPN连接</string>
+    <string name="loading">载入中&#8230;</string>
+    <string name="profile_not_found">未找到配置</string>
+    <string name="strongswan_shortcut">strongSwan快捷方式</string>
+
+    <!-- Log view -->
+    <string name="log_title">日志</string>
+    <string name="send_log">发送日志文件</string>
+    <string name="empty_log">日志文件为空</string>
+    <string name="log_mail_subject">strongSwan %1$s 日志文件</string>
+
+    <!-- VPN profile list -->
+    <string name="no_profiles">无配置.</string>
+    <string name="add_profile">添加VPN配置</string>
+    <string name="edit_profile">编辑</string>
+    <string name="delete_profile">删除</string>
+    <string name="select_profiles">选择配置</string>
+    <string name="profiles_deleted">所选配置已删除</string>
+    <string name="no_profile_selected">未选择配置</string>
+    <string name="one_profile_selected">已选择1项配置</string>
+    <string name="x_profiles_selected">已选择%1$d项配置</string>
+
+    <!-- VPN profile details -->
+    <string name="profile_edit_save">保存</string>
+    <string name="profile_edit_import">导入</string>
+    <string name="profile_edit_cancel">取消</string>
+    <string name="profile_name_label">配置名称 (可选)</string>
+    <string name="profile_name_label_simple">配置名称</string>
+    <string name="profile_name_hint">默认为已配置服务器地址</string>
+    <string name="profile_name_hint_gateway">默认为 \"%1$s\"</string>
+    <string name="profile_gateway_label">服务器地址</string>
+    <string name="profile_gateway_hint">IP地址或服务器域名</string>
+    <string name="profile_vpn_type_label">VPN类型</string>
+    <string name="profile_username_label">用户名</string>
+    <string name="profile_password_label">密码 (可选)</string>
+    <string name="profile_password_hint">留空则在要求时弹出</string>
+    <string name="profile_user_certificate_label">用户证书</string>
+    <string name="profile_user_select_certificate_label">选择用户证书</string>
+    <string name="profile_user_select_certificate">选择指定的用户证书</string>
+    <string name="profile_user_select_id_label">用户ID</string>
+    <string name="profile_user_select_id_init">首先选择一个证书</string>
+    <string name="profile_user_select_id_default">默认(%1$s)</string>
+    <string name="profile_ca_label">CA证书</string>
+    <string name="profile_ca_auto_label">自动选择</string>
+    <string name="profile_ca_select_certificate_label">选择CA证书</string>
+    <string name="profile_ca_select_certificate">选择一个指定的CA证书</string>
+    <string name="profile_advanced_label">高级设置</string>
+    <string name="profile_show_advanced_label">显示高级设置</string>
+    <string name="profile_remote_id_label">服务器ID</string>
+    <string name="profile_remote_id_hint">默认为已配置的服务器地址。自义定值将在鉴权期间被显式地发送至服务器</string>
+    <string name="profile_remote_id_hint_gateway">默认为 \"%1$s\"。自义定值将在鉴权期间被显式地发送至服务器</string>
+    <string name="profile_mtu_label">VPN隧道设备的MTU值</string>
+    <string name="profile_mtu_hint">假如在某一网络下默认值不合适</string>
+    <string name="profile_port_label">服务器端口</string>
+    <string name="profile_port_hint">如不同于默认值,则所需连接的UDP端口</string>
+    <string name="profile_split_tunneling_label">拆分隧道</string>
+    <string name="profile_split_tunneling_intro">By default, the client will route all network traffic through the VPN, unless the server narrows the subnets when the connection is established, in which case only traffic the server allows will be routed via VPN (by default, all other traffic is routed as if there was no VPN).</string>
+    <string name="profile_split_tunnelingv4_title">屏蔽不通过VPN的IPV4流量</string>
+    <string name="profile_split_tunnelingv6_title">屏蔽不通过VPN的IPV6流量</string>
+    <string name="profile_included_subnets_label">Custom subnets</string>
+    <string name="profile_included_subnets_hint">Only route traffic to specific subnets via VPN, everything else is routed as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Excluded subnets</string>
+    <string name="profile_excluded_subnets_hint">Traffic to these subnets will not be routed via VPN, but as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Applications</string>
+    <string name="profile_select_apps">Select applications</string>
+    <string name="profile_select_no_apps">No applications selected</string>
+    <string name="profile_select_one_app">One application selected</string>
+    <string name="profile_select_x_apps">%1$d applications selected</string>
+    <string name="profile_import">导入VPN配置</string>
+    <string name="profile_import_failed">导入VPN配置失败</string>
+    <string name="profile_import_failed_detail">导入VPN配置失败: %1$s</string>
+    <string name="profile_import_failed_not_found">文件未找到</string>
+    <string name="profile_import_failed_host">未知主机</string>
+    <string name="profile_import_failed_tls">TLS握手失败</string>
+    <string name="profile_import_failed_value">Invalid value in \"%1$s\"</string>
+    <string name="profile_import_exists">此VPN配置已经存在,当前设定将被覆盖。</string>
+    <string name="profile_cert_import">从VPN配置导入证书</string>
+    <string name="profile_cert_alias">\"%1$s\" 所对应的证书</string>
+    <!-- Warnings/Notifications in the details view -->
+    <string name="alert_text_no_input_gateway">必填信息以初始化连接</string>
+    <string name="alert_text_no_input_username">请输入您的用户名</string>
+    <string name="alert_text_nocertfound_title">未选择CA证书</string>
+    <string name="alert_text_nocertfound">请选择一项或激活 <i>自动选择</i></string>
+    <string name="alert_text_out_of_range">请输入一个数字范围从%1$d到%2$d</string>
+    <string name="alert_text_no_subnets">Please enter valid subnets and/or IP addresses, separated by spaces</string>
+    <string name="tnc_notice_title">EAP-TNC可能会影响您的隐私</string>
+    <string name="tnc_notice_subtitle">设备数据已被发送至服务器管理员</string>
+    <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) 允许服务器管理员评定一个用户设备的状况。</p><p>出于此目的,服务器管理员可能要求以下数据如独立ID、已安装软件列表、系统设置、或加密过的文件校验值。</p><b>任何数据都仅将在验证过服务器的身份ID之后被发出。</b>]]></string>
+
+    <!-- Trusted certificate selection -->
+    <string name="trusted_certs_title">CA证书</string>
+    <string name="no_certificates">无证书</string>
+    <string name="reload_trusted_certs">重载CA证书</string>
+    <string name="system_tab">系统</string>
+    <string name="user_tab">用户</string>
+    <string name="local_tab">已导入</string>
+    <string name="delete_certificate_question">是否删除证书?</string>
+    <string name="delete_certificate">证书将被永久移除!</string>
+    <string name="import_certificate">导入证书</string>
+    <string name="cert_imported_successfully">证书已成功被导入</string>
+    <string name="cert_import_failed">证书导入失败</string>
+
+    <!-- VPN state fragment -->
+    <string name="state_label">状态:</string>
+    <string name="profile_label">配置:</string>
+    <string name="disconnect">断开链接</string>
+    <string name="state_connecting">连接中&#8230;</string>
+    <string name="state_connected">已连接</string>
+    <string name="state_disconnecting">断开连接中&#8230;</string>
+    <string name="state_disabled">无活跃VPN</string>
+    <string name="state_error">错误</string>
+
+    <!-- IMC state fragment -->
+    <string name="imc_state_label">评估详情:</string>
+    <string name="imc_state_isolate">受限的</string>
+    <string name="imc_state_block">失败的</string>
+    <string name="show_remediation_instructions">浏览修复指引</string>
+
+    <!-- Remediation instructions -->
+    <string name="remediation_instructions_title">修复指引</string>
+
+    <!-- Dialogs -->
+    <string name="login_title">输入密码用于连接</string>
+    <string name="login_username">用户名</string>
+    <string name="login_password">密码</string>
+    <string name="login_confirm">连接</string>
+    <string name="error_introduction">无法建立VPN:</string>
+    <string name="error_lookup_failed">服务器地址查找失败。</string>
+    <string name="error_unreachable">服务器地址无法连接。</string>
+    <string name="error_peer_auth_failed">核验服务器鉴权失败。</string>
+    <string name="error_auth_failed">用户鉴权失败。</string>
+    <string name="error_assessment_failed">可靠性评估失败。</string>
+    <string name="error_generic">连接中遭遇未知失败。</string>
+    <string name="vpn_connected">VPN已连接</string>
+    <string name="vpn_profile_connected">此VPN配置目前已连接。</string>
+    <string name="reconnect">重连</string>
+    <string name="connect_profile_question">是否连接%1$s?</string>
+    <string name="replaces_active_connection">这将覆盖您当前活跃的VPN连接!</string>
+    <string name="connect">连接</string>
+
+</resources>
diff --git a/src/frontends/android/app/src/main/res/values-zh-rTW/arrays.xml b/src/frontends/android/app/src/main/res/values-zh-rTW/arrays.xml
new file mode 100644 (file)
index 0000000..e4ceccc
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 Chris Chiang
+
+    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>
+    <!-- the order here must match the enum entries in VpnType.java -->
+    <string-array name="vpn_types">
+        <item>IKEv2 EAP (用戶名稱/密碼)</item>
+        <item>IKEv2 憑證</item>
+        <item>IKEv2 憑證 + EAP (用戶名稱/密碼)</item>
+        <item>IKEv2 EAP-TLS (憑證)</item>
+        <item>IKEv2 EAP-TNC (用戶名稱/密碼)</item>
+    </string-array>
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>All applications use the VPN</item>
+        <item>Exclude selected applications from the VPN</item>
+        <item>Only selected applications use the VPN</item>
+    </string-array>
+</resources>
\ No newline at end of file
diff --git a/src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml b/src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
new file mode 100644 (file)
index 0000000..e1cdf32
--- /dev/null
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 Chris Chiang
+
+    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 用戶端</string>
+    <string name="main_activity_name">strongSwan</string>
+    <string name="show_log">觀看日誌</string>
+    <string name="search">搜尋</string>
+    <string name="vpn_not_supported_title">無法支援VPN</string>
+    <string name="vpn_not_supported">您的設備無法使用VPN。\n請聯絡供應商。</string>
+    <string name="vpn_not_supported_during_lockdown">鎖定模式無法連線VPN</string>
+    <string name="loading">載入中&#8230;</string>
+    <string name="profile_not_found">沒有找到設定檔</string>
+    <string name="strongswan_shortcut">strongSwan快速選單</string>
+
+    <!-- Log view -->
+    <string name="log_title">日誌</string>
+    <string name="send_log">發送日誌檔</string>
+    <string name="empty_log">沒有日誌檔</string>
+    <string name="log_mail_subject">strongSwan %1$s 日誌檔</string>
+
+    <!-- VPN profile list -->
+    <string name="no_profiles">沒有設定檔</string>
+    <string name="add_profile">新增VPN設定檔</string>
+    <string name="edit_profile">編輯</string>
+    <string name="delete_profile">刪除</string>
+    <string name="select_profiles">選擇設定檔</string>
+    <string name="profiles_deleted">選擇的設定檔已經刪除</string>
+    <string name="no_profile_selected">沒有選擇設定檔</string>
+    <string name="one_profile_selected">已選擇1個設定檔</string>
+    <string name="x_profiles_selected">已選擇%1$d個設定檔</string>
+
+    <!-- VPN profile details -->
+    <string name="profile_edit_save">儲存</string>
+    <string name="profile_edit_import">匯入</string>
+    <string name="profile_edit_cancel">取消</string>
+    <string name="profile_name_label">設定檔名稱(選填)</string>
+    <string name="profile_name_label_simple">設定檔名稱</string>
+    <string name="profile_name_hint">預設為已設定的伺服器位置</string>
+    <string name="profile_name_hint_gateway">預設為 \"%1$s\"</string>
+    <string name="profile_gateway_label">伺服器位置</string>
+    <string name="profile_gateway_hint">IP位置或伺服器網域</string>
+    <string name="profile_vpn_type_label">VPN類型</string>
+    <string name="profile_username_label">用戶ID</string>
+    <string name="profile_password_label">密碼(選填)</string>
+    <string name="profile_password_hint">可留空白在需要時才請您設定</string>
+    <string name="profile_user_certificate_label">用戶憑證</string>
+    <string name="profile_user_select_certificate_label">選擇用戶憑證</string>
+    <string name="profile_user_select_certificate">選擇指定的用戶憑證</string>
+    <string name="profile_user_select_id_label">用戶帳號</string>
+    <string name="profile_user_select_id_init">請先選擇一個憑證</string>
+    <string name="profile_user_select_id_default">預設(%1$s)</string>
+    <string name="profile_ca_label">CA憑證</string>
+    <string name="profile_ca_auto_label">自動選擇</string>
+    <string name="profile_ca_select_certificate_label">選擇CA憑證</string>
+    <string name="profile_ca_select_certificate">選擇一個指定的CA憑證</string>
+    <string name="profile_advanced_label">進階設定</string>
+    <string name="profile_show_advanced_label">顯示進階設定</string>
+    <string name="profile_remote_id_label">伺服器ID</string>
+    <string name="profile_remote_id_hint">預設為已設定的伺服器位置。自訂值會在授權期間送到伺服器</string>
+    <string name="profile_remote_id_hint_gateway">預設為 \"%1$s\"。自訂值會在授權期間送到伺服器</string>
+    <string name="profile_mtu_label">VPN通道裝置的MTU值</string>
+    <string name="profile_mtu_hint">如果在某個網路下預設值不適合</string>
+    <string name="profile_port_label">伺服器Port</string>
+    <string name="profile_port_hint">如果和預設值不同,則需要連接的UDP Port</string>
+    <string name="profile_split_tunneling_label">拆分隧道</string>
+    <string name="profile_split_tunneling_intro">By default, the client will route all network traffic through the VPN, unless the server narrows the subnets when the connection is established, in which case only traffic the server allows will be routed via VPN (by default, all other traffic is routed as if there was no VPN).</string>
+    <string name="profile_split_tunnelingv4_title">屏蔽不通过VPN的IPV4流量</string>
+    <string name="profile_split_tunnelingv6_title">屏蔽不通过VPN的IPV6流量</string>
+    <string name="profile_included_subnets_label">Custom subnets</string>
+    <string name="profile_included_subnets_hint">Only route traffic to specific subnets via VPN, everything else is routed as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Excluded subnets</string>
+    <string name="profile_excluded_subnets_hint">Traffic to these subnets will not be routed via VPN, but as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Applications</string>
+    <string name="profile_select_apps">Select applications</string>
+    <string name="profile_select_no_apps">No applications selected</string>
+    <string name="profile_select_one_app">One application selected</string>
+    <string name="profile_select_x_apps">%1$d applications selected</string>
+    <string name="profile_import">匯入VPN設定檔</string>
+    <string name="profile_import_failed">匯入VPN設定檔失敗</string>
+    <string name="profile_import_failed_detail">匯入VPN設定檔失敗: %1$s</string>
+    <string name="profile_import_failed_not_found">沒有找到檔案</string>
+    <string name="profile_import_failed_host">不明主機</string>
+    <string name="profile_import_failed_tls">TLS連線失敗</string>
+    <string name="profile_import_failed_value">Invalid value in \"%1$s\"</string>
+    <string name="profile_import_exists">這個VPN設定檔已經存在,當前設定檔會被覆蓋。</string>
+    <string name="profile_cert_import">從VPN設定檔匯入憑證</string>
+    <string name="profile_cert_alias">\"%1$s\" 對應的憑證</string>
+    <!-- Warnings/Notifications in the details view -->
+    <string name="alert_text_no_input_gateway">請填寫必要訊息才能初始化連線</string>
+    <string name="alert_text_no_input_username">請輸入您的用戶名稱</string>
+    <string name="alert_text_nocertfound_title">沒有選擇CA憑證</string>
+    <string name="alert_text_nocertfound">請選擇一項或啟動 <i>自動選擇</i></string>
+    <string name="alert_text_out_of_range">請輸入一個數字範圍從%1$d到%2$d</string>
+    <string name="alert_text_no_subnets">Please enter valid subnets and/or IP addresses, separated by spaces</string>
+    <string name="tnc_notice_title">EAP-TNC可能會影響您的隱私安全</string>
+    <string name="tnc_notice_subtitle">裝置資料已經發送給伺服器管理者</string>
+    <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) 可以讓伺服器管理者評估用戶裝置的狀況。</p><p>在這個目的下,伺服器管理者可能會要求以下資料,例如ID、已安裝的App項目、系統設定、或加密檔案驗證值。</p><b>任何資料都只有在驗證伺服器的身分ID之後才會被送出。</b>]]></string>
+
+    <!-- Trusted certificate selection -->
+    <string name="trusted_certs_title">CA憑證</string>
+    <string name="no_certificates">沒有憑證</string>
+    <string name="reload_trusted_certs">重新載入CA憑證</string>
+    <string name="system_tab">系統</string>
+    <string name="user_tab">用戶</string>
+    <string name="local_tab">已經匯入</string>
+    <string name="delete_certificate_question">是否刪除憑證?</string>
+    <string name="delete_certificate">憑證將被永久移除!</string>
+    <string name="import_certificate">導入憑證</string>
+    <string name="cert_imported_successfully">憑證已經成功匯入</string>
+    <string name="cert_import_failed">憑證匯入失敗</string>
+
+    <!-- VPN state fragment -->
+    <string name="state_label">狀態:</string>
+    <string name="profile_label">設定檔:</string>
+    <string name="disconnect">結束連線</string>
+    <string name="state_connecting">連線中&#8230;</string>
+    <string name="state_connected">已連線</string>
+    <string name="state_disconnecting">結束連線中&#8230;</string>
+    <string name="state_disabled">無運作中的VPN</string>
+    <string name="state_error">錯誤</string>
+
+    <!-- IMC state fragment -->
+    <string name="imc_state_label">評估詳情:</string>
+    <string name="imc_state_isolate">受限的</string>
+    <string name="imc_state_block">失敗的</string>
+    <string name="show_remediation_instructions">觀看修復說明</string>
+
+    <!-- Remediation instructions -->
+    <string name="remediation_instructions_title">修復說明</string>
+
+    <!-- Dialogs -->
+    <string name="login_title">輸入密碼進行連線</string>
+    <string name="login_username">用戶名稱</string>
+    <string name="login_password">密碼</string>
+    <string name="login_confirm">連線</string>
+    <string name="error_introduction">無法建立VPN:</string>
+    <string name="error_lookup_failed">伺服器位置查詢失敗。</string>
+    <string name="error_unreachable">伺服器位置無法連線。</string>
+    <string name="error_peer_auth_failed">驗證伺服器授權失敗。</string>
+    <string name="error_auth_failed">用戶授權失敗。</string>
+    <string name="error_assessment_failed">穩定性評估失敗。</string>
+    <string name="error_generic">連線中遇到不明錯誤。</string>
+    <string name="vpn_connected">VPN已連線</string>
+    <string name="vpn_profile_connected">這個VPN設定檔目前已經連線。</string>
+    <string name="reconnect">重新連線</string>
+    <string name="connect_profile_question">是否連線%1$s?</string>
+    <string name="replaces_active_connection">這將會覆蓋您當前運作的VPN連線!</string>
+    <string name="connect">連線</string>
+
+</resources>
diff --git a/src/frontends/android/app/src/main/res/values-zh/arrays.xml b/src/frontends/android/app/src/main/res/values-zh/arrays.xml
deleted file mode 100644 (file)
index 546000a..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    Copyright (C) 2016 Yick Xie
-
-    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>
-    <!-- the order here must match the enum entries in VpnType.java -->
-    <string-array name="vpn_types">
-        <item>IKEv2 EAP (用户名/密码)</item>
-        <item>IKEv2 证书</item>
-        <item>IKEv2 证书 + EAP (用户名/密码)</item>
-        <item>IKEv2 EAP-TLS (证书)</item>
-        <item>IKEv2 EAP-TNC (用户名/密码)</item>
-    </string-array>
-</resources>
\ No newline at end of file
diff --git a/src/frontends/android/app/src/main/res/values-zh/strings.xml b/src/frontends/android/app/src/main/res/values-zh/strings.xml
deleted file mode 100644 (file)
index 591aea4..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    Copyright (C) 2016-2017 Yick Xie
-
-    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 客户端</string>
-    <string name="main_activity_name">strongSwan</string>
-    <string name="show_log">浏览日志</string>
-    <string name="search">搜索</string>
-    <string name="vpn_not_supported_title">无法支持VPN</string>
-    <string name="vpn_not_supported">您的设备无法支持VPN应用。\n请联系供应商。</string>
-    <string name="vpn_not_supported_during_lockdown">锁定模式下无法支持VPN连接</string>
-    <string name="loading">载入中&#8230;</string>
-    <string name="profile_not_found">未找到配置</string>
-    <string name="strongswan_shortcut">strongSwan快捷方式</string>
-
-    <!-- Log view -->
-    <string name="log_title">日志</string>
-    <string name="send_log">发送日志文件</string>
-    <string name="empty_log">日志文件为空</string>
-    <string name="log_mail_subject">strongSwan %1$s 日志文件</string>
-
-    <!-- VPN profile list -->
-    <string name="no_profiles">无配置.</string>
-    <string name="add_profile">添加VPN配置</string>
-    <string name="edit_profile">编辑</string>
-    <string name="delete_profile">删除</string>
-    <string name="select_profiles">选择配置</string>
-    <string name="profiles_deleted">所选配置已删除</string>
-    <string name="no_profile_selected">未选择配置</string>
-    <string name="one_profile_selected">已选择1项配置</string>
-    <string name="x_profiles_selected">已选择%1$d项配置</string>
-
-    <!-- VPN profile details -->
-    <string name="profile_edit_save">保存</string>
-    <string name="profile_edit_import">导入</string>
-    <string name="profile_edit_cancel">取消</string>
-    <string name="profile_name_label">配置名称 (可选)</string>
-    <string name="profile_name_label_simple">配置名称</string>
-    <string name="profile_name_hint">默认为已配置服务器地址</string>
-    <string name="profile_name_hint_gateway">默认为 \"%1$s\"</string>
-    <string name="profile_gateway_label">服务器地址</string>
-    <string name="profile_gateway_hint">IP地址或服务器域名</string>
-    <string name="profile_vpn_type_label">VPN类型</string>
-    <string name="profile_username_label">用户名</string>
-    <string name="profile_password_label">密码 (可选)</string>
-    <string name="profile_password_hint">留空则在要求时弹出</string>
-    <string name="profile_user_certificate_label">用户证书</string>
-    <string name="profile_user_select_certificate_label">选择用户证书</string>
-    <string name="profile_user_select_certificate">选择指定的用户证书</string>
-    <string name="profile_user_select_id_label">用户ID</string>
-    <string name="profile_user_select_id_init">首先选择一个证书</string>
-    <string name="profile_user_select_id_default">默认(%1$s)</string>
-    <string name="profile_ca_label">CA证书</string>
-    <string name="profile_ca_auto_label">自动选择</string>
-    <string name="profile_ca_select_certificate_label">选择CA证书</string>
-    <string name="profile_ca_select_certificate">选择一个指定的CA证书</string>
-    <string name="profile_advanced_label">高级设置</string>
-    <string name="profile_show_advanced_label">显示高级设置</string>
-    <string name="profile_remote_id_label">服务器ID</string>
-    <string name="profile_remote_id_hint">默认为已配置的服务器地址。自义定值将在鉴权期间被显式地发送至服务器</string>
-    <string name="profile_remote_id_hint_gateway">默认为 \"%1$s\"。自义定值将在鉴权期间被显式地发送至服务器</string>
-    <string name="profile_mtu_label">VPN隧道设备的MTU值</string>
-    <string name="profile_mtu_hint">假如在某一网络下默认值不合适</string>
-    <string name="profile_port_label">服务器端口</string>
-    <string name="profile_port_hint">如不同于默认值,则所需连接的UDP端口</string>
-    <string name="profile_split_tunneling_label">拆分隧道</string>
-    <string name="profile_split_tunnelingv4_title">屏蔽不通过VPN的IPV4流量</string>
-    <string name="profile_split_tunnelingv6_title">屏蔽不通过VPN的IPV6流量</string>
-    <string name="profile_import">导入VPN配置</string>
-    <string name="profile_import_failed">导入VPN配置失败</string>
-    <string name="profile_import_failed_detail">导入VPN配置失败: %1$s</string>
-    <string name="profile_import_failed_not_found">文件未找到</string>
-    <string name="profile_import_failed_host">未知主机</string>
-    <string name="profile_import_failed_tls">TLS握手失败</string>
-    <string name="profile_import_exists">此VPN配置已经存在,当前设定将被覆盖。</string>
-    <string name="profile_cert_import">从VPN配置导入证书</string>
-    <string name="profile_cert_alias">\"%1$s\" 所对应的证书</string>
-    <!-- Warnings/Notifications in the details view -->
-    <string name="alert_text_no_input_gateway">必填信息以初始化连接</string>
-    <string name="alert_text_no_input_username">请输入您的用户名</string>
-    <string name="alert_text_nocertfound_title">未选择CA证书</string>
-    <string name="alert_text_nocertfound">请选择一项或激活 <i>自动选择</i></string>
-    <string name="alert_text_out_of_range">请输入一个数字范围从%1$d到%2$d</string>
-    <string name="tnc_notice_title">EAP-TNC可能会影响您的隐私</string>
-    <string name="tnc_notice_subtitle">设备数据已被发送至服务器管理员</string>
-    <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) 允许服务器管理员评定一个用户设备的状况。</p><p>出于此目的,服务器管理员可能要求以下数据如独立ID、已安装软件列表、系统设置、或加密过的文件校验值。</p><b>任何数据都仅将在验证过服务器的身份ID之后被发出。</b>]]></string>
-
-    <!-- Trusted certificate selection -->
-    <string name="trusted_certs_title">CA证书</string>
-    <string name="no_certificates">无证书</string>
-    <string name="reload_trusted_certs">重载CA证书</string>
-    <string name="system_tab">系统</string>
-    <string name="user_tab">用户</string>
-    <string name="local_tab">已导入</string>
-    <string name="delete_certificate_question">是否删除证书?</string>
-    <string name="delete_certificate">证书将被永久移除!</string>
-    <string name="import_certificate">导入证书</string>
-    <string name="cert_imported_successfully">证书已成功被导入</string>
-    <string name="cert_import_failed">证书导入失败</string>
-
-    <!-- VPN state fragment -->
-    <string name="state_label">状态:</string>
-    <string name="profile_label">配置:</string>
-    <string name="disconnect">断开链接</string>
-    <string name="state_connecting">连接中&#8230;</string>
-    <string name="state_connected">已连接</string>
-    <string name="state_disconnecting">断开连接中&#8230;</string>
-    <string name="state_disabled">无活跃VPN</string>
-    <string name="state_error">错误</string>
-
-    <!-- IMC state fragment -->
-    <string name="imc_state_label">评估详情:</string>
-    <string name="imc_state_isolate">受限的</string>
-    <string name="imc_state_block">失败的</string>
-    <string name="show_remediation_instructions">浏览修复指引</string>
-
-    <!-- Remediation instructions -->
-    <string name="remediation_instructions_title">修复指引</string>
-
-    <!-- Dialogs -->
-    <string name="login_title">输入密码用于连接</string>
-    <string name="login_username">用户名</string>
-    <string name="login_password">密码</string>
-    <string name="login_confirm">连接</string>
-    <string name="error_introduction">无法建立VPN:</string>
-    <string name="error_lookup_failed">服务器地址查找失败。</string>
-    <string name="error_unreachable">服务器地址无法连接。</string>
-    <string name="error_peer_auth_failed">核验服务器鉴权失败。</string>
-    <string name="error_auth_failed">用户鉴权失败。</string>
-    <string name="error_assessment_failed">可靠性评估失败。</string>
-    <string name="error_generic">连接中遭遇未知失败。</string>
-    <string name="vpn_connected">VPN已连接</string>
-    <string name="vpn_profile_connected">此VPN配置目前已连接。</string>
-    <string name="reconnect">重连</string>
-    <string name="connect_profile_question">是否连接%1$s?</string>
-    <string name="replaces_active_connection">这将覆盖您当前活跃的VPN连接!</string>
-    <string name="connect">连接</string>
-
-</resources>
index b324b59..be12aab 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2012-2014 Tobias Brunner
-    Hochschule fuer Technik Rapperswil
+    Copyright (C) 2012-2017 Tobias Brunner
+    HSR Hochschule fuer Technik Rapperswil
 
     This program is free software; you can redistribute it and/or modify it
     under the terms of the GNU General Public License as published by the
         <item>IKEv2 EAP-TLS (Certificate)</item>
         <item>IKEv2 EAP-TNC (Username/Password)</item>
     </string-array>
-</resources>
\ No newline at end of file
+
+    <!-- the order here must match the enum entries in VpnProfile.java -->
+    <string-array name="apps_handling">
+        <item>All applications use the VPN</item>
+        <item>Exclude selected applications from the VPN</item>
+        <item>Only selected applications use the VPN</item>
+    </string-array>
+</resources>
index f29ae19..eedfe23 100644 (file)
@@ -40,6 +40,9 @@
         name="panel_separator">#5a5a5a</color>
 
     <color
+        name="checked">#4a4a4a</color>
+
+    <color
         name="pressed">#5a5a5a</color>
 
 </resources>
index 3577d97..fa38753 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright (C) 2012-2016 Tobias Brunner
+    Copyright (C) 2012-2017 Tobias Brunner
     Copyright (C) 2012 Giuliano Grassi
     Copyright (C) 2012 Ralf Sager
     HSR Hochschule fuer Technik Rapperswil
     <string name="profile_port_label">Server port</string>
     <string name="profile_port_hint">UDP port to connect to, if different from the default</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
+    <string name="profile_split_tunneling_intro">By default, the client will route all network traffic through the VPN, unless the server narrows the subnets when the connection is established, in which case only traffic the server allows will be routed via VPN (by default, all other traffic is routed as if there was no VPN).</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_included_subnets_label">Custom subnets</string>
+    <string name="profile_included_subnets_hint">Only route traffic to specific subnets via VPN, everything else is routed as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_excluded_subnets_label">Excluded subnets</string>
+    <string name="profile_excluded_subnets_hint">Traffic to these subnets will not be routed via VPN, but as if there was no VPN (separated by spaces, e.g. \"192.168.1.0/24 2001:db8::/64\")</string>
+    <string name="profile_select_apps_label">Applications</string>
+    <string name="profile_select_apps">Select applications</string>
+    <string name="profile_select_no_apps">No applications selected</string>
+    <string name="profile_select_one_app">One application selected</string>
+    <string name="profile_select_x_apps">%1$d applications selected</string>
     <string name="profile_import">Import VPN profile</string>
     <string name="profile_import_failed">Failed to import VPN profile</string>
     <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string>
     <string name="profile_import_failed_not_found">File not found</string>
     <string name="profile_import_failed_host">Host unknown</string>
     <string name="profile_import_failed_tls">TLS handshake failed</string>
+    <string name="profile_import_failed_value">Invalid value in \"%1$s\"</string>
     <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
     <string name="profile_cert_import">Import certificate from VPN profile</string>
     <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <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>
     <string name="alert_text_out_of_range">Please enter a number in the range from %1$d - %2$d</string>
+    <string name="alert_text_no_subnets">Please enter valid subnets and/or IP addresses, separated by spaces</string>
     <string name="tnc_notice_title">EAP-TNC may affect your privacy</string>
     <string name="tnc_notice_subtitle">Device data is sent to the server operator</string>
     <string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
diff --git a/src/frontends/android/app/src/test/java/org/strongswan/android/test/IPRangeSetTest.java b/src/frontends/android/app/src/test/java/org/strongswan/android/test/IPRangeSetTest.java
new file mode 100644 (file)
index 0000000..af60c15
--- /dev/null
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.test;
+
+import org.junit.Test;
+import org.strongswan.android.utils.IPRange;
+import org.strongswan.android.utils.IPRangeSet;
+
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class IPRangeSetTest
+{
+       private void assertSubnets(IPRangeSet set, IPRange...exp)
+       {
+               Iterator<IPRange> subnets = set.subnets().iterator();
+               if (exp.length == 0)
+               {
+                       assertEquals("no subnets", false, subnets.hasNext());
+                       return;
+               }
+               for (IPRange e : exp)
+               {
+                       assertEquals("has subnet", true, subnets.hasNext());
+                       assertEquals("range", e, subnets.next());
+               }
+               assertEquals("done", false, subnets.hasNext());
+       }
+
+       @Test
+       public void testEmpty()
+       {
+               IPRangeSet set = new IPRangeSet();
+               assertEquals("size", 0, set.size());
+               assertSubnets(set);
+       }
+
+       @Test
+       public void testAddDistinct() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("10.0.1.0/24"));
+               assertEquals("size", 2, set.size());
+               assertSubnets(set, new IPRange("10.0.1.0/24"), new IPRange("192.168.1.0/24"));
+       }
+
+       @Test
+       public void testAddAdjacent() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("192.168.2.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"), new IPRange("192.168.2.0/24"));
+       }
+
+       @Test
+       public void testAddAdjacentJoin() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("192.168.3.0/24"));
+               assertEquals("size", 2, set.size());
+               set.add(new IPRange("192.168.2.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"), new IPRange("192.168.2.0/23"));
+       }
+
+       @Test
+       public void testAddSame() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("192.168.1.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+       }
+
+       @Test
+       public void testAddLarger() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("192.168.0.0/16"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.0.0/16"));
+               set.add(new IPRange("0.0.0.0/0"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("0.0.0.0/0"));
+       }
+
+       @Test
+       public void testAddLargerMulti() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("10.0.1.0/24"));
+               set.add(new IPRange("255.255.255.255/32"));
+               assertEquals("size", 3, set.size());
+               set.add(new IPRange("0.0.0.0/0"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("0.0.0.0/0"));
+       }
+
+       @Test
+       public void testAddAll() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               List<IPRange> list = new ArrayList<>();
+               list.add(new IPRange("192.168.1.0/24"));
+               list.add(new IPRange("10.0.1.0/24"));
+               list.add(new IPRange("255.255.255.255/32"));
+               set.addAll(list);
+               assertEquals("size", 3, set.size());
+               assertSubnets(set, new IPRange("10.0.1.0/24"), new IPRange("192.168.1.0/24"),
+                                         new IPRange("255.255.255.255/32"));
+       }
+
+       @Test
+       public void testAddSet() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               IPRangeSet other = new IPRangeSet();
+               other.add(new IPRange("192.168.1.0/24"));
+               other.add(new IPRange("10.0.1.0/24"));
+               other.add(new IPRange("255.255.255.255/32"));
+               set.add(other);
+               assertEquals("size", 3, set.size());
+               assertSubnets(set, new IPRange("10.0.1.0/24"), new IPRange("192.168.1.0/24"),
+                                         new IPRange("255.255.255.255/32"));
+       }
+
+       @Test
+       public void testAddSetIdent() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("10.0.1.0/24"));
+               set.add(new IPRange("255.255.255.255/32"));
+               set.add(set);
+               assertEquals("size", 3, set.size());
+               assertSubnets(set, new IPRange("10.0.1.0/24"), new IPRange("192.168.1.0/24"),
+                                         new IPRange("255.255.255.255/32"));
+       }
+
+       @Test
+       public void testRemoveNothing() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               set.remove(new IPRange("192.168.2.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+               set.remove(new IPRange("10.0.1.0/24"));
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+       }
+
+       @Test
+       public void testRemoveAll() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               set.remove(new IPRange("192.168.0.0/16"));
+               assertEquals("size", 0, set.size());
+               assertSubnets(set);
+               set.add(new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("10.0.1.0/24"));
+               set.add(new IPRange("255.255.255.255/32"));
+               assertEquals("size", 3, set.size());
+               set.remove(new IPRange("0.0.0.0/0"));
+               assertEquals("size", 0, set.size());
+               assertSubnets(set);
+       }
+
+       @Test
+       public void testRemoveOverlap() throws UnknownHostException
+       {
+               IPRangeSet set = new IPRangeSet();
+               set.add(new IPRange("192.168.1.0/24"));
+               set.add(new IPRange("192.168.2.0/24"));
+               set.remove(new IPRange("192.168.1.128", "192.168.2.127"));
+               assertEquals("size", 2, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/25"), new IPRange("192.168.2.128/25"));
+       }
+
+       @Test
+       public void testRemoveRangesIdent() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24 192.168.4.0/24");
+               set.remove(set);
+               assertEquals("size", 0, set.size());
+               assertSubnets(set);
+       }
+
+       @Test
+       public void testRemoveRanges() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.0.0/16");
+               IPRangeSet remove = IPRangeSet.fromString("192.168.1.0/24 192.168.3.0/24 192.168.16.0-192.168.255.255");
+               set.remove(remove);
+               assertEquals("size", 3, set.size());
+               assertSubnets(set, new IPRange("192.168.0.0/24"), new IPRange("192.168.2.0/24"),
+                                         new IPRange("192.168.4.0/22"), new IPRange("192.168.8.0/21"));
+       }
+
+       @Test
+       public void testFromStringSingle() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24");
+               assertEquals("size", 1, set.size());
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+       }
+
+       @Test
+       public void testFromString() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24 fec1::/64        10.0.1.0/24 255.255.255.255");
+               assertEquals("size", 4, set.size());
+               assertSubnets(set, new IPRange("10.0.1.0/24"), new IPRange("192.168.1.0/24"),
+                                         new IPRange("255.255.255.255/32"), new IPRange("fec1::/64"));
+       }
+
+       @Test
+       public void testFromStringRange() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24 10.0.1.0-10.0.1.16");
+               assertEquals("size", 2, set.size());
+               assertSubnets(set, new IPRange("10.0.1.0/28"), new IPRange("10.0.1.16/32"),
+                                         new IPRange("192.168.1.0/24"));
+       }
+
+       @Test
+       public void testFromStringInvalidPrefix() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/65");
+               assertEquals("failed", null, set);
+       }
+
+       @Test
+       public void testFromStringInvalidRange() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.1 - 192.168.1.10");
+               assertEquals("failed", null, set);
+       }
+
+       @Test
+       public void testIteratorRanges() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24 192.168.2.0/24");
+               assertSubnets(set, new IPRange("192.168.1.0/24"), new IPRange("192.168.2.0/24"));
+               Iterator<IPRange> iterator = set.iterator();
+               assertEquals("hasNext", true, iterator.hasNext());
+               assertEquals("next", new IPRange("192.168.1.0-192.168.2.255"), iterator.next());
+               assertEquals("hasNext", false, iterator.hasNext());
+       }
+
+       @Test
+       public void testIteratorRemove() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24");
+               assertSubnets(set, new IPRange("192.168.1.0/24"));
+               Iterator<IPRange> iterator = set.iterator();
+               assertEquals("next", new IPRange("192.168.1.0/24"), iterator.next());
+               iterator.remove();
+               assertEquals("hasNext", false, iterator.hasNext());
+               assertEquals("size", 0, set.size());
+       }
+
+       @Test(expected = UnsupportedOperationException.class)
+       public void testIteratorSubnetRemove() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.1.0/24");
+               Iterator<IPRange> iterator = set.subnets().iterator();
+               assertEquals("hasNext", true, iterator.hasNext());
+               assertEquals("next", new IPRange("192.168.1.0/24"), iterator.next());
+               iterator.remove();
+       }
+
+       @Test
+       public void testToString() throws UnknownHostException
+       {
+               IPRangeSet set = IPRangeSet.fromString("192.168.3.10/24 192.168.1.0/24 192.168.1.1-192.168.1.10");
+               assertEquals("string", "192.168.1.0/24 192.168.3.0/24", set.toString());
+       }
+}
diff --git a/src/frontends/android/app/src/test/java/org/strongswan/android/test/IPRangeTest.java b/src/frontends/android/app/src/test/java/org/strongswan/android/test/IPRangeTest.java
new file mode 100644 (file)
index 0000000..51e976e
--- /dev/null
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.test;
+
+import org.junit.Test;
+import org.strongswan.android.utils.IPRange;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class IPRangeTest
+{
+       @Test
+       public void testRangeReversed() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.10", "192.168.0.1");
+               assertEquals("from", "192.168.0.1", test.getFrom().getHostAddress());
+               assertEquals("to", "192.168.0.10", test.getTo().getHostAddress());
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testRangeInvalid() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.1", "fec1::1");
+               assertEquals("from", "192.168.0.1", test.getFrom().getHostAddress());
+       }
+
+       @Test(expected = UnknownHostException.class)
+       public void testPrefixAddrInvalid() throws UnknownHostException
+       {
+               IPRange test = new IPRange("a.b.c.d", 24);
+               assertEquals("from", "192.168.0.1", test.getFrom().getHostAddress());
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testPrefixNegative() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.1", -5);
+               assertEquals("from", "192.168.0.1", test.getFrom().getHostAddress());
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testPrefixLarge() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.1", 42);
+               assertEquals("from", "192.168.0.1", test.getFrom().getHostAddress());
+       }
+
+       private void testPrefix(String from, String to, Integer prefix) throws UnknownHostException
+       {
+               IPRange test = new IPRange(from, to);
+               assertEquals("prefix", prefix, test.getPrefix());
+       }
+
+       @Test
+       public void testPrefix() throws UnknownHostException
+       {
+               testPrefix("192.168.0.0", "192.168.0.255", 24);
+               testPrefix("192.168.0.8", "192.168.0.15", 29);
+               testPrefix("192.168.0.1", "192.168.0.255", null);
+               testPrefix("192.168.0.1", "192.168.0.1", 32);
+               testPrefix("fec1::0", "fec1::ffff", 112);
+               testPrefix("fec1::1", "fec1::ffff", null);
+               testPrefix("fec1::1", "fec1::1", 128);
+       }
+
+       private void testPrefixRange(String base, int prefix, String from, String to) throws UnknownHostException
+       {
+               IPRange test = new IPRange(InetAddress.getByName(base), prefix);
+               assertEquals("from", from, test.getFrom().getHostAddress());
+               assertEquals("to", to, test.getTo().getHostAddress());
+       }
+
+       @Test
+       public void testPrefixRange() throws UnknownHostException
+       {
+               testPrefixRange("0.0.0.0", 0, "0.0.0.0", "255.255.255.255");
+               testPrefixRange("0.0.0.0", 32, "0.0.0.0", "0.0.0.0");
+               testPrefixRange("192.168.1.0", 24, "192.168.1.0", "192.168.1.255");
+               testPrefixRange("192.168.1.10", 24, "192.168.1.0", "192.168.1.255");
+               testPrefixRange("192.168.1.64", 26, "192.168.1.64", "192.168.1.127");
+               testPrefixRange("192.168.1.64", 27, "192.168.1.64", "192.168.1.95");
+               testPrefixRange("192.168.1.64", 28, "192.168.1.64", "192.168.1.79");
+               testPrefixRange("192.168.1.255", 29, "192.168.1.248", "192.168.1.255");
+               testPrefixRange("192.168.1.255", 30, "192.168.1.252", "192.168.1.255");
+               testPrefixRange("192.168.1.255", 31, "192.168.1.254", "192.168.1.255");
+               testPrefixRange("192.168.1.255", 32, "192.168.1.255", "192.168.1.255");
+
+               testPrefixRange("::", 0, "0:0:0:0:0:0:0:0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+               testPrefixRange("fec1::", 64, "fec1:0:0:0:0:0:0:0", "fec1:0:0:0:ffff:ffff:ffff:ffff");
+               testPrefixRange("fec1::1", 128, "fec1:0:0:0:0:0:0:1", "fec1:0:0:0:0:0:0:1");
+               testPrefixRange("fec1::10:0", 108, "fec1:0:0:0:0:0:10:0", "fec1:0:0:0:0:0:1f:ffff");
+               testPrefixRange("fec1::10:0", 112, "fec1:0:0:0:0:0:10:0", "fec1:0:0:0:0:0:10:ffff");
+               testPrefixRange("fec1::10:0", 113, "fec1:0:0:0:0:0:10:0", "fec1:0:0:0:0:0:10:7fff");
+               testPrefixRange("fec1::10:0", 116, "fec1:0:0:0:0:0:10:0", "fec1:0:0:0:0:0:10:fff");
+               testPrefixRange("fec1::1:ffff", 112, "fec1:0:0:0:0:0:1:0", "fec1:0:0:0:0:0:1:ffff");
+               testPrefixRange("fec1::1:ffff", 113, "fec1:0:0:0:0:0:1:8000", "fec1:0:0:0:0:0:1:ffff");
+               testPrefixRange("fec1::1:ffff", 114, "fec1:0:0:0:0:0:1:c000", "fec1:0:0:0:0:0:1:ffff");
+               testPrefixRange("fec1::1:ffff", 115, "fec1:0:0:0:0:0:1:e000", "fec1:0:0:0:0:0:1:ffff");
+               testPrefixRange("fec1::1:ffff", 116, "fec1:0:0:0:0:0:1:f000", "fec1:0:0:0:0:0:1:ffff");
+               testPrefixRange("fec1::1:ffff", 117, "fec1:0:0:0:0:0:1:f800", "fec1:0:0:0:0:0:1:ffff");
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testCIDRAddressInvalid() throws UnknownHostException
+       {
+               IPRange test = new IPRange("asdf");
+               assertEquals("not reached", null, test);
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testCIDRIncomplete() throws UnknownHostException
+       {
+               IPRange test = new IPRange("/32");
+               assertEquals("not reached", null, test);
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testCIDRIncompletePrefix() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.1/");
+               assertEquals("not reached", null, test);
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testCIDRPrefixNegative() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.1/-5");
+               assertEquals("not reached", null, test);
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testCIDRPrefixLarge() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.0.1/33");
+               assertEquals("not reached", null, test);
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testCIDRPrefixLargev6() throws UnknownHostException
+       {
+               IPRange test = new IPRange("fec1::1/129");
+               assertEquals("not reached", null, test);
+       }
+
+       @Test(expected = IllegalArgumentException.class)
+       public void testRangeMixed() throws UnknownHostException
+       {
+               IPRange test = new IPRange("192.168.1.1-fec1::1");
+               assertEquals("not reached", null, test);
+       }
+
+       private void testCIDR(String cidr, IPRange exp) throws UnknownHostException
+       {
+               IPRange test = new IPRange(cidr);
+               assertEquals("cidr", exp, test);
+       }
+
+       @Test
+       public void testCIDR() throws UnknownHostException
+       {
+               testCIDR("0.0.0.0/0", new IPRange("0.0.0.0", 0));
+               testCIDR("192.168.1.0/24", new IPRange("192.168.1.0", 24));
+               testCIDR("192.168.1.10/24", new IPRange("192.168.1.0", 24));
+               testCIDR("192.168.1.1/32", new IPRange("192.168.1.1", 32));
+               testCIDR("192.168.1.1", new IPRange("192.168.1.1", 32));
+               testCIDR("192.168.1.1-192.168.1.10", new IPRange("192.168.1.1", "192.168.1.10"));
+               testCIDR("::/0", new IPRange("::", 0));
+               testCIDR("fec1::/64", new IPRange("fec1::", 64));
+               testCIDR("fec1::10/64", new IPRange("fec1::", 64));
+               testCIDR("fec1::1/128", new IPRange("fec1::1", 128));
+               testCIDR("fec1::1", new IPRange("fec1::1", 128));
+               testCIDR("fec1::1-fec1::5", new IPRange("fec1::1", "fec1::5"));
+       }
+
+       private void testToString(String f, String t, String exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(f, t);
+               assertEquals("string", exp, a.toString());
+       }
+
+       @Test
+       public void testToString() throws UnknownHostException
+       {
+               testToString("192.168.1.1", "192.168.1.1", "192.168.1.1/32");
+               testToString("192.168.1.0", "192.168.1.255", "192.168.1.0/24");
+               testToString("192.168.1.1", "192.168.1.255", "192.168.1.1-192.168.1.255");
+               testToString("0.0.0.0", "255.255.255.255", "0.0.0.0/0");
+               testToString("fec1::1", "fec1::1", "fec1:0:0:0:0:0:0:1/128");
+               testToString("fec1::", "fec1::ffff", "fec1:0:0:0:0:0:0:0/112");
+               testToString("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "0:0:0:0:0:0:0:0/0");
+       }
+
+       private void compare(String af, String at, String bf, String bt, int exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(af, at);
+               IPRange b = new IPRange(bf, bt);
+               assertEquals("compare", exp, a.compareTo(b));
+       }
+
+       private void compare(String a, int pa, String b, int pb, int exp) throws UnknownHostException
+       {
+               IPRange ar = new IPRange(a, pa);
+               IPRange br = new IPRange(b, pb);
+               assertEquals("compare", exp, ar.compareTo(br));
+       }
+
+       @Test
+       public void testCompareTo() throws UnknownHostException
+       {
+               compare("192.168.0.0", "192.168.0.10", "192.168.0.0", "192.168.0.10", 0);
+               compare("192.168.0.1", "192.168.0.10", "192.168.0.0", "192.168.0.10", 1);
+               compare("192.168.0.0", "192.168.0.9", "192.168.0.0", "192.168.0.10", -1);
+
+               compare("192.168.0.0", 24, "192.168.0.0", 24, 0);
+               compare("192.168.0.0", 24, "192.168.0.0", 28, 1);
+               compare("192.168.0.0", 28, "192.168.0.0", 24, -1);
+               compare("192.168.0.0", 32, "192.168.0.255", 32, -1);
+               compare("10.0.1.0", 24, "192.168.1.0", 24, -1);
+               compare("10.0.1.0", 24, "fec1::", 64, -1);
+               compare("fec1::1", 128, "fec1::2", 128, -1);
+               compare("fec1::1", 126, "fec1::2", 126, 0);
+       }
+
+       @Test
+       public void testEquals() throws UnknownHostException
+       {
+               IPRange a = new IPRange("192.168.1.0/24");
+               IPRange b = new IPRange("192.168.1.0/24");
+               assertEquals("same", true, a.equals(a));
+               assertEquals("equals", true, a.equals(b));
+               InetAddress c = InetAddress.getByName("192.168.1.0");
+               assertEquals("null", false, a.equals(c));
+               assertEquals("null", false, a.equals(null));
+       }
+
+       private void contains(String af, String at, String bf, String bt, boolean exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(af, at);
+               IPRange b = new IPRange(bf, bt);
+               assertEquals("contains", exp, a.contains(b));
+       }
+
+       @Test
+       public void testContains() throws UnknownHostException
+       {
+               contains("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.10", true);
+               contains("192.168.1.0", "192.168.1.10", "192.168.1.1", "192.168.1.10", true);
+               contains("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.9", true);
+               contains("192.168.1.0", "192.168.1.10", "192.168.1.1", "192.168.1.9", true);
+               contains("192.168.1.0", "192.168.1.10", "192.168.1.2", "192.168.1.2", true);
+               contains("192.168.1.0", "192.168.1.10", "192.168.1.1", "192.168.1.11", false);
+               contains("192.168.1.0", "192.168.1.10", "192.168.0.255", "192.168.1.10", false);
+               contains("192.168.1.0", "192.168.1.10", "192.168.0.255", "192.168.1.11", false);
+               contains("192.168.1.1", "192.168.1.1", "192.168.1.0", "192.168.1.0", false);
+               contains("192.168.1.1", "192.168.1.1", "192.168.1.2", "192.168.1.2", false);
+               contains("192.168.1.0", "192.168.1.10", "fec1::", "fec1::10", false);
+               contains("fec1::", "fec1::10", "192.168.1.0", "192.168.1.10", false);
+       }
+
+       private void overlaps(String af, String at, String bf, String bt, boolean exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(af, at);
+               IPRange b = new IPRange(bf, bt);
+               assertEquals("b overlaps with a", exp, a.overlaps(b));
+               assertEquals("a overlaps with b", exp, b.overlaps(a));
+       }
+
+       @Test
+       public void testOverlaps() throws UnknownHostException
+       {
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.10", true);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.1.1", "192.168.1.9", true);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.1.10", "192.168.1.20", true);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.1.9", "192.168.1.20", true);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.1.11", "192.168.1.20", false);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.0.245", "192.168.1.1", true);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.0.245", "192.168.1.0", true);
+               overlaps("192.168.1.0", "192.168.1.10", "192.168.0.245", "192.168.0.255", false);
+               overlaps("192.168.1.0", "192.168.1.10", "fec1::", "fec1::10", false);
+       }
+
+       private void remove(String af, String at, String bf, String bt, IPRange...exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(af, at);
+               IPRange b = new IPRange(bf, bt);
+               List<IPRange> l = a.remove(b);
+               assertEquals("ranges", exp.length, l.size());
+               for (int i = 0; i < exp.length; i++)
+               {
+                       assertEquals("range", exp[i], l.get(i));
+               }
+       }
+
+       @Test
+       public void testRemove() throws UnknownHostException
+       {
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.10");
+               remove("192.168.1.0", "192.168.1.10", "192.168.0.0", "192.168.1.255");
+               remove("192.168.1.0", "192.168.1.10", "10.0.1.0", "10.0.1.10",
+                          new IPRange("192.168.1.0", "192.168.1.10"));
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.5",
+                          new IPRange("192.168.1.6", "192.168.1.10"));
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.6", "192.168.1.10",
+                          new IPRange("192.168.1.0", "192.168.1.5"));
+               remove("192.168.1.0", "192.168.1.10", "192.168.0.0", "192.168.1.5",
+                          new IPRange("192.168.1.6", "192.168.1.10"));
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.6", "192.168.1.255",
+                          new IPRange("192.168.1.0", "192.168.1.5"));
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.1", "192.168.1.9",
+                          new IPRange("192.168.1.0", "192.168.1.0"), new IPRange("192.168.1.10", "192.168.1.10"));
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.3", "192.168.1.7",
+                          new IPRange("192.168.1.0", "192.168.1.2"), new IPRange("192.168.1.8", "192.168.1.10"));
+               remove("192.168.0.0", "192.168.1.255", "192.168.1.0", "192.168.1.255",
+                          new IPRange("192.168.0.0", "192.168.0.255"));
+               remove("192.168.0.0", "192.168.1.255", "192.168.0.0", "192.168.0.255",
+                          new IPRange("192.168.1.0", "192.168.1.255"));
+               remove("192.168.1.0", "192.168.1.10", "0.0.0.0", "192.168.1.10");
+               remove("192.168.1.0", "192.168.1.10", "192.168.1.0", "255.255.255.255");
+       }
+
+       private void merge(String af, String at, String bf, String bt, IPRange exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(af, at);
+               IPRange b = new IPRange(bf, bt);
+               IPRange r = a.merge(b);
+               assertEquals("merge", exp, r);
+               r = b.merge(a);
+               assertEquals("reverse", exp, r);
+       }
+
+       @Test
+       public void testMerge() throws UnknownHostException
+       {
+               merge("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.10", new IPRange("192.168.1.0", "192.168.1.10"));
+               merge("192.168.1.0", "192.168.1.10", "192.168.0.0", "192.168.1.10", new IPRange("192.168.0.0", "192.168.1.10"));
+               merge("192.168.1.0", "192.168.1.10", "192.168.0.0", "192.168.0.255", new IPRange("192.168.0.0", "192.168.1.10"));
+               merge("192.168.1.0", "192.168.1.10", "192.168.1.0", "192.168.1.255", new IPRange("192.168.1.0", "192.168.1.255"));
+               merge("192.168.1.0", "192.168.1.10", "192.168.1.11", "192.168.1.255", new IPRange("192.168.1.0", "192.168.1.255"));
+               merge("192.168.1.0", "192.168.1.10", "192.168.0.0", "192.168.1.255", new IPRange("192.168.0.0", "192.168.1.255"));
+               merge("255.255.255.0", "255.255.255.255", "0.0.0.0", "0.0.0.255", null);
+               merge("0.0.0.1", "255.255.255.255", "0.0.0.0", "0.0.0.0", new IPRange("0.0.0.0", 0));
+               merge("0.0.0.0", "255.255.255.254", "255.255.255.255", "255.255.255.255", new IPRange("0.0.0.0", 0));
+               merge("192.168.1.0", "192.168.1.10", "192.168.0.0", "192.168.0.254", null);
+               merge("192.168.1.0", "192.168.1.10", "192.168.1.12", "192.168.1.255", null);
+               merge("192.168.1.0", "192.168.1.10", "fec1::0", "fec1::10", null);
+               merge("fec1::1:0", "fec1::1:10", "fec1::1:8", "fec1::1:20", new IPRange("fec1::1:0", "fec1::1:20"));
+       }
+
+       private void toSubnet(String f, String t, IPRange...exp) throws UnknownHostException
+       {
+               IPRange a = new IPRange(f, t);
+               List<IPRange> l = a.toSubnets();
+               assertEquals("ranges", exp.length, l.size());
+               for (int i = 0; i < exp.length; i++)
+               {
+                       assertEquals("range", exp[i], l.get(i));
+               }
+       }
+
+       @Test
+       public void testToSubnet() throws UnknownHostException
+       {
+               toSubnet("0.0.0.0", "255.255.255.255", new IPRange("0.0.0.0", 0));
+               toSubnet("192.168.1.1", "192.168.1.1", new IPRange("192.168.1.1", 32));
+               toSubnet("192.168.1.0", "192.168.1.255", new IPRange("192.168.1.0", 24));
+               toSubnet("192.168.1.0", "192.168.1.10", new IPRange("192.168.1.0", 29),
+                                new IPRange("192.168.1.8", 31), new IPRange("192.168.1.10", 32));
+               toSubnet("192.168.1.1", "192.168.1.10", new IPRange("192.168.1.1", 32),
+                                new IPRange("192.168.1.2", 31), new IPRange("192.168.1.4", 30),
+                                new IPRange("192.168.1.8", 31), new IPRange("192.168.1.10", 32));
+               toSubnet("192.168.1.241", "192.168.1.255", new IPRange("192.168.1.241", 32),
+                               new IPRange("192.168.1.242", 31), new IPRange("192.168.1.244", 30),
+                               new IPRange("192.168.1.248", 29));
+               toSubnet("192.168.254.255", "192.168.255.1", new IPRange("192.168.254.255", 32),
+                                new IPRange("192.168.255.0", 31));
+               toSubnet("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", new IPRange("::", 0));
+               toSubnet("fec1::0", "fec1::a", new IPRange("fec1::0", 125), new IPRange("fec1::8", 127),
+                               new IPRange("fec1::a", 128));
+       }
+}
index aaa880c..cfad01d 100644 (file)
@@ -95,7 +95,8 @@ public class SettingsWriterTest
                SettingsWriter writer = new SettingsWriter();
                writer.setValue("key", "val\"ue");
                writer.setValue("nl", "val\nue");
-               assertEquals("serialized", "key=\"val\\\"ue\"\nnl=\"val\nue\"\n", writer.serialize());
+               writer.setValue("bs", "val\\ue");
+               assertEquals("serialized", "key=\"val\\\"ue\"\nnl=\"val\nue\"\nbs=\"val\\\\ue\"\n", writer.serialize());
        }
 
        @Test
index 3d5ba79..e52ed0a 100644 (file)
@@ -3,7 +3,7 @@ buildscript {
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.2.3'
+        classpath 'com.android.tools.build:gradle:2.3.3'
     }
 }
 
index a6bd91c..8c08a36 100644 (file)
@@ -1,6 +1,6 @@
-#Tue Sep 20 17:56:35 CEST 2016
+#Tue Apr 18 15:57:06 CEST 2017
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
index a5e6b72..afca134 100644 (file)
@@ -113,6 +113,8 @@ LOCAL_SRC_FILES += $(call add_plugin, pubkey)
 
 LOCAL_SRC_FILES += $(call add_plugin, random)
 
+LOCAL_SRC_FILES += $(call add_plugin, revocation)
+
 LOCAL_SRC_FILES += $(call add_plugin, sha1)
 
 LOCAL_SRC_FILES += $(call add_plugin, sha2)
@@ -133,6 +135,6 @@ LOCAL_ARM_MODE := arm
 
 LOCAL_PRELINK_MODULE := false
 
-LOCAL_SHARED_LIBRARIES += libdl
+LOCAL_LDLIBS += -ldl
 
 include $(BUILD_SHARED_LIBRARY)
index e379732..ba74965 100644 (file)
@@ -16,7 +16,7 @@ LOCAL_SRC_FILES := $(filter %.c,$(libtnccs_la_SOURCES))
 
 LOCAL_SRC_FILES += $(call add_plugin, tnc-imc)
 ifneq ($(call plugin_enabled, tnc-imc),)
-LOCAL_SHARED_LIBRARIES += libdl
+LOCAL_LDLIBS += -ldl
 endif
 
 LOCAL_SRC_FILES += $(call add_plugin, tnc-tnccs)