Merge branch 'android-proposals'
[strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / VpnProfileDetailActivity.java
1 /*
2 * Copyright (C) 2012-2017 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * HSR 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.Build;
26 import android.os.Bundle;
27 import android.security.KeyChain;
28 import android.security.KeyChainAliasCallback;
29 import android.security.KeyChainException;
30 import android.support.v4.content.LocalBroadcastManager;
31 import android.support.v7.app.AlertDialog;
32 import android.support.v7.app.AppCompatActivity;
33 import android.support.v7.app.AppCompatDialogFragment;
34 import android.text.Editable;
35 import android.text.Html;
36 import android.text.SpannableString;
37 import android.text.Spanned;
38 import android.text.TextUtils;
39 import android.text.TextWatcher;
40 import android.text.method.LinkMovementMethod;
41 import android.util.Log;
42 import android.view.Menu;
43 import android.view.MenuInflater;
44 import android.view.MenuItem;
45 import android.view.View;
46 import android.view.View.OnClickListener;
47 import android.view.ViewGroup;
48 import android.widget.AdapterView;
49 import android.widget.AdapterView.OnItemSelectedListener;
50 import android.widget.ArrayAdapter;
51 import android.widget.CheckBox;
52 import android.widget.CompoundButton;
53 import android.widget.CompoundButton.OnCheckedChangeListener;
54 import android.widget.EditText;
55 import android.widget.MultiAutoCompleteTextView;
56 import android.widget.RelativeLayout;
57 import android.widget.Spinner;
58 import android.widget.Switch;
59 import android.widget.TextView;
60
61 import org.strongswan.android.R;
62 import org.strongswan.android.data.VpnProfile;
63 import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
64 import org.strongswan.android.data.VpnProfileDataSource;
65 import org.strongswan.android.data.VpnType;
66 import org.strongswan.android.data.VpnType.VpnTypeFeature;
67 import org.strongswan.android.logic.TrustedCertificateManager;
68 import org.strongswan.android.security.TrustedCertificateEntry;
69 import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter;
70 import org.strongswan.android.ui.widget.TextInputLayoutHelper;
71 import org.strongswan.android.utils.Constants;
72 import org.strongswan.android.utils.IPRangeSet;
73 import org.strongswan.android.utils.Utils;
74
75 import java.security.cert.X509Certificate;
76 import java.util.ArrayList;
77 import java.util.SortedSet;
78 import java.util.TreeSet;
79 import java.util.UUID;
80
81 public class VpnProfileDetailActivity extends AppCompatActivity
82 {
83 private static final int SELECT_TRUSTED_CERTIFICATE = 0;
84 private static final int SELECT_APPLICATIONS = 1;
85
86 private VpnProfileDataSource mDataSource;
87 private Long mId;
88 private TrustedCertificateEntry mCertEntry;
89 private String mUserCertLoading;
90 private CertificateIdentitiesAdapter mSelectUserIdAdapter;
91 private String mSelectedUserId;
92 private TrustedCertificateEntry mUserCertEntry;
93 private VpnType mVpnType = VpnType.IKEV2_EAP;
94 private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
95 private SortedSet<String> mSelectedApps = new TreeSet<>();
96 private VpnProfile mProfile;
97 private MultiAutoCompleteTextView mName;
98 private TextInputLayoutHelper mNameWrap;
99 private EditText mGateway;
100 private TextInputLayoutHelper mGatewayWrap;
101 private Spinner mSelectVpnType;
102 private ViewGroup mUsernamePassword;
103 private EditText mUsername;
104 private TextInputLayoutHelper mUsernameWrap;
105 private EditText mPassword;
106 private ViewGroup mUserCertificate;
107 private RelativeLayout mSelectUserCert;
108 private Spinner mSelectUserId;
109 private CheckBox mCheckAuto;
110 private RelativeLayout mSelectCert;
111 private RelativeLayout mTncNotice;
112 private CheckBox mShowAdvanced;
113 private ViewGroup mAdvancedSettings;
114 private MultiAutoCompleteTextView mRemoteId;
115 private TextInputLayoutHelper mRemoteIdWrap;
116 private EditText mMTU;
117 private TextInputLayoutHelper mMTUWrap;
118 private EditText mPort;
119 private TextInputLayoutHelper mPortWrap;
120 private Switch mCertReq;
121 private EditText mNATKeepalive;
122 private TextInputLayoutHelper mNATKeepaliveWrap;
123 private EditText mIncludedSubnets;
124 private TextInputLayoutHelper mIncludedSubnetsWrap;
125 private EditText mExcludedSubnets;
126 private TextInputLayoutHelper mExcludedSubnetsWrap;
127 private CheckBox mBlockIPv4;
128 private CheckBox mBlockIPv6;
129 private Spinner mSelectSelectedAppsHandling;
130 private RelativeLayout mSelectApps;
131 private TextInputLayoutHelper mIkeProposalWrap;
132 private EditText mIkeProposal;
133 private TextInputLayoutHelper mEspProposalWrap;
134 private EditText mEspProposal;
135
136 @Override
137 public void onCreate(Bundle savedInstanceState)
138 {
139 super.onCreate(savedInstanceState);
140
141 /* the title is set when we load the profile, if any */
142 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
143
144 mDataSource = new VpnProfileDataSource(this);
145 mDataSource.open();
146
147 setContentView(R.layout.profile_detail_view);
148
149 mName = (MultiAutoCompleteTextView)findViewById(R.id.name);
150 mNameWrap = (TextInputLayoutHelper)findViewById(R.id.name_wrap);
151 mGateway = (EditText)findViewById(R.id.gateway);
152 mGatewayWrap = (TextInputLayoutHelper) findViewById(R.id.gateway_wrap);
153 mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
154 mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
155
156 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
157 mUsername = (EditText)findViewById(R.id.username);
158 mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
159 mPassword = (EditText)findViewById(R.id.password);
160
161 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
162 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
163 mSelectUserId = (Spinner)findViewById(R.id.select_user_id);
164
165 mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
166 mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
167
168 mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced);
169 mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings);
170
171 mRemoteId = (MultiAutoCompleteTextView)findViewById(R.id.remote_id);
172 mRemoteIdWrap = (TextInputLayoutHelper) findViewById(R.id.remote_id_wrap);
173 mMTU = (EditText)findViewById(R.id.mtu);
174 mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap);
175 mPort = (EditText)findViewById(R.id.port);
176 mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap);
177 mNATKeepalive = (EditText)findViewById(R.id.nat_keepalive);
178 mNATKeepaliveWrap = (TextInputLayoutHelper) findViewById(R.id.nat_keepalive_wrap);
179 mCertReq = (Switch)findViewById(R.id.cert_req);
180 mIncludedSubnets = (EditText)findViewById(R.id.included_subnets);
181 mIncludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.included_subnets_wrap);
182 mExcludedSubnets = (EditText)findViewById(R.id.excluded_subnets);
183 mExcludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.excluded_subnets_wrap);
184 mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4);
185 mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6);
186
187 mSelectSelectedAppsHandling = (Spinner)findViewById(R.id.apps_handling);
188 mSelectApps = (RelativeLayout)findViewById(R.id.select_applications);
189
190 mIkeProposal = (EditText)findViewById(R.id.ike_proposal);
191 mIkeProposalWrap = (TextInputLayoutHelper)findViewById(R.id.ike_proposal_wrap);
192 mEspProposal = (EditText)findViewById(R.id.esp_proposal);
193 mEspProposalWrap = (TextInputLayoutHelper)findViewById(R.id.esp_proposal_wrap);
194 /* make the link clickable */
195 ((TextView)findViewById(R.id.proposal_intro)).setMovementMethod(LinkMovementMethod.getInstance());
196
197 final SpaceTokenizer spaceTokenizer = new SpaceTokenizer();
198 mName.setTokenizer(spaceTokenizer);
199 mRemoteId.setTokenizer(spaceTokenizer);
200 final ArrayAdapter<String> completeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line);
201 mName.setAdapter(completeAdapter);
202 mRemoteId.setAdapter(completeAdapter);
203
204 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
205 {
206 findViewById(R.id.apps).setVisibility(View.GONE);
207 mSelectSelectedAppsHandling.setVisibility(View.GONE);
208 mSelectApps.setVisibility(View.GONE);
209 }
210
211 mGateway.addTextChangedListener(new TextWatcher() {
212 @Override
213 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
214
215 @Override
216 public void onTextChanged(CharSequence s, int start, int before, int count) {}
217
218 @Override
219 public void afterTextChanged(Editable s)
220 {
221 completeAdapter.clear();
222 completeAdapter.add(mGateway.getText().toString());
223 if (TextUtils.isEmpty(mGateway.getText()))
224 {
225 mNameWrap.setHelperText(getString(R.string.profile_name_hint));
226 mRemoteIdWrap.setHelperText(getString(R.string.profile_remote_id_hint));
227 }
228 else
229 {
230 mNameWrap.setHelperText(String.format(getString(R.string.profile_name_hint_gateway), mGateway.getText()));
231 mRemoteIdWrap.setHelperText(String.format(getString(R.string.profile_remote_id_hint_gateway), mGateway.getText()));
232 }
233 }
234 });
235
236 mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
237 @Override
238 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
239 {
240 mVpnType = VpnType.values()[position];
241 updateCredentialView();
242 }
243
244 @Override
245 public void onNothingSelected(AdapterView<?> parent)
246 { /* should not happen */
247 mVpnType = VpnType.IKEV2_EAP;
248 updateCredentialView();
249 }
250 });
251
252 ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
253 ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
254 mTncNotice.setOnClickListener(new OnClickListener() {
255 @Override
256 public void onClick(View v)
257 {
258 new TncNoticeDialog().show(VpnProfileDetailActivity.this.getSupportFragmentManager(), "TncNotice");
259 }
260 });
261
262 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
263 mSelectUserIdAdapter = new CertificateIdentitiesAdapter(this);
264 mSelectUserId.setAdapter(mSelectUserIdAdapter);
265 mSelectUserId.setOnItemSelectedListener(new OnItemSelectedListener() {
266 @Override
267 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
268 {
269 if (mUserCertEntry != null)
270 { /* we don't store the subject DN as it is in the reverse order and the default anyway */
271 mSelectedUserId = position == 0 ? null : mSelectUserIdAdapter.getItem(position);
272 }
273 }
274
275 @Override
276 public void onNothingSelected(AdapterView<?> parent)
277 {
278 mSelectedUserId = null;
279 }
280 });
281
282 mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
283 @Override
284 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
285 {
286 updateCertificateSelector();
287 }
288 });
289
290 mSelectCert.setOnClickListener(new OnClickListener() {
291 @Override
292 public void onClick(View v)
293 {
294 Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
295 intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
296 startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
297 }
298 });
299
300 mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() {
301 @Override
302 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
303 {
304 updateAdvancedSettings();
305 }
306 });
307
308 mSelectSelectedAppsHandling.setOnItemSelectedListener(new OnItemSelectedListener() {
309 @Override
310 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
311 {
312 mSelectedAppsHandling = SelectedAppsHandling.values()[position];
313 updateAppsSelector();
314 }
315
316 @Override
317 public void onNothingSelected(AdapterView<?> parent)
318 { /* should not happen */
319 mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
320 updateAppsSelector();
321 }
322 });
323
324 mSelectApps.setOnClickListener(new OnClickListener() {
325 @Override
326 public void onClick(View v)
327 {
328 Intent intent = new Intent(VpnProfileDetailActivity.this, SelectedApplicationsActivity.class);
329 intent.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
330 startActivityForResult(intent, SELECT_APPLICATIONS);
331 }
332 });
333
334 mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
335 if (mId == null)
336 {
337 Bundle extras = getIntent().getExtras();
338 mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
339 }
340
341 loadProfileData(savedInstanceState);
342
343 updateCredentialView();
344 updateCertificateSelector();
345 updateAdvancedSettings();
346 updateAppsSelector();
347 }
348
349 @Override
350 protected void onDestroy()
351 {
352 super.onDestroy();
353 mDataSource.close();
354 }
355
356 @Override
357 protected void onSaveInstanceState(Bundle outState)
358 {
359 super.onSaveInstanceState(outState);
360 if (mId != null)
361 {
362 outState.putLong(VpnProfileDataSource.KEY_ID, mId);
363 }
364 if (mUserCertEntry != null)
365 {
366 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
367 }
368 if (mSelectedUserId != null)
369 {
370 outState.putString(VpnProfileDataSource.KEY_LOCAL_ID, mSelectedUserId);
371 }
372 if (mCertEntry != null)
373 {
374 outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
375 }
376 outState.putStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
377 }
378
379 @Override
380 public boolean onCreateOptionsMenu(Menu menu)
381 {
382 MenuInflater inflater = getMenuInflater();
383 inflater.inflate(R.menu.profile_edit, menu);
384 return true;
385 }
386
387 @Override
388 public boolean onOptionsItemSelected(MenuItem item)
389 {
390 switch (item.getItemId())
391 {
392 case android.R.id.home:
393 case R.id.menu_cancel:
394 finish();
395 return true;
396 case R.id.menu_accept:
397 saveProfile();
398 return true;
399 default:
400 return super.onOptionsItemSelected(item);
401 }
402 }
403
404 @Override
405 protected void onActivityResult(int requestCode, int resultCode, Intent data)
406 {
407 switch (requestCode)
408 {
409 case SELECT_TRUSTED_CERTIFICATE:
410 if (resultCode == RESULT_OK)
411 {
412 String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
413 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
414 mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
415 updateCertificateSelector();
416 }
417 break;
418 case SELECT_APPLICATIONS:
419 if (resultCode == RESULT_OK)
420 {
421 ArrayList<String> selection = data.getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
422 mSelectedApps = new TreeSet<>(selection);
423 updateAppsSelector();
424 }
425 break;
426 default:
427 super.onActivityResult(requestCode, resultCode, data);
428 }
429 }
430
431 /**
432 * Update the UI to enter credentials depending on the type of VPN currently selected
433 */
434 private void updateCredentialView()
435 {
436 mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
437 mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
438 mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);
439
440 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
441 {
442 mSelectUserId.setEnabled(false);
443 if (mUserCertLoading != null)
444 {
445 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
446 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
447 }
448 else if (mUserCertEntry != null)
449 { /* clear any errors and set the new data */
450 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
451 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
452 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
453 mSelectUserIdAdapter.setCertificate(mUserCertEntry);
454 mSelectUserId.setSelection(mSelectUserIdAdapter.getPosition(mSelectedUserId));
455 mSelectUserId.setEnabled(true);
456 }
457 else
458 {
459 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
460 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
461 mSelectUserIdAdapter.setCertificate(null);
462 }
463 }
464 }
465
466 /**
467 * Show an alert in case the previously selected certificate is not found anymore
468 * or the user did not select a certificate in the spinner.
469 */
470 private void showCertificateAlert()
471 {
472 AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
473 adb.setTitle(R.string.alert_text_nocertfound_title);
474 adb.setMessage(R.string.alert_text_nocertfound);
475 adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
476 @Override
477 public void onClick(DialogInterface dialog, int id)
478 {
479 dialog.cancel();
480 }
481 });
482 adb.show();
483 }
484
485 /**
486 * Update the CA certificate selection UI depending on whether the
487 * certificate should be automatically selected or not.
488 */
489 private void updateCertificateSelector()
490 {
491 if (!mCheckAuto.isChecked())
492 {
493 mSelectCert.setEnabled(true);
494 mSelectCert.setVisibility(View.VISIBLE);
495
496 if (mCertEntry != null)
497 {
498 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
499 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
500 }
501 else
502 {
503 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
504 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
505 }
506 }
507 else
508 {
509 mSelectCert.setEnabled(false);
510 mSelectCert.setVisibility(View.GONE);
511 }
512 }
513
514 /**
515 * Update the application selection UI
516 */
517 private void updateAppsSelector()
518 {
519 if (mSelectedAppsHandling == SelectedAppsHandling.SELECTED_APPS_DISABLE)
520 {
521 mSelectApps.setEnabled(false);
522 mSelectApps.setVisibility(View.GONE);
523
524 }
525 else
526 {
527 mSelectApps.setEnabled(true);
528 mSelectApps.setVisibility(View.VISIBLE);
529
530 ((TextView)mSelectApps.findViewById(android.R.id.text1)).setText(R.string.profile_select_apps);
531 String selected;
532 switch (mSelectedApps.size())
533 {
534 case 0:
535 selected = getString(R.string.profile_select_no_apps);
536 break;
537 case 1:
538 selected = getString(R.string.profile_select_one_app);
539 break;
540 default:
541 selected = getString(R.string.profile_select_x_apps, mSelectedApps.size());
542 break;
543 }
544 ((TextView)mSelectApps.findViewById(android.R.id.text2)).setText(selected);
545 }
546 }
547
548 /**
549 * Update the advanced settings UI depending on whether any advanced
550 * settings have already been made.
551 */
552 private void updateAdvancedSettings()
553 {
554 boolean show = mShowAdvanced.isChecked();
555 if (!show && mProfile != null)
556 {
557 Integer st = mProfile.getSplitTunneling(), flags = mProfile.getFlags();
558 show = mProfile.getRemoteId() != null || mProfile.getMTU() != null ||
559 mProfile.getPort() != null || mProfile.getNATKeepAlive() != null ||
560 (flags != null && flags != 0) || (st != null && st != 0) ||
561 mProfile.getIncludedSubnets() != null || mProfile.getExcludedSubnets() != null ||
562 mProfile.getSelectedAppsHandling() != SelectedAppsHandling.SELECTED_APPS_DISABLE ||
563 mProfile.getIkeProposal() != null || mProfile.getEspProposal() != null;
564 }
565 mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
566 mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
567 }
568
569 /**
570 * Save or update the profile depending on whether we actually have a
571 * profile object or not (this was created in updateProfileData)
572 */
573 private void saveProfile()
574 {
575 if (verifyInput())
576 {
577 if (mProfile != null)
578 {
579 updateProfileData();
580 if (mProfile.getUUID() == null)
581 {
582 mProfile.setUUID(UUID.randomUUID());
583 }
584 mDataSource.updateVpnProfile(mProfile);
585 }
586 else
587 {
588 mProfile = new VpnProfile();
589 updateProfileData();
590 mDataSource.insertProfile(mProfile);
591 }
592 Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
593 intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
594 LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
595
596 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
597 finish();
598 }
599 }
600
601 /**
602 * Verify the user input and display error messages.
603 * @return true if the input is valid
604 */
605 private boolean verifyInput()
606 {
607 boolean valid = true;
608 if (mGateway.getText().toString().trim().isEmpty())
609 {
610 mGatewayWrap.setError(getString(R.string.alert_text_no_input_gateway));
611 valid = false;
612 }
613 if (mVpnType.has(VpnTypeFeature.USER_PASS))
614 {
615 if (mUsername.getText().toString().trim().isEmpty())
616 {
617 mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
618 valid = false;
619 }
620 }
621 if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
622 { /* let's show an error icon */
623 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
624 valid = false;
625 }
626 if (!mCheckAuto.isChecked() && mCertEntry == null)
627 {
628 showCertificateAlert();
629 valid = false;
630 }
631 if (!validateInteger(mMTU, Constants.MTU_MIN, Constants.MTU_MAX))
632 {
633 mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), Constants.MTU_MIN, Constants.MTU_MAX));
634 valid = false;
635 }
636 if (!validateSubnets(mIncludedSubnets))
637 {
638 mIncludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
639 valid = false;
640 }
641 if (!validateSubnets(mExcludedSubnets))
642 {
643 mExcludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
644 valid = false;
645 }
646 if (!validateInteger(mPort, 1, 65535))
647 {
648 mPortWrap.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535));
649 valid = false;
650 }
651 if (!validateInteger(mNATKeepalive, Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX))
652 {
653 mNATKeepaliveWrap.setError(String.format(getString(R.string.alert_text_out_of_range),
654 Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX));
655 valid = false;
656 }
657 if (!validateProposal(mIkeProposal, true))
658 {
659 mIkeProposalWrap.setError(getString(R.string.alert_text_no_proposal));
660 valid = false;
661 }
662 if (!validateProposal(mEspProposal, false))
663 {
664 mEspProposalWrap.setError(getString(R.string.alert_text_no_proposal));
665 valid = false;
666 }
667 return valid;
668 }
669
670 /**
671 * Update the profile object with the data entered by the user
672 */
673 private void updateProfileData()
674 {
675 /* the name is optional, we default to the gateway if none is given */
676 String name = mName.getText().toString().trim();
677 String gateway = mGateway.getText().toString().trim();
678 mProfile.setName(name.isEmpty() ? gateway : name);
679 mProfile.setGateway(gateway);
680 mProfile.setVpnType(mVpnType);
681 if (mVpnType.has(VpnTypeFeature.USER_PASS))
682 {
683 mProfile.setUsername(mUsername.getText().toString().trim());
684 String password = mPassword.getText().toString().trim();
685 password = password.isEmpty() ? null : password;
686 mProfile.setPassword(password);
687 }
688 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
689 {
690 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
691 mProfile.setLocalId(mSelectedUserId);
692 }
693 String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
694 mProfile.setCertificateAlias(certAlias);
695 String remote_id = mRemoteId.getText().toString().trim();
696 mProfile.setRemoteId(remote_id.isEmpty() ? null : remote_id);
697 mProfile.setMTU(getInteger(mMTU));
698 mProfile.setPort(getInteger(mPort));
699 mProfile.setNATKeepAlive(getInteger(mNATKeepalive));
700 int flags = 0;
701 flags |= !mCertReq.isChecked() ? VpnProfile.FLAGS_SUPPRESS_CERT_REQS : 0;
702 mProfile.setFlags(flags);
703 String included = mIncludedSubnets.getText().toString().trim();
704 mProfile.setIncludedSubnets(included.isEmpty() ? null : included);
705 String excluded = mExcludedSubnets.getText().toString().trim();
706 mProfile.setExcludedSubnets(excluded.isEmpty() ? null : excluded);
707 int st = 0;
708 st |= mBlockIPv4.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
709 st |= mBlockIPv6.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
710 mProfile.setSplitTunneling(st == 0 ? null : st);
711 mProfile.setSelectedAppsHandling(mSelectedAppsHandling);
712 mProfile.setSelectedApps(mSelectedApps);
713 String ike = mIkeProposal.getText().toString().trim();
714 mProfile.setIkeProposal(ike.isEmpty() ? null : ike);
715 String esp = mEspProposal.getText().toString().trim();
716 mProfile.setEspProposal(esp.isEmpty() ? null : esp);
717 }
718
719 /**
720 * Load an existing profile if we got an ID
721 *
722 * @param savedInstanceState previously saved state
723 */
724 private void loadProfileData(Bundle savedInstanceState)
725 {
726 String useralias = null, local_id = null, alias = null;
727 Integer flags = null;
728
729 getSupportActionBar().setTitle(R.string.add_profile);
730 if (mId != null && mId != 0)
731 {
732 mProfile = mDataSource.getVpnProfile(mId);
733 if (mProfile != null)
734 {
735 mName.setText(mProfile.getName());
736 mGateway.setText(mProfile.getGateway());
737 mVpnType = mProfile.getVpnType();
738 mUsername.setText(mProfile.getUsername());
739 mPassword.setText(mProfile.getPassword());
740 mRemoteId.setText(mProfile.getRemoteId());
741 mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
742 mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null);
743 mNATKeepalive.setText(mProfile.getNATKeepAlive() != null ? mProfile.getNATKeepAlive().toString() : null);
744 mIncludedSubnets.setText(mProfile.getIncludedSubnets());
745 mExcludedSubnets.setText(mProfile.getExcludedSubnets());
746 mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0);
747 mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0);
748 mSelectedAppsHandling = mProfile.getSelectedAppsHandling();
749 mSelectedApps = mProfile.getSelectedAppsSet();
750 mIkeProposal.setText(mProfile.getIkeProposal());
751 mEspProposal.setText(mProfile.getEspProposal());
752 flags = mProfile.getFlags();
753 useralias = mProfile.getUserCertificateAlias();
754 local_id = mProfile.getLocalId();
755 alias = mProfile.getCertificateAlias();
756 getSupportActionBar().setTitle(mProfile.getName());
757 }
758 else
759 {
760 Log.e(VpnProfileDetailActivity.class.getSimpleName(),
761 "VPN profile with id " + mId + " not found");
762 finish();
763 }
764 }
765
766 mSelectVpnType.setSelection(mVpnType.ordinal());
767 mCertReq.setChecked(flags == null || (flags & VpnProfile.FLAGS_SUPPRESS_CERT_REQS) == 0);
768
769 /* check if the user selected a user certificate previously */
770 useralias = savedInstanceState == null ? useralias : savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
771 local_id = savedInstanceState == null ? local_id : savedInstanceState.getString(VpnProfileDataSource.KEY_LOCAL_ID);
772 if (useralias != null)
773 {
774 UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
775 mUserCertLoading = useralias;
776 mSelectedUserId = local_id;
777 loader.execute();
778 }
779
780 /* check if the user selected a CA certificate previously */
781 alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
782 mCheckAuto.setChecked(alias == null);
783 if (alias != null)
784 {
785 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
786 if (certificate != null)
787 {
788 mCertEntry = new TrustedCertificateEntry(alias, certificate);
789 }
790 else
791 { /* previously selected certificate is not here anymore */
792 showCertificateAlert();
793 mCertEntry = null;
794 }
795 }
796
797 mSelectSelectedAppsHandling.setSelection(mSelectedAppsHandling.ordinal());
798 if (savedInstanceState != null)
799 {
800 ArrayList<String> selectedApps = savedInstanceState.getStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
801 mSelectedApps = new TreeSet<>(selectedApps);
802 }
803 }
804
805 /**
806 * Get the integer value in the given text box or null if empty
807 *
808 * @param view text box (numeric entry assumed)
809 */
810 private Integer getInteger(EditText view)
811 {
812 String value = view.getText().toString().trim();
813 try
814 {
815 return value.isEmpty() ? null : Integer.valueOf(value);
816 }
817 catch (NumberFormatException e)
818 {
819 return null;
820 }
821 }
822
823 /**
824 * Check that the value in the given text box is a valid integer in the given range
825 *
826 * @param view text box (numeric entry assumed)
827 * @param min minimum value (inclusive)
828 * @param max maximum value (inclusive)
829 */
830 private boolean validateInteger(EditText view, Integer min, Integer max)
831 {
832 String value = view.getText().toString().trim();
833 try
834 {
835 if (value.isEmpty())
836 {
837 return true;
838 }
839 Integer val = Integer.valueOf(value);
840 return min <= val && val <= max;
841 }
842 catch (NumberFormatException e)
843 {
844 return false;
845 }
846 }
847
848 /**
849 * Check that the value in the given text box is a valid list of subnets/ranges
850 *
851 * @param view text box
852 */
853 private boolean validateSubnets(EditText view)
854 {
855 String value = view.getText().toString().trim();
856 return value.isEmpty() || IPRangeSet.fromString(value) != null;
857 }
858
859 /**
860 * Check that the value in the given text box is a valid proposal
861 *
862 * @param view text box
863 */
864 private boolean validateProposal(EditText view, boolean ike)
865 {
866 String value = view.getText().toString().trim();
867 return value.isEmpty() || Utils.isProposalValid(ike, value);
868 }
869
870 private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
871 {
872 @Override
873 public void onClick(View v)
874 {
875 String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
876 KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
877 }
878
879 @Override
880 public void alias(final String alias)
881 {
882 if (alias != null)
883 { /* otherwise the dialog was canceled, the request denied */
884 try
885 {
886 final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
887 /* alias() is not called from our main thread */
888 runOnUiThread(new Runnable() {
889 @Override
890 public void run()
891 {
892 if (chain != null && chain.length > 0)
893 {
894 mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
895 }
896 updateCredentialView();
897 }
898 });
899 }
900 catch (KeyChainException | InterruptedException e)
901 {
902 e.printStackTrace();
903 }
904 }
905 }
906 }
907
908 /**
909 * Load the selected user certificate asynchronously. This cannot be done
910 * from the main thread as getCertificateChain() calls back to our main
911 * thread to bind to the KeyChain service resulting in a deadlock.
912 */
913 private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
914 {
915 private final Context mContext;
916 private final String mAlias;
917
918 public UserCertificateLoader(Context context, String alias)
919 {
920 mContext = context;
921 mAlias = alias;
922 }
923
924 @Override
925 protected X509Certificate doInBackground(Void... params)
926 {
927 X509Certificate[] chain = null;
928 try
929 {
930 chain = KeyChain.getCertificateChain(mContext, mAlias);
931 }
932 catch (KeyChainException | InterruptedException e)
933 {
934 e.printStackTrace();
935 }
936 if (chain != null && chain.length > 0)
937 {
938 return chain[0];
939 }
940 return null;
941 }
942
943 @Override
944 protected void onPostExecute(X509Certificate result)
945 {
946 if (result != null)
947 {
948 mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
949 }
950 else
951 { /* previously selected certificate is not here anymore */
952 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
953 mUserCertEntry = null;
954 }
955 mUserCertLoading = null;
956 updateCredentialView();
957 }
958 }
959
960 /**
961 * Dialog with notification message if EAP-TNC is used.
962 */
963 public static class TncNoticeDialog extends AppCompatDialogFragment
964 {
965 @Override
966 public Dialog onCreateDialog(Bundle savedInstanceState)
967 {
968 return new AlertDialog.Builder(getActivity())
969 .setTitle(R.string.tnc_notice_title)
970 .setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
971 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
972 @Override
973 public void onClick(DialogInterface dialog, int id)
974 {
975 dialog.dismiss();
976 }
977 }).create();
978 }
979 }
980
981 /**
982 * Tokenizer implementation that separates by white-space
983 */
984 public static class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer
985 {
986 @Override
987 public int findTokenStart(CharSequence text, int cursor)
988 {
989 int i = cursor;
990
991 while (i > 0 && !Character.isWhitespace(text.charAt(i - 1)))
992 {
993 i--;
994 }
995 return i;
996 }
997
998 @Override
999 public int findTokenEnd(CharSequence text, int cursor)
1000 {
1001 int i = cursor;
1002 int len = text.length();
1003
1004 while (i < len)
1005 {
1006 if (Character.isWhitespace(text.charAt(i)))
1007 {
1008 return i;
1009 }
1010 else
1011 {
1012 i++;
1013 }
1014 }
1015 return len;
1016 }
1017
1018 @Override
1019 public CharSequence terminateToken(CharSequence text)
1020 {
1021 int i = text.length();
1022
1023 if (i > 0 && Character.isWhitespace(text.charAt(i - 1)))
1024 {
1025 return text;
1026 }
1027 else
1028 {
1029 if (text instanceof Spanned)
1030 {
1031 SpannableString sp = new SpannableString(text + " ");
1032 TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0);
1033 return sp;
1034 }
1035 else
1036 {
1037 return text + " ";
1038 }
1039 }
1040 }
1041 }
1042 }