android: Move profile name field to the bottom and use server address as hint
[strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / VpnProfileDetailActivity.java
1 /*
2 * Copyright (C) 2012-2014 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * Hochschule fuer Technik Rapperswil
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation; either version 2 of the License, or (at your
10 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 * for more details.
16 */
17
18 package org.strongswan.android.ui;
19
20 import android.app.Dialog;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.security.KeyChain;
27 import android.security.KeyChainAliasCallback;
28 import android.security.KeyChainException;
29 import android.support.v7.app.AlertDialog;
30 import android.support.v7.app.AppCompatActivity;
31 import android.support.v7.app.AppCompatDialogFragment;
32 import android.text.Editable;
33 import android.text.Html;
34 import android.text.TextUtils;
35 import android.text.TextWatcher;
36 import android.util.Log;
37 import android.view.Menu;
38 import android.view.MenuInflater;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.View.OnClickListener;
42 import android.view.ViewGroup;
43 import android.widget.AdapterView;
44 import android.widget.AdapterView.OnItemSelectedListener;
45 import android.widget.CheckBox;
46 import android.widget.CompoundButton;
47 import android.widget.CompoundButton.OnCheckedChangeListener;
48 import android.widget.EditText;
49 import android.widget.RelativeLayout;
50 import android.widget.Spinner;
51 import android.widget.TextView;
52
53 import org.strongswan.android.R;
54 import org.strongswan.android.data.VpnProfile;
55 import org.strongswan.android.data.VpnProfileDataSource;
56 import org.strongswan.android.data.VpnType;
57 import org.strongswan.android.data.VpnType.VpnTypeFeature;
58 import org.strongswan.android.logic.TrustedCertificateManager;
59 import org.strongswan.android.security.TrustedCertificateEntry;
60
61 import java.security.cert.X509Certificate;
62
63 public class VpnProfileDetailActivity extends AppCompatActivity
64 {
65 private static final int SELECT_TRUSTED_CERTIFICATE = 0;
66 private static final int MTU_MIN = 1280;
67 private static final int MTU_MAX = 1500;
68
69 private VpnProfileDataSource mDataSource;
70 private Long mId;
71 private TrustedCertificateEntry mCertEntry;
72 private String mUserCertLoading;
73 private TrustedCertificateEntry mUserCertEntry;
74 private VpnType mVpnType = VpnType.IKEV2_EAP;
75 private VpnProfile mProfile;
76 private EditText mName;
77 private EditText mGateway;
78 private Spinner mSelectVpnType;
79 private ViewGroup mUsernamePassword;
80 private EditText mUsername;
81 private EditText mPassword;
82 private ViewGroup mUserCertificate;
83 private RelativeLayout mSelectUserCert;
84 private CheckBox mCheckAuto;
85 private RelativeLayout mSelectCert;
86 private RelativeLayout mTncNotice;
87 private CheckBox mShowAdvanced;
88 private ViewGroup mAdvancedSettings;
89 private EditText mMTU;
90 private EditText mPort;
91 private CheckBox mBlockIPv4;
92 private CheckBox mBlockIPv6;
93
94 @Override
95 public void onCreate(Bundle savedInstanceState)
96 {
97 super.onCreate(savedInstanceState);
98
99 /* the title is set when we load the profile, if any */
100 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
101
102 mDataSource = new VpnProfileDataSource(this);
103 mDataSource.open();
104
105 setContentView(R.layout.profile_detail_view);
106
107 mName = (EditText)findViewById(R.id.name);
108 mGateway = (EditText)findViewById(R.id.gateway);
109 mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
110 mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
111
112 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
113 mUsername = (EditText)findViewById(R.id.username);
114 mPassword = (EditText)findViewById(R.id.password);
115
116 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
117 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
118
119 mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
120 mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
121
122 mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced);
123 mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings);
124
125 mMTU = (EditText)findViewById(R.id.mtu);
126 mPort = (EditText)findViewById(R.id.port);
127 mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4);
128 mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6);
129
130 mGateway.addTextChangedListener(new TextWatcher() {
131 @Override
132 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
133
134 @Override
135 public void onTextChanged(CharSequence s, int start, int before, int count) {}
136
137 @Override
138 public void afterTextChanged(Editable s) {
139 if (TextUtils.isEmpty(mGateway.getText()))
140 {
141 mName.setHint(R.string.profile_name_hint);
142 }
143 else
144 {
145 mName.setHint("(" + mGateway.getText() + ")");
146 }
147 }
148 });
149
150 mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
151 @Override
152 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
153 {
154 mVpnType = VpnType.values()[position];
155 updateCredentialView();
156 }
157
158 @Override
159 public void onNothingSelected(AdapterView<?> parent)
160 { /* should not happen */
161 mVpnType = VpnType.IKEV2_EAP;
162 updateCredentialView();
163 }
164 });
165
166 ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
167 ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
168 mTncNotice.setOnClickListener(new OnClickListener() {
169 @Override
170 public void onClick(View v)
171 {
172 new TncNoticeDialog().show(VpnProfileDetailActivity.this.getSupportFragmentManager(), "TncNotice");
173 }
174 });
175
176 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
177
178 mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
179 @Override
180 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
181 {
182 updateCertificateSelector();
183 }
184 });
185
186 mSelectCert.setOnClickListener(new OnClickListener() {
187 @Override
188 public void onClick(View v)
189 {
190 Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
191 intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
192 startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
193 }
194 });
195
196 mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() {
197 @Override
198 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
199 {
200 updateAdvancedSettings();
201 }
202 });
203
204 mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
205 if (mId == null)
206 {
207 Bundle extras = getIntent().getExtras();
208 mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
209 }
210
211 loadProfileData(savedInstanceState);
212
213 updateCredentialView();
214 updateCertificateSelector();
215 updateAdvancedSettings();
216 }
217
218 @Override
219 protected void onDestroy()
220 {
221 super.onDestroy();
222 mDataSource.close();
223 }
224
225 @Override
226 protected void onSaveInstanceState(Bundle outState)
227 {
228 super.onSaveInstanceState(outState);
229 if (mId != null)
230 {
231 outState.putLong(VpnProfileDataSource.KEY_ID, mId);
232 }
233 if (mUserCertEntry != null)
234 {
235 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
236 }
237 if (mCertEntry != null)
238 {
239 outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
240 }
241 }
242
243 @Override
244 public boolean onCreateOptionsMenu(Menu menu)
245 {
246 MenuInflater inflater = getMenuInflater();
247 inflater.inflate(R.menu.profile_edit, menu);
248 return true;
249 }
250
251 @Override
252 public boolean onOptionsItemSelected(MenuItem item)
253 {
254 switch (item.getItemId())
255 {
256 case android.R.id.home:
257 case R.id.menu_cancel:
258 finish();
259 return true;
260 case R.id.menu_accept:
261 saveProfile();
262 return true;
263 default:
264 return super.onOptionsItemSelected(item);
265 }
266 }
267
268 @Override
269 protected void onActivityResult(int requestCode, int resultCode, Intent data)
270 {
271 switch (requestCode)
272 {
273 case SELECT_TRUSTED_CERTIFICATE:
274 if (resultCode == RESULT_OK)
275 {
276 String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
277 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
278 mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
279 updateCertificateSelector();
280 }
281 break;
282 default:
283 super.onActivityResult(requestCode, resultCode, data);
284 }
285 }
286
287 /**
288 * Update the UI to enter credentials depending on the type of VPN currently selected
289 */
290 private void updateCredentialView()
291 {
292 mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
293 mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
294 mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);
295
296 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
297 {
298 if (mUserCertLoading != null)
299 {
300 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
301 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
302 }
303 else if (mUserCertEntry != null)
304 { /* clear any errors and set the new data */
305 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
306 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
307 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
308 }
309 else
310 {
311 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
312 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
313 }
314 }
315 }
316
317 /**
318 * Show an alert in case the previously selected certificate is not found anymore
319 * or the user did not select a certificate in the spinner.
320 */
321 private void showCertificateAlert()
322 {
323 AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
324 adb.setTitle(R.string.alert_text_nocertfound_title);
325 adb.setMessage(R.string.alert_text_nocertfound);
326 adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
327 @Override
328 public void onClick(DialogInterface dialog, int id)
329 {
330 dialog.cancel();
331 }
332 });
333 adb.show();
334 }
335
336 /**
337 * Update the CA certificate selection UI depending on whether the
338 * certificate should be automatically selected or not.
339 */
340 private void updateCertificateSelector()
341 {
342 if (!mCheckAuto.isChecked())
343 {
344 mSelectCert.setEnabled(true);
345 mSelectCert.setVisibility(View.VISIBLE);
346
347 if (mCertEntry != null)
348 {
349 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
350 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
351 }
352 else
353 {
354 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
355 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
356 }
357 }
358 else
359 {
360 mSelectCert.setEnabled(false);
361 mSelectCert.setVisibility(View.GONE);
362 }
363 }
364
365 /**
366 * Update the advanced settings UI depending on whether any advanced
367 * settings have already been made.
368 */
369 private void updateAdvancedSettings()
370 {
371 boolean show = mShowAdvanced.isChecked();
372 if (!show && mProfile != null)
373 {
374 Integer st = mProfile.getSplitTunneling();
375 show = mProfile.getMTU() != null || mProfile.getPort() != null || (st != null && st != 0);
376 }
377 mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
378 mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
379 }
380
381 /**
382 * Save or update the profile depending on whether we actually have a
383 * profile object or not (this was created in updateProfileData)
384 */
385 private void saveProfile()
386 {
387 if (verifyInput())
388 {
389 if (mProfile != null)
390 {
391 updateProfileData();
392 mDataSource.updateVpnProfile(mProfile);
393 }
394 else
395 {
396 mProfile = new VpnProfile();
397 updateProfileData();
398 mDataSource.insertProfile(mProfile);
399 }
400 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
401 finish();
402 }
403 }
404
405 /**
406 * Verify the user input and display error messages.
407 * @return true if the input is valid
408 */
409 private boolean verifyInput()
410 {
411 boolean valid = true;
412 if (mGateway.getText().toString().trim().isEmpty())
413 {
414 mGateway.setError(getString(R.string.alert_text_no_input_gateway));
415 valid = false;
416 }
417 if (mVpnType.has(VpnTypeFeature.USER_PASS))
418 {
419 if (mUsername.getText().toString().trim().isEmpty())
420 {
421 mUsername.setError(getString(R.string.alert_text_no_input_username));
422 valid = false;
423 }
424 }
425 if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
426 { /* let's show an error icon */
427 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
428 valid = false;
429 }
430 if (!mCheckAuto.isChecked() && mCertEntry == null)
431 {
432 showCertificateAlert();
433 valid = false;
434 }
435 Integer mtu = getInteger(mMTU);
436 if (mtu != null && (mtu < MTU_MIN || mtu > MTU_MAX))
437 {
438 mMTU.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX));
439 valid = false;
440 }
441 Integer port = getInteger(mPort);
442 if (port != null && (port < 1 || port > 65535))
443 {
444 mPort.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535));
445 valid = false;
446 }
447 return valid;
448 }
449
450 /**
451 * Update the profile object with the data entered by the user
452 */
453 private void updateProfileData()
454 {
455 /* the name is optional, we default to the gateway if none is given */
456 String name = mName.getText().toString().trim();
457 String gateway = mGateway.getText().toString().trim();
458 mProfile.setName(name.isEmpty() ? gateway : name);
459 mProfile.setGateway(gateway);
460 mProfile.setVpnType(mVpnType);
461 if (mVpnType.has(VpnTypeFeature.USER_PASS))
462 {
463 mProfile.setUsername(mUsername.getText().toString().trim());
464 String password = mPassword.getText().toString().trim();
465 password = password.isEmpty() ? null : password;
466 mProfile.setPassword(password);
467 }
468 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
469 {
470 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
471 }
472 String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
473 mProfile.setCertificateAlias(certAlias);
474 mProfile.setMTU(getInteger(mMTU));
475 mProfile.setPort(getInteger(mPort));
476 int st = 0;
477 st |= mBlockIPv4.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
478 st |= mBlockIPv6.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
479 mProfile.setSplitTunneling(st == 0 ? null : st);
480 }
481
482 /**
483 * Load an existing profile if we got an ID
484 *
485 * @param savedInstanceState previously saved state
486 */
487 private void loadProfileData(Bundle savedInstanceState)
488 {
489 String useralias = null, alias = null;
490
491 getSupportActionBar().setTitle(R.string.add_profile);
492 if (mId != null && mId != 0)
493 {
494 mProfile = mDataSource.getVpnProfile(mId);
495 if (mProfile != null)
496 {
497 mName.setText(mProfile.getName());
498 mGateway.setText(mProfile.getGateway());
499 mVpnType = mProfile.getVpnType();
500 mUsername.setText(mProfile.getUsername());
501 mPassword.setText(mProfile.getPassword());
502 mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
503 mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null);
504 mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0 : false);
505 mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0 : false);
506 useralias = mProfile.getUserCertificateAlias();
507 alias = mProfile.getCertificateAlias();
508 getSupportActionBar().setTitle(mProfile.getName());
509 }
510 else
511 {
512 Log.e(VpnProfileDetailActivity.class.getSimpleName(),
513 "VPN profile with id " + mId + " not found");
514 finish();
515 }
516 }
517
518 mSelectVpnType.setSelection(mVpnType.ordinal());
519
520 /* check if the user selected a user certificate previously */
521 useralias = savedInstanceState == null ? useralias: savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
522 if (useralias != null)
523 {
524 UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
525 mUserCertLoading = useralias;
526 loader.execute();
527 }
528
529 /* check if the user selected a CA certificate previously */
530 alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
531 mCheckAuto.setChecked(alias == null);
532 if (alias != null)
533 {
534 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
535 if (certificate != null)
536 {
537 mCertEntry = new TrustedCertificateEntry(alias, certificate);
538 }
539 else
540 { /* previously selected certificate is not here anymore */
541 showCertificateAlert();
542 mCertEntry = null;
543 }
544 }
545 }
546
547 /**
548 * Get the integer value in the given text box or null if empty
549 *
550 * @param view text box (numeric entry assumed)
551 */
552 private Integer getInteger(EditText view)
553 {
554 String value = view.getText().toString().trim();
555 return value.isEmpty() ? null : Integer.valueOf(value);
556 }
557
558 private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
559 {
560 @Override
561 public void onClick(View v)
562 {
563 String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
564 KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
565 }
566
567 @Override
568 public void alias(final String alias)
569 {
570 if (alias != null)
571 { /* otherwise the dialog was canceled, the request denied */
572 try
573 {
574 final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
575 /* alias() is not called from our main thread */
576 runOnUiThread(new Runnable() {
577 @Override
578 public void run()
579 {
580 if (chain != null && chain.length > 0)
581 {
582 mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
583 }
584 updateCredentialView();
585 }
586 });
587 }
588 catch (KeyChainException e)
589 {
590 e.printStackTrace();
591 }
592 catch (InterruptedException e)
593 {
594 e.printStackTrace();
595 }
596 }
597 }
598 }
599
600 /**
601 * Load the selected user certificate asynchronously. This cannot be done
602 * from the main thread as getCertificateChain() calls back to our main
603 * thread to bind to the KeyChain service resulting in a deadlock.
604 */
605 private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
606 {
607 private final Context mContext;
608 private final String mAlias;
609
610 public UserCertificateLoader(Context context, String alias)
611 {
612 mContext = context;
613 mAlias = alias;
614 }
615
616 @Override
617 protected X509Certificate doInBackground(Void... params)
618 {
619 X509Certificate[] chain = null;
620 try
621 {
622 chain = KeyChain.getCertificateChain(mContext, mAlias);
623 }
624 catch (KeyChainException e)
625 {
626 e.printStackTrace();
627 }
628 catch (InterruptedException e)
629 {
630 e.printStackTrace();
631 }
632 if (chain != null && chain.length > 0)
633 {
634 return chain[0];
635 }
636 return null;
637 }
638
639 @Override
640 protected void onPostExecute(X509Certificate result)
641 {
642 if (result != null)
643 {
644 mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
645 }
646 else
647 { /* previously selected certificate is not here anymore */
648 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
649 mUserCertEntry = null;
650 }
651 mUserCertLoading = null;
652 updateCredentialView();
653 }
654 }
655
656 /**
657 * Dialog with notification message if EAP-TNC is used.
658 */
659 public static class TncNoticeDialog extends AppCompatDialogFragment
660 {
661 @Override
662 public Dialog onCreateDialog(Bundle savedInstanceState)
663 {
664 return new AlertDialog.Builder(getActivity())
665 .setTitle(R.string.tnc_notice_title)
666 .setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
667 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
668 @Override
669 public void onClick(DialogInterface dialog, int id)
670 {
671 dialog.dismiss();
672 }
673 }).create();
674 }
675 }
676 }