2 * Copyright (C) 2012 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * 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
.Fragment
;
22 import android
.app
.FragmentManager
;
23 import android
.app
.FragmentTransaction
;
24 import android
.app
.Service
;
25 import android
.content
.ActivityNotFoundException
;
26 import android
.content
.ComponentName
;
27 import android
.content
.DialogInterface
;
28 import android
.content
.Intent
;
29 import android
.content
.ServiceConnection
;
30 import android
.net
.VpnService
;
31 import android
.os
.AsyncTask
;
32 import android
.os
.Bundle
;
33 import android
.os
.IBinder
;
34 import android
.support
.v7
.app
.ActionBar
;
35 import android
.support
.v7
.app
.AlertDialog
;
36 import android
.support
.v7
.app
.AppCompatActivity
;
37 import android
.support
.v7
.app
.AppCompatDialogFragment
;
38 import android
.view
.LayoutInflater
;
39 import android
.view
.Menu
;
40 import android
.view
.MenuItem
;
41 import android
.view
.View
;
42 import android
.view
.Window
;
43 import android
.widget
.EditText
;
44 import android
.widget
.Toast
;
46 import org
.strongswan
.android
.R
;
47 import org
.strongswan
.android
.data
.VpnProfile
;
48 import org
.strongswan
.android
.data
.VpnProfileDataSource
;
49 import org
.strongswan
.android
.data
.VpnType
.VpnTypeFeature
;
50 import org
.strongswan
.android
.logic
.CharonVpnService
;
51 import org
.strongswan
.android
.logic
.TrustedCertificateManager
;
52 import org
.strongswan
.android
.logic
.VpnStateService
;
53 import org
.strongswan
.android
.logic
.VpnStateService
.State
;
54 import org
.strongswan
.android
.ui
.VpnProfileListFragment
.OnVpnProfileSelectedListener
;
56 public class MainActivity
extends AppCompatActivity
implements OnVpnProfileSelectedListener
58 public static final String CONTACT_EMAIL
= "android@strongswan.org";
59 public static final String START_PROFILE
= "org.strongswan.android.action.START_PROFILE";
60 public static final String EXTRA_VPN_PROFILE_ID
= "org.strongswan.android.VPN_PROFILE_ID";
62 * Use "bring your own device" (BYOD) features
64 public static final boolean USE_BYOD
= true
;
65 private static final int PREPARE_VPN_SERVICE
= 0;
66 private static final String PROFILE_NAME
= "org.strongswan.android.MainActivity.PROFILE_NAME";
67 private static final String PROFILE_REQUIRES_PASSWORD
= "org.strongswan.android.MainActivity.REQUIRES_PASSWORD";
68 private static final String PROFILE_RECONNECT
= "org.strongswan.android.MainActivity.RECONNECT";
69 private static final String DIALOG_TAG
= "Dialog";
71 private Bundle mProfileInfo
;
72 private VpnStateService mService
;
73 private final ServiceConnection mServiceConnection
= new ServiceConnection()
76 public void onServiceDisconnected(ComponentName name
)
82 public void onServiceConnected(ComponentName name
, IBinder service
)
84 mService
= ((VpnStateService
.LocalBinder
)service
).getService();
86 if (START_PROFILE
.equals(getIntent().getAction()))
88 startVpnProfile(getIntent());
94 public void onCreate(Bundle savedInstanceState
)
96 requestWindowFeature(Window
.FEATURE_INDETERMINATE_PROGRESS
);
97 super.onCreate(savedInstanceState
);
98 setContentView(R
.layout
.main
);
100 ActionBar bar
= getSupportActionBar();
101 bar
.setDisplayShowHomeEnabled(true
);
102 bar
.setDisplayShowTitleEnabled(false
);
103 bar
.setIcon(R
.drawable
.ic_launcher
);
105 this.bindService(new Intent(this, VpnStateService
.class),
106 mServiceConnection
, Service
.BIND_AUTO_CREATE
);
108 /* load CA certificates in a background task */
109 new LoadCertificatesTask().executeOnExecutor(AsyncTask
.THREAD_POOL_EXECUTOR
);
113 protected void onDestroy()
116 if (mService
!= null
)
118 this.unbindService(mServiceConnection
);
123 * Due to launchMode=singleTop this is called if the Activity already exists
126 protected void onNewIntent(Intent intent
)
128 super.onNewIntent(intent
);
130 if (START_PROFILE
.equals(intent
.getAction()))
132 startVpnProfile(intent
);
137 public boolean onCreateOptionsMenu(Menu menu
)
139 getMenuInflater().inflate(R
.menu
.main
, menu
);
144 public boolean onOptionsItemSelected(MenuItem item
)
146 switch (item
.getItemId())
148 case R
.id
.menu_manage_certs
:
149 Intent certIntent
= new Intent(this, TrustedCertificatesActivity
.class);
150 startActivity(certIntent
);
152 case R
.id
.menu_show_log
:
153 Intent logIntent
= new Intent(this, LogActivity
.class);
154 startActivity(logIntent
);
157 return super.onOptionsItemSelected(item
);
162 * Prepare the VpnService. If this succeeds the current VPN profile is
165 * @param profileInfo a bundle containing the information about the profile to be started
167 protected void prepareVpnService(Bundle profileInfo
)
172 intent
= VpnService
.prepare(this);
174 catch (IllegalStateException ex
)
176 /* this happens if the always-on VPN feature (Android 4.2+) is activated */
177 VpnNotSupportedError
.showWithMessage(this, R
.string
.vpn_not_supported_during_lockdown
);
180 /* store profile info until the user grants us permission */
181 mProfileInfo
= profileInfo
;
186 startActivityForResult(intent
, PREPARE_VPN_SERVICE
);
188 catch (ActivityNotFoundException ex
)
190 /* it seems some devices, even though they come with Android 4,
191 * don't have the VPN components built into the system image.
192 * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
193 * will not be found then */
194 VpnNotSupportedError
.showWithMessage(this, R
.string
.vpn_not_supported
);
198 { /* user already granted permission to use VpnService */
199 onActivityResult(PREPARE_VPN_SERVICE
, RESULT_OK
, null
);
204 protected void onActivityResult(int requestCode
, int resultCode
, Intent data
)
208 case PREPARE_VPN_SERVICE
:
209 if (resultCode
== RESULT_OK
&& mProfileInfo
!= null
)
211 Intent intent
= new Intent(this, CharonVpnService
.class);
212 intent
.putExtras(mProfileInfo
);
213 this.startService(intent
);
217 super.onActivityResult(requestCode
, resultCode
, data
);
222 public void onVpnProfileSelected(VpnProfile profile
)
224 Bundle profileInfo
= new Bundle();
225 profileInfo
.putLong(VpnProfileDataSource
.KEY_ID
, profile
.getId());
226 profileInfo
.putString(VpnProfileDataSource
.KEY_USERNAME
, profile
.getUsername());
227 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, profile
.getPassword());
228 profileInfo
.putBoolean(PROFILE_REQUIRES_PASSWORD
, profile
.getVpnType().has(VpnTypeFeature
.USER_PASS
));
229 profileInfo
.putString(PROFILE_NAME
, profile
.getName());
231 removeFragmentByTag(DIALOG_TAG
);
233 if (mService
!= null
&& mService
.getState() == State
.CONNECTED
)
235 profileInfo
.putBoolean(PROFILE_RECONNECT
, mService
.getProfile().getId() == profile
.getId());
237 ConfirmationDialog dialog
= new ConfirmationDialog();
238 dialog
.setArguments(profileInfo
);
239 dialog
.show(this.getSupportFragmentManager(), DIALOG_TAG
);
242 startVpnProfile(profileInfo
);
246 * Start the given VPN profile asking the user for a password if required.
248 * @param profileInfo data about the profile
250 private void startVpnProfile(Bundle profileInfo
)
252 if (profileInfo
.getBoolean(PROFILE_REQUIRES_PASSWORD
) &&
253 profileInfo
.getString(VpnProfileDataSource
.KEY_PASSWORD
) == null
)
255 LoginDialog login
= new LoginDialog();
256 login
.setArguments(profileInfo
);
257 login
.show(getSupportFragmentManager(), DIALOG_TAG
);
260 prepareVpnService(profileInfo
);
264 * Start the VPN profile referred to by the given intent. Displays an error
265 * if the profile doesn't exist.
267 * @param intent Intent that caused us to start this
269 private void startVpnProfile(Intent intent
)
271 long profileId
= intent
.getLongExtra(EXTRA_VPN_PROFILE_ID
, 0);
273 { /* invalid invocation */
276 VpnProfileDataSource dataSource
= new VpnProfileDataSource(this);
278 VpnProfile profile
= dataSource
.getVpnProfile(profileId
);
283 onVpnProfileSelected(profile
);
287 Toast
.makeText(this, R
.string
.profile_not_found
, Toast
.LENGTH_LONG
).show();
292 * Class that loads the cached CA certificates.
294 private class LoadCertificatesTask
extends AsyncTask
<Void
, Void
, TrustedCertificateManager
>
297 protected void onPreExecute()
299 setProgressBarIndeterminateVisibility(true
);
303 protected TrustedCertificateManager
doInBackground(Void
... params
)
305 return TrustedCertificateManager
.getInstance().load();
309 protected void onPostExecute(TrustedCertificateManager result
)
311 setProgressBarIndeterminateVisibility(false
);
316 * Dismiss dialog if shown
318 public void removeFragmentByTag(String tag
)
320 FragmentManager fm
= getFragmentManager();
321 Fragment login
= fm
.findFragmentByTag(tag
);
324 FragmentTransaction ft
= fm
.beginTransaction();
331 * Class that displays a confirmation dialog if a VPN profile is already connected
332 * and then initiates the selected VPN profile if the user confirms the dialog.
334 public static class ConfirmationDialog
extends AppCompatDialogFragment
337 public Dialog
onCreateDialog(Bundle savedInstanceState
)
339 final Bundle profileInfo
= getArguments();
340 int icon
= android
.R
.drawable
.ic_dialog_alert
;
341 int title
= R
.string
.connect_profile_question
;
342 int message
= R
.string
.replaces_active_connection
;
343 int button
= R
.string
.connect
;
345 if (profileInfo
.getBoolean(PROFILE_RECONNECT
))
347 icon
= android
.R
.drawable
.ic_dialog_info
;
348 title
= R
.string
.vpn_connected
;
349 message
= R
.string
.vpn_profile_connected
;
350 button
= R
.string
.reconnect
;
353 return new AlertDialog
.Builder(getActivity())
355 .setTitle(String
.format(getString(title
), profileInfo
.getString(PROFILE_NAME
)))
357 .setPositiveButton(button
, new DialogInterface
.OnClickListener()
360 public void onClick(DialogInterface dialog
, int whichButton
)
362 MainActivity activity
= (MainActivity
)getActivity();
363 activity
.startVpnProfile(profileInfo
);
366 .setNegativeButton(android
.R
.string
.cancel
, new DialogInterface
.OnClickListener()
369 public void onClick(DialogInterface dialog
, int which
)
378 * Class that displays a login dialog and initiates the selected VPN
379 * profile if the user confirms the dialog.
381 public static class LoginDialog
extends AppCompatDialogFragment
384 public Dialog
onCreateDialog(Bundle savedInstanceState
)
386 final Bundle profileInfo
= getArguments();
387 LayoutInflater inflater
= getActivity().getLayoutInflater();
388 View view
= inflater
.inflate(R
.layout
.login_dialog
, null
);
389 EditText username
= (EditText
)view
.findViewById(R
.id
.username
);
390 username
.setText(profileInfo
.getString(VpnProfileDataSource
.KEY_USERNAME
));
391 final EditText password
= (EditText
)view
.findViewById(R
.id
.password
);
393 AlertDialog
.Builder adb
= new AlertDialog
.Builder(getActivity());
395 adb
.setTitle(getString(R
.string
.login_title
));
396 adb
.setPositiveButton(R
.string
.login_confirm
, new DialogInterface
.OnClickListener()
399 public void onClick(DialogInterface dialog
, int whichButton
)
401 MainActivity activity
= (MainActivity
)getActivity();
402 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, password
.getText().toString().trim());
403 activity
.prepareVpnService(profileInfo
);
406 adb
.setNegativeButton(android
.R
.string
.cancel
, new DialogInterface
.OnClickListener()
409 public void onClick(DialogInterface dialog
, int which
)
419 * Class representing an error message which is displayed if VpnService is
420 * not supported on the current device.
422 public static class VpnNotSupportedError
extends AppCompatDialogFragment
424 static final String ERROR_MESSAGE_ID
= "org.strongswan.android.VpnNotSupportedError.MessageId";
426 public static void showWithMessage(AppCompatActivity activity
, int messageId
)
428 Bundle bundle
= new Bundle();
429 bundle
.putInt(ERROR_MESSAGE_ID
, messageId
);
430 VpnNotSupportedError dialog
= new VpnNotSupportedError();
431 dialog
.setArguments(bundle
);
432 dialog
.show(activity
.getSupportFragmentManager(), DIALOG_TAG
);
436 public Dialog
onCreateDialog(Bundle savedInstanceState
)
438 final Bundle arguments
= getArguments();
439 final int messageId
= arguments
.getInt(ERROR_MESSAGE_ID
);
440 return new AlertDialog
.Builder(getActivity())
441 .setTitle(R
.string
.vpn_not_supported_title
)
442 .setMessage(messageId
)
443 .setCancelable(false
)
444 .setPositiveButton(android
.R
.string
.ok
, new DialogInterface
.OnClickListener()
447 public void onClick(DialogInterface dialog
, int id
)