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