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