OSDN Git Service

Fix VPN settings flow.
[android-x86/packages-apps-Settings.git] / src / com / android / settings / vpn / VpnSettings.java
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.settings.vpn;
18
19 import com.android.settings.R;
20 import com.android.settings.SettingsPreferenceFragment;
21
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.Dialog;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.ServiceConnection;
31 import android.net.vpn.IVpnService;
32 import android.net.vpn.L2tpIpsecProfile;
33 import android.net.vpn.L2tpIpsecPskProfile;
34 import android.net.vpn.L2tpProfile;
35 import android.net.vpn.VpnManager;
36 import android.net.vpn.VpnProfile;
37 import android.net.vpn.VpnState;
38 import android.net.vpn.VpnType;
39 import android.os.Bundle;
40 import android.os.ConditionVariable;
41 import android.os.Handler;
42 import android.os.IBinder;
43 import android.preference.Preference;
44 import android.preference.PreferenceActivity;
45 import android.preference.PreferenceCategory;
46 import android.preference.PreferenceScreen;
47 import android.preference.Preference.OnPreferenceClickListener;
48 import android.security.Credentials;
49 import android.security.KeyStore;
50 import android.text.TextUtils;
51 import android.util.Log;
52 import android.view.ContextMenu;
53 import android.view.MenuItem;
54 import android.view.View;
55 import android.view.ContextMenu.ContextMenuInfo;
56 import android.widget.AdapterView.AdapterContextMenuInfo;
57
58 import java.io.File;
59 import java.io.FileInputStream;
60 import java.io.FileOutputStream;
61 import java.io.IOException;
62 import java.io.ObjectInputStream;
63 import java.io.ObjectOutputStream;
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.Comparator;
67 import java.util.LinkedHashMap;
68 import java.util.List;
69 import java.util.Map;
70
71 /**
72  * The preference activity for configuring VPN settings.
73  */
74 public class VpnSettings extends SettingsPreferenceFragment
75         implements DialogInterface.OnClickListener {
76
77     private static final boolean DEBUG = false;
78
79     // Key to the field exchanged for profile editing.
80     static final String KEY_VPN_PROFILE = "vpn_profile";
81
82     // Key to the field exchanged for VPN type selection.
83     static final String KEY_VPN_TYPE = "vpn_type";
84
85     private static final String TAG = VpnSettings.class.getSimpleName();
86
87     private static final String PREF_ADD_VPN = "add_new_vpn";
88     private static final String PREF_VPN_LIST = "vpn_list";
89
90     private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/";
91     private static final String PROFILE_OBJ_FILE = ".pobj";
92
93     private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
94     static final int REQUEST_SELECT_VPN_TYPE = 2;
95
96     private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0;
97     private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1;
98     private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2;
99     private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3;
100
101     private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE;
102     private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE;
103
104     private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1;
105     private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1;
106
107     private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR;
108
109     private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i';
110     private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l';
111
112     private PreferenceScreen mAddVpn;
113     private PreferenceCategory mVpnListContainer;
114
115     // profile name --> VpnPreference
116     private Map<String, VpnPreference> mVpnPreferenceMap;
117     private List<VpnProfile> mVpnProfileList;
118
119     // profile engaged in a connection
120     private VpnProfile mActiveProfile;
121
122     // actor engaged in connecting
123     private VpnProfileActor mConnectingActor;
124
125     // states saved for unlocking keystore
126     private Runnable mUnlockAction;
127
128     private KeyStore mKeyStore = KeyStore.getInstance();
129
130     private VpnManager mVpnManager;
131
132     private ConnectivityReceiver mConnectivityReceiver =
133             new ConnectivityReceiver();
134
135     private int mConnectingErrorCode = NO_ERROR;
136
137     private Dialog mShowingDialog;
138
139     private StatusChecker mStatusChecker = new StatusChecker();
140
141     private Handler mHandler = new Handler();
142
143     @Override
144     public void onCreate(Bundle savedInstanceState) {
145         super.onCreate(savedInstanceState);
146         addPreferencesFromResource(R.xml.vpn_settings);
147     }
148
149     @Override
150     public void onActivityCreated(Bundle savedInstanceState) {
151         super.onActivityCreated(savedInstanceState);
152
153         mVpnManager = new VpnManager(getActivity());
154         // restore VpnProfile list and construct VpnPreference map
155         mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST);
156
157         // set up the "add vpn" preference
158         mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN);
159         mAddVpn.setOnPreferenceClickListener(
160                 new OnPreferenceClickListener() {
161                     public boolean onPreferenceClick(Preference preference) {
162                         startVpnTypeSelection();
163                         return true;
164                     }
165                 });
166
167         // for long-press gesture on a profile preference
168         registerForContextMenu(getListView());
169
170         // listen to vpn connectivity event
171         mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
172
173         retrieveVpnListFromStorage();
174         checkVpnConnectionStatusInBackground();
175     }
176
177     @Override
178     public void onResume() {
179         super.onResume();
180         if (DEBUG)
181             Log.d(TAG, "onResume");
182         if ((mUnlockAction != null) && isKeyStoreUnlocked()) {
183             Runnable action = mUnlockAction;
184             mUnlockAction = null;
185             getActivity().runOnUiThread(action);
186         }
187     }
188
189     @Override
190     public void onDestroyView() {
191         unregisterForContextMenu(getListView());
192         mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
193         if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
194             mShowingDialog.dismiss();
195         }
196         // This should be called after the procedure above as ListView inside this Fragment
197         // will be deleted here.
198         super.onDestroyView();
199     }
200
201     @Override
202     public Dialog onCreateDialog (int id) {
203         switch (id) {
204             case DIALOG_CONNECT:
205                 return createConnectDialog();
206
207             case DIALOG_SECRET_NOT_SET:
208                 return createSecretNotSetDialog();
209
210             case VpnManager.VPN_ERROR_CHALLENGE:
211             case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
212             case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
213                 return createEditDialog(id);
214
215             default:
216                 Log.d(TAG, "create reconnect dialog for event " + id);
217                 return createReconnectDialog(id);
218         }
219     }
220
221     private Dialog createConnectDialog() {
222         final Activity activity = getActivity();
223         return new AlertDialog.Builder(activity)
224                 .setView(mConnectingActor.createConnectView())
225                 .setTitle(String.format(activity.getString(R.string.vpn_connect_to),
226                         mActiveProfile.getName()))
227                 .setPositiveButton(activity.getString(R.string.vpn_connect_button),
228                         this)
229                 .setNegativeButton(activity.getString(android.R.string.cancel),
230                         this)
231                 .setOnCancelListener(new DialogInterface.OnCancelListener() {
232                             public void onCancel(DialogInterface dialog) {
233                                 removeDialog(DIALOG_CONNECT);
234                                 changeState(mActiveProfile, VpnState.IDLE);
235                             }
236                         })
237                 .create();
238     }
239
240     private Dialog createReconnectDialog(int id) {
241         int msgId;
242         switch (id) {
243             case VpnManager.VPN_ERROR_AUTH:
244                 msgId = R.string.vpn_auth_error_dialog_msg;
245                 break;
246
247             case VpnManager.VPN_ERROR_REMOTE_HUNG_UP:
248                 msgId = R.string.vpn_remote_hung_up_error_dialog_msg;
249                 break;
250
251             case VpnManager.VPN_ERROR_CONNECTION_LOST:
252                 msgId = R.string.vpn_reconnect_from_lost;
253                 break;
254
255             case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP:
256                 msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg;
257                 break;
258
259             default:
260                 msgId = R.string.vpn_confirm_reconnect;
261         }
262         return createCommonDialogBuilder().setMessage(msgId).create();
263     }
264
265     private Dialog createEditDialog(int id) {
266         int msgId;
267         switch (id) {
268             case VpnManager.VPN_ERROR_CHALLENGE:
269                 msgId = R.string.vpn_challenge_error_dialog_msg;
270                 break;
271
272             case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
273                 msgId = R.string.vpn_unknown_server_dialog_msg;
274                 break;
275
276             case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
277                 msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg;
278                 break;
279
280             default:
281                 return null;
282         }
283         return createCommonEditDialogBuilder().setMessage(msgId).create();
284     }
285
286     private Dialog createSecretNotSetDialog() {
287         return createCommonDialogBuilder()
288                 .setMessage(R.string.vpn_secret_not_set_dialog_msg)
289                 .setPositiveButton(R.string.vpn_yes_button,
290                         new DialogInterface.OnClickListener() {
291                             public void onClick(DialogInterface dialog, int w) {
292                                 startVpnEditor(mActiveProfile, false);
293                             }
294                         })
295                 .create();
296     }
297
298     private AlertDialog.Builder createCommonEditDialogBuilder() {
299         return createCommonDialogBuilder()
300                 .setPositiveButton(R.string.vpn_yes_button,
301                         new DialogInterface.OnClickListener() {
302                             public void onClick(DialogInterface dialog, int w) {
303                                 VpnProfile p = mActiveProfile;
304                                 onIdle();
305                                 startVpnEditor(p, false);
306                             }
307                         });
308     }
309
310     private AlertDialog.Builder createCommonDialogBuilder() {
311         return new AlertDialog.Builder(getActivity())
312                 .setTitle(android.R.string.dialog_alert_title)
313                 .setIcon(android.R.drawable.ic_dialog_alert)
314                 .setPositiveButton(R.string.vpn_yes_button,
315                         new DialogInterface.OnClickListener() {
316                             public void onClick(DialogInterface dialog, int w) {
317                                 connectOrDisconnect(mActiveProfile);
318                             }
319                         })
320                 .setNegativeButton(R.string.vpn_no_button,
321                         new DialogInterface.OnClickListener() {
322                             public void onClick(DialogInterface dialog, int w) {
323                                 onIdle();
324                             }
325                         })
326                 .setOnCancelListener(new DialogInterface.OnCancelListener() {
327                             public void onCancel(DialogInterface dialog) {
328                                 onIdle();
329                             }
330                         });
331     }
332
333     @Override
334     public void onCreateContextMenu(ContextMenu menu, View v,
335             ContextMenuInfo menuInfo) {
336         super.onCreateContextMenu(menu, v, menuInfo);
337
338         VpnProfile p = getProfile(getProfilePositionFrom(
339                     (AdapterContextMenuInfo) menuInfo));
340         if (p != null) {
341             VpnState state = p.getState();
342             menu.setHeaderTitle(p.getName());
343
344             boolean isIdle = (state == VpnState.IDLE);
345             boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING)
346                     || (state == VpnState.CANCELLED));
347             menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect)
348                     .setEnabled(isIdle && (mActiveProfile == null));
349             menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0,
350                     R.string.vpn_menu_disconnect)
351                     .setEnabled(state == VpnState.CONNECTED);
352             menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit)
353                     .setEnabled(isNotConnect);
354             menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete)
355                     .setEnabled(isNotConnect);
356         }
357     }
358
359     @Override
360     public boolean onContextItemSelected(MenuItem item) {
361         int position = getProfilePositionFrom(
362                 (AdapterContextMenuInfo) item.getMenuInfo());
363         VpnProfile p = getProfile(position);
364
365         switch(item.getItemId()) {
366         case CONTEXT_MENU_CONNECT_ID:
367         case CONTEXT_MENU_DISCONNECT_ID:
368             connectOrDisconnect(p);
369             return true;
370
371         case CONTEXT_MENU_EDIT_ID:
372                 startVpnEditor(p, false);
373             return true;
374
375         case CONTEXT_MENU_DELETE_ID:
376             deleteProfile(position);
377             return true;
378         }
379
380         return super.onContextItemSelected(item);
381     }
382
383     @Override
384     public void onActivityResult(final int requestCode, final int resultCode,
385             final Intent data) {
386
387         if (DEBUG) Log.d(TAG, "onActivityResult , result = " + resultCode + ", data = " + data);
388         if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) {
389             Log.d(TAG, "no result returned by editor");
390             return;
391         }
392
393         if (requestCode == REQUEST_SELECT_VPN_TYPE) {
394             final String typeName = data.getStringExtra(KEY_VPN_TYPE);
395             mHandler.post(new Runnable() {
396
397                 public void run() {
398                     startVpnEditor(createVpnProfile(typeName), true);
399                 }
400             });
401         } else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) {
402             VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE);
403             if (p == null) {
404                 Log.e(TAG, "null object returned by editor");
405                 return;
406             }
407
408             final Activity activity = getActivity();
409             int index = getProfileIndexFromId(p.getId());
410             if (checkDuplicateName(p, index)) {
411                 final VpnProfile profile = p;
412                 Util.showErrorMessage(activity, String.format(
413                         activity.getString(R.string.vpn_error_duplicate_name),
414                         p.getName()),
415                         new DialogInterface.OnClickListener() {
416                             public void onClick(DialogInterface dialog, int w) {
417                                 startVpnEditor(profile, false);
418                             }
419                         });
420                 return;
421             }
422
423             if (needKeyStoreToSave(p)) {
424                 Runnable action = new Runnable() {
425                     public void run() {
426                         onActivityResult(requestCode, resultCode, data);
427                     }
428                 };
429                 if (!unlockKeyStore(p, action)) return;
430             }
431
432             try {
433                 if (index < 0) {
434                     addProfile(p);
435                     Util.showShortToastMessage(activity, String.format(
436                             activity.getString(R.string.vpn_profile_added), p.getName()));
437                 } else {
438                     replaceProfile(index, p);
439                     Util.showShortToastMessage(activity, String.format(
440                             activity.getString(R.string.vpn_profile_replaced),
441                             p.getName()));
442                 }
443             } catch (IOException e) {
444                 final VpnProfile profile = p;
445                 Util.showErrorMessage(activity, e + ": " + e.getMessage(),
446                         new DialogInterface.OnClickListener() {
447                             public void onClick(DialogInterface dialog, int w) {
448                                 startVpnEditor(profile, false);
449                             }
450                         });
451             }
452
453             // Remove cached VpnEditor as it is needless anymore.
454         } else {
455             throw new RuntimeException("unknown request code: " + requestCode);
456         }
457     }
458
459     // Called when the buttons on the connect dialog are clicked.
460     @Override
461     public synchronized void onClick(DialogInterface dialog, int which) {
462         if (which == CONNECT_BUTTON) {
463             Dialog d = (Dialog) dialog;
464             String error = mConnectingActor.validateInputs(d);
465             if (error == null) {
466                 mConnectingActor.connect(d);
467                 removeDialog(DIALOG_CONNECT);
468                 return;
469             } else {
470                 // dismissDialog(DIALOG_CONNECT);
471                 removeDialog(DIALOG_CONNECT);
472
473                 final Activity activity = getActivity();
474                 // show error dialog
475                 mShowingDialog = new AlertDialog.Builder(activity)
476                         .setTitle(android.R.string.dialog_alert_title)
477                         .setIcon(android.R.drawable.ic_dialog_alert)
478                         .setMessage(String.format(activity.getString(
479                                 R.string.vpn_error_miss_entering), error))
480                         .setPositiveButton(R.string.vpn_back_button,
481                                 new DialogInterface.OnClickListener() {
482                                     public void onClick(DialogInterface dialog,
483                                             int which) {
484                                         showDialog(DIALOG_CONNECT);
485                                     }
486                                 })
487                         .create();
488                 mShowingDialog.show();
489             }
490         } else {
491             removeDialog(DIALOG_CONNECT);
492             changeState(mActiveProfile, VpnState.IDLE);
493         }
494     }
495
496     private int getProfileIndexFromId(String id) {
497         int index = 0;
498         for (VpnProfile p : mVpnProfileList) {
499             if (p.getId().equals(id)) {
500                 return index;
501             } else {
502                 index++;
503             }
504         }
505         return -1;
506     }
507
508     // Replaces the profile at index in mVpnProfileList with p.
509     // Returns true if p's name is a duplicate.
510     private boolean checkDuplicateName(VpnProfile p, int index) {
511         List<VpnProfile> list = mVpnProfileList;
512         VpnPreference pref = mVpnPreferenceMap.get(p.getName());
513         if ((pref != null) && (index >= 0) && (index < list.size())) {
514             // not a duplicate if p is to replace the profile at index
515             if (pref.mProfile == list.get(index)) pref = null;
516         }
517         return (pref != null);
518     }
519
520     private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) {
521         // excludes mVpnListContainer and the preferences above it
522         return menuInfo.position - mVpnListContainer.getOrder() - 1;
523     }
524
525     // position: position in mVpnProfileList
526     private VpnProfile getProfile(int position) {
527         return ((position >= 0) ? mVpnProfileList.get(position) : null);
528     }
529
530     // position: position in mVpnProfileList
531     private void deleteProfile(final int position) {
532         if ((position < 0) || (position >= mVpnProfileList.size())) return;
533         DialogInterface.OnClickListener onClickListener =
534                 new DialogInterface.OnClickListener() {
535                     public void onClick(DialogInterface dialog, int which) {
536                         dialog.dismiss();
537                         if (which == OK_BUTTON) {
538                             VpnProfile p = mVpnProfileList.remove(position);
539                             VpnPreference pref =
540                                     mVpnPreferenceMap.remove(p.getName());
541                             mVpnListContainer.removePreference(pref);
542                             removeProfileFromStorage(p);
543                         }
544                     }
545                 };
546         mShowingDialog = new AlertDialog.Builder(getActivity())
547                 .setTitle(android.R.string.dialog_alert_title)
548                 .setIcon(android.R.drawable.ic_dialog_alert)
549                 .setMessage(R.string.vpn_confirm_profile_deletion)
550                 .setPositiveButton(android.R.string.ok, onClickListener)
551                 .setNegativeButton(R.string.vpn_no_button, onClickListener)
552                 .create();
553         mShowingDialog.show();
554     }
555
556     // Randomly generates an ID for the profile.
557     // The ID is unique and only set once when the profile is created.
558     private void setProfileId(VpnProfile profile) {
559         String id;
560
561         while (true) {
562             id = String.valueOf(Math.abs(
563                     Double.doubleToLongBits(Math.random())));
564             if (id.length() >= 8) break;
565         }
566         for (VpnProfile p : mVpnProfileList) {
567             if (p.getId().equals(id)) {
568                 setProfileId(profile);
569                 return;
570             }
571         }
572         profile.setId(id);
573     }
574
575     private void addProfile(VpnProfile p) throws IOException {
576         setProfileId(p);
577         processSecrets(p);
578         saveProfileToStorage(p);
579
580         mVpnProfileList.add(p);
581         addPreferenceFor(p);
582         disableProfilePreferencesIfOneActive();
583     }
584
585     private VpnPreference addPreferenceFor(VpnProfile p) {
586         return addPreferenceFor(p, true);
587     }
588
589     // Adds a preference in mVpnListContainer
590     private VpnPreference addPreferenceFor(
591             VpnProfile p, boolean addToContainer) {
592         VpnPreference pref = new VpnPreference(getActivity(), p);
593         mVpnPreferenceMap.put(p.getName(), pref);
594         if (addToContainer) mVpnListContainer.addPreference(pref);
595
596         pref.setOnPreferenceClickListener(
597                 new Preference.OnPreferenceClickListener() {
598                     public boolean onPreferenceClick(Preference pref) {
599                         connectOrDisconnect(((VpnPreference) pref).mProfile);
600                         return true;
601                     }
602                 });
603         return pref;
604     }
605
606     // index: index to mVpnProfileList
607     private void replaceProfile(int index, VpnProfile p) throws IOException {
608         Map<String, VpnPreference> map = mVpnPreferenceMap;
609         VpnProfile oldProfile = mVpnProfileList.set(index, p);
610         VpnPreference pref = map.remove(oldProfile.getName());
611         if (pref.mProfile != oldProfile) {
612             throw new RuntimeException("inconsistent state!");
613         }
614
615         p.setId(oldProfile.getId());
616
617         processSecrets(p);
618
619         // TODO: remove copyFiles once the setId() code propagates.
620         // Copy config files and remove the old ones if they are in different
621         // directories.
622         if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) {
623             removeProfileFromStorage(oldProfile);
624         }
625         saveProfileToStorage(p);
626
627         pref.setProfile(p);
628         map.put(p.getName(), pref);
629     }
630
631     private void startVpnTypeSelection() {
632         ((PreferenceActivity)getActivity()).startPreferencePanel(
633                 VpnTypeSelection.class.getCanonicalName(), null, R.string.vpn_type_title, null,
634                 this, REQUEST_SELECT_VPN_TYPE);
635     }
636
637     private boolean isKeyStoreUnlocked() {
638         return mKeyStore.test() == KeyStore.NO_ERROR;
639     }
640
641     // Returns true if the profile needs to access keystore
642     private boolean needKeyStoreToSave(VpnProfile p) {
643         switch (p.getType()) {
644             case L2TP_IPSEC_PSK:
645                 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
646                 String presharedKey = pskProfile.getPresharedKey();
647                 if (!TextUtils.isEmpty(presharedKey)) return true;
648                 // $FALL-THROUGH$
649             case L2TP:
650                 L2tpProfile l2tpProfile = (L2tpProfile) p;
651                 if (l2tpProfile.isSecretEnabled() &&
652                         !TextUtils.isEmpty(l2tpProfile.getSecretString())) {
653                     return true;
654                 }
655                 // $FALL-THROUGH$
656             default:
657                 return false;
658         }
659     }
660
661     // Returns true if the profile needs to access keystore
662     private boolean needKeyStoreToConnect(VpnProfile p) {
663         switch (p.getType()) {
664             case L2TP_IPSEC:
665             case L2TP_IPSEC_PSK:
666                 return true;
667
668             case L2TP:
669                 return ((L2tpProfile) p).isSecretEnabled();
670
671             default:
672                 return false;
673         }
674     }
675
676     // Returns true if keystore is unlocked or keystore is not a concern
677     private boolean unlockKeyStore(VpnProfile p, Runnable action) {
678         if (isKeyStoreUnlocked()) return true;
679         mUnlockAction = action;
680         Credentials.getInstance().unlock(getActivity());
681         return false;
682     }
683
684     private void startVpnEditor(final VpnProfile profile, boolean add) {
685         Bundle args = new Bundle();
686         args.putParcelable(KEY_VPN_PROFILE, profile);
687         // TODO: Show different titles for add and edit.
688         ((PreferenceActivity)getActivity()).startPreferencePanel(
689                 VpnEditor.class.getCanonicalName(), args,
690                 add ? R.string.vpn_details_title : R.string.vpn_details_title, null,
691                 this, REQUEST_ADD_OR_EDIT_PROFILE);
692     }
693
694     private synchronized void connect(final VpnProfile p) {
695         if (needKeyStoreToConnect(p)) {
696             Runnable action = new Runnable() {
697                 public void run() {
698                     connect(p);
699                 }
700             };
701             if (!unlockKeyStore(p, action)) return;
702         }
703
704         if (!checkSecrets(p)) return;
705         changeState(p, VpnState.CONNECTING);
706         if (mConnectingActor.isConnectDialogNeeded()) {
707             showDialog(DIALOG_CONNECT);
708         } else {
709             mConnectingActor.connect(null);
710         }
711     }
712
713     // Do connect or disconnect based on the current state.
714     private synchronized void connectOrDisconnect(VpnProfile p) {
715         VpnPreference pref = mVpnPreferenceMap.get(p.getName());
716         switch (p.getState()) {
717             case IDLE:
718                 connect(p);
719                 break;
720
721             case CONNECTING:
722                 // do nothing
723                 break;
724
725             case CONNECTED:
726             case DISCONNECTING:
727                 changeState(p, VpnState.DISCONNECTING);
728                 getActor(p).disconnect();
729                 break;
730         }
731     }
732
733     private void changeState(VpnProfile p, VpnState state) {
734         VpnState oldState = p.getState();
735         if (oldState == state) return;
736
737         p.setState(state);
738         mVpnPreferenceMap.get(p.getName()).setSummary(
739                 getProfileSummaryString(p));
740
741         switch (state) {
742         case CONNECTED:
743             mConnectingActor = null;
744             mActiveProfile = p;
745             disableProfilePreferencesIfOneActive();
746             break;
747
748         case CONNECTING:
749             mConnectingActor = getActor(p);
750             // $FALL-THROUGH$
751         case DISCONNECTING:
752             mActiveProfile = p;
753             disableProfilePreferencesIfOneActive();
754             break;
755
756         case CANCELLED:
757             changeState(p, VpnState.IDLE);
758             break;
759
760         case IDLE:
761             assert(mActiveProfile == p);
762
763             if (mConnectingErrorCode == NO_ERROR) {
764                 onIdle();
765             } else {
766                 showDialog(mConnectingErrorCode);
767                 mConnectingErrorCode = NO_ERROR;
768             }
769             break;
770         }
771     }
772
773     private void onIdle() {
774         Log.d(TAG, "   onIdle()");
775         mActiveProfile = null;
776         mConnectingActor = null;
777         enableProfilePreferences();
778     }
779
780     private void disableProfilePreferencesIfOneActive() {
781         if (mActiveProfile == null) return;
782
783         for (VpnProfile p : mVpnProfileList) {
784             switch (p.getState()) {
785                 case CONNECTING:
786                 case DISCONNECTING:
787                 case IDLE:
788                     mVpnPreferenceMap.get(p.getName()).setEnabled(false);
789                     break;
790
791                 default:
792                     mVpnPreferenceMap.get(p.getName()).setEnabled(true);
793             }
794         }
795     }
796
797     private void enableProfilePreferences() {
798         for (VpnProfile p : mVpnProfileList) {
799             mVpnPreferenceMap.get(p.getName()).setEnabled(true);
800         }
801     }
802
803     static String getProfileDir(VpnProfile p) {
804         return PROFILES_ROOT + p.getId();
805     }
806
807     static void saveProfileToStorage(VpnProfile p) throws IOException {
808         File f = new File(getProfileDir(p));
809         if (!f.exists()) f.mkdirs();
810         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
811                 new File(f, PROFILE_OBJ_FILE)));
812         oos.writeObject(p);
813         oos.close();
814     }
815
816     private void removeProfileFromStorage(VpnProfile p) {
817         Util.deleteFile(getProfileDir(p));
818     }
819
820     private void retrieveVpnListFromStorage() {
821         mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
822         mVpnProfileList = Collections.synchronizedList(
823                 new ArrayList<VpnProfile>());
824         mVpnListContainer.removeAll();
825
826         File root = new File(PROFILES_ROOT);
827         String[] dirs = root.list();
828         if (dirs == null) return;
829         for (String dir : dirs) {
830             File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
831             if (!f.exists()) continue;
832             try {
833                 VpnProfile p = deserialize(f);
834                 if (p == null) continue;
835                 if (!checkIdConsistency(dir, p)) continue;
836
837                 mVpnProfileList.add(p);
838             } catch (IOException e) {
839                 Log.e(TAG, "retrieveVpnListFromStorage()", e);
840             }
841         }
842         Collections.sort(mVpnProfileList, new Comparator<VpnProfile>() {
843             public int compare(VpnProfile p1, VpnProfile p2) {
844                 return p1.getName().compareTo(p2.getName());
845             }
846         });
847         for (VpnProfile p : mVpnProfileList) {
848             Preference pref = addPreferenceFor(p, false);
849         }
850         disableProfilePreferencesIfOneActive();
851     }
852
853     private void checkVpnConnectionStatusInBackground() {
854         new Thread(new Runnable() {
855             public void run() {
856                 mStatusChecker.check(mVpnProfileList);
857             }
858         }).start();
859     }
860
861     // A sanity check. Returns true if the profile directory name and profile ID
862     // are consistent.
863     private boolean checkIdConsistency(String dirName, VpnProfile p) {
864         if (!dirName.equals(p.getId())) {
865             Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId());
866             return false;
867         } else {
868             return true;
869         }
870     }
871
872     private VpnProfile deserialize(File profileObjectFile) throws IOException {
873         try {
874             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
875                     profileObjectFile));
876             VpnProfile p = (VpnProfile) ois.readObject();
877             ois.close();
878             return p;
879         } catch (ClassNotFoundException e) {
880             Log.d(TAG, "deserialize a profile", e);
881             return null;
882         }
883     }
884
885     private String getProfileSummaryString(VpnProfile p) {
886         final Activity activity = getActivity();
887         switch (p.getState()) {
888         case CONNECTING:
889             return activity.getString(R.string.vpn_connecting);
890         case DISCONNECTING:
891             return activity.getString(R.string.vpn_disconnecting);
892         case CONNECTED:
893             return activity.getString(R.string.vpn_connected);
894         default:
895             return activity.getString(R.string.vpn_connect_hint);
896         }
897     }
898
899     private VpnProfileActor getActor(VpnProfile p) {
900         return new AuthenticationActor(getActivity(), p);
901     }
902
903     private VpnProfile createVpnProfile(String type) {
904         return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
905     }
906
907     private boolean checkSecrets(VpnProfile p) {
908         boolean secretMissing = false;
909
910         if (p instanceof L2tpIpsecProfile) {
911             L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p;
912
913             String cert = certProfile.getCaCertificate();
914             if (TextUtils.isEmpty(cert) ||
915                     !mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) {
916                 certProfile.setCaCertificate(null);
917                 secretMissing = true;
918             }
919
920             cert = certProfile.getUserCertificate();
921             if (TextUtils.isEmpty(cert) ||
922                     !mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) {
923                 certProfile.setUserCertificate(null);
924                 secretMissing = true;
925             }
926         }
927
928         if (p instanceof L2tpIpsecPskProfile) {
929             L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
930             String presharedKey = pskProfile.getPresharedKey();
931             String key = KEY_PREFIX_IPSEC_PSK + p.getId();
932             if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) {
933                 pskProfile.setPresharedKey(null);
934                 secretMissing = true;
935             }
936         }
937
938         if (p instanceof L2tpProfile) {
939             L2tpProfile l2tpProfile = (L2tpProfile) p;
940             if (l2tpProfile.isSecretEnabled()) {
941                 String secret = l2tpProfile.getSecretString();
942                 String key = KEY_PREFIX_L2TP_SECRET + p.getId();
943                 if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) {
944                     l2tpProfile.setSecretString(null);
945                     secretMissing = true;
946                 }
947             }
948         }
949
950         if (secretMissing) {
951             mActiveProfile = p;
952             showDialog(DIALOG_SECRET_NOT_SET);
953             return false;
954         } else {
955             return true;
956         }
957     }
958
959     private void processSecrets(VpnProfile p) {
960         switch (p.getType()) {
961             case L2TP_IPSEC_PSK:
962                 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
963                 String presharedKey = pskProfile.getPresharedKey();
964                 String key = KEY_PREFIX_IPSEC_PSK + p.getId();
965                 if (!TextUtils.isEmpty(presharedKey) &&
966                         !mKeyStore.put(key, presharedKey)) {
967                     Log.e(TAG, "keystore write failed: key=" + key);
968                 }
969                 pskProfile.setPresharedKey(key);
970                 // $FALL-THROUGH$
971             case L2TP_IPSEC:
972             case L2TP:
973                 L2tpProfile l2tpProfile = (L2tpProfile) p;
974                 key = KEY_PREFIX_L2TP_SECRET + p.getId();
975                 if (l2tpProfile.isSecretEnabled()) {
976                     String secret = l2tpProfile.getSecretString();
977                     if (!TextUtils.isEmpty(secret) &&
978                             !mKeyStore.put(key, secret)) {
979                         Log.e(TAG, "keystore write failed: key=" + key);
980                     }
981                     l2tpProfile.setSecretString(key);
982                 } else {
983                     mKeyStore.delete(key);
984                 }
985                 break;
986         }
987     }
988
989     private class VpnPreference extends Preference {
990         VpnProfile mProfile;
991         VpnPreference(Context c, VpnProfile p) {
992             super(c);
993             setProfile(p);
994         }
995
996         void setProfile(VpnProfile p) {
997             mProfile = p;
998             setTitle(p.getName());
999             setSummary(getProfileSummaryString(p));
1000         }
1001     }
1002
1003     // to receive vpn connectivity events broadcast by VpnService
1004     private class ConnectivityReceiver extends BroadcastReceiver {
1005         @Override
1006         public void onReceive(Context context, Intent intent) {
1007             String profileName = intent.getStringExtra(
1008                     VpnManager.BROADCAST_PROFILE_NAME);
1009             if (profileName == null) return;
1010
1011             VpnState s = (VpnState) intent.getSerializableExtra(
1012                     VpnManager.BROADCAST_CONNECTION_STATE);
1013
1014             if (s == null) {
1015                 Log.e(TAG, "received null connectivity state");
1016                 return;
1017             }
1018
1019             mConnectingErrorCode = intent.getIntExtra(
1020                     VpnManager.BROADCAST_ERROR_CODE, NO_ERROR);
1021
1022             VpnPreference pref = mVpnPreferenceMap.get(profileName);
1023             if (pref != null) {
1024                 Log.d(TAG, "received connectivity: " + profileName
1025                         + ": connected? " + s
1026                         + "   err=" + mConnectingErrorCode);
1027                 changeState(pref.mProfile, s);
1028             } else {
1029                 Log.e(TAG, "received connectivity: " + profileName
1030                         + ": connected? " + s + ", but profile does not exist;"
1031                         + " just ignore it");
1032             }
1033         }
1034     }
1035
1036     // managing status check in a background thread
1037     private class StatusChecker {
1038         synchronized void check(final List<VpnProfile> list) {
1039             final ConditionVariable cv = new ConditionVariable();
1040             cv.close();
1041             mVpnManager.startVpnService();
1042             ServiceConnection c = new ServiceConnection() {
1043                 public synchronized void onServiceConnected(
1044                         ComponentName className, IBinder binder) {
1045                     cv.open();
1046
1047                     IVpnService service = IVpnService.Stub.asInterface(binder);
1048                     for (VpnProfile p : list) {
1049                         try {
1050                             service.checkStatus(p);
1051                         } catch (Throwable e) {
1052                             Log.e(TAG, " --- checkStatus(): " + p.getName(), e);
1053                             changeState(p, VpnState.IDLE);
1054                         }
1055                     }
1056                     getActivity().unbindService(this);
1057                     showPreferences();
1058                 }
1059
1060                 public void onServiceDisconnected(ComponentName className) {
1061                     cv.open();
1062
1063                     setDefaultState(list);
1064                     getActivity().unbindService(this);
1065                     showPreferences();
1066                 }
1067             };
1068             if (mVpnManager.bindVpnService(c)) {
1069                 if (!cv.block(1000)) {
1070                     Log.d(TAG, "checkStatus() bindService failed");
1071                     setDefaultState(list);
1072                 }
1073             } else {
1074                 setDefaultState(list);
1075             }
1076         }
1077
1078         private void showPreferences() {
1079             for (VpnProfile p : mVpnProfileList) {
1080                 VpnPreference pref = mVpnPreferenceMap.get(p.getName());
1081                 mVpnListContainer.addPreference(pref);
1082             }
1083         }
1084
1085         private void setDefaultState(List<VpnProfile> list) {
1086             for (VpnProfile p : list) changeState(p, VpnState.IDLE);
1087             showPreferences();
1088         }
1089     }
1090 }