From: Tobias Brunner Date: Thu, 7 Jun 2018 16:00:16 +0000 (+0200) Subject: android: Use separate activity to control VPN connections X-Git-Tag: 5.7.0dr5~20^2~54 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=58d139dad3fb8bed7c758c68bd3376cfd50fcdca;p=thirdparty%2Fstrongswan.git android: Use separate activity to control VPN connections This way we don't have to open the main window, but only show a dialog if necessary (or nothing in many cases). --- diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index bfea305cd6..fc0fb4f578 100644 --- a/src/frontends/android/app/src/main/AndroidManifest.xml +++ b/src/frontends/android/app/src/main/AndroidManifest.xml @@ -36,10 +36,21 @@ + + + + + + diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java index d573d882d1..33edcf7885 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -50,6 +50,7 @@ import org.strongswan.android.logic.VpnStateService.State; import org.strongswan.android.logic.imc.ImcState; import org.strongswan.android.logic.imc.RemediationInstruction; import org.strongswan.android.ui.MainActivity; +import org.strongswan.android.ui.VpnProfileControlActivity; import org.strongswan.android.utils.IPRange; import org.strongswan.android.utils.IPRangeSet; import org.strongswan.android.utils.SettingsWriter; @@ -397,8 +398,8 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe { if (add_action) { - Intent intent = new Intent(getApplicationContext(), MainActivity.class); - intent.setAction(MainActivity.DISCONNECT); + Intent intent = new Intent(getApplicationContext(), VpnProfileControlActivity.class); + intent.setAction(VpnProfileControlActivity.DISCONNECT); PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); builder.addAction(R.drawable.ic_notification_disconnect, getString(R.string.disconnect), pending); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java index a729362194..5971e1ad49 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Tobias Brunner + * Copyright (C) 2012-2018 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager * HSR Hochschule fuer Technik Rapperswil @@ -18,17 +18,11 @@ package org.strongswan.android.ui; import android.app.Dialog; -import android.app.Service; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; -import android.content.ServiceConnection; -import android.net.VpnService; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.IBinder; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; @@ -37,74 +31,30 @@ import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDialogFragment; import android.text.format.Formatter; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.view.Window; -import android.widget.EditText; import android.widget.Toast; import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; -import org.strongswan.android.data.VpnProfileDataSource; -import org.strongswan.android.data.VpnType.VpnTypeFeature; -import org.strongswan.android.logic.CharonVpnService; import org.strongswan.android.logic.TrustedCertificateManager; -import org.strongswan.android.logic.VpnStateService; -import org.strongswan.android.logic.VpnStateService.State; import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener; import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.UUID; public class MainActivity extends AppCompatActivity implements OnVpnProfileSelectedListener { public static final String CONTACT_EMAIL = "android@strongswan.org"; - public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE"; - public static final String DISCONNECT = "org.strongswan.android.action.DISCONNECT"; - public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID"; public static final String EXTRA_CRL_LIST = "org.strongswan.android.CRL_LIST"; + /** * Use "bring your own device" (BYOD) features */ public static final boolean USE_BYOD = true; - private static final int PREPARE_VPN_SERVICE = 0; - private static final String PROFILE_NAME = "org.strongswan.android.MainActivity.PROFILE_NAME"; - private static final String PROFILE_REQUIRES_PASSWORD = "org.strongswan.android.MainActivity.REQUIRES_PASSWORD"; - private static final String PROFILE_RECONNECT = "org.strongswan.android.MainActivity.RECONNECT"; - private static final String PROFILE_DISCONNECT = "org.strongswan.android.MainActivity.DISCONNECT"; - private static final String PROFILE_FOREGROUND = "org.strongswan.android.MainActivity.PROFILE_FOREGROUND"; - private static final String DIALOG_TAG = "Dialog"; - - private boolean mIsVisible; - private Bundle mProfileInfo; - private VpnStateService mService; - private final ServiceConnection mServiceConnection = new ServiceConnection() - { - @Override - public void onServiceDisconnected(ComponentName name) - { - mService = null; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) - { - mService = ((VpnStateService.LocalBinder)service).getService(); - if (START_PROFILE.equals(getIntent().getAction())) - { - startVpnProfile(getIntent(), false); - } - else if (DISCONNECT.equals(getIntent().getAction())) - { - disconnect(false); - } - } - }; + private static final String DIALOG_TAG = "Dialog"; @Override public void onCreate(Bundle savedInstanceState) @@ -117,55 +67,10 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec bar.setDisplayShowTitleEnabled(false); bar.setIcon(R.drawable.ic_launcher); - this.bindService(new Intent(this, VpnStateService.class), - mServiceConnection, Service.BIND_AUTO_CREATE); - /* load CA certificates in a background task */ new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - @Override - protected void onDestroy() - { - super.onDestroy(); - if (mService != null) - { - this.unbindService(mServiceConnection); - } - } - - @Override - protected void onStart() - { - super.onStart(); - mIsVisible = true; - } - - @Override - protected void onStop() - { - super.onStop(); - mIsVisible = false; - } - - /** - * Due to launchMode=singleTop this is called if the Activity already exists - */ - @Override - protected void onNewIntent(Intent intent) - { - super.onNewIntent(intent); - - if (START_PROFILE.equals(intent.getAction())) - { - startVpnProfile(intent, mIsVisible); - } - else if (DISCONNECT.equals(intent.getAction())) - { - disconnect(mIsVisible); - } - } - @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -208,192 +113,13 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec } } - /** - * Prepare the VpnService. If this succeeds the current VPN profile is - * started. - * - * @param profileInfo a bundle containing the information about the profile to be started - */ - protected void prepareVpnService(Bundle profileInfo) - { - Intent intent; - try - { - intent = VpnService.prepare(this); - } - catch (IllegalStateException ex) - { - /* this happens if the always-on VPN feature (Android 4.2+) is activated */ - VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_during_lockdown); - return; - } - catch (NullPointerException ex) - { - /* not sure when this happens exactly, but apparently it does */ - VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported); - return; - } - /* store profile info until the user grants us permission */ - mProfileInfo = profileInfo; - if (intent != null) - { - try - { - startActivityForResult(intent, PREPARE_VPN_SERVICE); - } - catch (ActivityNotFoundException ex) - { - /* it seems some devices, even though they come with Android 4, - * don't have the VPN components built into the system image. - * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog - * will not be found then */ - VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported); - } - } - else - { /* user already granted permission to use VpnService */ - onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null); - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) - { - switch (requestCode) - { - case PREPARE_VPN_SERVICE: - if (resultCode == RESULT_OK && mProfileInfo != null) - { - Intent intent = new Intent(this, CharonVpnService.class); - intent.putExtras(mProfileInfo); - this.startService(intent); - } - else - { /* this happens if the always-on VPN feature is activated by a different app or the user declined */ - VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_no_permission); - } - break; - default: - super.onActivityResult(requestCode, resultCode, data); - } - } - @Override public void onVpnProfileSelected(VpnProfile profile) { - startVpnProfile(profile, true); - } - - /** - * Start the given VPN profile - * - * @param profile VPN profile - * @param foreground whether this was initiated when the activity was visible - */ - public void startVpnProfile(VpnProfile profile, boolean foreground) - { - Bundle profileInfo = new Bundle(); - profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId()); - profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername()); - profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword()); - profileInfo.putBoolean(PROFILE_REQUIRES_PASSWORD, profile.getVpnType().has(VpnTypeFeature.USER_PASS)); - profileInfo.putString(PROFILE_NAME, profile.getName()); - - removeFragmentByTag(DIALOG_TAG); - - if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING)) - { - profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId()); - profileInfo.putBoolean(PROFILE_FOREGROUND, foreground); - - ConfirmationDialog dialog = new ConfirmationDialog(); - dialog.setArguments(profileInfo); - dialog.show(this.getSupportFragmentManager(), DIALOG_TAG); - return; - } - startVpnProfile(profileInfo); - } - - /** - * Start the given VPN profile asking the user for a password if required. - * - * @param profileInfo data about the profile - */ - private void startVpnProfile(Bundle profileInfo) - { - if (profileInfo.getBoolean(PROFILE_REQUIRES_PASSWORD) && - profileInfo.getString(VpnProfileDataSource.KEY_PASSWORD) == null) - { - LoginDialog login = new LoginDialog(); - login.setArguments(profileInfo); - login.show(getSupportFragmentManager(), DIALOG_TAG); - return; - } - prepareVpnService(profileInfo); - } - - /** - * Start the VPN profile referred to by the given intent. Displays an error - * if the profile doesn't exist. - * - * @param intent Intent that caused us to start this - * @param foreground whether this was initiated when the activity was visible - */ - private void startVpnProfile(Intent intent, boolean foreground) - { - VpnProfile profile = null; - - VpnProfileDataSource dataSource = new VpnProfileDataSource(this); - dataSource.open(); - String profileUUID = intent.getStringExtra(EXTRA_VPN_PROFILE_ID); - if (profileUUID != null) - { - try - { - profile = dataSource.getVpnProfile(UUID.fromString(profileUUID)); - } - catch (Exception e) - { /* invalid UUID */ - e.printStackTrace(); - } - } - else - { - long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0); - if (profileId > 0) - { - profile = dataSource.getVpnProfile(profileId); - } - } - dataSource.close(); - - if (profile != null) - { - startVpnProfile(profile, foreground); - } - else - { - Toast.makeText(this, R.string.profile_not_found, Toast.LENGTH_LONG).show(); - } - } - - /** - * Disconnect the current connection, if any (silently ignored if there is no connection). - */ - private void disconnect(boolean foreground) - { - removeFragmentByTag(DIALOG_TAG); - - if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING)) - { - Bundle args = new Bundle(); - args.putBoolean(PROFILE_DISCONNECT, true); - args.putBoolean(PROFILE_FOREGROUND, foreground); - - ConfirmationDialog dialog = new ConfirmationDialog(); - dialog.setArguments(args); - dialog.show(this.getSupportFragmentManager(), DIALOG_TAG); - } + Intent intent = new Intent(this, VpnProfileControlActivity.class); + intent.setAction(VpnProfileControlActivity.START_PROFILE); + intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString()); + startActivity(intent); } /** @@ -453,175 +179,6 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec } } - /** - * Class that displays a confirmation dialog if a VPN profile is already connected - * and then initiates the selected VPN profile if the user confirms the dialog. - */ - public static class ConfirmationDialog extends AppCompatDialogFragment - { - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - final Bundle profileInfo = getArguments(); - int icon = android.R.drawable.ic_dialog_alert; - int title = R.string.connect_profile_question; - int message = R.string.replaces_active_connection; - int button = R.string.connect; - - if (profileInfo.getBoolean(PROFILE_RECONNECT)) - { - icon = android.R.drawable.ic_dialog_info; - title = R.string.vpn_connected; - message = R.string.vpn_profile_connected; - button = R.string.reconnect; - } - else if (profileInfo.getBoolean(PROFILE_DISCONNECT)) - { - title = R.string.disconnect_question; - message = R.string.disconnect_active_connection; - button = R.string.disconnect; - } - - DialogInterface.OnClickListener connectListener = new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - MainActivity activity = (MainActivity)getActivity(); - activity.startVpnProfile(profileInfo); - } - }; - DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - MainActivity activity = (MainActivity)getActivity(); - if (activity.mService != null) - { - activity.mService.disconnect(); - } - } - }; - DialogInterface.OnClickListener cancelListener = new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - dismiss(); - if (!profileInfo.getBoolean(PROFILE_FOREGROUND)) - { /* if the app was not in the foreground before this action was triggered - * externally, we just close the activity if canceled */ - getActivity().finish(); - } - } - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setIcon(icon) - .setTitle(String.format(getString(title), profileInfo.getString(PROFILE_NAME))) - .setMessage(message); - - if (profileInfo.getBoolean(PROFILE_DISCONNECT)) - { - builder.setPositiveButton(button, disconnectListener); - } - else - { - builder.setPositiveButton(button, connectListener); - } - - if (profileInfo.getBoolean(PROFILE_RECONNECT)) - { - builder.setNegativeButton(R.string.disconnect, disconnectListener); - builder.setNeutralButton(android.R.string.cancel, cancelListener); - } - else - { - builder.setNegativeButton(android.R.string.cancel, cancelListener); - } - return builder.create(); - } - } - - /** - * Class that displays a login dialog and initiates the selected VPN - * profile if the user confirms the dialog. - */ - public static class LoginDialog extends AppCompatDialogFragment - { - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - final Bundle profileInfo = getArguments(); - LayoutInflater inflater = getActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.login_dialog, null); - EditText username = (EditText)view.findViewById(R.id.username); - username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME)); - final EditText password = (EditText)view.findViewById(R.id.password); - - AlertDialog.Builder adb = new AlertDialog.Builder(getActivity()); - adb.setView(view); - adb.setTitle(getString(R.string.login_title)); - adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int whichButton) - { - MainActivity activity = (MainActivity)getActivity(); - profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim()); - activity.prepareVpnService(profileInfo); - } - }); - adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - dismiss(); - } - }); - return adb.create(); - } - } - - /** - * Class representing an error message which is displayed if VpnService is - * not supported on the current device. - */ - public static class VpnNotSupportedError extends AppCompatDialogFragment - { - static final String ERROR_MESSAGE_ID = "org.strongswan.android.VpnNotSupportedError.MessageId"; - - public static void showWithMessage(AppCompatActivity activity, int messageId) - { - Bundle bundle = new Bundle(); - bundle.putInt(ERROR_MESSAGE_ID, messageId); - VpnNotSupportedError dialog = new VpnNotSupportedError(); - dialog.setArguments(bundle); - dialog.show(activity.getSupportFragmentManager(), DIALOG_TAG); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - final Bundle arguments = getArguments(); - final int messageId = arguments.getInt(ERROR_MESSAGE_ID); - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.vpn_not_supported_title) - .setMessage(messageId) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int id) - { - dialog.dismiss(); - } - }).create(); - } - } - /** * Confirmation dialog to clear CRL cache */ diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java new file mode 100644 index 0000000000..b8e74475c0 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2012-2018 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.ui; + +import android.app.Dialog; +import android.app.Service; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.VpnService; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.AppCompatDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnType.VpnTypeFeature; +import org.strongswan.android.logic.CharonVpnService; +import org.strongswan.android.logic.VpnStateService; +import org.strongswan.android.logic.VpnStateService.State; + +import java.util.UUID; + +public class VpnProfileControlActivity extends AppCompatActivity +{ + public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE"; + public static final String DISCONNECT = "org.strongswan.android.action.DISCONNECT"; + public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID"; + + private static final int PREPARE_VPN_SERVICE = 0; + private static final String PROFILE_NAME = "PROFILE_NAME"; + private static final String PROFILE_REQUIRES_PASSWORD = "REQUIRES_PASSWORD"; + private static final String PROFILE_RECONNECT = "RECONNECT"; + private static final String PROFILE_DISCONNECT = "DISCONNECT"; + private static final String DIALOG_TAG = "Dialog"; + + private Bundle mProfileInfo; + private VpnStateService mService; + private final ServiceConnection mServiceConnection = new ServiceConnection() + { + @Override + public void onServiceDisconnected(ComponentName name) + { + mService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + mService = ((VpnStateService.LocalBinder)service).getService(); + + if (START_PROFILE.equals(getIntent().getAction())) + { + startVpnProfile(getIntent()); + } + else if (DISCONNECT.equals(getIntent().getAction())) + { + disconnect(); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + this.bindService(new Intent(this, VpnStateService.class), + mServiceConnection, Service.BIND_AUTO_CREATE); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + if (mService != null) + { + this.unbindService(mServiceConnection); + } + } + + /** + * Due to launchMode=singleTop this is called if the Activity already exists + */ + @Override + protected void onNewIntent(Intent intent) + { + super.onNewIntent(intent); + + if (START_PROFILE.equals(intent.getAction())) + { + startVpnProfile(intent); + } + else if (DISCONNECT.equals(intent.getAction())) + { + disconnect(); + } + } + + /** + * Prepare the VpnService. If this succeeds the current VPN profile is + * started. + * + * @param profileInfo a bundle containing the information about the profile to be started + */ + protected void prepareVpnService(Bundle profileInfo) + { + Intent intent; + try + { + intent = VpnService.prepare(this); + } + catch (IllegalStateException ex) + { + /* this happens if the always-on VPN feature (Android 4.2+) is activated */ + VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_during_lockdown); + return; + } + catch (NullPointerException ex) + { + /* not sure when this happens exactly, but apparently it does */ + VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported); + return; + } + /* store profile info until the user grants us permission */ + mProfileInfo = profileInfo; + if (intent != null) + { + try + { + startActivityForResult(intent, PREPARE_VPN_SERVICE); + } + catch (ActivityNotFoundException ex) + { + /* it seems some devices, even though they come with Android 4, + * don't have the VPN components built into the system image. + * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog + * will not be found then */ + VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported); + } + } + else + { /* user already granted permission to use VpnService */ + onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + switch (requestCode) + { + case PREPARE_VPN_SERVICE: + if (resultCode == RESULT_OK && mProfileInfo != null) + { + Intent intent = new Intent(this, CharonVpnService.class); + intent.putExtras(mProfileInfo); + this.startService(intent); + finish(); + } + else + { /* this happens if the always-on VPN feature is activated by a different app or the user declined */ + VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_no_permission); + } + break; + default: + super.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * Start the given VPN profile + * + * @param profile VPN profile + */ + public void startVpnProfile(VpnProfile profile) + { + Bundle profileInfo = new Bundle(); + profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId()); + profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername()); + profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword()); + profileInfo.putBoolean(PROFILE_REQUIRES_PASSWORD, profile.getVpnType().has(VpnTypeFeature.USER_PASS)); + profileInfo.putString(PROFILE_NAME, profile.getName()); + + removeFragmentByTag(DIALOG_TAG); + + if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING)) + { + profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId()); + + ConfirmationDialog dialog = new ConfirmationDialog(); + dialog.setArguments(profileInfo); + dialog.show(this.getSupportFragmentManager(), DIALOG_TAG); + return; + } + startVpnProfile(profileInfo); + } + + /** + * Start the given VPN profile asking the user for a password if required. + * + * @param profileInfo data about the profile + */ + private void startVpnProfile(Bundle profileInfo) + { + if (profileInfo.getBoolean(PROFILE_REQUIRES_PASSWORD) && + profileInfo.getString(VpnProfileDataSource.KEY_PASSWORD) == null) + { + LoginDialog login = new LoginDialog(); + login.setArguments(profileInfo); + login.show(getSupportFragmentManager(), DIALOG_TAG); + return; + } + prepareVpnService(profileInfo); + } + + /** + * Start the VPN profile referred to by the given intent. Displays an error + * if the profile doesn't exist. + * + * @param intent Intent that caused us to start this + */ + private void startVpnProfile(Intent intent) + { + VpnProfile profile = null; + + VpnProfileDataSource dataSource = new VpnProfileDataSource(this); + dataSource.open(); + String profileUUID = intent.getStringExtra(EXTRA_VPN_PROFILE_ID); + if (profileUUID != null) + { + try + { + profile = dataSource.getVpnProfile(UUID.fromString(profileUUID)); + } + catch (Exception e) + { /* invalid UUID */ + e.printStackTrace(); + } + } + else + { + long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0); + if (profileId > 0) + { + profile = dataSource.getVpnProfile(profileId); + } + } + dataSource.close(); + + if (profile != null) + { + startVpnProfile(profile); + } + else + { + Toast.makeText(this, R.string.profile_not_found, Toast.LENGTH_LONG).show(); + finish(); + } + } + + /** + * Disconnect the current connection, if any (silently ignored if there is no connection). + */ + private void disconnect() + { + removeFragmentByTag(DIALOG_TAG); + + if (mService != null) + { + if (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING) + { + Bundle args = new Bundle(); + args.putBoolean(PROFILE_DISCONNECT, true); + + ConfirmationDialog dialog = new ConfirmationDialog(); + dialog.setArguments(args); + dialog.show(this.getSupportFragmentManager(), DIALOG_TAG); + } + else + { + finish(); + } + } + } + + /** + * Dismiss dialog if shown + */ + public void removeFragmentByTag(String tag) + { + FragmentManager fm = getSupportFragmentManager(); + Fragment login = fm.findFragmentByTag(tag); + if (login != null) + { + FragmentTransaction ft = fm.beginTransaction(); + ft.remove(login); + ft.commit(); + } + } + + /** + * Class that displays a confirmation dialog if a VPN profile is already connected + * and then initiates the selected VPN profile if the user confirms the dialog. + */ + public static class ConfirmationDialog extends AppCompatDialogFragment + { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + final Bundle profileInfo = getArguments(); + int icon = android.R.drawable.ic_dialog_alert; + int title = R.string.connect_profile_question; + int message = R.string.replaces_active_connection; + int button = R.string.connect; + + if (profileInfo.getBoolean(PROFILE_RECONNECT)) + { + icon = android.R.drawable.ic_dialog_info; + title = R.string.vpn_connected; + message = R.string.vpn_profile_connected; + button = R.string.reconnect; + } + else if (profileInfo.getBoolean(PROFILE_DISCONNECT)) + { + title = R.string.disconnect_question; + message = R.string.disconnect_active_connection; + button = R.string.disconnect; + } + + DialogInterface.OnClickListener connectListener = new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + VpnProfileControlActivity activity = (VpnProfileControlActivity)getActivity(); + activity.startVpnProfile(profileInfo); + } + }; + DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + VpnProfileControlActivity activity = (VpnProfileControlActivity)getActivity(); + if (activity.mService != null) + { + activity.mService.disconnect(); + } + getActivity().finish(); + } + }; + DialogInterface.OnClickListener cancelListener = new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + getActivity().finish(); + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) + .setIcon(icon) + .setTitle(String.format(getString(title), profileInfo.getString(PROFILE_NAME))) + .setMessage(message); + + if (profileInfo.getBoolean(PROFILE_DISCONNECT)) + { + builder.setPositiveButton(button, disconnectListener); + } + else + { + builder.setPositiveButton(button, connectListener); + } + + if (profileInfo.getBoolean(PROFILE_RECONNECT)) + { + builder.setNegativeButton(R.string.disconnect, disconnectListener); + builder.setNeutralButton(android.R.string.cancel, cancelListener); + } + else + { + builder.setNegativeButton(android.R.string.cancel, cancelListener); + } + return builder.create(); + } + + @Override + public void onCancel(DialogInterface dialog) + { + getActivity().finish(); + } + } + + /** + * Class that displays a login dialog and initiates the selected VPN + * profile if the user confirms the dialog. + */ + public static class LoginDialog extends AppCompatDialogFragment + { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + final Bundle profileInfo = getArguments(); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.login_dialog, null); + EditText username = (EditText)view.findViewById(R.id.username); + username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME)); + final EditText password = (EditText)view.findViewById(R.id.password); + + AlertDialog.Builder adb = new AlertDialog.Builder(getActivity()); + adb.setView(view); + adb.setTitle(getString(R.string.login_title)); + adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int whichButton) + { + VpnProfileControlActivity activity = (VpnProfileControlActivity)getActivity(); + profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim()); + activity.prepareVpnService(profileInfo); + } + }); + adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + getActivity().finish(); + } + }); + return adb.create(); + } + + @Override + public void onCancel(DialogInterface dialog) + { + getActivity().finish(); + } + } + + /** + * Class representing an error message which is displayed if VpnService is + * not supported on the current device. + */ + public static class VpnNotSupportedError extends AppCompatDialogFragment + { + static final String ERROR_MESSAGE_ID = "org.strongswan.android.VpnNotSupportedError.MessageId"; + + public static void showWithMessage(AppCompatActivity activity, int messageId) + { + Bundle bundle = new Bundle(); + bundle.putInt(ERROR_MESSAGE_ID, messageId); + VpnNotSupportedError dialog = new VpnNotSupportedError(); + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), DIALOG_TAG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + final Bundle arguments = getArguments(); + final int messageId = arguments.getInt(ERROR_MESSAGE_ID); + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.vpn_not_supported_title) + .setMessage(messageId) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int id) + { + getActivity().finish(); + } + }).create(); + } + + @Override + public void onCancel(DialogInterface dialog) + { + getActivity().finish(); + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java index 93af5f147b..27cecc7e99 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java @@ -39,8 +39,8 @@ public class VpnProfileSelectActivity extends AppCompatActivity implements OnVpn @Override public void onVpnProfileSelected(VpnProfile profile) { - Intent shortcut = new Intent(MainActivity.START_PROFILE); - shortcut.putExtra(MainActivity.EXTRA_VPN_PROFILE_ID, profile.getId()); + Intent shortcut = new Intent(VpnProfileControlActivity.START_PROFILE); + shortcut.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString()); Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut);