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).
This commit is contained in:
Tobias Brunner
2018-07-03 11:31:34 +02:00
parent 1d336cc2ae
commit 58d139dad3
5 changed files with 532 additions and 454 deletions
@@ -36,10 +36,21 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.VpnProfileControlActivity"
android:theme="@style/TransparentActivity"
android:taskAffinity=""
android:excludeFromRecents="true"
android:launchMode="singleTask" >
<intent-filter>
<action android:name="org.strongswan.android.action.START_PROFILE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="org.strongswan.android.action.DISCONNECT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.VpnProfileDetailActivity" >
@@ -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);
@@ -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,75 +31,31 @@ 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);
}
}
};
@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
*/
@@ -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 <http://www.fsf.org/copyleft/gpl.txt>.
*
* 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();
}
}
}
@@ -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);