]> git.ipfire.org Git - thirdparty/strongswan.git/blob - src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
android: Allow disconnecting via MainActivity but display a confirmation dialog
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / MainActivity.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.app.Service;
22 import android.content.ActivityNotFoundException;
23 import android.content.ComponentName;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.ServiceConnection;
27 import android.net.VpnService;
28 import android.os.AsyncTask;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.os.IBinder;
32 import android.support.v4.app.Fragment;
33 import android.support.v4.app.FragmentManager;
34 import android.support.v4.app.FragmentTransaction;
35 import android.support.v7.app.ActionBar;
36 import android.support.v7.app.AlertDialog;
37 import android.support.v7.app.AppCompatActivity;
38 import android.support.v7.app.AppCompatDialogFragment;
39 import android.view.LayoutInflater;
40 import android.view.Menu;
41 import android.view.MenuItem;
42 import android.view.View;
43 import android.view.Window;
44 import android.widget.EditText;
45 import android.widget.Toast;
46
47 import org.strongswan.android.R;
48 import org.strongswan.android.data.VpnProfile;
49 import org.strongswan.android.data.VpnProfileDataSource;
50 import org.strongswan.android.data.VpnType.VpnTypeFeature;
51 import org.strongswan.android.logic.CharonVpnService;
52 import org.strongswan.android.logic.TrustedCertificateManager;
53 import org.strongswan.android.logic.VpnStateService;
54 import org.strongswan.android.logic.VpnStateService.State;
55 import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
56
57 public class MainActivity extends AppCompatActivity implements OnVpnProfileSelectedListener
58 {
59 public static final String CONTACT_EMAIL = "android@strongswan.org";
60 public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE";
61 public static final String DISCONNECT = "org.strongswan.android.action.DISCONNECT";
62 public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID";
63 /**
64 * Use "bring your own device" (BYOD) features
65 */
66 public static final boolean USE_BYOD = true;
67 private static final int PREPARE_VPN_SERVICE = 0;
68 private static final String PROFILE_NAME = "org.strongswan.android.MainActivity.PROFILE_NAME";
69 private static final String PROFILE_REQUIRES_PASSWORD = "org.strongswan.android.MainActivity.REQUIRES_PASSWORD";
70 private static final String PROFILE_RECONNECT = "org.strongswan.android.MainActivity.RECONNECT";
71 private static final String PROFILE_DISCONNECT = "org.strongswan.android.MainActivity.DISCONNECT";
72 private static final String DIALOG_TAG = "Dialog";
73
74 private Bundle mProfileInfo;
75 private VpnStateService mService;
76 private final ServiceConnection mServiceConnection = new ServiceConnection()
77 {
78 @Override
79 public void onServiceDisconnected(ComponentName name)
80 {
81 mService = null;
82 }
83
84 @Override
85 public void onServiceConnected(ComponentName name, IBinder service)
86 {
87 mService = ((VpnStateService.LocalBinder)service).getService();
88
89 if (START_PROFILE.equals(getIntent().getAction()))
90 {
91 startVpnProfile(getIntent());
92 }
93 else if (DISCONNECT.equals(getIntent().getAction()))
94 {
95 disconnect();
96 }
97 }
98 };
99
100 @Override
101 public void onCreate(Bundle savedInstanceState)
102 {
103 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
104 super.onCreate(savedInstanceState);
105 setContentView(R.layout.main);
106
107 ActionBar bar = getSupportActionBar();
108 bar.setDisplayShowHomeEnabled(true);
109 bar.setDisplayShowTitleEnabled(false);
110 bar.setIcon(R.drawable.ic_launcher);
111
112 this.bindService(new Intent(this, VpnStateService.class),
113 mServiceConnection, Service.BIND_AUTO_CREATE);
114
115 /* load CA certificates in a background task */
116 new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
117 }
118
119 @Override
120 protected void onDestroy()
121 {
122 super.onDestroy();
123 if (mService != null)
124 {
125 this.unbindService(mServiceConnection);
126 }
127 }
128
129 /**
130 * Due to launchMode=singleTop this is called if the Activity already exists
131 */
132 @Override
133 protected void onNewIntent(Intent intent)
134 {
135 super.onNewIntent(intent);
136
137 if (START_PROFILE.equals(intent.getAction()))
138 {
139 startVpnProfile(intent);
140 }
141 else if (DISCONNECT.equals(intent.getAction()))
142 {
143 disconnect();
144 }
145 }
146
147 @Override
148 public boolean onCreateOptionsMenu(Menu menu)
149 {
150 getMenuInflater().inflate(R.menu.main, menu);
151 return true;
152 }
153
154 @Override
155 public boolean onPrepareOptionsMenu(Menu menu)
156 {
157 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
158 {
159 menu.removeItem(R.id.menu_import_profile);
160 }
161 return true;
162 }
163
164 @Override
165 public boolean onOptionsItemSelected(MenuItem item)
166 {
167 switch (item.getItemId())
168 {
169 case R.id.menu_import_profile:
170 Intent intent = new Intent(this, VpnProfileImportActivity.class);
171 startActivity(intent);
172 return true;
173 case R.id.menu_manage_certs:
174 Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
175 startActivity(certIntent);
176 return true;
177 case R.id.menu_show_log:
178 Intent logIntent = new Intent(this, LogActivity.class);
179 startActivity(logIntent);
180 return true;
181 default:
182 return super.onOptionsItemSelected(item);
183 }
184 }
185
186 /**
187 * Prepare the VpnService. If this succeeds the current VPN profile is
188 * started.
189 *
190 * @param profileInfo a bundle containing the information about the profile to be started
191 */
192 protected void prepareVpnService(Bundle profileInfo)
193 {
194 Intent intent;
195 try
196 {
197 intent = VpnService.prepare(this);
198 }
199 catch (IllegalStateException ex)
200 {
201 /* this happens if the always-on VPN feature (Android 4.2+) is activated */
202 VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_during_lockdown);
203 return;
204 }
205 /* store profile info until the user grants us permission */
206 mProfileInfo = profileInfo;
207 if (intent != null)
208 {
209 try
210 {
211 startActivityForResult(intent, PREPARE_VPN_SERVICE);
212 }
213 catch (ActivityNotFoundException ex)
214 {
215 /* it seems some devices, even though they come with Android 4,
216 * don't have the VPN components built into the system image.
217 * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
218 * will not be found then */
219 VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported);
220 }
221 }
222 else
223 { /* user already granted permission to use VpnService */
224 onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
225 }
226 }
227
228 @Override
229 protected void onActivityResult(int requestCode, int resultCode, Intent data)
230 {
231 switch (requestCode)
232 {
233 case PREPARE_VPN_SERVICE:
234 if (resultCode == RESULT_OK && mProfileInfo != null)
235 {
236 Intent intent = new Intent(this, CharonVpnService.class);
237 intent.putExtras(mProfileInfo);
238 this.startService(intent);
239 }
240 break;
241 default:
242 super.onActivityResult(requestCode, resultCode, data);
243 }
244 }
245
246 @Override
247 public void onVpnProfileSelected(VpnProfile profile)
248 {
249 Bundle profileInfo = new Bundle();
250 profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId());
251 profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername());
252 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword());
253 profileInfo.putBoolean(PROFILE_REQUIRES_PASSWORD, profile.getVpnType().has(VpnTypeFeature.USER_PASS));
254 profileInfo.putString(PROFILE_NAME, profile.getName());
255
256 removeFragmentByTag(DIALOG_TAG);
257
258 if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING))
259 {
260 profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId());
261
262 ConfirmationDialog dialog = new ConfirmationDialog();
263 dialog.setArguments(profileInfo);
264 dialog.show(this.getSupportFragmentManager(), DIALOG_TAG);
265 return;
266 }
267 startVpnProfile(profileInfo);
268 }
269
270 /**
271 * Start the given VPN profile asking the user for a password if required.
272 *
273 * @param profileInfo data about the profile
274 */
275 private void startVpnProfile(Bundle profileInfo)
276 {
277 if (profileInfo.getBoolean(PROFILE_REQUIRES_PASSWORD) &&
278 profileInfo.getString(VpnProfileDataSource.KEY_PASSWORD) == null)
279 {
280 LoginDialog login = new LoginDialog();
281 login.setArguments(profileInfo);
282 login.show(getSupportFragmentManager(), DIALOG_TAG);
283 return;
284 }
285 prepareVpnService(profileInfo);
286 }
287
288 /**
289 * Start the VPN profile referred to by the given intent. Displays an error
290 * if the profile doesn't exist.
291 *
292 * @param intent Intent that caused us to start this
293 */
294 private void startVpnProfile(Intent intent)
295 {
296 long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0);
297 if (profileId <= 0)
298 { /* invalid invocation */
299 return;
300 }
301 VpnProfileDataSource dataSource = new VpnProfileDataSource(this);
302 dataSource.open();
303 VpnProfile profile = dataSource.getVpnProfile(profileId);
304 dataSource.close();
305
306 if (profile != null)
307 {
308 onVpnProfileSelected(profile);
309 }
310 else
311 {
312 Toast.makeText(this, R.string.profile_not_found, Toast.LENGTH_LONG).show();
313 }
314 }
315
316 /**
317 * Disconnect the current connection, if any (silently ignored if there is no connection).
318 */
319 private void disconnect()
320 {
321 removeFragmentByTag(DIALOG_TAG);
322
323 if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING))
324 {
325 Bundle args = new Bundle();
326 args.putBoolean(PROFILE_DISCONNECT, true);
327
328 ConfirmationDialog dialog = new ConfirmationDialog();
329 dialog.setArguments(args);
330 dialog.show(this.getSupportFragmentManager(), DIALOG_TAG);
331 return;
332 }
333 }
334
335 /**
336 * Class that loads the cached CA certificates.
337 */
338 private class LoadCertificatesTask extends AsyncTask<Void, Void, TrustedCertificateManager>
339 {
340 @Override
341 protected void onPreExecute()
342 {
343 setProgressBarIndeterminateVisibility(true);
344 }
345
346 @Override
347 protected TrustedCertificateManager doInBackground(Void... params)
348 {
349 return TrustedCertificateManager.getInstance().load();
350 }
351
352 @Override
353 protected void onPostExecute(TrustedCertificateManager result)
354 {
355 setProgressBarIndeterminateVisibility(false);
356 }
357 }
358
359 /**
360 * Dismiss dialog if shown
361 */
362 public void removeFragmentByTag(String tag)
363 {
364 FragmentManager fm = getSupportFragmentManager();
365 Fragment login = fm.findFragmentByTag(tag);
366 if (login != null)
367 {
368 FragmentTransaction ft = fm.beginTransaction();
369 ft.remove(login);
370 ft.commit();
371 }
372 }
373
374 /**
375 * Class that displays a confirmation dialog if a VPN profile is already connected
376 * and then initiates the selected VPN profile if the user confirms the dialog.
377 */
378 public static class ConfirmationDialog extends AppCompatDialogFragment
379 {
380 @Override
381 public Dialog onCreateDialog(Bundle savedInstanceState)
382 {
383 final Bundle profileInfo = getArguments();
384 int icon = android.R.drawable.ic_dialog_alert;
385 int title = R.string.connect_profile_question;
386 int message = R.string.replaces_active_connection;
387 int button = R.string.connect;
388
389 if (profileInfo.getBoolean(PROFILE_RECONNECT))
390 {
391 icon = android.R.drawable.ic_dialog_info;
392 title = R.string.vpn_connected;
393 message = R.string.vpn_profile_connected;
394 button = R.string.reconnect;
395 }
396 else if (profileInfo.getBoolean(PROFILE_DISCONNECT))
397 {
398 title = R.string.disconnect_question;
399 message = R.string.disconnect_active_connection;
400 button = R.string.disconnect;
401 }
402
403 return new AlertDialog.Builder(getActivity())
404 .setIcon(icon)
405 .setTitle(String.format(getString(title), profileInfo.getString(PROFILE_NAME)))
406 .setMessage(message)
407 .setPositiveButton(button, new DialogInterface.OnClickListener()
408 {
409 @Override
410 public void onClick(DialogInterface dialog, int whichButton)
411 {
412 MainActivity activity = (MainActivity)getActivity();
413 if (profileInfo.getBoolean(PROFILE_DISCONNECT))
414 {
415 if (activity.mService != null)
416 {
417 activity.mService.disconnect();
418 }
419 }
420 else
421 {
422 activity.startVpnProfile(profileInfo);
423 }
424 }
425 })
426 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
427 {
428 @Override
429 public void onClick(DialogInterface dialog, int which)
430 {
431 dismiss();
432 }
433 }).create();
434 }
435 }
436
437 /**
438 * Class that displays a login dialog and initiates the selected VPN
439 * profile if the user confirms the dialog.
440 */
441 public static class LoginDialog extends AppCompatDialogFragment
442 {
443 @Override
444 public Dialog onCreateDialog(Bundle savedInstanceState)
445 {
446 final Bundle profileInfo = getArguments();
447 LayoutInflater inflater = getActivity().getLayoutInflater();
448 View view = inflater.inflate(R.layout.login_dialog, null);
449 EditText username = (EditText)view.findViewById(R.id.username);
450 username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME));
451 final EditText password = (EditText)view.findViewById(R.id.password);
452
453 AlertDialog.Builder adb = new AlertDialog.Builder(getActivity());
454 adb.setView(view);
455 adb.setTitle(getString(R.string.login_title));
456 adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener()
457 {
458 @Override
459 public void onClick(DialogInterface dialog, int whichButton)
460 {
461 MainActivity activity = (MainActivity)getActivity();
462 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim());
463 activity.prepareVpnService(profileInfo);
464 }
465 });
466 adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
467 {
468 @Override
469 public void onClick(DialogInterface dialog, int which)
470 {
471 dismiss();
472 }
473 });
474 return adb.create();
475 }
476 }
477
478 /**
479 * Class representing an error message which is displayed if VpnService is
480 * not supported on the current device.
481 */
482 public static class VpnNotSupportedError extends AppCompatDialogFragment
483 {
484 static final String ERROR_MESSAGE_ID = "org.strongswan.android.VpnNotSupportedError.MessageId";
485
486 public static void showWithMessage(AppCompatActivity activity, int messageId)
487 {
488 Bundle bundle = new Bundle();
489 bundle.putInt(ERROR_MESSAGE_ID, messageId);
490 VpnNotSupportedError dialog = new VpnNotSupportedError();
491 dialog.setArguments(bundle);
492 dialog.show(activity.getSupportFragmentManager(), DIALOG_TAG);
493 }
494
495 @Override
496 public Dialog onCreateDialog(Bundle savedInstanceState)
497 {
498 final Bundle arguments = getArguments();
499 final int messageId = arguments.getInt(ERROR_MESSAGE_ID);
500 return new AlertDialog.Builder(getActivity())
501 .setTitle(R.string.vpn_not_supported_title)
502 .setMessage(messageId)
503 .setCancelable(false)
504 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
505 {
506 @Override
507 public void onClick(DialogInterface dialog, int id)
508 {
509 dialog.dismiss();
510 }
511 }).create();
512 }
513 }
514 }