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