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
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>.
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
18 package org
.strongswan
.android
.ui
;
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
;
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
;
57 public class MainActivity
extends AppCompatActivity
implements OnVpnProfileSelectedListener
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";
64 * Use "bring your own device" (BYOD) features
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";
74 private Bundle mProfileInfo
;
75 private VpnStateService mService
;
76 private final ServiceConnection mServiceConnection
= new ServiceConnection()
79 public void onServiceDisconnected(ComponentName name
)
85 public void onServiceConnected(ComponentName name
, IBinder service
)
87 mService
= ((VpnStateService
.LocalBinder
)service
).getService();
89 if (START_PROFILE
.equals(getIntent().getAction()))
91 startVpnProfile(getIntent());
93 else if (DISCONNECT
.equals(getIntent().getAction()))
101 public void onCreate(Bundle savedInstanceState
)
103 requestWindowFeature(Window
.FEATURE_INDETERMINATE_PROGRESS
);
104 super.onCreate(savedInstanceState
);
105 setContentView(R
.layout
.main
);
107 ActionBar bar
= getSupportActionBar();
108 bar
.setDisplayShowHomeEnabled(true);
109 bar
.setDisplayShowTitleEnabled(false);
110 bar
.setIcon(R
.drawable
.ic_launcher
);
112 this.bindService(new Intent(this, VpnStateService
.class),
113 mServiceConnection
, Service
.BIND_AUTO_CREATE
);
115 /* load CA certificates in a background task */
116 new LoadCertificatesTask().executeOnExecutor(AsyncTask
.THREAD_POOL_EXECUTOR
);
120 protected void onDestroy()
123 if (mService
!= null)
125 this.unbindService(mServiceConnection
);
130 * Due to launchMode=singleTop this is called if the Activity already exists
133 protected void onNewIntent(Intent intent
)
135 super.onNewIntent(intent
);
137 if (START_PROFILE
.equals(intent
.getAction()))
139 startVpnProfile(intent
);
141 else if (DISCONNECT
.equals(intent
.getAction()))
148 public boolean onCreateOptionsMenu(Menu menu
)
150 getMenuInflater().inflate(R
.menu
.main
, menu
);
155 public boolean onPrepareOptionsMenu(Menu menu
)
157 if (Build
.VERSION
.SDK_INT
< Build
.VERSION_CODES
.KITKAT
)
159 menu
.removeItem(R
.id
.menu_import_profile
);
165 public boolean onOptionsItemSelected(MenuItem item
)
167 switch (item
.getItemId())
169 case R
.id
.menu_import_profile
:
170 Intent intent
= new Intent(this, VpnProfileImportActivity
.class);
171 startActivity(intent
);
173 case R
.id
.menu_manage_certs
:
174 Intent certIntent
= new Intent(this, TrustedCertificatesActivity
.class);
175 startActivity(certIntent
);
177 case R
.id
.menu_show_log
:
178 Intent logIntent
= new Intent(this, LogActivity
.class);
179 startActivity(logIntent
);
182 return super.onOptionsItemSelected(item
);
187 * Prepare the VpnService. If this succeeds the current VPN profile is
190 * @param profileInfo a bundle containing the information about the profile to be started
192 protected void prepareVpnService(Bundle profileInfo
)
197 intent
= VpnService
.prepare(this);
199 catch (IllegalStateException ex
)
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
);
205 /* store profile info until the user grants us permission */
206 mProfileInfo
= profileInfo
;
211 startActivityForResult(intent
, PREPARE_VPN_SERVICE
);
213 catch (ActivityNotFoundException ex
)
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
);
223 { /* user already granted permission to use VpnService */
224 onActivityResult(PREPARE_VPN_SERVICE
, RESULT_OK
, null);
229 protected void onActivityResult(int requestCode
, int resultCode
, Intent data
)
233 case PREPARE_VPN_SERVICE
:
234 if (resultCode
== RESULT_OK
&& mProfileInfo
!= null)
236 Intent intent
= new Intent(this, CharonVpnService
.class);
237 intent
.putExtras(mProfileInfo
);
238 this.startService(intent
);
242 super.onActivityResult(requestCode
, resultCode
, data
);
247 public void onVpnProfileSelected(VpnProfile profile
)
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());
256 removeFragmentByTag(DIALOG_TAG
);
258 if (mService
!= null && (mService
.getState() == State
.CONNECTED
|| mService
.getState() == State
.CONNECTING
))
260 profileInfo
.putBoolean(PROFILE_RECONNECT
, mService
.getProfile().getId() == profile
.getId());
262 ConfirmationDialog dialog
= new ConfirmationDialog();
263 dialog
.setArguments(profileInfo
);
264 dialog
.show(this.getSupportFragmentManager(), DIALOG_TAG
);
267 startVpnProfile(profileInfo
);
271 * Start the given VPN profile asking the user for a password if required.
273 * @param profileInfo data about the profile
275 private void startVpnProfile(Bundle profileInfo
)
277 if (profileInfo
.getBoolean(PROFILE_REQUIRES_PASSWORD
) &&
278 profileInfo
.getString(VpnProfileDataSource
.KEY_PASSWORD
) == null)
280 LoginDialog login
= new LoginDialog();
281 login
.setArguments(profileInfo
);
282 login
.show(getSupportFragmentManager(), DIALOG_TAG
);
285 prepareVpnService(profileInfo
);
289 * Start the VPN profile referred to by the given intent. Displays an error
290 * if the profile doesn't exist.
292 * @param intent Intent that caused us to start this
294 private void startVpnProfile(Intent intent
)
296 long profileId
= intent
.getLongExtra(EXTRA_VPN_PROFILE_ID
, 0);
298 { /* invalid invocation */
301 VpnProfileDataSource dataSource
= new VpnProfileDataSource(this);
303 VpnProfile profile
= dataSource
.getVpnProfile(profileId
);
308 onVpnProfileSelected(profile
);
312 Toast
.makeText(this, R
.string
.profile_not_found
, Toast
.LENGTH_LONG
).show();
317 * Disconnect the current connection, if any (silently ignored if there is no connection).
319 private void disconnect()
321 removeFragmentByTag(DIALOG_TAG
);
323 if (mService
!= null && (mService
.getState() == State
.CONNECTED
|| mService
.getState() == State
.CONNECTING
))
325 Bundle args
= new Bundle();
326 args
.putBoolean(PROFILE_DISCONNECT
, true);
328 ConfirmationDialog dialog
= new ConfirmationDialog();
329 dialog
.setArguments(args
);
330 dialog
.show(this.getSupportFragmentManager(), DIALOG_TAG
);
336 * Class that loads the cached CA certificates.
338 private class LoadCertificatesTask
extends AsyncTask
<Void
, Void
, TrustedCertificateManager
>
341 protected void onPreExecute()
343 setProgressBarIndeterminateVisibility(true);
347 protected TrustedCertificateManager
doInBackground(Void
... params
)
349 return TrustedCertificateManager
.getInstance().load();
353 protected void onPostExecute(TrustedCertificateManager result
)
355 setProgressBarIndeterminateVisibility(false);
360 * Dismiss dialog if shown
362 public void removeFragmentByTag(String tag
)
364 FragmentManager fm
= getSupportFragmentManager();
365 Fragment login
= fm
.findFragmentByTag(tag
);
368 FragmentTransaction ft
= fm
.beginTransaction();
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.
378 public static class ConfirmationDialog
extends AppCompatDialogFragment
381 public Dialog
onCreateDialog(Bundle savedInstanceState
)
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
;
389 if (profileInfo
.getBoolean(PROFILE_RECONNECT
))
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
;
396 else if (profileInfo
.getBoolean(PROFILE_DISCONNECT
))
398 title
= R
.string
.disconnect_question
;
399 message
= R
.string
.disconnect_active_connection
;
400 button
= R
.string
.disconnect
;
403 return new AlertDialog
.Builder(getActivity())
405 .setTitle(String
.format(getString(title
), profileInfo
.getString(PROFILE_NAME
)))
407 .setPositiveButton(button
, new DialogInterface
.OnClickListener()
410 public void onClick(DialogInterface dialog
, int whichButton
)
412 MainActivity activity
= (MainActivity
)getActivity();
413 if (profileInfo
.getBoolean(PROFILE_DISCONNECT
))
415 if (activity
.mService
!= null)
417 activity
.mService
.disconnect();
422 activity
.startVpnProfile(profileInfo
);
426 .setNegativeButton(android
.R
.string
.cancel
, new DialogInterface
.OnClickListener()
429 public void onClick(DialogInterface dialog
, int which
)
438 * Class that displays a login dialog and initiates the selected VPN
439 * profile if the user confirms the dialog.
441 public static class LoginDialog
extends AppCompatDialogFragment
444 public Dialog
onCreateDialog(Bundle savedInstanceState
)
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
);
453 AlertDialog
.Builder adb
= new AlertDialog
.Builder(getActivity());
455 adb
.setTitle(getString(R
.string
.login_title
));
456 adb
.setPositiveButton(R
.string
.login_confirm
, new DialogInterface
.OnClickListener()
459 public void onClick(DialogInterface dialog
, int whichButton
)
461 MainActivity activity
= (MainActivity
)getActivity();
462 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, password
.getText().toString().trim());
463 activity
.prepareVpnService(profileInfo
);
466 adb
.setNegativeButton(android
.R
.string
.cancel
, new DialogInterface
.OnClickListener()
469 public void onClick(DialogInterface dialog
, int which
)
479 * Class representing an error message which is displayed if VpnService is
480 * not supported on the current device.
482 public static class VpnNotSupportedError
extends AppCompatDialogFragment
484 static final String ERROR_MESSAGE_ID
= "org.strongswan.android.VpnNotSupportedError.MessageId";
486 public static void showWithMessage(AppCompatActivity activity
, int messageId
)
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
);
496 public Dialog
onCreateDialog(Bundle savedInstanceState
)
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()
507 public void onClick(DialogInterface dialog
, int id
)