diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle index 450a12fbd..af1737299 100644 --- a/src/frontends/android/app/build.gradle +++ b/src/frontends/android/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.strongswan.android" minSdkVersion 15 targetSdkVersion 22 - versionCode 36 - versionName "1.7.2" + versionCode 40 + versionName "1.8.0" } sourceSets.main { diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index da465ba74..bc2de9af7 100644 --- a/src/frontends/android/app/src/main/AndroidManifest.xml +++ b/src/frontends/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.ui; + +import android.app.Activity; +import android.app.LoaderManager; +import android.app.ProgressDialog; +import android.content.AsyncTaskLoader; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.Loader; +import android.net.Uri; +import android.os.Bundle; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.security.KeyChainException; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.util.Base64; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.json.JSONException; +import org.json.JSONObject; +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnType; +import org.strongswan.android.data.VpnType.VpnTypeFeature; +import org.strongswan.android.logic.TrustedCertificateManager; +import org.strongswan.android.security.TrustedCertificateEntry; +import org.strongswan.android.ui.widget.TextInputLayoutHelper; +import org.strongswan.android.utils.Constants; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.UUID; + +import javax.net.ssl.SSLHandshakeException; + +public class VpnProfileImportActivity extends AppCompatActivity +{ + private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED"; + private static final int INSTALL_PKCS12 = 0; + private static final int PROFILE_LOADER = 0; + private static final int USER_CERT_LOADER = 1; + + private VpnProfileDataSource mDataSource; + private ParsedVpnProfile mProfile; + private VpnProfile mExisting; + private TrustedCertificateEntry mCertEntry; + private TrustedCertificateEntry mUserCertEntry; + private String mUserCertLoading; + private boolean mHideImport; + private ProgressDialog mProgress; + private TextView mExistsWarning; + private ViewGroup mBasicDataGroup; + private TextView mName; + private TextView mGateway; + private TextView mSelectVpnType; + private ViewGroup mUsernamePassword; + private EditText mUsername; + private TextInputLayoutHelper mUsernameWrap; + private EditText mPassword; + private ViewGroup mUserCertificate; + private RelativeLayout mSelectUserCert; + private Button mImportUserCert; + private ViewGroup mRemoteCertificate; + private RelativeLayout mRemoteCert; + + private LoaderManager.LoaderCallbacks mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks() + { + @Override + public Loader onCreateLoader(int id, Bundle args) + { + return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData()); + } + + @Override + public void onLoadFinished(Loader loader, ProfileLoadResult data) + { + handleProfile(data); + } + + @Override + public void onLoaderReset(Loader loader) + { + + } + }; + + private LoaderManager.LoaderCallbacks mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks() + { + @Override + public Loader onCreateLoader(int id, Bundle args) + { + return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading); + } + + @Override + public void onLoadFinished(Loader loader, TrustedCertificateEntry data) + { + handleUserCertificate(data); + } + + @Override + public void onLoaderReset(Loader loader) + { + + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + mDataSource = new VpnProfileDataSource(this); + mDataSource.open(); + + setContentView(R.layout.profile_import_view); + + mExistsWarning = (TextView)findViewById(R.id.exists_warning); + mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group); + mName = (TextView)findViewById(R.id.name); + mGateway = (TextView)findViewById(R.id.gateway); + mSelectVpnType = (TextView)findViewById(R.id.vpn_type); + + mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group); + mUsername = (EditText)findViewById(R.id.username); + mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap); + mPassword = (EditText)findViewById(R.id.password); + + mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group); + mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate); + mImportUserCert = (Button)findViewById(R.id.import_user_certificate); + + mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group); + mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate); + + mExistsWarning.setVisibility(View.GONE); + mBasicDataGroup.setVisibility(View.GONE); + mUsernamePassword.setVisibility(View.GONE); + mUserCertificate.setVisibility(View.GONE); + mRemoteCertificate.setVisibility(View.GONE); + + mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener()); + mImportUserCert.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) + { + Intent intent = KeyChain.createInstallIntent(); + intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName())); + intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12); + startActivityForResult(intent, INSTALL_PKCS12); + } + }); + + Intent intent = getIntent(); + String action = intent.getAction(); + if (Intent.ACTION_VIEW.equals(action)) + { + mProgress = ProgressDialog.show(this, null, getString(R.string.loading), + true, true, new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) + { + finish(); + } + }); + + getLoaderManager().initLoader(PROFILE_LOADER, null, mProfileLoaderCallbacks); + } + + if (savedInstanceState != null) + { + mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE); + if (mUserCertLoading != null) + { + getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks); + } + mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED)); + } + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + mDataSource.close(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + if (mUserCertEntry != null) + { + outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias()); + } + outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.profile_import, menu); + if (mHideImport) + { + MenuItem item = menu.findItem(R.id.menu_accept); + item.setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case android.R.id.home: + finish(); + return true; + case R.id.menu_accept: + saveProfile(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) + { + case INSTALL_PKCS12: + if (resultCode == Activity.RESULT_OK) + { /* no need to import twice */ + mImportUserCert.setEnabled(false); + mSelectUserCert.performClick(); + } + } + } + + public void handleProfile(ProfileLoadResult data) + { + mProgress.dismiss(); + + mProfile = null; + if (data != null && data.ThrownException == null) + { + try + { + JSONObject obj = new JSONObject(data.Profile); + mProfile = parseProfile(obj); + } + catch (JSONException e) + { + mExistsWarning.setVisibility(View.VISIBLE); + mExistsWarning.setText(e.getLocalizedMessage()); + mHideImport = true; + invalidateOptionsMenu(); + return; + } + } + if (mProfile == null) + { + String error = null; + if (data.ThrownException != null) + { + try + { + throw data.ThrownException; + } + catch (FileNotFoundException e) + { + error = getString(R.string.profile_import_failed_not_found); + } + catch (UnknownHostException e) + { + error = getString(R.string.profile_import_failed_host); + } + catch (SSLHandshakeException e) + { + error = getString(R.string.profile_import_failed_tls); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + if (error != null) + { + Toast.makeText(this, getString(R.string.profile_import_failed_detail, error), Toast.LENGTH_LONG).show(); + } + else + { + Toast.makeText(this, R.string.profile_import_failed, Toast.LENGTH_LONG).show(); + } + finish(); + return; + } + mExisting = mDataSource.getVpnProfile(mProfile.getUUID()); + mExistsWarning.setVisibility(mExisting != null ? View.VISIBLE : View.GONE); + + mBasicDataGroup.setVisibility(View.VISIBLE); + mName.setText(mProfile.getName()); + mGateway.setText(mProfile.getGateway()); + mSelectVpnType.setText(getResources().getStringArray(R.array.vpn_types)[mProfile.getVpnType().ordinal()]); + + mUsernamePassword.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE); + if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS)) + { + mUsername.setText(mProfile.getUsername()); + if (mProfile.getUsername() != null && !mProfile.getUsername().isEmpty()) + { + mUsername.setEnabled(false); + } + } + + mUserCertificate.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE); + mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE); + mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE); + + updateUserCertView(); + + if (mProfile.Certificate != null) + { + try + { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(mProfile.Certificate)); + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + String alias = store.getCertificateAlias(certificate); + mCertEntry = new TrustedCertificateEntry(alias, certificate); + ((TextView)mRemoteCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary()); + ((TextView)mRemoteCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary()); + } + catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) + { + e.printStackTrace(); + mRemoteCertificate.setVisibility(View.GONE); + } + } + } + + private void handleUserCertificate(TrustedCertificateEntry data) + { + mUserCertEntry = data; + mUserCertLoading = null; + updateUserCertView(); + } + + private void updateUserCertView() + { + if (mUserCertLoading != null) + { + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading); + ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading); + } + else if (mUserCertEntry != null) + { /* clear any errors and set the new data */ + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null); + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias()); + ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString()); + } + else + { + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label); + ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate); + } + } + + private ParsedVpnProfile parseProfile(JSONObject obj) throws JSONException + { + UUID uuid; + try + { + uuid = UUID.fromString(obj.getString("uuid")); + } + catch (IllegalArgumentException e) + { + e.printStackTrace(); + return null; + } + ParsedVpnProfile profile = new ParsedVpnProfile(); + + profile.setUUID(uuid); + profile.setName(obj.getString("name")); + VpnType type = VpnType.fromIdentifier(obj.getString("type")); + profile.setVpnType(type); + + JSONObject remote = obj.getJSONObject("remote"); + profile.setGateway(remote.getString("addr")); + profile.setPort(getInteger(remote, "port", 1, 65535)); + profile.setRemoteId(remote.optString("id", null)); + profile.Certificate = decodeBase64(remote.optString("cert", null)); + + JSONObject local = obj.optJSONObject("local"); + if (local != null) + { + if (type.has(VpnTypeFeature.USER_PASS)) + { + profile.setUsername(local.optString("eap_id", null)); + } + + if (type.has(VpnTypeFeature.CERTIFICATE)) + { + profile.setLocalId(local.optString("id", null)); + profile.PKCS12 = decodeBase64(local.optString("p12", null)); + } + } + + profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX)); + JSONObject split = obj.optJSONObject("split-tunneling"); + if (split != null) + { + int st = 0; + st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0; + st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0; + profile.setSplitTunneling(st == 0 ? null : st); + } + return profile; + } + + private Integer getInteger(JSONObject obj, String key, int min, int max) + { + Integer res = obj.optInt(key); + return res < min || res > max ? null : res; + } + + /** + * Save or update the profile depending on whether we actually have a + * profile object or not (this was created in updateProfileData) + */ + private void saveProfile() + { + if (verifyInput()) + { + updateProfileData(); + if (mExisting != null) + { + mProfile.setId(mExisting.getId()); + mDataSource.updateVpnProfile(mProfile); + } + else + { + mDataSource.insertProfile(mProfile); + } + if (mCertEntry != null) + { + try + { /* store the CA/server certificate */ + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + store.setCertificateEntry(null, mCertEntry.getCertificate()); + TrustedCertificateManager.getInstance().reset(); + } + catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) + { + e.printStackTrace(); + } + } + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId()); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + + intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId())); + finish(); + } + } + + /** + * Verify the user input and display error messages. + * @return true if the input is valid + */ + private boolean verifyInput() + { + boolean valid = true; + if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS)) + { + if (mUsername.getText().toString().trim().isEmpty()) + { + mUsernameWrap.setError(getString(R.string.alert_text_no_input_username)); + valid = false; + } + } + if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null) + { /* let's show an error icon */ + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(""); + valid = false; + } + return valid; + } + + /** + * Update the profile object with the data entered by the user + */ + private void updateProfileData() + { + if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS)) + { + mProfile.setUsername(mUsername.getText().toString().trim()); + String password = mPassword.getText().toString().trim(); + password = password.isEmpty() ? null : password; + mProfile.setPassword(password); + } + if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE)) + { + mProfile.setUserCertificateAlias(mUserCertEntry.getAlias()); + } + if (mCertEntry != null) + { + mProfile.setCertificateAlias(mCertEntry.getAlias()); + } + } + + /** + * Load the JSON-encoded VPN profile from the given URI + */ + private static class ProfileLoader extends AsyncTaskLoader + { + private final Uri mUri; + private ProfileLoadResult mData; + + public ProfileLoader(Context context, Uri uri) + { + super(context); + mUri = uri; + } + + @Override + public ProfileLoadResult loadInBackground() + { + ProfileLoadResult result = new ProfileLoadResult(); + InputStream in = null; + + if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) || + ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) + { + try + { + in = getContext().getContentResolver().openInputStream(mUri); + } + catch (FileNotFoundException e) + { + result.ThrownException = e; + } + } + else + { + try + { + URL url = new URL(mUri.toString()); + in = url.openStream(); + } + catch (IOException e) + { + result.ThrownException = e; + } + } + if (in != null) + { + result.Profile = streamToString(in); + } + return result; + } + + @Override + protected void onStartLoading() + { + if (mData != null) + { /* if we have data ready, deliver it directly */ + deliverResult(mData); + } + if (takeContentChanged() || mData == null) + { + forceLoad(); + } + } + + @Override + public void deliverResult(ProfileLoadResult data) + { + if (isReset()) + { + return; + } + mData = data; + if (isStarted()) + { /* if it is started we deliver the data directly, + * otherwise this is handled in onStartLoading */ + super.deliverResult(data); + } + } + + @Override + protected void onReset() + { + mData = null; + super.onReset(); + } + + private String streamToString(InputStream in) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int len; + + try + { + while ((len = in.read(buf)) != -1) + { + out.write(buf, 0, len); + } + return out.toString("UTF-8"); + } + catch (IOException e) + { + e.printStackTrace(); + } + return null; + } + } + + private static class ProfileLoadResult + { + public String Profile; + public Exception ThrownException; + } + + /** + * Ask the user to select an available certificate. + */ + private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback + { + @Override + public void onClick(View v) + { + String alias = null; + if (mUserCertEntry != null) + { + alias = mUserCertEntry.getAlias(); + mUserCertEntry = null; + } + else if (mProfile != null) + { + alias = getString(R.string.profile_cert_alias, mProfile.getName()); + } + KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias); + } + + @Override + public void alias(final String alias) + { + /* alias() is not called from our main thread */ + runOnUiThread(new Runnable() { + @Override + public void run() + { + mUserCertLoading = alias; + updateUserCertView(); + if (alias != null) + { /* otherwise the dialog was canceled, the request denied */ + getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks); + } + } + }); + } + } + + /** + * Load the selected user certificate asynchronously. This cannot be done + * from the main thread as getCertificateChain() calls back to our main + * thread to bind to the KeyChain service resulting in a deadlock. + */ + private static class UserCertificateLoader extends AsyncTaskLoader + { + private final String mAlias; + private TrustedCertificateEntry mData; + + public UserCertificateLoader(Context context, String alias) + { + super(context); + mAlias = alias; + } + + @Override + public TrustedCertificateEntry loadInBackground() + { + X509Certificate[] chain = null; + try + { + chain = KeyChain.getCertificateChain(getContext(), mAlias); + } + catch (KeyChainException | InterruptedException e) + { + e.printStackTrace(); + } + if (chain != null && chain.length > 0) + { + return new TrustedCertificateEntry(mAlias, chain[0]); + } + return null; + } + + @Override + protected void onStartLoading() + { + if (mData != null) + { /* if we have data ready, deliver it directly */ + deliverResult(mData); + } + if (takeContentChanged() || mData == null) + { + forceLoad(); + } + } + + @Override + public void deliverResult(TrustedCertificateEntry data) + { + if (isReset()) + { + return; + } + mData = data; + if (isStarted()) + { /* if it is started we deliver the data directly, + * otherwise this is handled in onStartLoading */ + super.deliverResult(data); + } + } + + @Override + protected void onReset() + { + mData = null; + super.onReset(); + } + } + + private byte[] decodeBase64(String encoded) + { + if (encoded == null || encoded.isEmpty()) + { + return null; + } + byte[] data = null; + try + { + data = Base64.decode(encoded, Base64.DEFAULT); + } + catch (IllegalArgumentException e) + { + e.printStackTrace(); + } + return data; + } + + private class ParsedVpnProfile extends VpnProfile + { + public byte[] Certificate; + public byte[] PKCS12; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java index d8d99ff00..8210d597c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java @@ -17,12 +17,14 @@ package org.strongswan.android.ui; -import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; import android.util.AttributeSet; import android.view.ActionMode; import android.view.LayoutInflater; @@ -41,9 +43,11 @@ import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; import org.strongswan.android.ui.adapter.VpnProfileAdapter; +import org.strongswan.android.utils.Constants; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; public class VpnProfileListFragment extends Fragment @@ -58,11 +62,47 @@ public class VpnProfileListFragment extends Fragment private OnVpnProfileSelectedListener mListener; private boolean mReadOnly; + private BroadcastReceiver mProfilesChanged = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + long id, ids[]; + if ((id = intent.getLongExtra(Constants.VPN_PROFILES_SINGLE, 0)) > 0) + { + VpnProfile profile = mDataSource.getVpnProfile(id); + if (profile != null) + { /* in case this was an edit, we remove it first */ + mVpnProfiles.remove(profile); + mVpnProfiles.add(profile); + mListAdapter.notifyDataSetChanged(); + } + } + else if ((ids = intent.getLongArrayExtra(Constants.VPN_PROFILES_MULTIPLE)) != null) + { + for (long i : ids) + { + Iterator profiles = mVpnProfiles.iterator(); + while (profiles.hasNext()) + { + VpnProfile profile = profiles.next(); + if (profile.getId() == i) + { + profiles.remove(); + break; + } + } + } + mListAdapter.notifyDataSetChanged(); + } + } + }; + /** * The activity containing this fragment should implement this interface */ public interface OnVpnProfileSelectedListener { - public void onVpnProfileSelected(VpnProfile profile); + void onVpnProfileSelected(VpnProfile profile); } @Override @@ -116,6 +156,9 @@ public class VpnProfileListFragment extends Fragment mVpnProfiles = mDataSource.getAllVpnProfiles(); mListAdapter = new VpnProfileAdapter(getActivity(), R.layout.profile_list_item, mVpnProfiles); + + IntentFilter profileChangesFilter = new IntentFilter(Constants.VPN_PROFILES_CHANGED); + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mProfilesChanged, profileChangesFilter); } @Override @@ -123,6 +166,7 @@ public class VpnProfileListFragment extends Fragment { super.onDestroy(); mDataSource.close(); + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mProfilesChanged); } @Override @@ -157,30 +201,6 @@ public class VpnProfileListFragment extends Fragment } } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) - { - switch (requestCode) - { - case ADD_REQUEST: - case EDIT_REQUEST: - if (resultCode != Activity.RESULT_OK) - { - return; - } - long id = data.getLongExtra(VpnProfileDataSource.KEY_ID, 0); - VpnProfile profile = mDataSource.getVpnProfile(id); - if (profile != null) - { /* in case this was an edit, we remove it first */ - mVpnProfiles.remove(profile); - mVpnProfiles.add(profile); - mListAdapter.notifyDataSetChanged(); - } - return; - } - super.onActivityResult(requestCode, resultCode, data); - } - private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() { @Override public void onItemClick(AdapterView a, View v, int position, long id) @@ -213,7 +233,7 @@ public class VpnProfileListFragment extends Fragment MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.profile_list_context, menu); mEditProfile = menu.findItem(R.id.edit_profile); - mSelected = new HashSet(); + mSelected = new HashSet<>(); mode.setTitle(R.string.select_profiles); return true; } @@ -234,17 +254,21 @@ public class VpnProfileListFragment extends Fragment } case R.id.delete_profile: { - ArrayList profiles = new ArrayList(); + ArrayList profiles = new ArrayList<>(); for (int position : mSelected) { profiles.add((VpnProfile)mListView.getItemAtPosition(position)); } - for (VpnProfile profile : profiles) + long ids[] = new long[profiles.size()]; + for (int i = 0; i < profiles.size(); i++) { + VpnProfile profile = profiles.get(i); + ids[i] = profile.getId(); mDataSource.deleteVpnProfile(profile); - mVpnProfiles.remove(profile); } - mListAdapter.notifyDataSetChanged(); + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_MULTIPLE, ids); + LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); Toast.makeText(VpnProfileListFragment.this.getActivity(), R.string.profiles_deleted, Toast.LENGTH_SHORT).show(); break; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java new file mode 100644 index 000000000..413ecae97 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.utils; + +public final class Constants +{ + /** + * Intent action used to notify about changes to the VPN profiles + */ + public static final String VPN_PROFILES_CHANGED = "org.strongswan.android.VPN_PROFILES_CHANGED"; + + /** + * Used in the intent above to notify about edits or inserts of a VPN profile (long) + */ + public static final String VPN_PROFILES_SINGLE = "org.strongswan.android.VPN_PROFILES_SINGLE"; + + /** + * Used in the intent above to notify about the deletion of multiple VPN profiles (array of longs) + */ + public static final String VPN_PROFILES_MULTIPLE = "org.strongswan.android.VPN_PROFILES_MULTIPLE"; + + /** + * Limits for MTU + */ + public static final int MTU_MAX = 1500; + public static final int MTU_MIN = 1280; +} diff --git a/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..ceb1a1eeb Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png differ diff --git a/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..af7f8288d Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png differ diff --git a/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..b7c7ffd0e Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png differ diff --git a/src/frontends/android/app/src/main/res/layout/profile_import_view.xml b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml new file mode 100644 index 000000000..fc06aa5d4 --- /dev/null +++ b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +