Compare commits

...

22 Commits

Author SHA1 Message Date
Tobias Brunner e6a904de69 testing: Fix build of tkm and tkm-rpc 2021-08-17 19:20:45 +02:00
Tobias Brunner ce607d774b github: Use latest version of SonarScanner 2021-08-16 11:26:11 +02:00
Tobias Brunner 1c4e134f26 kernel-netlink: Initialize ifreq structs when detecting offload capability 2021-08-11 15:35:52 +02:00
Noel Kuntze 5d6e69a6e4 gitignore: Add nbproject/
As used by the NetBeans IDE.

Closes strongswan/strongswan#505.
2021-07-26 10:43:40 +02:00
Tobias Brunner 4ae9b482f2 Ignore android-* tags when using `git describe` 2021-07-14 10:31:52 +02:00
Tobias Brunner 1aa68a78f8 Merge branch 'android-updates'
Adds a button to install user certificates and updates the target SDK
version for Android 11 (and the related deprecation fixes), which will
be mandatory later this year.

The release also includes an older commit that changed how DNS servers
are applied to TUN devices (cd10ae2ff0 ("android: Explicitly apply DNS
servers to the TUN device")).

Also added are metadata for F-Droid.
2021-07-14 10:05:08 +02:00
Tobias Brunner 358d3d0ba1 android: New release after adding button to install PKCS#12 and SDK update
It also includes an unreleased change that affects when/how DNS servers
are applied to the TUN device.
2021-07-14 09:59:02 +02:00
Tobias Brunner a14337bcde android: Request QUERY_ALL_PACKAGES permission
This is required when targeting Android 11 (API 30) in order to see all
packages, which we use to allow selecting apps ex-/included from VPN
profiles and for the EAP-TNC use case.
2021-07-14 09:59:02 +02:00
Tobias Brunner 3eadd0fc3a android: Add Triple-T metadata for F-Droid
We currently don't use Triple-T but the metadata is also parsed by F-Droid
to provide information for included apps.
2021-07-14 09:59:02 +02:00
Tobias Brunner 2f1bf11dca android: Add a button to install user certificates
Newer Android versions don't provide this option anymore on the
selection dialog.
2021-07-14 09:59:02 +02:00
Tobias Brunner b3cdbe6693 android: Replace deprecated Switch with SwitchCompat 2021-07-14 09:59:02 +02:00
Tobias Brunner 26354d0aba android: Replace deprecated AsyncTask instances
As suggested by the Android docs, we use a global thread pool and handler
to avoid recreating them repeatedly.  Four threads should be more than
enough as we only use this to load CA certificates when the app starts
initially and to load user certs when editing a profile.
2021-07-14 09:59:02 +02:00
Tobias Brunner a8cc146240 android: Migrate from deprecated ViewPager to ViewPager2 for CA cert lists 2021-07-13 14:26:38 +02:00
Tobias Brunner c976165533 android: Replace deprecated FileObserver() constructor with newer SDKs 2021-07-13 14:25:46 +02:00
Tobias Brunner dc351a30e1 android: Replace usage of deprecated Handler() constructor 2021-07-13 14:25:46 +02:00
Tobias Brunner 93c494e295 android: Replace deprecated onActivityCreated() with onViewCreated() 2021-07-13 14:25:46 +02:00
Tobias Brunner 7cd50aeb64 android: Replace deprecated startActivityForResult/onActivityResult usage 2021-07-13 14:25:44 +02:00
Tobias Brunner a885e38265 android: Set compile-/targetSdkVersion to 30
This will be mandatory for new apps in August and for existing apps
in November.  However, several classes like AsyncTask are now deprecated
so this needs some work to avoid warnings and problems in the future.
2021-07-13 14:18:42 +02:00
Tobias Brunner 6f3725ea8b android: Replace jcenter with mavenCentral repository 2021-07-13 14:18:42 +02:00
Tobias Brunner 5831009941 android: Update dependencies 2021-07-13 14:18:42 +02:00
Tobias Brunner 44e16a63ef android: Update Gradle plugin 2021-07-13 14:18:42 +02:00
Tobias Brunner 2f9114bce1 Use wolfSSL 4.8.0 for tests 2021-07-13 10:19:56 +02:00
50 changed files with 472 additions and 287 deletions

View File

@ -46,7 +46,7 @@ jobs:
# for C builds, so we follow the "any CI" instructions
- name: Install sonar-scanner
env:
SONAR_SCANNER_VERSION: 4.4.0.2170
SONAR_SCANNER_VERSION: 4.6.2.2472
run: |
export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux
curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip

1
.gitignore vendored
View File

@ -52,3 +52,4 @@ coverage/
/*.files
/*.includes
test-driver
nbproject/

View File

@ -6,7 +6,7 @@ TARBALL=$SRCDIR/.tarball-git-version
if test -f $TARBALL; then
V=$(cat $TARBALL)
elif test -d $SRCDIR/.git; then
V=$(git -C $SRCDIR describe --tags HEAD 2>/dev/null)
V=$(git -C $SRCDIR describe --exclude 'android-*' --tags HEAD 2>/dev/null)
fi
if test -z "$V"; then

View File

@ -37,7 +37,7 @@ build_botan()
build_wolfssl()
{
WOLFSSL_REV=0caf3ba456f1 # v4.7.1r + SHA-3 fix
WOLFSSL_REV=v4.8.0-stable
WOLFSSL_DIR=$DEPS_BUILD_DIR/wolfssl
if test -d "$WOLFSSL_DIR"; then
@ -468,7 +468,7 @@ sonarcloud)
-Dsonar.projectKey=${SONAR_PROJECT} \
-Dsonar.organization=${SONAR_ORGANIZATION} \
-Dsonar.login=${SONAR_TOKEN} \
-Dsonar.projectVersion=$(git describe)+${BUILD_NUMBER} \
-Dsonar.projectVersion=$(git describe --exclude 'android-*')+${BUILD_NUMBER} \
-Dsonar.sources=. \
-Dsonar.cfamily.threads=2 \
-Dsonar.cfamily.cache.enabled=true \

View File

@ -1,14 +1,14 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
applicationId "org.strongswan.android"
minSdkVersion 15
targetSdkVersion 29
versionCode 74
versionName "2.3.2"
targetSdkVersion 30
versionCode 75
versionName "2.3.3"
}
sourceSets.main {
@ -46,13 +46,13 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.2.1'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.28.2'
testImplementation 'org.powermock:powermock-core:2.0.2'
testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:3.11.2'
testImplementation 'org.powermock:powermock-core:2.0.9'
testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
}

View File

@ -24,6 +24,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- necessary to allow users to select ex-/included apps and EAP-TNC -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".logic.StrongSwanApplication"

View File

@ -192,7 +192,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
mAppDir = getFilesDir().getAbsolutePath();
/* handler used to do changes in the main UI thread */
mHandler = new Handler();
mHandler = new Handler(getMainLooper());
mDataSource = new VpnProfileDataSource(this);
mDataSource.open();

View File

@ -16,6 +16,9 @@
package org.strongswan.android.logic;
import java.security.Security;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.strongswan.android.security.LocalCertificateKeyStoreProvider;
import org.strongswan.android.ui.MainActivity;
@ -23,10 +26,16 @@ import org.strongswan.android.ui.MainActivity;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import androidx.core.os.HandlerCompat;
public class StrongSwanApplication extends Application
{
private static Context mContext;
private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
private final Handler mMainHandler = HandlerCompat.createAsync(Looper.getMainLooper());
static {
Security.addProvider(new LocalCertificateKeyStoreProvider());
@ -48,6 +57,24 @@ public class StrongSwanApplication extends Application
return StrongSwanApplication.mContext;
}
/**
* Returns a thread pool to run tasks in separate threads
* @return thread pool
*/
public Executor getExecutor()
{
return mExecutorService;
}
/**
* Returns a handler to execute stuff by the main thread.
* @return handler
*/
public Handler getHandler()
{
return mMainHandler;
}
/*
* The libraries are extracted to /data/data/org.strongswan.android/...
* during installation. On newer releases most are loaded in JNI_OnLoad.

View File

@ -22,6 +22,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
@ -107,7 +108,7 @@ public class VpnStateService extends Service
{
/* this handler allows us to notify listeners from the UI thread and
* not from the threads that actually report any state changes */
mHandler = new RetryHandler(this);
mHandler = new RetryHandler(getMainLooper(), this);
}
@Override
@ -536,8 +537,9 @@ public class VpnStateService extends Service
private static class RetryHandler extends Handler {
WeakReference<VpnStateService> mService;
public RetryHandler(VpnStateService service)
public RetryHandler(Looper looper, VpnStateService service)
{
super(looper);
mService = new WeakReference<>(service);
}

View File

@ -16,9 +16,11 @@
package org.strongswan.android.ui;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.FileObserver;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -36,6 +38,7 @@ import java.io.StringReader;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
public class LogFragment extends Fragment
@ -55,9 +58,17 @@ public class LogFragment extends Fragment
mLogFilePath = getActivity().getFilesDir() + File.separator + CharonVpnService.LOG_FILE;
mLogHandler = new Handler();
mLogHandler = new Handler(Looper.getMainLooper());
mDirectoryObserver = new LogDirectoryObserver(getActivity().getFilesDir().getAbsolutePath());
File logdir = getActivity().getFilesDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
mDirectoryObserver = new LogDirectoryObserver(logdir);
}
else
{
mDirectoryObserver = new LogDirectoryObserver(logdir.getAbsolutePath());
}
}
@Override
@ -223,14 +234,20 @@ public class LogFragment extends Fragment
*/
private class LogDirectoryObserver extends FileObserver
{
private final File mFile;
private long mSize;
private static final int mMask = FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE;
private final File mFile = new File(mLogFilePath);
private long mSize = mFile.length();
@SuppressWarnings("deprecation")
public LogDirectoryObserver(String path)
{
super(path, FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE);
mFile = new File(mLogFilePath);
mSize = mFile.length();
super(path, mMask);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
public LogDirectoryObserver(File path)
{
super(path, mMask);
}
@Override

View File

@ -20,7 +20,6 @@ package org.strongswan.android.ui;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.format.Formatter;
@ -30,6 +29,7 @@ import android.widget.Toast;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.logic.StrongSwanApplication;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
@ -68,8 +68,10 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec
bar.setDisplayShowTitleEnabled(false);
bar.setIcon(R.mipmap.ic_app);
/* load CA certificates in a background task */
new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
/* load CA certificates in a background thread */
((StrongSwanApplication)getApplication()).getExecutor().execute(() -> {
TrustedCertificateManager.getInstance().load();
});
}
@Override
@ -157,18 +159,6 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec
dialog.show(this.getSupportFragmentManager(), DIALOG_TAG);
}
/**
* Class that loads the cached CA certificates.
*/
private class LoadCertificatesTask extends AsyncTask<Void, Void, TrustedCertificateManager>
{
@Override
protected TrustedCertificateManager doInBackground(Void... params)
{
return TrustedCertificateManager.getInstance().load();
}
}
/**
* Dismiss dialog if shown
*/

View File

@ -26,6 +26,8 @@ import android.widget.TextView;
import org.strongswan.android.R;
import org.strongswan.android.logic.imc.RemediationInstruction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
public class RemediationInstructionFragment extends ListFragment
@ -49,9 +51,9 @@ public class RemediationInstructionFragment extends ListFragment
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null)
{

View File

@ -26,6 +26,8 @@ import org.strongswan.android.ui.adapter.RemediationInstructionAdapter;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
public class RemediationInstructionsFragment extends ListFragment
@ -46,9 +48,9 @@ public class RemediationInstructionsFragment extends ListFragment
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null)
{

View File

@ -40,6 +40,7 @@ import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.ListFragment;
@ -53,9 +54,9 @@ public class SelectedApplicationsListFragment extends ListFragment implements Lo
private SortedSet<String> mSelection;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState)
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
super.onViewCreated(view, savedInstanceState);
setHasOptionsMenu(true);
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

View File

@ -16,7 +16,6 @@
package org.strongswan.android.ui;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
@ -37,6 +36,8 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDialogFragment;
@ -44,10 +45,21 @@ import androidx.fragment.app.FragmentTransaction;
public class TrustedCertificateImportActivity extends AppCompatActivity
{
private static final int OPEN_DOCUMENT = 0;
private static final String DIALOG_TAG = "Dialog";
private Uri mCertificateUri;
private final ActivityResultLauncher<Intent> mOpenDocument = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK && result.getData() != null)
{
mCertificateUri = result.getData().getData();
return;
}
finish();
}
);
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onCreate(Bundle savedInstanceState)
@ -71,7 +83,7 @@ public class TrustedCertificateImportActivity extends AppCompatActivity
openIntent.setType("*/*");
try
{
startActivityForResult(openIntent, OPEN_DOCUMENT);
mOpenDocument.launch(openIntent);
}
catch (ActivityNotFoundException e)
{ /* some devices are unable to browse for files */
@ -81,23 +93,6 @@ public class TrustedCertificateImportActivity extends AppCompatActivity
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode)
{
case OPEN_DOCUMENT:
if (resultCode == Activity.RESULT_OK && data != null)
{
mCertificateUri = data.getData();
return;
}
finish();
return;
}
}
@Override
protected void onPostResume()
{
@ -214,7 +209,7 @@ public class TrustedCertificateImportActivity extends AppCompatActivity
if (activity.storeCertificate(certificate))
{
Toast.makeText(getActivity(), R.string.cert_imported_successfully, Toast.LENGTH_LONG).show();
getActivity().setResult(Activity.RESULT_OK);
getActivity().setResult(RESULT_OK);
}
else
{

View File

@ -41,6 +41,8 @@ import java.util.Map.Entry;
import java.util.Observable;
import java.util.Observer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
@ -63,9 +65,9 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
super.onViewCreated(view, savedInstanceState);
setHasOptionsMenu(true);
setEmptyText(getString(R.string.no_certificates));

View File

@ -15,8 +15,6 @@
package org.strongswan.android.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
@ -24,6 +22,7 @@ import android.view.Menu;
import android.view.MenuItem;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfileDataSource;
@ -34,22 +33,34 @@ import org.strongswan.android.ui.CertificateDeleteConfirmationDialog.OnCertifica
import java.security.KeyStore;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
public class TrustedCertificatesActivity extends AppCompatActivity implements TrustedCertificateListFragment.OnTrustedCertificateSelectedListener, OnCertificateDeleteListener
{
public static final String SELECT_CERTIFICATE = "org.strongswan.android.action.SELECT_CERTIFICATE";
private static final String DIALOG_TAG = "Dialog";
private static final int IMPORT_CERTIFICATE = 0;
private TrustedCertificatesPagerAdapter mAdapter;
private ViewPager mPager;
private ViewPager2 mPager;
private boolean mSelect;
private final ActivityResultLauncher<Intent> mImportCertificate = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK)
{
reloadCertificates();
}
}
);
@Override
public void onCreate(Bundle savedInstanceState)
{
@ -59,13 +70,15 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
mAdapter = new TrustedCertificatesPagerAdapter(getSupportFragmentManager(), this);
mAdapter = new TrustedCertificatesPagerAdapter(this);
mPager = (ViewPager)findViewById(R.id.viewpager);
mPager = (ViewPager2)findViewById(R.id.viewpager);
mPager.setAdapter(mAdapter);
TabLayout tabs = (TabLayout)findViewById(R.id.tabs);
tabs.setupWithViewPager(mPager);
new TabLayoutMediator(tabs, mPager, (tab, position) -> {
tab.setText(mAdapter.getTitle(position));
}).attach();
mSelect = SELECT_CERTIFICATE.equals(getIntent().getAction());
}
@ -100,27 +113,12 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr
return true;
case R.id.menu_import_certificate:
Intent intent = new Intent(this, TrustedCertificateImportActivity.class);
startActivityForResult(intent, IMPORT_CERTIFICATE);
mImportCertificate.launch(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case IMPORT_CERTIFICATE:
if (resultCode == Activity.RESULT_OK)
{
reloadCertificates();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onTrustedCertificateSelected(TrustedCertificateEntry selected)
{
@ -129,7 +127,7 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr
/* the user selected a certificate, return to calling activity */
Intent intent = new Intent();
intent.putExtra(VpnProfileDataSource.KEY_CERTIFICATE, selected.getAlias());
setResult(Activity.RESULT_OK, intent);
setResult(RESULT_OK, intent);
finish();
}
else if (mAdapter.getSource(mPager.getCurrentItem()) == TrustedCertificateSource.LOCAL)
@ -163,28 +161,21 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr
TrustedCertificateManager.getInstance().reset();
}
public static class TrustedCertificatesPagerAdapter extends FragmentPagerAdapter
public static class TrustedCertificatesPagerAdapter extends FragmentStateAdapter
{
private TrustedCertificatesTab mTabs[];
public TrustedCertificatesPagerAdapter(FragmentManager fm, Context context)
public TrustedCertificatesPagerAdapter(@NonNull FragmentActivity fragmentActivity)
{
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
super(fragmentActivity);
mTabs = new TrustedCertificatesTab[]{
new TrustedCertificatesTab(context.getString(R.string.system_tab), TrustedCertificateSource.SYSTEM),
new TrustedCertificatesTab(context.getString(R.string.user_tab), TrustedCertificateSource.USER),
new TrustedCertificatesTab(context.getString(R.string.local_tab), TrustedCertificateSource.LOCAL),
new TrustedCertificatesTab(fragmentActivity.getString(R.string.system_tab), TrustedCertificateSource.SYSTEM),
new TrustedCertificatesTab(fragmentActivity.getString(R.string.user_tab), TrustedCertificateSource.USER),
new TrustedCertificatesTab(fragmentActivity.getString(R.string.local_tab), TrustedCertificateSource.LOCAL),
};
}
@Override
public int getCount()
{
return mTabs.length;
}
@Override
public CharSequence getPageTitle(int position)
public CharSequence getTitle(int position)
{
return mTabs[position].getTitle();
}
@ -195,7 +186,14 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr
}
@Override
public Fragment getItem(int position)
public int getItemCount()
{
return mTabs.length;
}
@NonNull
@Override
public Fragment createFragment(int position)
{
TrustedCertificateListFragment fragment = new TrustedCertificateListFragment();
Bundle args = new Bundle();

View File

@ -44,6 +44,8 @@ import org.strongswan.android.logic.VpnStateService;
import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.utils.Constants;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@ -59,8 +61,6 @@ public class VpnProfileControlActivity extends AppCompatActivity
public static final String DISCONNECT = "org.strongswan.android.action.DISCONNECT";
public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID";
private static final int PREPARE_VPN_SERVICE = 0;
private static final int ADD_TO_POWER_WHITELIST = 1;
private static final String WAITING_FOR_RESULT = "WAITING_FOR_RESULT";
private static final String PROFILE_NAME = "PROFILE_NAME";
private static final String PROFILE_REQUIRES_PASSWORD = "REQUIRES_PASSWORD";
@ -87,6 +87,33 @@ public class VpnProfileControlActivity extends AppCompatActivity
}
};
private final ActivityResultLauncher<Intent> mPrepareVpnService = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
mWaitingForResult = false;
if (result.getResultCode() == RESULT_OK && mProfileInfo != null)
{
onVpnServicePrepared();
}
else
{ /* this happens if the always-on VPN feature is activated by a different app or the user declined */
VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_no_permission);
}
}
);
private final ActivityResultLauncher<Intent> mAddToPowerWhitelist = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
mWaitingForResult = false;
if (mProfileInfo != null && mService != null)
{
mService.connect(mProfileInfo, true);
}
finish();
}
);
@Override
public void onCreate(Bundle savedInstanceState)
{
@ -173,7 +200,7 @@ public class VpnProfileControlActivity extends AppCompatActivity
try
{
mWaitingForResult = true;
startActivityForResult(intent, PREPARE_VPN_SERVICE);
mPrepareVpnService.launch(intent);
}
catch (ActivityNotFoundException ex)
{
@ -187,7 +214,23 @@ public class VpnProfileControlActivity extends AppCompatActivity
}
else
{ /* user already granted permission to use VpnService */
onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
onVpnServicePrepared();
}
}
/**
* Called once the VpnService has been prepared and permission has been granted
* by the user.
*/
protected void onVpnServicePrepared()
{
if (checkPowerWhitelist())
{
if (mService != null)
{
mService.connect(mProfileInfo, true);
}
finish();
}
}
@ -219,48 +262,6 @@ public class VpnProfileControlActivity extends AppCompatActivity
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case PREPARE_VPN_SERVICE:
mWaitingForResult = false;
if (resultCode == RESULT_OK && mProfileInfo != null)
{
if (checkPowerWhitelist())
{
if (mService != null)
{
mService.connect(mProfileInfo, true);
}
finish();
}
}
else
{ /* this happens if the always-on VPN feature is activated by a different app or the user declined */
if (getSupportFragmentManager().isStateSaved())
{ /* onActivityResult() might be called when we aren't active anymore e.g. if the
* user pressed the home button, if the activity is started again we land here
* before onNewIntent() is called */
return;
}
VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_no_permission);
}
break;
case ADD_TO_POWER_WHITELIST:
mWaitingForResult = false;
if (mProfileInfo != null && mService != null)
{
mService.connect(mProfileInfo, true);
}
finish();
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* Check if we are currently connected to a VPN connection
*
@ -597,7 +598,7 @@ public class VpnProfileControlActivity extends AppCompatActivity
activity.mWaitingForResult = true;
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, ADD_TO_POWER_WHITELIST);
activity.mAddToPowerWhitelist.launch(intent);
}).create();
}

View File

@ -21,9 +21,9 @@ import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
@ -43,6 +43,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
@ -50,7 +51,6 @@ import android.widget.EditText;
import android.widget.MultiAutoCompleteTextView;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import org.strongswan.android.R;
@ -59,6 +59,7 @@ 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;
import org.strongswan.android.logic.StrongSwanApplication;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.security.TrustedCertificateEntry;
import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter;
@ -73,18 +74,19 @@ import java.util.ArrayList;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Executor;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.text.HtmlCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
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;
private TrustedCertificateEntry mCertEntry;
@ -119,12 +121,12 @@ public class VpnProfileDetailActivity extends AppCompatActivity
private TextInputLayoutHelper mMTUWrap;
private EditText mPort;
private TextInputLayoutHelper mPortWrap;
private Switch mCertReq;
private Switch mUseCrl;
private Switch mUseOcsp;
private Switch mStrictRevocation;
private Switch mRsaPss;
private Switch mIPv6Transport;
private SwitchCompat mCertReq;
private SwitchCompat mUseCrl;
private SwitchCompat mUseOcsp;
private SwitchCompat mStrictRevocation;
private SwitchCompat mRsaPss;
private SwitchCompat mIPv6Transport;
private EditText mNATKeepalive;
private TextInputLayoutHelper mNATKeepaliveWrap;
private EditText mIncludedSubnets;
@ -144,6 +146,41 @@ public class VpnProfileDetailActivity extends AppCompatActivity
private EditText mDnsServers;
private TextInputLayoutHelper mDnsServersWrap;
private final ActivityResultLauncher<Intent> mInstallPKCS12 = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK)
{
mSelectUserCert.performClick();
}
}
);
private final ActivityResultLauncher<Intent> mSelectTrustedCertificate = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK)
{
String alias = result.getData().getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
updateCertificateSelector();
}
}
);
private final ActivityResultLauncher<Intent> mSelectApplications = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK)
{
ArrayList<String> selection = result.getData().getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
mSelectedApps = new TreeSet<>(selection);
updateAppsSelector();
}
}
);
@Override
public void onCreate(Bundle savedInstanceState)
{
@ -190,7 +227,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap);
mNATKeepalive = (EditText)findViewById(R.id.nat_keepalive);
mNATKeepaliveWrap = (TextInputLayoutHelper) findViewById(R.id.nat_keepalive_wrap);
mCertReq = (Switch)findViewById(R.id.cert_req);
mCertReq = findViewById(R.id.cert_req);
mUseCrl = findViewById(R.id.use_crl);
mUseOcsp = findViewById(R.id.use_ocsp);
mStrictRevocation= findViewById(R.id.strict_revocation);
@ -283,6 +320,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity
});
mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
((Button)findViewById(R.id.install_user_certificate)).setOnClickListener(v -> {
Intent intent = KeyChain.createInstallIntent();
mInstallPKCS12.launch(intent);
});
mSelectUserIdAdapter = new CertificateIdentitiesAdapter(this);
mLocalId.setAdapter(mSelectUserIdAdapter);
@ -300,7 +341,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
{
Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
mSelectTrustedCertificate.launch(intent);
}
});
@ -334,7 +375,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity
{
Intent intent = new Intent(VpnProfileDetailActivity.this, SelectedApplicationsActivity.class);
intent.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
startActivityForResult(intent, SELECT_APPLICATIONS);
mSelectApplications.launch(intent);
}
});
@ -404,33 +445,6 @@ public class VpnProfileDetailActivity extends AppCompatActivity
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case SELECT_TRUSTED_CERTIFICATE:
if (resultCode == RESULT_OK)
{
String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
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);
}
}
/**
* Update the UI to enter credentials depending on the type of VPN currently selected
*/
@ -791,9 +805,22 @@ public class VpnProfileDetailActivity extends AppCompatActivity
useralias = savedInstanceState == null ? useralias : savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
if (useralias != null)
{
UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
mUserCertLoading = useralias;
loader.execute();
UserCertificateLoader loader = new UserCertificateLoader(((StrongSwanApplication)getApplication()).getExecutor(),
((StrongSwanApplication)getApplication()).getHandler());
loader.loadCertifiate(this, useralias, result -> {
if (result != null)
{
mUserCertEntry = new TrustedCertificateEntry(mUserCertLoading, result);
}
else
{ /* previously selected certificate is not here anymore */
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
mUserCertEntry = null;
}
mUserCertLoading = null;
updateCredentialView();
});
}
/* check if the user selected a CA certificate previously */
@ -961,55 +988,53 @@ public class VpnProfileDetailActivity extends AppCompatActivity
}
}
/**
* Callback interface for the user certificate loader.
*/
private interface UserCertificateLoaderCallback {
void onComplete(X509Certificate result);
}
/**
* Load the selected user certificate asynchronously. This cannot be done
* from the main thread as getCertificateChain() calls back to our main
* thread to bind to the KeyChain service resulting in a deadlock.
*/
private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
private class UserCertificateLoader
{
private final Context mContext;
private final String mAlias;
private final Executor mExecutor;
private final Handler mHandler;
public UserCertificateLoader(Context context, String alias)
public UserCertificateLoader(Executor executor, Handler handler)
{
mContext = context;
mAlias = alias;
mExecutor = executor;
mHandler = handler;
}
@Override
protected X509Certificate doInBackground(Void... params)
public void loadCertifiate(Context context, String alias, UserCertificateLoaderCallback callback)
{
X509Certificate[] chain = null;
try
{
chain = KeyChain.getCertificateChain(mContext, mAlias);
}
catch (KeyChainException | InterruptedException e)
{
e.printStackTrace();
}
if (chain != null && chain.length > 0)
{
return chain[0];
}
return null;
mExecutor.execute(() -> {
X509Certificate[] chain = null;
try
{
chain = KeyChain.getCertificateChain(context, alias);
}
catch (KeyChainException | InterruptedException e)
{
e.printStackTrace();
}
if (chain != null && chain.length > 0)
{
complete(chain[0], callback);
return;
}
complete(null, callback);
});
}
@Override
protected void onPostExecute(X509Certificate result)
protected void complete(X509Certificate result, UserCertificateLoaderCallback callback)
{
if (result != null)
{
mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
}
else
{ /* previously selected certificate is not here anymore */
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
mUserCertEntry = null;
}
mUserCertLoading = null;
updateCredentialView();
mHandler.post(() -> callback.onComplete(result));
}
}

View File

@ -15,7 +15,6 @@
package org.strongswan.android.ui;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
@ -74,6 +73,8 @@ import java.util.UUID;
import javax.net.ssl.SSLHandshakeException;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.AsyncTaskLoader;
@ -84,8 +85,6 @@ 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;