android: Show confirmation dialog also when connecting
[strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / MainActivity.java
1 /*
2 * Copyright (C) 2012 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * Hochschule fuer Technik Rapperswil
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation; either version 2 of the License, or (at your
10 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 * for more details.
16 */
17
18 package org.strongswan.android.ui;
19
20 import android.app.Dialog;
21 import android.app.Service;
22 import android.content.ActivityNotFoundException;
23 import android.content.ComponentName;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.ServiceConnection;
27 import android.net.VpnService;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.os.IBinder;
31 import android.support.v4.app.Fragment;
32 import android.support.v4.app.FragmentManager;
33 import android.support.v4.app.FragmentTransaction;
34 import android.support.v7.app.ActionBar;
35 import android.support.v7.app.AlertDialog;
36 import android.support.v7.app.AppCompatActivity;
37 import android.support.v7.app.AppCompatDialogFragment;
38 import android.view.LayoutInflater;
39 import android.view.Menu;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.view.Window;
43 import android.widget.EditText;
44 import android.widget.Toast;
45
46 import org.strongswan.android.R;
47 import org.strongswan.android.data.VpnProfile;
48 import org.strongswan.android.data.VpnProfileDataSource;
49 import org.strongswan.android.data.VpnType.VpnTypeFeature;
50 import org.strongswan.android.logic.CharonVpnService;
51 import org.strongswan.android.logic.TrustedCertificateManager;
52 import org.strongswan.android.logic.VpnStateService;
53 import org.strongswan.android.logic.VpnStateService.State;
54 import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
55
56 public class MainActivity extends AppCompatActivity implements OnVpnProfileSelectedListener
57 {
58 public static final String CONTACT_EMAIL = "android@strongswan.org";
59 public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE";
60 public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID";
61 /**
62 * Use "bring your own device" (BYOD) features
63 */
64 public static final boolean USE_BYOD = true;
65 private static final int PREPARE_VPN_SERVICE = 0;
66 private static final String PROFILE_NAME = "org.strongswan.android.MainActivity.PROFILE_NAME";
67 private static final String PROFILE_REQUIRES_PASSWORD = "org.strongswan.android.MainActivity.REQUIRES_PASSWORD";
68 private static final String PROFILE_RECONNECT = "org.strongswan.android.MainActivity.RECONNECT";
69 private static final String DIALOG_TAG = "Dialog";
70
71 private Bundle mProfileInfo;
72 private VpnStateService mService;
73 private final ServiceConnection mServiceConnection = new ServiceConnection()
74 {
75 @Override
76 public void onServiceDisconnected(ComponentName name)
77 {
78 mService = null;
79 }
80
81 @Override
82 public void onServiceConnected(ComponentName name, IBinder service)
83 {
84 mService = ((VpnStateService.LocalBinder)service).getService();
85
86 if (START_PROFILE.equals(getIntent().getAction()))
87 {
88 startVpnProfile(getIntent());
89 }
90 }
91 };
92
93 @Override
94 public void onCreate(Bundle savedInstanceState)
95 {
96 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
97 super.onCreate(savedInstanceState);
98 setContentView(R.layout.main);
99
100 ActionBar bar = getSupportActionBar();
101 bar.setDisplayShowHomeEnabled(true);
102 bar.setDisplayShowTitleEnabled(false);
103 bar.setIcon(R.drawable.ic_launcher);
104
105 this.bindService(new Intent(this, VpnStateService.class),
106 mServiceConnection, Service.BIND_AUTO_CREATE);
107
108 /* load CA certificates in a background task */
109 new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
110 }
111
112 @Override
113 protected void onDestroy()
114 {
115 super.onDestroy();
116 if (mService != null)
117 {
118 this.unbindService(mServiceConnection);
119 }
120 }
121
122 /**
123 * Due to launchMode=singleTop this is called if the Activity already exists
124 */
125 @Override
126 protected void onNewIntent(Intent intent)
127 {
128 super.onNewIntent(intent);
129
130 if (START_PROFILE.equals(intent.getAction()))
131 {
132 startVpnProfile(intent);
133 }
134 }
135
136 @Override
137 public boolean onCreateOptionsMenu(Menu menu)
138 {
139 getMenuInflater().inflate(R.menu.main, menu);
140 return true;
141 }
142
143 @Override
144 public boolean onOptionsItemSelected(MenuItem item)
145 {
146 switch (item.getItemId())
147 {
148 case R.id.menu_manage_certs:
149 Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
150 startActivity(certIntent);
151 return true;
152 case R.id.menu_show_log:
153 Intent logIntent = new Intent(this, LogActivity.class);
154 startActivity(logIntent);
155 return true;
156 default:
157 return super.onOptionsItemSelected(item);
158 }
159 }
160
161 /**
162 * Prepare the VpnService. If this succeeds the current VPN profile is
163 * started.
164 *
165 * @param profileInfo a bundle containing the information about the profile to be started
166 */
167 protected void prepareVpnService(Bundle profileInfo)
168 {
169 Intent intent;
170 try
171 {
172 intent = VpnService.prepare(this);
173 }
174 catch (IllegalStateException ex)
175 {
176 /* this happens if the always-on VPN feature (Android 4.2+) is activated */
177 VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_during_lockdown);
178 return;
179 }
180 /* store profile info until the user grants us permission */
181 mProfileInfo = profileInfo;
182 if (intent != null)
183 {
184 try
185 {
186 startActivityForResult(intent, PREPARE_VPN_SERVICE);
187 }
188 catch (ActivityNotFoundException ex)
189 {
190 /* it seems some devices, even though they come with Android 4,
191 * don't have the VPN components built into the system image.
192 * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
193 * will not be found then */
194 VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported);
195 }
196 }
197 else
198 { /* user already granted permission to use VpnService */
199 onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
200 }
201 }
202
203 @Override
204 protected void onActivityResult(int requestCode, int resultCode, Intent data)
205 {
206 switch (requestCode)
207 {
208 case PREPARE_VPN_SERVICE:
209 if (resultCode == RESULT_OK && mProfileInfo != null)
210 {
211 Intent intent = new Intent(this, CharonVpnService.class);
212 intent.putExtras(mProfileInfo);
213 this.startService(intent);
214 }
215 break;
216 default:
217 super.onActivityResult(requestCode, resultCode, data);
218 }
219 }
220
221 @Override
222 public void onVpnProfileSelected(VpnProfile profile)
223 {
224 Bundle profileInfo = new Bundle();
225 profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId());
226 profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername());
227 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword());
228 profileInfo.putBoolean(PROFILE_REQUIRES_PASSWORD, profile.getVpnType().has(VpnTypeFeature.USER_PASS));
229 profileInfo.putString(PROFILE_NAME, profile.getName());
230
231 removeFragmentByTag(DIALOG_TAG);
232
233 if (mService != null && (mService.getState() == State.CONNECTED || mService.getState() == State.CONNECTING))
234 {
235 profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId());
236
237 ConfirmationDialog dialog = new ConfirmationDialog();
238 dialog.setArguments(profileInfo);
239 dialog.show(this.getSupportFragmentManager(), DIALOG_TAG);
240 return;
241 }
242 startVpnProfile(profileInfo);
243 }
244
245 /**
246 * Start the given VPN profile asking the user for a password if required.
247 *
248 * @param profileInfo data about the profile
249 */
250 private void startVpnProfile(Bundle profileInfo)
251 {
252 if (profileInfo.getBoolean(PROFILE_REQUIRES_PASSWORD) &&
253 profileInfo.getString(VpnProfileDataSource.KEY_PASSWORD) == null)
254 {
255 LoginDialog login = new LoginDialog();
256 login.setArguments(profileInfo);
257 login.show(getSupportFragmentManager(), DIALOG_TAG);
258 return;
259 }
260 prepareVpnService(profileInfo);
261 }
262
263 /**
264 * Start the VPN profile referred to by the given intent. Displays an error
265 * if the profile doesn't exist.
266 *
267 * @param intent Intent that caused us to start this
268 */
269 private void startVpnProfile(Intent intent)
270 {
271 long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0);
272 if (profileId <= 0)
273 { /* invalid invocation */
274 return;
275 }
276 VpnProfileDataSource dataSource = new VpnProfileDataSource(this);
277 dataSource.open();
278 VpnProfile profile = dataSource.getVpnProfile(profileId);
279 dataSource.close();
280
281 if (profile != null)
282 {
283 onVpnProfileSelected(profile);
284 }
285 else
286 {
287 Toast.makeText(this, R.string.profile_not_found, Toast.LENGTH_LONG).show();
288 }
289 }
290
291 /**
292 * Class that loads the cached CA certificates.
293 */
294 private class LoadCertificatesTask extends AsyncTask<Void, Void, TrustedCertificateManager>
295 {
296 @Override
297 protected void onPreExecute()
298 {
299 setProgressBarIndeterminateVisibility(true);
300 }
301
302 @Override
303 protected TrustedCertificateManager doInBackground(Void... params)
304 {
305 return TrustedCertificateManager.getInstance().load();
306 }
307
308 @Override
309 protected void onPostExecute(TrustedCertificateManager result)
310 {
311 setProgressBarIndeterminateVisibility(false);
312 }
313 }
314
315 /**
316 * Dismiss dialog if shown
317 */
318 public void removeFragmentByTag(String tag)
319 {
320 FragmentManager fm = getSupportFragmentManager();
321 Fragment login = fm.findFragmentByTag(tag);
322 if (login != null)
323 {
324 FragmentTransaction ft = fm.beginTransaction();
325 ft.remove(login);
326 ft.commit();
327 }
328 }
329
330 /**
331 * Class that displays a confirmation dialog if a VPN profile is already connected
332 * and then initiates the selected VPN profile if the user confirms the dialog.
333 */
334 public static class ConfirmationDialog extends AppCompatDialogFragment
335 {
336 @Override
337 public Dialog onCreateDialog(Bundle savedInstanceState)
338 {
339 final Bundle profileInfo = getArguments();
340 int icon = android.R.drawable.ic_dialog_alert;
341 int title = R.string.connect_profile_question;
342 int message = R.string.replaces_active_connection;
343 int button = R.string.connect;
344
345 if (profileInfo.getBoolean(PROFILE_RECONNECT))
346 {
347 icon = android.R.drawable.ic_dialog_info;
348 title = R.string.vpn_connected;
349 message = R.string.vpn_profile_connected;
350 button = R.string.reconnect;
351 }
352
353 return new AlertDialog.Builder(getActivity())
354 .setIcon(icon)
355 .setTitle(String.format(getString(title), profileInfo.getString(PROFILE_NAME)))
356 .setMessage(message)
357 .setPositiveButton(button, new DialogInterface.OnClickListener()
358 {
359 @Override
360 public void onClick(DialogInterface dialog, int whichButton)
361 {
362 MainActivity activity = (MainActivity)getActivity();
363 activity.startVpnProfile(profileInfo);
364 }
365 })
366 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
367 {
368 @Override
369 public void onClick(DialogInterface dialog, int which)
370 {
371 dismiss();
372 }
373 }).create();
374 }
375 }
376
377 /**
378 * Class that displays a login dialog and initiates the selected VPN
379 * profile if the user confirms the dialog.
380 */
381 public static class LoginDialog extends AppCompatDialogFragment
382 {
383 @Override
384 public Dialog onCreateDialog(Bundle savedInstanceState)
385 {
386 final Bundle profileInfo = getArguments();
387 LayoutInflater inflater = getActivity().getLayoutInflater();
388 View view = inflater.inflate(R.layout.login_dialog, null);
389 EditText username = (EditText)view.findViewById(R.id.username);
390 username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME));
391 final EditText password = (EditText)view.findViewById(R.id.password);
392
393 AlertDialog.Builder adb = new AlertDialog.Builder(getActivity());
394 adb.setView(view);
395 adb.setTitle(getString(R.string.login_title));
396 adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener()
397 {
398 @Override
399 public void onClick(DialogInterface dialog, int whichButton)
400 {
401 MainActivity activity = (MainActivity)getActivity();
402 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim());
403 activity.prepareVpnService(profileInfo);
404 }
405 });
406 adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
407 {
408 @Override
409 public void onClick(DialogInterface dialog, int which)
410 {
411 dismiss();
412 }
413 });
414 return adb.create();
415 }
416 }
417
418 /**
419 * Class representing an error message which is displayed if VpnService is
420 * not supported on the current device.
421 */
422 public static class VpnNotSupportedError extends AppCompatDialogFragment
423 {
424 static final String ERROR_MESSAGE_ID = "org.strongswan.android.VpnNotSupportedError.MessageId";
425
426 public static void showWithMessage(AppCompatActivity activity, int messageId)
427 {
428 Bundle bundle = new Bundle();
429 bundle.putInt(ERROR_MESSAGE_ID, messageId);
430 VpnNotSupportedError dialog = new VpnNotSupportedError();
431 dialog.setArguments(bundle);
432 dialog.show(activity.getSupportFragmentManager(), DIALOG_TAG);
433 }
434
435 @Override
436 public Dialog onCreateDialog(Bundle savedInstanceState)
437 {
438 final Bundle arguments = getArguments();
439 final int messageId = arguments.getInt(ERROR_MESSAGE_ID);
440 return new AlertDialog.Builder(getActivity())
441 .setTitle(R.string.vpn_not_supported_title)
442 .setMessage(messageId)
443 .setCancelable(false)
444 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
445 {
446 @Override
447 public void onClick(DialogInterface dialog, int id)
448 {
449 dialog.dismiss();
450 }
451 }).create();
452 }
453 }
454 }