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