]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
android: Use separate activity to control VPN connections
authorTobias Brunner <tobias@strongswan.org>
Thu, 7 Jun 2018 16:00:16 +0000 (18:00 +0200)
committerTobias Brunner <tobias@strongswan.org>
Tue, 3 Jul 2018 09:31:34 +0000 (11:31 +0200)
This way we don't have to open the main window, but only show a dialog
if necessary (or nothing in many cases).

src/frontends/android/app/src/main/AndroidManifest.xml
src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java

index bfea305cd6356ecc742cf2ed463da88f58ae8371..fc0fb4f5784815968232046f797554b6620afbfc 100644 (file)
                 <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" >
index d573d882d131abc0acb55221cd2a0bafa7b1d373..33edcf78859837f62ea7f519a9e33573d3aaac3a 100644 (file)
@@ -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);
index a729362194a0d1742d0c5195e17f741634ceb273..5971e1ad499231d9b9950a7b408ad9a746745da4 100644 (file)
@@ -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
 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 (file)
index 0000000..b8e7447
--- /dev/null
@@ -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();
+               }
+       }
+}
index 93af5f147b4df0ff03aeefb9c7f0caf863696417..27cecc7e99ba230572343877e1c752af539d1aa6 100644 (file)
@@ -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);