2 * Copyright (C) 2012-2020 Tobias Brunner
3 * HSR Hochschule fuer Technik Rapperswil
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the
7 * Free Software Foundation; either version 2 of the License, or (at your
8 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
16 package org
.strongswan
.android
.ui
;
18 import android
.app
.Dialog
;
19 import android
.app
.Service
;
20 import android
.content
.ActivityNotFoundException
;
21 import android
.content
.ComponentName
;
22 import android
.content
.Context
;
23 import android
.content
.DialogInterface
;
24 import android
.content
.Intent
;
25 import android
.content
.ServiceConnection
;
26 import android
.net
.Uri
;
27 import android
.net
.VpnService
;
28 import android
.os
.Build
;
29 import android
.os
.Bundle
;
30 import android
.os
.IBinder
;
31 import android
.os
.PowerManager
;
32 import android
.provider
.Settings
;
33 import android
.view
.LayoutInflater
;
34 import android
.view
.View
;
35 import android
.widget
.EditText
;
36 import android
.widget
.Toast
;
38 import org
.strongswan
.android
.R
;
39 import org
.strongswan
.android
.data
.VpnProfile
;
40 import org
.strongswan
.android
.data
.VpnProfileDataSource
;
41 import org
.strongswan
.android
.data
.VpnType
.VpnTypeFeature
;
42 import org
.strongswan
.android
.logic
.VpnStateService
;
43 import org
.strongswan
.android
.logic
.VpnStateService
.State
;
45 import androidx
.annotation
.NonNull
;
46 import androidx
.appcompat
.app
.AlertDialog
;
47 import androidx
.appcompat
.app
.AppCompatActivity
;
48 import androidx
.appcompat
.app
.AppCompatDialogFragment
;
49 import androidx
.fragment
.app
.Fragment
;
50 import androidx
.fragment
.app
.FragmentManager
;
51 import androidx
.fragment
.app
.FragmentTransaction
;
53 public class VpnProfileControlActivity
extends AppCompatActivity
55 public static final String START_PROFILE
= "org.strongswan.android.action.START_PROFILE";
56 public static final String DISCONNECT
= "org.strongswan.android.action.DISCONNECT";
57 public static final String EXTRA_VPN_PROFILE_ID
= "org.strongswan.android.VPN_PROFILE_ID";
59 private static final int PREPARE_VPN_SERVICE
= 0;
60 private static final int ADD_TO_POWER_WHITELIST
= 1;
61 private static final String WAITING_FOR_RESULT
= "WAITING_FOR_RESULT";
62 private static final String PROFILE_NAME
= "PROFILE_NAME";
63 private static final String PROFILE_REQUIRES_PASSWORD
= "REQUIRES_PASSWORD";
64 private static final String PROFILE_RECONNECT
= "RECONNECT";
65 private static final String PROFILE_DISCONNECT
= "DISCONNECT";
66 private static final String DIALOG_TAG
= "Dialog";
68 private Bundle mProfileInfo
;
69 private boolean mWaitingForResult
;
70 private VpnStateService mService
;
71 private final ServiceConnection mServiceConnection
= new ServiceConnection()
74 public void onServiceDisconnected(ComponentName name
)
80 public void onServiceConnected(ComponentName name
, IBinder service
)
82 mService
= ((VpnStateService
.LocalBinder
)service
).getService();
88 public void onCreate(Bundle savedInstanceState
)
90 super.onCreate(savedInstanceState
);
92 if (savedInstanceState
!= null)
94 mWaitingForResult
= savedInstanceState
.getBoolean(WAITING_FOR_RESULT
, false);
96 this.bindService(new Intent(this, VpnStateService
.class),
97 mServiceConnection
, Service
.BIND_AUTO_CREATE
);
101 protected void onSaveInstanceState(Bundle outState
)
103 super.onSaveInstanceState(outState
);
104 outState
.putBoolean(WAITING_FOR_RESULT
, mWaitingForResult
);
108 protected void onDestroy()
111 if (mService
!= null)
113 this.unbindService(mServiceConnection
);
118 * Due to launchMode=singleTop this is called if the Activity already exists
121 protected void onNewIntent(Intent intent
)
123 super.onNewIntent(intent
);
125 /* store this intent in case the service is not yet connected or the activity is restarted */
128 if (mService
!= null)
135 * Prepare the VpnService. If this succeeds the current VPN profile is
138 * @param profileInfo a bundle containing the information about the profile to be started
140 protected void prepareVpnService(Bundle profileInfo
)
144 if (mWaitingForResult
)
146 mProfileInfo
= profileInfo
;
152 intent
= VpnService
.prepare(this);
154 catch (IllegalStateException ex
)
156 /* this happens if the always-on VPN feature (Android 4.2+) is activated */
157 VpnNotSupportedError
.showWithMessage(this, R
.string
.vpn_not_supported_during_lockdown
);
160 catch (NullPointerException ex
)
162 /* not sure when this happens exactly, but apparently it does */
163 VpnNotSupportedError
.showWithMessage(this, R
.string
.vpn_not_supported
);
166 /* store profile info until the user grants us permission */
167 mProfileInfo
= profileInfo
;
172 mWaitingForResult
= true;
173 startActivityForResult(intent
, PREPARE_VPN_SERVICE
);
175 catch (ActivityNotFoundException ex
)
177 /* it seems some devices, even though they come with Android 4,
178 * don't have the VPN components built into the system image.
179 * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
180 * will not be found then */
181 VpnNotSupportedError
.showWithMessage(this, R
.string
.vpn_not_supported
);
182 mWaitingForResult
= false;
186 { /* user already granted permission to use VpnService */
187 onActivityResult(PREPARE_VPN_SERVICE
, RESULT_OK
, null);
192 * Check if we are on the system's power whitelist, if necessary, or ask the user
194 * @return true if profile can be initiated immediately
196 private boolean checkPowerWhitelist()
198 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.M
)
200 PowerManager pm
= (PowerManager
)this.getSystemService(Context
.POWER_SERVICE
);
201 if (!pm
.isIgnoringBatteryOptimizations(this.getPackageName()))
203 PowerWhitelistRequired whitelist
= new PowerWhitelistRequired();
204 mWaitingForResult
= true;
205 whitelist
.show(getSupportFragmentManager(), DIALOG_TAG
);
213 protected void onActivityResult(int requestCode
, int resultCode
, Intent data
)
217 case PREPARE_VPN_SERVICE
:
218 mWaitingForResult
= false;
219 if (resultCode
== RESULT_OK
&& mProfileInfo
!= null)
221 if (checkPowerWhitelist())
223 if (mService
!= null)
225 mService
.connect(mProfileInfo
, true);
231 { /* this happens if the always-on VPN feature is activated by a different app or the user declined */
232 if (getSupportFragmentManager().isStateSaved())
233 { /* onActivityResult() might be called when we aren't active anymore e.g. if the
234 * user pressed the home button, if the activity is started again we land here
235 * before onNewIntent() is called */
238 VpnNotSupportedError
.showWithMessage(this, R
.string
.vpn_not_supported_no_permission
);
241 case ADD_TO_POWER_WHITELIST
:
242 mWaitingForResult
= false;
243 if (mProfileInfo
!= null && mService
!= null)
245 mService
.connect(mProfileInfo
, true);
250 super.onActivityResult(requestCode
, resultCode
, data
);
255 * Check if we are currently connected to a VPN connection
257 * @return true if currently connected
259 private boolean isConnected()
261 if (mService
== null)
265 if (mService
.getErrorState() != VpnStateService
.ErrorState
.NO_ERROR
)
266 { /* allow reconnecting (even to a different profile) without confirmation if there is an error */
269 return (mService
.getState() == State
.CONNECTED
|| mService
.getState() == State
.CONNECTING
);
273 * Start the given VPN profile
275 * @param profile VPN profile
277 public void startVpnProfile(VpnProfile profile
)
279 Bundle profileInfo
= new Bundle();
280 profileInfo
.putString(VpnProfileDataSource
.KEY_UUID
, profile
.getUUID().toString());
281 profileInfo
.putString(VpnProfileDataSource
.KEY_USERNAME
, profile
.getUsername());
282 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, profile
.getPassword());
283 profileInfo
.putBoolean(PROFILE_REQUIRES_PASSWORD
, profile
.getVpnType().has(VpnTypeFeature
.USER_PASS
));
284 profileInfo
.putString(PROFILE_NAME
, profile
.getName());
286 removeFragmentByTag(DIALOG_TAG
);
290 profileInfo
.putBoolean(PROFILE_RECONNECT
, mService
.getProfile().getUUID().equals(profile
.getUUID()));
292 ConfirmationDialog dialog
= new ConfirmationDialog();
293 dialog
.setArguments(profileInfo
);
294 dialog
.show(this.getSupportFragmentManager(), DIALOG_TAG
);
297 startVpnProfile(profileInfo
);
301 * Start the given VPN profile asking the user for a password if required.
303 * @param profileInfo data about the profile
305 private void startVpnProfile(Bundle profileInfo
)
307 if (profileInfo
.getBoolean(PROFILE_REQUIRES_PASSWORD
) &&
308 profileInfo
.getString(VpnProfileDataSource
.KEY_PASSWORD
) == null)
310 LoginDialog login
= new LoginDialog();
311 login
.setArguments(profileInfo
);
312 login
.show(getSupportFragmentManager(), DIALOG_TAG
);
315 prepareVpnService(profileInfo
);
319 * Start the VPN profile referred to by the given intent. Displays an error
320 * if the profile doesn't exist.
322 * @param intent Intent that caused us to start this
324 private void startVpnProfile(Intent intent
)
326 VpnProfile profile
= null;
328 VpnProfileDataSource dataSource
= new VpnProfileDataSource(this);
330 String profileUUID
= intent
.getStringExtra(EXTRA_VPN_PROFILE_ID
);
331 if (profileUUID
!= null)
333 profile
= dataSource
.getVpnProfile(profileUUID
);
337 long profileId
= intent
.getLongExtra(EXTRA_VPN_PROFILE_ID
, 0);
340 profile
= dataSource
.getVpnProfile(profileId
);
347 startVpnProfile(profile
);
351 Toast
.makeText(this, R
.string
.profile_not_found
, Toast
.LENGTH_LONG
).show();
357 * Disconnect the current connection, if any (silently ignored if there is no connection).
359 * @param intent Intent that caused us to start this
361 private void disconnect(Intent intent
)
363 VpnProfile profile
= null;
365 removeFragmentByTag(DIALOG_TAG
);
367 String profileUUID
= intent
.getStringExtra(EXTRA_VPN_PROFILE_ID
);
368 if (profileUUID
!= null)
370 VpnProfileDataSource dataSource
= new VpnProfileDataSource(this);
372 profile
= dataSource
.getVpnProfile(profileUUID
);
376 if (mService
!= null)
378 if (mService
.getState() == State
.CONNECTED
||
379 mService
.getState() == State
.CONNECTING
)
381 if (profile
!= null && profile
.equals(mService
.getProfile()))
382 { /* allow explicit termination without confirmation */
383 mService
.disconnect();
387 Bundle args
= new Bundle();
388 args
.putBoolean(PROFILE_DISCONNECT
, true);
390 ConfirmationDialog dialog
= new ConfirmationDialog();
391 dialog
.setArguments(args
);
392 dialog
.show(this.getSupportFragmentManager(), DIALOG_TAG
);
402 * Handle the Intent of this Activity depending on its action
404 private void handleIntent()
406 Intent intent
= getIntent();
408 if (START_PROFILE
.equals(intent
.getAction()))
410 startVpnProfile(intent
);
412 else if (DISCONNECT
.equals(intent
.getAction()))
419 * Dismiss dialog if shown
421 public void removeFragmentByTag(String tag
)
423 FragmentManager fm
= getSupportFragmentManager();
424 Fragment login
= fm
.findFragmentByTag(tag
);
427 FragmentTransaction ft
= fm
.beginTransaction();
434 * Class that displays a confirmation dialog if a VPN profile is already connected
435 * and then initiates the selected VPN profile if the user confirms the dialog.
437 public static class ConfirmationDialog
extends AppCompatDialogFragment
440 public Dialog
onCreateDialog(Bundle savedInstanceState
)
442 final Bundle profileInfo
= getArguments();
443 int icon
= android
.R
.drawable
.ic_dialog_alert
;
444 int title
= R
.string
.connect_profile_question
;
445 int message
= R
.string
.replaces_active_connection
;
446 int button
= R
.string
.connect
;
448 if (profileInfo
.getBoolean(PROFILE_RECONNECT
))
450 icon
= android
.R
.drawable
.ic_dialog_info
;
451 title
= R
.string
.vpn_connected
;
452 message
= R
.string
.vpn_profile_connected
;
453 button
= R
.string
.reconnect
;
455 else if (profileInfo
.getBoolean(PROFILE_DISCONNECT
))
457 title
= R
.string
.disconnect_question
;
458 message
= R
.string
.disconnect_active_connection
;
459 button
= R
.string
.disconnect
;
462 DialogInterface
.OnClickListener connectListener
= new DialogInterface
.OnClickListener()
465 public void onClick(DialogInterface dialog
, int which
)
467 VpnProfileControlActivity activity
= (VpnProfileControlActivity
)getActivity();
468 activity
.startVpnProfile(profileInfo
);
471 DialogInterface
.OnClickListener disconnectListener
= new DialogInterface
.OnClickListener()
474 public void onClick(DialogInterface dialog
, int which
)
476 VpnProfileControlActivity activity
= (VpnProfileControlActivity
)getActivity();
477 if (activity
.mService
!= null)
479 activity
.mService
.disconnect();
484 DialogInterface
.OnClickListener cancelListener
= new DialogInterface
.OnClickListener()
487 public void onClick(DialogInterface dialog
, int which
)
489 getActivity().finish();
493 AlertDialog
.Builder builder
= new AlertDialog
.Builder(getActivity())
495 .setTitle(String
.format(getString(title
), profileInfo
.getString(PROFILE_NAME
)))
496 .setMessage(message
);
498 if (profileInfo
.getBoolean(PROFILE_DISCONNECT
))
500 builder
.setPositiveButton(button
, disconnectListener
);
504 builder
.setPositiveButton(button
, connectListener
);
507 if (profileInfo
.getBoolean(PROFILE_RECONNECT
))
509 builder
.setNegativeButton(R
.string
.disconnect
, disconnectListener
);
510 builder
.setNeutralButton(android
.R
.string
.cancel
, cancelListener
);
514 builder
.setNegativeButton(android
.R
.string
.cancel
, cancelListener
);
516 return builder
.create();
520 public void onCancel(DialogInterface dialog
)
522 getActivity().finish();
527 * Class that displays a login dialog and initiates the selected VPN
528 * profile if the user confirms the dialog.
530 public static class LoginDialog
extends AppCompatDialogFragment
533 public Dialog
onCreateDialog(Bundle savedInstanceState
)
535 final Bundle profileInfo
= getArguments();
536 LayoutInflater inflater
= getActivity().getLayoutInflater();
537 View view
= inflater
.inflate(R
.layout
.login_dialog
, null);
538 EditText username
= (EditText
)view
.findViewById(R
.id
.username
);
539 username
.setText(profileInfo
.getString(VpnProfileDataSource
.KEY_USERNAME
));
540 final EditText password
= (EditText
)view
.findViewById(R
.id
.password
);
542 AlertDialog
.Builder adb
= new AlertDialog
.Builder(getActivity());
544 adb
.setTitle(getString(R
.string
.login_title
));
545 adb
.setPositiveButton(R
.string
.login_confirm
, new DialogInterface
.OnClickListener()
548 public void onClick(DialogInterface dialog
, int whichButton
)
550 VpnProfileControlActivity activity
= (VpnProfileControlActivity
)getActivity();
551 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, password
.getText().toString().trim());
552 activity
.prepareVpnService(profileInfo
);
555 adb
.setNegativeButton(android
.R
.string
.cancel
, new DialogInterface
.OnClickListener()
558 public void onClick(DialogInterface dialog
, int which
)
560 getActivity().finish();
567 public void onCancel(DialogInterface dialog
)
569 getActivity().finish();
574 * Class that displays a warning before asking the user to add the app to the
575 * device's power whitelist.
577 public static class PowerWhitelistRequired
extends AppCompatDialogFragment
580 public Dialog
onCreateDialog(Bundle savedInstanceState
)
582 return new AlertDialog
.Builder(getActivity())
583 .setTitle(R
.string
.power_whitelist_title
)
584 .setMessage(R
.string
.power_whitelist_text
)
585 .setPositiveButton(android
.R
.string
.ok
, (dialog
, id
) -> {
586 Intent intent
= new Intent(Settings
.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
,
587 Uri
.parse("package:" + getActivity().getPackageName()));
588 getActivity().startActivityForResult(intent
, ADD_TO_POWER_WHITELIST
);
593 public void onCancel(@NonNull DialogInterface dialog
)
595 getActivity().finish();
600 * Class representing an error message which is displayed if VpnService is
601 * not supported on the current device.
603 public static class VpnNotSupportedError
extends AppCompatDialogFragment
605 static final String ERROR_MESSAGE_ID
= "org.strongswan.android.VpnNotSupportedError.MessageId";
607 public static void showWithMessage(AppCompatActivity activity
, int messageId
)
609 Bundle bundle
= new Bundle();
610 bundle
.putInt(ERROR_MESSAGE_ID
, messageId
);
611 VpnNotSupportedError dialog
= new VpnNotSupportedError();
612 dialog
.setArguments(bundle
);
613 dialog
.show(activity
.getSupportFragmentManager(), DIALOG_TAG
);
617 public Dialog
onCreateDialog(Bundle savedInstanceState
)
619 final Bundle arguments
= getArguments();
620 final int messageId
= arguments
.getInt(ERROR_MESSAGE_ID
);
621 return new AlertDialog
.Builder(getActivity())
622 .setTitle(R
.string
.vpn_not_supported_title
)
623 .setMessage(messageId
)
624 .setPositiveButton(android
.R
.string
.ok
, new DialogInterface
.OnClickListener()
627 public void onClick(DialogInterface dialog
, int id
)
629 getActivity().finish();
635 public void onCancel(DialogInterface dialog
)
637 getActivity().finish();