android: Change state handling to display errors occurring while the app is hidden
[strongswan.git] / src / frontends / android / src / org / strongswan / android / ui / VpnStateFragment.java
1 /*
2 * Copyright (C) 2012-2013 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 java.util.ArrayList;
21 import java.util.List;
22
23 import org.strongswan.android.R;
24 import org.strongswan.android.data.VpnProfile;
25 import org.strongswan.android.logic.VpnStateService;
26 import org.strongswan.android.logic.VpnStateService.ErrorState;
27 import org.strongswan.android.logic.VpnStateService.State;
28 import org.strongswan.android.logic.VpnStateService.VpnStateListener;
29 import org.strongswan.android.logic.imc.ImcState;
30 import org.strongswan.android.logic.imc.RemediationInstruction;
31
32 import android.app.AlertDialog;
33 import android.app.Fragment;
34 import android.app.ProgressDialog;
35 import android.app.Service;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.content.DialogInterface;
39 import android.content.Intent;
40 import android.content.ServiceConnection;
41 import android.os.Bundle;
42 import android.os.IBinder;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.View.OnClickListener;
46 import android.view.ViewGroup;
47 import android.widget.Button;
48 import android.widget.TextView;
49
50 public class VpnStateFragment extends Fragment implements VpnStateListener
51 {
52 private static final String KEY_ERROR_CONNECTION_ID = "error_connection_id";
53 private static final String KEY_DISMISSED_CONNECTION_ID = "dismissed_connection_id";
54
55 private TextView mProfileNameView;
56 private TextView mProfileView;
57 private TextView mStateView;
58 private int stateBaseColor;
59 private Button mActionButton;
60 private ProgressDialog mProgressDialog;
61 private AlertDialog mErrorDialog;
62 private long mErrorConnectionID;
63 private long mDismissedConnectionID;
64 private VpnStateService mService;
65 private final ServiceConnection mServiceConnection = new ServiceConnection() {
66 @Override
67 public void onServiceDisconnected(ComponentName name)
68 {
69 mService = null;
70 }
71
72 @Override
73 public void onServiceConnected(ComponentName name, IBinder service)
74 {
75 mService = ((VpnStateService.LocalBinder)service).getService();
76 mService.registerListener(VpnStateFragment.this);
77 updateView();
78 }
79 };
80
81 @Override
82 public void onCreate(Bundle savedInstanceState)
83 {
84 super.onCreate(savedInstanceState);
85
86 /* bind to the service only seems to work from the ApplicationContext */
87 Context context = getActivity().getApplicationContext();
88 context.bindService(new Intent(context, VpnStateService.class),
89 mServiceConnection, Service.BIND_AUTO_CREATE);
90
91 mErrorConnectionID = 0;
92 mDismissedConnectionID = 0;
93 if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ERROR_CONNECTION_ID))
94 {
95 mErrorConnectionID = (Long)savedInstanceState.getSerializable(KEY_ERROR_CONNECTION_ID);
96 mDismissedConnectionID = (Long)savedInstanceState.getSerializable(KEY_DISMISSED_CONNECTION_ID);
97 }
98 }
99
100 @Override
101 public void onSaveInstanceState(Bundle outState)
102 {
103 super.onSaveInstanceState(outState);
104
105 outState.putSerializable(KEY_ERROR_CONNECTION_ID, mErrorConnectionID);
106 outState.putSerializable(KEY_DISMISSED_CONNECTION_ID, mDismissedConnectionID);
107 }
108
109 @Override
110 public View onCreateView(LayoutInflater inflater, ViewGroup container,
111 Bundle savedInstanceState)
112 {
113 View view = inflater.inflate(R.layout.vpn_state_fragment, null);
114
115 mActionButton = (Button)view.findViewById(R.id.action);
116 mActionButton.setOnClickListener(new OnClickListener() {
117 @Override
118 public void onClick(View v)
119 {
120 if (mService != null)
121 {
122 mService.disconnect();
123 }
124 }
125 });
126 enableActionButton(false);
127
128 mStateView = (TextView)view.findViewById(R.id.vpn_state);
129 stateBaseColor = mStateView.getCurrentTextColor();
130 mProfileView = (TextView)view.findViewById(R.id.vpn_profile_label);
131 mProfileNameView = (TextView)view.findViewById(R.id.vpn_profile_name);
132
133 return view;
134 }
135
136 @Override
137 public void onStart()
138 {
139 super.onStart();
140 if (mService != null)
141 {
142 mService.registerListener(this);
143 updateView();
144 }
145 }
146
147 @Override
148 public void onStop()
149 {
150 super.onStop();
151 if (mService != null)
152 {
153 mService.unregisterListener(this);
154 }
155 hideErrorDialog();
156 hideProgressDialog();
157 }
158
159 @Override
160 public void onDestroy()
161 {
162 super.onDestroy();
163 if (mService != null)
164 {
165 getActivity().getApplicationContext().unbindService(mServiceConnection);
166 }
167 }
168
169 @Override
170 public void stateChanged()
171 {
172 updateView();
173 }
174
175 public void updateView()
176 {
177 long connectionID = mService.getConnectionID();
178 VpnProfile profile = mService.getProfile();
179 State state = mService.getState();
180 ErrorState error = mService.getErrorState();
181 ImcState imcState = mService.getImcState();
182 String name = "", gateway = "";
183
184 if (profile != null)
185 {
186 name = profile.getName();
187 gateway = profile.getGateway();
188 }
189
190 if (reportError(connectionID, name, error, imcState))
191 {
192 return;
193 }
194
195 hideProgressDialog();
196 enableActionButton(false);
197 mProfileNameView.setText(name);
198
199 switch (state)
200 {
201 case DISABLED:
202 showProfile(false);
203 mStateView.setText(R.string.state_disabled);
204 mStateView.setTextColor(stateBaseColor);
205 break;
206 case CONNECTING:
207 showProfile(true);
208 showConnectDialog(name, gateway);
209 mStateView.setText(R.string.state_connecting);
210 mStateView.setTextColor(stateBaseColor);
211 break;
212 case CONNECTED:
213 showProfile(true);
214 enableActionButton(true);
215 mStateView.setText(R.string.state_connected);
216 mStateView.setTextColor(getResources().getColor(R.color.success_text));
217 break;
218 case DISCONNECTING:
219 showProfile(true);
220 showDisconnectDialog(name);
221 mStateView.setText(R.string.state_disconnecting);
222 mStateView.setTextColor(stateBaseColor);
223 break;
224 }
225 }
226
227 private boolean reportError(long connectionID, String name, ErrorState error, ImcState imcState)
228 {
229 if (connectionID > mDismissedConnectionID)
230 { /* report error if it hasn't been dismissed yet */
231 mErrorConnectionID = connectionID;
232 }
233 else
234 { /* ignore all other errors */
235 error = ErrorState.NO_ERROR;
236 }
237 if (error == ErrorState.NO_ERROR)
238 {
239 hideErrorDialog();
240 return false;
241 }
242 else if (mErrorDialog != null)
243 { /* we already show the dialog */
244 return true;
245 }
246 hideProgressDialog();
247 mProfileNameView.setText(name);
248 showProfile(true);
249 enableActionButton(false);
250 mStateView.setText(R.string.state_error);
251 mStateView.setTextColor(getResources().getColor(R.color.error_text));
252 switch (error)
253 {
254 case AUTH_FAILED:
255 if (imcState == ImcState.BLOCK)
256 {
257 showErrorDialog(R.string.error_assessment_failed);
258 }
259 else
260 {
261 showErrorDialog(R.string.error_auth_failed);
262 }
263 break;
264 case PEER_AUTH_FAILED:
265 showErrorDialog(R.string.error_peer_auth_failed);
266 break;
267 case LOOKUP_FAILED:
268 showErrorDialog(R.string.error_lookup_failed);
269 break;
270 case UNREACHABLE:
271 showErrorDialog(R.string.error_unreachable);
272 break;
273 default:
274 showErrorDialog(R.string.error_generic);
275 break;
276 }
277 return true;
278 }
279
280 private void showProfile(boolean show)
281 {
282 mProfileView.setVisibility(show ? View.VISIBLE : View.GONE);
283 mProfileNameView.setVisibility(show ? View.VISIBLE : View.GONE);
284 }
285
286 private void enableActionButton(boolean enable)
287 {
288 mActionButton.setEnabled(enable);
289 mActionButton.setVisibility(enable ? View.VISIBLE : View.GONE);
290 }
291
292 private void hideProgressDialog()
293 {
294 if (mProgressDialog != null)
295 {
296 mProgressDialog.dismiss();
297 mProgressDialog = null;
298 }
299 }
300
301 private void hideErrorDialog()
302 {
303 if (mErrorDialog != null)
304 {
305 mErrorDialog.dismiss();
306 mErrorDialog = null;
307 }
308 }
309
310 private void clearError()
311 {
312 mDismissedConnectionID = mErrorConnectionID;
313 updateView();
314 }
315
316 private void showConnectDialog(String profile, String gateway)
317 {
318 mProgressDialog = new ProgressDialog(getActivity());
319 mProgressDialog.setTitle(String.format(getString(R.string.connecting_title), profile));
320 mProgressDialog.setMessage(String.format(getString(R.string.connecting_message), gateway));
321 mProgressDialog.setIndeterminate(true);
322 mProgressDialog.setCancelable(false);
323 mProgressDialog.setButton(getString(android.R.string.cancel),
324 new DialogInterface.OnClickListener()
325 {
326 @Override
327 public void onClick(DialogInterface dialog, int which)
328 {
329 if (mService != null)
330 {
331 mService.disconnect();
332 }
333 }
334 });
335 mProgressDialog.show();
336 }
337
338 private void showDisconnectDialog(String profile)
339 {
340 mProgressDialog = new ProgressDialog(getActivity());
341 mProgressDialog.setMessage(getString(R.string.state_disconnecting));
342 mProgressDialog.setIndeterminate(true);
343 mProgressDialog.setCancelable(false);
344 mProgressDialog.show();
345 }
346
347 private void showErrorDialog(int textid)
348 {
349 final List<RemediationInstruction> instructions = mService.getRemediationInstructions();
350 final boolean show_instructions = mService.getImcState() == ImcState.BLOCK && !instructions.isEmpty();
351 int text = show_instructions ? R.string.show_remediation_instructions : R.string.show_log;
352
353 mErrorDialog = new AlertDialog.Builder(getActivity())
354 .setMessage(getString(R.string.error_introduction) + " " + getString(textid))
355 .setCancelable(false)
356 .setNeutralButton(text, new DialogInterface.OnClickListener() {
357 @Override
358 public void onClick(DialogInterface dialog, int which)
359 {
360 clearError();
361 dialog.dismiss();
362 Intent intent;
363 if (show_instructions)
364 {
365 intent = new Intent(getActivity(), RemediationInstructionsActivity.class);
366 intent.putParcelableArrayListExtra(RemediationInstructionsFragment.EXTRA_REMEDIATION_INSTRUCTIONS,
367 new ArrayList<RemediationInstruction>(instructions));
368 }
369 else
370 {
371 intent = new Intent(getActivity(), LogActivity.class);
372 }
373 startActivity(intent);
374 }
375 })
376 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
377 @Override
378 public void onClick(DialogInterface dialog, int id)
379 {
380 clearError();
381 dialog.dismiss();
382 }
383 }).create();
384 mErrorDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
385 @Override
386 public void onDismiss(DialogInterface dialog)
387 {
388 mErrorDialog = null;
389 }
390 });
391 mErrorDialog.show();
392 }
393 }